diff --git a/.gitignore b/.gitignore index 17271201..0a14ca5b 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ qcapi claude.json bard.json /*yaml +!.pre-commit-config.yaml !components.yaml !/docker-compose.yaml data/labels/instance_id.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..6efb3e3e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,9 @@ +repos: +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.11.7 + hooks: + # Run the linter. + - id: ruff + # Run the formatter. + - id: ruff-format \ No newline at end of file diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 00000000..992262ff --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,39 @@ +[lint] + +ignore = [ + "E712", # Comparison to true should be 'if cond is true:' or 'if cond:' (E712) + "F402", # Import `loader` from line 8 shadowed by loop variable + "F403", # * used, unable to detect undefined names + "F405", # may be undefined, or defined from star imports + "E741", # Ambiguous variable name: `l` + "E722", # bare-except + "E721", # type-comparison + "F821", # undefined-all + "FURB113", # repeated-append + "FURB152", # math-constant + "UP007", # non-pep604-annotation + "UP032", # f-string + "UP045", # non-pep604-annotation-optional + "B005", # strip-with-multi-characters + "B006", # mutable-argument-default + "B007", # unused-loop-control-variable + "B026", # star-arg-unpacking-after-keyword-arg + "B903", # class-as-data-structure + "B904", # raise-without-from-inside-except + "B905", # zip-without-explicit-strict + "N806", # non-lowercase-variable-in-function + "N815", # mixed-case-variable-in-class-scope + "PT011", # pytest-raises-too-broad + "SIM102", # collapsible-if + "SIM103", # needless-bool + "SIM105", # suppressible-exception + "SIM107", # return-in-try-except-finally + "SIM108", # if-else-block-instead-of-if-exp + "SIM113", # enumerate-for-loop + "SIM117", # multiple-with-statements + "SIM210", # if-expr-with-true-false +] + +[format] +# 5. Use single quotes in `ruff format`. +quote-style = "single" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 428ad14d..5b6d4bff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ WORKDIR /app COPY . . -COPY --from=node /app/web/dist ./web/dist +COPY --from=node /app/web/out ./web/out RUN apt update \ && apt install gcc -y \ diff --git a/README.md b/README.md index 0b7df23d..d0e9696c 100644 --- a/README.md +++ b/README.md @@ -139,10 +139,3 @@ - -以及 LangBot 核心团队成员: - -- [RockChinQ](https://github.com/RockChinQ) -- [the-lazy-me](https://github.com/the-lazy-me) -- [wangcham](https://github.com/wangcham) -- [KaedeSAMA](https://github.com/KaedeSAMA) \ No newline at end of file diff --git a/README_EN.md b/README_EN.md index 47df5ec1..a5755f83 100644 --- a/README_EN.md +++ b/README_EN.md @@ -122,10 +122,3 @@ Thank you for the following [code contributors](https://github.com/RockChinQ/Lan - -And the core team members of LangBot: - -- [RockChinQ](https://github.com/RockChinQ) -- [the-lazy-me](https://github.com/the-lazy-me) -- [wangcham](https://github.com/wangcham) -- [KaedeSAMA](https://github.com/KaedeSAMA) diff --git a/README_JP.md b/README_JP.md index db4b0cf7..6ddb6655 100644 --- a/README_JP.md +++ b/README_JP.md @@ -121,10 +121,3 @@ LangBot への貢献に対して、以下の [コード貢献者](https://github - -LangBot の核心チームメンバー: - -- [RockChinQ](https://github.com/RockChinQ) -- [the-lazy-me](https://github.com/the-lazy-me) -- [wangcham](https://github.com/wangcham) -- [KaedeSAMA](https://github.com/KaedeSAMA) diff --git a/libs/dify_service_api/__init__.py b/libs/dify_service_api/__init__.py index 5f178abb..bd6f6d4f 100644 --- a/libs/dify_service_api/__init__.py +++ b/libs/dify_service_api/__init__.py @@ -1,2 +1,4 @@ -from .v1 import client -from .v1 import errors \ No newline at end of file +from .v1 import client as client +from .v1 import errors as errors + +__all__ = ['client', 'errors'] diff --git a/libs/dify_service_api/test.py b/libs/dify_service_api/test.py index faf7571a..b7e2281c 100644 --- a/libs/dify_service_api/test.py +++ b/libs/dify_service_api/test.py @@ -8,25 +8,33 @@ import json class TestDifyClient: async def test_chat_messages(self): - cln = client.AsyncDifyServiceClient(api_key=os.getenv("DIFY_API_KEY"), base_url=os.getenv("DIFY_BASE_URL")) + cln = client.AsyncDifyServiceClient( + api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL') + ) - async for chunk in cln.chat_messages(inputs={}, query="调用工具查看现在几点?", user="test"): + async for chunk in cln.chat_messages( + inputs={}, query='调用工具查看现在几点?', user='test' + ): print(json.dumps(chunk, ensure_ascii=False, indent=4)) async def test_upload_file(self): - cln = client.AsyncDifyServiceClient(api_key=os.getenv("DIFY_API_KEY"), base_url=os.getenv("DIFY_BASE_URL")) + cln = client.AsyncDifyServiceClient( + api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL') + ) - file_bytes = open("img.png", "rb").read() + file_bytes = open('img.png', 'rb').read() print(type(file_bytes)) - file = ("img2.png", file_bytes, "image/png") + file = ('img2.png', file_bytes, 'image/png') - resp = await cln.upload_file(file=file, user="test") + resp = await cln.upload_file(file=file, user='test') print(json.dumps(resp, ensure_ascii=False, indent=4)) async def test_workflow_run(self): - cln = client.AsyncDifyServiceClient(api_key=os.getenv("DIFY_API_KEY"), base_url=os.getenv("DIFY_BASE_URL")) + cln = client.AsyncDifyServiceClient( + api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL') + ) # resp = await cln.workflow_run(inputs={}, user="test") # # print(json.dumps(resp, ensure_ascii=False, indent=4)) @@ -34,11 +42,12 @@ class TestDifyClient: chunks = [] ignored_events = ['text_chunk'] - async for chunk in cln.workflow_run(inputs={}, user="test"): + async for chunk in cln.workflow_run(inputs={}, user='test'): if chunk['event'] in ignored_events: continue chunks.append(chunk) print(json.dumps(chunks, ensure_ascii=False, indent=4)) -if __name__ == "__main__": + +if __name__ == '__main__': asyncio.run(TestDifyClient().test_chat_messages()) diff --git a/libs/dify_service_api/v1/client.py b/libs/dify_service_api/v1/client.py index 70a804b7..35defe2c 100644 --- a/libs/dify_service_api/v1/client.py +++ b/libs/dify_service_api/v1/client.py @@ -12,11 +12,11 @@ class AsyncDifyServiceClient: api_key: str base_url: str - + def __init__( self, api_key: str, - base_url: str = "https://api.dify.ai/v1", + base_url: str = 'https://api.dify.ai/v1', ) -> None: self.api_key = api_key self.base_url = base_url @@ -26,76 +26,81 @@ class AsyncDifyServiceClient: inputs: dict[str, typing.Any], query: str, user: str, - response_mode: str = "streaming", # 当前不支持 blocking - conversation_id: str = "", + response_mode: str = 'streaming', # 当前不支持 blocking + conversation_id: str = '', files: list[dict[str, typing.Any]] = [], timeout: float = 30.0, ) -> typing.AsyncGenerator[dict[str, typing.Any], None]: """发送消息""" - if response_mode != "streaming": - raise DifyAPIError("当前仅支持 streaming 模式") - + if response_mode != 'streaming': + raise DifyAPIError('当前仅支持 streaming 模式') + async with httpx.AsyncClient( base_url=self.base_url, trust_env=True, timeout=timeout, ) as client: async with client.stream( - "POST", - "/chat-messages", - headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}, + 'POST', + '/chat-messages', + headers={ + 'Authorization': f'Bearer {self.api_key}', + 'Content-Type': 'application/json', + }, json={ - "inputs": inputs, - "query": query, - "user": user, - "response_mode": response_mode, - "conversation_id": conversation_id, - "files": files, + 'inputs': inputs, + 'query': query, + 'user': user, + 'response_mode': response_mode, + 'conversation_id': conversation_id, + 'files': files, }, ) as r: async for chunk in r.aiter_lines(): if r.status_code != 200: - raise DifyAPIError(f"{r.status_code} {chunk}") - if chunk.strip() == "": + raise DifyAPIError(f'{r.status_code} {chunk}') + if chunk.strip() == '': continue - if chunk.startswith("data:"): + if chunk.startswith('data:'): yield json.loads(chunk[5:]) - + async def workflow_run( self, inputs: dict[str, typing.Any], user: str, - response_mode: str = "streaming", # 当前不支持 blocking + response_mode: str = 'streaming', # 当前不支持 blocking files: list[dict[str, typing.Any]] = [], timeout: float = 30.0, ) -> typing.AsyncGenerator[dict[str, typing.Any], None]: """运行工作流""" - if response_mode != "streaming": - raise DifyAPIError("当前仅支持 streaming 模式") - + if response_mode != 'streaming': + raise DifyAPIError('当前仅支持 streaming 模式') + async with httpx.AsyncClient( base_url=self.base_url, trust_env=True, timeout=timeout, ) as client: - async with client.stream( - "POST", - "/workflows/run", - headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}, + 'POST', + '/workflows/run', + headers={ + 'Authorization': f'Bearer {self.api_key}', + 'Content-Type': 'application/json', + }, json={ - "inputs": inputs, - "user": user, - "response_mode": response_mode, - "files": files, + 'inputs': inputs, + 'user': user, + 'response_mode': response_mode, + 'files': files, }, ) as r: async for chunk in r.aiter_lines(): if r.status_code != 200: - raise DifyAPIError(f"{r.status_code} {chunk}") - if chunk.strip() == "": + raise DifyAPIError(f'{r.status_code} {chunk}') + if chunk.strip() == '': continue - if chunk.startswith("data:"): + if chunk.startswith('data:'): yield json.loads(chunk[5:]) async def upload_file( @@ -112,15 +117,15 @@ class AsyncDifyServiceClient: ) as client: # multipart/form-data response = await client.post( - "/files/upload", - headers={"Authorization": f"Bearer {self.api_key}"}, + '/files/upload', + headers={'Authorization': f'Bearer {self.api_key}'}, files={ - "file": file, - "user": (None, user), + 'file': file, + 'user': (None, user), }, ) if response.status_code != 201: - raise DifyAPIError(f"{response.status_code} {response.text}") + raise DifyAPIError(f'{response.status_code} {response.text}') return response.json() diff --git a/libs/dify_service_api/v1/client_test.py b/libs/dify_service_api/v1/client_test.py index 58ef53b4..2695b2ea 100644 --- a/libs/dify_service_api/v1/client_test.py +++ b/libs/dify_service_api/v1/client_test.py @@ -7,11 +7,11 @@ import os class TestDifyClient: async def test_chat_messages(self): - cln = client.DifyClient(api_key=os.getenv("DIFY_API_KEY")) + cln = client.DifyClient(api_key=os.getenv('DIFY_API_KEY')) - resp = await cln.chat_messages(inputs={}, query="Who are you?", user_id="test") + resp = await cln.chat_messages(inputs={}, query='Who are you?', user_id='test') print(resp) -if __name__ == "__main__": +if __name__ == '__main__': asyncio.run(TestDifyClient().test_chat_messages()) diff --git a/libs/dingtalk_api/EchoHandler.py b/libs/dingtalk_api/EchoHandler.py index 4cf0f563..793c3d6d 100644 --- a/libs/dingtalk_api/EchoHandler.py +++ b/libs/dingtalk_api/EchoHandler.py @@ -1,8 +1,8 @@ import asyncio -import json import dingtalk_stream from dingtalk_stream import AckMessage + class EchoTextHandler(dingtalk_stream.ChatbotHandler): def __init__(self, client): self.msg_id = '' @@ -10,6 +10,7 @@ class EchoTextHandler(dingtalk_stream.ChatbotHandler): self.client = client # 用于更新 DingTalkClient 中的 incoming_message """处理钉钉消息""" + async def process(self, callback: dingtalk_stream.CallbackMessage): incoming_message = dingtalk_stream.ChatbotMessage.from_dict(callback.data) if incoming_message.message_id != self.msg_id: @@ -26,6 +27,8 @@ class EchoTextHandler(dingtalk_stream.ChatbotHandler): return self.incoming_message + async def get_dingtalk_client(client_id, client_secret): from api import DingTalkClient # 延迟导入,避免循环导入 + return DingTalkClient(client_id, client_secret) diff --git a/libs/dingtalk_api/api.py b/libs/dingtalk_api/api.py index fa4d0421..b908fd4f 100644 --- a/libs/dingtalk_api/api.py +++ b/libs/dingtalk_api/api.py @@ -10,7 +10,9 @@ import traceback class DingTalkClient: - def __init__(self, client_id: str, client_secret: str,robot_name:str,robot_code:str): + def __init__( + self, client_id: str, client_secret: str, robot_name: str, robot_code: str + ): """初始化 WebSocket 连接并自动启动""" self.credential = dingtalk_stream.Credential(client_id, client_secret) self.client = dingtalk_stream.DingTalkStreamClient(self.credential) @@ -18,106 +20,91 @@ class DingTalkClient: self.secret = client_secret # 在 DingTalkClient 中传入自己作为参数,避免循环导入 self.EchoTextHandler = EchoTextHandler(self) - self.client.register_callback_handler(dingtalk_stream.chatbot.ChatbotMessage.TOPIC, self.EchoTextHandler) + self.client.register_callback_handler( + dingtalk_stream.chatbot.ChatbotMessage.TOPIC, self.EchoTextHandler + ) self._message_handlers = { - "example":[], + 'example': [], } self.access_token = '' self.robot_name = robot_name self.robot_code = robot_code self.access_token_expiry_time = '' - - async def get_access_token(self): - url = "https://api.dingtalk.com/v1.0/oauth2/accessToken" - headers = { - "Content-Type": "application/json" - } - data = { - "appKey": self.key, - "appSecret": self.secret - } + url = 'https://api.dingtalk.com/v1.0/oauth2/accessToken' + headers = {'Content-Type': 'application/json'} + data = {'appKey': self.key, 'appSecret': self.secret} async with httpx.AsyncClient() as client: try: - response = await client.post(url,json=data,headers=headers) + response = await client.post(url, json=data, headers=headers) if response.status_code == 200: response_data = response.json() - self.access_token = response_data.get("accessToken") - expires_in = int(response_data.get("expireIn",7200)) + self.access_token = response_data.get('accessToken') + expires_in = int(response_data.get('expireIn', 7200)) self.access_token_expiry_time = time.time() + expires_in - 60 except Exception as e: raise Exception(e) - async def is_token_expired(self): """检查token是否过期""" if self.access_token_expiry_time is None: return True return time.time() > self.access_token_expiry_time - + async def check_access_token(self): if not self.access_token or await self.is_token_expired(): return False return bool(self.access_token and self.access_token.strip()) - async def download_image(self,download_code:str): + async def download_image(self, download_code: str): if not await self.check_access_token(): await self.get_access_token() url = 'https://api.dingtalk.com/v1.0/robot/messageFiles/download' - params = { - "downloadCode":download_code, - "robotCode":self.robot_code - } - headers ={ - "x-acs-dingtalk-access-token": self.access_token - } + params = {'downloadCode': download_code, 'robotCode': self.robot_code} + headers = {'x-acs-dingtalk-access-token': self.access_token} async with httpx.AsyncClient() as client: response = await client.post(url, headers=headers, json=params) if response.status_code == 200: result = response.json() - download_url = result.get("downloadUrl") + download_url = result.get('downloadUrl') else: - raise Exception(f"Error: {response.status_code}, {response.text}") + raise Exception(f'Error: {response.status_code}, {response.text}') if download_url: return await self.download_url_to_base64(download_url) - async def download_url_to_base64(self,download_url): + async def download_url_to_base64(self, download_url): async with httpx.AsyncClient() as client: response = await client.get(download_url) - + if response.status_code == 200: - file_bytes = response.content - base64_str = base64.b64encode(file_bytes).decode('utf-8') # 返回字符串格式 + base64_str = base64.b64encode(file_bytes).decode( + 'utf-8' + ) # 返回字符串格式 return base64_str else: - raise Exception("获取文件失败") - - async def get_audio_url(self,download_code:str): + raise Exception('获取文件失败') + + async def get_audio_url(self, download_code: str): if not await self.check_access_token(): await self.get_access_token() url = 'https://api.dingtalk.com/v1.0/robot/messageFiles/download' - params = { - "downloadCode":download_code, - "robotCode":self.robot_code - } - headers ={ - "x-acs-dingtalk-access-token": self.access_token - } + params = {'downloadCode': download_code, 'robotCode': self.robot_code} + headers = {'x-acs-dingtalk-access-token': self.access_token} async with httpx.AsyncClient() as client: response = await client.post(url, headers=headers, json=params) if response.status_code == 200: result = response.json() - download_url = result.get("downloadUrl") + download_url = result.get('downloadUrl') if download_url: return await self.download_url_to_base64(download_url) else: - raise Exception("获取音频失败") + raise Exception('获取音频失败') else: - raise Exception(f"Error: {response.status_code}, {response.text}") - + raise Exception(f'Error: {response.status_code}, {response.text}') + async def update_incoming_message(self, message): """异步更新 DingTalkClient 中的 incoming_message""" message_data = await self.get_message(message) @@ -125,24 +112,21 @@ class DingTalkClient: event = DingTalkEvent.from_payload(message_data) if event: await self._handle_message(event) - - async def send_message(self,content:str,incoming_message): - self.EchoTextHandler.reply_text(content,incoming_message) - + async def send_message(self, content: str, incoming_message): + self.EchoTextHandler.reply_text(content, incoming_message) async def get_incoming_message(self): """获取收到的消息""" return await self.EchoTextHandler.get_incoming_message() - - def on_message(self, msg_type: str): def decorator(func: Callable[[DingTalkEvent], None]): if msg_type not in self._message_handlers: self._message_handlers[msg_type] = [] self._message_handlers[msg_type].append(func) return func + return decorator async def _handle_message(self, event: DingTalkEvent): @@ -154,40 +138,44 @@ class DingTalkClient: for handler in self._message_handlers[msg_type]: await handler(event) - - async def get_message(self,incoming_message:dingtalk_stream.chatbot.ChatbotMessage): + async def get_message( + self, incoming_message: dingtalk_stream.chatbot.ChatbotMessage + ): try: - # print(json.dumps(incoming_message.to_dict(), indent=4, ensure_ascii=False)) message_data = { - "IncomingMessage":incoming_message, + 'IncomingMessage': incoming_message, } if str(incoming_message.conversation_type) == '1': - message_data["conversation_type"] = 'FriendMessage' + message_data['conversation_type'] = 'FriendMessage' elif str(incoming_message.conversation_type) == '2': - message_data["conversation_type"] = 'GroupMessage' + message_data['conversation_type'] = 'GroupMessage' - if incoming_message.message_type == 'richText': - data = incoming_message.rich_text_content.to_dict() for item in data['richText']: if 'text' in item: - message_data["Content"] = item['text'] + message_data['Content'] = item['text'] if incoming_message.get_image_list()[0]: - message_data["Picture"] = await self.download_image(incoming_message.get_image_list()[0]) - message_data["Type"] = 'text' - + message_data['Picture'] = await self.download_image( + incoming_message.get_image_list()[0] + ) + message_data['Type'] = 'text' + elif incoming_message.message_type == 'text': message_data['Content'] = incoming_message.get_text_list()[0] - message_data["Type"] = 'text' + message_data['Type'] = 'text' elif incoming_message.message_type == 'picture': - message_data['Picture'] = await self.download_image(incoming_message.get_image_list()[0]) - + message_data['Picture'] = await self.download_image( + incoming_message.get_image_list()[0] + ) + message_data['Type'] = 'image' elif incoming_message.message_type == 'audio': - message_data['Audio'] = await self.get_audio_url(incoming_message.to_dict()['content']['downloadCode']) + message_data['Audio'] = await self.get_audio_url( + incoming_message.to_dict()['content']['downloadCode'] + ) message_data['Type'] = 'audio' @@ -196,56 +184,55 @@ class DingTalkClient: # print("message_data:", json.dumps(copy_message_data, indent=4, ensure_ascii=False)) except Exception: traceback.print_exc() - + return message_data - async def send_proactive_message_to_one(self,target_id:str,content:str): + async def send_proactive_message_to_one(self, target_id: str, content: str): if not await self.check_access_token(): await self.get_access_token() url = 'https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend' - headers ={ - "x-acs-dingtalk-access-token":self.access_token, - "Content-Type":"application/json", + headers = { + 'x-acs-dingtalk-access-token': self.access_token, + 'Content-Type': 'application/json', } - data ={ - "robotCode":self.robot_code, - "userIds":[target_id], - "msgKey": "sampleText", - "msgParam": json.dumps({"content":content}), + data = { + 'robotCode': self.robot_code, + 'userIds': [target_id], + 'msgKey': 'sampleText', + 'msgParam': json.dumps({'content': content}), } try: async with httpx.AsyncClient() as client: - response = await client.post(url,headers=headers,json=data) + await client.post(url, headers=headers, json=data) except Exception: traceback.print_exc() - - async def send_proactive_message_to_group(self,target_id:str,content:str): + async def send_proactive_message_to_group(self, target_id: str, content: str): if not await self.check_access_token(): await self.get_access_token() url = 'https://api.dingtalk.com/v1.0/robot/groupMessages/send' - headers ={ - "x-acs-dingtalk-access-token":self.access_token, - "Content-Type":"application/json", + headers = { + 'x-acs-dingtalk-access-token': self.access_token, + 'Content-Type': 'application/json', } - data ={ - "robotCode":self.robot_code, - "openConversationId":target_id, - "msgKey": "sampleText", - "msgParam": json.dumps({"content":content}), + data = { + 'robotCode': self.robot_code, + 'openConversationId': target_id, + 'msgKey': 'sampleText', + 'msgParam': json.dumps({'content': content}), } try: async with httpx.AsyncClient() as client: - response = await client.post(url,headers=headers,json=data) + await client.post(url, headers=headers, json=data) except Exception: traceback.print_exc() - + async def start(self): """启动 WebSocket 连接,监听消息""" - await self.client.start() + await self.client.start() diff --git a/libs/dingtalk_api/dingtalkevent.py b/libs/dingtalk_api/dingtalkevent.py index 4feca010..df968e74 100644 --- a/libs/dingtalk_api/dingtalkevent.py +++ b/libs/dingtalk_api/dingtalkevent.py @@ -1,41 +1,39 @@ from typing import Dict, Any, Optional import dingtalk_stream + class DingTalkEvent(dict): @staticmethod - def from_payload(payload: Dict[str, Any]) -> Optional["DingTalkEvent"]: + def from_payload(payload: Dict[str, Any]) -> Optional['DingTalkEvent']: try: event = DingTalkEvent(payload) return event except KeyError: return None - - - @property - def content(self): - return self.get("Content","") @property - def incoming_message(self) -> Optional["dingtalk_stream.chatbot.ChatbotMessage"]: - return self.get("IncomingMessage") + def content(self): + return self.get('Content', '') + + @property + def incoming_message(self) -> Optional['dingtalk_stream.chatbot.ChatbotMessage']: + return self.get('IncomingMessage') @property def type(self): - return self.get("Type","") - + return self.get('Type', '') + @property def picture(self): - return self.get("Picture","") - + return self.get('Picture', '') + @property def audio(self): - return self.get("Audio","") + return self.get('Audio', '') @property def conversation(self): - return self.get("conversation_type","") - - + return self.get('conversation_type', '') def __getattr__(self, key: str) -> Optional[Any]: """ @@ -66,4 +64,4 @@ class DingTalkEvent(dict): Returns: str: 字符串表示。 """ - return f"" + return f'' diff --git a/libs/official_account_api/api.py b/libs/official_account_api/api.py index a8d318dc..fc392c30 100644 --- a/libs/official_account_api/api.py +++ b/libs/official_account_api/api.py @@ -1,20 +1,14 @@ # 微信公众号的加解密算法与企业微信一样,所以直接使用企业微信的加解密算法文件 -from collections import deque import time import traceback from ..wecom_api.WXBizMsgCrypt3 import WXBizMsgCrypt import xml.etree.ElementTree as ET -from quart import Quart,request +from quart import Quart, request import hashlib -from typing import Callable, Dict, Any +from typing import Callable from .oaevent import OAEvent -import httpx import asyncio -import time -import xml.etree.ElementTree as ET -from pkg.platform.sources import officialaccount as oa - xml_template = """ @@ -28,9 +22,8 @@ xml_template = """ """ -class OAClient(): - - def __init__(self,token:str,EncodingAESKey:str,AppID:str,Appsecret:str): +class OAClient: + def __init__(self, token: str, EncodingAESKey: str, AppID: str, Appsecret: str): self.token = token self.aes = EncodingAESKey self.appid = AppID @@ -38,121 +31,130 @@ class OAClient(): self.base_url = 'https://api.weixin.qq.com' self.access_token = '' self.app = Quart(__name__) - self.app.add_url_rule('/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']) + self.app.add_url_rule( + '/callback/command', + 'handle_callback', + self.handle_callback_request, + methods=['GET', 'POST'], + ) self._message_handlers = { - "example":[], + 'example': [], } self.access_token_expiry_time = None self.msg_id_map = {} self.generated_content = {} async def handle_callback_request(self): - try: # 每隔100毫秒查询是否生成ai回答 start_time = time.time() - signature = request.args.get("signature", "") - timestamp = request.args.get("timestamp", "") - nonce = request.args.get("nonce", "") - echostr = request.args.get("echostr", "") - msg_signature = request.args.get("msg_signature","") + signature = request.args.get('signature', '') + timestamp = request.args.get('timestamp', '') + nonce = request.args.get('nonce', '') + echostr = request.args.get('echostr', '') + msg_signature = request.args.get('msg_signature', '') if msg_signature is None: - raise Exception("msg_signature不在请求体中") + raise Exception('msg_signature不在请求体中') if request.method == 'GET': # 校验签名 - check_str = "".join(sorted([self.token, timestamp, nonce])) - check_signature = hashlib.sha1(check_str.encode("utf-8")).hexdigest() - + check_str = ''.join(sorted([self.token, timestamp, nonce])) + check_signature = hashlib.sha1(check_str.encode('utf-8')).hexdigest() + if check_signature == signature: return echostr # 验证成功返回echostr else: - raise Exception("拒绝请求") - elif request.method == "POST": + raise Exception('拒绝请求') + elif request.method == 'POST': encryt_msg = await request.data - wxcpt = WXBizMsgCrypt(self.token,self.aes,self.appid) - ret,xml_msg = wxcpt.DecryptMsg(encryt_msg,msg_signature,timestamp,nonce) + wxcpt = WXBizMsgCrypt(self.token, self.aes, self.appid) + ret, xml_msg = wxcpt.DecryptMsg( + encryt_msg, msg_signature, timestamp, nonce + ) xml_msg = xml_msg.decode('utf-8') if ret != 0: - raise Exception("消息解密失败") + raise Exception('消息解密失败') message_data = await self.get_message(xml_msg) - if message_data : + if message_data: event = OAEvent.from_payload(message_data) if event: await self._handle_message(event) root = ET.fromstring(xml_msg) - from_user = root.find("FromUserName").text # 发送者 - to_user = root.find("ToUserName").text # 机器人 - + from_user = root.find('FromUserName').text # 发送者 + to_user = root.find('ToUserName').text # 机器人 + timeout = 4.80 interval = 0.1 while True: - content = self.generated_content.pop(message_data["MsgId"], None) + content = self.generated_content.pop(message_data['MsgId'], None) if content: response_xml = xml_template.format( to_user=from_user, from_user=to_user, create_time=int(time.time()), - content = content + content=content, ) return response_xml - + if time.time() - start_time >= timeout: break - + await asyncio.sleep(interval) - if self.msg_id_map.get(message_data["MsgId"], 1) == 3: - + if self.msg_id_map.get(message_data['MsgId'], 1) == 3: # response_xml = xml_template.format( # to_user=from_user, # from_user=to_user, # create_time=int(time.time()), # content = "请求失效:暂不支持公众号超过15秒的请求,如有需求,请联系 LangBot 团队。" # ) - print("请求失效:暂不支持公众号超过15秒的请求,如有需求,请联系 LangBot 团队。") + print( + '请求失效:暂不支持公众号超过15秒的请求,如有需求,请联系 LangBot 团队。' + ) return '' - except Exception as e: + except Exception: traceback.print_exc() - async def get_message(self, xml_msg: str): - root = ET.fromstring(xml_msg) message_data = { - "ToUserName": root.find("ToUserName").text, - "FromUserName": root.find("FromUserName").text, - "CreateTime": int(root.find("CreateTime").text), - "MsgType": root.find("MsgType").text, - "Content": root.find("Content").text if root.find("Content") is not None else None, - "MsgId": int(root.find("MsgId").text) if root.find("MsgId") is not None else None, + 'ToUserName': root.find('ToUserName').text, + 'FromUserName': root.find('FromUserName').text, + 'CreateTime': int(root.find('CreateTime').text), + 'MsgType': root.find('MsgType').text, + 'Content': root.find('Content').text + if root.find('Content') is not None + else None, + 'MsgId': int(root.find('MsgId').text) + if root.find('MsgId') is not None + else None, } return message_data - async def run_task(self, host: str, port: int, *args, **kwargs): """ 启动 Quart 应用。 """ await self.app.run_task(host=host, port=port, *args, **kwargs) - def on_message(self, msg_type: str): """ 注册消息类型处理器。 """ + def decorator(func: Callable[[OAEvent], None]): if msg_type not in self._message_handlers: self._message_handlers[msg_type] = [] self._message_handlers[msg_type].append(func) return func + return decorator async def _handle_message(self, event: OAEvent): @@ -170,14 +172,19 @@ class OAClient(): for handler in self._message_handlers[msg_type]: await handler(event) - async def set_message(self,msg_id:int,content:str): + async def set_message(self, msg_id: int, content: str): self.generated_content[msg_id] = content - -class OAClientForLongerResponse(): - - def __init__(self,token:str,EncodingAESKey:str,AppID:str,Appsecret:str,LoadingMessage:str): +class OAClientForLongerResponse: + def __init__( + self, + token: str, + EncodingAESKey: str, + AppID: str, + Appsecret: str, + LoadingMessage: str, + ): self.token = token self.aes = EncodingAESKey self.appid = AppID @@ -185,9 +192,14 @@ class OAClientForLongerResponse(): self.base_url = 'https://api.weixin.qq.com' self.access_token = '' self.app = Quart(__name__) - self.app.add_url_rule('/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']) + self.app.add_url_rule( + '/callback/command', + 'handle_callback', + self.handle_callback_request, + methods=['GET', 'POST'], + ) self._message_handlers = { - "example":[], + 'example': [], } self.access_token_expiry_time = None self.loading_message = LoadingMessage @@ -196,50 +208,55 @@ class OAClientForLongerResponse(): async def handle_callback_request(self): try: - start_time = time.time() - signature = request.args.get("signature", "") - timestamp = request.args.get("timestamp", "") - nonce = request.args.get("nonce", "") - echostr = request.args.get("echostr", "") - msg_signature = request.args.get("msg_signature", "") + signature = request.args.get('signature', '') + timestamp = request.args.get('timestamp', '') + nonce = request.args.get('nonce', '') + echostr = request.args.get('echostr', '') + msg_signature = request.args.get('msg_signature', '') if msg_signature is None: - raise Exception("msg_signature不在请求体中") + raise Exception('msg_signature不在请求体中') if request.method == 'GET': - check_str = "".join(sorted([self.token, timestamp, nonce])) - check_signature = hashlib.sha1(check_str.encode("utf-8")).hexdigest() - return echostr if check_signature == signature else "拒绝请求" + check_str = ''.join(sorted([self.token, timestamp, nonce])) + check_signature = hashlib.sha1(check_str.encode('utf-8')).hexdigest() + return echostr if check_signature == signature else '拒绝请求' - elif request.method == "POST": + elif request.method == 'POST': encryt_msg = await request.data wxcpt = WXBizMsgCrypt(self.token, self.aes, self.appid) - ret, xml_msg = wxcpt.DecryptMsg(encryt_msg, msg_signature, timestamp, nonce) + ret, xml_msg = wxcpt.DecryptMsg( + encryt_msg, msg_signature, timestamp, nonce + ) xml_msg = xml_msg.decode('utf-8') if ret != 0: - raise Exception("消息解密失败") + raise Exception('消息解密失败') # 解析 XML root = ET.fromstring(xml_msg) - from_user = root.find("FromUserName").text - to_user = root.find("ToUserName").text - - - if self.msg_queue.get(from_user) and self.msg_queue[from_user][0]["content"]: + from_user = root.find('FromUserName').text + to_user = root.find('ToUserName').text + if ( + self.msg_queue.get(from_user) + and self.msg_queue[from_user][0]['content'] + ): queue_top = self.msg_queue[from_user].pop(0) - queue_content = queue_top["content"] + queue_content = queue_top['content'] # 弹出用户消息 - if self.user_msg_queue.get(from_user) and self.user_msg_queue[from_user]: + if ( + self.user_msg_queue.get(from_user) + and self.user_msg_queue[from_user] + ): self.user_msg_queue[from_user].pop(0) response_xml = xml_template.format( to_user=from_user, from_user=to_user, create_time=int(time.time()), - content=queue_content + content=queue_content, ) return response_xml @@ -248,65 +265,67 @@ class OAClientForLongerResponse(): to_user=from_user, from_user=to_user, create_time=int(time.time()), - content=self.loading_message + content=self.loading_message, ) - - if self.user_msg_queue.get(from_user) and self.user_msg_queue[from_user][0]["content"]: + + if ( + self.user_msg_queue.get(from_user) + and self.user_msg_queue[from_user][0]['content'] + ): return response_xml else: message_data = await self.get_message(xml_msg) - + if message_data: event = OAEvent.from_payload(message_data) if event: - self.user_msg_queue.setdefault(from_user,[]).append( + self.user_msg_queue.setdefault(from_user, []).append( { - "content":event.message, + 'content': event.message, } ) await self._handle_message(event) return response_xml - except Exception as e: + except Exception: traceback.print_exc() - - async def get_message(self, xml_msg: str): - root = ET.fromstring(xml_msg) message_data = { - "ToUserName": root.find("ToUserName").text, - "FromUserName": root.find("FromUserName").text, - "CreateTime": int(root.find("CreateTime").text), - "MsgType": root.find("MsgType").text, - "Content": root.find("Content").text if root.find("Content") is not None else None, - "MsgId": int(root.find("MsgId").text) if root.find("MsgId") is not None else None, + 'ToUserName': root.find('ToUserName').text, + 'FromUserName': root.find('FromUserName').text, + 'CreateTime': int(root.find('CreateTime').text), + 'MsgType': root.find('MsgType').text, + 'Content': root.find('Content').text + if root.find('Content') is not None + else None, + 'MsgId': int(root.find('MsgId').text) + if root.find('MsgId') is not None + else None, } return message_data - - async def run_task(self, host: str, port: int, *args, **kwargs): """ 启动 Quart 应用。 """ await self.app.run_task(host=host, port=port, *args, **kwargs) - - def on_message(self, msg_type: str): """ 注册消息类型处理器。 """ + def decorator(func: Callable[[OAEvent], None]): if msg_type not in self._message_handlers: self._message_handlers[msg_type] = [] self._message_handlers[msg_type].append(func) return func + return decorator async def _handle_message(self, event: OAEvent): @@ -319,22 +338,13 @@ class OAClientForLongerResponse(): for handler in self._message_handlers[msg_type]: await handler(event) - async def set_message(self,from_user:int,message_id:int,content:str): - if from_user not in self.msg_queue: + async def set_message(self, from_user: int, message_id: int, content: str): + if from_user not in self.msg_queue: self.msg_queue[from_user] = [] - + self.msg_queue[from_user].append( { - "msg_id":message_id, - "content":content, + 'msg_id': message_id, + 'content': content, } ) - - - - - - - - - diff --git a/libs/official_account_api/oaevent.py b/libs/official_account_api/oaevent.py index ebbccd7e..d4de3914 100644 --- a/libs/official_account_api/oaevent.py +++ b/libs/official_account_api/oaevent.py @@ -9,7 +9,7 @@ class OAEvent(dict): """ @staticmethod - def from_payload(payload: Dict[str, Any]) -> Optional["OAEvent"]: + def from_payload(payload: Dict[str, Any]) -> Optional['OAEvent']: """ 从微信公众号事件数据构造 `WecomEvent` 对象。 @@ -34,14 +34,14 @@ class OAEvent(dict): Returns: str: 事件类型。 """ - return self.get("MsgType", "") - + return self.get('MsgType', '') + @property def picurl(self) -> str: """ 图片链接 """ - return self.get("PicUrl","") + return self.get('PicUrl', '') @property def detail_type(self) -> str: @@ -53,8 +53,8 @@ class OAEvent(dict): Returns: str: 事件详细类型。 """ - if self.type == "event": - return self.get("Event", "") + if self.type == 'event': + return self.get('Event', '') return self.type @property @@ -65,15 +65,14 @@ class OAEvent(dict): Returns: str: 事件名。 """ - return f"{self.type}.{self.detail_type}" + return f'{self.type}.{self.detail_type}' @property def user_id(self) -> Optional[str]: """ 发送方账号 """ - return self.get("FromUserName") - + return self.get('FromUserName') @property def receiver_id(self) -> Optional[str]: @@ -83,7 +82,7 @@ class OAEvent(dict): Returns: Optional[str]: 接收者 ID。 """ - return self.get("ToUserName") + return self.get('ToUserName') @property def message_id(self) -> Optional[str]: @@ -93,7 +92,7 @@ class OAEvent(dict): Returns: Optional[str]: 消息 ID。 """ - return self.get("MsgId") + return self.get('MsgId') @property def message(self) -> Optional[str]: @@ -103,7 +102,7 @@ class OAEvent(dict): Returns: Optional[str]: 消息内容。 """ - return self.get("Content") + return self.get('Content') @property def media_id(self) -> Optional[str]: @@ -113,7 +112,7 @@ class OAEvent(dict): Returns: Optional[str]: 媒体文件 ID。 """ - return self.get("MediaId") + return self.get('MediaId') @property def timestamp(self) -> Optional[int]: @@ -123,7 +122,7 @@ class OAEvent(dict): Returns: Optional[int]: 时间戳。 """ - return self.get("CreateTime") + return self.get('CreateTime') @property def event_key(self) -> Optional[str]: @@ -133,7 +132,7 @@ class OAEvent(dict): Returns: Optional[str]: 事件 Key。 """ - return self.get("EventKey") + return self.get('EventKey') def __getattr__(self, key: str) -> Optional[Any]: """ @@ -164,4 +163,4 @@ class OAEvent(dict): Returns: str: 字符串表示。 """ - return f"" + return f'' diff --git a/libs/qq_official_api/api.py b/libs/qq_official_api/api.py index 62f252df..89360881 100644 --- a/libs/qq_official_api/api.py +++ b/libs/qq_official_api/api.py @@ -1,24 +1,16 @@ import time from quart import request -import base64 -import binascii import httpx from quart import Quart -import xml.etree.ElementTree as ET from typing import Callable, Dict, Any -from pkg.platform.types import events as platform_events, message as platform_message -import aiofiles +from pkg.platform.types import events as platform_events from .qqofficialevent import QQOfficialEvent import json -import hmac -import base64 -import hashlib import traceback from cryptography.hazmat.primitives.asymmetric import ed25519 -from .qqofficialevent import QQOfficialEvent + def handle_validation(body: dict, bot_secret: str): - # bot正确的secert是32位的,此处仅为了适配演示demo while len(bot_secret) < 32: bot_secret = bot_secret * 2 @@ -36,29 +28,26 @@ def handle_validation(body: dict, bot_secret: str): signature_hex = signature.hex() - response = { - "plain_token": body['d']['plain_token'], - "signature": signature_hex - } + response = {'plain_token': body['d']['plain_token'], 'signature': signature_hex} return response + class QQOfficialClient: def __init__(self, secret: str, token: str, app_id: str): self.app = Quart(__name__) self.app.add_url_rule( - "/callback/command", - "handle_callback", + '/callback/command', + 'handle_callback', self.handle_callback_request, - methods=["GET", "POST"], + methods=['GET', 'POST'], ) self.secret = secret self.token = token self.app_id = app_id - self._message_handlers = { - } - self.base_url = "https://api.sgroup.qq.com" - self.access_token = "" + self._message_handlers = {} + self.base_url = 'https://api.sgroup.qq.com' + self.access_token = '' self.access_token_expiry_time = None async def check_access_token(self): @@ -66,30 +55,29 @@ class QQOfficialClient: if not self.access_token or await self.is_token_expired(): return False return bool(self.access_token and self.access_token.strip()) - + async def get_access_token(self): """获取access_token""" - url = "https://bots.qq.com/app/getAppAccessToken" + url = 'https://bots.qq.com/app/getAppAccessToken' async with httpx.AsyncClient() as client: params = { - "appId":self.app_id, - "clientSecret":self.secret, + 'appId': self.app_id, + 'clientSecret': self.secret, } headers = { - "content-type":"application/json", + 'content-type': 'application/json', } try: - response = await client.post(url,json=params,headers=headers) + response = await client.post(url, json=params, headers=headers) if response.status_code == 200: response_data = response.json() - access_token = response_data.get("access_token") - expires_in = int(response_data.get("expires_in",7200)) + access_token = response_data.get('access_token') + expires_in = int(response_data.get('expires_in', 7200)) self.access_token_expiry_time = time.time() + expires_in - 60 if access_token: self.access_token = access_token except Exception as e: - raise Exception(f"获取access_token失败: {e}") - + raise Exception(f'获取access_token失败: {e}') async def handle_callback_request(self): """处理回调请求""" @@ -98,27 +86,24 @@ class QQOfficialClient: body = await request.get_data() payload = json.loads(body) - # 验证是否为回调验证请求 - if payload.get("op") == 13: + if payload.get('op') == 13: # 生成签名 response = handle_validation(payload, self.secret) return response - if payload.get("op") == 0: - message_data = await self.get_message(payload) - if message_data: - event = QQOfficialEvent.from_payload(message_data) - await self._handle_message(event) - - return {"code": 0, "message": "success"} + if payload.get('op') == 0: + message_data = await self.get_message(payload) + if message_data: + event = QQOfficialEvent.from_payload(message_data) + await self._handle_message(event) + + return {'code': 0, 'message': 'success'} except Exception as e: traceback.print_exc() - return {"error": str(e)}, 400 - - + return {'error': str(e)}, 400 async def run_task(self, host: str, port: int, *args, **kwargs): """启动 Quart 应用""" @@ -135,133 +120,140 @@ class QQOfficialClient: return decorator - async def _handle_message(self, event:QQOfficialEvent): + async def _handle_message(self, event: QQOfficialEvent): """处理消息事件""" msg_type = event.t if msg_type in self._message_handlers: for handler in self._message_handlers[msg_type]: await handler(event) - - async def get_message(self,msg:dict) -> Dict[str,Any]: + async def get_message(self, msg: dict) -> Dict[str, Any]: """获取消息""" message_data = { - "t": msg.get("t",{}), - "user_openid": msg.get("d",{}).get("author",{}).get("user_openid",{}), - "timestamp": msg.get("d",{}).get("timestamp",{}), - "d_author_id": msg.get("d",{}).get("author",{}).get("id",{}), - "content": msg.get("d",{}).get("content",{}), - "d_id": msg.get("d",{}).get("id",{}), - "id": msg.get("id",{}), - "channel_id": msg.get("d",{}).get("channel_id",{}), - "username": msg.get("d",{}).get("author",{}).get("username",{}), - "guild_id": msg.get("d",{}).get("guild_id",{}), - "member_openid": msg.get("d",{}).get("author",{}).get("openid",{}), - "group_openid": msg.get("d",{}).get("group_openid",{}) + 't': msg.get('t', {}), + 'user_openid': msg.get('d', {}).get('author', {}).get('user_openid', {}), + 'timestamp': msg.get('d', {}).get('timestamp', {}), + 'd_author_id': msg.get('d', {}).get('author', {}).get('id', {}), + 'content': msg.get('d', {}).get('content', {}), + 'd_id': msg.get('d', {}).get('id', {}), + 'id': msg.get('id', {}), + 'channel_id': msg.get('d', {}).get('channel_id', {}), + 'username': msg.get('d', {}).get('author', {}).get('username', {}), + 'guild_id': msg.get('d', {}).get('guild_id', {}), + 'member_openid': msg.get('d', {}).get('author', {}).get('openid', {}), + 'group_openid': msg.get('d', {}).get('group_openid', {}), } - attachments = msg.get("d", {}).get("attachments", []) - image_attachments = [attachment['url'] for attachment in attachments if await self.is_image(attachment)] - image_attachments_type = [attachment['content_type'] for attachment in attachments if await self.is_image(attachment)] + attachments = msg.get('d', {}).get('attachments', []) + image_attachments = [ + attachment['url'] + for attachment in attachments + if await self.is_image(attachment) + ] + image_attachments_type = [ + attachment['content_type'] + for attachment in attachments + if await self.is_image(attachment) + ] if image_attachments: - message_data["image_attachments"] = image_attachments[0] - message_data["content_type"] = image_attachments_type[0] + message_data['image_attachments'] = image_attachments[0] + message_data['content_type'] = image_attachments_type[0] else: - - message_data["image_attachments"] = None - - return message_data - + message_data['image_attachments'] = None - async def is_image(self,attachment:dict) -> bool: + return message_data + + async def is_image(self, attachment: dict) -> bool: """判断是否为图片附件""" - content_type = attachment.get("content_type","") - return content_type.startswith("image/") - - - async def send_private_text_msg(self,user_openid:str,content:str,msg_id:str): + content_type = attachment.get('content_type', '') + return content_type.startswith('image/') + + async def send_private_text_msg(self, user_openid: str, content: str, msg_id: str): """发送私聊消息""" if not await self.check_access_token(): - await self.get_access_token() + await self.get_access_token() - url = self.base_url + "/v2/users/" + user_openid + "/messages" + url = self.base_url + '/v2/users/' + user_openid + '/messages' async with httpx.AsyncClient() as client: headers = { - "Authorization": f"QQBot {self.access_token}", - "Content-Type": "application/json", + 'Authorization': f'QQBot {self.access_token}', + 'Content-Type': 'application/json', } data = { - "content": content, - "msg_type": 0, - "msg_id": msg_id, + 'content': content, + 'msg_type': 0, + 'msg_id': msg_id, } - response = await client.post(url,headers=headers,json=data) + response = await client.post(url, headers=headers, json=data) if response.status_code == 200: return else: raise ValueError(response) - - async def send_group_text_msg(self,group_openid:str,content:str,msg_id:str): + async def send_group_text_msg(self, group_openid: str, content: str, msg_id: str): """发送群聊消息""" if not await self.check_access_token(): await self.get_access_token() - url = self.base_url + "/v2/groups/" + group_openid + "/messages" + url = self.base_url + '/v2/groups/' + group_openid + '/messages' async with httpx.AsyncClient() as client: headers = { - "Authorization": f"QQBot {self.access_token}", - "Content-Type": "application/json", + 'Authorization': f'QQBot {self.access_token}', + 'Content-Type': 'application/json', } data = { - "content": content, - "msg_type": 0, - "msg_id": msg_id, + 'content': content, + 'msg_type': 0, + 'msg_id': msg_id, } - response = await client.post(url,headers=headers,json=data) + response = await client.post(url, headers=headers, json=data) if response.status_code == 200: return else: raise Exception(response.read().decode()) - async def send_channle_group_text_msg(self,channel_id:str,content:str,msg_id:str): + async def send_channle_group_text_msg( + self, channel_id: str, content: str, msg_id: str + ): """发送频道群聊消息""" if not await self.check_access_token(): - await self.get_access_token() + await self.get_access_token() - url = self.base_url + "/channels/" + channel_id + "/messages" + url = self.base_url + '/channels/' + channel_id + '/messages' async with httpx.AsyncClient() as client: headers = { - "Authorization": f"QQBot {self.access_token}", - "Content-Type": "application/json", + 'Authorization': f'QQBot {self.access_token}', + 'Content-Type': 'application/json', } params = { - "content": content, - "msg_type": 0, - "msg_id": msg_id, + 'content': content, + 'msg_type': 0, + 'msg_id': msg_id, } - response = await client.post(url,headers=headers,json=params) + response = await client.post(url, headers=headers, json=params) if response.status_code == 200: return True else: raise Exception(response) - async def send_channle_private_text_msg(self,guild_id:str,content:str,msg_id:str): + async def send_channle_private_text_msg( + self, guild_id: str, content: str, msg_id: str + ): """发送频道私聊消息""" if not await self.check_access_token(): - await self.get_access_token() + await self.get_access_token() - url = self.base_url + "/dms/" + guild_id + "/messages" + url = self.base_url + '/dms/' + guild_id + '/messages' async with httpx.AsyncClient() as client: headers = { - "Authorization": f"QQBot {self.access_token}", - "Content-Type": "application/json", + 'Authorization': f'QQBot {self.access_token}', + 'Content-Type': 'application/json', } params = { - "content": content, - "msg_type": 0, - "msg_id": msg_id, + 'content': content, + 'msg_type': 0, + 'msg_id': msg_id, } - response = await client.post(url,headers=headers,json=params) + response = await client.post(url, headers=headers, json=params) if response.status_code == 200: return True else: diff --git a/libs/qq_official_api/qqofficialevent.py b/libs/qq_official_api/qqofficialevent.py index 41e842f1..7c29b9d8 100644 --- a/libs/qq_official_api/qqofficialevent.py +++ b/libs/qq_official_api/qqofficialevent.py @@ -1,114 +1,112 @@ from typing import Dict, Any, Optional + class QQOfficialEvent(dict): @staticmethod - def from_payload(payload: Dict[str, Any]) -> Optional["QQOfficialEvent"]: + def from_payload(payload: Dict[str, Any]) -> Optional['QQOfficialEvent']: try: event = QQOfficialEvent(payload) return event except KeyError: return None - @property def t(self) -> str: """ 事件类型 """ - return self.get("t", "") - + return self.get('t', '') + @property def user_openid(self) -> str: """ 用户openid """ - return self.get("user_openid",{}) - + return self.get('user_openid', {}) + @property def timestamp(self) -> str: """ 时间戳 """ - return self.get("timestamp",{}) - - + return self.get('timestamp', {}) + @property def d_author_id(self) -> str: """ 作者id """ - return self.get("id",{}) - + return self.get('id', {}) + @property def content(self) -> str: """ 内容 """ - return self.get("content",'') - + return self.get('content', '') + @property def d_id(self) -> str: """ d_id """ - return self.get("d_id",{}) - + return self.get('d_id', {}) + @property def id(self) -> str: """ 消息id,msg_id """ - return self.get("id",{}) - + return self.get('id', {}) + @property def channel_id(self) -> str: """ 频道id """ - return self.get("channel_id",{}) - + return self.get('channel_id', {}) + @property def username(self) -> str: """ 用户名 """ - return self.get("username",{}) - + return self.get('username', {}) + @property def guild_id(self) -> str: """ 频道id """ - return self.get("guild_id",{}) - + return self.get('guild_id', {}) + @property def member_openid(self) -> str: """ 成员openid """ - return self.get("openid",{}) - + return self.get('openid', {}) + @property def attachments(self) -> str: """ 附件url """ - url = self.get("image_attachments", "") - if url and not url.startswith("https://"): - url = "https://" + url + url = self.get('image_attachments', '') + if url and not url.startswith('https://'): + url = 'https://' + url return url - + @property def group_openid(self) -> str: """ 群组id """ - return self.get("group_openid",{}) - + return self.get('group_openid', {}) + @property def content_type(self) -> str: """ 文件类型 """ - return self.get("content_type","") - + return self.get('content_type', '') diff --git a/libs/wecom_api/WXBizMsgCrypt3.py b/libs/wecom_api/WXBizMsgCrypt3.py index 0123c7d1..a9a7bc89 100644 --- a/libs/wecom_api/WXBizMsgCrypt3.py +++ b/libs/wecom_api/WXBizMsgCrypt3.py @@ -1,10 +1,11 @@ #!/usr/bin/env python # -*- encoding:utf-8 -*- -""" 对企业微信发送给企业后台的消息加解密示例代码. +"""对企业微信发送给企业后台的消息加解密示例代码. @copyright: Copyright (c) 1998-2014 Tencent Inc. """ + # ------------------------------------------------------------------------ import logging import base64 @@ -49,7 +50,7 @@ class SHA1: sortlist = [token, timestamp, nonce, encrypt] sortlist.sort() sha = hashlib.sha1() - sha.update("".join(sortlist).encode()) + sha.update(''.join(sortlist).encode()) return ierror.WXBizMsgCrypt_OK, sha.hexdigest() except Exception as e: logger = logging.getLogger() @@ -75,7 +76,7 @@ class XMLParse: """ try: xml_tree = ET.fromstring(xmltext) - encrypt = xml_tree.find("Encrypt") + encrypt = xml_tree.find('Encrypt') return ierror.WXBizMsgCrypt_OK, encrypt.text except Exception as e: logger = logging.getLogger() @@ -100,13 +101,13 @@ class XMLParse: return resp_xml -class PKCS7Encoder(): +class PKCS7Encoder: """提供基于PKCS7算法的加解密接口""" block_size = 32 def encode(self, text): - """ 对需要加密的明文进行填充补位 + """对需要加密的明文进行填充补位 @param text: 需要进行填充补位操作的明文 @return: 补齐明文字符串 """ @@ -134,7 +135,6 @@ class Prpcrypt(object): """提供接收和推送给企业微信消息的加解密接口""" def __init__(self, key): - # self.key = base64.b64decode(key+"=") self.key = key # 设置加解密模式为AES的CBC模式 @@ -147,7 +147,12 @@ class Prpcrypt(object): """ # 16位随机字符串添加到明文开头 text = text.encode() - text = self.get_random_str() + struct.pack("I", socket.htonl(len(text))) + text + receiveid.encode() + text = ( + self.get_random_str() + + struct.pack('I', socket.htonl(len(text))) + + text + + receiveid.encode() + ) # 使用自定义的填充方式对明文进行补位填充 pkcs7 = PKCS7Encoder() @@ -183,9 +188,9 @@ class Prpcrypt(object): # plain_text = pkcs7.encode(plain_text) # 去除16位随机字符串 content = plain_text[16:-pad] - xml_len = socket.ntohl(struct.unpack("I", content[: 4])[0]) - xml_content = content[4: xml_len + 4] - from_receiveid = content[xml_len + 4:] + xml_len = socket.ntohl(struct.unpack('I', content[:4])[0]) + xml_content = content[4 : xml_len + 4] + from_receiveid = content[xml_len + 4 :] except Exception as e: logger = logging.getLogger() logger.error(e) @@ -196,7 +201,7 @@ class Prpcrypt(object): return 0, xml_content def get_random_str(self): - """ 随机生成16位字符串 + """随机生成16位字符串 @return: 16位字符串 """ return str(random.randint(1000000000000000, 9999999999999999)).encode() @@ -206,10 +211,10 @@ class WXBizMsgCrypt(object): # 构造函数 def __init__(self, sToken, sEncodingAESKey, sReceiveId): try: - self.key = base64.b64decode(sEncodingAESKey + "=") + self.key = base64.b64decode(sEncodingAESKey + '=') assert len(self.key) == 32 - except: - throw_exception("[error]: EncodingAESKey unvalid !", FormatException) + except Exception: + throw_exception('[error]: EncodingAESKey unvalid !', FormatException) # return ierror.WXBizMsgCrypt_IllegalAesKey,None self.m_sToken = sToken self.m_sReceiveId = sReceiveId diff --git a/libs/wecom_api/api.py b/libs/wecom_api/api.py index 61458f8e..8993885b 100644 --- a/libs/wecom_api/api.py +++ b/libs/wecom_api/api.py @@ -7,15 +7,22 @@ from quart import Quart import xml.etree.ElementTree as ET from typing import Callable, Dict, Any from .wecomevent import WecomEvent -from pkg.platform.types import events as platform_events, message as platform_message +from pkg.platform.types import message as platform_message import aiofiles -class WecomClient(): - def __init__(self,corpid:str,secret:str,token:str,EncodingAESKey:str,contacts_secret:str): +class WecomClient: + def __init__( + self, + corpid: str, + secret: str, + token: str, + EncodingAESKey: str, + contacts_secret: str, + ): self.corpid = corpid self.secret = secret - self.access_token_for_contacts ='' + self.access_token_for_contacts = '' self.token = token self.aes = EncodingAESKey self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin' @@ -23,19 +30,26 @@ class WecomClient(): self.secret_for_contacts = contacts_secret self.app = Quart(__name__) self.wxcpt = WXBizMsgCrypt(self.token, self.aes, self.corpid) - self.app.add_url_rule('/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']) + self.app.add_url_rule( + '/callback/command', + 'handle_callback', + self.handle_callback_request, + methods=['GET', 'POST'], + ) self._message_handlers = { - "example":[], + 'example': [], } - #access——token操作 + # access——token操作 async def check_access_token(self): return bool(self.access_token and self.access_token.strip()) async def check_access_token_for_contacts(self): - return bool(self.access_token_for_contacts and self.access_token_for_contacts.strip()) + return bool( + self.access_token_for_contacts and self.access_token_for_contacts.strip() + ) - async def get_access_token(self,secret): + async def get_access_token(self, secret): url = f'https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={self.corpid}&corpsecret={secret}' async with httpx.AsyncClient() as client: response = await client.get(url) @@ -43,146 +57,163 @@ class WecomClient(): if 'access_token' in data: return data['access_token'] else: - raise Exception(f"未获取access token: {data}") + raise Exception(f'未获取access token: {data}') async def get_users(self): if not self.check_access_token_for_contacts(): - self.access_token_for_contacts = await self.get_access_token(self.secret_for_contacts) + self.access_token_for_contacts = await self.get_access_token( + self.secret_for_contacts + ) - url = self.base_url+'/user/list_id?access_token='+self.access_token_for_contacts + url = ( + self.base_url + + '/user/list_id?access_token=' + + self.access_token_for_contacts + ) async with httpx.AsyncClient() as client: params = { - "cursor":"", - "limit":10000, + 'cursor': '', + 'limit': 10000, } - response = await client.post(url,json=params) + response = await client.post(url, json=params) data = response.json() if data['errcode'] == 0: dept_users = data['dept_user'] userid = [] for user in dept_users: - userid.append(user["userid"]) + userid.append(user['userid']) return userid else: - raise Exception("未获取用户") - - async def send_to_all(self,content:str,agent_id:int): - if not self.check_access_token_for_contacts(): - self.access_token_for_contacts = await self.get_access_token(self.secret_for_contacts) + raise Exception('未获取用户') - url = self.base_url+'/message/send?access_token='+self.access_token_for_contacts + async def send_to_all(self, content: str, agent_id: int): + if not self.check_access_token_for_contacts(): + self.access_token_for_contacts = await self.get_access_token( + self.secret_for_contacts + ) + + url = ( + self.base_url + + '/message/send?access_token=' + + self.access_token_for_contacts + ) user_ids = await self.get_users() - user_ids_string = "|".join(user_ids) + user_ids_string = '|'.join(user_ids) async with httpx.AsyncClient() as client: params = { - "touser" : user_ids_string, - "msgtype" : "text", - "agentid" : agent_id, - "text" : { - "content" : content, - }, - "safe":0, - "enable_id_trans": 0, - "enable_duplicate_check": 0, - "duplicate_check_interval": 1800 + 'touser': user_ids_string, + 'msgtype': 'text', + 'agentid': agent_id, + 'text': { + 'content': content, + }, + 'safe': 0, + 'enable_id_trans': 0, + 'enable_duplicate_check': 0, + 'duplicate_check_interval': 1800, } - response = await client.post(url,json=params) + response = await client.post(url, json=params) data = response.json() if data['errcode'] != 0: - raise Exception("Failed to send message: "+str(data)) + raise Exception('Failed to send message: ' + str(data)) - async def send_image(self,user_id:str,agent_id:int,media_id:str): + async def send_image(self, user_id: str, agent_id: int, media_id: str): if not await self.check_access_token(): self.access_token = await self.get_access_token(self.secret) - url = self.base_url+'/media/upload?access_token='+self.access_token + url = self.base_url + '/media/upload?access_token=' + self.access_token async with httpx.AsyncClient() as client: params = { - "touser" : user_id, - "toparty" : "", - "totag":"", - "agentid" : agent_id, - "msgtype" : "image", - "image" : { - "media_id" : media_id, + 'touser': user_id, + 'toparty': '', + 'totag': '', + 'agentid': agent_id, + 'msgtype': 'image', + 'image': { + 'media_id': media_id, }, - "safe":0, - "enable_id_trans": 0, - "enable_duplicate_check": 0, - "duplicate_check_interval": 1800 + 'safe': 0, + 'enable_id_trans': 0, + 'enable_duplicate_check': 0, + 'duplicate_check_interval': 1800, } try: - response = await client.post(url,json=params) + response = await client.post(url, json=params) data = response.json() except Exception as e: - raise Exception("Failed to send image: "+str(e)) + raise Exception('Failed to send image: ' + str(e)) # 企业微信错误码40014和42001,代表accesstoken问题 if data['errcode'] == 40014 or data['errcode'] == 42001: self.access_token = await self.get_access_token(self.secret) - return await self.send_image(user_id,agent_id,media_id) + return await self.send_image(user_id, agent_id, media_id) if data['errcode'] != 0: - raise Exception("Failed to send image: "+str(data)) - - async def send_private_msg(self,user_id:str, agent_id:int,content:str): + raise Exception('Failed to send image: ' + str(data)) + + async def send_private_msg(self, user_id: str, agent_id: int, content: str): if not await self.check_access_token(): self.access_token = await self.get_access_token(self.secret) - url = self.base_url+'/message/send?access_token='+self.access_token + url = self.base_url + '/message/send?access_token=' + self.access_token async with httpx.AsyncClient() as client: - params={ - "touser" : user_id, - "msgtype" : "text", - "agentid" : agent_id, - "text" : { - "content" : content, + params = { + 'touser': user_id, + 'msgtype': 'text', + 'agentid': agent_id, + 'text': { + 'content': content, }, - "safe":0, - "enable_id_trans": 0, - "enable_duplicate_check": 0, - "duplicate_check_interval": 1800 + 'safe': 0, + 'enable_id_trans': 0, + 'enable_duplicate_check': 0, + 'duplicate_check_interval': 1800, } - response = await client.post(url,json=params) + response = await client.post(url, json=params) data = response.json() if data['errcode'] == 40014 or data['errcode'] == 42001: self.access_token = await self.get_access_token(self.secret) - return await self.send_private_msg(user_id,agent_id,content) + return await self.send_private_msg(user_id, agent_id, content) if data['errcode'] != 0: - raise Exception("Failed to send message: "+str(data)) + raise Exception('Failed to send message: ' + str(data)) async def handle_callback_request(self): """ 处理回调请求,包括 GET 验证和 POST 消息接收。 """ try: + msg_signature = request.args.get('msg_signature') + timestamp = request.args.get('timestamp') + nonce = request.args.get('nonce') - msg_signature = request.args.get("msg_signature") - timestamp = request.args.get("timestamp") - nonce = request.args.get("nonce") - - if request.method == "GET": - echostr = request.args.get("echostr") - ret, reply_echo_str = self.wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr) + if request.method == 'GET': + echostr = request.args.get('echostr') + ret, reply_echo_str = self.wxcpt.VerifyURL( + msg_signature, timestamp, nonce, echostr + ) if ret != 0: - raise Exception(f"验证失败,错误码: {ret}") + raise Exception(f'验证失败,错误码: {ret}') return reply_echo_str - elif request.method == "POST": + elif request.method == 'POST': encrypt_msg = await request.data - ret, xml_msg = self.wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce) + ret, xml_msg = self.wxcpt.DecryptMsg( + encrypt_msg, msg_signature, timestamp, nonce + ) if ret != 0: - raise Exception(f"消息解密失败,错误码: {ret}") + raise Exception(f'消息解密失败,错误码: {ret}') # 解析消息并处理 message_data = await self.get_message(xml_msg) if message_data: - event = WecomEvent.from_payload(message_data) # 转换为 WecomEvent 对象 + event = WecomEvent.from_payload( + message_data + ) # 转换为 WecomEvent 对象 if event: await self._handle_message(event) - return "success" + return 'success' except Exception as e: - return f"Error processing request: {str(e)}", 400 + return f'Error processing request: {str(e)}', 400 async def run_task(self, host: str, port: int, *args, **kwargs): """ @@ -194,11 +225,13 @@ class WecomClient(): """ 注册消息类型处理器。 """ + def decorator(func: Callable[[WecomEvent], None]): if msg_type not in self._message_handlers: self._message_handlers[msg_type] = [] self._message_handlers[msg_type].append(func) return func + return decorator async def _handle_message(self, event: WecomEvent): @@ -216,38 +249,47 @@ class WecomClient(): """ root = ET.fromstring(xml_msg) message_data = { - "ToUserName": root.find("ToUserName").text, - "FromUserName": root.find("FromUserName").text, - "CreateTime": int(root.find("CreateTime").text), - "MsgType": root.find("MsgType").text, - "Content": root.find("Content").text if root.find("Content") is not None else None, - "MsgId": int(root.find("MsgId").text) if root.find("MsgId") is not None else None, - "AgentID": int(root.find("AgentID").text) if root.find("AgentID") is not None else None, + 'ToUserName': root.find('ToUserName').text, + 'FromUserName': root.find('FromUserName').text, + 'CreateTime': int(root.find('CreateTime').text), + 'MsgType': root.find('MsgType').text, + 'Content': root.find('Content').text + if root.find('Content') is not None + else None, + 'MsgId': int(root.find('MsgId').text) + if root.find('MsgId') is not None + else None, + 'AgentID': int(root.find('AgentID').text) + if root.find('AgentID') is not None + else None, } - if message_data["MsgType"] == "image": - message_data["MediaId"] = root.find("MediaId").text if root.find("MediaId") is not None else None - message_data["PicUrl"] = root.find("PicUrl").text if root.find("PicUrl") is not None else None - + if message_data['MsgType'] == 'image': + message_data['MediaId'] = ( + root.find('MediaId').text if root.find('MediaId') is not None else None + ) + message_data['PicUrl'] = ( + root.find('PicUrl').text if root.find('PicUrl') is not None else None + ) + return message_data - + @staticmethod async def get_image_type(image_bytes: bytes) -> str: """ 通过图片的magic numbers判断图片类型 """ magic_numbers = { - b'\xFF\xD8\xFF': 'jpg', - b'\x89\x50\x4E\x47': 'png', + b'\xff\xd8\xff': 'jpg', + b'\x89\x50\x4e\x47': 'png', b'\x47\x49\x46': 'gif', - b'\x42\x4D': 'bmp', - b'\x00\x00\x01\x00': 'ico' + b'\x42\x4d': 'bmp', + b'\x00\x00\x01\x00': 'ico', } - + for magic, ext in magic_numbers.items(): if image_bytes.startswith(magic): return ext return 'jpg' # 默认返回jpg - async def upload_to_work(self, image: platform_message.Image): """ @@ -256,9 +298,14 @@ class WecomClient(): if not await self.check_access_token(): self.access_token = await self.get_access_token(self.secret) - url = self.base_url + '/media/upload?access_token=' + self.access_token + '&type=file' + url = ( + self.base_url + + '/media/upload?access_token=' + + self.access_token + + '&type=file' + ) file_bytes = None - file_name = "uploaded_file.txt" + file_name = 'uploaded_file.txt' # 获取文件的二进制数据 if image.path: @@ -277,20 +324,22 @@ class WecomClient(): padded_base64 = base64_data + '=' * padding file_bytes = base64.b64decode(padded_base64) except binascii.Error as e: - raise ValueError(f"Invalid base64 string: {str(e)}") + raise ValueError(f'Invalid base64 string: {str(e)}') else: - raise ValueError("image对象出错") + raise ValueError('image对象出错') # 设置 multipart/form-data 格式的文件 - boundary = "-------------------------acebdf13572468" - headers = { - 'Content-Type': f'multipart/form-data; boundary={boundary}' - } + boundary = '-------------------------acebdf13572468' + headers = {'Content-Type': f'multipart/form-data; boundary={boundary}'} body = ( - f"--{boundary}\r\n" - f"Content-Disposition: form-data; name=\"media\"; filename=\"{file_name}\"; filelength={len(file_bytes)}\r\n" - f"Content-Type: application/octet-stream\r\n\r\n" - ).encode('utf-8') + file_bytes + f"\r\n--{boundary}--\r\n".encode('utf-8') + ( + f'--{boundary}\r\n' + f'Content-Disposition: form-data; name="media"; filename="{file_name}"; filelength={len(file_bytes)}\r\n' + f'Content-Type: application/octet-stream\r\n\r\n' + ).encode('utf-8') + + file_bytes + + f'\r\n--{boundary}--\r\n'.encode('utf-8') + ) # 上传文件 async with httpx.AsyncClient() as client: @@ -300,19 +349,18 @@ 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: - raise Exception("failed to upload file") + raise Exception('failed to upload file') media_id = data.get('media_id') return media_id - async def download_image_to_bytes(self,url:str) -> bytes: + async def download_image_to_bytes(self, url: str) -> bytes: async with httpx.AsyncClient() as client: response = await client.get(url) response.raise_for_status() return response.content - #进行media_id的获取 + # 进行media_id的获取 async def get_media_id(self, image: platform_message.Image): - media_id = await self.upload_to_work(image=image) return media_id diff --git a/libs/wecom_api/ierror.py b/libs/wecom_api/ierror.py index 8985b886..6c7ca122 100644 --- a/libs/wecom_api/ierror.py +++ b/libs/wecom_api/ierror.py @@ -4,7 +4,7 @@ # Author: jonyqin # Created Time: Thu 11 Sep 2014 01:53:58 PM CST # File Name: ierror.py -# Description:定义错误码含义 +# Description:定义错误码含义 ######################################################################### WXBizMsgCrypt_OK = 0 WXBizMsgCrypt_ValidateSignature_Error = -40001 @@ -17,4 +17,4 @@ WXBizMsgCrypt_DecryptAES_Error = -40007 WXBizMsgCrypt_IllegalBuffer = -40008 WXBizMsgCrypt_EncodeBase64_Error = -40009 WXBizMsgCrypt_DecodeBase64_Error = -40010 -WXBizMsgCrypt_GenReturnXml_Error = -40011 \ No newline at end of file +WXBizMsgCrypt_GenReturnXml_Error = -40011 diff --git a/libs/wecom_api/wecomevent.py b/libs/wecom_api/wecomevent.py index 3606cdf5..a0c2c7da 100644 --- a/libs/wecom_api/wecomevent.py +++ b/libs/wecom_api/wecomevent.py @@ -9,7 +9,7 @@ class WecomEvent(dict): """ @staticmethod - def from_payload(payload: Dict[str, Any]) -> Optional["WecomEvent"]: + def from_payload(payload: Dict[str, Any]) -> Optional['WecomEvent']: """ 从企业微信事件数据构造 `WecomEvent` 对象。 @@ -34,14 +34,14 @@ class WecomEvent(dict): Returns: str: 事件类型。 """ - return self.get("MsgType", "") - + return self.get('MsgType', '') + @property def picurl(self) -> str: """ 图片链接 """ - return self.get("PicUrl") + return self.get('PicUrl') @property def detail_type(self) -> str: @@ -53,8 +53,8 @@ class WecomEvent(dict): Returns: str: 事件详细类型。 """ - if self.type == "event": - return self.get("Event", "") + if self.type == 'event': + return self.get('Event', '') return self.type @property @@ -65,7 +65,7 @@ class WecomEvent(dict): Returns: str: 事件名。 """ - return f"{self.type}.{self.detail_type}" + return f'{self.type}.{self.detail_type}' @property def user_id(self) -> Optional[str]: @@ -75,8 +75,8 @@ class WecomEvent(dict): Returns: Optional[str]: 用户 ID。 """ - return self.get("FromUserName") - + return self.get('FromUserName') + @property def agent_id(self) -> Optional[int]: """ @@ -85,7 +85,7 @@ class WecomEvent(dict): Returns: Optional[int]: 机器人 ID。 """ - return self.get("AgentID") + return self.get('AgentID') @property def receiver_id(self) -> Optional[str]: @@ -95,7 +95,7 @@ class WecomEvent(dict): Returns: Optional[str]: 接收者 ID。 """ - return self.get("ToUserName") + return self.get('ToUserName') @property def message_id(self) -> Optional[str]: @@ -105,7 +105,7 @@ class WecomEvent(dict): Returns: Optional[str]: 消息 ID。 """ - return self.get("MsgId") + return self.get('MsgId') @property def message(self) -> Optional[str]: @@ -115,7 +115,7 @@ class WecomEvent(dict): Returns: Optional[str]: 消息内容。 """ - return self.get("Content") + return self.get('Content') @property def media_id(self) -> Optional[str]: @@ -125,7 +125,7 @@ class WecomEvent(dict): Returns: Optional[str]: 媒体文件 ID。 """ - return self.get("MediaId") + return self.get('MediaId') @property def timestamp(self) -> Optional[int]: @@ -135,7 +135,7 @@ class WecomEvent(dict): Returns: Optional[int]: 时间戳。 """ - return self.get("CreateTime") + return self.get('CreateTime') @property def event_key(self) -> Optional[str]: @@ -145,7 +145,7 @@ class WecomEvent(dict): Returns: Optional[str]: 事件 Key。 """ - return self.get("EventKey") + return self.get('EventKey') def __getattr__(self, key: str) -> Optional[Any]: """ @@ -176,4 +176,4 @@ class WecomEvent(dict): Returns: str: 字符串表示。 """ - return f"" + return f'' diff --git a/main.py b/main.py index 5c86b7ca..8be603c6 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,4 @@ +import asyncio # LangBot 终端启动入口 # 在此层级解决依赖项检查。 # LangBot/main.py @@ -14,9 +15,6 @@ asciiart = r""" """ -import asyncio - - async def main_entry(loop: asyncio.AbstractEventLoop): print(asciiart) @@ -29,20 +27,22 @@ async def main_entry(loop: asyncio.AbstractEventLoop): missing_deps = await deps.check_deps() if missing_deps: - print("以下依赖包未安装,将自动安装,请完成后重启程序:") + print('以下依赖包未安装,将自动安装,请完成后重启程序:') for dep in missing_deps: - print("-", dep) + print('-', dep) await deps.install_deps(missing_deps) - print("已自动安装缺失的依赖包,请重启程序。") + print('已自动安装缺失的依赖包,请重启程序。') sys.exit(0) - + # check plugin deps await deps.precheck_plugin_deps() # 检查pydantic版本,如果没有 pydantic.v1,则把 pydantic 映射为 v1 import pydantic.version + if pydantic.version.VERSION < '2.0': import pydantic + sys.modules['pydantic.v1'] = pydantic # 检查配置文件 @@ -52,11 +52,12 @@ async def main_entry(loop: asyncio.AbstractEventLoop): generated_files = await files.generate_files() if generated_files: - print("以下文件不存在,已自动生成:") + print('以下文件不存在,已自动生成:') for file in generated_files: - print("-", file) + print('-', file) from pkg.core import boot + await boot.main(loop) @@ -66,8 +67,8 @@ if __name__ == '__main__': # 必须大于 3.10.1 if sys.version_info < (3, 10, 1): - print("需要 Python 3.10.1 及以上版本,当前 Python 版本为:", sys.version) - input("按任意键退出...") + print('需要 Python 3.10.1 及以上版本,当前 Python 版本为:', sys.version) + input('按任意键退出...') exit(1) # 检查本目录是否有main.py,且包含LangBot字符串 @@ -78,11 +79,11 @@ if __name__ == '__main__': else: with open('main.py', 'r', encoding='utf-8') as f: content = f.read() - if "LangBot/main.py" not in content: + if 'LangBot/main.py' not in content: invalid_pwd = True if invalid_pwd: - print("请在 LangBot 项目根目录下以命令形式运行此程序。") - input("按任意键退出...") + print('请在 LangBot 项目根目录下以命令形式运行此程序。') + input('按任意键退出...') exit(1) loop = asyncio.new_event_loop() diff --git a/pkg/api/http/controller/group.py b/pkg/api/http/controller/group.py index 7186802f..efbb7247 100644 --- a/pkg/api/http/controller/group.py +++ b/pkg/api/http/controller/group.py @@ -13,6 +13,7 @@ from ....core import app preregistered_groups: list[type[RouterGroup]] = [] """RouterGroup 的预注册列表""" + def group_class(name: str, path: str) -> None: """注册一个 RouterGroup""" @@ -27,12 +28,12 @@ def group_class(name: str, path: str) -> None: class AuthType(enum.Enum): """认证类型""" + NONE = 'none' USER_TOKEN = 'user-token' class RouterGroup(abc.ABC): - name: str path: str @@ -49,17 +50,24 @@ class RouterGroup(abc.ABC): async def initialize(self) -> None: pass - def route(self, rule: str, auth_type: AuthType = AuthType.USER_TOKEN, **options: typing.Any) -> typing.Callable[[RouteCallable], RouteCallable]: # decorator + def route( + self, + rule: str, + auth_type: AuthType = AuthType.USER_TOKEN, + **options: typing.Any, + ) -> typing.Callable[[RouteCallable], RouteCallable]: # decorator """注册一个路由""" + def decorator(f: RouteCallable) -> RouteCallable: nonlocal rule rule = self.path + rule async def handler_error(*args, **kwargs): - if auth_type == AuthType.USER_TOKEN: # 从Authorization头中获取token - token = quart.request.headers.get('Authorization', '').replace('Bearer ', '') + token = quart.request.headers.get('Authorization', '').replace( + 'Bearer ', '' + ) if not token: return self.http_status(401, -1, '未提供有效的用户令牌') @@ -75,11 +83,11 @@ class RouterGroup(abc.ABC): try: return await f(*args, **kwargs) - except Exception as e: # 自动 500 + except Exception: # 自动 500 traceback.print_exc() # return self.http_status(500, -2, str(e)) return self.http_status(500, -2, 'internal server error') - + new_f = handler_error new_f.__name__ = (self.name + rule).replace('/', '__') new_f.__doc__ = f.__doc__ @@ -91,20 +99,24 @@ class RouterGroup(abc.ABC): def success(self, data: typing.Any = None) -> quart.Response: """返回一个 200 响应""" - return quart.jsonify({ - 'code': 0, - 'msg': 'ok', - 'data': data, - }) - + return quart.jsonify( + { + 'code': 0, + 'msg': 'ok', + 'data': data, + } + ) + def fail(self, code: int, msg: str) -> quart.Response: """返回一个异常响应""" - return quart.jsonify({ - 'code': code, - 'msg': msg, - }) - + return quart.jsonify( + { + 'code': code, + 'msg': msg, + } + ) + def http_status(self, status: int, code: int, msg: str) -> quart.Response: """返回一个指定状态码的响应""" return self.fail(code, msg), status diff --git a/pkg/api/http/controller/groups/logs.py b/pkg/api/http/controller/groups/logs.py index 4244d889..b0643cb6 100644 --- a/pkg/api/http/controller/groups/logs.py +++ b/pkg/api/http/controller/groups/logs.py @@ -1,32 +1,29 @@ from __future__ import annotations -import traceback import quart -from .....core import app from .. import group @group.group_class('logs', '/api/v1/logs') class LogsRouterGroup(group.RouterGroup): - async def initialize(self) -> None: @self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN) async def _() -> str: - start_page_number = int(quart.request.args.get('start_page_number', 0)) start_offset = int(quart.request.args.get('start_offset', 0)) - logs_str, end_page_number, end_offset = self.ap.log_cache.get_log_by_pointer( - start_page_number=start_page_number, - start_offset=start_offset + logs_str, end_page_number, end_offset = ( + self.ap.log_cache.get_log_by_pointer( + start_page_number=start_page_number, start_offset=start_offset + ) ) return self.success( data={ - "logs": logs_str, - "end_page_number": end_page_number, - "end_offset": end_offset + 'logs': logs_str, + 'end_page_number': end_page_number, + 'end_offset': end_offset, } ) diff --git a/pkg/api/http/controller/groups/pipelines.py b/pkg/api/http/controller/groups/pipelines.py index 400da593..02564e58 100644 --- a/pkg/api/http/controller/groups/pipelines.py +++ b/pkg/api/http/controller/groups/pipelines.py @@ -3,46 +3,41 @@ from __future__ import annotations import quart from .. import group -from .....entity.persistence import pipeline @group.group_class('pipelines', '/api/v1/pipelines') class PipelinesRouterGroup(group.RouterGroup): - async def initialize(self) -> None: - @self.route('', methods=['GET', 'POST']) async def _() -> str: if quart.request.method == 'GET': - return self.success(data={ - 'pipelines': await self.ap.pipeline_service.get_pipelines() - }) + return self.success( + data={'pipelines': await self.ap.pipeline_service.get_pipelines()} + ) elif quart.request.method == 'POST': json_data = await quart.request.json - pipeline_uuid = await self.ap.pipeline_service.create_pipeline(json_data) + pipeline_uuid = await self.ap.pipeline_service.create_pipeline( + json_data + ) - return self.success(data={ - 'uuid': pipeline_uuid - }) + return self.success(data={'uuid': pipeline_uuid}) @self.route('/_/metadata', methods=['GET']) async def _() -> str: - return self.success(data={ - 'configs': await self.ap.pipeline_service.get_pipeline_metadata() - }) + return self.success( + data={'configs': await self.ap.pipeline_service.get_pipeline_metadata()} + ) @self.route('/', methods=['GET', 'PUT', 'DELETE']) async def _(pipeline_uuid: str) -> str: if quart.request.method == 'GET': pipeline = await self.ap.pipeline_service.get_pipeline(pipeline_uuid) - + if pipeline is None: return self.http_status(404, -1, 'pipeline not found') - return self.success(data={ - 'pipeline': pipeline - }) + return self.success(data={'pipeline': pipeline}) elif quart.request.method == 'PUT': json_data = await quart.request.json @@ -53,4 +48,3 @@ class PipelinesRouterGroup(group.RouterGroup): await self.ap.pipeline_service.delete_pipeline(pipeline_uuid) return self.success() - diff --git a/pkg/api/http/controller/groups/platform/adapters.py b/pkg/api/http/controller/groups/platform/adapters.py index fa8b2d9c..f6fb8489 100644 --- a/pkg/api/http/controller/groups/platform/adapters.py +++ b/pkg/api/http/controller/groups/platform/adapters.py @@ -5,29 +5,33 @@ from ... import group @group.group_class('adapters', '/api/v1/platform/adapters') class AdaptersRouterGroup(group.RouterGroup): - async def initialize(self) -> None: @self.route('', methods=['GET']) async def _() -> str: - return self.success(data={ - 'adapters': self.ap.platform_mgr.get_available_adapters_info() - }) - + return self.success( + data={'adapters': self.ap.platform_mgr.get_available_adapters_info()} + ) + @self.route('/', methods=['GET']) async def _(adapter_name: str) -> str: - adapter_info = self.ap.platform_mgr.get_available_adapter_info_by_name(adapter_name) + adapter_info = self.ap.platform_mgr.get_available_adapter_info_by_name( + adapter_name + ) if adapter_info is None: return self.http_status(404, -1, 'adapter not found') - return self.success(data={ - 'adapter': adapter_info - }) - - @self.route('//icon', methods=['GET']) - async def _(adapter_name: str) -> quart.Response: + return self.success(data={'adapter': adapter_info}) - adapter_manifest = self.ap.platform_mgr.get_available_adapter_manifest_by_name(adapter_name) + @self.route( + '//icon', methods=['GET'], auth_type=group.AuthType.NONE + ) + async def _(adapter_name: str) -> quart.Response: + adapter_manifest = ( + self.ap.platform_mgr.get_available_adapter_manifest_by_name( + adapter_name + ) + ) if adapter_manifest is None: return self.http_status(404, -1, 'adapter not found') @@ -37,4 +41,4 @@ class AdaptersRouterGroup(group.RouterGroup): if icon_path is None: return self.http_status(404, -1, 'icon not found') - return await quart.send_file(icon_path) \ No newline at end of file + return await quart.send_file(icon_path) diff --git a/pkg/api/http/controller/groups/platform/bots.py b/pkg/api/http/controller/groups/platform/bots.py index fe20aa53..af248fac 100644 --- a/pkg/api/http/controller/groups/platform/bots.py +++ b/pkg/api/http/controller/groups/platform/bots.py @@ -5,34 +5,27 @@ from ... import group @group.group_class('bots', '/api/v1/platform/bots') class BotsRouterGroup(group.RouterGroup): - async def initialize(self) -> None: @self.route('', methods=['GET', 'POST']) async def _() -> str: if quart.request.method == 'GET': - return self.success(data={ - 'bots': await self.ap.bot_service.get_bots() - }) + return self.success(data={'bots': await self.ap.bot_service.get_bots()}) elif quart.request.method == 'POST': json_data = await quart.request.json bot_uuid = await self.ap.bot_service.create_bot(json_data) - return self.success(data={ - 'uuid': bot_uuid - }) - + return self.success(data={'uuid': bot_uuid}) + @self.route('/', methods=['GET', 'PUT', 'DELETE']) async def _(bot_uuid: str) -> str: if quart.request.method == 'GET': bot = await self.ap.bot_service.get_bot(bot_uuid) if bot is None: return self.http_status(404, -1, 'bot not found') - return self.success(data={ - 'bot': bot - }) + return self.success(data={'bot': bot}) elif quart.request.method == 'PUT': json_data = await quart.request.json await self.ap.bot_service.update_bot(bot_uuid, json_data) return self.success() elif quart.request.method == 'DELETE': await self.ap.bot_service.delete_bot(bot_uuid) - return self.success() \ No newline at end of file + return self.success() diff --git a/pkg/api/http/controller/groups/plugins.py b/pkg/api/http/controller/groups/plugins.py index 330231c2..1deecca6 100644 --- a/pkg/api/http/controller/groups/plugins.py +++ b/pkg/api/http/controller/groups/plugins.py @@ -1,17 +1,14 @@ from __future__ import annotations -import traceback - import quart -from .....core import app, taskmgr +from .....core import taskmgr from .. import group @group.group_class('plugins', '/api/v1/plugins') class PluginsRouterGroup(group.RouterGroup): - async def initialize(self) -> None: @self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN) async def _() -> str: @@ -19,63 +16,69 @@ class PluginsRouterGroup(group.RouterGroup): plugins_data = [plugin.model_dump() for plugin in plugins] - return self.success(data={ - 'plugins': plugins_data - }) - - @self.route('///toggle', methods=['PUT'], auth_type=group.AuthType.USER_TOKEN) + return self.success(data={'plugins': plugins_data}) + + @self.route( + '///toggle', + methods=['PUT'], + auth_type=group.AuthType.USER_TOKEN, + ) async def _(author: str, plugin_name: str) -> str: data = await quart.request.json target_enabled = data.get('target_enabled') await self.ap.plugin_mgr.update_plugin_switch(plugin_name, target_enabled) return self.success() - - @self.route('///update', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) + + @self.route( + '///update', + methods=['POST'], + auth_type=group.AuthType.USER_TOKEN, + ) async def _(author: str, plugin_name: str) -> str: ctx = taskmgr.TaskContext.new() wrapper = self.ap.task_mgr.create_user_task( self.ap.plugin_mgr.update_plugin(plugin_name, task_context=ctx), - kind="plugin-operation", - name=f"plugin-update-{plugin_name}", - label=f"更新插件 {plugin_name}", - context=ctx + kind='plugin-operation', + name=f'plugin-update-{plugin_name}', + label=f'更新插件 {plugin_name}', + context=ctx, ) - return self.success(data={ - 'task_id': wrapper.id - }) - - @self.route('//', methods=['GET', 'DELETE'], auth_type=group.AuthType.USER_TOKEN) + return self.success(data={'task_id': wrapper.id}) + + @self.route( + '//', + methods=['GET', 'DELETE'], + auth_type=group.AuthType.USER_TOKEN, + ) async def _(author: str, plugin_name: str) -> str: if quart.request.method == 'GET': plugin = self.ap.plugin_mgr.get_plugin(author, plugin_name) if plugin is None: return self.http_status(404, -1, 'plugin not found') - return self.success(data={ - 'plugin': plugin.model_dump() - }) + return self.success(data={'plugin': plugin.model_dump()}) elif quart.request.method == 'DELETE': ctx = taskmgr.TaskContext.new() wrapper = self.ap.task_mgr.create_user_task( self.ap.plugin_mgr.uninstall_plugin(plugin_name, task_context=ctx), - kind="plugin-operation", + kind='plugin-operation', name=f'plugin-remove-{plugin_name}', label=f'删除插件 {plugin_name}', - context=ctx + context=ctx, ) - return self.success(data={ - 'task_id': wrapper.id - }) - - @self.route('///config', methods=['GET', 'PUT'], auth_type=group.AuthType.USER_TOKEN) + return self.success(data={'task_id': wrapper.id}) + + @self.route( + '///config', + methods=['GET', 'PUT'], + auth_type=group.AuthType.USER_TOKEN, + ) async def _(author: str, plugin_name: str) -> quart.Response: plugin = self.ap.plugin_mgr.get_plugin(author, plugin_name) if plugin is None: return self.http_status(404, -1, 'plugin not found') if quart.request.method == 'GET': - return self.success(data={ - 'config': plugin.plugin_config - }) + return self.success(data={'config': plugin.plugin_config}) elif quart.request.method == 'PUT': data = await quart.request.json @@ -88,21 +91,21 @@ class PluginsRouterGroup(group.RouterGroup): data = await quart.request.json await self.ap.plugin_mgr.reorder_plugins(data.get('plugins')) return self.success() - - @self.route('/install/github', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) + + @self.route( + '/install/github', methods=['POST'], auth_type=group.AuthType.USER_TOKEN + ) async def _() -> str: data = await quart.request.json - + ctx = taskmgr.TaskContext.new() short_source_str = data['source'][-8:] wrapper = self.ap.task_mgr.create_user_task( self.ap.plugin_mgr.install_plugin(data['source'], task_context=ctx), - kind="plugin-operation", - name=f'plugin-install-github', + kind='plugin-operation', + name='plugin-install-github', label=f'安装插件 ...{short_source_str}', - context=ctx + context=ctx, ) - return self.success(data={ - 'task_id': wrapper.id - }) + return self.success(data={'task_id': wrapper.id}) diff --git a/pkg/api/http/controller/groups/provider/models.py b/pkg/api/http/controller/groups/provider/models.py index 81e9078b..eaf96047 100644 --- a/pkg/api/http/controller/groups/provider/models.py +++ b/pkg/api/http/controller/groups/provider/models.py @@ -1,28 +1,23 @@ import quart -import uuid from ... import group -from ......entity.persistence import model @group.group_class('models/llm', '/api/v1/provider/models/llm') class LLMModelsRouterGroup(group.RouterGroup): - async def initialize(self) -> None: @self.route('', methods=['GET', 'POST']) async def _() -> str: if quart.request.method == 'GET': - return self.success(data={ - 'models': await self.ap.model_service.get_llm_models() - }) + return self.success( + data={'models': await self.ap.model_service.get_llm_models()} + ) elif quart.request.method == 'POST': json_data = await quart.request.json model_uuid = await self.ap.model_service.create_llm_model(json_data) - return self.success(data={ - 'uuid': model_uuid - }) + return self.success(data={'uuid': model_uuid}) @self.route('/', methods=['GET', 'DELETE']) async def _(model_uuid: str) -> str: @@ -32,9 +27,7 @@ class LLMModelsRouterGroup(group.RouterGroup): if model is None: return self.http_status(404, -1, 'model not found') - return self.success(data={ - 'model': model - }) + return self.success(data={'model': model}) # elif quart.request.method == 'PUT': # json_data = await quart.request.json diff --git a/pkg/api/http/controller/groups/provider/requesters.py b/pkg/api/http/controller/groups/provider/requesters.py index 18939d32..5d93b9cb 100644 --- a/pkg/api/http/controller/groups/provider/requesters.py +++ b/pkg/api/http/controller/groups/provider/requesters.py @@ -5,29 +5,33 @@ from ... import group @group.group_class('provider/requesters', '/api/v1/provider/requesters') class RequestersRouterGroup(group.RouterGroup): - async def initialize(self) -> None: @self.route('', methods=['GET']) async def _() -> quart.Response: - return self.success(data={ - 'requesters': self.ap.model_mgr.get_available_requesters_info() - }) - + return self.success( + data={'requesters': self.ap.model_mgr.get_available_requesters_info()} + ) + @self.route('/', methods=['GET']) async def _(requester_name: str) -> quart.Response: - - requester_info = self.ap.model_mgr.get_available_requester_info_by_name(requester_name) + requester_info = self.ap.model_mgr.get_available_requester_info_by_name( + requester_name + ) if requester_info is None: return self.http_status(404, -1, 'requester not found') - return self.success(data={ - 'requester': requester_info - }) - - @self.route('//icon', methods=['GET']) + return self.success(data={'requester': requester_info}) + + @self.route( + '//icon', methods=['GET'], auth_type=group.AuthType.NONE + ) async def _(requester_name: str) -> quart.Response: - requester_manifest = self.ap.model_mgr.get_available_requester_manifest_by_name(requester_name) + requester_manifest = ( + self.ap.model_mgr.get_available_requester_manifest_by_name( + requester_name + ) + ) if requester_manifest is None: return self.http_status(404, -1, 'requester not found') diff --git a/pkg/api/http/controller/groups/stats.py b/pkg/api/http/controller/groups/stats.py index 43d56f27..7b1d4353 100644 --- a/pkg/api/http/controller/groups/stats.py +++ b/pkg/api/http/controller/groups/stats.py @@ -1,23 +1,21 @@ -import quart -import asyncio - -from .....core import app, taskmgr from .. import group @group.group_class('stats', '/api/v1/stats') class StatsRouterGroup(group.RouterGroup): - async def initialize(self) -> None: @self.route('/basic', methods=['GET'], auth_type=group.AuthType.USER_TOKEN) async def _() -> str: - conv_count = 0 for session in self.ap.sess_mgr.session_list: - conv_count += len(session.conversations if session.conversations is not None else []) + conv_count += len( + session.conversations if session.conversations is not None else [] + ) - return self.success(data={ - 'active_session_count': len(self.ap.sess_mgr.session_list), - 'conversation_count': conv_count, - 'query_count': self.ap.query_pool.query_id_counter, - }) + return self.success( + data={ + 'active_session_count': len(self.ap.sess_mgr.session_list), + 'conversation_count': conv_count, + 'query_count': self.ap.query_pool.query_id_counter, + } + ) diff --git a/pkg/api/http/controller/groups/system.py b/pkg/api/http/controller/groups/system.py index 04ace284..c586ea27 100644 --- a/pkg/api/http/controller/groups/system.py +++ b/pkg/api/http/controller/groups/system.py @@ -1,63 +1,62 @@ import quart -import asyncio -from .....core import app, taskmgr from .. import group from .....utils import constants @group.group_class('system', '/api/v1/system') class SystemRouterGroup(group.RouterGroup): - async def initialize(self) -> None: @self.route('/info', methods=['GET'], auth_type=group.AuthType.NONE) async def _() -> str: return self.success( data={ - "version": constants.semantic_version, - "debug": constants.debug_mode, - "enabled_platform_count": len(self.ap.platform_mgr.get_running_adapters()) + 'version': constants.semantic_version, + 'debug': constants.debug_mode, + 'enabled_platform_count': len( + self.ap.platform_mgr.get_running_adapters() + ), } ) @self.route('/tasks', methods=['GET'], auth_type=group.AuthType.USER_TOKEN) async def _() -> str: - task_type = quart.request.args.get("type") + task_type = quart.request.args.get('type') if task_type == '': task_type = None - return self.success( - data=self.ap.task_mgr.get_tasks_dict(task_type) - ) - - @self.route('/tasks/', methods=['GET'], auth_type=group.AuthType.USER_TOKEN) + return self.success(data=self.ap.task_mgr.get_tasks_dict(task_type)) + + @self.route( + '/tasks/', methods=['GET'], auth_type=group.AuthType.USER_TOKEN + ) async def _(task_id: str) -> str: task = self.ap.task_mgr.get_task_by_id(int(task_id)) if task is None: - return self.http_status(404, 404, "Task not found") - + return self.http_status(404, 404, 'Task not found') + return self.success(data=task.to_dict()) - + @self.route('/reload', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) async def _() -> str: json_data = await quart.request.json - scope = json_data.get("scope") + scope = json_data.get('scope') - await self.ap.reload( - scope=scope - ) + await self.ap.reload(scope=scope) return self.success() - @self.route('/_debug/exec', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) + @self.route( + '/_debug/exec', methods=['POST'], auth_type=group.AuthType.USER_TOKEN + ) async def _() -> str: if not constants.debug_mode: - return self.http_status(403, 403, "Forbidden") - + return self.http_status(403, 403, 'Forbidden') + py_code = await quart.request.data ap = self.ap - return self.success(data=exec(py_code, {"ap": ap})) + return self.success(data=exec(py_code, {'ap': ap})) diff --git a/pkg/api/http/controller/groups/user.py b/pkg/api/http/controller/groups/user.py index 3cd08240..4c330782 100644 --- a/pkg/api/http/controller/groups/user.py +++ b/pkg/api/http/controller/groups/user.py @@ -1,22 +1,19 @@ import quart -import jwt import argon2 from .. import group -from .....entity.persistence import user @group.group_class('user', '/api/v1/user') class UserRouterGroup(group.RouterGroup): - async def initialize(self) -> None: @self.route('/init', methods=['GET', 'POST'], auth_type=group.AuthType.NONE) async def _() -> str: if quart.request.method == 'GET': - return self.success(data={ - 'initialized': await self.ap.user_service.is_initialized() - }) - + return self.success( + data={'initialized': await self.ap.user_service.is_initialized()} + ) + if await self.ap.user_service.is_initialized(): return self.fail(1, '系统已初始化') @@ -28,24 +25,24 @@ class UserRouterGroup(group.RouterGroup): await self.ap.user_service.create_user(user_email, password) return self.success() - + @self.route('/auth', methods=['POST'], auth_type=group.AuthType.NONE) async def _() -> str: json_data = await quart.request.json try: - token = await self.ap.user_service.authenticate(json_data['user'], json_data['password']) + token = await self.ap.user_service.authenticate( + json_data['user'], json_data['password'] + ) except argon2.exceptions.VerifyMismatchError: return self.fail(1, '用户名或密码错误') - return self.success(data={ - 'token': token - }) + return self.success(data={'token': token}) - @self.route('/check-token', methods=['GET'], auth_type=group.AuthType.USER_TOKEN) + @self.route( + '/check-token', methods=['GET'], auth_type=group.AuthType.USER_TOKEN + ) async def _(user_email: str) -> str: token = await self.ap.user_service.generate_jwt_token(user_email) - return self.success(data={ - 'token': token - }) + return self.success(data={'token': token}) diff --git a/pkg/api/http/controller/main.py b/pkg/api/http/controller/main.py index 79838d7a..e3aab6f4 100644 --- a/pkg/api/http/controller/main.py +++ b/pkg/api/http/controller/main.py @@ -7,15 +7,19 @@ import quart import quart_cors from ....core import app, entities as core_entities +from ....utils import importutil -from .groups import logs, system, plugins, stats, user, pipelines -from .groups.provider import models, requesters -from .groups.platform import bots, adapters +from . import groups from . import group +from .groups import provider as groups_provider +from .groups import platform as groups_platform + +importutil.import_modules_in_pkg(groups) +importutil.import_modules_in_pkg(groups_provider) +importutil.import_modules_in_pkg(groups_platform) class HTTPController: - ap: app.Application quart_app: quart.Quart @@ -23,7 +27,7 @@ class HTTPController: def __init__(self, ap: app.Application) -> None: self.ap = ap self.quart_app = quart.Quart(__name__) - quart_cors.cors(self.quart_app, allow_origin="*") + quart_cors.cors(self.quart_app, allow_origin='*') async def initialize(self) -> None: await self.register_routes() @@ -37,11 +41,9 @@ class HTTPController: async def exception_handler(*args, **kwargs): try: - await self.quart_app.run_task( - *args, **kwargs - ) + await self.quart_app.run_task(*args, **kwargs) except Exception as e: - self.ap.logger.error(f"启动 HTTP 服务失败: {e}") + self.ap.logger.error(f'启动 HTTP 服务失败: {e}') self.ap.task_mgr.create_task( exception_handler( @@ -49,28 +51,63 @@ class HTTPController: port=self.ap.instance_config.data['api']['port'], shutdown_trigger=shutdown_trigger_placeholder, ), - name="http-api-quart", + name='http-api-quart', scopes=[core_entities.LifecycleControlScope.APPLICATION], ) # await asyncio.sleep(5) async def register_routes(self) -> None: - - @self.quart_app.route("/healthz") + @self.quart_app.route('/healthz') async def healthz(): - return {"code": 0, "msg": "ok"} + return {'code': 0, 'msg': 'ok'} for g in group.preregistered_groups: ginst = g(self.ap, self.quart_app) await ginst.initialize() - frontend_path = "web/dist" + frontend_path = 'web/out' - @self.quart_app.route("/") + @self.quart_app.route('/') async def index(): - return await quart.send_from_directory(frontend_path, "index.html") + return await quart.send_from_directory( + frontend_path, 'index.html', mimetype='text/html' + ) - @self.quart_app.route("/") + @self.quart_app.route('/') async def static_file(path: str): - return await quart.send_from_directory(frontend_path, path) + print(f'path: {path}') + if not (os.path.exists(os.path.join(frontend_path, path)) and os.path.isfile(os.path.join(frontend_path, path))): + if os.path.exists(os.path.join(frontend_path, path + '.html')): + path += '.html' + else: + return await quart.send_from_directory(frontend_path, '404.html') + + mimetype = None + + if path.endswith('.html'): + mimetype = 'text/html' + elif path.endswith('.js'): + mimetype = 'application/javascript' + elif path.endswith('.css'): + mimetype = 'text/css' + elif path.endswith('.png'): + mimetype = 'image/png' + elif path.endswith('.jpg'): + mimetype = 'image/jpeg' + elif path.endswith('.jpeg'): + mimetype = 'image/jpeg' + elif path.endswith('.gif'): + mimetype = 'image/gif' + elif path.endswith('.svg'): + mimetype = 'image/svg+xml' + elif path.endswith('.ico'): + mimetype = 'image/x-icon' + elif path.endswith('.json'): + mimetype = 'application/json' + elif path.endswith('.txt'): + mimetype = 'text/plain' + + return await quart.send_from_directory( + frontend_path, path, mimetype=mimetype + ) diff --git a/pkg/api/http/service/bot.py b/pkg/api/http/service/bot.py index 557b63f1..23e9fa5b 100644 --- a/pkg/api/http/service/bot.py +++ b/pkg/api/http/service/bot.py @@ -1,7 +1,6 @@ from __future__ import annotations import uuid -import datetime import sqlalchemy from ....core import app @@ -29,13 +28,15 @@ class BotService: self.ap.persistence_mgr.serialize_model(persistence_bot.Bot, bot) for bot in bots ] - + async def get_bot(self, bot_uuid: str) -> dict | None: """获取机器人""" result = await self.ap.persistence_mgr.execute_async( - sqlalchemy.select(persistence_bot.Bot).where(persistence_bot.Bot.uuid == bot_uuid) + sqlalchemy.select(persistence_bot.Bot).where( + persistence_bot.Bot.uuid == bot_uuid + ) ) - + bot = result.first() if bot is None: @@ -50,7 +51,9 @@ class BotService: # checkout the default pipeline result = await self.ap.persistence_mgr.execute_async( - sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(persistence_pipeline.LegacyPipeline.is_default == True) + sqlalchemy.select(persistence_pipeline.LegacyPipeline).where( + persistence_pipeline.LegacyPipeline.is_default == True + ) ) pipeline = result.first() if pipeline is not None: @@ -64,7 +67,7 @@ class BotService: bot = await self.get_bot(bot_data['uuid']) await self.ap.platform_mgr.load_bot(bot) - + return bot_data['uuid'] async def update_bot(self, bot_uuid: str, bot_data: dict) -> None: @@ -75,19 +78,24 @@ class BotService: # set use_pipeline_name if 'use_pipeline_uuid' in bot_data: result = await self.ap.persistence_mgr.execute_async( - sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(persistence_pipeline.LegacyPipeline.uuid == bot_data['use_pipeline_uuid']) + sqlalchemy.select(persistence_pipeline.LegacyPipeline).where( + persistence_pipeline.LegacyPipeline.uuid + == bot_data['use_pipeline_uuid'] + ) ) pipeline = result.first() if pipeline is not None: bot_data['use_pipeline_name'] = pipeline.name else: - raise Exception("Pipeline not found") + raise Exception('Pipeline not found') await self.ap.persistence_mgr.execute_async( - sqlalchemy.update(persistence_bot.Bot).values(bot_data).where(persistence_bot.Bot.uuid == bot_uuid) + sqlalchemy.update(persistence_bot.Bot) + .values(bot_data) + .where(persistence_bot.Bot.uuid == bot_uuid) ) await self.ap.platform_mgr.remove_bot(bot_uuid) - + # select from db bot = await self.get_bot(bot_uuid) @@ -100,7 +108,7 @@ class BotService: """删除机器人""" await self.ap.platform_mgr.remove_bot(bot_uuid) await self.ap.persistence_mgr.execute_async( - sqlalchemy.delete(persistence_bot.Bot).where(persistence_bot.Bot.uuid == bot_uuid) + sqlalchemy.delete(persistence_bot.Bot).where( + persistence_bot.Bot.uuid == bot_uuid + ) ) - - diff --git a/pkg/api/http/service/model.py b/pkg/api/http/service/model.py index e74e975d..8a71bf1c 100644 --- a/pkg/api/http/service/model.py +++ b/pkg/api/http/service/model.py @@ -1,7 +1,6 @@ from __future__ import annotations import uuid -import datetime import sqlalchemy from ....core import app @@ -10,7 +9,6 @@ from ....entity.persistence import pipeline as persistence_pipeline class ModelsService: - ap: app.Application def __init__(self, ap: app.Application) -> None: @@ -26,15 +24,12 @@ class ModelsService: self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, model) for model in models ] - - async def create_llm_model(self, model_data: dict) -> str: + async def create_llm_model(self, model_data: dict) -> str: model_data['uuid'] = str(uuid.uuid4()) await self.ap.persistence_mgr.execute_async( - sqlalchemy.insert(persistence_model.LLMModel).values( - **model_data - ) + sqlalchemy.insert(persistence_model.LLMModel).values(**model_data) ) llm_model = await self.get_llm_model(model_data['uuid']) @@ -43,22 +38,24 @@ class ModelsService: # check if default pipeline has no model bound result = await self.ap.persistence_mgr.execute_async( - sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(persistence_pipeline.LegacyPipeline.is_default == True) + sqlalchemy.select(persistence_pipeline.LegacyPipeline).where( + persistence_pipeline.LegacyPipeline.is_default == True + ) ) pipeline = result.first() if pipeline is not None and pipeline.config['ai']['local-agent']['model'] == '': pipeline_config = pipeline.config pipeline_config['ai']['local-agent']['model'] = model_data['uuid'] - pipeline_data = { - "config": pipeline_config - } - await self.ap.pipeline_service.update_pipeline(pipeline.uuid, pipeline_data) + pipeline_data = {'config': pipeline_config} + await self.ap.pipeline_service.update_pipeline(pipeline.uuid, pipeline_data) return model_data['uuid'] async def get_llm_model(self, model_uuid: str) -> dict | None: result = await self.ap.persistence_mgr.execute_async( - sqlalchemy.select(persistence_model.LLMModel).where(persistence_model.LLMModel.uuid == model_uuid) + sqlalchemy.select(persistence_model.LLMModel).where( + persistence_model.LLMModel.uuid == model_uuid + ) ) model = result.first() @@ -66,14 +63,18 @@ class ModelsService: if model is None: return None - return self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, model) + return self.ap.persistence_mgr.serialize_model( + persistence_model.LLMModel, model + ) async def update_llm_model(self, model_uuid: str, model_data: dict) -> None: if 'uuid' in model_data: del model_data['uuid'] - + await self.ap.persistence_mgr.execute_async( - sqlalchemy.update(persistence_model.LLMModel).where(persistence_model.LLMModel.uuid == model_uuid).values(**model_data) + sqlalchemy.update(persistence_model.LLMModel) + .where(persistence_model.LLMModel.uuid == model_uuid) + .values(**model_data) ) await self.ap.model_mgr.remove_llm_model(model_uuid) @@ -84,7 +85,9 @@ class ModelsService: async def delete_llm_model(self, model_uuid: str) -> None: await self.ap.persistence_mgr.execute_async( - sqlalchemy.delete(persistence_model.LLMModel).where(persistence_model.LLMModel.uuid == model_uuid) + sqlalchemy.delete(persistence_model.LLMModel).where( + persistence_model.LLMModel.uuid == model_uuid + ) ) await self.ap.model_mgr.remove_llm_model(model_uuid) diff --git a/pkg/api/http/service/pipeline.py b/pkg/api/http/service/pipeline.py index 6e8a66dc..0dd73ef2 100644 --- a/pkg/api/http/service/pipeline.py +++ b/pkg/api/http/service/pipeline.py @@ -2,7 +2,6 @@ from __future__ import annotations import uuid import json -import datetime import sqlalchemy from ....core import app @@ -10,69 +9,79 @@ from ....entity.persistence import pipeline as persistence_pipeline default_stage_order = [ - "GroupRespondRuleCheckStage", # 群响应规则检查 - "BanSessionCheckStage", # 封禁会话检查 - "PreContentFilterStage", # 内容过滤前置阶段 - "PreProcessor", # 预处理器 - "ConversationMessageTruncator", # 会话消息截断器 - "RequireRateLimitOccupancy", # 请求速率限制占用 - "MessageProcessor", # 处理器 - "ReleaseRateLimitOccupancy", # 释放速率限制占用 - "PostContentFilterStage", # 内容过滤后置阶段 - "ResponseWrapper", # 响应包装器 - "LongTextProcessStage", # 长文本处理 - "SendResponseBackStage", # 发送响应 + 'GroupRespondRuleCheckStage', # 群响应规则检查 + 'BanSessionCheckStage', # 封禁会话检查 + 'PreContentFilterStage', # 内容过滤前置阶段 + 'PreProcessor', # 预处理器 + 'ConversationMessageTruncator', # 会话消息截断器 + 'RequireRateLimitOccupancy', # 请求速率限制占用 + 'MessageProcessor', # 处理器 + 'ReleaseRateLimitOccupancy', # 释放速率限制占用 + 'PostContentFilterStage', # 内容过滤后置阶段 + 'ResponseWrapper', # 响应包装器 + 'LongTextProcessStage', # 长文本处理 + 'SendResponseBackStage', # 发送响应 ] class PipelineService: ap: app.Application - + def __init__(self, ap: app.Application) -> None: self.ap = ap - + async def get_pipeline_metadata(self) -> dict: return [ self.ap.pipeline_config_meta_trigger.data, self.ap.pipeline_config_meta_safety.data, self.ap.pipeline_config_meta_ai.data, - self.ap.pipeline_config_meta_output.data + self.ap.pipeline_config_meta_output.data, ] async def get_pipelines(self) -> list[dict]: result = await self.ap.persistence_mgr.execute_async( sqlalchemy.select(persistence_pipeline.LegacyPipeline) ) - + pipelines = result.all() return [ - self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline) + self.ap.persistence_mgr.serialize_model( + persistence_pipeline.LegacyPipeline, pipeline + ) for pipeline in pipelines ] - + async def get_pipeline(self, pipeline_uuid: str) -> dict | None: result = await self.ap.persistence_mgr.execute_async( - sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(persistence_pipeline.LegacyPipeline.uuid == pipeline_uuid) + sqlalchemy.select(persistence_pipeline.LegacyPipeline).where( + persistence_pipeline.LegacyPipeline.uuid == pipeline_uuid + ) ) - + pipeline = result.first() if pipeline is None: return None - return self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline) + return self.ap.persistence_mgr.serialize_model( + persistence_pipeline.LegacyPipeline, pipeline + ) async def create_pipeline(self, pipeline_data: dict, default: bool = False) -> str: pipeline_data['uuid'] = str(uuid.uuid4()) pipeline_data['for_version'] = self.ap.ver_mgr.get_current_version() pipeline_data['stages'] = default_stage_order.copy() pipeline_data['is_default'] = default - pipeline_data['config'] = json.load(open('templates/default-pipeline-config.json', 'r', encoding='utf-8')) + pipeline_data['config'] = json.load( + open('templates/default-pipeline-config.json', 'r', encoding='utf-8') + ) await self.ap.persistence_mgr.execute_async( - sqlalchemy.insert(persistence_pipeline.LegacyPipeline).values(**pipeline_data) + sqlalchemy.insert(persistence_pipeline.LegacyPipeline).values( + **pipeline_data + ) ) - + pipeline = await self.get_pipeline(pipeline_data['uuid']) await self.ap.pipeline_mgr.load_pipeline(pipeline) @@ -90,7 +99,9 @@ class PipelineService: del pipeline_data['is_default'] await self.ap.persistence_mgr.execute_async( - sqlalchemy.update(persistence_pipeline.LegacyPipeline).where(persistence_pipeline.LegacyPipeline.uuid == pipeline_uuid).values(**pipeline_data) + sqlalchemy.update(persistence_pipeline.LegacyPipeline) + .where(persistence_pipeline.LegacyPipeline.uuid == pipeline_uuid) + .values(**pipeline_data) ) await self.ap.pipeline_mgr.remove_pipeline(pipeline_uuid) @@ -101,6 +112,8 @@ class PipelineService: async def delete_pipeline(self, pipeline_uuid: str) -> None: await self.ap.persistence_mgr.execute_async( - sqlalchemy.delete(persistence_pipeline.LegacyPipeline).where(persistence_pipeline.LegacyPipeline.uuid == pipeline_uuid) + sqlalchemy.delete(persistence_pipeline.LegacyPipeline).where( + persistence_pipeline.LegacyPipeline.uuid == pipeline_uuid + ) ) await self.ap.pipeline_mgr.remove_pipeline(pipeline_uuid) diff --git a/pkg/api/http/service/user.py b/pkg/api/http/service/user.py index 4b31f1c0..2edfd874 100644 --- a/pkg/api/http/service/user.py +++ b/pkg/api/http/service/user.py @@ -11,7 +11,6 @@ from ....utils import constants class UserService: - ap: app.Application def __init__(self, ap: app.Application) -> None: @@ -24,7 +23,7 @@ class UserService: result_list = result.all() return result_list is not None and len(result_list) > 0 - + async def create_user(self, user_email: str, password: str) -> None: ph = argon2.PasswordHasher() @@ -32,8 +31,7 @@ class UserService: await self.ap.persistence_mgr.execute_async( sqlalchemy.insert(user.User).values( - user=user_email, - password=hashed_password + user=user_email, password=hashed_password ) ) @@ -61,12 +59,12 @@ class UserService: payload = { 'user': user_email, - 'iss': 'LangBot-'+constants.edition, - 'exp': datetime.datetime.now() + datetime.timedelta(seconds=jwt_expire) + 'iss': 'LangBot-' + constants.edition, + 'exp': datetime.datetime.now() + datetime.timedelta(seconds=jwt_expire), } return jwt.encode(payload, jwt_secret, algorithm='HS256') - + async def verify_jwt_token(self, token: str) -> str: jwt_secret = self.ap.instance_config.data['system']['jwt']['secret'] diff --git a/pkg/audit/__init__.py b/pkg/audit/__init__.py index c1a8353b..5f89c8b8 100644 --- a/pkg/audit/__init__.py +++ b/pkg/audit/__init__.py @@ -1,3 +1,3 @@ """ 审计相关操作 -""" \ No newline at end of file +""" diff --git a/pkg/audit/center/apigroup.py b/pkg/audit/center/apigroup.py index 4b20a09a..ac5e5117 100644 --- a/pkg/audit/center/apigroup.py +++ b/pkg/audit/center/apigroup.py @@ -3,11 +3,9 @@ from __future__ import annotations import abc import uuid import json -import logging import asyncio import aiohttp -import requests from ...core import app, entities as core_entities @@ -38,22 +36,22 @@ class APIGroup(metaclass=abc.ABCMeta): """ 执行请求 """ - self._runtime_info["account_id"] = "-1" + self._runtime_info['account_id'] = '-1' url = self.prefix + path data = json.dumps(data) - headers["Content-Type"] = "application/json" + headers['Content-Type'] = 'application/json' try: async with aiohttp.ClientSession() as session: async with session.request( method, url, data=data, params=params, headers=headers, **kwargs ) as resp: - self.ap.logger.debug("data: %s", data) - self.ap.logger.debug("ret: %s", await resp.text()) + self.ap.logger.debug('data: %s', data) + self.ap.logger.debug('ret: %s', await resp.text()) except Exception as e: - self.ap.logger.debug(f"上报失败: {e}") + self.ap.logger.debug(f'上报失败: {e}') async def do( self, @@ -68,8 +66,8 @@ class APIGroup(metaclass=abc.ABCMeta): return self.ap.task_mgr.create_task( self._do(method, path, data, params, headers, **kwargs), - kind="telemetry-operation", - name=f"{method} {path}", + kind='telemetry-operation', + name=f'{method} {path}', scopes=[core_entities.LifecycleControlScope.APPLICATION], ).task @@ -80,7 +78,7 @@ class APIGroup(metaclass=abc.ABCMeta): def basic_info(self): """获取基本信息""" basic_info = APIGroup._basic_info.copy() - basic_info["rid"] = self.gen_rid() + basic_info['rid'] = self.gen_rid() return basic_info def runtime_info(self): diff --git a/pkg/audit/center/groups/main.py b/pkg/audit/center/groups/main.py index 854437a1..2c2302d1 100644 --- a/pkg/audit/center/groups/main.py +++ b/pkg/audit/center/groups/main.py @@ -9,7 +9,7 @@ class V2MainDataAPI(apigroup.APIGroup): def __init__(self, prefix: str, ap: app.Application): self.ap = ap - super().__init__(prefix+"/main", ap) + super().__init__(prefix + '/main', ap) async def do(self, *args, **kwargs): if not self.ap.instance_config.data['telemetry']['report']: @@ -25,31 +25,31 @@ class V2MainDataAPI(apigroup.APIGroup): ): """提交更新记录""" return await self.do( - "POST", - "/update", + 'POST', + '/update', data={ - "basic": self.basic_info(), - "update_info": { - "spent_seconds": spent_seconds, - "infer_reason": infer_reason, - "old_version": old_version, - "new_version": new_version, - } - } + 'basic': self.basic_info(), + 'update_info': { + 'spent_seconds': spent_seconds, + 'infer_reason': infer_reason, + 'old_version': old_version, + 'new_version': new_version, + }, + }, ) - + async def post_announcement_showed( self, ids: list[int], ): """提交公告已阅""" return await self.do( - "POST", - "/announcement", + 'POST', + '/announcement', data={ - "basic": self.basic_info(), - "announcement_info": { - "ids": ids, - } - } + 'basic': self.basic_info(), + 'announcement_info': { + 'ids': ids, + }, + }, ) diff --git a/pkg/audit/center/groups/plugin.py b/pkg/audit/center/groups/plugin.py index d6ed0b02..4978e5d0 100644 --- a/pkg/audit/center/groups/plugin.py +++ b/pkg/audit/center/groups/plugin.py @@ -9,39 +9,33 @@ class V2PluginDataAPI(apigroup.APIGroup): def __init__(self, prefix: str, ap: app.Application): self.ap = ap - super().__init__(prefix+"/plugin", ap) + super().__init__(prefix + '/plugin', ap) async def do(self, *args, **kwargs): if not self.ap.instance_config.data['telemetry']['report']: return None return await super().do(*args, **kwargs) - async def post_install_record( - self, - plugin: dict - ): + async def post_install_record(self, plugin: dict): """提交插件安装记录""" return await self.do( - "POST", - "/install", + 'POST', + '/install', data={ - "basic": self.basic_info(), - "plugin": plugin, - } + 'basic': self.basic_info(), + 'plugin': plugin, + }, ) - async def post_remove_record( - self, - plugin: dict - ): + async def post_remove_record(self, plugin: dict): """提交插件卸载记录""" return await self.do( - "POST", - "/remove", + 'POST', + '/remove', data={ - "basic": self.basic_info(), - "plugin": plugin, - } + 'basic': self.basic_info(), + 'plugin': plugin, + }, ) async def post_update_record( @@ -52,14 +46,14 @@ class V2PluginDataAPI(apigroup.APIGroup): ): """提交插件更新记录""" return await self.do( - "POST", - "/update", + 'POST', + '/update', data={ - "basic": self.basic_info(), - "plugin": plugin, - "update_info": { - "old_version": old_version, - "new_version": new_version, - } - } + 'basic': self.basic_info(), + 'plugin': plugin, + 'update_info': { + 'old_version': old_version, + 'new_version': new_version, + }, + }, ) diff --git a/pkg/audit/center/groups/usage.py b/pkg/audit/center/groups/usage.py index 79bc56f5..bdbb27eb 100644 --- a/pkg/audit/center/groups/usage.py +++ b/pkg/audit/center/groups/usage.py @@ -9,7 +9,7 @@ class V2UsageDataAPI(apigroup.APIGroup): def __init__(self, prefix: str, ap: app.Application): self.ap = ap - super().__init__(prefix+"/usage", ap) + super().__init__(prefix + '/usage', ap) async def do(self, *args, **kwargs): if not self.ap.instance_config.data['telemetry']['report']: @@ -28,25 +28,25 @@ class V2UsageDataAPI(apigroup.APIGroup): ): """提交请求记录""" return await self.do( - "POST", - "/query", + 'POST', + '/query', data={ - "basic": self.basic_info(), - "runtime": self.runtime_info(), - "session_info": { - "type": session_type, - "id": session_id, + 'basic': self.basic_info(), + 'runtime': self.runtime_info(), + 'session_info': { + 'type': session_type, + 'id': session_id, }, - "query_info": { - "ability_provider": query_ability_provider, - "usage": usage, - "model_name": model_name, - "response_seconds": response_seconds, - "retry_times": retry_times, - } - } + 'query_info': { + 'ability_provider': query_ability_provider, + 'usage': usage, + 'model_name': model_name, + 'response_seconds': response_seconds, + 'retry_times': retry_times, + }, + }, ) - + async def post_event_record( self, plugins: list[dict], @@ -54,18 +54,18 @@ class V2UsageDataAPI(apigroup.APIGroup): ): """提交事件触发记录""" return await self.do( - "POST", - "/event", + 'POST', + '/event', data={ - "basic": self.basic_info(), - "runtime": self.runtime_info(), - "plugins": plugins, - "event_info": { - "name": event_name, - } - } + 'basic': self.basic_info(), + 'runtime': self.runtime_info(), + 'plugins': plugins, + 'event_info': { + 'name': event_name, + }, + }, ) - + async def post_function_record( self, plugin: dict, @@ -74,15 +74,14 @@ class V2UsageDataAPI(apigroup.APIGroup): ): """提交内容函数使用记录""" return await self.do( - "POST", - "/function", + 'POST', + '/function', data={ - "basic": self.basic_info(), - "plugin": plugin, - "function_info": { - "name": function_name, - "description": function_description, - } - } + 'basic': self.basic_info(), + 'plugin': plugin, + 'function_info': { + 'name': function_name, + 'description': function_description, + }, + }, ) - diff --git a/pkg/audit/center/v2.py b/pkg/audit/center/v2.py index 234e6d22..e9df6f91 100644 --- a/pkg/audit/center/v2.py +++ b/pkg/audit/center/v2.py @@ -11,7 +11,7 @@ from ...core import app class V2CenterAPI: """中央服务器 v2 API 交互类""" - + main: main.V2MainDataAPI = None """主 API 组""" @@ -21,15 +21,20 @@ class V2CenterAPI: plugin: plugin.V2PluginDataAPI = None """插件 API 组""" - def __init__(self, ap: app.Application, backend_url: str, basic_info: dict = None, runtime_info: dict = None): + def __init__( + self, + ap: app.Application, + backend_url: str, + basic_info: dict = None, + runtime_info: dict = None, + ): """初始化""" - logging.debug("basic_info: %s, runtime_info: %s", basic_info, runtime_info) - + logging.debug('basic_info: %s, runtime_info: %s', basic_info, runtime_info) + apigroup.APIGroup._basic_info = basic_info apigroup.APIGroup._runtime_info = runtime_info self.main = main.V2MainDataAPI(backend_url, ap) self.usage = usage.V2UsageDataAPI(backend_url, ap) self.plugin = plugin.V2PluginDataAPI(backend_url, ap) - diff --git a/pkg/audit/identifier.py b/pkg/audit/identifier.py index 3e2ec57d..b10d093c 100644 --- a/pkg/audit/identifier.py +++ b/pkg/audit/identifier.py @@ -16,6 +16,7 @@ identifier = { HOST_ID_FILE = os.path.expanduser('~/.langbot/host_id.json') INSTANCE_ID_FILE = 'data/labels/instance_id.json' + def init(): global identifier @@ -23,14 +24,11 @@ def init(): os.mkdir(os.path.expanduser('~/.langbot')) if not os.path.exists(HOST_ID_FILE): - new_host_id = 'host_'+str(uuid.uuid4()) + new_host_id = 'host_' + str(uuid.uuid4()) new_host_create_ts = int(time.time()) with open(HOST_ID_FILE, 'w') as f: - json.dump({ - 'host_id': new_host_id, - 'host_create_ts': new_host_create_ts - }, f) + json.dump({'host_id': new_host_id, 'host_create_ts': new_host_create_ts}, f) identifier['host_id'] = new_host_id identifier['host_create_ts'] = new_host_create_ts @@ -51,20 +49,25 @@ def init(): instance_id = {} with open(INSTANCE_ID_FILE, 'r') as f: instance_id = json.load(f) - - if instance_id['host_id'] != identifier['host_id']: # 如果实例 id 不是当前主机的,删除 + + if ( + instance_id['host_id'] != identifier['host_id'] + ): # 如果实例 id 不是当前主机的,删除 os.remove(INSTANCE_ID_FILE) if not os.path.exists(INSTANCE_ID_FILE): - new_instance_id = 'instance_'+str(uuid.uuid4()) + new_instance_id = 'instance_' + str(uuid.uuid4()) new_instance_create_ts = int(time.time()) with open(INSTANCE_ID_FILE, 'w') as f: - json.dump({ - 'host_id': identifier['host_id'], - 'instance_id': new_instance_id, - 'instance_create_ts': new_instance_create_ts - }, f) + json.dump( + { + 'host_id': identifier['host_id'], + 'instance_id': new_instance_id, + 'instance_create_ts': new_instance_create_ts, + }, + f, + ) identifier['instance_id'] = new_instance_id identifier['instance_create_ts'] = new_instance_create_ts @@ -80,6 +83,7 @@ def init(): identifier['instance_id'] = loaded_instance_id identifier['instance_create_ts'] = loaded_instance_create_ts + def print_out(): global identifier print(identifier) diff --git a/pkg/command/cmdmgr.py b/pkg/command/cmdmgr.py index 3275b6fc..10d76067 100644 --- a/pkg/command/cmdmgr.py +++ b/pkg/command/cmdmgr.py @@ -3,17 +3,17 @@ from __future__ import annotations import typing from ..core import app, entities as core_entities -from ..provider import entities as llm_entities from . import entities, operator, errors -from ..config import manager as cfg_mgr +from ..utils import importutil # 引入所有算子以便注册 -from .operators import func, plugin, reset, list as list_cmd, last, next, delc, resend, prompt, cmd, help, version, update, ollama, model +from . import operators + +importutil.import_modules_in_pkg(operators) class CommandManager: - """命令管理器 - """ + """命令管理器""" ap: app.Application @@ -26,14 +26,13 @@ class CommandManager: self.ap = ap async def initialize(self): - # 设置各个类的路径 def set_path(cls: operator.CommandOperator, ancestors: list[str]): cls.path = '.'.join(ancestors + [cls.name]) for op in operator.preregistered_operators: if op.parent_class == cls: set_path(op, ancestors + [cls.name]) - + for cls in operator.preregistered_operators: if cls.parent_class is None: set_path(cls, []) @@ -41,14 +40,18 @@ class CommandManager: # 应用命令权限配置 for cls in operator.preregistered_operators: if cls.path in self.ap.instance_config.data['command']['privilege']: - cls.lowest_privilege = self.ap.instance_config.data['command']['privilege'][cls.path] + cls.lowest_privilege = self.ap.instance_config.data['command'][ + 'privilege' + ][cls.path] # 实例化所有类 self.cmd_list = [cls(self.ap) for cls in operator.preregistered_operators] # 设置所有类的子节点 for cmd in self.cmd_list: - cmd.children = [child for child in self.cmd_list if child.parent_class == cmd.__class__] + cmd.children = [ + child for child in self.cmd_list if child.parent_class == cmd.__class__ + ] # 初始化所有类 for cmd in self.cmd_list: @@ -58,27 +61,25 @@ class CommandManager: self, context: entities.ExecuteContext, operator_list: list[operator.CommandOperator], - operator: operator.CommandOperator = None + operator: operator.CommandOperator = None, ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - """执行命令 - """ + """执行命令""" found = False if len(context.crt_params) > 0: # 查找下一个参数是否对应此节点的某个子节点名 for oper in operator_list: - if (context.crt_params[0] == oper.name \ - or context.crt_params[0] in oper.alias) \ - and (oper.parent_class is None or oper.parent_class == operator.__class__): + if ( + context.crt_params[0] == oper.name + or context.crt_params[0] in oper.alias + ) and ( + oper.parent_class is None or oper.parent_class == operator.__class__ + ): found = True context.crt_command = context.crt_params[0] context.crt_params = context.crt_params[1:] - async for ret in self._execute( - context, - oper.children, - oper - ): + async for ret in self._execute(context, oper.children, oper): yield ret break @@ -96,19 +97,20 @@ class CommandManager: async for ret in operator.execute(context): yield ret - async def execute( self, command_text: str, query: core_entities.Query, - session: core_entities.Session + session: core_entities.Session, ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - """执行命令 - """ + """执行命令""" privilege = 1 - if f'{query.launcher_type.value}_{query.launcher_id}' in self.ap.instance_config.data['admins']: + if ( + f'{query.launcher_type.value}_{query.launcher_id}' + in self.ap.instance_config.data['admins'] + ): privilege = 2 ctx = entities.ExecuteContext( @@ -119,11 +121,8 @@ class CommandManager: crt_command='', params=command_text.split(' '), crt_params=command_text.split(' '), - privilege=privilege + privilege=privilege, ) - async for ret in self._execute( - ctx, - self.cmd_list - ): + async for ret in self._execute(ctx, self.cmd_list): yield ret diff --git a/pkg/command/entities.py b/pkg/command/entities.py index 538766bf..cccd588e 100644 --- a/pkg/command/entities.py +++ b/pkg/command/entities.py @@ -4,14 +4,13 @@ import typing import pydantic.v1 as pydantic -from ..core import app, entities as core_entities -from . import errors, operator +from ..core import entities as core_entities +from . import errors from ..platform.types import message as platform_message class CommandReturn(pydantic.BaseModel): - """命令返回值 - """ + """命令返回值""" text: typing.Optional[str] = None """文本 @@ -24,7 +23,7 @@ class CommandReturn(pydantic.BaseModel): """图片链接 """ - error: typing.Optional[errors.CommandError]= None + error: typing.Optional[errors.CommandError] = None """错误 """ @@ -33,8 +32,7 @@ class CommandReturn(pydantic.BaseModel): class ExecuteContext(pydantic.BaseModel): - """单次命令执行上下文 - """ + """单次命令执行上下文""" query: core_entities.Query """本次消息的请求对象""" diff --git a/pkg/command/errors.py b/pkg/command/errors.py index 5bc253f6..df05b3d1 100644 --- a/pkg/command/errors.py +++ b/pkg/command/errors.py @@ -1,33 +1,26 @@ - - class CommandError(Exception): - def __init__(self, message: str = None): self.message = message - + def __str__(self): return self.message class CommandNotFoundError(CommandError): - def __init__(self, message: str = None): - super().__init__("未知命令: "+message) + super().__init__('未知命令: ' + message) class CommandPrivilegeError(CommandError): - def __init__(self, message: str = None): - super().__init__("权限不足: "+message) + super().__init__('权限不足: ' + message) class ParamNotEnoughError(CommandError): - def __init__(self, message: str = None): - super().__init__("参数不足: "+message) + super().__init__('参数不足: ' + message) class CommandOperationError(CommandError): - def __init__(self, message: str = None): - super().__init__("操作失败: "+message) + super().__init__('操作失败: ' + message) diff --git a/pkg/command/operator.py b/pkg/command/operator.py index 5e3b1a8f..7072edf7 100644 --- a/pkg/command/operator.py +++ b/pkg/command/operator.py @@ -3,7 +3,7 @@ from __future__ import annotations import typing import abc -from ..core import app, entities as core_entities +from ..core import app from . import entities @@ -13,14 +13,14 @@ preregistered_operators: list[typing.Type[CommandOperator]] = [] def operator_class( name: str, - help: str = "", + help: str = '', usage: str = None, alias: list[str] = [], - privilege: int=1, # 1为普通用户,2为管理员 - parent_class: typing.Type[CommandOperator] = None + privilege: int = 1, # 1为普通用户,2为管理员 + parent_class: typing.Type[CommandOperator] = None, ) -> typing.Callable[[typing.Type[CommandOperator]], typing.Type[CommandOperator]]: """命令类装饰器 - + Args: name (str): 名称 help (str, optional): 帮助信息. Defaults to "". @@ -35,7 +35,7 @@ def operator_class( def decorator(cls: typing.Type[CommandOperator]) -> typing.Type[CommandOperator]: assert issubclass(cls, CommandOperator) - + cls.name = name cls.alias = alias cls.help = help @@ -96,14 +96,13 @@ class CommandOperator(metaclass=abc.ABCMeta): @abc.abstractmethod async def execute( - self, - context: entities.ExecuteContext + self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: """实现此方法以执行命令 支持多次yield以返回多个结果。 例如:一个安装插件的命令,可能会有下载、解压、安装等多个步骤,每个步骤都可以返回一个结果。 - + Args: context (entities.ExecuteContext): 命令执行上下文 diff --git a/pkg/command/operators/cmd.py b/pkg/command/operators/cmd.py index 17b5ed08..a13d5b35 100644 --- a/pkg/command/operators/cmd.py +++ b/pkg/command/operators/cmd.py @@ -2,49 +2,46 @@ from __future__ import annotations import typing -from .. import operator, entities, cmdmgr, errors +from .. import operator, entities, errors -@operator.operator_class( - name="cmd", - help='显示命令列表', - usage='!cmd\n!cmd <命令名称>' -) +@operator.operator_class(name='cmd', help='显示命令列表', usage='!cmd\n!cmd <命令名称>') class CmdOperator(operator.CommandOperator): - """命令列表 - """ + """命令列表""" async def execute( - self, - context: entities.ExecuteContext + self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - """执行 - """ + """执行""" if len(context.crt_params) == 0: - reply_str = "当前所有命令: \n\n" + reply_str = '当前所有命令: \n\n' for cmd in self.ap.cmd_mgr.cmd_list: if cmd.parent_class is None: - reply_str += f"{cmd.name}: {cmd.help}\n" - - reply_str += "\n使用 !cmd <命令名称> 查看命令的详细帮助" + reply_str += f'{cmd.name}: {cmd.help}\n' + + reply_str += '\n使用 !cmd <命令名称> 查看命令的详细帮助' yield entities.CommandReturn(text=reply_str.strip()) - + else: cmd_name = context.crt_params[0] cmd = None for _cmd in self.ap.cmd_mgr.cmd_list: - if (cmd_name == _cmd.name or cmd_name in _cmd.alias) and (_cmd.parent_class is None): + if (cmd_name == _cmd.name or cmd_name in _cmd.alias) and ( + _cmd.parent_class is None + ): cmd = _cmd break if cmd is None: - yield entities.CommandReturn(error=errors.CommandNotFoundError(cmd_name)) + yield entities.CommandReturn( + error=errors.CommandNotFoundError(cmd_name) + ) else: - reply_str = f"{cmd.name}: {cmd.help}\n\n" - reply_str += f"使用方法: \n{cmd.usage}" + reply_str = f'{cmd.name}: {cmd.help}\n\n' + reply_str += f'使用方法: \n{cmd.usage}' yield entities.CommandReturn(text=reply_str.strip()) diff --git a/pkg/command/operators/delc.py b/pkg/command/operators/delc.py index db865ff7..9ae507f5 100644 --- a/pkg/command/operators/delc.py +++ b/pkg/command/operators/delc.py @@ -1,62 +1,60 @@ from __future__ import annotations import typing -import datetime -from .. import operator, entities, cmdmgr, errors +from .. import operator, entities, errors @operator.operator_class( - name="del", - help="删除当前会话的历史记录", - usage='!del <序号>\n!del all' + name='del', help='删除当前会话的历史记录', usage='!del <序号>\n!del all' ) class DelOperator(operator.CommandOperator): - async def execute( - self, - context: entities.ExecuteContext + self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - if context.session.conversations: delete_index = 0 if len(context.crt_params) > 0: try: delete_index = int(context.crt_params[0]) - except: - yield entities.CommandReturn(error=errors.CommandOperationError('索引必须是整数')) + except Exception: + yield entities.CommandReturn( + error=errors.CommandOperationError('索引必须是整数') + ) return - - if delete_index < 0 or delete_index >= len(context.session.conversations): - yield entities.CommandReturn(error=errors.CommandOperationError('索引超出范围')) - return - - # 倒序 - to_delete_index = len(context.session.conversations)-1-delete_index - if context.session.conversations[to_delete_index] == context.session.using_conversation: + if delete_index < 0 or delete_index >= len(context.session.conversations): + yield entities.CommandReturn( + error=errors.CommandOperationError('索引超出范围') + ) + return + + # 倒序 + to_delete_index = len(context.session.conversations) - 1 - delete_index + + if ( + context.session.conversations[to_delete_index] + == context.session.using_conversation + ): context.session.using_conversation = None del context.session.conversations[to_delete_index] - yield entities.CommandReturn(text=f"已删除对话: {delete_index}") + yield entities.CommandReturn(text=f'已删除对话: {delete_index}') else: - yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话')) + yield entities.CommandReturn( + error=errors.CommandOperationError('当前没有对话') + ) @operator.operator_class( - name="all", - help="删除此会话的所有历史记录", - parent_class=DelOperator + name='all', help='删除此会话的所有历史记录', parent_class=DelOperator ) class DelAllOperator(operator.CommandOperator): - async def execute( - self, - context: entities.ExecuteContext + self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - context.session.conversations = [] context.session.using_conversation = None - yield entities.CommandReturn(text="已删除所有对话") \ No newline at end of file + yield entities.CommandReturn(text='已删除所有对话') diff --git a/pkg/command/operators/func.py b/pkg/command/operators/func.py index ae2ba4c1..9cb3fd32 100644 --- a/pkg/command/operators/func.py +++ b/pkg/command/operators/func.py @@ -1,16 +1,15 @@ from __future__ import annotations from typing import AsyncGenerator -from .. import operator, entities, cmdmgr -from ...plugin import context as plugin_context +from .. import operator, entities -@operator.operator_class(name="func", help="查看所有已注册的内容函数", usage='!func') +@operator.operator_class(name='func', help='查看所有已注册的内容函数', usage='!func') class FuncOperator(operator.CommandOperator): async def execute( self, context: entities.ExecuteContext ) -> AsyncGenerator[entities.CommandReturn, None]: - reply_str = "当前已启用的内容函数: \n\n" + reply_str = '当前已启用的内容函数: \n\n' index = 1 @@ -19,7 +18,7 @@ class FuncOperator(operator.CommandOperator): ) for func in all_functions: - reply_str += "{}. {}:\n{}\n\n".format( + reply_str += '{}. {}:\n{}\n\n'.format( index, func.name, func.description, diff --git a/pkg/command/operators/help.py b/pkg/command/operators/help.py index d8b42137..c718d4b9 100644 --- a/pkg/command/operators/help.py +++ b/pkg/command/operators/help.py @@ -2,19 +2,13 @@ from __future__ import annotations import typing -from .. import operator, entities, cmdmgr, errors +from .. import operator, entities -@operator.operator_class( - name='help', - help='显示帮助', - usage='!help\n!help <命令名称>' -) +@operator.operator_class(name='help', help='显示帮助', usage='!help\n!help <命令名称>') class HelpOperator(operator.CommandOperator): - async def execute( - self, - context: entities.ExecuteContext + self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: help = 'LangBot - 大语言模型原生即时通信机器人平台\n链接:https://langbot.app' diff --git a/pkg/command/operators/last.py b/pkg/command/operators/last.py index e7a14c83..7e2f2453 100644 --- a/pkg/command/operators/last.py +++ b/pkg/command/operators/last.py @@ -1,36 +1,43 @@ from __future__ import annotations import typing -import datetime -from .. import operator, entities, cmdmgr, errors +from .. import operator, entities, errors -@operator.operator_class( - name="last", - help="切换到前一个对话", - usage='!last' -) +@operator.operator_class(name='last', help='切换到前一个对话', usage='!last') class LastOperator(operator.CommandOperator): - async def execute( - self, - context: entities.ExecuteContext + self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - if context.session.conversations: # 找到当前会话的上一个会话 - for index in range(len(context.session.conversations)-1, -1, -1): - if context.session.conversations[index] == context.session.using_conversation: + for index in range(len(context.session.conversations) - 1, -1, -1): + if ( + context.session.conversations[index] + == context.session.using_conversation + ): if index == 0: - yield entities.CommandReturn(error=errors.CommandOperationError('已经是第一个对话了')) + yield entities.CommandReturn( + error=errors.CommandOperationError('已经是第一个对话了') + ) return else: - context.session.using_conversation = context.session.conversations[index-1] - time_str = context.session.using_conversation.create_time.strftime("%Y-%m-%d %H:%M:%S") + context.session.using_conversation = ( + context.session.conversations[index - 1] + ) + time_str = ( + context.session.using_conversation.create_time.strftime( + '%Y-%m-%d %H:%M:%S' + ) + ) - yield entities.CommandReturn(text=f"已切换到上一个对话: {index} {time_str}: {context.session.using_conversation.messages[0].readable_str()}") + yield entities.CommandReturn( + text=f'已切换到上一个对话: {index} {time_str}: {context.session.using_conversation.messages[0].readable_str()}' + ) return else: - yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话')) \ No newline at end of file + yield entities.CommandReturn( + error=errors.CommandOperationError('当前没有对话') + ) diff --git a/pkg/command/operators/list.py b/pkg/command/operators/list.py index ff90d4dd..1aa63c94 100644 --- a/pkg/command/operators/list.py +++ b/pkg/command/operators/list.py @@ -1,30 +1,26 @@ from __future__ import annotations import typing -import datetime -from .. import operator, entities, cmdmgr, errors +from .. import operator, entities, errors @operator.operator_class( - name="list", - help="列出此会话中的所有历史对话", - usage='!list\n!list <页码>' + name='list', help='列出此会话中的所有历史对话', usage='!list\n!list <页码>' ) class ListOperator(operator.CommandOperator): - async def execute( - self, - context: entities.ExecuteContext + self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - page = 0 if len(context.crt_params) > 0: try: - page = int(context.crt_params[0]-1) - except: - yield entities.CommandReturn(error=errors.CommandOperationError('页码应为整数')) + page = int(context.crt_params[0] - 1) + except Exception: + yield entities.CommandReturn( + error=errors.CommandOperationError('页码应为整数') + ) return record_per_page = 10 @@ -36,21 +32,21 @@ class ListOperator(operator.CommandOperator): using_conv_index = 0 for conv in context.session.conversations[::-1]: - time_str = conv.create_time.strftime("%Y-%m-%d %H:%M:%S") + time_str = conv.create_time.strftime('%Y-%m-%d %H:%M:%S') if conv == context.session.using_conversation: using_conv_index = index if index >= page * record_per_page and index < (page + 1) * record_per_page: - content += f"{index} {time_str}: {conv.messages[0].readable_str() if len(conv.messages) > 0 else '无内容'}\n" + content += f'{index} {time_str}: {conv.messages[0].readable_str() if len(conv.messages) > 0 else "无内容"}\n' index += 1 if content == '': content = '无' else: if context.session.using_conversation is None: - content += "\n当前处于新会话" + content += '\n当前处于新会话' else: - content += f"\n当前会话: {using_conv_index} {context.session.using_conversation.create_time.strftime('%Y-%m-%d %H:%M:%S')}: {context.session.using_conversation.messages[0].readable_str() if len(context.session.using_conversation.messages) > 0 else '无内容'}" - - yield entities.CommandReturn(text=f"第 {page + 1} 页 (时间倒序):\n{content}") + content += f'\n当前会话: {using_conv_index} {context.session.using_conversation.create_time.strftime("%Y-%m-%d %H:%M:%S")}: {context.session.using_conversation.messages[0].readable_str() if len(context.session.using_conversation.messages) > 0 else "无内容"}' + + yield entities.CommandReturn(text=f'第 {page + 1} 页 (时间倒序):\n{content}') diff --git a/pkg/command/operators/model.py b/pkg/command/operators/model.py index f46c9590..cc3ef5b9 100644 --- a/pkg/command/operators/model.py +++ b/pkg/command/operators/model.py @@ -2,42 +2,44 @@ from __future__ import annotations import typing -from .. import operator, entities, cmdmgr, errors +from .. import operator, entities, errors + @operator.operator_class( - name="model", + name='model', help='显示和切换模型列表', usage='!model\n!model show <模型名>\n!model set <模型名>', - privilege=2 + privilege=2, ) class ModelOperator(operator.CommandOperator): """Model命令""" - - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + + async def execute( + self, context: entities.ExecuteContext + ) -> typing.AsyncGenerator[entities.CommandReturn, None]: content = '模型列表:\n' model_list = self.ap.model_mgr.model_list for model in model_list: - content += f"\n名称: {model.name}\n" - content += f"请求器: {model.requester.name}\n" + content += f'\n名称: {model.name}\n' + content += f'请求器: {model.requester.name}\n' - content += f"\n当前对话使用模型: {context.query.use_model.name}\n" - content += f"新对话默认使用模型: {self.ap.provider_cfg.data.get('model')}\n" + content += f'\n当前对话使用模型: {context.query.use_model.name}\n' + content += f'新对话默认使用模型: {self.ap.provider_cfg.data.get("model")}\n' yield entities.CommandReturn(text=content.strip()) @operator.operator_class( - name="show", - help='显示模型详情', - privilege=2, - parent_class=ModelOperator + name='show', help='显示模型详情', privilege=2, parent_class=ModelOperator ) class ModelShowOperator(operator.CommandOperator): """Model Show命令""" - - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + + async def execute( + self, context: entities.ExecuteContext + ) -> typing.AsyncGenerator[entities.CommandReturn, None]: model_name = context.crt_params[0] model = None @@ -47,29 +49,31 @@ class ModelShowOperator(operator.CommandOperator): break if model is None: - yield entities.CommandReturn(error=errors.CommandError(f"未找到模型 {model_name}")) + yield entities.CommandReturn( + error=errors.CommandError(f'未找到模型 {model_name}') + ) else: - content = f"模型详情\n" - content += f"名称: {model.name}\n" + content = '模型详情\n' + content += f'名称: {model.name}\n' if model.model_name is not None: - content += f"请求模型名称: {model.model_name}\n" - content += f"请求器: {model.requester.name}\n" - content += f"密钥组: {model.token_mgr.name}\n" - content += f"支持视觉: {model.vision_supported}\n" - content += f"支持工具: {model.tool_call_supported}\n" + content += f'请求模型名称: {model.model_name}\n' + content += f'请求器: {model.requester.name}\n' + content += f'密钥组: {model.token_mgr.name}\n' + content += f'支持视觉: {model.vision_supported}\n' + content += f'支持工具: {model.tool_call_supported}\n' yield entities.CommandReturn(text=content.strip()) + @operator.operator_class( - name="set", - help='设置默认使用模型', - privilege=2, - parent_class=ModelOperator + name='set', help='设置默认使用模型', privilege=2, parent_class=ModelOperator ) class ModelSetOperator(operator.CommandOperator): """Model Set命令""" - - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + + async def execute( + self, context: entities.ExecuteContext + ) -> typing.AsyncGenerator[entities.CommandReturn, None]: model_name = context.crt_params[0] model = None @@ -79,8 +83,12 @@ class ModelSetOperator(operator.CommandOperator): break if model is None: - yield entities.CommandReturn(error=errors.CommandError(f"未找到模型 {model_name}")) + yield entities.CommandReturn( + error=errors.CommandError(f'未找到模型 {model_name}') + ) else: self.ap.provider_cfg.data['model'] = model_name await self.ap.provider_cfg.dump_config() - yield entities.CommandReturn(text=f"已设置当前使用模型为 {model_name},重置会话以生效") + yield entities.CommandReturn( + text=f'已设置当前使用模型为 {model_name},重置会话以生效' + ) diff --git a/pkg/command/operators/next.py b/pkg/command/operators/next.py index 8f4b5a5a..ef5ae103 100644 --- a/pkg/command/operators/next.py +++ b/pkg/command/operators/next.py @@ -1,35 +1,42 @@ from __future__ import annotations import typing -import datetime -from .. import operator, entities, cmdmgr, errors +from .. import operator, entities, errors -@operator.operator_class( - name="next", - help="切换到后一个对话", - usage='!next' -) +@operator.operator_class(name='next', help='切换到后一个对话', usage='!next') class NextOperator(operator.CommandOperator): - async def execute( - self, - context: entities.ExecuteContext + self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - if context.session.conversations: # 找到当前会话的下一个会话 for index in range(len(context.session.conversations)): - if context.session.conversations[index] == context.session.using_conversation: - if index == len(context.session.conversations)-1: - yield entities.CommandReturn(error=errors.CommandOperationError('已经是最后一个对话了')) + if ( + context.session.conversations[index] + == context.session.using_conversation + ): + if index == len(context.session.conversations) - 1: + yield entities.CommandReturn( + error=errors.CommandOperationError('已经是最后一个对话了') + ) return else: - context.session.using_conversation = context.session.conversations[index+1] - time_str = context.session.using_conversation.create_time.strftime("%Y-%m-%d %H:%M:%S") + context.session.using_conversation = ( + context.session.conversations[index + 1] + ) + time_str = ( + context.session.using_conversation.create_time.strftime( + '%Y-%m-%d %H:%M:%S' + ) + ) - yield entities.CommandReturn(text=f"已切换到后一个对话: {index} {time_str}: {context.session.using_conversation.messages[0].content}") + yield entities.CommandReturn( + text=f'已切换到后一个对话: {index} {time_str}: {context.session.using_conversation.messages[0].content}' + ) return else: - yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话')) \ No newline at end of file + yield entities.CommandReturn( + error=errors.CommandOperationError('当前没有对话') + ) diff --git a/pkg/command/operators/ollama.py b/pkg/command/operators/ollama.py index f5ed382d..7e65d440 100644 --- a/pkg/command/operators/ollama.py +++ b/pkg/command/operators/ollama.py @@ -2,31 +2,32 @@ from __future__ import annotations import json import typing -import traceback import ollama from .. import operator, entities, errors @operator.operator_class( - name="ollama", - help="ollama平台操作", - usage="!ollama\n!ollama show <模型名>\n!ollama pull <模型名>\n!ollama del <模型名>" + name='ollama', + help='ollama平台操作', + usage='!ollama\n!ollama show <模型名>\n!ollama pull <模型名>\n!ollama del <模型名>', ) class OllamaOperator(operator.CommandOperator): async def execute( - self, context: entities.ExecuteContext + self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: try: content: str = '模型列表:\n' model_list: list = ollama.list().get('models', []) for model in model_list: - content += f"名称: {model['name']}\n" - content += f"修改时间: {model['modified_at']}\n" - content += f"大小: {bytes_to_mb(model['size'])}MB\n\n" - yield entities.CommandReturn(text=f"{content.strip()}") - except ollama.ResponseError as e: - yield entities.CommandReturn(error=errors.CommandError(f"无法获取模型列表,请确认 Ollama 服务正常")) + content += f'名称: {model["name"]}\n' + content += f'修改时间: {model["modified_at"]}\n' + content += f'大小: {bytes_to_mb(model["size"])}MB\n\n' + yield entities.CommandReturn(text=f'{content.strip()}') + except ollama.ResponseError: + yield entities.CommandReturn( + error=errors.CommandError('无法获取模型列表,请确认 Ollama 服务正常') + ) def bytes_to_mb(num_bytes): @@ -35,14 +36,11 @@ def bytes_to_mb(num_bytes): @operator.operator_class( - name="show", - help="ollama模型详情", - privilege=2, - parent_class=OllamaOperator + name='show', help='ollama模型详情', privilege=2, parent_class=OllamaOperator ) class OllamaShowOperator(operator.CommandOperator): async def execute( - self, context: entities.ExecuteContext + self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: content: str = '模型详情:\n' try: @@ -53,31 +51,36 @@ class OllamaShowOperator(operator.CommandOperator): for key in ['license', 'modelfile']: show[key] = ignore_show - for key in ['tokenizer.chat_template.rag', 'tokenizer.chat_template.tool_use']: + for key in [ + 'tokenizer.chat_template.rag', + 'tokenizer.chat_template.tool_use', + ]: model_info[key] = ignore_show content += json.dumps(show, indent=4) yield entities.CommandReturn(text=content.strip()) - except ollama.ResponseError as e: - yield entities.CommandReturn(error=errors.CommandError(f"无法获取模型详情,请确认 Ollama 服务正常")) + except ollama.ResponseError: + yield entities.CommandReturn( + error=errors.CommandError('无法获取模型详情,请确认 Ollama 服务正常') + ) + @operator.operator_class( - name="pull", - help="ollama模型拉取", - privilege=2, - parent_class=OllamaOperator + name='pull', help='ollama模型拉取', privilege=2, parent_class=OllamaOperator ) class OllamaPullOperator(operator.CommandOperator): async def execute( - self, context: entities.ExecuteContext + self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: try: model_list: list = ollama.list().get('models', []) if context.crt_params[0] in [model['name'] for model in model_list]: - yield entities.CommandReturn(text="模型已存在") + yield entities.CommandReturn(text='模型已存在') return - except ollama.ResponseError as e: - yield entities.CommandReturn(error=errors.CommandError(f"无法获取模型列表,请确认 Ollama 服务正常")) + except ollama.ResponseError: + yield entities.CommandReturn( + error=errors.CommandError('无法获取模型列表,请确认 Ollama 服务正常') + ) return on_progress: bool = False @@ -99,23 +102,21 @@ class OllamaPullOperator(operator.CommandOperator): if percentage_completed > progress_count: progress_count += 10 yield entities.CommandReturn( - text=f"下载进度: {completed}/{total} ({percentage_completed:.2f}%)") + text=f'下载进度: {completed}/{total} ({percentage_completed:.2f}%)' + ) except ollama.ResponseError as e: - yield entities.CommandReturn(text=f"拉取失败: {e.error}") + yield entities.CommandReturn(text=f'拉取失败: {e.error}') @operator.operator_class( - name="del", - help="ollama模型删除", - privilege=2, - parent_class=OllamaOperator + name='del', help='ollama模型删除', privilege=2, parent_class=OllamaOperator ) class OllamaDelOperator(operator.CommandOperator): async def execute( - self, context: entities.ExecuteContext + self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: try: ret: str = ollama.delete(model=context.crt_params[0])['status'] except ollama.ResponseError as e: - ret = f"{e.error}" + ret = f'{e.error}' yield entities.CommandReturn(text=ret) diff --git a/pkg/command/operators/plugin.py b/pkg/command/operators/plugin.py index c36fbbc3..1bf4c7af 100644 --- a/pkg/command/operators/plugin.py +++ b/pkg/command/operators/plugin.py @@ -2,31 +2,30 @@ from __future__ import annotations import typing import traceback -from .. import operator, entities, cmdmgr, errors -from ...core import app +from .. import operator, entities, errors @operator.operator_class( - name="plugin", - help="插件操作", - usage="!plugin\n!plugin get <插件仓库地址>\n!plugin update\n!plugin del <插件名>\n!plugin on <插件名>\n!plugin off <插件名>" + name='plugin', + help='插件操作', + usage='!plugin\n!plugin get <插件仓库地址>\n!plugin update\n!plugin del <插件名>\n!plugin on <插件名>\n!plugin off <插件名>', ) class PluginOperator(operator.CommandOperator): - async def execute( - self, - context: entities.ExecuteContext + self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - plugin_list = self.ap.plugin_mgr.plugins() - reply_str = "所有插件({}):\n".format(len(plugin_list)) + reply_str = '所有插件({}):\n'.format(len(plugin_list)) idx = 0 for plugin in plugin_list: - reply_str += "\n#{} {} {}\n{}\nv{}\n作者: {}\n"\ - .format((idx+1), plugin.plugin_name, - "[已禁用]" if not plugin.enabled else "", - plugin.plugin_description, - plugin.plugin_version, plugin.plugin_author) + reply_str += '\n#{} {} {}\n{}\nv{}\n作者: {}\n'.format( + (idx + 1), + plugin.plugin_name, + '[已禁用]' if not plugin.enabled else '', + plugin.plugin_description, + plugin.plugin_version, + plugin.plugin_author, + ) idx += 1 @@ -34,48 +33,42 @@ class PluginOperator(operator.CommandOperator): @operator.operator_class( - name="get", - help="安装插件", - privilege=2, - parent_class=PluginOperator + name='get', help='安装插件', privilege=2, parent_class=PluginOperator ) class PluginGetOperator(operator.CommandOperator): - async def execute( - self, - context: entities.ExecuteContext + self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - if len(context.crt_params) == 0: - yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件仓库地址')) + yield entities.CommandReturn( + error=errors.ParamNotEnoughError('请提供插件仓库地址') + ) else: repo = context.crt_params[0] - yield entities.CommandReturn(text="正在安装插件...") + yield entities.CommandReturn(text='正在安装插件...') try: await self.ap.plugin_mgr.install_plugin(repo) - yield entities.CommandReturn(text="插件安装成功,请重启程序以加载插件") + yield entities.CommandReturn(text='插件安装成功,请重启程序以加载插件') except Exception as e: traceback.print_exc() - yield entities.CommandReturn(error=errors.CommandError("插件安装失败: "+str(e))) + yield entities.CommandReturn( + error=errors.CommandError('插件安装失败: ' + str(e)) + ) @operator.operator_class( - name="update", - help="更新插件", - privilege=2, - parent_class=PluginOperator + name='update', help='更新插件', privilege=2, parent_class=PluginOperator ) class PluginUpdateOperator(operator.CommandOperator): - async def execute( - self, - context: entities.ExecuteContext + self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - if len(context.crt_params) == 0: - yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称')) + yield entities.CommandReturn( + error=errors.ParamNotEnoughError('请提供插件名称') + ) else: plugin_name = context.crt_params[0] @@ -83,36 +76,34 @@ class PluginUpdateOperator(operator.CommandOperator): plugin_container = self.ap.plugin_mgr.get_plugin_by_name(plugin_name) if plugin_container is not None: - yield entities.CommandReturn(text="正在更新插件...") + yield entities.CommandReturn(text='正在更新插件...') await self.ap.plugin_mgr.update_plugin(plugin_name) - yield entities.CommandReturn(text="插件更新成功,请重启程序以加载插件") + yield entities.CommandReturn( + text='插件更新成功,请重启程序以加载插件' + ) else: - yield entities.CommandReturn(error=errors.CommandError("插件更新失败: 未找到插件")) + yield entities.CommandReturn( + error=errors.CommandError('插件更新失败: 未找到插件') + ) except Exception as e: traceback.print_exc() - yield entities.CommandReturn(error=errors.CommandError("插件更新失败: "+str(e))) + yield entities.CommandReturn( + error=errors.CommandError('插件更新失败: ' + str(e)) + ) + @operator.operator_class( - name="all", - help="更新所有插件", - privilege=2, - parent_class=PluginUpdateOperator + name='all', help='更新所有插件', privilege=2, parent_class=PluginUpdateOperator ) class PluginUpdateAllOperator(operator.CommandOperator): - async def execute( - self, - context: entities.ExecuteContext + self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - try: - plugins = [ - p.plugin_name - for p in self.ap.plugin_mgr.plugins() - ] + plugins = [p.plugin_name for p in self.ap.plugin_mgr.plugins()] if plugins: - yield entities.CommandReturn(text="正在更新插件...") + yield entities.CommandReturn(text='正在更新插件...') updated = [] try: for plugin_name in plugins: @@ -120,30 +111,32 @@ class PluginUpdateAllOperator(operator.CommandOperator): updated.append(plugin_name) except Exception as e: traceback.print_exc() - yield entities.CommandReturn(error=errors.CommandError("插件更新失败: "+str(e))) - yield entities.CommandReturn(text="已更新插件: {}".format(", ".join(updated))) + yield entities.CommandReturn( + error=errors.CommandError('插件更新失败: ' + str(e)) + ) + yield entities.CommandReturn( + text='已更新插件: {}'.format(', '.join(updated)) + ) else: - yield entities.CommandReturn(text="没有可更新的插件") + yield entities.CommandReturn(text='没有可更新的插件') except Exception as e: traceback.print_exc() - yield entities.CommandReturn(error=errors.CommandError("插件更新失败: "+str(e))) + yield entities.CommandReturn( + error=errors.CommandError('插件更新失败: ' + str(e)) + ) @operator.operator_class( - name="del", - help="删除插件", - privilege=2, - parent_class=PluginOperator + name='del', help='删除插件', privilege=2, parent_class=PluginOperator ) class PluginDelOperator(operator.CommandOperator): - async def execute( - self, - context: entities.ExecuteContext + self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - if len(context.crt_params) == 0: - yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称')) + yield entities.CommandReturn( + error=errors.ParamNotEnoughError('请提供插件名称') + ) else: plugin_name = context.crt_params[0] @@ -151,67 +144,81 @@ class PluginDelOperator(operator.CommandOperator): plugin_container = self.ap.plugin_mgr.get_plugin_by_name(plugin_name) if plugin_container is not None: - yield entities.CommandReturn(text="正在删除插件...") + yield entities.CommandReturn(text='正在删除插件...') await self.ap.plugin_mgr.uninstall_plugin(plugin_name) - yield entities.CommandReturn(text="插件删除成功,请重启程序以加载插件") + yield entities.CommandReturn( + text='插件删除成功,请重启程序以加载插件' + ) else: - yield entities.CommandReturn(error=errors.CommandError("插件删除失败: 未找到插件")) + yield entities.CommandReturn( + error=errors.CommandError('插件删除失败: 未找到插件') + ) except Exception as e: traceback.print_exc() - yield entities.CommandReturn(error=errors.CommandError("插件删除失败: "+str(e))) + yield entities.CommandReturn( + error=errors.CommandError('插件删除失败: ' + str(e)) + ) @operator.operator_class( - name="on", - help="启用插件", - privilege=2, - parent_class=PluginOperator + name='on', help='启用插件', privilege=2, parent_class=PluginOperator ) class PluginEnableOperator(operator.CommandOperator): - async def execute( - self, - context: entities.ExecuteContext + self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - if len(context.crt_params) == 0: - yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称')) + yield entities.CommandReturn( + error=errors.ParamNotEnoughError('请提供插件名称') + ) else: plugin_name = context.crt_params[0] try: if await self.ap.plugin_mgr.update_plugin_switch(plugin_name, True): - yield entities.CommandReturn(text="已启用插件: {}".format(plugin_name)) + yield entities.CommandReturn( + text='已启用插件: {}'.format(plugin_name) + ) else: - yield entities.CommandReturn(error=errors.CommandError("插件状态修改失败: 未找到插件 {}".format(plugin_name))) + yield entities.CommandReturn( + error=errors.CommandError( + '插件状态修改失败: 未找到插件 {}'.format(plugin_name) + ) + ) except Exception as e: traceback.print_exc() - yield entities.CommandReturn(error=errors.CommandError("插件状态修改失败: "+str(e))) + yield entities.CommandReturn( + error=errors.CommandError('插件状态修改失败: ' + str(e)) + ) @operator.operator_class( - name="off", - help="禁用插件", - privilege=2, - parent_class=PluginOperator + name='off', help='禁用插件', privilege=2, parent_class=PluginOperator ) class PluginDisableOperator(operator.CommandOperator): - async def execute( - self, - context: entities.ExecuteContext + self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - if len(context.crt_params) == 0: - yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称')) + yield entities.CommandReturn( + error=errors.ParamNotEnoughError('请提供插件名称') + ) else: plugin_name = context.crt_params[0] try: if await self.ap.plugin_mgr.update_plugin_switch(plugin_name, False): - yield entities.CommandReturn(text="已禁用插件: {}".format(plugin_name)) + yield entities.CommandReturn( + text='已禁用插件: {}'.format(plugin_name) + ) else: - yield entities.CommandReturn(error=errors.CommandError("插件状态修改失败: 未找到插件 {}".format(plugin_name))) + yield entities.CommandReturn( + error=errors.CommandError( + '插件状态修改失败: 未找到插件 {}'.format(plugin_name) + ) + ) except Exception as e: traceback.print_exc() - yield entities.CommandReturn(error=errors.CommandError("插件状态修改失败: "+str(e))) + yield entities.CommandReturn( + error=errors.CommandError('插件状态修改失败: ' + str(e)) + ) diff --git a/pkg/command/operators/prompt.py b/pkg/command/operators/prompt.py index 29d688a6..41f42de4 100644 --- a/pkg/command/operators/prompt.py +++ b/pkg/command/operators/prompt.py @@ -2,28 +2,23 @@ from __future__ import annotations import typing -from .. import operator, entities, cmdmgr, errors +from .. import operator, entities, errors -@operator.operator_class( - name="prompt", - help="查看当前对话的前文", - usage='!prompt' -) +@operator.operator_class(name='prompt', help='查看当前对话的前文', usage='!prompt') class PromptOperator(operator.CommandOperator): - async def execute( - self, - context: entities.ExecuteContext + self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - """执行 - """ + """执行""" if context.session.using_conversation is None: - yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话')) + yield entities.CommandReturn( + error=errors.CommandOperationError('当前没有对话') + ) else: reply_str = '当前对话所有内容:\n\n' for msg in context.session.using_conversation.messages: - reply_str += f"{msg.role}: {msg.content}\n" + reply_str += f'{msg.role}: {msg.content}\n' - yield entities.CommandReturn(text=reply_str) \ No newline at end of file + yield entities.CommandReturn(text=reply_str) diff --git a/pkg/command/operators/resend.py b/pkg/command/operators/resend.py index 6d930413..44e5a35c 100644 --- a/pkg/command/operators/resend.py +++ b/pkg/command/operators/resend.py @@ -2,26 +2,22 @@ from __future__ import annotations import typing -from .. import operator, entities, cmdmgr, errors +from .. import operator, entities, errors @operator.operator_class( - name="resend", - help="重发当前会话的最后一条消息", - usage='!resend' + name='resend', help='重发当前会话的最后一条消息', usage='!resend' ) class ResendOperator(operator.CommandOperator): - async def execute( - self, - context: entities.ExecuteContext + self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: # 回滚到最后一条用户message前 if context.session.using_conversation is None: - yield entities.CommandReturn(error=errors.CommandError("当前没有对话")) + yield entities.CommandReturn(error=errors.CommandError('当前没有对话')) else: conv_msg = context.session.using_conversation.messages - + # 倒序一直删到最后一条用户message while len(conv_msg) > 0 and conv_msg[-1].role != 'user': conv_msg.pop() @@ -31,4 +27,4 @@ class ResendOperator(operator.CommandOperator): conv_msg.pop() # 不重发了,提示用户已删除就行了 - yield entities.CommandReturn(text="已删除最后一次请求记录") + yield entities.CommandReturn(text='已删除最后一次请求记录') diff --git a/pkg/command/operators/reset.py b/pkg/command/operators/reset.py index 5d1402ac..7ef54e08 100644 --- a/pkg/command/operators/reset.py +++ b/pkg/command/operators/reset.py @@ -2,22 +2,15 @@ from __future__ import annotations import typing -from .. import operator, entities, cmdmgr, errors +from .. import operator, entities -@operator.operator_class( - name="reset", - help="重置当前会话", - usage='!reset' -) +@operator.operator_class(name='reset', help='重置当前会话', usage='!reset') class ResetOperator(operator.CommandOperator): - async def execute( - self, - context: entities.ExecuteContext + self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - """执行 - """ + """执行""" context.session.using_conversation = None - yield entities.CommandReturn(text="已重置当前会话") + yield entities.CommandReturn(text='已重置当前会话') diff --git a/pkg/command/operators/update.py b/pkg/command/operators/update.py index 524a26dd..775ee26a 100644 --- a/pkg/command/operators/update.py +++ b/pkg/command/operators/update.py @@ -3,28 +3,22 @@ from __future__ import annotations import typing import traceback -from .. import operator, entities, cmdmgr, errors +from .. import operator, entities, errors -@operator.operator_class( - name="update", - help="更新程序", - usage='!update', - privilege=2 -) +@operator.operator_class(name='update', help='更新程序', usage='!update', privilege=2) class UpdateCommand(operator.CommandOperator): - async def execute( - self, - context: entities.ExecuteContext + self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - try: - yield entities.CommandReturn(text="正在进行更新...") + yield entities.CommandReturn(text='正在进行更新...') if await self.ap.ver_mgr.update_all(): - yield entities.CommandReturn(text="更新完成,请重启程序以应用更新") + yield entities.CommandReturn(text='更新完成,请重启程序以应用更新') else: - yield entities.CommandReturn(text="当前已是最新版本") + yield entities.CommandReturn(text='当前已是最新版本') except Exception as e: traceback.print_exc() - yield entities.CommandReturn(error=errors.CommandError("更新失败: "+str(e))) \ No newline at end of file + yield entities.CommandReturn( + error=errors.CommandError('更新失败: ' + str(e)) + ) diff --git a/pkg/command/operators/version.py b/pkg/command/operators/version.py index a5d7a81b..267b1113 100644 --- a/pkg/command/operators/version.py +++ b/pkg/command/operators/version.py @@ -2,26 +2,20 @@ from __future__ import annotations import typing -from .. import operator, cmdmgr, entities, errors +from .. import operator, entities -@operator.operator_class( - name="version", - help="显示版本信息", - usage='!version' -) +@operator.operator_class(name='version', help='显示版本信息', usage='!version') class VersionCommand(operator.CommandOperator): - async def execute( - self, - context: entities.ExecuteContext + self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - reply_str = f"当前版本: \n{self.ap.ver_mgr.get_current_version()}" + reply_str = f'当前版本: \n{self.ap.ver_mgr.get_current_version()}' try: if await self.ap.ver_mgr.is_new_version_available(): - reply_str += "\n\n有新版本可用。" - except: + reply_str += '\n\n有新版本可用。' + except Exception: pass - yield entities.CommandReturn(text=reply_str.strip()) \ No newline at end of file + yield entities.CommandReturn(text=reply_str.strip()) diff --git a/pkg/config/impls/json.py b/pkg/config/impls/json.py index e414e451..07fc533c 100644 --- a/pkg/config/impls/json.py +++ b/pkg/config/impls/json.py @@ -9,7 +9,10 @@ class JSONConfigFile(file_model.ConfigFile): """JSON配置文件""" def __init__( - self, config_file_name: str, template_file_name: str = None, template_data: dict = None + self, + config_file_name: str, + template_file_name: str = None, + template_data: dict = None, ) -> None: self.config_file_name = config_file_name self.template_file_name = template_file_name @@ -22,28 +25,26 @@ class JSONConfigFile(file_model.ConfigFile): if self.template_file_name is not None: shutil.copyfile(self.template_file_name, self.config_file_name) elif self.template_data is not None: - with open(self.config_file_name, "w", encoding="utf-8") as f: + with open(self.config_file_name, 'w', encoding='utf-8') as f: json.dump(self.template_data, f, indent=4, ensure_ascii=False) else: - raise ValueError("template_file_name or template_data must be provided") - - async def load(self, completion: bool=True) -> dict: + raise ValueError('template_file_name or template_data must be provided') + async def load(self, completion: bool = True) -> dict: if not self.exists(): await self.create() if self.template_file_name is not None: - with open(self.template_file_name, "r", encoding="utf-8") as f: + with open(self.template_file_name, 'r', encoding='utf-8') as f: self.template_data = json.load(f) - with open(self.config_file_name, "r", encoding="utf-8") as f: + with open(self.config_file_name, 'r', encoding='utf-8') as f: try: cfg = json.load(f) except json.JSONDecodeError as e: - raise Exception(f"配置文件 {self.config_file_name} 语法错误: {e}") + raise Exception(f'配置文件 {self.config_file_name} 语法错误: {e}') if completion: - for key in self.template_data: if key not in cfg: cfg[key] = self.template_data[key] @@ -51,9 +52,9 @@ class JSONConfigFile(file_model.ConfigFile): return cfg async def save(self, cfg: dict): - with open(self.config_file_name, "w", encoding="utf-8") as f: + with open(self.config_file_name, 'w', encoding='utf-8') as f: json.dump(cfg, f, indent=4, ensure_ascii=False) def save_sync(self, cfg: dict): - with open(self.config_file_name, "w", encoding="utf-8") as f: + with open(self.config_file_name, 'w', encoding='utf-8') as f: json.dump(cfg, f, indent=4, ensure_ascii=False) diff --git a/pkg/config/impls/pymodule.py b/pkg/config/impls/pymodule.py index 67e5867d..2311992e 100644 --- a/pkg/config/impls/pymodule.py +++ b/pkg/config/impls/pymodule.py @@ -25,10 +25,10 @@ class PythonModuleConfigFile(file_model.ConfigFile): async def create(self): shutil.copyfile(self.template_file_name, self.config_file_name) - async def load(self, completion: bool=True) -> dict: + async def load(self, completion: bool = True) -> dict: module_name = os.path.splitext(os.path.basename(self.config_file_name))[0] module = importlib.import_module(module_name) - + cfg = {} allowed_types = (int, float, str, bool, list, dict) @@ -63,4 +63,4 @@ class PythonModuleConfigFile(file_model.ConfigFile): logging.warning('Python模块配置文件不支持保存') def save_sync(self, data: dict): - logging.warning('Python模块配置文件不支持保存') \ No newline at end of file + logging.warning('Python模块配置文件不支持保存') diff --git a/pkg/config/impls/yaml.py b/pkg/config/impls/yaml.py index f4518003..55045186 100644 --- a/pkg/config/impls/yaml.py +++ b/pkg/config/impls/yaml.py @@ -9,7 +9,10 @@ class YAMLConfigFile(file_model.ConfigFile): """YAML配置文件""" def __init__( - self, config_file_name: str, template_file_name: str = None, template_data: dict = None + self, + config_file_name: str, + template_file_name: str = None, + template_data: dict = None, ) -> None: self.config_file_name = config_file_name self.template_file_name = template_file_name @@ -22,28 +25,26 @@ class YAMLConfigFile(file_model.ConfigFile): if self.template_file_name is not None: shutil.copyfile(self.template_file_name, self.config_file_name) elif self.template_data is not None: - with open(self.config_file_name, "w", encoding="utf-8") as f: + with open(self.config_file_name, 'w', encoding='utf-8') as f: yaml.dump(self.template_data, f, indent=4, allow_unicode=True) else: - raise ValueError("template_file_name or template_data must be provided") - - async def load(self, completion: bool=True) -> dict: + raise ValueError('template_file_name or template_data must be provided') + async def load(self, completion: bool = True) -> dict: if not self.exists(): await self.create() if self.template_file_name is not None: - with open(self.template_file_name, "r", encoding="utf-8") as f: + with open(self.template_file_name, 'r', encoding='utf-8') as f: self.template_data = yaml.load(f, Loader=yaml.FullLoader) - with open(self.config_file_name, "r", encoding="utf-8") as f: + with open(self.config_file_name, 'r', encoding='utf-8') as f: try: cfg = yaml.load(f, Loader=yaml.FullLoader) except yaml.YAMLError as e: - raise Exception(f"配置文件 {self.config_file_name} 语法错误: {e}") + raise Exception(f'配置文件 {self.config_file_name} 语法错误: {e}') if completion: - for key in self.template_data: if key not in cfg: cfg[key] = self.template_data[key] @@ -51,9 +52,9 @@ class YAMLConfigFile(file_model.ConfigFile): return cfg async def save(self, cfg: dict): - with open(self.config_file_name, "w", encoding="utf-8") as f: + with open(self.config_file_name, 'w', encoding='utf-8') as f: yaml.dump(cfg, f, indent=4, allow_unicode=True) def save_sync(self, cfg: dict): - with open(self.config_file_name, "w", encoding="utf-8") as f: - yaml.dump(cfg, f, indent=4, allow_unicode=True) \ No newline at end of file + with open(self.config_file_name, 'w', encoding='utf-8') as f: + yaml.dump(cfg, f, indent=4, allow_unicode=True) diff --git a/pkg/config/manager.py b/pkg/config/manager.py index 4421003c..2385c6b5 100644 --- a/pkg/config/manager.py +++ b/pkg/config/manager.py @@ -6,7 +6,7 @@ from .impls import pymodule, json as json_file, yaml as yaml_file class ConfigManager: """配置文件管理器""" - + name: str = None """配置管理器名""" @@ -31,7 +31,7 @@ class ConfigManager: self.file = cfg_file self.data = {} - async def load_config(self, completion: bool=True): + async def load_config(self, completion: bool = True): self.data = await self.file.load(completion=completion) async def dump_config(self): @@ -41,9 +41,11 @@ class ConfigManager: self.file.save_sync(self.data) -async def load_python_module_config(config_name: str, template_name: str, completion: bool=True) -> ConfigManager: +async def load_python_module_config( + config_name: str, template_name: str, completion: bool = True +) -> ConfigManager: """加载Python模块配置文件 - + Args: config_name (str): 配置文件名 template_name (str): 模板文件名 @@ -52,10 +54,7 @@ async def load_python_module_config(config_name: str, template_name: str, comple Returns: ConfigManager: 配置文件管理器 """ - cfg_inst = pymodule.PythonModuleConfigFile( - config_name, - template_name - ) + cfg_inst = pymodule.PythonModuleConfigFile(config_name, template_name) cfg_mgr = ConfigManager(cfg_inst) await cfg_mgr.load_config(completion=completion) @@ -63,20 +62,21 @@ async def load_python_module_config(config_name: str, template_name: str, comple return cfg_mgr -async def load_json_config(config_name: str, template_name: str=None, template_data: dict=None, completion: bool=True) -> ConfigManager: +async def load_json_config( + config_name: str, + template_name: str = None, + template_data: dict = None, + completion: bool = True, +) -> ConfigManager: """加载JSON配置文件 - + Args: config_name (str): 配置文件名 template_name (str): 模板文件名 template_data (dict): 模板数据 completion (bool): 是否自动补全内存中的配置文件 """ - cfg_inst = json_file.JSONConfigFile( - config_name, - template_name, - template_data - ) + cfg_inst = json_file.JSONConfigFile(config_name, template_name, template_data) cfg_mgr = ConfigManager(cfg_inst) await cfg_mgr.load_config(completion=completion) @@ -84,9 +84,14 @@ async def load_json_config(config_name: str, template_name: str=None, template_d return cfg_mgr -async def load_yaml_config(config_name: str, template_name: str=None, template_data: dict=None, completion: bool=True) -> ConfigManager: +async def load_yaml_config( + config_name: str, + template_name: str = None, + template_data: dict = None, + completion: bool = True, +) -> ConfigManager: """加载YAML配置文件 - + Args: config_name (str): 配置文件名 template_name (str): 模板文件名 @@ -96,11 +101,7 @@ async def load_yaml_config(config_name: str, template_name: str=None, template_d Returns: ConfigManager: 配置文件管理器 """ - cfg_inst = yaml_file.YAMLConfigFile( - config_name, - template_name, - template_data - ) + cfg_inst = yaml_file.YAMLConfigFile(config_name, template_name, template_data) cfg_mgr = ConfigManager(cfg_inst) await cfg_mgr.load_config(completion=completion) diff --git a/pkg/config/model.py b/pkg/config/model.py index 153123e3..f3536804 100644 --- a/pkg/config/model.py +++ b/pkg/config/model.py @@ -22,7 +22,7 @@ class ConfigFile(metaclass=abc.ABCMeta): pass @abc.abstractmethod - async def load(self, completion: bool=True) -> dict: + async def load(self, completion: bool = True) -> dict: pass @abc.abstractmethod diff --git a/pkg/core/app.py b/pkg/core/app.py index 1c4042f8..6e631e5c 100644 --- a/pkg/core/app.py +++ b/pkg/core/app.py @@ -2,9 +2,7 @@ from __future__ import annotations import logging import asyncio -import threading import traceback -import enum import sys import os @@ -29,7 +27,6 @@ from ..discover import engine as discover_engine from ..utils import logcache, ip from . import taskmgr from . import entities as core_entities -from .bootutils import config class Application: @@ -123,33 +120,55 @@ class Application: async def run(self): try: await self.plugin_mgr.initialize_plugins() + # 后续可能会允许动态重启其他任务 # 故为了防止程序在非 Ctrl-C 情况下退出,这里创建一个不会结束的协程 async def never_ending(): while True: await asyncio.sleep(1) - self.task_mgr.create_task(self.platform_mgr.run(), name="platform-manager", scopes=[core_entities.LifecycleControlScope.APPLICATION, core_entities.LifecycleControlScope.PLATFORM]) - self.task_mgr.create_task(self.ctrl.run(), name="query-controller", scopes=[core_entities.LifecycleControlScope.APPLICATION]) - self.task_mgr.create_task(self.http_ctrl.run(), name="http-api-controller", scopes=[core_entities.LifecycleControlScope.APPLICATION]) - self.task_mgr.create_task(never_ending(), name="never-ending-task", scopes=[core_entities.LifecycleControlScope.APPLICATION]) + self.task_mgr.create_task( + self.platform_mgr.run(), + name='platform-manager', + scopes=[ + core_entities.LifecycleControlScope.APPLICATION, + core_entities.LifecycleControlScope.PLATFORM, + ], + ) + self.task_mgr.create_task( + self.ctrl.run(), + name='query-controller', + scopes=[core_entities.LifecycleControlScope.APPLICATION], + ) + self.task_mgr.create_task( + self.http_ctrl.run(), + name='http-api-controller', + scopes=[core_entities.LifecycleControlScope.APPLICATION], + ) + self.task_mgr.create_task( + never_ending(), + name='never-ending-task', + scopes=[core_entities.LifecycleControlScope.APPLICATION], + ) await self.print_web_access_info() await self.task_mgr.wait_all() except asyncio.CancelledError: pass except Exception as e: - self.logger.error(f"应用运行致命异常: {e}") - self.logger.debug(f"Traceback: {traceback.format_exc()}") + self.logger.error(f'应用运行致命异常: {e}') + self.logger.debug(f'Traceback: {traceback.format_exc()}') async def print_web_access_info(self): """打印访问 webui 的提示""" - if not os.path.exists(os.path.join(".", "web/dist")): - self.logger.warning("WebUI 文件缺失,请根据文档获取:https://docs.langbot.app/webui/intro.html") + if not os.path.exists(os.path.join('.', 'web/out')): + self.logger.warning( + 'WebUI 文件缺失,请根据文档获取:https://docs.langbot.app/webui/intro.html' + ) return - host_ip = "127.0.0.1" + host_ip = '127.0.0.1' public_ip = await ip.get_myip() @@ -170,7 +189,7 @@ class Application: 🤯 WebUI 仍处于 Beta 测试阶段,如有问题或建议请反馈到 https://github.com/RockChinQ/LangBot/issues ======================================= """.strip() - for line in tips.split("\n"): + for line in tips.split('\n'): self.logger.info(line) async def reload( @@ -179,21 +198,28 @@ class Application: ): match scope: case core_entities.LifecycleControlScope.PLATFORM.value: - self.logger.info("执行热重载 scope="+scope) + self.logger.info('执行热重载 scope=' + scope) await self.platform_mgr.shutdown() self.platform_mgr = im_mgr.PlatformManager(self) await self.platform_mgr.initialize() - self.task_mgr.create_task(self.platform_mgr.run(), name="platform-manager", scopes=[core_entities.LifecycleControlScope.APPLICATION, core_entities.LifecycleControlScope.PLATFORM]) + self.task_mgr.create_task( + self.platform_mgr.run(), + name='platform-manager', + scopes=[ + core_entities.LifecycleControlScope.APPLICATION, + core_entities.LifecycleControlScope.PLATFORM, + ], + ) case core_entities.LifecycleControlScope.PLUGIN.value: - self.logger.info("执行热重载 scope="+scope) + self.logger.info('执行热重载 scope=' + scope) await self.plugin_mgr.destroy_plugins() # 删除 sys.module 中所有的 plugins/* 下的模块 for mod in list(sys.modules.keys()): - if mod.startswith("plugins."): + if mod.startswith('plugins.'): del sys.modules[mod] self.plugin_mgr = plugin_mgr.PluginManager(self) @@ -204,7 +230,7 @@ class Application: await self.plugin_mgr.load_plugins() await self.plugin_mgr.initialize_plugins() case core_entities.LifecycleControlScope.PROVIDER.value: - self.logger.info("执行热重载 scope="+scope) + self.logger.info('执行热重载 scope=' + scope) await self.tool_mgr.shutdown() @@ -220,4 +246,4 @@ class Application: await llm_tool_mgr_inst.initialize() self.tool_mgr = llm_tool_mgr_inst case _: - pass \ No newline at end of file + pass diff --git a/pkg/core/boot.py b/pkg/core/boot.py index 307fa95c..e3f2a9da 100644 --- a/pkg/core/boot.py +++ b/pkg/core/boot.py @@ -7,29 +7,30 @@ import os from . import app from ..audit import identifier from . import stage -from ..utils import constants +from ..utils import constants, importutil # 引入启动阶段实现以便注册 -from .stages import load_config, setup_logger, build_app, migrate, show_notes, genkeys +from . import stages + +importutil.import_modules_in_pkg(stages) stage_order = [ - "LoadConfigStage", - "MigrationStage", - "GenKeysStage", - "SetupLoggerStage", - "BuildAppStage", - "ShowNotesStage" + 'LoadConfigStage', + 'MigrationStage', + 'GenKeysStage', + 'SetupLoggerStage', + 'BuildAppStage', + 'ShowNotesStage', ] async def make_app(loop: asyncio.AbstractEventLoop) -> app.Application: - # 生成标识符 identifier.init() # 确定是否为调试模式 - if "DEBUG" in os.environ and os.environ["DEBUG"] in ["true", "1"]: + if 'DEBUG' in os.environ and os.environ['DEBUG'] in ['true', '1']: constants.debug_mode = True ap = app.Application() @@ -50,21 +51,17 @@ async def make_app(loop: asyncio.AbstractEventLoop) -> app.Application: async def main(loop: asyncio.AbstractEventLoop): try: - # 挂系统信号处理 import signal - ap: app.Application - def signal_handler(sig, frame): - print("[Signal] 程序退出.") + print('[Signal] 程序退出.') # ap.shutdown() os._exit(0) signal.signal(signal.SIGINT, signal_handler) app_inst = await make_app(loop) - ap = app_inst await app_inst.run() - except Exception as e: + except Exception: traceback.print_exc() diff --git a/pkg/core/bootutils/config.py b/pkg/core/bootutils/config.py index 794c329a..cea4af45 100644 --- a/pkg/core/bootutils/config.py +++ b/pkg/core/bootutils/config.py @@ -1,11 +1,9 @@ from __future__ import annotations -import json from ...config import manager as config_mgr -from ...config.impls import pymodule load_python_module_config = config_mgr.load_python_module_config load_json_config = config_mgr.load_json_config -load_yaml_config = config_mgr.load_yaml_config \ No newline at end of file +load_yaml_config = config_mgr.load_yaml_config diff --git a/pkg/core/bootutils/deps.py b/pkg/core/bootutils/deps.py index 4443079c..b0ba7983 100644 --- a/pkg/core/bootutils/deps.py +++ b/pkg/core/bootutils/deps.py @@ -5,39 +5,39 @@ from ...utils import pkgmgr # 检查依赖,防止用户未安装 # 左边为引入名称,右边为依赖名称 required_deps = { - "requests": "requests", - "openai": "openai", - "anthropic": "anthropic", - "colorlog": "colorlog", - "aiocqhttp": "aiocqhttp", - "botpy": "qq-botpy-rc", - "PIL": "pillow", - "nakuru": "nakuru-project-idk", - "tiktoken": "tiktoken", - "yaml": "pyyaml", - "aiohttp": "aiohttp", - "psutil": "psutil", - "async_lru": "async-lru", - "ollama": "ollama", - "quart": "quart", - "quart_cors": "quart-cors", - "sqlalchemy": "sqlalchemy[asyncio]", - "aiosqlite": "aiosqlite", - "aiofiles": "aiofiles", - "aioshutil": "aioshutil", - "argon2": "argon2-cffi", - "jwt": "pyjwt", - "Crypto": "pycryptodome", - "lark_oapi": "lark-oapi", - "discord": "discord.py", - "cryptography": "cryptography", - "gewechat_client": "gewechat-client", - "dingtalk_stream": "dingtalk_stream", - "dashscope": "dashscope", - "telegram": "python-telegram-bot", - "certifi": "certifi", - "mcp": "mcp", - "sqlmodel": "sqlmodel", + 'requests': 'requests', + 'openai': 'openai', + 'anthropic': 'anthropic', + 'colorlog': 'colorlog', + 'aiocqhttp': 'aiocqhttp', + 'botpy': 'qq-botpy-rc', + 'PIL': 'pillow', + 'nakuru': 'nakuru-project-idk', + 'tiktoken': 'tiktoken', + 'yaml': 'pyyaml', + 'aiohttp': 'aiohttp', + 'psutil': 'psutil', + 'async_lru': 'async-lru', + 'ollama': 'ollama', + 'quart': 'quart', + 'quart_cors': 'quart-cors', + 'sqlalchemy': 'sqlalchemy[asyncio]', + 'aiosqlite': 'aiosqlite', + 'aiofiles': 'aiofiles', + 'aioshutil': 'aioshutil', + 'argon2': 'argon2-cffi', + 'jwt': 'pyjwt', + 'Crypto': 'pycryptodome', + 'lark_oapi': 'lark-oapi', + 'discord': 'discord.py', + 'cryptography': 'cryptography', + 'gewechat_client': 'gewechat-client', + 'dingtalk_stream': 'dingtalk_stream', + 'dashscope': 'dashscope', + 'telegram': 'python-telegram-bot', + 'certifi': 'certifi', + 'mcp': 'mcp', + 'sqlmodel': 'sqlmodel', } @@ -52,18 +52,25 @@ async def check_deps() -> list[str]: missing_deps.append(dep) return missing_deps + async def install_deps(deps: list[str]): global required_deps - + for dep in deps: - pip.main(["install", required_deps[dep]]) + pip.main(['install', required_deps[dep]]) + async def precheck_plugin_deps(): print('[Startup] Prechecking plugin dependencies...') - for dir in os.listdir("plugins"): - subdir = os.path.join("plugins", dir) - if not os.path.isdir(subdir): - continue - if 'requirements.txt' in os.listdir(subdir): - pkgmgr.install_requirements(os.path.join(subdir, 'requirements.txt'), extra_params=['-q', '-q', '-q']) + # 只有在plugins目录存在时才执行插件依赖安装 + if os.path.exists('plugins'): + for dir in os.listdir('plugins'): + subdir = os.path.join('plugins', dir) + if not os.path.isdir(subdir): + continue + if 'requirements.txt' in os.listdir(subdir): + pkgmgr.install_requirements( + os.path.join(subdir, 'requirements.txt'), + extra_params=['-q', '-q', '-q'], + ) diff --git a/pkg/core/bootutils/files.py b/pkg/core/bootutils/files.py index 9a2dff71..3599e41b 100644 --- a/pkg/core/bootutils/files.py +++ b/pkg/core/bootutils/files.py @@ -2,23 +2,23 @@ from __future__ import annotations import os import shutil -import sys required_files = { - "plugins/__init__.py": "templates/__init__.py", - "data/config.yaml": "templates/config.yaml", + 'plugins/__init__.py': 'templates/__init__.py', + 'data/config.yaml': 'templates/config.yaml', } required_paths = [ - "temp", - "data", - "data/metadata", - "data/logs", - "data/labels", - "plugins" + 'temp', + 'data', + 'data/metadata', + 'data/logs', + 'data/labels', + 'plugins', ] + async def generate_files() -> list[str]: global required_files, required_paths diff --git a/pkg/core/bootutils/log.py b/pkg/core/bootutils/log.py index 7cbb7412..df65e1ba 100644 --- a/pkg/core/bootutils/log.py +++ b/pkg/core/bootutils/log.py @@ -1,5 +1,4 @@ import logging -import os import sys import time @@ -9,11 +8,11 @@ from ...utils import constants log_colors_config = { - "DEBUG": "green", # cyan white - "INFO": "white", - "WARNING": "yellow", - "ERROR": "red", - "CRITICAL": "cyan", + 'DEBUG': 'green', # cyan white + 'INFO': 'white', + 'WARNING': 'yellow', + 'ERROR': 'red', + 'CRITICAL': 'cyan', } @@ -27,26 +26,31 @@ async def init_logging(extra_handlers: list[logging.Handler] = None) -> logging. if constants.debug_mode: level = logging.DEBUG - log_file_name = "data/logs/langbot-%s.log" % time.strftime( - "%Y-%m-%d", time.localtime() + log_file_name = 'data/logs/langbot-%s.log' % time.strftime( + '%Y-%m-%d', time.localtime() ) - qcg_logger = logging.getLogger("langbot") + qcg_logger = logging.getLogger('langbot') qcg_logger.setLevel(level) color_formatter = colorlog.ColoredFormatter( - fmt="%(log_color)s[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : %(message)s", - datefmt="%m-%d %H:%M:%S", + fmt='%(log_color)s[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : %(message)s', + datefmt='%m-%d %H:%M:%S', log_colors=log_colors_config, ) stream_handler = logging.StreamHandler(sys.stdout) # stream_handler.setLevel(level) # stream_handler.setFormatter(color_formatter) - stream_handler.stream = open(sys.stdout.fileno(), mode='w', encoding='utf-8', buffering=1) + stream_handler.stream = open( + sys.stdout.fileno(), mode='w', encoding='utf-8', buffering=1 + ) - log_handlers: list[logging.Handler] = [stream_handler, logging.FileHandler(log_file_name, encoding='utf-8')] + log_handlers: list[logging.Handler] = [ + stream_handler, + logging.FileHandler(log_file_name, encoding='utf-8'), + ] log_handlers += extra_handlers if extra_handlers is not None else [] for handler in log_handlers: @@ -54,13 +58,13 @@ async def init_logging(extra_handlers: list[logging.Handler] = None) -> logging. handler.setFormatter(color_formatter) qcg_logger.addHandler(handler) - qcg_logger.debug("日志初始化完成,日志级别:%s" % level) + qcg_logger.debug('日志初始化完成,日志级别:%s' % level) logging.basicConfig( level=logging.CRITICAL, # 设置日志输出格式 - format="[DEPR][%(asctime)s.%(msecs)03d] %(pathname)s (%(lineno)d) - [%(levelname)s] :\n%(message)s", + format='[DEPR][%(asctime)s.%(msecs)03d] %(pathname)s (%(lineno)d) - [%(levelname)s] :\n%(message)s', # 日志输出的格式 # -8表示占位符,让输出左对齐,输出长度都为8位 - datefmt="%Y-%m-%d %H:%M:%S", # 时间输出的格式 + datefmt='%Y-%m-%d %H:%M:%S', # 时间输出的格式 handlers=[logging.NullHandler()], ) diff --git a/pkg/core/entities.py b/pkg/core/entities.py index 1753495b..9eddc935 100644 --- a/pkg/core/entities.py +++ b/pkg/core/entities.py @@ -8,21 +8,18 @@ import asyncio import pydantic.v1 as pydantic from ..provider import entities as llm_entities -from ..provider.modelmgr import entities, modelmgr, requester +from ..provider.modelmgr import requester from ..provider.tools import entities as tools_entities from ..platform import adapter as msadapter from ..platform.types import message as platform_message from ..platform.types import events as platform_events -from ..platform.types import entities as platform_entities - class LifecycleControlScope(enum.Enum): - - APPLICATION = "application" - PLATFORM = "platform" - PLUGIN = "plugin" - PROVIDER = "provider" + APPLICATION = 'application' + PLATFORM = 'platform' + PLUGIN = 'plugin' + PROVIDER = 'provider' class LauncherTypes(enum.Enum): @@ -89,14 +86,17 @@ class Query(pydantic.BaseModel): use_funcs: typing.Optional[list[tools_entities.LLMFunction]] = None """使用的函数,由前置处理器阶段设置""" - resp_messages: typing.Optional[list[llm_entities.Message]] | typing.Optional[list[platform_message.MessageChain]] = [] + resp_messages: ( + typing.Optional[list[llm_entities.Message]] + | typing.Optional[list[platform_message.MessageChain]] + ) = [] """由Process阶段生成的回复消息对象列表""" resp_message_chain: typing.Optional[list[platform_message.MessageChain]] = None """回复消息链,从resp_messages包装而得""" # ======= 内部保留 ======= - current_stage: "pkg.pipeline.pipelinemgr.StageInstContainer" = None + current_stage: typing.Optional['pkg.pipeline.pipelinemgr.StageInstContainer'] = None """当前所处阶段""" class Config: @@ -109,13 +109,13 @@ class Query(pydantic.BaseModel): if self.variables is None: self.variables = {} self.variables[key] = value - + def get_variable(self, key: str) -> typing.Any: """获取变量""" if self.variables is None: return None return self.variables.get(key) - + def get_variables(self) -> dict[str, typing.Any]: """获取所有变量""" if self.variables is None: @@ -130,9 +130,13 @@ class Conversation(pydantic.BaseModel): messages: list[llm_entities.Message] - create_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now) + create_time: typing.Optional[datetime.datetime] = pydantic.Field( + default_factory=datetime.datetime.now + ) - update_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now) + update_time: typing.Optional[datetime.datetime] = pydantic.Field( + default_factory=datetime.datetime.now + ) use_llm_model: requester.RuntimeLLMModel @@ -147,6 +151,7 @@ class Conversation(pydantic.BaseModel): class Session(pydantic.BaseModel): """会话,一个 Session 对应一个 {launcher_type.value}_{launcher_id}""" + launcher_type: LauncherTypes launcher_id: typing.Union[int, str] @@ -157,11 +162,17 @@ class Session(pydantic.BaseModel): using_conversation: typing.Optional[Conversation] = None - conversations: typing.Optional[list[Conversation]] = pydantic.Field(default_factory=list) + conversations: typing.Optional[list[Conversation]] = pydantic.Field( + default_factory=list + ) - create_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now) + create_time: typing.Optional[datetime.datetime] = pydantic.Field( + default_factory=datetime.datetime.now + ) - update_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now) + update_time: typing.Optional[datetime.datetime] = pydantic.Field( + default_factory=datetime.datetime.now + ) semaphore: typing.Optional[asyncio.Semaphore] = None """当前会话的信号量,用于限制并发""" diff --git a/pkg/core/migration.py b/pkg/core/migration.py index 2c5c7597..e97c0cf3 100644 --- a/pkg/core/migration.py +++ b/pkg/core/migration.py @@ -9,21 +9,21 @@ from . import app preregistered_migrations: list[typing.Type[Migration]] = [] """当前阶段暂不支持扩展""" + def migration_class(name: str, number: int): - """注册一个迁移 - """ + """注册一个迁移""" + def decorator(cls: typing.Type[Migration]) -> typing.Type[Migration]: cls.name = name cls.number = number preregistered_migrations.append(cls) return cls - + return decorator class Migration(abc.ABC): - """一个版本的迁移 - """ + """一个版本的迁移""" name: str @@ -33,15 +33,13 @@ class Migration(abc.ABC): def __init__(self, ap: app.Application): self.ap = ap - + @abc.abstractmethod async def need_migrate(self) -> bool: - """判断当前环境是否需要运行此迁移 - """ + """判断当前环境是否需要运行此迁移""" pass @abc.abstractmethod async def run(self): - """执行迁移 - """ + """执行迁移""" pass diff --git a/pkg/core/migrations/m001_sensitive_word_migration.py b/pkg/core/migrations/m001_sensitive_word_migration.py index 6e435eeb..72200346 100644 --- a/pkg/core/migrations/m001_sensitive_word_migration.py +++ b/pkg/core/migrations/m001_sensitive_word_migration.py @@ -1,26 +1,26 @@ from __future__ import annotations import os -import sys from .. import migration -@migration.migration_class("sensitive-word-migration", 1) +@migration.migration_class('sensitive-word-migration', 1) class SensitiveWordMigration(migration.Migration): - """敏感词迁移 - """ + """敏感词迁移""" async def need_migrate(self) -> bool: - """判断当前环境是否需要运行此迁移 - """ - return os.path.exists("data/config/sensitive-words.json") and not os.path.exists("data/metadata/sensitive-words.json") + """判断当前环境是否需要运行此迁移""" + return os.path.exists( + 'data/config/sensitive-words.json' + ) and not os.path.exists('data/metadata/sensitive-words.json') async def run(self): - """执行迁移 - """ + """执行迁移""" # 移动文件 - os.rename("data/config/sensitive-words.json", "data/metadata/sensitive-words.json") + os.rename( + 'data/config/sensitive-words.json', 'data/metadata/sensitive-words.json' + ) # 重新加载配置 await self.ap.sensitive_meta.load_config() diff --git a/pkg/core/migrations/m002_openai_config_migration.py b/pkg/core/migrations/m002_openai_config_migration.py index 2f2553ef..6892110f 100644 --- a/pkg/core/migrations/m002_openai_config_migration.py +++ b/pkg/core/migrations/m002_openai_config_migration.py @@ -3,19 +3,16 @@ from __future__ import annotations from .. import migration -@migration.migration_class("openai-config-migration", 2) +@migration.migration_class('openai-config-migration', 2) class OpenAIConfigMigration(migration.Migration): - """OpenAI配置迁移 - """ + """OpenAI配置迁移""" async def need_migrate(self) -> bool: - """判断当前环境是否需要运行此迁移 - """ + """判断当前环境是否需要运行此迁移""" return 'openai-config' in self.ap.provider_cfg.data async def run(self): - """执行迁移 - """ + """执行迁移""" old_openai_config = self.ap.provider_cfg.data['openai-config'].copy() if 'keys' not in self.ap.provider_cfg.data: @@ -26,7 +23,9 @@ class OpenAIConfigMigration(migration.Migration): self.ap.provider_cfg.data['keys']['openai'] = old_openai_config['api-keys'] - self.ap.provider_cfg.data['model'] = old_openai_config['chat-completions-params']['model'] + self.ap.provider_cfg.data['model'] = old_openai_config[ + 'chat-completions-params' + ]['model'] del old_openai_config['chat-completions-params']['model'] @@ -35,7 +34,7 @@ class OpenAIConfigMigration(migration.Migration): if 'openai-chat-completions' not in self.ap.provider_cfg.data['requester']: self.ap.provider_cfg.data['requester']['openai-chat-completions'] = {} - + self.ap.provider_cfg.data['requester']['openai-chat-completions'] = { 'base-url': old_openai_config['base_url'], 'args': old_openai_config['chat-completions-params'], @@ -44,4 +43,4 @@ class OpenAIConfigMigration(migration.Migration): del self.ap.provider_cfg.data['openai-config'] - await self.ap.provider_cfg.dump_config() \ No newline at end of file + await self.ap.provider_cfg.dump_config() diff --git a/pkg/core/migrations/m003_anthropic_requester_cfg_completion.py b/pkg/core/migrations/m003_anthropic_requester_cfg_completion.py index 101b03d0..19369679 100644 --- a/pkg/core/migrations/m003_anthropic_requester_cfg_completion.py +++ b/pkg/core/migrations/m003_anthropic_requester_cfg_completion.py @@ -3,26 +3,23 @@ from __future__ import annotations from .. import migration -@migration.migration_class("anthropic-requester-config-completion", 3) +@migration.migration_class('anthropic-requester-config-completion', 3) class AnthropicRequesterConfigCompletionMigration(migration.Migration): - """OpenAI配置迁移 - """ + """OpenAI配置迁移""" async def need_migrate(self) -> bool: - """判断当前环境是否需要运行此迁移 - """ - return 'anthropic-messages' not in self.ap.provider_cfg.data['requester'] \ + """判断当前环境是否需要运行此迁移""" + return ( + 'anthropic-messages' not in self.ap.provider_cfg.data['requester'] or 'anthropic' not in self.ap.provider_cfg.data['keys'] + ) async def run(self): - """执行迁移 - """ + """执行迁移""" if 'anthropic-messages' not in self.ap.provider_cfg.data['requester']: self.ap.provider_cfg.data['requester']['anthropic-messages'] = { 'base-url': 'https://api.anthropic.com', - 'args': { - 'max_tokens': 1024 - }, + 'args': {'max_tokens': 1024}, 'timeout': 120, } diff --git a/pkg/core/migrations/m004_moonshot_cfg_completion.py b/pkg/core/migrations/m004_moonshot_cfg_completion.py index b1f7e9ed..de086159 100644 --- a/pkg/core/migrations/m004_moonshot_cfg_completion.py +++ b/pkg/core/migrations/m004_moonshot_cfg_completion.py @@ -3,20 +3,19 @@ from __future__ import annotations from .. import migration -@migration.migration_class("moonshot-config-completion", 4) +@migration.migration_class('moonshot-config-completion', 4) class MoonshotConfigCompletionMigration(migration.Migration): - """OpenAI配置迁移 - """ + """OpenAI配置迁移""" async def need_migrate(self) -> bool: - """判断当前环境是否需要运行此迁移 - """ - return 'moonshot-chat-completions' not in self.ap.provider_cfg.data['requester'] \ + """判断当前环境是否需要运行此迁移""" + return ( + 'moonshot-chat-completions' not in self.ap.provider_cfg.data['requester'] or 'moonshot' not in self.ap.provider_cfg.data['keys'] + ) async def run(self): - """执行迁移 - """ + """执行迁移""" if 'moonshot-chat-completions' not in self.ap.provider_cfg.data['requester']: self.ap.provider_cfg.data['requester']['moonshot-chat-completions'] = { 'base-url': 'https://api.moonshot.cn/v1', diff --git a/pkg/core/migrations/m005_deepseek_cfg_completion.py b/pkg/core/migrations/m005_deepseek_cfg_completion.py index bd8aa2ee..d4d82e3f 100644 --- a/pkg/core/migrations/m005_deepseek_cfg_completion.py +++ b/pkg/core/migrations/m005_deepseek_cfg_completion.py @@ -3,20 +3,19 @@ from __future__ import annotations from .. import migration -@migration.migration_class("deepseek-config-completion", 5) +@migration.migration_class('deepseek-config-completion', 5) class DeepseekConfigCompletionMigration(migration.Migration): - """OpenAI配置迁移 - """ + """OpenAI配置迁移""" async def need_migrate(self) -> bool: - """判断当前环境是否需要运行此迁移 - """ - return 'deepseek-chat-completions' not in self.ap.provider_cfg.data['requester'] \ + """判断当前环境是否需要运行此迁移""" + return ( + 'deepseek-chat-completions' not in self.ap.provider_cfg.data['requester'] or 'deepseek' not in self.ap.provider_cfg.data['keys'] + ) async def run(self): - """执行迁移 - """ + """执行迁移""" if 'deepseek-chat-completions' not in self.ap.provider_cfg.data['requester']: self.ap.provider_cfg.data['requester']['deepseek-chat-completions'] = { 'base-url': 'https://api.deepseek.com', @@ -27,4 +26,4 @@ class DeepseekConfigCompletionMigration(migration.Migration): if 'deepseek' not in self.ap.provider_cfg.data['keys']: self.ap.provider_cfg.data['keys']['deepseek'] = [] - await self.ap.provider_cfg.dump_config() \ No newline at end of file + await self.ap.provider_cfg.dump_config() diff --git a/pkg/core/migrations/m006_vision_config.py b/pkg/core/migrations/m006_vision_config.py index 8084611e..ea824d44 100644 --- a/pkg/core/migrations/m006_vision_config.py +++ b/pkg/core/migrations/m006_vision_config.py @@ -3,17 +3,17 @@ from __future__ import annotations from .. import migration -@migration.migration_class("vision-config", 6) +@migration.migration_class('vision-config', 6) class VisionConfigMigration(migration.Migration): """迁移""" async def need_migrate(self) -> bool: """判断当前环境是否需要运行此迁移""" - return "enable-vision" not in self.ap.provider_cfg.data + return 'enable-vision' not in self.ap.provider_cfg.data async def run(self): """执行迁移""" - if "enable-vision" not in self.ap.provider_cfg.data: - self.ap.provider_cfg.data["enable-vision"] = False + if 'enable-vision' not in self.ap.provider_cfg.data: + self.ap.provider_cfg.data['enable-vision'] = False await self.ap.provider_cfg.dump_config() diff --git a/pkg/core/migrations/m007_qcg_center_url.py b/pkg/core/migrations/m007_qcg_center_url.py index cecd6b11..b3fcd853 100644 --- a/pkg/core/migrations/m007_qcg_center_url.py +++ b/pkg/core/migrations/m007_qcg_center_url.py @@ -3,18 +3,20 @@ from __future__ import annotations from .. import migration -@migration.migration_class("qcg-center-url-config", 7) +@migration.migration_class('qcg-center-url-config', 7) class QCGCenterURLConfigMigration(migration.Migration): """迁移""" async def need_migrate(self) -> bool: """判断当前环境是否需要运行此迁移""" - return "qcg-center-url" not in self.ap.system_cfg.data + return 'qcg-center-url' not in self.ap.system_cfg.data async def run(self): """执行迁移""" - - if "qcg-center-url" not in self.ap.system_cfg.data: - self.ap.system_cfg.data["qcg-center-url"] = "https://api.qchatgpt.rockchin.top/api/v2" - + + if 'qcg-center-url' not in self.ap.system_cfg.data: + self.ap.system_cfg.data['qcg-center-url'] = ( + 'https://api.qchatgpt.rockchin.top/api/v2' + ) + await self.ap.system_cfg.dump_config() diff --git a/pkg/core/migrations/m008_ad_fixwin_config_migrate.py b/pkg/core/migrations/m008_ad_fixwin_config_migrate.py index ccd6fbd7..96fd58e7 100644 --- a/pkg/core/migrations/m008_ad_fixwin_config_migrate.py +++ b/pkg/core/migrations/m008_ad_fixwin_config_migrate.py @@ -3,27 +3,27 @@ from __future__ import annotations from .. import migration -@migration.migration_class("ad-fixwin-cfg-migration", 8) +@migration.migration_class('ad-fixwin-cfg-migration', 8) class AdFixwinConfigMigration(migration.Migration): """迁移""" async def need_migrate(self) -> bool: """判断当前环境是否需要运行此迁移""" return isinstance( - self.ap.pipeline_cfg.data["rate-limit"]["fixwin"]["default"], - int + self.ap.pipeline_cfg.data['rate-limit']['fixwin']['default'], int ) async def run(self): """执行迁移""" - - for session_name in self.ap.pipeline_cfg.data["rate-limit"]["fixwin"]: + for session_name in self.ap.pipeline_cfg.data['rate-limit']['fixwin']: temp_dict = { - "window-size": 60, - "limit": self.ap.pipeline_cfg.data["rate-limit"]["fixwin"][session_name] + 'window-size': 60, + 'limit': self.ap.pipeline_cfg.data['rate-limit']['fixwin'][ + session_name + ], } - - self.ap.pipeline_cfg.data["rate-limit"]["fixwin"][session_name] = temp_dict - await self.ap.pipeline_cfg.dump_config() \ No newline at end of file + self.ap.pipeline_cfg.data['rate-limit']['fixwin'][session_name] = temp_dict + + await self.ap.pipeline_cfg.dump_config() diff --git a/pkg/core/migrations/m009_msg_truncator_cfg.py b/pkg/core/migrations/m009_msg_truncator_cfg.py index 369b60eb..066af126 100644 --- a/pkg/core/migrations/m009_msg_truncator_cfg.py +++ b/pkg/core/migrations/m009_msg_truncator_cfg.py @@ -3,7 +3,7 @@ from __future__ import annotations from .. import migration -@migration.migration_class("msg-truncator-cfg-migration", 9) +@migration.migration_class('msg-truncator-cfg-migration', 9) class MsgTruncatorConfigMigration(migration.Migration): """迁移""" @@ -13,12 +13,10 @@ class MsgTruncatorConfigMigration(migration.Migration): async def run(self): """执行迁移""" - + self.ap.pipeline_cfg.data['msg-truncate'] = { 'method': 'round', - 'round': { - 'max-round': 10 - } + 'round': {'max-round': 10}, } await self.ap.pipeline_cfg.dump_config() diff --git a/pkg/core/migrations/m010_ollama_requester_config.py b/pkg/core/migrations/m010_ollama_requester_config.py index 56e49663..8e2e15eb 100644 --- a/pkg/core/migrations/m010_ollama_requester_config.py +++ b/pkg/core/migrations/m010_ollama_requester_config.py @@ -3,7 +3,7 @@ from __future__ import annotations from .. import migration -@migration.migration_class("ollama-requester-config", 10) +@migration.migration_class('ollama-requester-config', 10) class MsgTruncatorConfigMigration(migration.Migration): """迁移""" @@ -13,11 +13,11 @@ class MsgTruncatorConfigMigration(migration.Migration): async def run(self): """执行迁移""" - + self.ap.provider_cfg.data['requester']['ollama-chat'] = { - "base-url": "http://127.0.0.1:11434", - "args": {}, - "timeout": 600 + 'base-url': 'http://127.0.0.1:11434', + 'args': {}, + 'timeout': 600, } await self.ap.provider_cfg.dump_config() diff --git a/pkg/core/migrations/m011_command_prefix_config.py b/pkg/core/migrations/m011_command_prefix_config.py index 6a9e1118..6165ae47 100644 --- a/pkg/core/migrations/m011_command_prefix_config.py +++ b/pkg/core/migrations/m011_command_prefix_config.py @@ -3,7 +3,7 @@ from __future__ import annotations from .. import migration -@migration.migration_class("command-prefix-config", 11) +@migration.migration_class('command-prefix-config', 11) class CommandPrefixConfigMigration(migration.Migration): """迁移""" @@ -13,9 +13,7 @@ class CommandPrefixConfigMigration(migration.Migration): async def run(self): """执行迁移""" - - self.ap.command_cfg.data['command-prefix'] = [ - "!", "!" - ] + + self.ap.command_cfg.data['command-prefix'] = ['!', '!'] await self.ap.command_cfg.dump_config() diff --git a/pkg/core/migrations/m012_runner_config.py b/pkg/core/migrations/m012_runner_config.py index fa236bb7..e7f0e67a 100644 --- a/pkg/core/migrations/m012_runner_config.py +++ b/pkg/core/migrations/m012_runner_config.py @@ -3,7 +3,7 @@ from __future__ import annotations from .. import migration -@migration.migration_class("runner-config", 12) +@migration.migration_class('runner-config', 12) class RunnerConfigMigration(migration.Migration): """迁移""" @@ -13,7 +13,7 @@ class RunnerConfigMigration(migration.Migration): async def run(self): """执行迁移""" - + self.ap.provider_cfg.data['runner'] = 'local-agent' await self.ap.provider_cfg.dump_config() diff --git a/pkg/core/migrations/m013_http_api_config.py b/pkg/core/migrations/m013_http_api_config.py index c5fe55ba..55aff2b9 100644 --- a/pkg/core/migrations/m013_http_api_config.py +++ b/pkg/core/migrations/m013_http_api_config.py @@ -3,29 +3,30 @@ from __future__ import annotations from .. import migration -@migration.migration_class("http-api-config", 13) +@migration.migration_class('http-api-config', 13) class HttpApiConfigMigration(migration.Migration): """迁移""" async def need_migrate(self) -> bool: """判断当前环境是否需要运行此迁移""" - return 'http-api' not in self.ap.system_cfg.data or "persistence" not in self.ap.system_cfg.data + return ( + 'http-api' not in self.ap.system_cfg.data + or 'persistence' not in self.ap.system_cfg.data + ) async def run(self): """执行迁移""" - + self.ap.system_cfg.data['http-api'] = { - "enable": True, - "host": "0.0.0.0", - "port": 5300, - "jwt-expire": 604800 + 'enable': True, + 'host': '0.0.0.0', + 'port': 5300, + 'jwt-expire': 604800, } self.ap.system_cfg.data['persistence'] = { - "sqlite": { - "path": "data/persistence.db" - }, - "use": "sqlite" + 'sqlite': {'path': 'data/persistence.db'}, + 'use': 'sqlite', } await self.ap.system_cfg.dump_config() diff --git a/pkg/core/migrations/m014_force_delay_config.py b/pkg/core/migrations/m014_force_delay_config.py index 55521c9c..005a2ca2 100644 --- a/pkg/core/migrations/m014_force_delay_config.py +++ b/pkg/core/migrations/m014_force_delay_config.py @@ -3,20 +3,20 @@ from __future__ import annotations from .. import migration -@migration.migration_class("force-delay-config", 14) +@migration.migration_class('force-delay-config', 14) class ForceDelayConfigMigration(migration.Migration): """迁移""" async def need_migrate(self) -> bool: """判断当前环境是否需要运行此迁移""" - return type(self.ap.platform_cfg.data['force-delay']) == list + return isinstance(self.ap.platform_cfg.data['force-delay'], list) async def run(self): """执行迁移""" self.ap.platform_cfg.data['force-delay'] = { - "min": self.ap.platform_cfg.data['force-delay'][0], - "max": self.ap.platform_cfg.data['force-delay'][1] + 'min': self.ap.platform_cfg.data['force-delay'][0], + 'max': self.ap.platform_cfg.data['force-delay'][1], } await self.ap.platform_cfg.dump_config() diff --git a/pkg/core/migrations/m015_gitee_ai_config.py b/pkg/core/migrations/m015_gitee_ai_config.py index b41071ad..7dd9b853 100644 --- a/pkg/core/migrations/m015_gitee_ai_config.py +++ b/pkg/core/migrations/m015_gitee_ai_config.py @@ -3,24 +3,25 @@ from __future__ import annotations from .. import migration -@migration.migration_class("gitee-ai-config", 15) +@migration.migration_class('gitee-ai-config', 15) class GiteeAIConfigMigration(migration.Migration): """迁移""" async def need_migrate(self) -> bool: """判断当前环境是否需要运行此迁移""" - return 'gitee-ai-chat-completions' not in self.ap.provider_cfg.data['requester'] or 'gitee-ai' not in self.ap.provider_cfg.data['keys'] + return ( + 'gitee-ai-chat-completions' not in self.ap.provider_cfg.data['requester'] + or 'gitee-ai' not in self.ap.provider_cfg.data['keys'] + ) async def run(self): """执行迁移""" self.ap.provider_cfg.data['requester']['gitee-ai-chat-completions'] = { - "base-url": "https://ai.gitee.com/v1", - "args": {}, - "timeout": 120 + 'base-url': 'https://ai.gitee.com/v1', + 'args': {}, + 'timeout': 120, } - self.ap.provider_cfg.data['keys']['gitee-ai'] = [ - "XXXXX" - ] + self.ap.provider_cfg.data['keys']['gitee-ai'] = ['XXXXX'] await self.ap.provider_cfg.dump_config() diff --git a/pkg/core/migrations/m016_dify_service_api.py b/pkg/core/migrations/m016_dify_service_api.py index 123879f8..e7c4dc6d 100644 --- a/pkg/core/migrations/m016_dify_service_api.py +++ b/pkg/core/migrations/m016_dify_service_api.py @@ -3,7 +3,7 @@ from __future__ import annotations from .. import migration -@migration.migration_class("dify-service-api-config", 16) +@migration.migration_class('dify-service-api-config', 16) class DifyServiceAPICfgMigration(migration.Migration): """迁移""" @@ -14,15 +14,10 @@ class DifyServiceAPICfgMigration(migration.Migration): async def run(self): """执行迁移""" self.ap.provider_cfg.data['dify-service-api'] = { - "base-url": "https://api.dify.ai/v1", - "app-type": "chat", - "chat": { - "api-key": "app-1234567890" - }, - "workflow": { - "api-key": "app-1234567890", - "output-key": "summary" - } + 'base-url': 'https://api.dify.ai/v1', + 'app-type': 'chat', + 'chat': {'api-key': 'app-1234567890'}, + 'workflow': {'api-key': 'app-1234567890', 'output-key': 'summary'}, } await self.ap.provider_cfg.dump_config() diff --git a/pkg/core/migrations/m017_dify_api_timeout_params.py b/pkg/core/migrations/m017_dify_api_timeout_params.py index a0e502a4..7ce9133c 100644 --- a/pkg/core/migrations/m017_dify_api_timeout_params.py +++ b/pkg/core/migrations/m017_dify_api_timeout_params.py @@ -3,22 +3,26 @@ from __future__ import annotations from .. import migration -@migration.migration_class("dify-api-timeout-params", 17) +@migration.migration_class('dify-api-timeout-params', 17) class DifyAPITimeoutParamsMigration(migration.Migration): """迁移""" async def need_migrate(self) -> bool: """判断当前环境是否需要运行此迁移""" - return 'timeout' not in self.ap.provider_cfg.data['dify-service-api']['chat'] or 'timeout' not in self.ap.provider_cfg.data['dify-service-api']['workflow'] \ + return ( + 'timeout' not in self.ap.provider_cfg.data['dify-service-api']['chat'] + or 'timeout' + not in self.ap.provider_cfg.data['dify-service-api']['workflow'] or 'agent' not in self.ap.provider_cfg.data['dify-service-api'] + ) async def run(self): """执行迁移""" self.ap.provider_cfg.data['dify-service-api']['chat']['timeout'] = 120 self.ap.provider_cfg.data['dify-service-api']['workflow']['timeout'] = 120 self.ap.provider_cfg.data['dify-service-api']['agent'] = { - "api-key": "app-1234567890", - "timeout": 120 + 'api-key': 'app-1234567890', + 'timeout': 120, } await self.ap.provider_cfg.dump_config() diff --git a/pkg/core/migrations/m018_xai_config.py b/pkg/core/migrations/m018_xai_config.py index bf422451..db5ed5bf 100644 --- a/pkg/core/migrations/m018_xai_config.py +++ b/pkg/core/migrations/m018_xai_config.py @@ -3,7 +3,7 @@ from __future__ import annotations from .. import migration -@migration.migration_class("xai-config", 18) +@migration.migration_class('xai-config', 18) class XaiConfigMigration(migration.Migration): """迁移""" @@ -14,12 +14,10 @@ class XaiConfigMigration(migration.Migration): async def run(self): """执行迁移""" self.ap.provider_cfg.data['requester']['xai-chat-completions'] = { - "base-url": "https://api.x.ai/v1", - "args": {}, - "timeout": 120 + 'base-url': 'https://api.x.ai/v1', + 'args': {}, + 'timeout': 120, } - self.ap.provider_cfg.data['keys']['xai'] = [ - "xai-1234567890" - ] + self.ap.provider_cfg.data['keys']['xai'] = ['xai-1234567890'] await self.ap.provider_cfg.dump_config() diff --git a/pkg/core/migrations/m019_zhipuai_config.py b/pkg/core/migrations/m019_zhipuai_config.py index 67f33340..081d8dcf 100644 --- a/pkg/core/migrations/m019_zhipuai_config.py +++ b/pkg/core/migrations/m019_zhipuai_config.py @@ -3,7 +3,7 @@ from __future__ import annotations from .. import migration -@migration.migration_class("zhipuai-config", 19) +@migration.migration_class('zhipuai-config', 19) class ZhipuaiConfigMigration(migration.Migration): """迁移""" @@ -14,12 +14,10 @@ class ZhipuaiConfigMigration(migration.Migration): async def run(self): """执行迁移""" self.ap.provider_cfg.data['requester']['zhipuai-chat-completions'] = { - "base-url": "https://open.bigmodel.cn/api/paas/v4", - "args": {}, - "timeout": 120 + 'base-url': 'https://open.bigmodel.cn/api/paas/v4', + 'args': {}, + 'timeout': 120, } - self.ap.provider_cfg.data['keys']['zhipuai'] = [ - "xxxxxxx" - ] + self.ap.provider_cfg.data['keys']['zhipuai'] = ['xxxxxxx'] await self.ap.provider_cfg.dump_config() diff --git a/pkg/core/migrations/m020_wecom_config.py b/pkg/core/migrations/m020_wecom_config.py index 9581cb91..3e833d3e 100644 --- a/pkg/core/migrations/m020_wecom_config.py +++ b/pkg/core/migrations/m020_wecom_config.py @@ -3,13 +3,13 @@ from __future__ import annotations from .. import migration -@migration.migration_class("wecom-config", 20) +@migration.migration_class('wecom-config', 20) class WecomConfigMigration(migration.Migration): """迁移""" async def need_migrate(self) -> bool: """判断当前环境是否需要运行此迁移""" - + # for adapter in self.ap.platform_cfg.data['platform-adapters']: # if adapter['adapter'] == 'wecom': # return False @@ -19,16 +19,18 @@ class WecomConfigMigration(migration.Migration): async def run(self): """执行迁移""" - self.ap.platform_cfg.data['platform-adapters'].append({ - "adapter": "wecom", - "enable": False, - "host": "0.0.0.0", - "port": 2290, - "corpid": "", - "secret": "", - "token": "", - "EncodingAESKey": "", - "contacts_secret": "" - }) + self.ap.platform_cfg.data['platform-adapters'].append( + { + 'adapter': 'wecom', + 'enable': False, + 'host': '0.0.0.0', + 'port': 2290, + 'corpid': '', + 'secret': '', + 'token': '', + 'EncodingAESKey': '', + 'contacts_secret': '', + } + ) await self.ap.platform_cfg.dump_config() diff --git a/pkg/core/migrations/m021_lark_config.py b/pkg/core/migrations/m021_lark_config.py index 49d9bb8f..04f29db4 100644 --- a/pkg/core/migrations/m021_lark_config.py +++ b/pkg/core/migrations/m021_lark_config.py @@ -3,13 +3,13 @@ from __future__ import annotations from .. import migration -@migration.migration_class("lark-config", 21) +@migration.migration_class('lark-config', 21) class LarkConfigMigration(migration.Migration): """迁移""" async def need_migrate(self) -> bool: """判断当前环境是否需要运行此迁移""" - + # for adapter in self.ap.platform_cfg.data['platform-adapters']: # if adapter['adapter'] == 'lark': # return False @@ -19,15 +19,17 @@ class LarkConfigMigration(migration.Migration): async def run(self): """执行迁移""" - self.ap.platform_cfg.data['platform-adapters'].append({ - "adapter": "lark", - "enable": False, - "app_id": "cli_abcdefgh", - "app_secret": "XXXXXXXXXX", - "bot_name": "LangBot", - "enable-webhook": False, - "port": 2285, - "encrypt-key": "xxxxxxxxx" - }) + self.ap.platform_cfg.data['platform-adapters'].append( + { + 'adapter': 'lark', + 'enable': False, + 'app_id': 'cli_abcdefgh', + 'app_secret': 'XXXXXXXXXX', + 'bot_name': 'LangBot', + 'enable-webhook': False, + 'port': 2285, + 'encrypt-key': 'xxxxxxxxx', + } + ) await self.ap.platform_cfg.dump_config() diff --git a/pkg/core/migrations/m022_lmstudio_config.py b/pkg/core/migrations/m022_lmstudio_config.py index 5506b37b..bffc6bb8 100644 --- a/pkg/core/migrations/m022_lmstudio_config.py +++ b/pkg/core/migrations/m022_lmstudio_config.py @@ -3,21 +3,21 @@ from __future__ import annotations from .. import migration -@migration.migration_class("lmstudio-config", 22) +@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 + 'base-url': 'http://127.0.0.1:1234/v1', + 'args': {}, + 'timeout': 120, } await self.ap.provider_cfg.dump_config() diff --git a/pkg/core/migrations/m023_siliconflow_config.py b/pkg/core/migrations/m023_siliconflow_config.py index a0e65c6a..fdf696eb 100644 --- a/pkg/core/migrations/m023_siliconflow_config.py +++ b/pkg/core/migrations/m023_siliconflow_config.py @@ -3,25 +3,25 @@ from __future__ import annotations from .. import migration -@migration.migration_class("siliconflow-config", 23) +@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'] + + 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['keys']['siliconflow'] = ['xxxxxxx'] self.ap.provider_cfg.data['requester']['siliconflow-chat-completions'] = { - "base-url": "https://api.siliconflow.cn/v1", - "args": {}, - "timeout": 120 + 'base-url': 'https://api.siliconflow.cn/v1', + 'args': {}, + 'timeout': 120, } await self.ap.provider_cfg.dump_config() diff --git a/pkg/core/migrations/m024_discord_config.py b/pkg/core/migrations/m024_discord_config.py index fcfac6e6..ebcae232 100644 --- a/pkg/core/migrations/m024_discord_config.py +++ b/pkg/core/migrations/m024_discord_config.py @@ -3,13 +3,13 @@ from __future__ import annotations from .. import migration -@migration.migration_class("discord-config", 24) +@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 @@ -19,11 +19,13 @@ class DiscordConfigMigration(migration.Migration): async def run(self): """执行迁移""" - self.ap.platform_cfg.data['platform-adapters'].append({ - "adapter": "discord", - "enable": False, - "client_id": "1234567890", - "token": "XXXXXXXXXX" - }) + self.ap.platform_cfg.data['platform-adapters'].append( + { + 'adapter': 'discord', + 'enable': False, + 'client_id': '1234567890', + 'token': 'XXXXXXXXXX', + } + ) await self.ap.platform_cfg.dump_config() diff --git a/pkg/core/migrations/m025_gewechat_config.py b/pkg/core/migrations/m025_gewechat_config.py index 65b5c1d5..bb729854 100644 --- a/pkg/core/migrations/m025_gewechat_config.py +++ b/pkg/core/migrations/m025_gewechat_config.py @@ -3,13 +3,13 @@ from __future__ import annotations from .. import migration -@migration.migration_class("gewechat-config", 25) +@migration.migration_class('gewechat-config', 25) class GewechatConfigMigration(migration.Migration): """迁移""" async def need_migrate(self) -> bool: """判断当前环境是否需要运行此迁移""" - + # for adapter in self.ap.platform_cfg.data['platform-adapters']: # if adapter['adapter'] == 'gewechat': # return False @@ -19,15 +19,17 @@ class GewechatConfigMigration(migration.Migration): async def run(self): """执行迁移""" - self.ap.platform_cfg.data['platform-adapters'].append({ - "adapter": "gewechat", - "enable": False, - "gewechat_url": "http://your-gewechat-server:2531", - "gewechat_file_url": "http://your-gewechat-server:2532", - "port": 2286, - "callback_url": "http://your-callback-url:2286/gewechat/callback", - "app_id": "", - "token": "" - }) + self.ap.platform_cfg.data['platform-adapters'].append( + { + 'adapter': 'gewechat', + 'enable': False, + 'gewechat_url': 'http://your-gewechat-server:2531', + 'gewechat_file_url': 'http://your-gewechat-server:2532', + 'port': 2286, + 'callback_url': 'http://your-callback-url:2286/gewechat/callback', + 'app_id': '', + 'token': '', + } + ) await self.ap.platform_cfg.dump_config() diff --git a/pkg/core/migrations/m026_qqofficial_config.py b/pkg/core/migrations/m026_qqofficial_config.py index b4745806..90674341 100644 --- a/pkg/core/migrations/m026_qqofficial_config.py +++ b/pkg/core/migrations/m026_qqofficial_config.py @@ -3,13 +3,13 @@ from __future__ import annotations from .. import migration -@migration.migration_class("qqofficial-config", 26) +@migration.migration_class('qqofficial-config', 26) class QQOfficialConfigMigration(migration.Migration): """迁移""" async def need_migrate(self) -> bool: """判断当前环境是否需要运行此迁移""" - + # for adapter in self.ap.platform_cfg.data['platform-adapters']: # if adapter['adapter'] == 'qqofficial': # return False @@ -19,13 +19,15 @@ class QQOfficialConfigMigration(migration.Migration): async def run(self): """执行迁移""" - self.ap.platform_cfg.data['platform-adapters'].append({ - "adapter": "qqofficial", - "enable": False, - "appid": "", - "secret": "", - "port": 2284, - "token": "" - }) + self.ap.platform_cfg.data['platform-adapters'].append( + { + 'adapter': 'qqofficial', + 'enable': False, + 'appid': '', + 'secret': '', + 'port': 2284, + 'token': '', + } + ) await self.ap.platform_cfg.dump_config() diff --git a/pkg/core/migrations/m027_wx_official_account_config.py b/pkg/core/migrations/m027_wx_official_account_config.py index 5abaad87..7c5b0e35 100644 --- a/pkg/core/migrations/m027_wx_official_account_config.py +++ b/pkg/core/migrations/m027_wx_official_account_config.py @@ -3,13 +3,13 @@ from __future__ import annotations from .. import migration -@migration.migration_class("wx-official-account-config", 27) +@migration.migration_class('wx-official-account-config', 27) class WXOfficialAccountConfigMigration(migration.Migration): """迁移""" async def need_migrate(self) -> bool: """判断当前环境是否需要运行此迁移""" - + # for adapter in self.ap.platform_cfg.data['platform-adapters']: # if adapter['adapter'] == 'officialaccount': # return False @@ -19,15 +19,17 @@ class WXOfficialAccountConfigMigration(migration.Migration): async def run(self): """执行迁移""" - self.ap.platform_cfg.data['platform-adapters'].append({ - "adapter": "officialaccount", - "enable": False, - "token": "", - "EncodingAESKey": "", - "AppID": "", - "AppSecret": "", - "host": "0.0.0.0", - "port": 2287 - }) + self.ap.platform_cfg.data['platform-adapters'].append( + { + 'adapter': 'officialaccount', + 'enable': False, + 'token': '', + 'EncodingAESKey': '', + 'AppID': '', + 'AppSecret': '', + 'host': '0.0.0.0', + 'port': 2287, + } + ) await self.ap.platform_cfg.dump_config() diff --git a/pkg/core/migrations/m028_aliyun_requester_config.py b/pkg/core/migrations/m028_aliyun_requester_config.py index f28bc04f..8d80727a 100644 --- a/pkg/core/migrations/m028_aliyun_requester_config.py +++ b/pkg/core/migrations/m028_aliyun_requester_config.py @@ -3,25 +3,23 @@ from __future__ import annotations from .. import migration -@migration.migration_class("bailian-requester-config", 28) +@migration.migration_class('bailian-requester-config', 28) class BailianRequesterConfigMigration(migration.Migration): """迁移""" async def need_migrate(self) -> bool: """判断当前环境是否需要运行此迁移""" - + return 'bailian-chat-completions' not in self.ap.provider_cfg.data['requester'] async def run(self): """执行迁移""" - self.ap.provider_cfg.data['keys']['bailian'] = [ - "sk-xxxxxxx" - ] + self.ap.provider_cfg.data['keys']['bailian'] = ['sk-xxxxxxx'] self.ap.provider_cfg.data['requester']['bailian-chat-completions'] = { - "base-url": "https://dashscope.aliyuncs.com/compatible-mode/v1", - "args": {}, - "timeout": 120 + 'base-url': 'https://dashscope.aliyuncs.com/compatible-mode/v1', + 'args': {}, + 'timeout': 120, } await self.ap.provider_cfg.dump_config() diff --git a/pkg/core/migrations/m029_dashscope_app_api_config.py b/pkg/core/migrations/m029_dashscope_app_api_config.py index 3a069bac..5a61fe0d 100644 --- a/pkg/core/migrations/m029_dashscope_app_api_config.py +++ b/pkg/core/migrations/m029_dashscope_app_api_config.py @@ -3,7 +3,7 @@ from __future__ import annotations from .. import migration -@migration.migration_class("dashscope-app-api-config", 29) +@migration.migration_class('dashscope-app-api-config', 29) class DashscopeAppAPICfgMigration(migration.Migration): """迁移""" @@ -14,20 +14,14 @@ class DashscopeAppAPICfgMigration(migration.Migration): async def run(self): """执行迁移""" self.ap.provider_cfg.data['dashscope-app-api'] = { - "app-type": "agent", - "api-key": "sk-1234567890", - "agent": { - "app-id": "Your_app_id", - "references_quote": "参考资料来自:" + 'app-type': 'agent', + 'api-key': 'sk-1234567890', + 'agent': {'app-id': 'Your_app_id', 'references_quote': '参考资料来自:'}, + 'workflow': { + 'app-id': 'Your_app_id', + 'references_quote': '参考资料来自:', + 'biz_params': {'city': '北京', 'date': '2023-08-10'}, }, - "workflow": { - "app-id": "Your_app_id", - "references_quote": "参考资料来自:", - "biz_params": { - "city": "北京", - "date": "2023-08-10" - } - } } await self.ap.provider_cfg.dump_config() diff --git a/pkg/core/migrations/m030_lark_config_cmpl.py b/pkg/core/migrations/m030_lark_config_cmpl.py index e016af7b..37e8fabe 100644 --- a/pkg/core/migrations/m030_lark_config_cmpl.py +++ b/pkg/core/migrations/m030_lark_config_cmpl.py @@ -3,13 +3,13 @@ from __future__ import annotations from .. import migration -@migration.migration_class("lark-config-cmpl", 30) +@migration.migration_class('lark-config-cmpl', 30) class LarkConfigCmplMigration(migration.Migration): """迁移""" async def need_migrate(self) -> bool: """判断当前环境是否需要运行此迁移""" - + for adapter in self.ap.platform_cfg.data['platform-adapters']: if adapter['adapter'] == 'lark': if 'enable-webhook' not in adapter: @@ -26,6 +26,6 @@ class LarkConfigCmplMigration(migration.Migration): if 'port' not in adapter: adapter['port'] = 2285 if 'encrypt-key' not in adapter: - adapter['encrypt-key'] = "xxxxxxxxx" + adapter['encrypt-key'] = 'xxxxxxxxx' await self.ap.platform_cfg.dump_config() diff --git a/pkg/core/migrations/m031_dingtalk_config.py b/pkg/core/migrations/m031_dingtalk_config.py index 7dbc4735..22ba0bbf 100644 --- a/pkg/core/migrations/m031_dingtalk_config.py +++ b/pkg/core/migrations/m031_dingtalk_config.py @@ -3,13 +3,13 @@ from __future__ import annotations from .. import migration -@migration.migration_class("dingtalk-config", 31) +@migration.migration_class('dingtalk-config', 31) class DingTalkConfigMigration(migration.Migration): """迁移""" async def need_migrate(self) -> bool: """判断当前环境是否需要运行此迁移""" - + # for adapter in self.ap.platform_cfg.data['platform-adapters']: # if adapter['adapter'] == 'dingtalk': # return False @@ -19,13 +19,15 @@ class DingTalkConfigMigration(migration.Migration): async def run(self): """执行迁移""" - self.ap.platform_cfg.data['platform-adapters'].append({ - "adapter": "dingtalk", - "enable": False, - "client_id": "", - "client_secret": "", - "robot_code": "", - "robot_name": "" - }) + self.ap.platform_cfg.data['platform-adapters'].append( + { + 'adapter': 'dingtalk', + 'enable': False, + 'client_id': '', + 'client_secret': '', + 'robot_code': '', + 'robot_name': '', + } + ) await self.ap.platform_cfg.dump_config() diff --git a/pkg/core/migrations/m032_volcark_config.py b/pkg/core/migrations/m032_volcark_config.py index a07e5686..ae8feb52 100644 --- a/pkg/core/migrations/m032_volcark_config.py +++ b/pkg/core/migrations/m032_volcark_config.py @@ -3,25 +3,23 @@ from __future__ import annotations from .. import migration -@migration.migration_class("volcark-requester-config", 32) +@migration.migration_class('volcark-requester-config', 32) class VolcArkRequesterConfigMigration(migration.Migration): """迁移""" async def need_migrate(self) -> bool: """判断当前环境是否需要运行此迁移""" - + return 'volcark-chat-completions' not in self.ap.provider_cfg.data['requester'] async def run(self): """执行迁移""" - self.ap.provider_cfg.data['keys']['volcark'] = [ - "xxxxxxxx" - ] + self.ap.provider_cfg.data['keys']['volcark'] = ['xxxxxxxx'] self.ap.provider_cfg.data['requester']['volcark-chat-completions'] = { - "base-url": "https://ark.cn-beijing.volces.com/api/v3", - "args": {}, - "timeout": 120 + 'base-url': 'https://ark.cn-beijing.volces.com/api/v3', + 'args': {}, + 'timeout': 120, } await self.ap.provider_cfg.dump_config() diff --git a/pkg/core/migrations/m033_dify_thinking_config.py b/pkg/core/migrations/m033_dify_thinking_config.py index 1f663b46..d25a4aad 100644 --- a/pkg/core/migrations/m033_dify_thinking_config.py +++ b/pkg/core/migrations/m033_dify_thinking_config.py @@ -3,24 +3,27 @@ from __future__ import annotations from .. import migration -@migration.migration_class("dify-thinking-config", 33) +@migration.migration_class('dify-thinking-config', 33) class DifyThinkingConfigMigration(migration.Migration): """迁移""" async def need_migrate(self) -> bool: """判断当前环境是否需要运行此迁移""" - - if 'options' not in self.ap.provider_cfg.data["dify-service-api"]: + + if 'options' not in self.ap.provider_cfg.data['dify-service-api']: return True - if 'convert-thinking-tips' not in self.ap.provider_cfg.data["dify-service-api"]["options"]: + if ( + 'convert-thinking-tips' + not in self.ap.provider_cfg.data['dify-service-api']['options'] + ): return True return False - + async def run(self): """执行迁移""" - self.ap.provider_cfg.data["dify-service-api"]["options"] = { - "convert-thinking-tips": "plain" + self.ap.provider_cfg.data['dify-service-api']['options'] = { + 'convert-thinking-tips': 'plain' } await self.ap.provider_cfg.dump_config() diff --git a/pkg/core/migrations/m034_gewechat_file_url_config.py b/pkg/core/migrations/m034_gewechat_file_url_config.py index 44bbd65e..8c3e0a83 100644 --- a/pkg/core/migrations/m034_gewechat_file_url_config.py +++ b/pkg/core/migrations/m034_gewechat_file_url_config.py @@ -5,7 +5,7 @@ from urllib.parse import urlparse from .. import migration -@migration.migration_class("gewechat-file-url-config", 34) +@migration.migration_class('gewechat-file-url-config', 34) class GewechatFileUrlConfigMigration(migration.Migration): """迁移""" @@ -24,6 +24,8 @@ class GewechatFileUrlConfigMigration(migration.Migration): if adapter['adapter'] == 'gewechat': if 'gewechat_file_url' not in adapter: parsed_url = urlparse(adapter['gewechat_url']) - adapter['gewechat_file_url'] = f"{parsed_url.scheme}://{parsed_url.hostname}:2532" + adapter['gewechat_file_url'] = ( + f'{parsed_url.scheme}://{parsed_url.hostname}:2532' + ) await self.ap.platform_cfg.dump_config() diff --git a/pkg/core/migrations/m035_wxoa_mode.py b/pkg/core/migrations/m035_wxoa_mode.py index ce0ce628..6b675e30 100644 --- a/pkg/core/migrations/m035_wxoa_mode.py +++ b/pkg/core/migrations/m035_wxoa_mode.py @@ -3,7 +3,7 @@ from __future__ import annotations from .. import migration -@migration.migration_class("wxoa-mode", 35) +@migration.migration_class('wxoa-mode', 35) class WxoaModeMigration(migration.Migration): """迁移""" diff --git a/pkg/core/migrations/m036_wxoa_loading_message.py b/pkg/core/migrations/m036_wxoa_loading_message.py index 682be435..29ecba20 100644 --- a/pkg/core/migrations/m036_wxoa_loading_message.py +++ b/pkg/core/migrations/m036_wxoa_loading_message.py @@ -3,7 +3,7 @@ from __future__ import annotations from .. import migration -@migration.migration_class("wxoa-loading-message", 36) +@migration.migration_class('wxoa-loading-message', 36) class WxoaLoadingMessageMigration(migration.Migration): """迁移""" diff --git a/pkg/core/migrations/m037_mcp_config.py b/pkg/core/migrations/m037_mcp_config.py index f045f0ff..3752193e 100644 --- a/pkg/core/migrations/m037_mcp_config.py +++ b/pkg/core/migrations/m037_mcp_config.py @@ -3,7 +3,7 @@ from __future__ import annotations from .. import migration -@migration.migration_class("mcp-config", 37) +@migration.migration_class('mcp-config', 37) class MCPConfigMigration(migration.Migration): """迁移""" @@ -13,8 +13,6 @@ class MCPConfigMigration(migration.Migration): async def run(self): """执行迁移""" - self.ap.provider_cfg.data['mcp'] = { - "servers": [] - } + self.ap.provider_cfg.data['mcp'] = {'servers': []} await self.ap.provider_cfg.dump_config() diff --git a/pkg/core/note.py b/pkg/core/note.py index 6ffbff51..07171581 100644 --- a/pkg/core/note.py +++ b/pkg/core/note.py @@ -7,9 +7,10 @@ from . import app preregistered_notes: list[typing.Type[LaunchNote]] = [] + def note_class(name: str, number: int): - """注册一个启动信息 - """ + """注册一个启动信息""" + def decorator(cls: typing.Type[LaunchNote]) -> typing.Type[LaunchNote]: cls.name = name cls.number = number @@ -20,8 +21,8 @@ def note_class(name: str, number: int): class LaunchNote(abc.ABC): - """启动信息 - """ + """启动信息""" + name: str number: int @@ -33,12 +34,10 @@ class LaunchNote(abc.ABC): @abc.abstractmethod async def need_show(self) -> bool: - """判断当前环境是否需要显示此启动信息 - """ + """判断当前环境是否需要显示此启动信息""" pass @abc.abstractmethod async def yield_note(self) -> typing.AsyncGenerator[typing.Tuple[str, int], None]: - """生成启动信息 - """ + """生成启动信息""" pass diff --git a/pkg/core/notes/n001_classic_msgs.py b/pkg/core/notes/n001_classic_msgs.py index bdc5c44e..3f3bd8e0 100644 --- a/pkg/core/notes/n001_classic_msgs.py +++ b/pkg/core/notes/n001_classic_msgs.py @@ -2,19 +2,17 @@ from __future__ import annotations import typing -from .. import note, app +from .. import note -@note.note_class("ClassicNotes", 1) +@note.note_class('ClassicNotes', 1) class ClassicNotes(note.LaunchNote): - """经典启动信息 - """ + """经典启动信息""" async def need_show(self) -> bool: return True async def yield_note(self) -> typing.AsyncGenerator[typing.Tuple[str, int], None]: - yield await self.ap.ann_mgr.show_announcements() - yield await self.ap.ver_mgr.show_version_update() \ No newline at end of file + yield await self.ap.ver_mgr.show_version_update() diff --git a/pkg/core/notes/n002_selection_mode_on_windows.py b/pkg/core/notes/n002_selection_mode_on_windows.py index 961d697d..23bff24a 100644 --- a/pkg/core/notes/n002_selection_mode_on_windows.py +++ b/pkg/core/notes/n002_selection_mode_on_windows.py @@ -2,20 +2,20 @@ from __future__ import annotations import typing import os -import sys import logging -from .. import note, app +from .. import note -@note.note_class("SelectionModeOnWindows", 2) +@note.note_class('SelectionModeOnWindows', 2) class SelectionModeOnWindows(note.LaunchNote): - """Windows 上的选择模式提示信息 - """ + """Windows 上的选择模式提示信息""" async def need_show(self) -> bool: return os.name == 'nt' async def yield_note(self) -> typing.AsyncGenerator[typing.Tuple[str, int], None]: - - yield """您正在使用 Windows 系统,若窗口左上角显示处于”选择“模式,程序将被暂停运行,此时请右键窗口中空白区域退出选择模式。""", logging.INFO + yield ( + """您正在使用 Windows 系统,若窗口左上角显示处于”选择“模式,程序将被暂停运行,此时请右键窗口中空白区域退出选择模式。""", + logging.INFO, + ) diff --git a/pkg/core/notes/n003_print_version.py b/pkg/core/notes/n003_print_version.py index 91208fdf..18eebf4f 100644 --- a/pkg/core/notes/n003_print_version.py +++ b/pkg/core/notes/n003_print_version.py @@ -1,21 +1,17 @@ from __future__ import annotations import typing -import os -import sys import logging -from .. import note, app +from .. import note -@note.note_class("PrintVersion", 3) +@note.note_class('PrintVersion', 3) class PrintVersion(note.LaunchNote): - """Print Version Information - """ + """Print Version Information""" async def need_show(self) -> bool: return True async def yield_note(self) -> typing.AsyncGenerator[typing.Tuple[str, int], None]: - - yield f"Current Version: {self.ap.ver_mgr.get_current_version()}", logging.INFO + yield f'Current Version: {self.ap.ver_mgr.get_current_version()}', logging.INFO diff --git a/pkg/core/stage.py b/pkg/core/stage.py index f1c65295..220c474d 100644 --- a/pkg/core/stage.py +++ b/pkg/core/stage.py @@ -12,9 +12,8 @@ preregistered_stages: dict[str, typing.Type[BootingStage]] = {} 当前阶段暂不支持扩展 """ -def stage_class( - name: str -): + +def stage_class(name: str): def decorator(cls: typing.Type[BootingStage]) -> typing.Type[BootingStage]: preregistered_stages[name] = cls return cls @@ -23,12 +22,11 @@ def stage_class( class BootingStage(abc.ABC): - """启动阶段 - """ + """启动阶段""" + name: str = None @abc.abstractmethod async def run(self, ap: app.Application): - """启动 - """ + """启动""" pass diff --git a/pkg/core/stages/build_app.py b/pkg/core/stages/build_app.py index 7d62e9c9..5dee9386 100644 --- a/pkg/core/stages/build_app.py +++ b/pkg/core/stages/build_app.py @@ -1,6 +1,5 @@ from __future__ import annotations -import sys from .. import stage, app from ...utils import version, proxy, announce, platform @@ -24,26 +23,22 @@ from ...utils import logcache from .. import taskmgr -@stage.stage_class("BuildAppStage") +@stage.stage_class('BuildAppStage') class BuildAppStage(stage.BootingStage): - """构建应用阶段 - """ + """构建应用阶段""" async def run(self, ap: app.Application): - """构建app对象的各个组件对象并初始化 - """ + """构建app对象的各个组件对象并初始化""" ap.task_mgr = taskmgr.AsyncTaskManager(ap) discover = discover_engine.ComponentDiscoveryEngine(ap) - discover.discover_blueprint( - "components.yaml" - ) + discover.discover_blueprint('components.yaml') ap.discover = discover proxy_mgr = proxy.ProxyManager(ap) await proxy_mgr.initialize() ap.proxy_mgr = proxy_mgr - + ver_mgr = version.VersionManager(ap) await ver_mgr.initialize() ap.ver_mgr = ver_mgr @@ -52,14 +47,14 @@ class BuildAppStage(stage.BootingStage): ap, backend_url=ap.instance_config.data['telemetry']['url'], basic_info={ - "host_id": identifier.identifier["host_id"], - "instance_id": identifier.identifier["instance_id"], - "semantic_version": ver_mgr.get_current_version(), - "platform": platform.get_platform(), + 'host_id': identifier.identifier['host_id'], + 'instance_id': identifier.identifier['instance_id'], + 'semantic_version': ver_mgr.get_current_version(), + 'platform': platform.get_platform(), }, runtime_info={ - "admin_id": "{}".format(ap.instance_config.data["admins"]), - "msg_source": str([]), + 'admin_id': '{}'.format(ap.instance_config.data['admins']), + 'msg_source': str([]), }, ) ap.ctr_mgr = center_v2_api diff --git a/pkg/core/stages/genkeys.py b/pkg/core/stages/genkeys.py index 843f1532..c24ebd70 100644 --- a/pkg/core/stages/genkeys.py +++ b/pkg/core/stages/genkeys.py @@ -1,20 +1,17 @@ from __future__ import annotations import secrets -import os from .. import stage, app -@stage.stage_class("GenKeysStage") +@stage.stage_class('GenKeysStage') class GenKeysStage(stage.BootingStage): - """生成密钥阶段 - """ + """生成密钥阶段""" async def run(self, ap: app.Application): - """启动 - """ - + """启动""" + if not ap.instance_config.data['system']['jwt']['secret']: ap.instance_config.data['system']['jwt']['secret'] = secrets.token_hex(16) await ap.instance_config.dump_config() diff --git a/pkg/core/stages/load_config.py b/pkg/core/stages/load_config.py index edfe5915..ac2f0c37 100644 --- a/pkg/core/stages/load_config.py +++ b/pkg/core/stages/load_config.py @@ -7,45 +7,80 @@ from .. import stage, app from ..bootutils import config -@stage.stage_class("LoadConfigStage") +@stage.stage_class('LoadConfigStage') class LoadConfigStage(stage.BootingStage): - """加载配置文件阶段 - """ + """加载配置文件阶段""" async def run(self, ap: app.Application): - """启动 - """ + """启动""" # ======= deprecated ======= - if os.path.exists("data/config/command.json"): - ap.command_cfg = await config.load_json_config("data/config/command.json", "templates/legacy/command.json", completion=False) + if os.path.exists('data/config/command.json'): + ap.command_cfg = await config.load_json_config( + 'data/config/command.json', + 'templates/legacy/command.json', + completion=False, + ) - if os.path.exists("data/config/pipeline.json"): - ap.pipeline_cfg = await config.load_json_config("data/config/pipeline.json", "templates/legacy/pipeline.json", completion=False) + if os.path.exists('data/config/pipeline.json'): + ap.pipeline_cfg = await config.load_json_config( + 'data/config/pipeline.json', + 'templates/legacy/pipeline.json', + completion=False, + ) - if os.path.exists("data/config/platform.json"): - ap.platform_cfg = await config.load_json_config("data/config/platform.json", "templates/legacy/platform.json", completion=False) + if os.path.exists('data/config/platform.json'): + ap.platform_cfg = await config.load_json_config( + 'data/config/platform.json', + 'templates/legacy/platform.json', + completion=False, + ) - if os.path.exists("data/config/provider.json"): - ap.provider_cfg = await config.load_json_config("data/config/provider.json", "templates/legacy/provider.json", completion=False) + if os.path.exists('data/config/provider.json'): + ap.provider_cfg = await config.load_json_config( + 'data/config/provider.json', + 'templates/legacy/provider.json', + completion=False, + ) - if os.path.exists("data/config/system.json"): - ap.system_cfg = await config.load_json_config("data/config/system.json", "templates/legacy/system.json", completion=False) + if os.path.exists('data/config/system.json'): + ap.system_cfg = await config.load_json_config( + 'data/config/system.json', + 'templates/legacy/system.json', + completion=False, + ) - if os.path.exists("data/metadata/instance-secret.json"): - ap.instance_secret_meta = await config.load_json_config("data/metadata/instance-secret.json", template_data={ - 'jwt_secret': secrets.token_hex(16) - }) + if os.path.exists('data/metadata/instance-secret.json'): + ap.instance_secret_meta = await config.load_json_config( + 'data/metadata/instance-secret.json', + template_data={'jwt_secret': secrets.token_hex(16)}, + ) await ap.instance_secret_meta.dump_config() # ======= deprecated ======= - ap.instance_config = await config.load_yaml_config("data/config.yaml", "templates/config.yaml", completion=False) + ap.instance_config = await config.load_yaml_config( + 'data/config.yaml', 'templates/config.yaml', completion=False + ) await ap.instance_config.dump_config() - ap.sensitive_meta = await config.load_json_config("data/metadata/sensitive-words.json", "templates/metadata/sensitive-words.json") + ap.sensitive_meta = await config.load_json_config( + 'data/metadata/sensitive-words.json', + 'templates/metadata/sensitive-words.json', + ) await ap.sensitive_meta.dump_config() - ap.pipeline_config_meta_trigger = await config.load_yaml_config("templates/metadata/pipeline/trigger.yaml", "templates/metadata/pipeline/trigger.yaml") - ap.pipeline_config_meta_safety = await config.load_yaml_config("templates/metadata/pipeline/safety.yaml", "templates/metadata/pipeline/safety.yaml") - ap.pipeline_config_meta_ai = await config.load_yaml_config("templates/metadata/pipeline/ai.yaml", "templates/metadata/pipeline/ai.yaml") - ap.pipeline_config_meta_output = await config.load_yaml_config("templates/metadata/pipeline/output.yaml", "templates/metadata/pipeline/output.yaml") + ap.pipeline_config_meta_trigger = await config.load_yaml_config( + 'templates/metadata/pipeline/trigger.yaml', + 'templates/metadata/pipeline/trigger.yaml', + ) + ap.pipeline_config_meta_safety = await config.load_yaml_config( + 'templates/metadata/pipeline/safety.yaml', + 'templates/metadata/pipeline/safety.yaml', + ) + ap.pipeline_config_meta_ai = await config.load_yaml_config( + 'templates/metadata/pipeline/ai.yaml', 'templates/metadata/pipeline/ai.yaml' + ) + ap.pipeline_config_meta_output = await config.load_yaml_config( + 'templates/metadata/pipeline/output.yaml', + 'templates/metadata/pipeline/output.yaml', + ) diff --git a/pkg/core/stages/migrate.py b/pkg/core/stages/migrate.py index e902431a..02b03256 100644 --- a/pkg/core/stages/migrate.py +++ b/pkg/core/stages/migrate.py @@ -1,37 +1,30 @@ from __future__ import annotations -import importlib -import os from .. import stage, app from .. import migration -from ..migrations import m001_sensitive_word_migration, m002_openai_config_migration, m003_anthropic_requester_cfg_completion, m004_moonshot_cfg_completion -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 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, m022_lmstudio_config, m023_siliconflow_config, m024_discord_config, m025_gewechat_config -from ..migrations import m026_qqofficial_config, m027_wx_official_account_config, m028_aliyun_requester_config -from ..migrations import m029_dashscope_app_api_config, m030_lark_config_cmpl, m031_dingtalk_config, m032_volcark_config -from ..migrations import m033_dify_thinking_config, m034_gewechat_file_url_config, m035_wxoa_mode, m036_wxoa_loading_message -from ..migrations import m037_mcp_config +from ...utils import importutil +from .. import migrations + +importutil.import_modules_in_pkg(migrations) -@stage.stage_class("MigrationStage") +@stage.stage_class('MigrationStage') class MigrationStage(stage.BootingStage): - """迁移阶段 - """ + """迁移阶段""" async def run(self, ap: app.Application): - """启动 - """ + """启动""" - if any([ - ap.command_cfg is None, - ap.pipeline_cfg is None, - ap.platform_cfg is None, - ap.provider_cfg is None, - ap.system_cfg is None, - ]): # only run migration when version is 3.x + if any( + [ + ap.command_cfg is None, + ap.pipeline_cfg is None, + ap.platform_cfg is None, + ap.provider_cfg is None, + ap.system_cfg is None, + ] + ): # only run migration when version is 3.x return migrations = migration.preregistered_migrations diff --git a/pkg/core/stages/setup_logger.py b/pkg/core/stages/setup_logger.py index 8f385d1f..0c630175 100644 --- a/pkg/core/stages/setup_logger.py +++ b/pkg/core/stages/setup_logger.py @@ -1,8 +1,6 @@ from __future__ import annotations import logging -import asyncio -from datetime import datetime from .. import stage, app from ..bootutils import log @@ -12,6 +10,7 @@ class PersistenceHandler(logging.Handler, object): """ 保存日志到数据库 """ + ap: app.Application def __init__(self, name, ap: app.Application): @@ -28,19 +27,17 @@ class PersistenceHandler(logging.Handler, object): msg = self.format(record) if self.ap.log_cache is not None: self.ap.log_cache.add_log(msg) - + except Exception: self.handleError(record) -@stage.stage_class("SetupLoggerStage") +@stage.stage_class('SetupLoggerStage') class SetupLoggerStage(stage.BootingStage): - """设置日志器阶段 - """ + """设置日志器阶段""" async def run(self, ap: app.Application): - """启动 - """ + """启动""" persistence_handler = PersistenceHandler('LoggerHandler', ap) extra_handlers = [] diff --git a/pkg/core/stages/show_notes.py b/pkg/core/stages/show_notes.py index 63d8f580..e7c98b42 100644 --- a/pkg/core/stages/show_notes.py +++ b/pkg/core/stages/show_notes.py @@ -1,16 +1,18 @@ from __future__ import annotations from .. import stage, app, note -from ..notes import n001_classic_msgs, n002_selection_mode_on_windows, n003_print_version +from ...utils import importutil + +from .. import notes + +importutil.import_modules_in_pkg(notes) -@stage.stage_class("ShowNotesStage") +@stage.stage_class('ShowNotesStage') class ShowNotesStage(stage.BootingStage): - """显示启动信息阶段 - """ + """显示启动信息阶段""" async def run(self, ap: app.Application): - # 排序 note.preregistered_notes.sort(key=lambda x: x.number) @@ -24,5 +26,5 @@ class ShowNotesStage(stage.BootingStage): msg, level = ret if msg: ap.logger.log(level, msg) - except Exception as e: + except Exception: continue diff --git a/pkg/core/taskmgr.py b/pkg/core/taskmgr.py index d5887019..ae2394cf 100644 --- a/pkg/core/taskmgr.py +++ b/pkg/core/taskmgr.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio import typing import datetime -import traceback from . import app from . import entities as core_entities @@ -19,11 +18,11 @@ class TaskContext: """记录日志""" def __init__(self): - self.current_action = "default" - self.log = "" + self.current_action = 'default' + self.log = '' def _log(self, msg: str): - self.log += msg + "\n" + self.log += msg + '\n' def set_current_action(self, action: str): self.current_action = action @@ -37,16 +36,16 @@ class TaskContext: self.set_current_action(action) self._log( - f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')} | {self.current_action} | {msg}" + f'{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")} | {self.current_action} | {msg}' ) def to_dict(self) -> dict: - return {"current_action": self.current_action, "log": self.log} - + return {'current_action': self.current_action, 'log': self.log} + @staticmethod def new() -> TaskContext: return TaskContext() - + @staticmethod def placeholder() -> TaskContext: global placeholder_context @@ -69,16 +68,16 @@ class TaskWrapper: id: int """任务ID""" - task_type: str = "system" # 任务类型: system 或 user + task_type: str = 'system' # 任务类型: system 或 user """任务类型""" - kind: str = "system_task" # 由发起者确定任务种类,通常同质化的任务种类相同 + kind: str = 'system_task' # 由发起者确定任务种类,通常同质化的任务种类相同 """任务种类""" - name: str = "" + name: str = '' """任务唯一名称""" - label: str = "" + label: str = '' """任务显示名称""" task_context: TaskContext @@ -100,12 +99,14 @@ class TaskWrapper: self, ap: app.Application, coro: typing.Coroutine, - task_type: str = "system", - kind: str = "system_task", - name: str = "", - label: str = "", + task_type: str = 'system', + kind: str = 'system_task', + name: str = '', + label: str = '', context: TaskContext = None, - scopes: list[core_entities.LifecycleControlScope] = [core_entities.LifecycleControlScope.APPLICATION], + scopes: list[core_entities.LifecycleControlScope] = [ + core_entities.LifecycleControlScope.APPLICATION + ], ): self.id = TaskWrapper._id_index TaskWrapper._id_index += 1 @@ -115,7 +116,7 @@ class TaskWrapper: self.task_type = task_type self.kind = kind self.name = name - self.label = label if label != "" else name + self.label = label if label != '' else name self.task.set_name(name) self.scopes = scopes @@ -125,43 +126,46 @@ class TaskWrapper: if self.task_stack is None: self.task_stack = self.task.get_stack() return exception - except: + except Exception: return None def assume_result(self): try: return self.task.result() - except: + except Exception: return None def to_dict(self) -> dict: - exception_traceback = None if self.assume_exception() is not None: exception_traceback = 'Traceback (most recent call last):\n' for frame in self.task_stack: - exception_traceback += f" File \"{frame.f_code.co_filename}\", line {frame.f_lineno}, in {frame.f_code.co_name}\n" + exception_traceback += f' File "{frame.f_code.co_filename}", line {frame.f_lineno}, in {frame.f_code.co_name}\n' - exception_traceback += f" {self.assume_exception().__str__()}\n" + exception_traceback += f' {self.assume_exception().__str__()}\n' return { - "id": self.id, - "task_type": self.task_type, - "kind": self.kind, - "name": self.name, - "label": self.label, - "scopes": [scope.value for scope in self.scopes], - "task_context": self.task_context.to_dict(), - "runtime": { - "done": self.task.done(), - "state": self.task._state, - "exception": self.assume_exception().__str__() if self.assume_exception() is not None else None, - "exception_traceback": exception_traceback, - "result": self.assume_result().__str__() if self.assume_result() is not None else None, + 'id': self.id, + 'task_type': self.task_type, + 'kind': self.kind, + 'name': self.name, + 'label': self.label, + 'scopes': [scope.value for scope in self.scopes], + 'task_context': self.task_context.to_dict(), + 'runtime': { + 'done': self.task.done(), + 'state': self.task._state, + 'exception': self.assume_exception().__str__() + if self.assume_exception() is not None + else None, + 'exception_traceback': exception_traceback, + 'result': self.assume_result().__str__() + if self.assume_result() is not None + else None, }, } - + def cancel(self): self.task.cancel() @@ -182,27 +186,33 @@ class AsyncTaskManager: def create_task( self, coro: typing.Coroutine, - task_type: str = "system", - kind: str = "system-task", - name: str = "", - label: str = "", + task_type: str = 'system', + kind: str = 'system-task', + name: str = '', + label: str = '', context: TaskContext = None, - scopes: list[core_entities.LifecycleControlScope] = [core_entities.LifecycleControlScope.APPLICATION], + scopes: list[core_entities.LifecycleControlScope] = [ + core_entities.LifecycleControlScope.APPLICATION + ], ) -> TaskWrapper: - wrapper = TaskWrapper(self.ap, coro, task_type, kind, name, label, context, scopes) + wrapper = TaskWrapper( + self.ap, coro, task_type, kind, name, label, context, scopes + ) self.tasks.append(wrapper) return wrapper def create_user_task( self, coro: typing.Coroutine, - kind: str = "user-task", - name: str = "", - label: str = "", + kind: str = 'user-task', + name: str = '', + label: str = '', context: TaskContext = None, - scopes: list[core_entities.LifecycleControlScope] = [core_entities.LifecycleControlScope.APPLICATION], + scopes: list[core_entities.LifecycleControlScope] = [ + core_entities.LifecycleControlScope.APPLICATION + ], ) -> TaskWrapper: - return self.create_task(coro, "user", kind, name, label, context, scopes) + return self.create_task(coro, 'user', kind, name, label, context, scopes) async def wait_all(self): await asyncio.gather(*[t.task for t in self.tasks], return_exceptions=True) @@ -215,12 +225,12 @@ class AsyncTaskManager: type: str = None, ) -> dict: return { - "tasks": [ + 'tasks': [ t.to_dict() for t in self.tasks if type is None or t.task_type == type ], - "id_index": TaskWrapper._id_index, + 'id_index': TaskWrapper._id_index, } - + def get_task_by_id(self, id: int) -> TaskWrapper | None: for t in self.tasks: if t.id == id: @@ -229,9 +239,7 @@ class AsyncTaskManager: def cancel_by_scope(self, scope: core_entities.LifecycleControlScope): for wrapper in self.tasks: - if not wrapper.task.done() and scope in wrapper.scopes: - wrapper.task.cancel() def cancel_task(self, task_id: int): diff --git a/pkg/discover/engine.py b/pkg/discover/engine.py index e79a97ad..be23a4ac 100644 --- a/pkg/discover/engine.py +++ b/pkg/discover/engine.py @@ -3,8 +3,6 @@ from __future__ import annotations import typing import importlib import os -import inspect -import mimetypes import yaml import pydantic @@ -61,11 +59,9 @@ class Metadata(pydantic.BaseModel): def __init__(self, **kwargs): super().__init__(**kwargs) - + if self.description is None: - self.description = I18nString( - en_US='' - ) + self.description = I18nString(en_US='') if self.icon is None: self.icon = '' @@ -118,47 +114,60 @@ class Component(pydantic.BaseModel): _execution: Execution """组件执行""" - def __init__(self, owner: str, manifest: typing.Dict[str, typing.Any], rel_path: str): + def __init__( + self, owner: str, manifest: typing.Dict[str, typing.Any], rel_path: str + ): super().__init__( owner=owner, manifest=manifest, rel_path=rel_path, - rel_dir=os.path.dirname(rel_path) + rel_dir=os.path.dirname(rel_path), ) self._metadata = Metadata(**manifest['metadata']) self._spec = manifest['spec'] - self._execution = Execution(**manifest['execution']) if 'execution' in manifest else None + self._execution = ( + Execution(**manifest['execution']) if 'execution' in manifest else None + ) @classmethod def is_component_manifest(cls, manifest: typing.Dict[str, typing.Any]) -> bool: """判断是否为组件清单""" - return 'apiVersion' in manifest and 'kind' in manifest and 'metadata' in manifest and 'spec' in manifest + return ( + 'apiVersion' in manifest + and 'kind' in manifest + and 'metadata' in manifest + and 'spec' in manifest + ) @property def kind(self) -> str: """组件类型""" return self.manifest['kind'] - + @property def metadata(self) -> Metadata: """组件元数据""" return self._metadata - + @property def spec(self) -> typing.Dict[str, typing.Any]: """组件规格""" return self._spec - + @property def execution(self) -> Execution: """组件可执行文件信息""" return self._execution - + @property def icon_rel_path(self) -> str: """图标相对路径""" - return os.path.join(self.rel_dir, self.metadata.icon) if self.metadata.icon is not None and self.metadata.icon.strip() != '' else None - + return ( + os.path.join(self.rel_dir, self.metadata.icon) + if self.metadata.icon is not None and self.metadata.icon.strip() != '' + else None + ) + def get_python_component_class(self) -> typing.Type[typing.Any]: """获取Python组件类""" module_path = os.path.join(self.rel_dir, self.execution.python.path) @@ -167,7 +176,7 @@ class Component(pydantic.BaseModel): module_path = module_path.replace('/', '.').replace('\\', '.') module = importlib.import_module(module_path) return getattr(module, self.execution.python.attr) - + def to_plain_dict(self) -> dict: """转换为平铺字典""" return { @@ -175,7 +184,7 @@ class Component(pydantic.BaseModel): 'label': self.metadata.label.to_dict(), 'description': self.metadata.description.to_dict(), 'icon': self.metadata.icon, - 'spec': self.spec + 'spec': self.spec, } @@ -191,24 +200,28 @@ class ComponentDiscoveryEngine: def __init__(self, ap: app.Application): self.ap = ap - def load_component_manifest(self, path: str, owner: str = 'builtin', no_save: bool = False) -> Component | None: + def load_component_manifest( + self, path: str, owner: str = 'builtin', no_save: bool = False + ) -> Component | None: """加载组件清单""" with open(path, 'r', encoding='utf-8') as f: manifest = yaml.safe_load(f) if not Component.is_component_manifest(manifest): return None - comp = Component( - owner=owner, - manifest=manifest, - rel_path=path - ) + comp = Component(owner=owner, manifest=manifest, rel_path=path) if not no_save: if comp.kind not in self.components: self.components[comp.kind] = [] self.components[comp.kind].append(comp) return comp - - def load_component_manifests_in_dir(self, path: str, owner: str = 'builtin', no_save: bool = False, max_depth: int = 1) -> typing.List[Component]: + + def load_component_manifests_in_dir( + self, + path: str, + owner: str = 'builtin', + no_save: bool = False, + max_depth: int = 1, + ) -> typing.List[Component]: """加载目录中的组件清单""" components: typing.List[Component] = [] @@ -216,17 +229,25 @@ class ComponentDiscoveryEngine: if depth > max_depth: return for file in os.listdir(path): - if (not os.path.isdir(os.path.join(path, file))) and (file.endswith('.yaml') or file.endswith('.yml')): - comp = self.load_component_manifest(os.path.join(path, file), owner, no_save) + if (not os.path.isdir(os.path.join(path, file))) and ( + file.endswith('.yaml') or file.endswith('.yml') + ): + comp = self.load_component_manifest( + os.path.join(path, file), owner, no_save + ) if comp is not None: components.append(comp) elif os.path.isdir(os.path.join(path, file)): - recursive_load_component_manifests_in_dir(os.path.join(path, file), depth + 1) + recursive_load_component_manifests_in_dir( + os.path.join(path, file), depth + 1 + ) recursive_load_component_manifests_in_dir(path) return components - - def load_blueprint_comp_group(self, group: dict, owner: str = 'builtin', no_save: bool = False) -> typing.List[Component]: + + def load_blueprint_comp_group( + self, group: dict, owner: str = 'builtin', no_save: bool = False + ) -> typing.List[Component]: """加载蓝图组件组""" components: typing.List[Component] = [] if 'fromFiles' in group: @@ -238,12 +259,18 @@ class ComponentDiscoveryEngine: for dir in group['fromDirs']: path = dir['path'] max_depth = dir['maxDepth'] if 'maxDepth' in dir else 1 - components.extend(self.load_component_manifests_in_dir(path, owner, no_save, max_depth)) + components.extend( + self.load_component_manifests_in_dir( + path, owner, no_save, max_depth + ) + ) return components def discover_blueprint(self, blueprint_manifest_path: str, owner: str = 'builtin'): """发现蓝图""" - blueprint_manifest = self.load_component_manifest(blueprint_manifest_path, owner, no_save=True) + blueprint_manifest = self.load_component_manifest( + blueprint_manifest_path, owner, no_save=True + ) if blueprint_manifest is None: raise ValueError(f'Invalid blueprint manifest: {blueprint_manifest_path}') assert blueprint_manifest.kind == 'Blueprint', '`Kind` must be `Blueprint`' @@ -251,13 +278,15 @@ class ComponentDiscoveryEngine: # load ComponentTemplate first if 'ComponentTemplate' in blueprint_manifest.spec['components']: - components['ComponentTemplate'] = self.load_blueprint_comp_group(blueprint_manifest.spec['components']['ComponentTemplate'], owner) + components['ComponentTemplate'] = self.load_blueprint_comp_group( + blueprint_manifest.spec['components']['ComponentTemplate'], owner + ) for name, component in blueprint_manifest.spec['components'].items(): if name == 'ComponentTemplate': continue components[name] = self.load_blueprint_comp_group(component, owner) - + self.ap.logger.debug(f'Components: {components}') return blueprint_manifest, components @@ -268,7 +297,9 @@ class ComponentDiscoveryEngine: return [] return self.components[kind] - def find_components(self, kind: str, component_list: typing.List[Component]) -> typing.List[Component]: + def find_components( + self, kind: str, component_list: typing.List[Component] + ) -> typing.List[Component]: """查找组件""" result: typing.List[Component] = [] for component in component_list: diff --git a/pkg/entity/persistence/base.py b/pkg/entity/persistence/base.py index 9d9ea759..b0d8b5db 100644 --- a/pkg/entity/persistence/base.py +++ b/pkg/entity/persistence/base.py @@ -1,5 +1,4 @@ import sqlalchemy.orm -import pydantic class Base(sqlalchemy.orm.DeclarativeBase): diff --git a/pkg/entity/persistence/bot.py b/pkg/entity/persistence/bot.py index 0cd7bce7..86932cac 100644 --- a/pkg/entity/persistence/bot.py +++ b/pkg/entity/persistence/bot.py @@ -5,6 +5,7 @@ from .base import Base class Bot(Base): """机器人""" + __tablename__ = 'bots' uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True) @@ -15,5 +16,12 @@ class Bot(Base): enable = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=False) use_pipeline_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=True) use_pipeline_uuid = sqlalchemy.Column(sqlalchemy.String(255), nullable=True) - created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now()) - updated_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now(), onupdate=sqlalchemy.func.now()) + created_at = sqlalchemy.Column( + sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now() + ) + updated_at = sqlalchemy.Column( + sqlalchemy.DateTime, + nullable=False, + server_default=sqlalchemy.func.now(), + onupdate=sqlalchemy.func.now(), + ) diff --git a/pkg/entity/persistence/metadata.py b/pkg/entity/persistence/metadata.py index e1ebaefd..d9e03663 100644 --- a/pkg/entity/persistence/metadata.py +++ b/pkg/entity/persistence/metadata.py @@ -13,6 +13,7 @@ initial_metadata = [ class Metadata(Base): """数据库元数据""" + __tablename__ = 'metadata' key = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True) diff --git a/pkg/entity/persistence/model.py b/pkg/entity/persistence/model.py index 13700f25..65e016f3 100644 --- a/pkg/entity/persistence/model.py +++ b/pkg/entity/persistence/model.py @@ -1,11 +1,11 @@ import sqlalchemy -import datetime from .base import Base class LLMModel(Base): """LLM 模型""" + __tablename__ = 'llm_models' uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True) @@ -16,5 +16,12 @@ class LLMModel(Base): api_keys = sqlalchemy.Column(sqlalchemy.JSON, nullable=False) abilities = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default=[]) extra_args = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={}) - created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now()) - updated_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now(), onupdate=sqlalchemy.func.now()) \ No newline at end of file + created_at = sqlalchemy.Column( + sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now() + ) + updated_at = sqlalchemy.Column( + sqlalchemy.DateTime, + nullable=False, + server_default=sqlalchemy.func.now(), + onupdate=sqlalchemy.func.now(), + ) diff --git a/pkg/entity/persistence/pipeline.py b/pkg/entity/persistence/pipeline.py index aaa393a7..ca854203 100644 --- a/pkg/entity/persistence/pipeline.py +++ b/pkg/entity/persistence/pipeline.py @@ -5,13 +5,21 @@ from .base import Base class LegacyPipeline(Base): """旧版流水线""" + __tablename__ = 'legacy_pipelines' uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True) name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) description = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) - created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now()) - updated_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now(), onupdate=sqlalchemy.func.now()) + created_at = sqlalchemy.Column( + sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now() + ) + updated_at = sqlalchemy.Column( + sqlalchemy.DateTime, + nullable=False, + server_default=sqlalchemy.func.now(), + onupdate=sqlalchemy.func.now(), + ) for_version = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) is_default = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=False) @@ -21,13 +29,21 @@ class LegacyPipeline(Base): class PipelineRunRecord(Base): """流水线运行记录""" + __tablename__ = 'pipeline_run_records' uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True) pipeline_uuid = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) status = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) - created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now()) - updated_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now(), onupdate=sqlalchemy.func.now()) + created_at = sqlalchemy.Column( + sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now() + ) + updated_at = sqlalchemy.Column( + sqlalchemy.DateTime, + nullable=False, + server_default=sqlalchemy.func.now(), + onupdate=sqlalchemy.func.now(), + ) started_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False) finished_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False) result = sqlalchemy.Column(sqlalchemy.JSON, nullable=False) diff --git a/pkg/entity/persistence/plugin.py b/pkg/entity/persistence/plugin.py index b1e2cac4..94d6b8b4 100644 --- a/pkg/entity/persistence/plugin.py +++ b/pkg/entity/persistence/plugin.py @@ -5,6 +5,7 @@ from .base import Base class PluginSetting(Base): """插件配置""" + __tablename__ = 'plugin_settings' plugin_author = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True) @@ -12,5 +13,12 @@ class PluginSetting(Base): enabled = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=True) priority = sqlalchemy.Column(sqlalchemy.Integer, nullable=False, default=0) config = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default=dict) - created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now()) - updated_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now(), onupdate=sqlalchemy.func.now()) + created_at = sqlalchemy.Column( + sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now() + ) + updated_at = sqlalchemy.Column( + sqlalchemy.DateTime, + nullable=False, + server_default=sqlalchemy.func.now(), + onupdate=sqlalchemy.func.now(), + ) diff --git a/pkg/entity/persistence/user.py b/pkg/entity/persistence/user.py index 23d309c4..a0d9f168 100644 --- a/pkg/entity/persistence/user.py +++ b/pkg/entity/persistence/user.py @@ -1,5 +1,4 @@ import sqlalchemy -import sqlmodel from .base import Base @@ -10,5 +9,12 @@ class User(Base): id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True) user = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) password = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) - created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now()) - updated_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now(), onupdate=sqlalchemy.func.now()) \ No newline at end of file + created_at = sqlalchemy.Column( + sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now() + ) + updated_at = sqlalchemy.Column( + sqlalchemy.DateTime, + nullable=False, + server_default=sqlalchemy.func.now(), + onupdate=sqlalchemy.func.now(), + ) diff --git a/pkg/persistence/database.py b/pkg/persistence/database.py index 0dd82817..528c6a34 100644 --- a/pkg/persistence/database.py +++ b/pkg/persistence/database.py @@ -9,6 +9,7 @@ from ..core import app preregistered_managers: list[type[BaseDatabaseManager]] = [] + def manager_class(name: str) -> None: """注册一个数据库管理类""" diff --git a/pkg/persistence/databases/sqlite.py b/pkg/persistence/databases/sqlite.py index 0bc3db32..1b12def8 100644 --- a/pkg/persistence/databases/sqlite.py +++ b/pkg/persistence/databases/sqlite.py @@ -5,10 +5,12 @@ import sqlalchemy.ext.asyncio as sqlalchemy_asyncio from .. import database -@database.manager_class("sqlite") +@database.manager_class('sqlite') class SQLiteDatabaseManager(database.BaseDatabaseManager): """SQLite 数据库管理类""" - + async def initialize(self) -> None: sqlite_path = 'data/langbot.db' - self.engine = sqlalchemy_asyncio.create_async_engine(f"sqlite+aiosqlite:///{sqlite_path}") + self.engine = sqlalchemy_asyncio.create_async_engine( + f'sqlite+aiosqlite:///{sqlite_path}' + ) diff --git a/pkg/persistence/mgr.py b/pkg/persistence/mgr.py index f17b169e..e8f953ab 100644 --- a/pkg/persistence/mgr.py +++ b/pkg/persistence/mgr.py @@ -1,6 +1,5 @@ from __future__ import annotations -import asyncio import datetime import typing import json @@ -10,12 +9,16 @@ import sqlalchemy.ext.asyncio as sqlalchemy_asyncio import sqlalchemy from . import database, migration -from ..entity.persistence import base, user, model, pipeline, bot, plugin, metadata +from ..entity.persistence import base, pipeline, metadata +from ..entity import persistence from ..core import app -from .databases import sqlite -from ..utils import constants -from .migrations import dbm001_migrate_v3_config +from ..utils import constants, importutil from ..api.http.service import pipeline as pipeline_service +from . import databases, migrations + +importutil.import_modules_in_pkg(databases) +importutil.import_modules_in_pkg(migrations) +importutil.import_modules_in_pkg(persistence) class PersistenceManager: @@ -33,9 +36,8 @@ class PersistenceManager: self.meta = base.Base.metadata async def initialize(self): + self.ap.logger.info('Initializing database...') - self.ap.logger.info("Initializing database...") - for manager in database.preregistered_managers: self.db = manager(self.ap) await self.db.initialize() @@ -43,7 +45,6 @@ class PersistenceManager: await self.create_tables() async def create_tables(self): - # create tables async with self.get_db_engine().connect() as conn: await conn.run_sync(self.meta.create_all) @@ -53,26 +54,28 @@ class PersistenceManager: # ======= write initial data ======= # write initial metadata - self.ap.logger.info("Creating initial metadata...") + self.ap.logger.info('Creating initial metadata...') for item in metadata.initial_metadata: # check if the item exists result = await self.execute_async( - sqlalchemy.select(metadata.Metadata).where(metadata.Metadata.key == item['key']) + sqlalchemy.select(metadata.Metadata).where( + metadata.Metadata.key == item['key'] + ) ) row = result.first() if row is None: await self.execute_async( sqlalchemy.insert(metadata.Metadata).values(item) ) - - # write default pipeline - result = await self.execute_async( - sqlalchemy.select(pipeline.LegacyPipeline) - ) - if result.first() is None: - self.ap.logger.info("Creating default pipeline...") - pipeline_config = json.load(open('templates/default-pipeline-config.json', 'r', encoding='utf-8')) + # write default pipeline + result = await self.execute_async(sqlalchemy.select(pipeline.LegacyPipeline)) + if result.first() is None: + self.ap.logger.info('Creating default pipeline...') + + pipeline_config = json.load( + open('templates/default-pipeline-config.json', 'r', encoding='utf-8') + ) pipeline_data = { 'uuid': str(uuid.uuid4()), @@ -91,7 +94,9 @@ class PersistenceManager: # run migrations database_version = await self.execute_async( - sqlalchemy.select(metadata.Metadata).where(metadata.Metadata.key == 'database_version') + sqlalchemy.select(metadata.Metadata).where( + metadata.Metadata.key == 'database_version' + ) ) database_version = int(database_version.fetchone()[1]) @@ -106,24 +111,27 @@ class PersistenceManager: for migration_cls in migrations: migration_instance = migration_cls(self.ap) - if migration_instance.number > database_version and migration_instance.number <= required_database_version: + if ( + migration_instance.number > database_version + and migration_instance.number <= required_database_version + ): await migration_instance.upgrade() await self.execute_async( - sqlalchemy.update(metadata.Metadata).where(metadata.Metadata.key == 'database_version').values( - { - 'value': str(migration_instance.number) - } - ) + sqlalchemy.update(metadata.Metadata) + .where(metadata.Metadata.key == 'database_version') + .values({'value': str(migration_instance.number)}) ) last_migration_number = migration_instance.number - self.ap.logger.info(f'Migration {migration_instance.number} completed.') - - self.ap.logger.info(f'Successfully upgraded database to version {last_migration_number}.') + self.ap.logger.info( + f'Migration {migration_instance.number} completed.' + ) + + self.ap.logger.info( + f'Successfully upgraded database to version {last_migration_number}.' + ) async def execute_async( - self, - *args, - **kwargs + self, *args, **kwargs ) -> sqlalchemy.engine.cursor.CursorResult: async with self.get_db_engine().connect() as conn: result = await conn.execute(*args, **kwargs) @@ -132,9 +140,13 @@ class PersistenceManager: def get_db_engine(self) -> sqlalchemy_asyncio.AsyncEngine: return self.db.get_engine() - - def serialize_model(self, model: typing.Type[sqlalchemy.Base], data: sqlalchemy.Base) -> dict: + + def serialize_model( + self, model: typing.Type[sqlalchemy.Base], data: sqlalchemy.Base + ) -> dict: return { - column.name: getattr(data, column.name) if not isinstance(getattr(data, column.name), (datetime.datetime)) else getattr(data, column.name).isoformat() + column.name: getattr(data, column.name) + if not isinstance(getattr(data, column.name), (datetime.datetime)) + else getattr(data, column.name).isoformat() for column in model.__table__.columns } diff --git a/pkg/persistence/migration.py b/pkg/persistence/migration.py index 81a3aac3..c191b686 100644 --- a/pkg/persistence/migration.py +++ b/pkg/persistence/migration.py @@ -8,6 +8,7 @@ from ..core import app preregistered_db_migrations: list[typing.Type[DBMigration]] = [] + def migration_class(number: int): """迁移类装饰器""" @@ -15,6 +16,7 @@ def migration_class(number: int): cls.number = number preregistered_db_migrations.append(cls) return cls + return wrapper diff --git a/pkg/persistence/migrations/dbm001_migrate_v3_config.py b/pkg/persistence/migrations/dbm001_migrate_v3_config.py index afed5eea..6aee2854 100644 --- a/pkg/persistence/migrations/dbm001_migrate_v3_config.py +++ b/pkg/persistence/migrations/dbm001_migrate_v3_config.py @@ -1,5 +1,3 @@ -from .. import migration - # TODO fill this # @migration.migration_class(1) # class DBMigrationV3(migration.DBMigration): @@ -10,4 +8,4 @@ from .. import migration # pass # async def downgrade(self): -# """降级""" \ No newline at end of file +# """降级""" diff --git a/pkg/pipeline/bansess/bansess.py b/pkg/pipeline/bansess/bansess.py index 38fb9794..dad6a3ab 100644 --- a/pkg/pipeline/bansess/bansess.py +++ b/pkg/pipeline/bansess/bansess.py @@ -1,15 +1,13 @@ from __future__ import annotations -import re from .. import stage, entities from ...core import entities as core_entities -from ...config import manager as cfg_mgr @stage.stage_class('BanSessionCheckStage') class BanSessionCheckStage(stage.PipelineStage): """访问控制处理阶段 - + 仅检查query中群号或个人号是否在访问控制列表中。 """ @@ -17,26 +15,24 @@ class BanSessionCheckStage(stage.PipelineStage): pass async def process( - self, - query: core_entities.Query, - stage_inst_name: str + self, query: core_entities.Query, stage_inst_name: str ) -> entities.StageProcessResult: - found = False mode = query.pipeline_config['trigger']['access-control']['mode'] sess_list = query.pipeline_config['trigger']['access-control'][mode] - if (query.launcher_type.value == 'group' and 'group_*' in sess_list) \ - or (query.launcher_type.value == 'person' and 'person_*' in sess_list): + if (query.launcher_type.value == 'group' and 'group_*' in sess_list) or ( + query.launcher_type.value == 'person' and 'person_*' in sess_list + ): found = True else: for sess in sess_list: - if sess == f"{query.launcher_type.value}_{query.launcher_id}": + if sess == f'{query.launcher_type.value}_{query.launcher_id}': found = True break - + ctn = False if mode == 'whitelist': @@ -45,7 +41,11 @@ class BanSessionCheckStage(stage.PipelineStage): ctn = not found return entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE if ctn else entities.ResultType.INTERRUPT, + result_type=entities.ResultType.CONTINUE + if ctn + else entities.ResultType.INTERRUPT, new_query=query, - console_notice=f'根据访问控制忽略消息: {query.launcher_type.value}_{query.launcher_id}' if not ctn else '' + console_notice=f'根据访问控制忽略消息: {query.launcher_type.value}_{query.launcher_id}' + if not ctn + else '', ) diff --git a/pkg/pipeline/cntfilter/cntfilter.py b/pkg/pipeline/cntfilter/cntfilter.py index dbf7c52e..6547cb16 100644 --- a/pkg/pipeline/cntfilter/cntfilter.py +++ b/pkg/pipeline/cntfilter/cntfilter.py @@ -4,20 +4,21 @@ from ...core import app from .. import stage, entities from ...core import entities as core_entities -from ...config import manager as cfg_mgr from . import filter as filter_model, entities as filter_entities -from .filters import cntignore, banwords, baiduexamine from ...provider import entities as llm_entities from ...platform.types import message as platform_message -from ...platform.types import events as platform_events -from ...platform.types import entities as platform_entities +from ...utils import importutil + +from . import filters + +importutil.import_modules_in_pkg(filters) @stage.stage_class('PostContentFilterStage') @stage.stage_class('PreContentFilterStage') class ContentFilterStage(stage.PipelineStage): """内容过滤阶段 - + 前置: 检查消息是否符合规则,不符合则拦截。 改写: @@ -36,13 +37,12 @@ class ContentFilterStage(stage.PipelineStage): super().__init__(ap) async def initialize(self, pipeline_config: dict): - filters_required = [ - "content-ignore", + 'content-ignore', ] if pipeline_config['safety']['content-filter']['check-sensitive-words']: - filters_required.append("ban-word-filter") + filters_required.append('ban-word-filter') # TODO revert it # if self.ap.pipeline_cfg.data['baidu-cloud-examine']['enable']: @@ -50,9 +50,7 @@ class ContentFilterStage(stage.PipelineStage): for filter in filter_model.preregistered_filters: if filter.name in filters_required: - self.filter_chain.append( - filter(self.ap) - ) + self.filter_chain.append(filter(self.ap)) for filter in self.filter_chain: await filter.initialize() @@ -68,8 +66,7 @@ class ContentFilterStage(stage.PipelineStage): if query.pipeline_config['safety']['content-filter']['scope'] == 'output-msg': return entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query + result_type=entities.ResultType.CONTINUE, new_query=query ) else: for filter in self.filter_chain: @@ -78,26 +75,25 @@ class ContentFilterStage(stage.PipelineStage): if result.level in [ filter_entities.ResultLevel.BLOCK, - filter_entities.ResultLevel.MASKED + filter_entities.ResultLevel.MASKED, ]: return entities.StageProcessResult( result_type=entities.ResultType.INTERRUPT, new_query=query, user_notice=result.user_notice, - console_notice=result.console_notice + console_notice=result.console_notice, ) elif result.level == filter_entities.ResultLevel.PASS: # 传到下一个 message = result.replacement - + query.message_chain = platform_message.MessageChain( platform_message.Plain(message) ) return entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query + result_type=entities.ResultType.CONTINUE, new_query=query ) - + async def _post_process( self, message: str, @@ -108,8 +104,7 @@ class ContentFilterStage(stage.PipelineStage): """ if query.pipeline_config['safety']['content-filter']['scope'] == 'income-msg': return entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query + result_type=entities.ResultType.CONTINUE, new_query=query ) else: message = message.strip() @@ -122,30 +117,25 @@ class ContentFilterStage(stage.PipelineStage): result_type=entities.ResultType.INTERRUPT, new_query=query, user_notice=result.user_notice, - console_notice=result.console_notice + console_notice=result.console_notice, ) elif result.level in [ filter_entities.ResultLevel.PASS, - filter_entities.ResultLevel.MASKED + filter_entities.ResultLevel.MASKED, ]: message = result.replacement query.resp_messages[-1].content = message return entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query + result_type=entities.ResultType.CONTINUE, new_query=query ) async def process( - self, - query: core_entities.Query, - stage_inst_name: str + self, query: core_entities.Query, stage_inst_name: str ) -> entities.StageProcessResult: - """处理 - """ + """处理""" if stage_inst_name == 'PreContentFilterStage': - contain_non_text = False text_components = [platform_message.Plain, platform_message.Source] @@ -156,28 +146,24 @@ class ContentFilterStage(stage.PipelineStage): break if contain_non_text: - self.ap.logger.debug(f"消息中包含非文本消息,跳过内容过滤器检查。") + self.ap.logger.debug('消息中包含非文本消息,跳过内容过滤器检查。') return entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query + result_type=entities.ResultType.CONTINUE, new_query=query ) - return await self._pre_process( - str(query.message_chain).strip(), - query - ) + return await self._pre_process(str(query.message_chain).strip(), query) elif stage_inst_name == 'PostContentFilterStage': # 仅处理 query.resp_messages[-1].content 是 str 的情况 - if isinstance(query.resp_messages[-1], llm_entities.Message) and isinstance(query.resp_messages[-1].content, str): - return await self._post_process( - query.resp_messages[-1].content, - query - ) + if isinstance(query.resp_messages[-1], llm_entities.Message) and isinstance( + query.resp_messages[-1].content, str + ): + return await self._post_process(query.resp_messages[-1].content, query) else: - self.ap.logger.debug(f"resp_messages[-1] 不是 Message 类型或 query.resp_messages[-1].content 不是 str 类型,跳过内容过滤器检查。") + self.ap.logger.debug( + 'resp_messages[-1] 不是 Message 类型或 query.resp_messages[-1].content 不是 str 类型,跳过内容过滤器检查。' + ) return entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query + result_type=entities.ResultType.CONTINUE, new_query=query ) else: raise ValueError(f'未知的 stage_inst_name: {stage_inst_name}') diff --git a/pkg/pipeline/cntfilter/entities.py b/pkg/pipeline/cntfilter/entities.py index b4bc0f7e..5e804c0d 100644 --- a/pkg/pipeline/cntfilter/entities.py +++ b/pkg/pipeline/cntfilter/entities.py @@ -1,14 +1,11 @@ - -import typing import enum import pydantic.v1 as pydantic -from ...provider import entities as llm_entities - class ResultLevel(enum.Enum): """结果等级""" + PASS = enum.auto() """通过""" @@ -24,6 +21,7 @@ class ResultLevel(enum.Enum): class EnableStage(enum.Enum): """启用阶段""" + PRE = enum.auto() """预处理""" @@ -55,14 +53,15 @@ class FilterResult(pydantic.BaseModel): class ManagerResultLevel(enum.Enum): """处理器结果等级""" + CONTINUE = enum.auto() """继续""" INTERRUPT = enum.auto() """中断""" -class FilterManagerResult(pydantic.BaseModel): +class FilterManagerResult(pydantic.BaseModel): level: ManagerResultLevel replacement: str diff --git a/pkg/pipeline/cntfilter/filter.py b/pkg/pipeline/cntfilter/filter.py index 970e11f1..ae7ceb79 100644 --- a/pkg/pipeline/cntfilter/filter.py +++ b/pkg/pipeline/cntfilter/filter.py @@ -5,14 +5,13 @@ import typing from ...core import app, entities as core_entities from . import entities -from ...provider import entities as llm_entities preregistered_filters: list[typing.Type[ContentFilter]] = [] def filter_class( - name: str + name: str, ) -> typing.Callable[[typing.Type[ContentFilter]], typing.Type[ContentFilter]]: """内容过滤器类装饰器 @@ -22,6 +21,7 @@ def filter_class( Returns: typing.Callable[[typing.Type[ContentFilter]], typing.Type[ContentFilter]]: 装饰器 """ + def decorator(cls: typing.Type[ContentFilter]) -> typing.Type[ContentFilter]: assert issubclass(cls, ContentFilter) @@ -53,23 +53,21 @@ class ContentFilter(metaclass=abc.ABCMeta): entity.EnableStage.PRE: 消息请求AI前,此时需要检查的内容是用户的输入消息。 entity.EnableStage.POST: 消息请求AI后,此时需要检查的内容是AI的回复消息。 """ - return [ - entities.EnableStage.PRE, - entities.EnableStage.POST - ] + return [entities.EnableStage.PRE, entities.EnableStage.POST] async def initialize(self): - """初始化过滤器 - """ + """初始化过滤器""" pass @abc.abstractmethod - async def process(self, query: core_entities.Query, message: str=None, image_url=None) -> entities.FilterResult: + async def process( + self, query: core_entities.Query, message: str = None, image_url=None + ) -> entities.FilterResult: """处理消息 分为前后阶段,具体取决于 enable_stages 的值。 对于内容过滤器来说,不需要考虑消息所处的阶段,只需要检查消息内容即可。 - + Args: message (str): 需要检查的内容 image_url (str): 要检查的图片的 URL diff --git a/pkg/pipeline/cntfilter/filters/baiduexamine.py b/pkg/pipeline/cntfilter/filters/baiduexamine.py index 800f0099..c3776bc9 100644 --- a/pkg/pipeline/cntfilter/filters/baiduexamine.py +++ b/pkg/pipeline/cntfilter/filters/baiduexamine.py @@ -7,11 +7,11 @@ from .. import filter as filter_model from ....core import entities as core_entities -BAIDU_EXAMINE_URL = "https://aip.baidubce.com/rest/2.0/solution/v1/text_censor/v2/user_defined?access_token={}" -BAIDU_EXAMINE_TOKEN_URL = "https://aip.baidubce.com/oauth/2.0/token" +BAIDU_EXAMINE_URL = 'https://aip.baidubce.com/rest/2.0/solution/v1/text_censor/v2/user_defined?access_token={}' +BAIDU_EXAMINE_TOKEN_URL = 'https://aip.baidubce.com/oauth/2.0/token' -@filter_model.filter_class("baidu-cloud-examine") +@filter_model.filter_class('baidu-cloud-examine') class BaiduCloudExamine(filter_model.ContentFilter): """百度云内容审核""" @@ -20,44 +20,52 @@ class BaiduCloudExamine(filter_model.ContentFilter): async with session.post( BAIDU_EXAMINE_TOKEN_URL, params={ - "grant_type": "client_credentials", - "client_id": self.ap.pipeline_cfg.data['baidu-cloud-examine']['api-key'], - "client_secret": self.ap.pipeline_cfg.data['baidu-cloud-examine']['api-secret'] - } + 'grant_type': 'client_credentials', + 'client_id': self.ap.pipeline_cfg.data['baidu-cloud-examine'][ + 'api-key' + ], + 'client_secret': self.ap.pipeline_cfg.data['baidu-cloud-examine'][ + 'api-secret' + ], + }, ) as resp: return (await resp.json())['access_token'] - async def process(self, query: core_entities.Query, message: str) -> entities.FilterResult: - + async def process( + self, query: core_entities.Query, message: str + ) -> entities.FilterResult: async with aiohttp.ClientSession() as session: async with session.post( BAIDU_EXAMINE_URL.format(await self._get_token()), - headers={'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json'}, - data=f"text={message}".encode('utf-8') + headers={ + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json', + }, + data=f'text={message}'.encode('utf-8'), ) as resp: result = await resp.json() - if "error_code" in result: + if 'error_code' in result: return entities.FilterResult( level=entities.ResultLevel.BLOCK, replacement=message, user_notice='', - console_notice=f"百度云判定出错,错误信息:{result['error_msg']}" + console_notice=f'百度云判定出错,错误信息:{result["error_msg"]}', ) else: - conclusion = result["conclusion"] + conclusion = result['conclusion'] - if conclusion in ("合规"): + if conclusion in ('合规'): return entities.FilterResult( level=entities.ResultLevel.PASS, replacement=message, user_notice='', - console_notice=f"百度云判定结果:{conclusion}" + console_notice=f'百度云判定结果:{conclusion}', ) else: return entities.FilterResult( level=entities.ResultLevel.BLOCK, replacement=message, - user_notice="消息中存在不合适的内容, 请修改", - console_notice=f"百度云判定结果:{conclusion}" + user_notice='消息中存在不合适的内容, 请修改', + console_notice=f'百度云判定结果:{conclusion}', ) diff --git a/pkg/pipeline/cntfilter/filters/banwords.py b/pkg/pipeline/cntfilter/filters/banwords.py index cd3d412c..598fa299 100644 --- a/pkg/pipeline/cntfilter/filters/banwords.py +++ b/pkg/pipeline/cntfilter/filters/banwords.py @@ -6,14 +6,16 @@ from .. import entities from ....core import entities as core_entities -@filter_model.filter_class("ban-word-filter") +@filter_model.filter_class('ban-word-filter') class BanWordFilter(filter_model.ContentFilter): """根据内容过滤""" async def initialize(self): pass - async def process(self, query: core_entities.Query, message: str) -> entities.FilterResult: + async def process( + self, query: core_entities.Query, message: str + ) -> entities.FilterResult: found = False for word in self.ap.sensitive_meta.data['words']: @@ -23,9 +25,10 @@ class BanWordFilter(filter_model.ContentFilter): found = True for i in range(len(match)): - if self.ap.sensitive_meta.data['mask_word'] == "": + if self.ap.sensitive_meta.data['mask_word'] == '': message = message.replace( - match[i], self.ap.sensitive_meta.data['mask'] * len(match[i]) + match[i], + self.ap.sensitive_meta.data['mask'] * len(match[i]), ) else: message = message.replace( @@ -36,5 +39,5 @@ class BanWordFilter(filter_model.ContentFilter): level=entities.ResultLevel.MASKED if found else entities.ResultLevel.PASS, replacement=message, user_notice='消息中存在不合适的内容, 请修改' if found else '', - console_notice='' - ) \ No newline at end of file + console_notice='', + ) diff --git a/pkg/pipeline/cntfilter/filters/cntignore.py b/pkg/pipeline/cntfilter/filters/cntignore.py index 381d5c51..cb563593 100644 --- a/pkg/pipeline/cntfilter/filters/cntignore.py +++ b/pkg/pipeline/cntfilter/filters/cntignore.py @@ -6,7 +6,7 @@ from .. import filter as filter_model from ....core import entities as core_entities -@filter_model.filter_class("content-ignore") +@filter_model.filter_class('content-ignore') class ContentIgnore(filter_model.ContentFilter): """根据内容忽略消息""" @@ -16,7 +16,9 @@ class ContentIgnore(filter_model.ContentFilter): entities.EnableStage.PRE, ] - async def process(self, query: core_entities.Query, message: str) -> entities.FilterResult: + async def process( + self, query: core_entities.Query, message: str + ) -> entities.FilterResult: if 'prefix' in query.pipeline_config['trigger']['ignore-rules']: for rule in query.pipeline_config['trigger']['ignore-rules']['prefix']: if message.startswith(rule): @@ -24,9 +26,9 @@ class ContentIgnore(filter_model.ContentFilter): level=entities.ResultLevel.BLOCK, replacement='', user_notice='', - console_notice='根据 ignore_rules 中的 prefix 规则,忽略消息' + console_notice='根据 ignore_rules 中的 prefix 规则,忽略消息', ) - + if 'regexp' in query.pipeline_config['trigger']['ignore-rules']: for rule in query.pipeline_config['trigger']['ignore-rules']['regexp']: if re.search(rule, message): @@ -34,12 +36,12 @@ class ContentIgnore(filter_model.ContentFilter): level=entities.ResultLevel.BLOCK, replacement='', user_notice='', - console_notice='根据 ignore_rules 中的 regexp 规则,忽略消息' + console_notice='根据 ignore_rules 中的 regexp 规则,忽略消息', ) return entities.FilterResult( level=entities.ResultLevel.PASS, replacement=message, user_notice='', - console_notice='' - ) \ No newline at end of file + console_notice='', + ) diff --git a/pkg/pipeline/controller.py b/pkg/pipeline/controller.py index a7cf2153..2ad1690f 100644 --- a/pkg/pipeline/controller.py +++ b/pkg/pipeline/controller.py @@ -1,18 +1,14 @@ from __future__ import annotations import asyncio -import typing import traceback from ..core import app, entities -from . import entities as pipeline_entities -from ..plugin import events -from ..platform.types import message as platform_message class Controller: - """总控制器 - """ + """总控制器""" + ap: app.Application semaphore: asyncio.Semaphore = None @@ -20,11 +16,12 @@ class Controller: def __init__(self, ap: app.Application): self.ap = ap - self.semaphore = asyncio.Semaphore(self.ap.instance_config.data['concurrency']['pipeline']) + self.semaphore = asyncio.Semaphore( + self.ap.instance_config.data['concurrency']['pipeline'] + ) async def consumer(self): - """事件处理循环 - """ + """事件处理循环""" try: while True: selected_query: entities.Query = None @@ -35,7 +32,9 @@ class Controller: for query in queries: session = await self.ap.sess_mgr.get_session(query) - self.ap.logger.debug(f"Checking query {query} session {session}") + self.ap.logger.debug( + f'Checking query {query} session {session}' + ) if not session.semaphore.locked(): selected_query = query @@ -56,30 +55,40 @@ class Controller: # find pipeline # Here firstly find the bot, then find the pipeline, in case the bot adapter's config is not the latest one. # Like aiocqhttp, once a client is connected, even the adapter was updated and restarted, the existing client connection will not be affected. - bot = await self.ap.platform_mgr.get_bot_by_uuid(selected_query.bot_uuid) + bot = await self.ap.platform_mgr.get_bot_by_uuid( + selected_query.bot_uuid + ) if bot: - pipeline = await self.ap.pipeline_mgr.get_pipeline_by_uuid(bot.bot_entity.use_pipeline_uuid) + pipeline = ( + await self.ap.pipeline_mgr.get_pipeline_by_uuid( + bot.bot_entity.use_pipeline_uuid + ) + ) if pipeline: await pipeline.run(selected_query) - + async with self.ap.query_pool: - (await self.ap.sess_mgr.get_session(selected_query)).semaphore.release() + ( + await self.ap.sess_mgr.get_session(selected_query) + ).semaphore.release() # 通知其他协程,有新的请求可以处理了 self.ap.query_pool.condition.notify_all() self.ap.task_mgr.create_task( _process_query(selected_query), - kind="query", - name=f"query-{selected_query.query_id}", - scopes=[entities.LifecycleControlScope.APPLICATION, entities.LifecycleControlScope.PLATFORM], + kind='query', + name=f'query-{selected_query.query_id}', + scopes=[ + entities.LifecycleControlScope.APPLICATION, + entities.LifecycleControlScope.PLATFORM, + ], ) except Exception as e: # traceback.print_exc() - self.ap.logger.error(f"控制器循环出错: {e}") - self.ap.logger.error(f"Traceback: {traceback.format_exc()}") + self.ap.logger.error(f'控制器循环出错: {e}') + self.ap.logger.error(f'Traceback: {traceback.format_exc()}') async def run(self): - """运行控制器 - """ + """运行控制器""" await self.consumer() diff --git a/pkg/pipeline/entities.py b/pkg/pipeline/entities.py index ffcc4654..dd6434c0 100644 --- a/pkg/pipeline/entities.py +++ b/pkg/pipeline/entities.py @@ -10,7 +10,6 @@ from ..core import entities class ResultType(enum.Enum): - CONTINUE = enum.auto() """继续流水线""" @@ -19,12 +18,18 @@ class ResultType(enum.Enum): class StageProcessResult(pydantic.BaseModel): - result_type: ResultType new_query: entities.Query - user_notice: typing.Optional[typing.Union[str, list[platform_message.MessageComponent], platform_message.MessageChain, None]] = [] + user_notice: typing.Optional[ + typing.Union[ + str, + list[platform_message.MessageComponent], + platform_message.MessageChain, + None, + ] + ] = [] """只要设置了就会发送给用户""" console_notice: typing.Optional[str] = '' diff --git a/pkg/pipeline/longtext/longtext.py b/pkg/pipeline/longtext/longtext.py index ac03ad42..ab20f3eb 100644 --- a/pkg/pipeline/longtext/longtext.py +++ b/pkg/pipeline/longtext/longtext.py @@ -2,18 +2,19 @@ from __future__ import annotations import os import traceback -from PIL import Image, ImageDraw, ImageFont -from ...core import app from . import strategy -from .strategies import image, forward from .. import stage, entities from ...core import entities as core_entities -from ...config import manager as cfg_mgr from ...platform.types import message as platform_message +from ...utils import importutil + +from . import strategies + +importutil.import_modules_in_pkg(strategies) -@stage.stage_class("LongTextProcessStage") +@stage.stage_class('LongTextProcessStage') class LongTextProcessStage(stage.PipelineStage): """长消息处理阶段 @@ -31,34 +32,48 @@ class LongTextProcessStage(stage.PipelineStage): # 检查是否存在 if not os.path.exists(use_font): # 若是windows系统,使用微软雅黑 - if os.name == "nt": - use_font = "C:/Windows/Fonts/msyh.ttc" + if os.name == 'nt': + use_font = 'C:/Windows/Fonts/msyh.ttc' if not os.path.exists(use_font): - self.ap.logger.warn("未找到字体文件,且无法使用Windows自带字体,更换为转发消息组件以发送长消息,您可以在配置文件中调整相关设置。") - config['blob_message_strategy'] = "forward" + self.ap.logger.warn( + '未找到字体文件,且无法使用Windows自带字体,更换为转发消息组件以发送长消息,您可以在配置文件中调整相关设置。' + ) + config['blob_message_strategy'] = 'forward' else: - self.ap.logger.info("使用Windows自带字体:" + use_font) + self.ap.logger.info('使用Windows自带字体:' + use_font) config['font-path'] = use_font else: - self.ap.logger.warn("未找到字体文件,且无法使用系统自带字体,更换为转发消息组件以发送长消息,您可以在配置文件中调整相关设置。") + self.ap.logger.warn( + '未找到字体文件,且无法使用系统自带字体,更换为转发消息组件以发送长消息,您可以在配置文件中调整相关设置。' + ) - pipeline_config['output']['long-text-processing']['strategy'] = "forward" - except: + pipeline_config['output']['long-text-processing'][ + 'strategy' + ] = 'forward' + except Exception: traceback.print_exc() - self.ap.logger.error("加载字体文件失败({}),更换为转发消息组件以发送长消息,您可以在配置文件中调整相关设置。".format(use_font)) + self.ap.logger.error( + '加载字体文件失败({}),更换为转发消息组件以发送长消息,您可以在配置文件中调整相关设置。'.format( + use_font + ) + ) - pipeline_config['output']['long-text-processing']['strategy'] = "forward" + pipeline_config['output']['long-text-processing']['strategy'] = ( + 'forward' + ) for strategy_cls in strategy.preregistered_strategies: if strategy_cls.name == config['strategy']: self.strategy_impl = strategy_cls(self.ap) break else: - raise ValueError(f"未找到名为 {config['strategy']} 的长消息处理策略") + raise ValueError(f'未找到名为 {config["strategy"]} 的长消息处理策略') await self.strategy_impl.initialize() - - async def process(self, query: core_entities.Query, stage_inst_name: str) -> entities.StageProcessResult: + + async def process( + self, query: core_entities.Query, stage_inst_name: str + ) -> entities.StageProcessResult: # 检查是否包含非 Plain 组件 contains_non_plain = False @@ -66,13 +81,19 @@ class LongTextProcessStage(stage.PipelineStage): if not isinstance(msg, platform_message.Plain): contains_non_plain = True break - + if contains_non_plain: - self.ap.logger.debug("消息中包含非 Plain 组件,跳过长消息处理。") - elif len(str(query.resp_message_chain[-1])) > query.pipeline_config['output']['long-text-processing']['threshold']: - query.resp_message_chain[-1] = platform_message.MessageChain(await self.strategy_impl.process(str(query.resp_message_chain[-1]), query)) + self.ap.logger.debug('消息中包含非 Plain 组件,跳过长消息处理。') + elif ( + len(str(query.resp_message_chain[-1])) + > query.pipeline_config['output']['long-text-processing']['threshold'] + ): + query.resp_message_chain[-1] = platform_message.MessageChain( + await self.strategy_impl.process( + str(query.resp_message_chain[-1]), query + ) + ) return entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query + result_type=entities.ResultType.CONTINUE, new_query=query ) diff --git a/pkg/pipeline/longtext/strategies/forward.py b/pkg/pipeline/longtext/strategies/forward.py index 7abb9c6e..57084d76 100644 --- a/pkg/pipeline/longtext/strategies/forward.py +++ b/pkg/pipeline/longtext/strategies/forward.py @@ -1,8 +1,6 @@ # 转发消息组件 from __future__ import annotations -import typing -import pydantic.v1 as pydantic from .. import strategy as strategy_model from ....core import entities as core_entities @@ -13,29 +11,27 @@ ForwardMessageDiaplay = platform_message.ForwardMessageDiaplay Forward = platform_message.Forward -@strategy_model.strategy_class("forward") +@strategy_model.strategy_class('forward') class ForwardComponentStrategy(strategy_model.LongTextStrategy): - - async def process(self, message: str, query: core_entities.Query) -> list[platform_message.MessageComponent]: + async def process( + self, message: str, query: core_entities.Query + ) -> list[platform_message.MessageComponent]: display = ForwardMessageDiaplay( - title="群聊的聊天记录", - brief="[聊天记录]", - source="聊天记录", - preview=["QQ用户: "+message], - summary="查看1条转发消息" + title='群聊的聊天记录', + brief='[聊天记录]', + source='聊天记录', + preview=['QQ用户: ' + message], + summary='查看1条转发消息', ) node_list = [ platform_message.ForwardMessageNode( sender_id=query.adapter.bot_account_id, sender_name='QQ用户', - message_chain=platform_message.MessageChain([message]) + message_chain=platform_message.MessageChain([message]), ) ] - forward = Forward( - display=display, - node_list=node_list - ) + forward = Forward(display=display, node_list=node_list) return [forward] diff --git a/pkg/pipeline/longtext/strategies/image.py b/pkg/pipeline/longtext/strategies/image.py index b30d3a81..26c4b731 100644 --- a/pkg/pipeline/longtext/strategies/image.py +++ b/pkg/pipeline/longtext/strategies/image.py @@ -1,6 +1,5 @@ from __future__ import annotations -import typing import os import base64 import time @@ -15,26 +14,30 @@ from .. import strategy as strategy_model from ....core import entities as core_entities -@strategy_model.strategy_class("image") +@strategy_model.strategy_class('image') class Text2ImageStrategy(strategy_model.LongTextStrategy): - async def initialize(self): pass @functools.lru_cache(maxsize=16) def get_font(self, query: core_entities.Query): - return ImageFont.truetype(query.pipeline_config['output']['long-text-processing']['font-path'], 32, encoding="utf-8") - - async def process(self, message: str, query: core_entities.Query) -> list[platform_message.MessageComponent]: + return ImageFont.truetype( + query.pipeline_config['output']['long-text-processing']['font-path'], + 32, + encoding='utf-8', + ) + + async def process( + self, message: str, query: core_entities.Query + ) -> list[platform_message.MessageComponent]: img_path = self.text_to_image( text_str=message, save_as='temp/{}.png'.format(int(time.time())), - query=query + query=query, ) compressed_path, size = self.compress_image( - img_path, - outfile="temp/{}_compressed.png".format(int(time.time())) + img_path, outfile='temp/{}_compressed.png'.format(int(time.time())) ) with open(compressed_path, 'rb') as f: @@ -93,13 +96,11 @@ class Text2ImageStrategy(strategy_model.LongTextStrategy): resultIndex.append(v) return resultIndex - def get_size(self, file): # 获取文件大小:KB size = os.path.getsize(file) return size / 1024 - def get_outfile(self, infile, outfile): if outfile: return outfile @@ -107,7 +108,6 @@ class Text2ImageStrategy(strategy_model.LongTextStrategy): outfile = '{}-out{}'.format(dir, suffix) return outfile - def compress_image(self, infile, outfile='', kb=100, step=20, quality=90): """不改变图片尺寸压缩到指定大小 :param infile: 压缩源文件 @@ -130,24 +130,28 @@ class Text2ImageStrategy(strategy_model.LongTextStrategy): o_size = self.get_size(outfile) return outfile, self.get_size(outfile) + def text_to_image( + self, + text_str: str, + save_as='temp.png', + width=800, + query: core_entities.Query = None, + ): + text_str = text_str.replace('\t', ' ') - def text_to_image(self, text_str: str, save_as="temp.png", width=800, query: core_entities.Query = None): - - text_str = text_str.replace("\t", " ") - # 分行 lines = text_str.split('\n') # 计算并分割 final_lines = [] - text_width = width-80 + text_width = width - 80 - self.ap.logger.debug("lines: {}, text_width: {}".format(lines, text_width)) + self.ap.logger.debug('lines: {}, text_width: {}'.format(lines, text_width)) for line in lines: # 如果长了就分割 line_width = self.get_font(query).getlength(line) - self.ap.logger.debug("line_width: {}".format(line_width)) + self.ap.logger.debug('line_width: {}'.format(line_width)) if line_width < text_width: final_lines.append(line) continue @@ -161,7 +165,10 @@ class Text2ImageStrategy(strategy_model.LongTextStrategy): numbers = self.indexNumber(rest_text) for number in numbers: - if number[1] < point < number[1] + len(number[0]) and number[1] != 0: + if ( + number[1] < point < number[1] + len(number[0]) + and number[1] != 0 + ): point = number[1] break @@ -174,16 +181,23 @@ class Text2ImageStrategy(strategy_model.LongTextStrategy): else: continue # 准备画布 - img = Image.new('RGBA', (width, max(280, len(final_lines) * 35 + 65)), (255, 255, 255, 255)) + img = Image.new( + 'RGBA', (width, max(280, len(final_lines) * 35 + 65)), (255, 255, 255, 255) + ) draw = ImageDraw.Draw(img, mode='RGBA') - self.ap.logger.debug("正在绘制图片...") + self.ap.logger.debug('正在绘制图片...') # 绘制正文 line_number = 0 offset_x = 20 offset_y = 30 for final_line in final_lines: - draw.text((offset_x, offset_y + 35 * line_number), final_line, fill=(0, 0, 0), font=self.text_render_font) + draw.text( + (offset_x, offset_y + 35 * line_number), + final_line, + fill=(0, 0, 0), + font=self.text_render_font, + ) # 遍历此行,检查是否有emoji idx_in_line = 0 for ch in final_line: @@ -196,7 +210,7 @@ class Text2ImageStrategy(strategy_model.LongTextStrategy): line_number += 1 - self.ap.logger.debug("正在保存图片...") + self.ap.logger.debug('正在保存图片...') img.save(save_as) return save_as diff --git a/pkg/pipeline/longtext/strategy.py b/pkg/pipeline/longtext/strategy.py index 6f66bbff..4e141045 100644 --- a/pkg/pipeline/longtext/strategy.py +++ b/pkg/pipeline/longtext/strategy.py @@ -12,7 +12,7 @@ preregistered_strategies: list[typing.Type[LongTextStrategy]] = [] def strategy_class( - name: str + name: str, ) -> typing.Callable[[typing.Type[LongTextStrategy]], typing.Type[LongTextStrategy]]: """长文本处理策略类装饰器 @@ -36,8 +36,7 @@ def strategy_class( class LongTextStrategy(metaclass=abc.ABCMeta): - """长文本处理策略抽象类 - """ + """长文本处理策略抽象类""" name: str @@ -45,12 +44,14 @@ class LongTextStrategy(metaclass=abc.ABCMeta): def __init__(self, ap: app.Application): self.ap = ap - + async def initialize(self): pass - + @abc.abstractmethod - async def process(self, message: str, query: core_entities.Query) -> list[platform_message.MessageComponent]: + async def process( + self, message: str, query: core_entities.Query + ) -> list[platform_message.MessageComponent]: """处理长文本 在 platform.json 中配置 long-text-process 字段,只要 文本长度超过了 threshold 就会调用此方法 diff --git a/pkg/pipeline/msgtrun/msgtrun.py b/pkg/pipeline/msgtrun/msgtrun.py index b3fb593a..2595e289 100644 --- a/pkg/pipeline/msgtrun/msgtrun.py +++ b/pkg/pipeline/msgtrun/msgtrun.py @@ -3,33 +3,38 @@ from __future__ import annotations from .. import stage, entities from ...core import entities as core_entities from . import truncator -from .truncators import round +from ...utils import importutil + +from . import truncators + +importutil.import_modules_in_pkg(truncators) -@stage.stage_class("ConversationMessageTruncator") +@stage.stage_class('ConversationMessageTruncator') class ConversationMessageTruncator(stage.PipelineStage): """会话消息截断器 用于截断会话消息链,以适应平台消息长度限制。 """ + trun: truncator.Truncator async def initialize(self, pipeline_config: dict): - use_method = "round" + use_method = 'round' for trun in truncator.preregistered_truncators: if trun.name == use_method: self.trun = trun(self.ap) break else: - raise ValueError(f"未知的截断器: {use_method}") + raise ValueError(f'未知的截断器: {use_method}') - async def process(self, query: core_entities.Query, stage_inst_name: str) -> entities.StageProcessResult: - """处理 - """ + async def process( + self, query: core_entities.Query, stage_inst_name: str + ) -> entities.StageProcessResult: + """处理""" query = await self.trun.truncate(query) return entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query - ) \ No newline at end of file + result_type=entities.ResultType.CONTINUE, new_query=query + ) diff --git a/pkg/pipeline/msgtrun/truncator.py b/pkg/pipeline/msgtrun/truncator.py index 4afaf9fb..9e8b8a6c 100644 --- a/pkg/pipeline/msgtrun/truncator.py +++ b/pkg/pipeline/msgtrun/truncator.py @@ -10,7 +10,7 @@ preregistered_truncators: list[typing.Type[Truncator]] = [] def truncator_class( - name: str + name: str, ) -> typing.Callable[[typing.Type[Truncator]], typing.Type[Truncator]]: """截断器类装饰器 @@ -20,6 +20,7 @@ def truncator_class( Returns: typing.Callable[[typing.Type[Truncator]], typing.Type[Truncator]]: 装饰器 """ + def decorator(cls: typing.Type[Truncator]) -> typing.Type[Truncator]: assert issubclass(cls, Truncator) @@ -33,13 +34,12 @@ def truncator_class( class Truncator(abc.ABC): - """消息截断器基类 - """ + """消息截断器基类""" name: str ap: app.Application - + def __init__(self, ap: app.Application): self.ap = ap diff --git a/pkg/pipeline/msgtrun/truncators/round.py b/pkg/pipeline/msgtrun/truncators/round.py index 46fce5f3..fa72a0e1 100644 --- a/pkg/pipeline/msgtrun/truncators/round.py +++ b/pkg/pipeline/msgtrun/truncators/round.py @@ -4,14 +4,12 @@ from .. import truncator from ....core import entities as core_entities -@truncator.truncator_class("round") +@truncator.truncator_class('round') class RoundTruncator(truncator.Truncator): - """前文回合数阶段器 - """ + """前文回合数阶段器""" async def truncate(self, query: core_entities.Query) -> core_entities.Query: - """截断 - """ + """截断""" max_round = query.pipeline_config['ai']['local-agent']['max-round'] temp_messages = [] @@ -26,7 +24,7 @@ class RoundTruncator(truncator.Truncator): current_round += 1 else: break - + query.messages = temp_messages[::-1] return query diff --git a/pkg/pipeline/pipelinemgr.py b/pkg/pipeline/pipelinemgr.py index b7eaaab4..8ca0d592 100644 --- a/pkg/pipeline/pipelinemgr.py +++ b/pkg/pipeline/pipelinemgr.py @@ -11,22 +11,39 @@ from ..entity.persistence import pipeline as persistence_pipeline from . import stage from ..platform.types import message as platform_message, events as platform_events from ..plugin import events +from ..utils import importutil -from .resprule import resprule -from .bansess import bansess -from .cntfilter import cntfilter -from .process import process -from .longtext import longtext -from .respback import respback -from .wrapper import wrapper -from .preproc import preproc -from .ratelimit import ratelimit -from .msgtrun import msgtrun +from . import ( + resprule, + bansess, + cntfilter, + process, + longtext, + respback, + wrapper, + preproc, + ratelimit, + msgtrun, +) + +importutil.import_modules_in_pkgs( + [ + resprule, + bansess, + cntfilter, + process, + longtext, + respback, + wrapper, + preproc, + ratelimit, + msgtrun, + ] +) -class StageInstContainer(): - """阶段实例容器 - """ +class StageInstContainer: + """阶段实例容器""" inst_name: str @@ -48,7 +65,12 @@ class RuntimePipeline: stage_containers: list[StageInstContainer] """阶段实例容器""" - def __init__(self, ap: app.Application, pipeline_entity: persistence_pipeline.LegacyPipeline, stage_containers: list[StageInstContainer]): + def __init__( + self, + ap: app.Application, + pipeline_entity: persistence_pipeline.LegacyPipeline, + stage_containers: list[StageInstContainer], + ): self.ap = ap self.pipeline_entity = pipeline_entity self.stage_containers = stage_containers @@ -57,9 +79,10 @@ class RuntimePipeline: query.pipeline_config = self.pipeline_entity.config await self.process_query(query) - async def _check_output(self, query: entities.Query, result: pipeline_entities.StageProcessResult): - """检查输出 - """ + async def _check_output( + self, query: entities.Query, result: pipeline_entities.StageProcessResult + ): + """检查输出""" if result.user_notice: # 处理str类型 @@ -68,22 +91,19 @@ class RuntimePipeline: platform_message.Plain(result.user_notice) ) elif isinstance(result.user_notice, list): - result.user_notice = platform_message.MessageChain( - *result.user_notice - ) + result.user_notice = platform_message.MessageChain(*result.user_notice) - if query.pipeline_config['output']['misc']['at-sender'] and isinstance(query.message_event, platform_events.GroupMessage): + if query.pipeline_config['output']['misc']['at-sender'] and isinstance( + query.message_event, platform_events.GroupMessage + ): result.user_notice.insert( - 0, - platform_message.At( - query.message_event.sender.id - ) + 0, platform_message.At(query.message_event.sender.id) ) await query.adapter.reply_message( message_source=query.message_event, message=result.user_notice, - quote_origin=query.pipeline_config['output']['misc']['quote-origin'] + quote_origin=query.pipeline_config['output']['misc']['quote-origin'], ) if result.debug_notice: self.ap.logger.debug(result.debug_notice) @@ -123,32 +143,44 @@ class RuntimePipeline: stage_container = self.stage_containers[i] query.current_stage = stage_container # 标记到 Query 对象里 - + result = stage_container.inst.process(query, stage_container.inst_name) if isinstance(result, typing.Coroutine): result = await result if isinstance(result, pipeline_entities.StageProcessResult): # 直接返回结果 - self.ap.logger.debug(f"Stage {stage_container.inst_name} processed query {query} res {result}") + self.ap.logger.debug( + f'Stage {stage_container.inst_name} processed query {query} res {result}' + ) await self._check_output(query, result) if result.result_type == pipeline_entities.ResultType.INTERRUPT: - self.ap.logger.debug(f"Stage {stage_container.inst_name} interrupted query {query}") + self.ap.logger.debug( + f'Stage {stage_container.inst_name} interrupted query {query}' + ) break elif result.result_type == pipeline_entities.ResultType.CONTINUE: query = result.new_query elif isinstance(result, typing.AsyncGenerator): # 生成器 - self.ap.logger.debug(f"Stage {stage_container.inst_name} processed query {query} gen") + self.ap.logger.debug( + f'Stage {stage_container.inst_name} processed query {query} gen' + ) async for sub_result in result: - self.ap.logger.debug(f"Stage {stage_container.inst_name} processed query {query} res {sub_result}") + self.ap.logger.debug( + f'Stage {stage_container.inst_name} processed query {query} res {sub_result}' + ) await self._check_output(query, sub_result) if sub_result.result_type == pipeline_entities.ResultType.INTERRUPT: - self.ap.logger.debug(f"Stage {stage_container.inst_name} interrupted query {query}") + self.ap.logger.debug( + f'Stage {stage_container.inst_name} interrupted query {query}' + ) break - elif sub_result.result_type == pipeline_entities.ResultType.CONTINUE: + elif ( + sub_result.result_type == pipeline_entities.ResultType.CONTINUE + ): query = sub_result.new_query await self._execute_from_stage(i + 1, query) break @@ -156,12 +188,14 @@ class RuntimePipeline: i += 1 async def process_query(self, query: entities.Query): - """处理请求 - """ + """处理请求""" try: - # ======== 触发 MessageReceived 事件 ======== - event_type = events.PersonMessageReceived if query.launcher_type == entities.LauncherTypes.PERSON else events.GroupMessageReceived + event_type = ( + events.PersonMessageReceived + if query.launcher_type == entities.LauncherTypes.PERSON + else events.GroupMessageReceived + ) event_ctx = await self.ap.plugin_mgr.emit_event( event=event_type( @@ -169,22 +203,26 @@ class RuntimePipeline: launcher_id=query.launcher_id, sender_id=query.sender_id, message_chain=query.message_chain, - query=query + query=query, ) ) if event_ctx.is_prevented_default(): return - - self.ap.logger.debug(f"Processing query {query}") + + self.ap.logger.debug(f'Processing query {query}') await self._execute_from_stage(0, query) except Exception as e: - inst_name = query.current_stage.inst_name if query.current_stage else 'unknown' - self.ap.logger.error(f"处理请求时出错 query_id={query.query_id} stage={inst_name} : {e}") - self.ap.logger.debug(f"Traceback: {traceback.format_exc()}") + inst_name = ( + query.current_stage.inst_name if query.current_stage else 'unknown' + ) + self.ap.logger.error( + f'处理请求时出错 query_id={query.query_id} stage={inst_name} : {e}' + ) + self.ap.logger.debug(f'Traceback: {traceback.format_exc()}') finally: - self.ap.logger.debug(f"Query {query} processed") + self.ap.logger.debug(f'Query {query} processed') class PipelineManager: @@ -203,7 +241,9 @@ class PipelineManager: self.pipelines = [] async def initialize(self): - self.stage_dict = {name: cls for name, cls in stage.preregistered_stages.items()} + self.stage_dict = { + name: cls for name, cls in stage.preregistered_stages.items() + } await self.load_pipelines_from_db() @@ -220,24 +260,31 @@ class PipelineManager: for pipeline in pipelines: await self.load_pipeline(pipeline) - async def load_pipeline(self, pipeline_entity: persistence_pipeline.LegacyPipeline | sqlalchemy.Row[persistence_pipeline.LegacyPipeline] | dict): - + async def load_pipeline( + self, + pipeline_entity: persistence_pipeline.LegacyPipeline + | sqlalchemy.Row[persistence_pipeline.LegacyPipeline] + | dict, + ): if isinstance(pipeline_entity, sqlalchemy.Row): - pipeline_entity = persistence_pipeline.LegacyPipeline(**pipeline_entity._mapping) + pipeline_entity = persistence_pipeline.LegacyPipeline( + **pipeline_entity._mapping + ) elif isinstance(pipeline_entity, dict): pipeline_entity = persistence_pipeline.LegacyPipeline(**pipeline_entity) # initialize stage containers according to pipeline_entity.stages stage_containers: list[StageInstContainer] = [] for stage_name in pipeline_entity.stages: - stage_containers.append(StageInstContainer( - inst_name=stage_name, - inst=self.stage_dict[stage_name](self.ap) - )) + stage_containers.append( + StageInstContainer( + inst_name=stage_name, inst=self.stage_dict[stage_name](self.ap) + ) + ) for stage_container in stage_containers: await stage_container.inst.initialize(pipeline_entity.config) - + runtime_pipeline = RuntimePipeline(self.ap, pipeline_entity, stage_containers) self.pipelines.append(runtime_pipeline) @@ -251,4 +298,4 @@ class PipelineManager: for pipeline in self.pipelines: if pipeline.pipeline_entity.uuid == uuid: self.pipelines.remove(pipeline) - return \ No newline at end of file + return diff --git a/pkg/pipeline/pool.py b/pkg/pipeline/pool.py index df4d0741..3da4e19b 100644 --- a/pkg/pipeline/pool.py +++ b/pkg/pipeline/pool.py @@ -47,7 +47,7 @@ class QueryPool: message_chain=message_chain, resp_messages=[], resp_message_chain=[], - adapter=adapter + adapter=adapter, ) self.queries.append(query) self.query_id_counter += 1 diff --git a/pkg/pipeline/preproc/preproc.py b/pkg/pipeline/preproc/preproc.py index 42bb5b4c..bab1127d 100644 --- a/pkg/pipeline/preproc/preproc.py +++ b/pkg/pipeline/preproc/preproc.py @@ -9,7 +9,7 @@ from ...plugin import events from ...platform.types import message as platform_message -@stage.stage_class("PreProcessor") +@stage.stage_class('PreProcessor') class PreProcessor(stage.PipelineStage): """请求预处理阶段 @@ -29,11 +29,12 @@ class PreProcessor(stage.PipelineStage): query: core_entities.Query, stage_inst_name: str, ) -> entities.StageProcessResult: - """处理 - """ + """处理""" session = await self.ap.sess_mgr.get_session(query) - conversation = await self.ap.sess_mgr.get_conversation(query, session, query.pipeline_config['ai']['local-agent']['prompt']) + conversation = await self.ap.sess_mgr.get_conversation( + query, session, query.pipeline_config['ai']['local-agent']['prompt'] + ) # 设置query query.session = session @@ -42,17 +43,26 @@ class PreProcessor(stage.PipelineStage): query.use_llm_model = conversation.use_llm_model - query.use_funcs = conversation.use_funcs if query.use_llm_model.model_entity.abilities.__contains__('tool_call') else None + query.use_funcs = ( + conversation.use_funcs + if query.use_llm_model.model_entity.abilities.__contains__('tool_call') + else None + ) query.variables = { - "session_id": f"{query.session.launcher_type.value}_{query.session.launcher_id}", - "conversation_id": conversation.uuid, - "msg_create_time": int(query.message_event.time) if query.message_event.time else int(datetime.datetime.now().timestamp()), + 'session_id': f'{query.session.launcher_type.value}_{query.session.launcher_id}', + 'conversation_id': conversation.uuid, + 'msg_create_time': int(query.message_event.time) + if query.message_event.time + else int(datetime.datetime.now().timestamp()), } # Check if this model supports vision, if not, remove all images # TODO this checking should be performed in runner, and in this stage, the image should be reserved - if query.pipeline_config['ai']['runner']['runner'] == 'local-agent' and not query.use_llm_model.model_entity.abilities.__contains__('vision'): + if ( + query.pipeline_config['ai']['runner']['runner'] == 'local-agent' + and not query.use_llm_model.model_entity.abilities.__contains__('vision') + ): for msg in query.messages: if isinstance(msg.content, list): for me in msg.content: @@ -61,16 +71,17 @@ class PreProcessor(stage.PipelineStage): content_list = [] - plain_text = "" + plain_text = '' for me in query.message_chain: if isinstance(me, platform_message.Plain): - content_list.append( - llm_entities.ContentElement.from_text(me.text) - ) + content_list.append(llm_entities.ContentElement.from_text(me.text)) plain_text += me.text elif isinstance(me, platform_message.Image): - if query.pipeline_config['ai']['runner']['runner'] != 'local-agent' or query.use_llm_model.model_entity.abilities.__contains__('vision'): + if ( + query.pipeline_config['ai']['runner']['runner'] != 'local-agent' + or query.use_llm_model.model_entity.abilities.__contains__('vision') + ): if me.base64 is not None: content_list.append( llm_entities.ContentElement.from_image_base64(me.base64) @@ -78,10 +89,7 @@ class PreProcessor(stage.PipelineStage): query.variables['user_message_text'] = plain_text - query.user_message = llm_entities.Message( - role='user', - content=content_list - ) + query.user_message = llm_entities.Message(role='user', content=content_list) # =========== 触发事件 PromptPreProcessing event_ctx = await self.ap.plugin_mgr.emit_event( @@ -89,7 +97,7 @@ class PreProcessor(stage.PipelineStage): session_name=f'{query.session.launcher_type.value}_{query.session.launcher_id}', default_prompt=query.prompt.messages, prompt=query.messages, - query=query + query=query, ) ) @@ -97,6 +105,5 @@ class PreProcessor(stage.PipelineStage): query.messages = event_ctx.event.prompt return entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query + result_type=entities.ResultType.CONTINUE, new_query=query ) diff --git a/pkg/pipeline/process/handler.py b/pkg/pipeline/process/handler.py index 879b4cfe..8a32bcfb 100644 --- a/pkg/pipeline/process/handler.py +++ b/pkg/pipeline/process/handler.py @@ -8,7 +8,6 @@ from .. import entities class MessageHandler(metaclass=abc.ABCMeta): - ap: app.Application def __init__(self, ap: app.Application): diff --git a/pkg/pipeline/process/handlers/chat.py b/pkg/pipeline/process/handlers/chat.py index 9d231dda..7943d8d1 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -3,33 +3,36 @@ from __future__ import annotations import typing import time import traceback -import json from .. import handler from ... import entities from ....core import entities as core_entities -from ....provider import entities as llm_entities from ....provider import runner as runner_module -from ....provider.runners import localagent, difysvapi, dashscopeapi from ....plugin import events from ....platform.types import message as platform_message +from ....utils import importutil +from ....provider import runners + +importutil.import_modules_in_pkg(runners) class ChatMessageHandler(handler.MessageHandler): - async def handle( self, query: core_entities.Query, ) -> typing.AsyncGenerator[entities.StageProcessResult, None]: - """处理 - """ + """处理""" # 调API # 生成器 # 触发插件事件 - event_class = events.PersonNormalMessageReceived if query.launcher_type == core_entities.LauncherTypes.PERSON else events.GroupNormalMessageReceived + event_class = ( + events.PersonNormalMessageReceived + if query.launcher_type == core_entities.LauncherTypes.PERSON + else events.GroupNormalMessageReceived + ) event_ctx = await self.ap.plugin_mgr.emit_event( event=event_class( @@ -37,7 +40,7 @@ class ChatMessageHandler(handler.MessageHandler): launcher_id=query.launcher_id, sender_id=query.sender_id, text_message=str(query.message_chain), - query=query + query=query, ) ) @@ -48,16 +51,13 @@ class ChatMessageHandler(handler.MessageHandler): query.resp_messages.append(mc) yield entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query + result_type=entities.ResultType.CONTINUE, new_query=query ) else: yield entities.StageProcessResult( - result_type=entities.ResultType.INTERRUPT, - new_query=query + result_type=entities.ResultType.INTERRUPT, new_query=query ) else: - if event_ctx.event.alter is not None: # if isinstance(event_ctx.event, str): # 现在暂时不考虑多模态alter query.user_message.content = event_ctx.event.alter @@ -67,48 +67,52 @@ class ChatMessageHandler(handler.MessageHandler): start_time = time.time() try: - for r in runner_module.preregistered_runners: - if r.name == query.pipeline_config["ai"]["runner"]["runner"]: + if r.name == query.pipeline_config['ai']['runner']['runner']: runner = r(self.ap, query.pipeline_config) break else: - raise ValueError(f"未找到请求运行器: {query.pipeline_config['ai']['runner']['runner']}") + raise ValueError( + f'未找到请求运行器: {query.pipeline_config["ai"]["runner"]["runner"]}' + ) async for result in runner.run(query): query.resp_messages.append(result) - self.ap.logger.info(f'对话({query.query_id})响应: {self.cut_str(result.readable_str())}') + self.ap.logger.info( + f'对话({query.query_id})响应: {self.cut_str(result.readable_str())}' + ) if result.content is not None: text_length += len(result.content) yield entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query + result_type=entities.ResultType.CONTINUE, new_query=query ) query.session.using_conversation.messages.append(query.user_message) query.session.using_conversation.messages.extend(query.resp_messages) except Exception as e: - - self.ap.logger.error(f'对话({query.query_id})请求失败: {type(e).__name__} {str(e)}') + self.ap.logger.error( + f'对话({query.query_id})请求失败: {type(e).__name__} {str(e)}' + ) - hide_exception_info = query.pipeline_config['output']['misc']['hide-exception'] + hide_exception_info = query.pipeline_config['output']['misc'][ + 'hide-exception' + ] yield entities.StageProcessResult( result_type=entities.ResultType.INTERRUPT, new_query=query, user_notice='请求失败' if hide_exception_info else f'{e}', error_notice=f'{e}', - debug_notice=traceback.format_exc() + debug_notice=traceback.format_exc(), ) finally: - await self.ap.ctr_mgr.usage.post_query_record( session_type=query.session.launcher_type.value, session_id=str(query.session.launcher_id), - query_ability_provider="LangBot.Chat", + query_ability_provider='LangBot.Chat', usage=text_length, model_name=query.use_model.name, response_seconds=int(time.time() - start_time), diff --git a/pkg/pipeline/process/handlers/command.py b/pkg/pipeline/process/handlers/command.py index b0316e1f..af1357b5 100644 --- a/pkg/pipeline/process/handlers/command.py +++ b/pkg/pipeline/process/handlers/command.py @@ -11,24 +11,29 @@ from ....platform.types import message as platform_message class CommandHandler(handler.MessageHandler): - async def handle( self, query: core_entities.Query, ) -> typing.AsyncGenerator[entities.StageProcessResult, None]: - """处理 - """ + """处理""" command_text = str(query.message_chain).strip()[1:] privilege = 1 - - if f'{query.launcher_type.value}_{query.launcher_id}' in self.ap.instance_config.data['admins']: + + if ( + f'{query.launcher_type.value}_{query.launcher_id}' + in self.ap.instance_config.data['admins'] + ): privilege = 2 spt = command_text.split(' ') - event_class = events.PersonCommandSent if query.launcher_type == core_entities.LauncherTypes.PERSON else events.GroupCommandSent + event_class = ( + events.PersonCommandSent + if query.launcher_type == core_entities.LauncherTypes.PERSON + else events.GroupCommandSent + ) event_ctx = await self.ap.plugin_mgr.emit_event( event=event_class( @@ -38,41 +43,35 @@ class CommandHandler(handler.MessageHandler): command=spt[0], params=spt[1:] if len(spt) > 1 else [], text_message=str(query.message_chain), - is_admin=(privilege==2), - query=query + is_admin=(privilege == 2), + query=query, ) ) if event_ctx.is_prevented_default(): - if event_ctx.event.reply is not None: mc = platform_message.MessageChain(event_ctx.event.reply) query.resp_messages.append(mc) yield entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query + result_type=entities.ResultType.CONTINUE, new_query=query ) else: yield entities.StageProcessResult( - result_type=entities.ResultType.INTERRUPT, - new_query=query + result_type=entities.ResultType.INTERRUPT, new_query=query ) else: - if event_ctx.event.alter is not None: - query.message_chain = platform_message.MessageChain([ - platform_message.Plain(event_ctx.event.alter) - ]) + query.message_chain = platform_message.MessageChain( + [platform_message.Plain(event_ctx.event.alter)] + ) session = await self.ap.sess_mgr.get_session(query) async for ret in self.ap.cmd_mgr.execute( - command_text=command_text, - query=query, - session=session + command_text=command_text, query=query, session=session ): if ret.error is not None: query.resp_messages.append( @@ -82,20 +81,18 @@ class CommandHandler(handler.MessageHandler): ) ) - self.ap.logger.info(f'命令({query.query_id})报错: {self.cut_str(str(ret.error))}') + self.ap.logger.info( + f'命令({query.query_id})报错: {self.cut_str(str(ret.error))}' + ) yield entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query + result_type=entities.ResultType.CONTINUE, new_query=query ) elif ret.text is not None or ret.image_url is not None: - - content: list[llm_entities.ContentElement]= [] + content: list[llm_entities.ContentElement] = [] if ret.text is not None: - content.append( - llm_entities.ContentElement.from_text(ret.text) - ) + content.append(llm_entities.ContentElement.from_text(ret.text)) if ret.image_url is not None: content.append( @@ -112,11 +109,9 @@ class CommandHandler(handler.MessageHandler): self.ap.logger.info(f'命令返回: {self.cut_str(str(content[0]))}') yield entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query + result_type=entities.ResultType.CONTINUE, new_query=query ) else: yield entities.StageProcessResult( - result_type=entities.ResultType.INTERRUPT, - new_query=query + result_type=entities.ResultType.INTERRUPT, new_query=query ) diff --git a/pkg/pipeline/process/process.py b/pkg/pipeline/process/process.py index 11f43d3c..64903552 100644 --- a/pkg/pipeline/process/process.py +++ b/pkg/pipeline/process/process.py @@ -1,18 +1,16 @@ from __future__ import annotations -from ...core import app, entities as core_entities +from ...core import entities as core_entities from . import handler from .handlers import chat, command from .. import entities -from .. import stage, entities -from ...core import entities as core_entities -from ...config import manager as cfg_mgr +from .. import stage -@stage.stage_class("MessageProcessor") +@stage.stage_class('MessageProcessor') class Processor(stage.PipelineStage): """请求实际处理阶段 - + 通过命令处理器和聊天处理器处理消息。 改写: @@ -35,11 +33,12 @@ class Processor(stage.PipelineStage): query: core_entities.Query, stage_inst_name: str, ) -> entities.StageProcessResult: - """处理 - """ + """处理""" message_text = str(query.message_chain).strip() - self.ap.logger.info(f"处理 {query.launcher_type.value}_{query.launcher_id} 的请求({query.query_id}): {message_text}") + self.ap.logger.info( + f'处理 {query.launcher_type.value}_{query.launcher_id} 的请求({query.query_id}): {message_text}' + ) async def generator(): cmd_prefix = self.ap.instance_config.data['command']['prefix'] @@ -50,5 +49,5 @@ class Processor(stage.PipelineStage): else: async for result in self.chat_handler.handle(query): yield result - + return generator() diff --git a/pkg/pipeline/ratelimit/algo.py b/pkg/pipeline/ratelimit/algo.py index d9baa801..3bcc347a 100644 --- a/pkg/pipeline/ratelimit/algo.py +++ b/pkg/pipeline/ratelimit/algo.py @@ -7,19 +7,19 @@ from ...core import app, entities as core_entities preregistered_algos: list[typing.Type[ReteLimitAlgo]] = [] + def algo_class(name: str): - def decorator(cls: typing.Type[ReteLimitAlgo]) -> typing.Type[ReteLimitAlgo]: cls.name = name preregistered_algos.append(cls) return cls - + return decorator class ReteLimitAlgo(metaclass=abc.ABCMeta): """限流算法抽象类""" - + name: str = None ap: app.Application @@ -31,11 +31,16 @@ class ReteLimitAlgo(metaclass=abc.ABCMeta): pass @abc.abstractmethod - async def require_access(self, query: core_entities.Query, launcher_type: str, launcher_id: typing.Union[int, str]) -> bool: + async def require_access( + self, + query: core_entities.Query, + launcher_type: str, + launcher_id: typing.Union[int, str], + ) -> bool: """进入处理流程 这个方法对等待是友好的,意味着算法可以实现在这里等待一段时间以控制速率。 - + Args: launcher_type (str): 请求者类型 群聊为 group 私聊为 person launcher_id (int): 请求者ID @@ -44,15 +49,19 @@ class ReteLimitAlgo(metaclass=abc.ABCMeta): bool: 是否允许进入处理流程,若返回false,则直接丢弃该请求 """ raise NotImplementedError - + @abc.abstractmethod - async def release_access(self, query: core_entities.Query, launcher_type: str, launcher_id: typing.Union[int, str]): + async def release_access( + self, + query: core_entities.Query, + launcher_type: str, + launcher_id: typing.Union[int, str], + ): """退出处理流程 Args: launcher_type (str): 请求者类型 群聊为 group 私聊为 person launcher_id (int): 请求者ID """ - + raise NotImplementedError - \ No newline at end of file diff --git a/pkg/pipeline/ratelimit/algos/fixedwin.py b/pkg/pipeline/ratelimit/algos/fixedwin.py index f17e93b8..32079a97 100644 --- a/pkg/pipeline/ratelimit/algos/fixedwin.py +++ b/pkg/pipeline/ratelimit/algos/fixedwin.py @@ -5,9 +5,9 @@ import typing from .. import algo from ....core import entities as core_entities + # 固定窗口算法 class SessionContainer: - wait_lock: asyncio.Lock records: dict[int, int] @@ -18,9 +18,8 @@ class SessionContainer: self.records = {} -@algo.algo_class("fixwin") +@algo.algo_class('fixwin') class FixedWindowAlgo(algo.ReteLimitAlgo): - containers_lock: asyncio.Lock """访问记录容器锁""" @@ -31,7 +30,12 @@ class FixedWindowAlgo(algo.ReteLimitAlgo): self.containers_lock = asyncio.Lock() self.containers = {} - async def require_access(self, query: core_entities.Query, launcher_type: str, launcher_id: typing.Union[int, str]) -> bool: + async def require_access( + self, + query: core_entities.Query, + launcher_type: str, + launcher_id: typing.Union[int, str], + ) -> bool: # 加锁,找容器 container: SessionContainer = None @@ -46,7 +50,6 @@ class FixedWindowAlgo(algo.ReteLimitAlgo): # 等待锁 async with container.wait_lock: - # 获取窗口大小和限制 window_size = query.pipeline_config['safety']['rate-limit']['window-length'] limitation = query.pipeline_config['safety']['rate-limit']['limitation'] @@ -69,13 +72,15 @@ class FixedWindowAlgo(algo.ReteLimitAlgo): if count >= limitation: if query.pipeline_config['safety']['rate-limit']['strategy'] == 'drop': return False - elif query.pipeline_config['safety']['rate-limit']['strategy'] == 'wait': + elif ( + query.pipeline_config['safety']['rate-limit']['strategy'] == 'wait' + ): # 等待下一窗口 await asyncio.sleep(window_size - time.time() % window_size) - + now = int(time.time()) now = now - now % window_size - + if now not in container.records: container.records = {} container.records[now] = 1 @@ -85,6 +90,11 @@ class FixedWindowAlgo(algo.ReteLimitAlgo): # 返回True return True - - async def release_access(self, query: core_entities.Query, launcher_type: str, launcher_id: typing.Union[int, str]): + + async def release_access( + self, + query: core_entities.Query, + launcher_type: str, + launcher_id: typing.Union[int, str], + ): pass diff --git a/pkg/pipeline/ratelimit/ratelimit.py b/pkg/pipeline/ratelimit/ratelimit.py index c74db978..23de4ec6 100644 --- a/pkg/pipeline/ratelimit/ratelimit.py +++ b/pkg/pipeline/ratelimit/ratelimit.py @@ -4,22 +4,25 @@ import typing from .. import entities, stage from . import algo -from .algos import fixedwin from ...core import entities as core_entities +from ...utils import importutil + +from . import algos + +importutil.import_modules_in_pkg(algos) -@stage.stage_class("RequireRateLimitOccupancy") -@stage.stage_class("ReleaseRateLimitOccupancy") +@stage.stage_class('RequireRateLimitOccupancy') +@stage.stage_class('ReleaseRateLimitOccupancy') class RateLimit(stage.PipelineStage): """限速器控制阶段 - + 不改写query,只检查是否需要限速。 """ algo: algo.ReteLimitAlgo async def initialize(self, pipeline_config: dict): - algo_name = 'fixwin' algo_class = None @@ -42,9 +45,8 @@ class RateLimit(stage.PipelineStage): entities.StageProcessResult, typing.AsyncGenerator[entities.StageProcessResult, None], ]: - """处理 - """ - if stage_inst_name == "RequireRateLimitOccupancy": + """处理""" + if stage_inst_name == 'RequireRateLimitOccupancy': if await self.algo.require_access( query, query.launcher_type.value, @@ -58,10 +60,10 @@ class RateLimit(stage.PipelineStage): return entities.StageProcessResult( result_type=entities.ResultType.INTERRUPT, new_query=query, - console_notice=f"根据限速规则忽略 {query.launcher_type.value}:{query.launcher_id} 消息", - user_notice=f"请求数超过限速器设定值,已丢弃本消息。" + console_notice=f'根据限速规则忽略 {query.launcher_type.value}:{query.launcher_id} 消息', + user_notice='请求数超过限速器设定值,已丢弃本消息。', ) - elif stage_inst_name == "ReleaseRateLimitOccupancy": + elif stage_inst_name == 'ReleaseRateLimitOccupancy': await self.algo.release_access( query, query.launcher_type.value, diff --git a/pkg/pipeline/respback/respback.py b/pkg/pipeline/respback/respback.py index 8c074d89..42c141c8 100644 --- a/pkg/pipeline/respback/respback.py +++ b/pkg/pipeline/respback/respback.py @@ -4,41 +4,38 @@ import random import asyncio -from ...core import app from ...platform.types import events as platform_events from ...platform.types import message as platform_message from .. import stage, entities from ...core import entities as core_entities -from ...config import manager as cfg_mgr -@stage.stage_class("SendResponseBackStage") +@stage.stage_class('SendResponseBackStage') class SendResponseBackStage(stage.PipelineStage): - """发送响应消息 - """ + """发送响应消息""" - async def process(self, query: core_entities.Query, stage_inst_name: str) -> entities.StageProcessResult: - """处理 - """ + async def process( + self, query: core_entities.Query, stage_inst_name: str + ) -> entities.StageProcessResult: + """处理""" - random_range = (query.pipeline_config['output']['force-delay']['min'], query.pipeline_config['output']['force-delay']['max']) + random_range = ( + query.pipeline_config['output']['force-delay']['min'], + query.pipeline_config['output']['force-delay']['max'], + ) random_delay = random.uniform(*random_range) - self.ap.logger.debug( - "根据规则强制延迟回复: %s s", - random_delay - ) + self.ap.logger.debug('根据规则强制延迟回复: %s s', random_delay) await asyncio.sleep(random_delay) - if query.pipeline_config['output']['misc']['at-sender'] and isinstance(query.message_event, platform_events.GroupMessage): + if query.pipeline_config['output']['misc']['at-sender'] and isinstance( + query.message_event, platform_events.GroupMessage + ): query.resp_message_chain[-1].insert( - 0, - platform_message.At( - query.message_event.sender.id - ) + 0, platform_message.At(query.message_event.sender.id) ) quote_origin = query.pipeline_config['output']['misc']['quote-origin'] @@ -46,10 +43,9 @@ class SendResponseBackStage(stage.PipelineStage): await query.adapter.reply_message( message_source=query.message_event, message=query.resp_message_chain[-1], - quote_origin=quote_origin + quote_origin=quote_origin, ) return entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query - ) \ No newline at end of file + result_type=entities.ResultType.CONTINUE, new_query=query + ) diff --git a/pkg/pipeline/resprule/entities.py b/pkg/pipeline/resprule/entities.py index a334e843..a0ba7807 100644 --- a/pkg/pipeline/resprule/entities.py +++ b/pkg/pipeline/resprule/entities.py @@ -4,7 +4,6 @@ from ...platform.types import message as platform_message class RuleJudgeResult(pydantic.BaseModel): - matching: bool = False replacement: platform_message.MessageChain = None diff --git a/pkg/pipeline/resprule/resprule.py b/pkg/pipeline/resprule/resprule.py index 08ba49e8..99402351 100644 --- a/pkg/pipeline/resprule/resprule.py +++ b/pkg/pipeline/resprule/resprule.py @@ -1,16 +1,18 @@ from __future__ import annotations -from ...core import app -from . import entities as rule_entities, rule -from .rules import atbot, prefix, regexp, random +from . import rule from .. import stage, entities from ...core import entities as core_entities -from ...config import manager as cfg_mgr +from ...utils import importutil + +from . import rules + +importutil.import_modules_in_pkg(rules) -@stage.stage_class("GroupRespondRuleCheckStage") +@stage.stage_class('GroupRespondRuleCheckStage') class GroupRespondRuleCheckStage(stage.PipelineStage): """群组响应规则检查器 @@ -21,8 +23,7 @@ class GroupRespondRuleCheckStage(stage.PipelineStage): """检查器实例""" async def initialize(self, pipeline_config: dict): - """初始化检查器 - """ + """初始化检查器""" self.rule_matchers = [] @@ -31,12 +32,12 @@ class GroupRespondRuleCheckStage(stage.PipelineStage): await rule_inst.initialize() self.rule_matchers.append(rule_inst) - async def process(self, query: core_entities.Query, stage_inst_name: str) -> entities.StageProcessResult: - + async def process( + self, query: core_entities.Query, stage_inst_name: str + ) -> entities.StageProcessResult: if query.launcher_type.value != 'group': # 只处理群消息 return entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query + result_type=entities.ResultType.CONTINUE, new_query=query ) rules = query.pipeline_config['trigger']['group-respond-rules'] @@ -48,7 +49,9 @@ class GroupRespondRuleCheckStage(stage.PipelineStage): # use_rule = rules[str(query.launcher_id)] for rule_matcher in self.rule_matchers: # 任意一个匹配就放行 - res = await rule_matcher.match(str(query.message_chain), query.message_chain, use_rule, query) + res = await rule_matcher.match( + str(query.message_chain), query.message_chain, use_rule, query + ) if res.matching: query.message_chain = res.replacement @@ -56,8 +59,7 @@ class GroupRespondRuleCheckStage(stage.PipelineStage): result_type=entities.ResultType.CONTINUE, new_query=query, ) - + return entities.StageProcessResult( - result_type=entities.ResultType.INTERRUPT, - new_query=query + result_type=entities.ResultType.INTERRUPT, new_query=query ) diff --git a/pkg/pipeline/resprule/rule.py b/pkg/pipeline/resprule/rule.py index ad69d8a0..3fdb0386 100644 --- a/pkg/pipeline/resprule/rule.py +++ b/pkg/pipeline/resprule/rule.py @@ -10,17 +10,19 @@ from ...platform.types import message as platform_message preregisetered_rules: list[typing.Type[GroupRespondRule]] = [] + def rule_class(name: str): def decorator(cls: typing.Type[GroupRespondRule]) -> typing.Type[GroupRespondRule]: cls.name = name preregisetered_rules.append(cls) return cls + return decorator class GroupRespondRule(metaclass=abc.ABCMeta): - """群组响应规则的抽象类 - """ + """群组响应规则的抽象类""" + name: str ap: app.Application @@ -37,8 +39,7 @@ class GroupRespondRule(metaclass=abc.ABCMeta): message_text: str, message_chain: platform_message.MessageChain, rule_dict: dict, - query: core_entities.Query + query: core_entities.Query, ) -> entities.RuleJudgeResult: - """判断消息是否匹配规则 - """ + """判断消息是否匹配规则""" raise NotImplementedError diff --git a/pkg/pipeline/resprule/rules/atbot.py b/pkg/pipeline/resprule/rules/atbot.py index a0b7a7c8..0f4845f8 100644 --- a/pkg/pipeline/resprule/rules/atbot.py +++ b/pkg/pipeline/resprule/rules/atbot.py @@ -7,21 +7,24 @@ from ....core import entities as core_entities from ....platform.types import message as platform_message -@rule_model.rule_class("at-bot") +@rule_model.rule_class('at-bot') class AtBotRule(rule_model.GroupRespondRule): - async def match( self, message_text: str, message_chain: platform_message.MessageChain, rule_dict: dict, - query: core_entities.Query + query: core_entities.Query, ) -> entities.RuleJudgeResult: - - if message_chain.has(platform_message.At(query.adapter.bot_account_id)) and rule_dict['at']: + if ( + message_chain.has(platform_message.At(query.adapter.bot_account_id)) + and rule_dict['at'] + ): message_chain.remove(platform_message.At(query.adapter.bot_account_id)) - if message_chain.has(platform_message.At(query.adapter.bot_account_id)): # 回复消息时会at两次,检查并删除重复的 + if message_chain.has( + platform_message.At(query.adapter.bot_account_id) + ): # 回复消息时会at两次,检查并删除重复的 message_chain.remove(platform_message.At(query.adapter.bot_account_id)) return entities.RuleJudgeResult( @@ -29,7 +32,4 @@ class AtBotRule(rule_model.GroupRespondRule): replacement=message_chain, ) - return entities.RuleJudgeResult( - matching=False, - replacement = message_chain - ) + return entities.RuleJudgeResult(matching=False, replacement=message_chain) diff --git a/pkg/pipeline/resprule/rules/prefix.py b/pkg/pipeline/resprule/rules/prefix.py index fb7bbcfc..c712d3e8 100644 --- a/pkg/pipeline/resprule/rules/prefix.py +++ b/pkg/pipeline/resprule/rules/prefix.py @@ -1,36 +1,30 @@ - from .. import rule as rule_model from .. import entities from ....core import entities as core_entities from ....platform.types import message as platform_message -@rule_model.rule_class("prefix") +@rule_model.rule_class('prefix') class PrefixRule(rule_model.GroupRespondRule): - async def match( self, message_text: str, message_chain: platform_message.MessageChain, rule_dict: dict, - query: core_entities.Query + query: core_entities.Query, ) -> entities.RuleJudgeResult: prefixes = rule_dict['prefix'] for prefix in prefixes: if message_text.startswith(prefix): - # 查找第一个plain元素 for me in message_chain: if isinstance(me, platform_message.Plain): - me.text = me.text[len(prefix):] + me.text = me.text[len(prefix) :] return entities.RuleJudgeResult( matching=True, replacement=message_chain, ) - return entities.RuleJudgeResult( - matching=False, - replacement=message_chain - ) + return entities.RuleJudgeResult(matching=False, replacement=message_chain) diff --git a/pkg/pipeline/resprule/rules/random.py b/pkg/pipeline/resprule/rules/random.py index 0178f2c4..535bfe6b 100644 --- a/pkg/pipeline/resprule/rules/random.py +++ b/pkg/pipeline/resprule/rules/random.py @@ -7,19 +7,17 @@ from ....core import entities as core_entities from ....platform.types import message as platform_message -@rule_model.rule_class("random") +@rule_model.rule_class('random') class RandomRespRule(rule_model.GroupRespondRule): - async def match( self, message_text: str, message_chain: platform_message.MessageChain, rule_dict: dict, - query: core_entities.Query + query: core_entities.Query, ) -> entities.RuleJudgeResult: random_rate = rule_dict['random'] - + return entities.RuleJudgeResult( - matching=random.random() < random_rate, - replacement=message_chain - ) \ No newline at end of file + matching=random.random() < random_rate, replacement=message_chain + ) diff --git a/pkg/pipeline/resprule/rules/regexp.py b/pkg/pipeline/resprule/rules/regexp.py index f5f5b3f6..daac0869 100644 --- a/pkg/pipeline/resprule/rules/regexp.py +++ b/pkg/pipeline/resprule/rules/regexp.py @@ -7,15 +7,14 @@ from ....core import entities as core_entities from ....platform.types import message as platform_message -@rule_model.rule_class("regexp") +@rule_model.rule_class('regexp') class RegExpRule(rule_model.GroupRespondRule): - async def match( self, message_text: str, message_chain: platform_message.MessageChain, rule_dict: dict, - query: core_entities.Query + query: core_entities.Query, ) -> entities.RuleJudgeResult: regexps = rule_dict['regexp'] @@ -27,8 +26,5 @@ class RegExpRule(rule_model.GroupRespondRule): matching=True, replacement=message_chain, ) - - return entities.RuleJudgeResult( - matching=False, - replacement=message_chain - ) + + return entities.RuleJudgeResult(matching=False, replacement=message_chain) diff --git a/pkg/pipeline/stage.py b/pkg/pipeline/stage.py index 859286d9..18636e9f 100644 --- a/pkg/pipeline/stage.py +++ b/pkg/pipeline/stage.py @@ -11,17 +11,15 @@ preregistered_stages: dict[str, PipelineStage] = {} def stage_class(name: str): - def decorator(cls): preregistered_stages[name] = cls return cls - + return decorator class PipelineStage(metaclass=abc.ABCMeta): - """流水线阶段 - """ + """流水线阶段""" ap: app.Application @@ -29,8 +27,7 @@ class PipelineStage(metaclass=abc.ABCMeta): self.ap = ap async def initialize(self, pipeline_config: dict): - """初始化 - """ + """初始化""" pass @abc.abstractmethod @@ -42,6 +39,5 @@ class PipelineStage(metaclass=abc.ABCMeta): entities.StageProcessResult, typing.AsyncGenerator[entities.StageProcessResult, None], ]: - """处理 - """ + """处理""" raise NotImplementedError diff --git a/pkg/pipeline/wrapper/wrapper.py b/pkg/pipeline/wrapper/wrapper.py index 6b12ca65..bca02527 100644 --- a/pkg/pipeline/wrapper/wrapper.py +++ b/pkg/pipeline/wrapper/wrapper.py @@ -3,21 +3,19 @@ from __future__ import annotations import typing -from ...core import app, entities as core_entities -from .. import entities -from .. import stage, entities from ...core import entities as core_entities -from ...config import manager as cfg_mgr +from .. import entities +from .. import stage from ...plugin import events from ...platform.types import message as platform_message -@stage.stage_class("ResponseWrapper") +@stage.stage_class('ResponseWrapper') class ResponseWrapper(stage.PipelineStage): """回复包装阶段 把回复的 message 包装成人类识读的形式。 - + 改写: - resp_message_chain """ @@ -30,36 +28,36 @@ class ResponseWrapper(stage.PipelineStage): query: core_entities.Query, stage_inst_name: str, ) -> typing.AsyncGenerator[entities.StageProcessResult, None]: - """处理 - """ + """处理""" # 如果 resp_messages[-1] 已经是 MessageChain 了 if isinstance(query.resp_messages[-1], platform_message.MessageChain): query.resp_message_chain.append(query.resp_messages[-1]) yield entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query + result_type=entities.ResultType.CONTINUE, new_query=query ) else: - if query.resp_messages[-1].role == 'command': - query.resp_message_chain.append(query.resp_messages[-1].get_content_platform_message_chain(prefix_text='[bot] ')) + query.resp_message_chain.append( + query.resp_messages[-1].get_content_platform_message_chain( + prefix_text='[bot] ' + ) + ) yield entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query + result_type=entities.ResultType.CONTINUE, new_query=query ) elif query.resp_messages[-1].role == 'plugin': - query.resp_message_chain.append(query.resp_messages[-1].get_content_platform_message_chain()) + query.resp_message_chain.append( + query.resp_messages[-1].get_content_platform_message_chain() + ) yield entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query + result_type=entities.ResultType.CONTINUE, new_query=query ) else: - if query.resp_messages[-1].role == 'assistant': result = query.resp_messages[-1] session = await self.ap.sess_mgr.get_session(query) @@ -79,39 +77,51 @@ class ResponseWrapper(stage.PipelineStage): prefix='', response_text=reply_text, finish_reason='stop', - funcs_called=[fc.function.name for fc in result.tool_calls] if result.tool_calls is not None else [], - query=query + funcs_called=[ + fc.function.name for fc in result.tool_calls + ] + if result.tool_calls is not None + else [], + query=query, ) ) if event_ctx.is_prevented_default(): yield entities.StageProcessResult( result_type=entities.ResultType.INTERRUPT, - new_query=query + new_query=query, ) else: if event_ctx.event.reply is not None: - - query.resp_message_chain.append(platform_message.MessageChain(event_ctx.event.reply)) + query.resp_message_chain.append( + platform_message.MessageChain(event_ctx.event.reply) + ) else: - - query.resp_message_chain.append(result.get_content_platform_message_chain()) + query.resp_message_chain.append( + result.get_content_platform_message_chain() + ) yield entities.StageProcessResult( result_type=entities.ResultType.CONTINUE, - new_query=query + new_query=query, ) - if result.tool_calls is not None and len(result.tool_calls) > 0: # 有函数调用 - + if ( + result.tool_calls is not None and len(result.tool_calls) > 0 + ): # 有函数调用 function_names = [tc.function.name for tc in result.tool_calls] reply_text = f'调用函数 {".".join(function_names)}...' - query.resp_message_chain.append(platform_message.MessageChain([platform_message.Plain(reply_text)])) + query.resp_message_chain.append( + platform_message.MessageChain( + [platform_message.Plain(reply_text)] + ) + ) - if query.pipeline_config['output']['misc']['track-function-calls']: - + if query.pipeline_config['output']['misc'][ + 'track-function-calls' + ]: event_ctx = await self.ap.plugin_mgr.emit_event( event=events.NormalMessageResponded( launcher_type=query.launcher_type.value, @@ -121,26 +131,36 @@ class ResponseWrapper(stage.PipelineStage): prefix='', response_text=reply_text, finish_reason='stop', - funcs_called=[fc.function.name for fc in result.tool_calls] if result.tool_calls is not None else [], - query=query + funcs_called=[ + fc.function.name for fc in result.tool_calls + ] + if result.tool_calls is not None + else [], + query=query, ) ) if event_ctx.is_prevented_default(): yield entities.StageProcessResult( result_type=entities.ResultType.INTERRUPT, - new_query=query + new_query=query, ) else: if event_ctx.event.reply is not None: - - query.resp_message_chain.append(platform_message.MessageChain(event_ctx.event.reply)) + query.resp_message_chain.append( + platform_message.MessageChain( + event_ctx.event.reply + ) + ) else: - - query.resp_message_chain.append(platform_message.MessageChain([platform_message.Plain(reply_text)])) + query.resp_message_chain.append( + platform_message.MessageChain( + [platform_message.Plain(reply_text)] + ) + ) yield entities.StageProcessResult( result_type=entities.ResultType.CONTINUE, - new_query=query + new_query=query, ) diff --git a/pkg/platform/adapter.py b/pkg/platform/adapter.py index 42ea75e0..61ff32cd 100644 --- a/pkg/platform/adapter.py +++ b/pkg/platform/adapter.py @@ -17,7 +17,7 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): bot_account_id: int """机器人账号ID,需要在初始化时设置""" - + config: dict ap: app.Application @@ -32,14 +32,11 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): self.config = config self.ap = ap - async def send_message( - self, - target_type: str, - target_id: str, - message: platform_message.MessageChain + async def send_message( + self, target_type: str, target_id: str, message: platform_message.MessageChain ): """主动发送消息 - + Args: target_type (str): 目标类型,`person`或`group` target_id (str): 目标ID @@ -51,7 +48,7 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): self, message_source: platform_events.MessageEvent, message: platform_message.MessageChain, - quote_origin: bool = False + quote_origin: bool = False, ): """回复消息 @@ -69,23 +66,27 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): def register_listener( self, event_type: typing.Type[platform_message.Event], - callback: typing.Callable[[platform_message.Event, MessagePlatformAdapter], None] + callback: typing.Callable[ + [platform_message.Event, MessagePlatformAdapter], None + ], ): """注册事件监听器 - + Args: event_type (typing.Type[platform.types.Event]): 事件类型 callback (typing.Callable[[platform.types.Event], None]): 回调函数,接收一个参数,为事件 """ raise NotImplementedError - + def unregister_listener( self, event_type: typing.Type[platform_message.Event], - callback: typing.Callable[[platform_message.Event, MessagePlatformAdapter], None] + callback: typing.Callable[ + [platform_message.Event, MessagePlatformAdapter], None + ], ): """注销事件监听器 - + Args: event_type (typing.Type[platform.types.Event]): 事件类型 callback (typing.Callable[[platform.types.Event], None]): 回调函数,接收一个参数,为事件 @@ -98,7 +99,7 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): async def kill(self) -> bool: """关闭适配器 - + Returns: bool: 是否成功关闭,热重载时若此函数返回False则不会重载MessageSource底层 """ @@ -107,6 +108,7 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): class MessageConverter: """消息链转换器基类""" + @staticmethod def yiri2target(message_chain: platform_message.MessageChain): """将源平台消息链转换为目标平台消息链 diff --git a/pkg/platform/botmgr.py b/pkg/platform/botmgr.py index 6073243d..507f067e 100644 --- a/pkg/platform/botmgr.py +++ b/pkg/platform/botmgr.py @@ -1,23 +1,16 @@ from __future__ import annotations -import json -import os import sys -import logging import asyncio import traceback import sqlalchemy -from .sources import qqofficial # FriendMessage, Image, MessageChain, Plain from . import adapter as msadapter from ..core import app, entities as core_entities, taskmgr -from ..plugin import events -from .types import message as platform_message from .types import events as platform_events -from .types import entities as platform_entities from ..discover import engine @@ -25,6 +18,7 @@ from ..entity.persistence import bot as persistence_bot # 处理 3.4 移除了 YiriMirai 之后,插件的兼容性问题 from . import types as mirai + sys.modules['mirai'] = mirai @@ -43,7 +37,12 @@ class RuntimeBot: task_context: taskmgr.TaskContext - def __init__(self, ap: app.Application, bot_entity: persistence_bot.Bot, adapter: msadapter.MessagePlatformAdapter): + def __init__( + self, + ap: app.Application, + bot_entity: persistence_bot.Bot, + adapter: msadapter.MessagePlatformAdapter, + ): self.ap = ap self.bot_entity = bot_entity self.enable = bot_entity.enable @@ -51,9 +50,10 @@ class RuntimeBot: self.task_context = taskmgr.TaskContext() async def initialize(self): - - async def on_friend_message(event: platform_events.FriendMessage, adapter: msadapter.MessagePlatformAdapter): - + async def on_friend_message( + event: platform_events.FriendMessage, + adapter: msadapter.MessagePlatformAdapter, + ): await self.ap.query_pool.add_query( bot_uuid=self.bot_entity.uuid, launcher_type=core_entities.LauncherTypes.PERSON, @@ -64,8 +64,10 @@ class RuntimeBot: adapter=adapter, ) - async def on_group_message(event: platform_events.GroupMessage, adapter: msadapter.MessagePlatformAdapter): - + async def on_group_message( + event: platform_events.GroupMessage, + adapter: msadapter.MessagePlatformAdapter, + ): await self.ap.query_pool.add_query( bot_uuid=self.bot_entity.uuid, launcher_type=core_entities.LauncherTypes.GROUP, @@ -76,17 +78,10 @@ class RuntimeBot: adapter=adapter, ) - self.adapter.register_listener( - platform_events.FriendMessage, - on_friend_message - ) - self.adapter.register_listener( - platform_events.GroupMessage, - on_group_message - ) + self.adapter.register_listener(platform_events.FriendMessage, on_friend_message) + self.adapter.register_listener(platform_events.GroupMessage, on_group_message) async def run(self): - async def exception_wrapper(): try: self.task_context.set_current_action('Running...') @@ -98,16 +93,19 @@ class RuntimeBot: return self.task_context.set_current_action('Exited with error.') self.task_context.log(f'平台适配器运行出错: {e}') - self.task_context.log(f"Traceback: {traceback.format_exc()}") + self.task_context.log(f'Traceback: {traceback.format_exc()}') self.ap.logger.error(f'平台适配器运行出错: {e}') - self.ap.logger.debug(f"Traceback: {traceback.format_exc()}") + self.ap.logger.debug(f'Traceback: {traceback.format_exc()}') self.task_wrapper = self.ap.task_mgr.create_task( exception_wrapper(), - kind="platform-adapter", - name=f"platform-adapter-{self.adapter.__class__.__name__}", + kind='platform-adapter', + name=f'platform-adapter-{self.adapter.__class__.__name__}', context=self.task_context, - scopes=[core_entities.LifecycleControlScope.APPLICATION, core_entities.LifecycleControlScope.PLATFORM] + scopes=[ + core_entities.LifecycleControlScope.APPLICATION, + core_entities.LifecycleControlScope.PLATFORM, + ], ) async def shutdown(self): @@ -118,7 +116,6 @@ class RuntimeBot: # 控制QQ消息输入输出的类 class PlatformManager: - # ====== 4.0 ====== ap: app.Application = None @@ -129,18 +126,20 @@ class PlatformManager: adapter_dict: dict[str, type[msadapter.MessagePlatformAdapter]] def __init__(self, ap: app.Application = None): - self.ap = ap self.bots = [] self.adapter_components = [] self.adapter_dict = {} - - async def initialize(self): - self.adapter_components = self.ap.discover.get_components_by_kind('MessagePlatformAdapter') + async def initialize(self): + self.adapter_components = self.ap.discover.get_components_by_kind( + 'MessagePlatformAdapter' + ) adapter_dict: dict[str, type[msadapter.MessagePlatformAdapter]] = {} for component in self.adapter_components: - adapter_dict[component.metadata.name] = component.get_python_component_class() + adapter_dict[component.metadata.name] = ( + component.get_python_component_class() + ) self.adapter_dict = adapter_dict await self.load_bots_from_db() @@ -158,12 +157,15 @@ class PlatformManager: ) bots = result.all() - + for bot in bots: # load all bots here, enable or disable will be handled in runtime await self.load_bot(bot) - async def load_bot(self, bot_entity: persistence_bot.Bot | sqlalchemy.Row[persistence_bot.Bot] | dict) -> RuntimeBot: + async def load_bot( + self, + bot_entity: persistence_bot.Bot | sqlalchemy.Row[persistence_bot.Bot] | dict, + ) -> RuntimeBot: """加载机器人""" if isinstance(bot_entity, sqlalchemy.Row): bot_entity = persistence_bot.Bot(**bot_entity._mapping) @@ -171,14 +173,11 @@ class PlatformManager: bot_entity = persistence_bot.Bot(**bot_entity) adapter_inst = self.adapter_dict[bot_entity.adapter]( - bot_entity.adapter_config, - self.ap + bot_entity.adapter_config, self.ap ) runtime_bot = RuntimeBot( - ap=self.ap, - bot_entity=bot_entity, - adapter=adapter_inst + ap=self.ap, bot_entity=bot_entity, adapter=adapter_inst ) await runtime_bot.initialize() @@ -186,7 +185,7 @@ class PlatformManager: self.bots.append(runtime_bot) return runtime_bot - + async def get_bot_by_uuid(self, bot_uuid: str) -> RuntimeBot | None: for bot in self.bots: if bot.bot_entity.uuid == bot_uuid: @@ -202,24 +201,28 @@ class PlatformManager: return def get_available_adapters_info(self) -> list[dict]: - return [ - component.to_plain_dict() - for component in self.adapter_components - ] + return [component.to_plain_dict() for component in self.adapter_components] def get_available_adapter_info_by_name(self, name: str) -> dict | None: for component in self.adapter_components: if component.metadata.name == name: return component.to_plain_dict() return None - - def get_available_adapter_manifest_by_name(self, name: str) -> engine.Component | None: + + def get_available_adapter_manifest_by_name( + self, name: str + ) -> engine.Component | None: for component in self.adapter_components: if component.metadata.name == name: return component return None - async def write_back_config(self, adapter_name: str, adapter_inst: msadapter.MessagePlatformAdapter, config: dict): + async def write_back_config( + self, + adapter_name: str, + adapter_inst: msadapter.MessagePlatformAdapter, + config: dict, + ): # index = -2 # for i, adapter in enumerate(self.adapters): @@ -251,7 +254,7 @@ class PlatformManager: # TODO implement this pass - async def run(self): + async def run(self): # This method will only be called when the application launching for bot in self.bots: if bot.enable: diff --git a/pkg/platform/sources/aiocqhttp.py b/pkg/platform/sources/aiocqhttp.py index 9149e427..48116507 100644 --- a/pkg/platform/sources/aiocqhttp.py +++ b/pkg/platform/sources/aiocqhttp.py @@ -2,24 +2,23 @@ from __future__ import annotations import typing import asyncio import traceback -import time import datetime import aiocqhttp -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 AiocqhttpMessageConverter(adapter.MessageConverter): +class AiocqhttpMessageConverter(adapter.MessageConverter): @staticmethod - async def yiri2target(message_chain: platform_message.MessageChain) -> typing.Tuple[list, int, datetime.datetime]: + async def yiri2target( + message_chain: platform_message.MessageChain, + ) -> typing.Tuple[list, int, datetime.datetime]: msg_list = aiocqhttp.Message() msg_id = 0 @@ -35,7 +34,7 @@ class AiocqhttpMessageConverter(adapter.MessageConverter): arg = '' if msg.base64: arg = msg.base64 - msg_list.append(aiocqhttp.MessageSegment.image(f"base64://{arg}")) + msg_list.append(aiocqhttp.MessageSegment.image(f'base64://{arg}')) elif msg.url: arg = msg.url msg_list.append(aiocqhttp.MessageSegment.image(arg)) @@ -45,12 +44,12 @@ class AiocqhttpMessageConverter(adapter.MessageConverter): elif type(msg) is platform_message.At: msg_list.append(aiocqhttp.MessageSegment.at(msg.target)) elif type(msg) is platform_message.AtAll: - msg_list.append(aiocqhttp.MessageSegment.at("all")) + msg_list.append(aiocqhttp.MessageSegment.at('all')) elif type(msg) is platform_message.Voice: arg = '' if msg.base64: arg = msg.base64 - msg_list.append(aiocqhttp.MessageSegment.record(f"base64://{arg}")) + msg_list.append(aiocqhttp.MessageSegment.record(f'base64://{arg}')) elif msg.url: arg = msg.url msg_list.append(aiocqhttp.MessageSegment.record(arg)) @@ -58,10 +57,15 @@ class AiocqhttpMessageConverter(adapter.MessageConverter): arg = msg.path msg_list.append(aiocqhttp.MessageSegment.record(msg.path)) elif type(msg) is platform_message.Forward: - for node in msg.node_list: - msg_list.extend((await AiocqhttpMessageConverter.yiri2target(node.message_chain))[0]) - + msg_list.extend( + ( + await AiocqhttpMessageConverter.yiri2target( + node.message_chain + ) + )[0] + ) + else: msg_list.append(aiocqhttp.MessageSegment.text(str(msg))) @@ -78,20 +82,26 @@ class AiocqhttpMessageConverter(adapter.MessageConverter): ) for msg in message: - if msg.type == "at": - if msg.data["qq"] == "all": + if msg.type == 'at': + if msg.data['qq'] == 'all': yiri_msg_list.append(platform_message.AtAll()) else: yiri_msg_list.append( platform_message.At( - target=msg.data["qq"], + target=msg.data['qq'], ) ) - elif msg.type == "text": - yiri_msg_list.append(platform_message.Plain(text=msg.data["text"])) - elif msg.type == "image": - image_base64, image_format = await image.qq_image_url_to_base64(msg.data['url']) - yiri_msg_list.append(platform_message.Image(base64=f"data:image/{image_format};base64,{image_base64}")) + elif msg.type == 'text': + yiri_msg_list.append(platform_message.Plain(text=msg.data['text'])) + elif msg.type == 'image': + image_base64, image_format = await image.qq_image_url_to_base64( + msg.data['url'] + ) + yiri_msg_list.append( + platform_message.Image( + base64=f'data:image/{image_format};base64,{image_base64}' + ) + ) chain = platform_message.MessageChain(yiri_msg_list) @@ -99,7 +109,6 @@ class AiocqhttpMessageConverter(adapter.MessageConverter): class AiocqhttpEventConverter(adapter.EventConverter): - @staticmethod async def yiri2target(event: platform_events.MessageEvent, bot_account_id: int): return event.source_platform_object @@ -110,49 +119,50 @@ class AiocqhttpEventConverter(adapter.EventConverter): event.message, event.message_id ) - if event.message_type == "group": - permission = "MEMBER" + if event.message_type == 'group': + permission = 'MEMBER' - if "role" in event.sender: - if event.sender["role"] == "admin": - permission = "ADMINISTRATOR" - elif event.sender["role"] == "owner": - permission = "OWNER" + if 'role' in event.sender: + if event.sender['role'] == 'admin': + permission = 'ADMINISTRATOR' + elif event.sender['role'] == 'owner': + permission = 'OWNER' converted_event = platform_events.GroupMessage( sender=platform_entities.GroupMember( - id=event.sender["user_id"], # message_seq 放哪? - member_name=event.sender["nickname"], + id=event.sender['user_id'], # message_seq 放哪? + member_name=event.sender['nickname'], permission=permission, group=platform_entities.Group( id=event.group_id, - name=event.sender["nickname"], + name=event.sender['nickname'], permission=platform_entities.Permission.Member, ), - special_title=event.sender["title"] if "title" in event.sender else "", + special_title=event.sender['title'] + if 'title' in event.sender + else '', join_timestamp=0, last_speak_timestamp=0, mute_time_remaining=0, ), message_chain=yiri_chain, time=event.time, - source_platform_object=event + source_platform_object=event, ) return converted_event - elif event.message_type == "private": + elif event.message_type == 'private': return platform_events.FriendMessage( sender=platform_entities.Friend( - id=event.sender["user_id"], - nickname=event.sender["nickname"], - remark="", + id=event.sender['user_id'], + nickname=event.sender['nickname'], + remark='', ), message_chain=yiri_chain, time=event.time, - source_platform_object=event + source_platform_object=event, ) class AiocqhttpAdapter(adapter.MessagePlatformAdapter): - bot: aiocqhttp.CQHttp bot_account_id: int @@ -170,14 +180,14 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter): async def shutdown_trigger_placeholder(): while True: await asyncio.sleep(1) - + self.config['shutdown_trigger'] = shutdown_trigger_placeholder self.ap = ap - if "access-token" in config: - self.bot = aiocqhttp.CQHttp(access_token=config["access-token"]) - del self.config["access-token"] + if 'access-token' in config: + self.bot = aiocqhttp.CQHttp(access_token=config['access-token']) + del self.config['access-token'] else: self.bot = aiocqhttp.CQHttp() @@ -186,9 +196,9 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter): ): aiocq_msg = (await AiocqhttpMessageConverter.yiri2target(message))[0] - if target_type == "group": + if target_type == 'group': await self.bot.send_group_msg(group_id=int(target_id), message=aiocq_msg) - elif target_type == "person": + elif target_type == 'person': await self.bot.send_private_msg(user_id=int(target_id), message=aiocq_msg) async def reply_message( @@ -196,16 +206,17 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter): message_source: platform_events.MessageEvent, message: platform_message.MessageChain, quote_origin: bool = False, - ): - aiocq_event = await AiocqhttpEventConverter.yiri2target(message_source, self.bot_account_id) + ): + aiocq_event = await AiocqhttpEventConverter.yiri2target( + message_source, self.bot_account_id + ) aiocq_msg = (await AiocqhttpMessageConverter.yiri2target(message))[0] if quote_origin: - aiocq_msg = aiocqhttp.MessageSegment.reply(aiocq_event.message_id) + aiocq_msg + aiocq_msg = ( + aiocqhttp.MessageSegment.reply(aiocq_event.message_id) + aiocq_msg + ) - return await self.bot.send( - aiocq_event, - aiocq_msg - ) + return await self.bot.send(aiocq_event, aiocq_msg) async def is_muted(self, group_id: int) -> bool: return False @@ -213,24 +224,30 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, adapter.MessagePlatformAdapter], None + ], ): async def on_message(event: aiocqhttp.Event): self.bot_account_id = event.self_id try: - return await callback(await self.event_converter.target2yiri(event), self) - except: + return await callback( + await self.event_converter.target2yiri(event), self + ) + except Exception: traceback.print_exc() if event_type == platform_events.GroupMessage: - self.bot.on_message("group")(on_message) + self.bot.on_message('group')(on_message) elif event_type == platform_events.FriendMessage: - self.bot.on_message("private")(on_message) + self.bot.on_message('private')(on_message) def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, adapter.MessagePlatformAdapter], None + ], ): return super().unregister_listener(event_type, callback) diff --git a/pkg/platform/sources/aiocqhttp.yaml b/pkg/platform/sources/aiocqhttp.yaml index b5da6a8c..c915e464 100644 --- a/pkg/platform/sources/aiocqhttp.yaml +++ b/pkg/platform/sources/aiocqhttp.yaml @@ -3,8 +3,8 @@ kind: MessagePlatformAdapter metadata: name: aiocqhttp label: - en_US: OneBot v11 Adapter - zh_CN: OneBot v11 适配器 + en_US: OneBot v11 + zh_CN: OneBot v11 description: en_US: OneBot v11 Adapter zh_CN: OneBot v11 适配器,请查看文档了解使用方式 diff --git a/pkg/platform/sources/dingtalk.py b/pkg/platform/sources/dingtalk.py index aa768039..52f4a832 100644 --- a/pkg/platform/sources/dingtalk.py +++ b/pkg/platform/sources/dingtalk.py @@ -1,37 +1,30 @@ - import traceback import typing from libs.dingtalk_api.dingtalkevent import DingTalkEvent from pkg.platform.types import message as platform_message from pkg.platform.adapter import MessagePlatformAdapter -from pkg.platform.types import events as platform_events, message as platform_message -from pkg.core import app 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 ...command.errors import ParamNotEnoughError from libs.dingtalk_api.api import DingTalkClient import datetime class DingTalkMessageConverter(adapter.MessageConverter): - @staticmethod - async def yiri2target( - message_chain:platform_message.MessageChain - ): + async def yiri2target(message_chain: platform_message.MessageChain): for msg in message_chain: if type(msg) is platform_message.Plain: return msg.text @staticmethod - async def target2yiri(event:DingTalkEvent, bot_name:str): + async def target2yiri(event: DingTalkEvent, bot_name: str): yiri_msg_list = [] yiri_msg_list.append( - platform_message.Source(id = event.incoming_message.message_id,time=datetime.datetime.now()) + platform_message.Source( + id=event.incoming_message.message_id, time=datetime.datetime.now() + ) ) for atUser in event.incoming_message.at_users: @@ -39,7 +32,7 @@ class DingTalkMessageConverter(adapter.MessageConverter): yiri_msg_list.append(platform_message.At(target=bot_name)) if event.content: - text_content = event.content.replace("@"+bot_name, '') + text_content = event.content.replace('@' + bot_name, '') yiri_msg_list.append(platform_message.Plain(text=text_content)) if event.picture: yiri_msg_list.append(platform_message.Image(base64=event.picture)) @@ -47,60 +40,51 @@ class DingTalkMessageConverter(adapter.MessageConverter): yiri_msg_list.append(platform_message.Voice(base64=event.audio)) chain = platform_message.MessageChain(yiri_msg_list) - + return chain class DingTalkEventConverter(adapter.EventConverter): - @staticmethod - async def yiri2target( - event:platform_events.MessageEvent - ): + async def yiri2target(event: platform_events.MessageEvent): return event.source_platform_object @staticmethod - async def target2yiri( - event:DingTalkEvent, - bot_name:str - ): - + async def target2yiri(event: DingTalkEvent, bot_name: str): message_chain = await DingTalkMessageConverter.target2yiri(event, bot_name) - if event.conversation == 'FriendMessage': - return platform_events.FriendMessage( sender=platform_entities.Friend( id=event.incoming_message.sender_id, - nickname = event.incoming_message.sender_nick, - remark="" + nickname=event.incoming_message.sender_nick, + remark='', ), - message_chain = message_chain, - time = event.incoming_message.create_at, + message_chain=message_chain, + time=event.incoming_message.create_at, source_platform_object=event, ) elif event.conversation == 'GroupMessage': sender = platform_entities.GroupMember( - id = event.incoming_message.sender_id, + id=event.incoming_message.sender_id, member_name=event.incoming_message.sender_nick, - permission= 'MEMBER', - group = platform_entities.Group( - id = event.incoming_message.conversation_id, - name = event.incoming_message.conversation_title, - permission=platform_entities.Permission.Member + permission='MEMBER', + group=platform_entities.Group( + id=event.incoming_message.conversation_id, + name=event.incoming_message.conversation_title, + permission=platform_entities.Permission.Member, ), special_title='', join_timestamp=0, last_speak_timestamp=0, - mute_time_remaining=0 + mute_time_remaining=0, ) time = event.incoming_message.create_at return platform_events.GroupMessage( - sender =sender, - message_chain = message_chain, - time = time, - source_platform_object=event + sender=sender, + message_chain=message_chain, + time=time, + source_platform_object=event, ) @@ -112,28 +96,28 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): event_converter: DingTalkEventConverter = DingTalkEventConverter() config: dict - def __init__(self,config:dict,ap:app.Application): + def __init__(self, config: dict, ap: app.Application): self.config = config self.ap = ap required_keys = [ - "client_id", - "client_secret", - "robot_name", - "robot_code", + 'client_id', + 'client_secret', + 'robot_name', + 'robot_code', ] missing_keys = [key for key in required_keys if key not in config] if missing_keys: - raise ParamNotEnoughError("钉钉缺少相关配置项,请查看文档或联系管理员") + raise Exception('钉钉缺少相关配置项,请查看文档或联系管理员') + + self.bot_account_id = self.config['robot_name'] - self.bot_account_id = self.config["robot_name"] - self.bot = DingTalkClient( - client_id=config["client_id"], - client_secret=config["client_secret"], - robot_name = config["robot_name"], - robot_code=config["robot_code"] + client_id=config['client_id'], + client_secret=config['client_secret'], + robot_name=config['robot_name'], + robot_code=config['robot_code'], ) - + async def reply_message( self, message_source: platform_events.MessageEvent, @@ -146,17 +130,16 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): incoming_message = event.incoming_message content = await DingTalkMessageConverter.yiri2target(message) - await self.bot.send_message(content,incoming_message) - + await self.bot.send_message(content, incoming_message) async def send_message( self, target_type: str, target_id: str, message: platform_message.MessageChain ): content = await DingTalkMessageConverter.yiri2target(message) if target_type == 'person': - await self.bot.send_proactive_message_to_one(target_id,content) + await self.bot.send_proactive_message_to_one(target_id, content) if target_type == 'group': - await self.bot.send_proactive_message_to_group(target_id,content) + await self.bot.send_proactive_message_to_group(target_id, content) def register_listener( self, @@ -168,15 +151,18 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): async def on_message(event: DingTalkEvent): try: return await callback( - await self.event_converter.target2yiri(event, self.config["robot_name"]), self + await self.event_converter.target2yiri( + event, self.config['robot_name'] + ), + self, ) - except: + except Exception: traceback.print_exc() if event_type == platform_events.FriendMessage: - self.bot.on_message("FriendMessage")(on_message) + self.bot.on_message('FriendMessage')(on_message) elif event_type == platform_events.GroupMessage: - self.bot.on_message("GroupMessage")(on_message) + self.bot.on_message('GroupMessage')(on_message) async def run_async(self): await self.bot.start() @@ -187,7 +173,8 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): async def unregister_listener( self, event_type: type, - callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, MessagePlatformAdapter], None + ], ): return super().unregister_listener(event_type, callback) - diff --git a/pkg/platform/sources/discord.py b/pkg/platform/sources/discord.py index 961b031a..07dd586f 100644 --- a/pkg/platform/sources/discord.py +++ b/pkg/platform/sources/discord.py @@ -3,39 +3,32 @@ 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 + 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 = "" + text_string = '' image_files = [] for ele in message_chain: @@ -49,46 +42,49 @@ class DiscordMessageConverter(adapter.MessageConverter): async with session.get(ele.url) as response: image_bytes = await response.read() elif ele.path: - with open(ele.path, "rb") as f: + with open(ele.path, 'rb') as f: image_bytes = f.read() - image_files.append(discord.File(fp=image_bytes, filename=f"{uuid.uuid4()}.png")) + image_files.append( + discord.File(fp=image_bytes, filename=f'{uuid.uuid4()}.png') + ) elif isinstance(ele, platform_message.Plain): text_string += ele.text elif isinstance(ele, platform_message.Forward): for node in ele.node_list: - text_string, image_files = await DiscordMessageConverter.yiri2target(node.message_chain) + ( + text_string, + image_files, + ) = await DiscordMessageConverter.yiri2target(node.message_chain) text_string += text_string image_files.extend(image_files) return text_string, image_files @staticmethod - async def target2yiri( - message: discord.Message - ) -> platform_message.MessageChain: + 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) - ) + 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 == "": + 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_pattern = re.compile(r'(@everyone|@here|<@[\d]+>)') at_matches = at_pattern.findall(text_ele) - + if len(at_matches) > 0: mid_at = at_matches[0] @@ -96,18 +92,19 @@ class DiscordMessageConverter(adapter.MessageConverter): mid_at_component = [] - if mid_at == "@everyone" or mid_at == "@here": + 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]) + 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 @@ -115,28 +112,27 @@ class DiscordMessageConverter(adapter.MessageConverter): 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}")) + 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: + async def yiri2target(event: platform_events.Event) -> discord.Message: pass @staticmethod - async def target2yiri( - event: discord.Message - ) -> platform_events.Event: + async def target2yiri(event: discord.Message) -> platform_events.Event: message_chain = await DiscordMessageConverter.target2yiri(event) - if type(event.channel) == discord.DMChannel: + if isinstance(event.channel, discord.DMChannel): return platform_events.FriendMessage( sender=platform_entities.Friend( id=event.author.id, @@ -147,7 +143,7 @@ class DiscordEventConverter(adapter.EventConverter): time=event.created_at.timestamp(), source_platform_object=event, ) - elif type(event.channel) == discord.TextChannel: + elif isinstance(event.channel, discord.TextChannel): return platform_events.GroupMessage( sender=platform_entities.GroupMember( id=event.author.id, @@ -158,7 +154,7 @@ class DiscordEventConverter(adapter.EventConverter): name=event.channel.name, permission=platform_entities.Permission.Member, ), - special_title="", + special_title='', join_timestamp=0, last_speak_timestamp=0, mute_time_remaining=0, @@ -170,7 +166,6 @@ class DiscordEventConverter(adapter.EventConverter): class DiscordAdapter(adapter.MessagePlatformAdapter): - bot: discord.Client bot_account_id: str # 用于在流水线中识别at是否是本bot,直接以bot_name作为标识 @@ -191,12 +186,11 @@ class DiscordAdapter(adapter.MessagePlatformAdapter): self.config = config self.ap = ap - self.bot_account_id = self.config["client_id"] + 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 @@ -209,11 +203,11 @@ class DiscordAdapter(adapter.MessagePlatformAdapter): args = {} - if os.getenv("http_proxy"): - args["proxy"] = os.getenv("http_proxy") + 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 ): @@ -229,17 +223,17 @@ class DiscordAdapter(adapter.MessagePlatformAdapter): assert isinstance(message_source.source_platform_object, discord.Message) args = { - "content": msg_to_send, + 'content': msg_to_send, } if len(image_files) > 0: - args["files"] = image_files + args['files'] = image_files if quote_origin: - args["reference"] = message_source.source_platform_object + args['reference'] = message_source.source_platform_object if message.has(platform_message.At): - args["mention_author"] = True + args['mention_author'] = True await message_source.source_platform_object.channel.send(**args) @@ -249,20 +243,24 @@ class DiscordAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, adapter.MessagePlatformAdapter], None + ], ): self.listeners[event_type] = callback def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, adapter.MessagePlatformAdapter], None + ], ): self.listeners.pop(event_type) async def run_async(self): async with self.bot: - await self.bot.start(self.config["token"], reconnect=True) + await self.bot.start(self.config['token'], reconnect=True) async def kill(self) -> bool: await self.bot.close() diff --git a/pkg/platform/sources/gewechat.py b/pkg/platform/sources/gewechat.py index 869e05c6..2c9e5733 100644 --- a/pkg/platform/sources/gewechat.py +++ b/pkg/platform/sources/gewechat.py @@ -8,18 +8,13 @@ import traceback import time import re import base64 -import uuid -import json -import os import copy -import datetime import threading import quart 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 @@ -29,109 +24,123 @@ import xml.etree.ElementTree as ET class GewechatMessageConverter(adapter.MessageConverter): - def __init__(self, config: dict): self.config = config @staticmethod - async def yiri2target( - message_chain: platform_message.MessageChain - ) -> list[dict]: + async def yiri2target(message_chain: platform_message.MessageChain) -> list[dict]: content_list = [] for component in message_chain: if 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}) + content_list.append({'type': 'text', 'content': component.text}) elif isinstance(component, platform_message.Image): if not component.url: pass - content_list.append({"type": "image", "image": component.url}) - + content_list.append({'type': 'image', 'image': component.url}) elif isinstance(component, platform_message.Voice): - content_list.append({"type": "voice", "url": component.url, "length": component.length}) + content_list.append( + {'type': 'voice', 'url': component.url, 'length': component.length} + ) elif isinstance(component, platform_message.Forward): for node in component.node_list: - content_list.extend(await GewechatMessageConverter.yiri2target(node.message_chain)) + content_list.extend( + await GewechatMessageConverter.yiri2target(node.message_chain) + ) return content_list async def target2yiri( - self, - message: dict, - bot_account_id: str + self, message: dict, bot_account_id: str ) -> platform_message.MessageChain: - - - - if message["Data"]["MsgType"] == 1: + if message['Data']['MsgType'] == 1: # 检查消息开头,如果有 wxid_sbitaz0mt65n22:\n 则删掉 - regex = re.compile(r"^wxid_.*:") + regex = re.compile(r'^wxid_.*:') # print(message) - line_split = message["Data"]["Content"]["string"].split("\n") + line_split = message['Data']['Content']['string'].split('\n') if len(line_split) > 0 and regex.match(line_split[0]): - message["Data"]["Content"]["string"] = "\n".join(line_split[1:]) - + message['Data']['Content']['string'] = '\n'.join(line_split[1:]) # 正则表达式模式,匹配'@'后跟任意数量的非空白字符 pattern = r'@\S+' - at_string = f"@{bot_account_id}" + at_string = f'@{bot_account_id}' content_list = [] - if at_string in message["Data"]["Content"]["string"]: + if at_string in message['Data']['Content']['string']: content_list.append(platform_message.At(target=bot_account_id)) - content_list.append(platform_message.Plain(message["Data"]["Content"]["string"].replace(at_string, '', 1))) + content_list.append( + platform_message.Plain( + message['Data']['Content']['string'].replace(at_string, '', 1) + ) + ) # 更优雅的替换改名后@机器人,仅仅限于单独AT的情况 - elif "PushContent" in message['Data'] and '在群聊中@了你' in message["Data"]["PushContent"]: - if '@所有人' in message["Data"]["Content"]["string"]: # at全员时候传入atll不当作at自己 + elif ( + 'PushContent' in message['Data'] + and '在群聊中@了你' in message['Data']['PushContent'] + ): + if ( + '@所有人' in message['Data']['Content']['string'] + ): # at全员时候传入atll不当作at自己 content_list.append(platform_message.AtAll()) else: content_list.append(platform_message.At(target=bot_account_id)) - content_list.append(platform_message.Plain(re.sub(pattern, '', message["Data"]["Content"]["string"]))) + content_list.append( + platform_message.Plain( + re.sub(pattern, '', message['Data']['Content']['string']) + ) + ) else: - content_list = [platform_message.Plain(message["Data"]["Content"]["string"])] + content_list = [ + platform_message.Plain(message['Data']['Content']['string']) + ] return platform_message.MessageChain(content_list) - - elif message["Data"]["MsgType"] == 3: - image_xml = message["Data"]["Content"]["string"] - if not image_xml: - return platform_message.MessageChain([ - platform_message.Plain(text="[图片内容为空]") - ]) + elif message['Data']['MsgType'] == 3: + image_xml = message['Data']['Content']['string'] + if not image_xml: + return platform_message.MessageChain( + [platform_message.Plain(text='[图片内容为空]')] + ) try: base64_str, image_format = await image.get_gewechat_image_base64( - gewechat_url=self.config["gewechat_url"], - gewechat_file_url=self.config["gewechat_file_url"], - app_id=self.config["app_id"], + gewechat_url=self.config['gewechat_url'], + gewechat_file_url=self.config['gewechat_file_url'], + app_id=self.config['app_id'], xml_content=image_xml, - token=self.config["token"], + token=self.config['token'], image_type=2, ) - return platform_message.MessageChain([ - platform_message.Image( - base64=f"data:image/{image_format};base64,{base64_str}" - ) - ]) + return platform_message.MessageChain( + [ + platform_message.Image( + base64=f'data:image/{image_format};base64,{base64_str}' + ) + ] + ) except Exception as e: - print(f"处理图片消息失败: {str(e)}") - return platform_message.MessageChain([ - platform_message.Plain(text=f"[图片处理失败]") - ]) - elif message["Data"]["MsgType"] == 34: - audio_base64 = message["Data"]["ImgBuf"]["buffer"] + print(f'处理图片消息失败: {str(e)}') + return platform_message.MessageChain( + [platform_message.Plain(text='[图片处理失败]')] + ) + elif message['Data']['MsgType'] == 34: + audio_base64 = message['Data']['ImgBuf']['buffer'] return platform_message.MessageChain( - [platform_message.Voice(base64=f"data:audio/silk;base64,{audio_base64}")] + [ + platform_message.Voice( + base64=f'data:audio/silk;base64,{audio_base64}' + ) + ] ) - elif message["Data"]["MsgType"] == 49: + elif message['Data']['MsgType'] == 49: # 支持微信聊天记录的消息类型,将 XML 内容转换为 MessageChain 传递 try: - content = message["Data"]["Content"]["string"] + content = message['Data']['Content']['string'] # 有三种可能的消息结构weid开头,私聊直接和直接 if content.startswith('wxid'): xml_list = content.split('\n')[2:] @@ -145,140 +154,145 @@ class GewechatMessageConverter(adapter.MessageConverter): content_data = ET.fromstring(xml_data) # print(xml_data) # 拿到细分消息类型,按照gewe接口中描述 - ''' + """ 小程序:33/36 引用消息:57 转账消息:2000 红包消息:2001 视频号消息:51 - ''' + """ appmsg_data = content_data.find('.//appmsg') data_type = appmsg_data.find('.//type').text if data_type == '57': user_data = appmsg_data.find('.//title').text # 拿到用户消息 - quote_data = appmsg_data.find('.//refermsg').find('.//content').text # 引用原文 - sender_id = appmsg_data.find('.//refermsg').find('.//chatusr').text # 引用用户id + quote_data = ( + appmsg_data.find('.//refermsg').find('.//content').text + ) # 引用原文 + sender_id = ( + appmsg_data.find('.//refermsg').find('.//chatusr').text + ) # 引用用户id from_name = message['Data']['FromUserName']['string'] - message_list =[] - if message['Wxid'] == sender_id and from_name.endswith('@chatroom'): # 因为引用机制暂时无法响应用户,所以当引用用户是机器人是构建一个at激活机器人 + message_list = [] + if ( + message['Wxid'] == sender_id and from_name.endswith('@chatroom') + ): # 因为引用机制暂时无法响应用户,所以当引用用户是机器人是构建一个at激活机器人 message_list.append(platform_message.At(target=bot_account_id)) - message_list.append(platform_message.Quote( + message_list.append( + platform_message.Quote( sender_id=sender_id, origin=platform_message.MessageChain( [platform_message.Plain(quote_data)] - ))) + ), + ) + ) message_list.append(platform_message.Plain(user_data)) return platform_message.MessageChain(message_list) elif data_type == '51': return platform_message.MessageChain( - [platform_message.Plain(text=f'[视频号消息]')] + [platform_message.Plain(text='[视频号消息]')] ) # print(content_data) elif data_type == '2000': return platform_message.MessageChain( - [platform_message.Plain(text=f'[转账消息]')] + [platform_message.Plain(text='[转账消息]')] ) elif data_type == '2001': return platform_message.MessageChain( - [platform_message.Plain(text=f'[红包消息]')] + [platform_message.Plain(text='[红包消息]')] ) elif data_type == '5': return platform_message.MessageChain( - [platform_message.Plain(text=f'[公众号消息]')] + [platform_message.Plain(text='[公众号消息]')] ) elif data_type == '33' or data_type == '36': return platform_message.MessageChain( - [platform_message.Plain(text=f'[小程序消息]')] + [platform_message.Plain(text='[小程序消息]')] ) # print(data_type.text) else: - - try: content_bytes = content.encode('utf-8') decoded_content = base64.b64decode(content_bytes) return platform_message.MessageChain( [platform_message.Unknown(content=decoded_content)] ) - except Exception as e: + except Exception: return platform_message.MessageChain( [platform_message.Plain(text=content)] ) except Exception as e: - print(f"Error processing type 49 message: {str(e)}") + print(f'Error processing type 49 message: {str(e)}') return platform_message.MessageChain( - [platform_message.Plain(text="[无法解析的消息]")] + [platform_message.Plain(text='[无法解析的消息]')] ) -class GewechatEventConverter(adapter.EventConverter): +class GewechatEventConverter(adapter.EventConverter): def __init__(self, config: dict): self.config = config self.message_converter = GewechatMessageConverter(config) @staticmethod - async def yiri2target( - event: platform_events.MessageEvent - ) -> dict: + 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: # print(event) # 排除自己发消息回调回答问题 if event['Wxid'] == event['Data']['FromUserName']['string']: return None # 排除公众号以及微信团队消息 - if event['Data']['FromUserName']['string'].startswith('gh_')\ - or event['Data']['FromUserName']['string'].startswith('weixin'): + if event['Data']['FromUserName']['string'].startswith('gh_') or event['Data'][ + 'FromUserName' + ]['string'].startswith('weixin'): return None - message_chain = await self.message_converter.target2yiri(copy.deepcopy(event), bot_account_id) + message_chain = await self.message_converter.target2yiri( + copy.deepcopy(event), bot_account_id + ) if not message_chain: return None - - if '@chatroom' in event["Data"]["FromUserName"]["string"]: + + if '@chatroom' in event['Data']['FromUserName']['string']: # 找出开头的 wxid_ 字符串,以:结尾 - sender_wxid = event["Data"]["Content"]["string"].split(":")[0] + sender_wxid = event['Data']['Content']['string'].split(':')[0] return platform_events.GroupMessage( sender=platform_entities.GroupMember( id=sender_wxid, - member_name=event["Data"]["FromUserName"]["string"], + member_name=event['Data']['FromUserName']['string'], permission=platform_entities.Permission.Member, group=platform_entities.Group( - id=event["Data"]["FromUserName"]["string"], - name=event["Data"]["FromUserName"]["string"], + id=event['Data']['FromUserName']['string'], + name=event['Data']['FromUserName']['string'], permission=platform_entities.Permission.Member, ), - special_title="", + special_title='', join_timestamp=0, last_speak_timestamp=0, mute_time_remaining=0, ), message_chain=message_chain, - time=event["Data"]["CreateTime"], + time=event['Data']['CreateTime'], source_platform_object=event, ) else: return platform_events.FriendMessage( sender=platform_entities.Friend( - id=event["Data"]["FromUserName"]["string"], - nickname=event["Data"]["FromUserName"]["string"], + id=event['Data']['FromUserName']['string'], + nickname=event['Data']['FromUserName']['string'], remark='', ), message_chain=message_chain, - time=event["Data"]["CreateTime"], + time=event['Data']['CreateTime'], source_platform_object=event, ) class GeWeChatAdapter(adapter.MessagePlatformAdapter): - - name: str = "gewechat" # 定义适配器名称 + name: str = 'gewechat' # 定义适配器名称 bot: gewechat_client.GewechatClient quart_app: quart.Quart @@ -296,7 +310,7 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): typing.Type[platform_events.Event], typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], ] = {} - + def __init__(self, config: dict, ap: app.Application): self.config = config self.ap = ap @@ -310,21 +324,21 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): async def gewechat_callback(): data = await quart.request.json # print(json.dumps(data, indent=4, ensure_ascii=False)) - + if 'data' in data: data['Data'] = data['data'] if 'type_name' in data: data['TypeName'] = data['type_name'] # print(json.dumps(data, indent=4, ensure_ascii=False)) - if 'testMsg' in data: return 'ok' elif 'TypeName' in data and data['TypeName'] == 'AddMsg': try: - - event = await self.event_converter.target2yiri(data.copy(), self.bot_account_id) - except Exception as e: + event = await self.event_converter.target2yiri( + data.copy(), self.bot_account_id + ) + except Exception: traceback.print_exc() if event.__class__ in self.listeners: @@ -333,65 +347,67 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): return 'ok' async def send_message( - self, - target_type: str, - target_id: str, - message: platform_message.MessageChain + self, target_type: str, target_id: str, message: platform_message.MessageChain ): geweap_msg = await self.message_converter.yiri2target(message) # 此处加上群消息at处理 - ats = [item["target"] for item in geweap_msg if item["type"] == "at"] - + ats = [item['target'] for item in geweap_msg if item['type'] == 'at'] for msg in geweap_msg: # at主动发送消息 if msg['type'] == 'text': if ats: member_info = self.bot.get_chatroom_member_detail( - self.config["app_id"], - target_id, - ats[::-1] - )["data"] + self.config['app_id'], target_id, ats[::-1] + )['data'] for member in member_info: msg['content'] = f'@{member["nickName"]} {msg["content"]}' - self.bot.post_text(app_id=self.config['app_id'], to_wxid=target_id, content=msg['content'], - ats=",".join(ats)) + self.bot.post_text( + app_id=self.config['app_id'], + to_wxid=target_id, + content=msg['content'], + ats=','.join(ats), + ) elif msg['type'] == 'image': - - self.bot.post_image(app_id=self.config['app_id'], to_wxid=target_id, img_url=msg["image"]) - - + self.bot.post_image( + app_id=self.config['app_id'], + to_wxid=target_id, + img_url=msg['image'], + ) async def reply_message( self, message_source: platform_events.MessageEvent, message: platform_message.MessageChain, - quote_origin: bool = False + quote_origin: bool = False, ): content_list = await self.message_converter.yiri2target(message) - ats = [item["target"] for item in content_list if item["type"] == "at"] + ats = [item['target'] for item in content_list if item['type'] == 'at'] for msg in content_list: - if msg["type"] == "text": - + if msg['type'] == 'text': if ats: member_info = self.bot.get_chatroom_member_detail( - self.config["app_id"], - message_source.source_platform_object["Data"]["FromUserName"]["string"], - ats[::-1] - )["data"] + self.config['app_id'], + message_source.source_platform_object['Data']['FromUserName'][ + 'string' + ], + ats[::-1], + )['data'] for member in member_info: msg['content'] = f'@{member["nickName"]} {msg["content"]}' self.bot.post_text( - app_id=self.config["app_id"], - to_wxid=message_source.source_platform_object["Data"]["FromUserName"]["string"], - content=msg["content"], - ats=",".join(ats) + app_id=self.config['app_id'], + to_wxid=message_source.source_platform_object['Data'][ + 'FromUserName' + ]['string'], + content=msg['content'], + ats=','.join(ats), ) async def is_muted(self, group_id: int) -> bool: @@ -400,51 +416,57 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None] + callback: typing.Callable[ + [platform_events.Event, adapter.MessagePlatformAdapter], None + ], ): self.listeners[event_type] = callback def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None] + callback: typing.Callable[ + [platform_events.Event, adapter.MessagePlatformAdapter], None + ], ): pass async def run_async(self): - - if not self.config["token"]: + if not self.config['token']: async with aiohttp.ClientSession() as session: async with session.post( - f"{self.config['gewechat_url']}/v2/api/tools/getTokenId", - json={"app_id": self.config["app_id"]} + f'{self.config["gewechat_url"]}/v2/api/tools/getTokenId', + json={'app_id': self.config['app_id']}, ) as response: if response.status != 200: - raise Exception(f"获取gewechat token失败: {await response.text()}") - self.config["token"] = (await response.json())["data"] + raise Exception( + f'获取gewechat token失败: {await response.text()}' + ) + self.config['token'] = (await response.json())['data'] self.bot = gewechat_client.GewechatClient( - f"{self.config['gewechat_url']}/v2/api", - self.config["token"] + f'{self.config["gewechat_url"]}/v2/api', self.config['token'] ) - app_id, error_msg = self.bot.login(self.config["app_id"]) + app_id, error_msg = self.bot.login(self.config['app_id']) if error_msg: - raise Exception(f"Gewechat 登录失败: {error_msg}") + raise Exception(f'Gewechat 登录失败: {error_msg}') - self.config["app_id"] = app_id + self.config['app_id'] = app_id - self.ap.logger.info(f"Gewechat 登录成功,app_id: {app_id}") + self.ap.logger.info(f'Gewechat 登录成功,app_id: {app_id}') await self.ap.platform_mgr.write_back_config('gewechat', self, self.config) # 获取 nickname - profile = self.bot.get_profile(self.config["app_id"]) - self.bot_account_id = profile["data"]["nickName"] + profile = self.bot.get_profile(self.config['app_id']) + self.bot_account_id = profile['data']['nickName'] def thread_set_callback(): time.sleep(3) - ret = self.bot.set_callback(self.config["token"], self.config["callback_url"]) + ret = self.bot.set_callback( + self.config['token'], self.config['callback_url'] + ) print('设置 Gewechat 回调:', ret) threading.Thread(target=thread_set_callback).start() @@ -455,7 +477,7 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): await self.quart_app.run_task( host='0.0.0.0', - port=self.config["port"], + port=self.config['port'], shutdown_trigger=shutdown_trigger_placeholder, ) diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index 4c87640b..1396ba3b 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -5,56 +5,53 @@ import lark_oapi import typing import asyncio import traceback -import time import re import base64 import uuid import json import datetime import hashlib -import base64 from Crypto.Cipher import AES import aiohttp import lark_oapi.ws.exception import quart -from flask import jsonify from lark_oapi.api.im.v1 import * -from lark_oapi.api.verification.v1 import GetVerificationRequest 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 AESCipher(object): +class AESCipher(object): def __init__(self, key): self.bs = AES.block_size - self.key=hashlib.sha256(AESCipher.str_to_bytes(key)).digest() + self.key = hashlib.sha256(AESCipher.str_to_bytes(key)).digest() + @staticmethod def str_to_bytes(data): - u_type = type(b"".decode('utf8')) + u_type = type(b''.decode('utf8')) if isinstance(data, u_type): return data.encode('utf8') return data + @staticmethod def _unpad(s): - return s[:-ord(s[len(s) - 1:])] + return s[: -ord(s[len(s) - 1 :])] + def decrypt(self, enc): - iv = enc[:AES.block_size] + iv = enc[: AES.block_size] cipher = AES.new(self.key, AES.MODE_CBC, iv) - return self._unpad(cipher.decrypt(enc[AES.block_size:])) + return self._unpad(cipher.decrypt(enc[AES.block_size :])) + def decrypt_string(self, enc): enc = base64.b64decode(enc) - return self.decrypt(enc).decode('utf8') + return self.decrypt(enc).decode('utf8') class LarkMessageConverter(adapter.MessageConverter): - @staticmethod async def yiri2target( message_chain: platform_message.MessageChain, api_client: lark_oapi.Client @@ -65,15 +62,14 @@ class LarkMessageConverter(adapter.MessageConverter): for msg in message_chain: if isinstance(msg, platform_message.Plain): - pending_paragraph.append({"tag": "md", "text": msg.text}) + pending_paragraph.append({'tag': 'md', 'text': msg.text}) elif isinstance(msg, platform_message.At): pending_paragraph.append( - {"tag": "at", "user_id": msg.target, "style": []} + {'tag': 'at', 'user_id': msg.target, 'style': []} ) elif isinstance(msg, platform_message.AtAll): - pending_paragraph.append({"tag": "at", "user_id": "all", "style": []}) + pending_paragraph.append({'tag': 'at', 'user_id': 'all', 'style': []}) elif isinstance(msg, platform_message.Image): - image_bytes = None if msg.base64: @@ -83,14 +79,14 @@ class LarkMessageConverter(adapter.MessageConverter): async with session.get(msg.url) as response: image_bytes = await response.read() elif msg.path: - with open(msg.path, "rb") as f: + with open(msg.path, 'rb') as f: image_bytes = f.read() request: CreateImageRequest = ( CreateImageRequest.builder() .request_body( CreateImageRequestBody.builder() - .image_type("message") + .image_type('message') .image(image_bytes) .build() ) @@ -103,7 +99,7 @@ class LarkMessageConverter(adapter.MessageConverter): if not response.success(): raise Exception( - f"client.im.v1.image.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}" + f'client.im.v1.image.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}' ) image_key = response.data.image_key @@ -112,15 +108,19 @@ class LarkMessageConverter(adapter.MessageConverter): message_elements.append( [ { - "tag": "img", - "image_key": image_key, + 'tag': 'img', + 'image_key': image_key, } ] ) pending_paragraph = [] elif isinstance(msg, platform_message.Forward): for node in msg.node_list: - message_elements.extend(await LarkMessageConverter.yiri2target(node.message_chain, api_client)) + message_elements.extend( + await LarkMessageConverter.yiri2target( + node.message_chain, api_client + ) + ) if pending_paragraph: message_elements.append(pending_paragraph) @@ -144,15 +144,15 @@ class LarkMessageConverter(adapter.MessageConverter): platform_message.Source(id=message.message_id, time=msg_create_time) ) - if message.message_type == "text": + if message.message_type == 'text': element_list = [] def text_element_recur(text_ele: dict) -> list[dict]: - if text_ele["text"] == "": + if text_ele['text'] == '': return [] - at_pattern = re.compile(r"@_user_[\d]+") - at_matches = at_pattern.findall(text_ele["text"]) + at_pattern = re.compile(r'@_user_[\d]+') + at_matches = at_pattern.findall(text_ele['text']) name_mapping = {} for mathc in at_matches: @@ -165,7 +165,7 @@ class LarkMessageConverter(adapter.MessageConverter): return [text_ele] # 只处理第一个,剩下的递归处理 - text_split = text_ele["text"].split(list(name_mapping.keys())[0]) + text_split = text_ele['text'].split(list(name_mapping.keys())[0]) new_list = [] @@ -173,58 +173,58 @@ class LarkMessageConverter(adapter.MessageConverter): right_text = text_split[1] new_list.extend( - text_element_recur({"tag": "text", "text": left_text, "style": []}) + text_element_recur({'tag': 'text', 'text': left_text, 'style': []}) ) new_list.append( { - "tag": "at", - "user_id": list(name_mapping.keys())[0], - "user_name": name_mapping[list(name_mapping.keys())[0]], - "style": [], + 'tag': 'at', + 'user_id': list(name_mapping.keys())[0], + 'user_name': name_mapping[list(name_mapping.keys())[0]], + 'style': [], } ) new_list.extend( - text_element_recur({"tag": "text", "text": right_text, "style": []}) + text_element_recur({'tag': 'text', 'text': right_text, 'style': []}) ) return new_list element_list = text_element_recur( - {"tag": "text", "text": message_content["text"], "style": []} + {'tag': 'text', 'text': message_content['text'], 'style': []} ) - message_content = {"title": "", "content": element_list} + message_content = {'title': '', 'content': element_list} - elif message.message_type == "post": + elif message.message_type == 'post': new_list = [] - for ele in message_content["content"]: + for ele in message_content['content']: if type(ele) is dict: new_list.append(ele) elif type(ele) is list: new_list.extend(ele) - message_content["content"] = new_list - elif message.message_type == "image": - message_content["content"] = [ - {"tag": "img", "image_key": message_content["image_key"], "style": []} + message_content['content'] = new_list + elif message.message_type == 'image': + message_content['content'] = [ + {'tag': 'img', 'image_key': message_content['image_key'], 'style': []} ] - for ele in message_content["content"]: - if ele["tag"] == "text": - lb_msg_list.append(platform_message.Plain(text=ele["text"])) - elif ele["tag"] == "at": - lb_msg_list.append(platform_message.At(target=ele["user_name"])) - elif ele["tag"] == "img": - image_key = ele["image_key"] + for ele in message_content['content']: + if ele['tag'] == 'text': + lb_msg_list.append(platform_message.Plain(text=ele['text'])) + elif ele['tag'] == 'at': + lb_msg_list.append(platform_message.At(target=ele['user_name'])) + elif ele['tag'] == 'img': + image_key = ele['image_key'] request: GetMessageResourceRequest = ( GetMessageResourceRequest.builder() .message_id(message.message_id) .file_key(image_key) - .type("image") + .type('image') .build() ) @@ -234,17 +234,17 @@ class LarkMessageConverter(adapter.MessageConverter): if not response.success(): raise Exception( - f"client.im.v1.message_resource.get failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}" + f'client.im.v1.message_resource.get failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}' ) image_bytes = response.file.read() image_base64 = base64.b64encode(image_bytes).decode() - image_format = response.raw.headers["content-type"] + image_format = response.raw.headers['content-type'] lb_msg_list.append( platform_message.Image( - base64=f"data:{image_format};base64,{image_base64}" + base64=f'data:{image_format};base64,{image_base64}' ) ) @@ -252,7 +252,6 @@ class LarkMessageConverter(adapter.MessageConverter): class LarkEventConverter(adapter.EventConverter): - @staticmethod async def yiri2target( event: platform_events.MessageEvent, @@ -267,17 +266,17 @@ class LarkEventConverter(adapter.EventConverter): event.event.message, api_client ) - if event.event.message.chat_type == "p2p": + if event.event.message.chat_type == 'p2p': return platform_events.FriendMessage( sender=platform_entities.Friend( id=event.event.sender.sender_id.open_id, nickname=event.event.sender.sender_id.union_id, - remark="", + remark='', ), message_chain=message_chain, time=event.event.message.create_time, ) - elif event.event.message.chat_type == "group": + elif event.event.message.chat_type == 'group': return platform_events.GroupMessage( sender=platform_entities.GroupMember( id=event.event.sender.sender_id.open_id, @@ -285,10 +284,10 @@ class LarkEventConverter(adapter.EventConverter): permission=platform_entities.Permission.Member, group=platform_entities.Group( id=event.event.message.chat_id, - name="", + name='', permission=platform_entities.Permission.Member, ), - special_title="", + special_title='', join_timestamp=0, last_speak_timestamp=0, mute_time_remaining=0, @@ -299,7 +298,6 @@ class LarkEventConverter(adapter.EventConverter): class LarkAdapter(adapter.MessagePlatformAdapter): - bot: lark_oapi.ws.Client api_client: lark_oapi.Client @@ -333,17 +331,15 @@ class LarkAdapter(adapter.MessagePlatformAdapter): data = cipher.decrypt_string(data['encrypt']) data = json.loads(data) - type = data.get("type") - if type is None : + type = data.get('type') + if type is None: context = EventContext(data) type = context.header.event_type - + if 'url_verification' == type: - print(data.get("challenge")) + print(data.get('challenge')) # todo 验证verification token - return { - "challenge": data.get("challenge") - } + return {'challenge': data.get('challenge')} context = EventContext(data) type = context.header.event_type p2v1 = P2ImMessageReceiveV1() @@ -355,20 +351,21 @@ class LarkAdapter(adapter.MessagePlatformAdapter): p2v1.schema = context.schema if 'im.message.receive_v1' == type: try: - event = await self.event_converter.target2yiri(p2v1, self.api_client) - except Exception as e: + event = await self.event_converter.target2yiri( + p2v1, self.api_client + ) + except Exception: traceback.print_exc() if event.__class__ in self.listeners: await self.listeners[event.__class__](event, self) - return {"code": 200, "message": "ok"} - except Exception as e: + return {'code': 200, 'message': 'ok'} + except Exception: traceback.print_exc() - return {"code": 500, "message": "error"} + return {'code': 500, 'message': 'error'} async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1): - lb_event = await self.event_converter.target2yiri(event, self.api_client) await self.listeners[type(lb_event)](lb_event, self) @@ -377,20 +374,20 @@ class LarkAdapter(adapter.MessagePlatformAdapter): asyncio.create_task(on_message(event)) event_handler = ( - lark_oapi.EventDispatcherHandler.builder("", "") + lark_oapi.EventDispatcherHandler.builder('', '') .register_p2_im_message_receive_v1(sync_on_message) .build() ) - self.bot_account_id = config["bot_name"] + self.bot_account_id = config['bot_name'] self.bot = lark_oapi.ws.Client( - config["app_id"], config["app_secret"], event_handler=event_handler + config['app_id'], config['app_secret'], event_handler=event_handler ) self.api_client = ( lark_oapi.Client.builder() - .app_id(config["app_id"]) - .app_secret(config["app_secret"]) + .app_id(config['app_id']) + .app_secret(config['app_secret']) .build() ) @@ -405,7 +402,6 @@ class LarkAdapter(adapter.MessagePlatformAdapter): message: platform_message.MessageChain, quote_origin: bool = False, ): - # 不再需要了,因为message_id已经被包含到message_chain中 # lark_event = await self.event_converter.yiri2target(message_source) lark_message = await self.message_converter.yiri2target( @@ -413,9 +409,9 @@ class LarkAdapter(adapter.MessagePlatformAdapter): ) final_content = { - "zh_cn": { - "title": "", - "content": lark_message, + 'zh_cn': { + 'title': '', + 'content': lark_message, }, } @@ -425,7 +421,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): .request_body( ReplyMessageRequestBody.builder() .content(json.dumps(final_content)) - .msg_type("post") + .msg_type('post') .reply_in_thread(False) .uuid(str(uuid.uuid4())) .build() @@ -439,7 +435,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): if not response.success(): raise Exception( - f"client.im.v1.message.reply failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}" + f'client.im.v1.message.reply failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}' ) async def is_muted(self, group_id: int) -> bool: @@ -479,6 +475,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): else: raise e else: + async def shutdown_trigger_placeholder(): while True: await asyncio.sleep(1) @@ -488,5 +485,6 @@ class LarkAdapter(adapter.MessagePlatformAdapter): port=port, shutdown_trigger=shutdown_trigger_placeholder, ) + async def kill(self) -> bool: return False diff --git a/pkg/platform/sources/nakuru.py b/pkg/platform/sources/nakuru.py index 8dcf6e52..7038af1d 100644 --- a/pkg/platform/sources/nakuru.py +++ b/pkg/platform/sources/nakuru.py @@ -4,7 +4,6 @@ import asyncio import typing import traceback -import logging import nakuru @@ -19,6 +18,7 @@ from ...platform.types import events as platform_events class NakuruProjectMessageConverter(adapter_model.MessageConverter): """消息转换器""" + @staticmethod def yiri2target(message_chain: platform_message.MessageChain) -> list: msg_list = [] @@ -29,10 +29,12 @@ class NakuruProjectMessageConverter(adapter_model.MessageConverter): elif type(message_chain) is str: msg_list = [platform_message.Plain(message_chain)] else: - raise Exception("Unknown message type: " + str(message_chain) + str(type(message_chain))) - + raise Exception( + 'Unknown message type: ' + str(message_chain) + str(type(message_chain)) + ) + nakuru_msg_list = [] - + # 遍历并转换 for component in msg_list: if type(component) is platform_message.Plain: @@ -61,33 +63,43 @@ class NakuruProjectMessageConverter(adapter_model.MessageConverter): # 遍历并转换 for yiri_forward_node in yiri_forward_node_list: try: - content_list = NakuruProjectMessageConverter.yiri2target(yiri_forward_node.message_chain) + content_list = NakuruProjectMessageConverter.yiri2target( + yiri_forward_node.message_chain + ) nakuru_forward_node = nkc.Node( name=yiri_forward_node.sender_name, uin=yiri_forward_node.sender_id, - time=int(yiri_forward_node.time.timestamp()) if yiri_forward_node.time is not None else None, - content=content_list + time=int(yiri_forward_node.time.timestamp()) + if yiri_forward_node.time is not None + else None, + content=content_list, ) nakuru_forward_node_list.append(nakuru_forward_node) - except Exception as e: + except Exception: import traceback + traceback.print_exc() nakuru_msg_list.append(nakuru_forward_node_list) else: nakuru_msg_list.append(nkc.Plain(str(component))) - + return nakuru_msg_list @staticmethod - def target2yiri(message_chain: typing.Any, message_id: int = -1) -> platform_message.MessageChain: + def target2yiri( + message_chain: typing.Any, message_id: int = -1 + ) -> platform_message.MessageChain: """将Yiri的消息链转换为YiriMirai的消息链""" assert type(message_chain) is list yiri_msg_list = [] import datetime + # 添加Source组件以标记message_id等信息 - yiri_msg_list.append(platform_message.Source(id=message_id, time=datetime.datetime.now())) + yiri_msg_list.append( + platform_message.Source(id=message_id, time=datetime.datetime.now()) + ) for component in message_chain: if type(component) is nkc.Plain: yiri_msg_list.append(platform_message.Plain(text=component.text)) @@ -106,6 +118,7 @@ class NakuruProjectMessageConverter(adapter_model.MessageConverter): class NakuruProjectEventConverter(adapter_model.EventConverter): """事件转换器""" + @staticmethod def yiri2target(event: typing.Type[platform_events.Event]): if event is platform_events.GroupMessage: @@ -113,28 +126,30 @@ class NakuruProjectEventConverter(adapter_model.EventConverter): elif event is platform_events.FriendMessage: return nakuru.FriendMessage else: - raise Exception("未支持转换的事件类型: " + str(event)) + raise Exception('未支持转换的事件类型: ' + str(event)) @staticmethod def target2yiri(event: typing.Any) -> platform_events.Event: - yiri_chain = NakuruProjectMessageConverter.target2yiri(event.message, event.message_id) + yiri_chain = NakuruProjectMessageConverter.target2yiri( + event.message, event.message_id + ) if type(event) is nakuru.FriendMessage: # 私聊消息事件 return platform_events.FriendMessage( sender=platform_entities.Friend( id=event.sender.user_id, nickname=event.sender.nickname, - remark=event.sender.nickname + remark=event.sender.nickname, ), message_chain=yiri_chain, - time=event.time + time=event.time, ) elif type(event) is nakuru.GroupMessage: # 群聊消息事件 - permission = "MEMBER" + permission = 'MEMBER' - if event.sender.role == "admin": - permission = "ADMINISTRATOR" - elif event.sender.role == "owner": - permission = "OWNER" + if event.sender.role == 'admin': + permission = 'ADMINISTRATOR' + elif event.sender.role == 'owner': + permission = 'OWNER' return platform_events.GroupMessage( sender=platform_entities.GroupMember( @@ -144,7 +159,7 @@ class NakuruProjectEventConverter(adapter_model.EventConverter): group=platform_entities.Group( id=event.group_id, name=event.sender.nickname, - permission=platform_entities.Permission.Member + permission=platform_entities.Permission.Member, ), special_title=event.sender.title, join_timestamp=0, @@ -152,14 +167,15 @@ class NakuruProjectEventConverter(adapter_model.EventConverter): mute_time_remaining=0, ), message_chain=yiri_chain, - time=event.time + time=event.time, ) else: - raise Exception("未支持转换的事件类型: " + str(event)) + raise Exception('未支持转换的事件类型: ' + str(event)) class NakuruAdapter(adapter_model.MessagePlatformAdapter): """nakuru-project适配器""" + bot: nakuru.CQHTTP bot_account_id: int @@ -186,12 +202,14 @@ class NakuruAdapter(adapter_model.MessagePlatformAdapter): target_type: str, target_id: str, message: typing.Union[platform_message.MessageChain, list], - converted: bool = False + converted: bool = False, ): task = None - converted_msg = self.message_converter.yiri2target(message) if not converted else message - + converted_msg = ( + self.message_converter.yiri2target(message) if not converted else message + ) + # 检查是否有转发消息 has_forward = False for msg in converted_msg: @@ -200,19 +218,19 @@ class NakuruAdapter(adapter_model.MessagePlatformAdapter): converted_msg = msg break if has_forward: - if target_type == "group": + if target_type == 'group': task = self.bot.sendGroupForwardMessage(int(target_id), converted_msg) - elif target_type == "person": + elif target_type == 'person': task = self.bot.sendPrivateForwardMessage(int(target_id), converted_msg) else: - raise Exception("Unknown target type: " + target_type) + raise Exception('Unknown target type: ' + target_type) else: - if target_type == "group": + if target_type == 'group': task = self.bot.sendGroupMessage(int(target_id), converted_msg) - elif target_type == "person": + elif target_type == 'person': task = self.bot.sendFriendMessage(int(target_id), converted_msg) else: - raise Exception("Unknown target type: " + target_type) + raise Exception('Unknown target type: ' + target_type) await task @@ -220,45 +238,45 @@ class NakuruAdapter(adapter_model.MessagePlatformAdapter): self, message_source: platform_events.MessageEvent, message: platform_message.MessageChain, - quote_origin: bool = False + quote_origin: bool = False, ): message = self.message_converter.yiri2target(message) if quote_origin: # 在前方添加引用组件 - message.insert(0, nkc.Reply( + message.insert( + 0, + nkc.Reply( id=message_source.message_chain.message_id, - ) + ), ) if type(message_source) is platform_events.GroupMessage: await self.send_message( - "group", - message_source.sender.group.id, - message, - converted=True + 'group', message_source.sender.group.id, message, converted=True ) elif type(message_source) is platform_events.FriendMessage: await self.send_message( - "person", - message_source.sender.id, - message, - converted=True + 'person', message_source.sender.id, message, converted=True ) else: - raise Exception("Unknown message source type: " + str(type(message_source))) + raise Exception('Unknown message source type: ' + str(type(message_source))) def is_muted(self, group_id: int) -> bool: import time + # 检查是否被禁言 - group_member_info = asyncio.run(self.bot.getGroupMemberInfo(group_id, self.bot_account_id)) + group_member_info = asyncio.run( + self.bot.getGroupMemberInfo(group_id, self.bot_account_id) + ) return group_member_info.shut_up_timestamp > int(time.time()) def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter_model.MessagePlatformAdapter], None] + callback: typing.Callable[ + [platform_events.Event, adapter_model.MessagePlatformAdapter], None + ], ): try: - source_cls = NakuruProjectEventConverter.yiri2target(event_type) # 包装函数 @@ -268,9 +286,9 @@ class NakuruAdapter(adapter_model.MessagePlatformAdapter): # 将包装函数和原函数的对应关系存入列表 self.listener_list.append( { - "event_type": event_type, - "callable": callback, - "wrapper": listener_wrapper, + 'event_type': event_type, + 'callable': callback, + 'wrapper': listener_wrapper, } ) @@ -283,7 +301,9 @@ class NakuruAdapter(adapter_model.MessagePlatformAdapter): def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter_model.MessagePlatformAdapter], None] + callback: typing.Callable[ + [platform_events.Event, adapter_model.MessagePlatformAdapter], None + ], ): nakuru_event_name = self.event_converter.yiri2target(event_type).__name__ @@ -292,13 +312,16 @@ class NakuruAdapter(adapter_model.MessagePlatformAdapter): # 从本对象的监听器列表中查找并删除 target_wrapper = None for listener in self.listener_list: - if listener["event_type"] == event_type and listener["callable"] == callback: - target_wrapper = listener["wrapper"] + if ( + listener['event_type'] == event_type + and listener['callable'] == callback + ): + target_wrapper = listener['wrapper'] self.listener_list.remove(listener) break if target_wrapper is None: - raise Exception("未找到对应的监听器") + raise Exception('未找到对应的监听器') for func in self.bot.event[nakuru_event_name]: if func.callable != target_wrapper: @@ -309,23 +332,30 @@ class NakuruAdapter(adapter_model.MessagePlatformAdapter): async def run_async(self): try: import requests + resp = requests.get( - url="http://{}:{}/get_login_info".format(self.cfg['host'], self.cfg['http_port']), + url='http://{}:{}/get_login_info'.format( + self.cfg['host'], self.cfg['http_port'] + ), headers={ - 'Authorization': "Bearer " + self.cfg['token'] if 'token' in self.cfg else "" + 'Authorization': 'Bearer ' + self.cfg['token'] + if 'token' in self.cfg + else '' }, timeout=5, - proxies=None + proxies=None, ) if resp.status_code == 403: - raise Exception("go-cqhttp拒绝访问,请检查配置文件中nakuru适配器的配置") + raise Exception('go-cqhttp拒绝访问,请检查配置文件中nakuru适配器的配置') self.bot_account_id = int(resp.json()['data']['user_id']) - except Exception as e: - raise Exception("获取go-cqhttp账号信息失败, 请检查是否已启动go-cqhttp并配置正确") + except Exception: + raise Exception( + '获取go-cqhttp账号信息失败, 请检查是否已启动go-cqhttp并配置正确' + ) await self.bot._run() - self.ap.logger.info("运行 Nakuru 适配器") + self.ap.logger.info('运行 Nakuru 适配器') while True: await asyncio.sleep(1) async def kill(self) -> bool: - return False \ No newline at end of file + return False diff --git a/pkg/platform/sources/officialaccount.py b/pkg/platform/sources/officialaccount.py index 0816824f..6e7eaf2f 100644 --- a/pkg/platform/sources/officialaccount.py +++ b/pkg/platform/sources/officialaccount.py @@ -4,20 +4,13 @@ import asyncio import traceback import datetime -from pkg.core import app from pkg.platform.adapter import MessagePlatformAdapter from pkg.platform.types import events as platform_events, message as platform_message -from collections import deque from libs.official_account_api.oaevent import OAEvent -from pkg.platform.adapter import MessagePlatformAdapter -from pkg.platform.types import events as platform_events, message as platform_message from libs.official_account_api.api import OAClient from libs.official_account_api.api import OAClientForLongerResponse -from pkg.core import app from .. import adapter 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 ...command.errors import ParamNotEnoughError @@ -28,10 +21,9 @@ class OAMessageConverter(adapter.MessageConverter): for msg in message_chain: if type(msg) is platform_message.Plain: return msg.text - @staticmethod - async def target2yiri(message:str,message_id =-1): + async def target2yiri(message: str, message_id=-1): yiri_msg_list = [] yiri_msg_list.append( platform_message.Source(id=message_id, time=datetime.datetime.now()) @@ -41,12 +33,12 @@ class OAMessageConverter(adapter.MessageConverter): chain = platform_message.MessageChain(yiri_msg_list) return chain - + class OAEventConverter(adapter.EventConverter): @staticmethod - async def target2yiri(event:OAEvent): - if event.type == "text": + async def target2yiri(event: OAEvent): + if event.type == 'text': yiri_chain = await OAMessageConverter.target2yiri( event.message, event.message_id ) @@ -54,91 +46,101 @@ class OAEventConverter(adapter.EventConverter): friend = platform_entities.Friend( id=event.user_id, nickname=str(event.user_id), - remark="", + remark='', ) return platform_events.FriendMessage( - sender=friend, message_chain=yiri_chain, time=event.timestamp, source_platform_object=event + sender=friend, + message_chain=yiri_chain, + time=event.timestamp, + source_platform_object=event, ) else: return None -class OfficialAccountAdapter(adapter.MessagePlatformAdapter): - bot : OAClient | OAClientForLongerResponse - ap : app.Application +class OfficialAccountAdapter(adapter.MessagePlatformAdapter): + bot: OAClient | OAClientForLongerResponse + ap: app.Application bot_account_id: str message_converter: OAMessageConverter = OAMessageConverter() event_converter: OAEventConverter = OAEventConverter() config: dict - def __init__(self, config: dict, ap: app.Application): self.config = config - + self.ap = ap required_keys = [ - "token", - "EncodingAESKey", - "AppSecret", - "AppID", - "Mode", + 'token', + 'EncodingAESKey', + 'AppSecret', + 'AppID', + 'Mode', ] missing_keys = [key for key in required_keys if key not in config] if missing_keys: - raise ParamNotEnoughError("微信公众号缺少相关配置项,请查看文档或联系管理员") - - - if self.config['Mode'] == "drop": + raise ParamNotEnoughError( + '微信公众号缺少相关配置项,请查看文档或联系管理员' + ) + + if self.config['Mode'] == 'drop': self.bot = OAClient( token=config['token'], EncodingAESKey=config['EncodingAESKey'], Appsecret=config['AppSecret'], - AppID=config['AppID'], + AppID=config['AppID'], ) - elif self.config['Mode'] == "passive": + elif self.config['Mode'] == 'passive': self.bot = OAClientForLongerResponse( token=config['token'], EncodingAESKey=config['EncodingAESKey'], Appsecret=config['AppSecret'], - AppID=config['AppID'], - LoadingMessage=config['LoadingMessage'] + AppID=config['AppID'], + LoadingMessage=config['LoadingMessage'], ) else: - raise KeyError("请设置微信公众号通信模式") + raise KeyError('请设置微信公众号通信模式') - - async def reply_message(self, message_source: platform_events.FriendMessage, message: platform_message.MessageChain, quote_origin: bool = False): - - content = await OAMessageConverter.yiri2target( - message - ) - if type(self.bot) == OAClient: - await self.bot.set_message(message_source.message_chain.message_id,content) - if type(self.bot) == OAClientForLongerResponse: + async def reply_message( + self, + message_source: platform_events.FriendMessage, + message: platform_message.MessageChain, + quote_origin: bool = False, + ): + content = await OAMessageConverter.yiri2target(message) + if isinstance(self.bot, OAClient): + await self.bot.set_message(message_source.message_chain.message_id, content) + elif isinstance(self.bot, OAClientForLongerResponse): from_user = message_source.sender.id - await self.bot.set_message(from_user,message_source.message_chain.message_id,content) + await self.bot.set_message( + from_user, message_source.message_chain.message_id, content + ) - async def send_message( self, target_type: str, target_id: str, message: platform_message.MessageChain ): pass - - def register_listener(self, event_type: type, callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None]): + def register_listener( + self, + event_type: type, + callback: typing.Callable[ + [platform_events.Event, MessagePlatformAdapter], None + ], + ): async def on_message(event: OAEvent): self.bot_account_id = event.receiver_id try: return await callback( await self.event_converter.target2yiri(event), self ) - except: + except Exception: traceback.print_exc() if event_type == platform_events.FriendMessage: - self.bot.on_message("text")(on_message) + self.bot.on_message('text')(on_message) elif event_type == platform_events.GroupMessage: pass @@ -148,8 +150,8 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter): await asyncio.sleep(1) await self.bot.run_task( - host=self.config["host"], - port=self.config["port"], + host=self.config['host'], + port=self.config['port'], shutdown_trigger=shutdown_trigger_placeholder, ) @@ -159,8 +161,8 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter): async def unregister_listener( self, event_type: type, - callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, MessagePlatformAdapter], None + ], ): return super().unregister_listener(event_type, callback) - - \ No newline at end of file diff --git a/pkg/platform/sources/qqbotpy.py b/pkg/platform/sources/qqbotpy.py index 716da80f..a91f86dd 100644 --- a/pkg/platform/sources/qqbotpy.py +++ b/pkg/platform/sources/qqbotpy.py @@ -22,12 +22,20 @@ from ...platform.types import message as platform_message class OfficialGroupMessage(platform_events.GroupMessage): pass + class OfficialFriendMessage(platform_events.FriendMessage): pass + event_handler_mapping = { - platform_events.GroupMessage: ["on_at_message_create", "on_group_at_message_create"], - platform_events.FriendMessage: ["on_direct_message_create", "on_c2c_message_create"], + platform_events.GroupMessage: [ + 'on_at_message_create', + 'on_group_at_message_create', + ], + platform_events.FriendMessage: [ + 'on_direct_message_create', + 'on_c2c_message_create', + ], } @@ -53,9 +61,10 @@ def char_to_value(char): return ord(char) - ord('0') elif 'A' <= char <= 'Z': return ord(char) - ord('A') + 10 - + return ord(char) - ord('a') + 36 + def digest(s: str) -> int: """计算字符串的hash值。""" # 取末尾的8位 @@ -69,19 +78,24 @@ def digest(s: str) -> int: return number -K = typing.TypeVar("K") -V = typing.TypeVar("V") + +K = typing.TypeVar('K') +V = typing.TypeVar('V') class OpenIDMapping(typing.Generic[K, V]): - map: dict[K, V] dump_func: typing.Callable digest_func: typing.Callable[[K], V] - def __init__(self, map: dict[K, V], dump_func: typing.Callable, digest_func: typing.Callable[[K], V] = digest): + def __init__( + self, + map: dict[K, V], + dump_func: typing.Callable, + digest_func: typing.Callable[[K], V] = digest, + ): self.map = map self.dump_func = dump_func @@ -104,12 +118,11 @@ class OpenIDMapping(typing.Generic[K, V]): def getkey(self, value: V) -> K: return list(self.map.keys())[list(self.map.values()).index(value)] - + def save_openid(self, key: K) -> V: - if key in self.map: return self.map[key] - + value = self.digest_func(key) self.map[key] = value @@ -135,7 +148,7 @@ class OfficialMessageConverter(adapter_model.MessageConverter): msg_list = [platform_message.Plain(text=message_chain)] else: raise Exception( - "Unknown message type: " + str(message_chain) + str(type(message_chain)) + 'Unknown message type: ' + str(message_chain) + str(type(message_chain)) ) offcial_messages: list[dict] = [] @@ -154,23 +167,23 @@ class OfficialMessageConverter(adapter_model.MessageConverter): # 遍历并转换 for component in msg_list: if type(component) is platform_message.Plain: - offcial_messages.append({"type": "text", "content": component.text}) + offcial_messages.append({'type': 'text', 'content': component.text}) elif type(component) is platform_message.Image: if component.url is not None: - offcial_messages.append({"type": "image", "content": component.url}) + offcial_messages.append({'type': 'image', 'content': component.url}) elif component.path is not None: offcial_messages.append( - {"type": "file_image", "content": component.path} + {'type': 'file_image', 'content': component.path} ) elif type(component) is platform_message.At: - offcial_messages.append({"type": "at", "content": ""}) + offcial_messages.append({'type': 'at', 'content': ''}) elif type(component) is platform_message.AtAll: print( - "上层组件要求发送 AtAll 消息,但 QQ 官方 API 不支持此消息类型,忽略此消息。" + '上层组件要求发送 AtAll 消息,但 QQ 官方 API 不支持此消息类型,忽略此消息。' ) elif type(component) is platform_message.Voice: print( - "上层组件要求发送 Voice 消息,但 QQ 官方 API 不支持此消息类型,忽略此消息。" + '上层组件要求发送 Voice 消息,但 QQ 官方 API 不支持此消息类型,忽略此消息。' ) elif type(component) is forward.Forward: # 转发消息 @@ -185,7 +198,7 @@ class OfficialMessageConverter(adapter_model.MessageConverter): offcial_messages.extend( OfficialMessageConverter.yiri2target(message_chain) ) - except Exception as e: + except Exception: import traceback traceback.print_exc() @@ -194,7 +207,12 @@ class OfficialMessageConverter(adapter_model.MessageConverter): @staticmethod def extract_message_chain_from_obj( - message: typing.Union[botpy_message.Message, botpy_message.DirectMessage, botpy_message.GroupMessage, botpy_message.C2CMessage], + message: typing.Union[ + botpy_message.Message, + botpy_message.DirectMessage, + botpy_message.GroupMessage, + botpy_message.C2CMessage, + ], message_id: str = None, bot_account_id: int = 0, ) -> platform_message.MessageChain: @@ -210,7 +228,7 @@ class OfficialMessageConverter(adapter_model.MessageConverter): if type(message) not in [botpy_message.DirectMessage, botpy_message.C2CMessage]: yiri_msg_list.append(platform_message.At(target=bot_account_id)) - if hasattr(message, "mentions"): + if hasattr(message, 'mentions'): for mention in message.mentions: if mention.bot: continue @@ -218,15 +236,15 @@ class OfficialMessageConverter(adapter_model.MessageConverter): yiri_msg_list.append(platform_message.At(target=mention.id)) for attachment in message.attachments: - if attachment.content_type.startswith("image"): + if attachment.content_type.startswith('image'): yiri_msg_list.append(platform_message.Image(url=attachment.url)) else: logging.warning( - "不支持的附件类型:" + attachment.content_type + ",忽略此附件。" + '不支持的附件类型:' + attachment.content_type + ',忽略此附件。' ) - content = re.sub(r"<@!\d+>", "", str(message.content)) - if content.strip() != "": + content = re.sub(r'<@!\d+>', '', str(message.content)) + if content.strip() != '': yiri_msg_list.append(platform_message.Plain(text=content)) chain = platform_message.MessageChain(yiri_msg_list) @@ -247,21 +265,25 @@ class OfficialEventConverter(adapter_model.EventConverter): return botpy_message.DirectMessage else: raise Exception( - "未支持转换的事件类型(YiriMirai -> Official): " + str(event) + '未支持转换的事件类型(YiriMirai -> Official): ' + str(event) ) def target2yiri( self, - event: typing.Union[botpy_message.Message, botpy_message.DirectMessage, botpy_message.GroupMessage, botpy_message.C2CMessage], + event: typing.Union[ + botpy_message.Message, + botpy_message.DirectMessage, + botpy_message.GroupMessage, + botpy_message.C2CMessage, + ], ) -> platform_events.Event: + if isinstance(event, botpy_message.Message): # 频道内,转群聊事件 + permission = 'MEMBER' - if type(event) == botpy_message.Message: # 频道内,转群聊事件 - permission = "MEMBER" - - if "2" in event.member.roles: - permission = "ADMINISTRATOR" - elif "4" in event.member.roles: - permission = "OWNER" + if '2' in event.member.roles: + permission = 'ADMINISTRATOR' + elif '4' in event.member.roles: + permission = 'OWNER' return platform_events.GroupMessage( sender=platform_entities.GroupMember( @@ -273,10 +295,10 @@ class OfficialEventConverter(adapter_model.EventConverter): name=event.author.username, permission=platform_entities.Permission.Member, ), - special_title="", + special_title='', join_timestamp=int( datetime.datetime.strptime( - event.member.joined_at, "%Y-%m-%dT%H:%M:%S%z" + event.member.joined_at, '%Y-%m-%dT%H:%M:%S%z' ).timestamp() ), last_speak_timestamp=datetime.datetime.now().timestamp(), @@ -287,11 +309,11 @@ class OfficialEventConverter(adapter_model.EventConverter): ), time=int( datetime.datetime.strptime( - event.timestamp, "%Y-%m-%dT%H:%M:%S%z" + event.timestamp, '%Y-%m-%dT%H:%M:%S%z' ).timestamp() ), ) - elif type(event) == botpy_message.DirectMessage: # 频道私聊,转私聊事件 + elif isinstance(event, botpy_message.DirectMessage): # 频道私聊,转私聊事件 return platform_events.FriendMessage( sender=platform_entities.Friend( id=event.guild_id, @@ -303,25 +325,24 @@ class OfficialEventConverter(adapter_model.EventConverter): ), time=int( datetime.datetime.strptime( - event.timestamp, "%Y-%m-%dT%H:%M:%S%z" + event.timestamp, '%Y-%m-%dT%H:%M:%S%z' ).timestamp() ), ) - elif type(event) == botpy_message.GroupMessage: # 群聊,转群聊事件 - + elif isinstance(event, botpy_message.GroupMessage): # 群聊,转群聊事件 author_member_id = event.author.member_openid return OfficialGroupMessage( sender=platform_entities.GroupMember( id=author_member_id, member_name=author_member_id, - permission="MEMBER", + permission='MEMBER', group=platform_entities.Group( id=event.group_openid, name=author_member_id, permission=platform_entities.Permission.Member, ), - special_title="", + special_title='', join_timestamp=int(0), last_speak_timestamp=datetime.datetime.now().timestamp(), mute_time_remaining=0, @@ -331,12 +352,11 @@ class OfficialEventConverter(adapter_model.EventConverter): ), time=int( datetime.datetime.strptime( - event.timestamp, "%Y-%m-%dT%H:%M:%S%z" + event.timestamp, '%Y-%m-%dT%H:%M:%S%z' ).timestamp() ), ) - elif type(event) == botpy_message.C2CMessage: # 私聊,转私聊事件 - + elif isinstance(event, botpy_message.C2CMessage): # 私聊,转私聊事件 user_id_alter = event.author.user_openid return OfficialFriendMessage( @@ -350,7 +370,7 @@ class OfficialEventConverter(adapter_model.EventConverter): ), time=int( datetime.datetime.strptime( - event.timestamp, "%Y-%m-%dT%H:%M:%S%z" + event.timestamp, '%Y-%m-%dT%H:%M:%S%z' ).timestamp() ), ) @@ -391,10 +411,10 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter): switchs = {} - for intent in cfg["intents"]: + for intent in cfg['intents']: switchs[intent] = True - del cfg["intents"] + del cfg['intents'] intents = botpy.Intents(**switchs) @@ -408,21 +428,21 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter): for msg in message_list: args = {} - if msg["type"] == "text": - args["content"] = msg["content"] - elif msg["type"] == "image": - args["image"] = msg["content"] - elif msg["type"] == "file_image": - args["file_image"] = msg["content"] + if msg['type'] == 'text': + args['content'] = msg['content'] + elif msg['type'] == 'image': + args['image'] = msg['content'] + elif msg['type'] == 'file_image': + args['file_image'] = msg['content'] else: continue - if target_type == "group": - args["channel_id"] = str(target_id) + if target_type == 'group': + args['channel_id'] = str(target_id) await self.bot.api.post_message(**args) - elif target_type == "person": - args["guild_id"] = str(target_id) + elif target_type == 'person': + args['guild_id'] = str(target_id) await self.bot.api.post_dms(**args) @@ -432,86 +452,82 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter): message: platform_message.MessageChain, quote_origin: bool = False, ): - message_list = self.message_converter.yiri2target(message) for msg in message_list: args = {} - if msg["type"] == "text": - args["content"] = msg["content"] - elif msg["type"] == "image": - args["image"] = msg["content"] - elif msg["type"] == "file_image": - args["file_image"] = msg["content"] + if msg['type'] == 'text': + args['content'] = msg['content'] + elif msg['type'] == 'image': + args['image'] = msg['content'] + elif msg['type'] == 'file_image': + args['file_image'] = msg['content'] else: continue if quote_origin: - args["message_reference"] = botpy_message_type.Reference( + args['message_reference'] = botpy_message_type.Reference( message_id=cached_message_ids[ str(message_source.message_chain.message_id) ] ) - if type(message_source) == platform_events.GroupMessage: - args["channel_id"] = str(message_source.sender.group.id) - args["msg_id"] = cached_message_ids[ + if isinstance(message_source, platform_events.GroupMessage): + args['channel_id'] = str(message_source.sender.group.id) + args['msg_id'] = cached_message_ids[ str(message_source.message_chain.message_id) ] await self.bot.api.post_message(**args) - elif type(message_source) == platform_events.FriendMessage: - args["guild_id"] = str(message_source.sender.id) - args["msg_id"] = cached_message_ids[ + elif isinstance(message_source, platform_events.FriendMessage): + args['guild_id'] = str(message_source.sender.id) + args['msg_id'] = cached_message_ids[ str(message_source.message_chain.message_id) ] await self.bot.api.post_dms(**args) - elif type(message_source) == OfficialGroupMessage: - - if "file_image" in args: # 暂不支持发送文件图片 + elif isinstance(message_source, OfficialGroupMessage): + if 'file_image' in args: # 暂不支持发送文件图片 continue - args["group_openid"] = message_source.sender.group.id + args['group_openid'] = message_source.sender.group.id - if "image" in args: + if 'image' in args: uploadMedia = await self.bot.api.post_group_file( - group_openid=args["group_openid"], + group_openid=args['group_openid'], file_type=1, - url=str(args['image']) + url=str(args['image']), ) del args['image'] args['media'] = uploadMedia args['msg_type'] = 7 - args["msg_id"] = cached_message_ids[ + args['msg_id'] = cached_message_ids[ str(message_source.message_chain.message_id) ] - args["msg_seq"] = self.group_msg_seq + args['msg_seq'] = self.group_msg_seq self.group_msg_seq += 1 await self.bot.api.post_group_message(**args) - elif type(message_source) == OfficialFriendMessage: - if "file_image" in args: + elif isinstance(message_source, OfficialFriendMessage): + if 'file_image' in args: continue - args["openid"] = message_source.sender.id + args['openid'] = message_source.sender.id - if "image" in args: + if 'image' in args: uploadMedia = await self.bot.api.post_c2c_file( - openid=args["openid"], - file_type=1, - url=str(args['image']) + openid=args['openid'], file_type=1, url=str(args['image']) ) del args['image'] args['media'] = uploadMedia args['msg_type'] = 7 - args["msg_id"] = cached_message_ids[ + args['msg_id'] = cached_message_ids[ str(message_source.message_chain.message_id) ] - args["msg_seq"] = self.c2c_msg_seq + args['msg_seq'] = self.c2c_msg_seq self.c2c_msg_seq += 1 await self.bot.api.post_c2c_message(**args) @@ -526,7 +542,6 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter): [platform_events.Event, adapter_model.MessagePlatformAdapter], None ], ): - try: async def wrapper( @@ -534,7 +549,7 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter): botpy_message.Message, botpy_message.DirectMessage, botpy_message.GroupMessage, - ] + ], ): self.cached_official_messages[str(message.id)] = message await callback(self.event_converter.target2yiri(message), self) @@ -555,7 +570,6 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter): delattr(self.bot, event_handler_mapping[event_type]) async def run_async(self): - self.metadata = self.ap.adapter_qq_botpy_meta self.message_converter = OfficialMessageConverter() @@ -563,7 +577,7 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter): self.cfg['ret_coro'] = True - self.ap.logger.info("运行 QQ 官方适配器") + self.ap.logger.info('运行 QQ 官方适配器') await (await self.bot.start(**self.cfg)) async def kill(self) -> bool: diff --git a/pkg/platform/sources/qqofficial.py b/pkg/platform/sources/qqofficial.py index bfef2135..06893485 100644 --- a/pkg/platform/sources/qqofficial.py +++ b/pkg/platform/sources/qqofficial.py @@ -7,12 +7,8 @@ import datetime from pkg.platform.adapter import MessagePlatformAdapter from pkg.platform.types import events as platform_events, message as platform_message -from pkg.core import app from .. import adapter - 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 ...command.errors import ParamNotEnoughError from libs.qq_official_api.api import QQOfficialClient @@ -21,157 +17,164 @@ from ...utils import image class QQOfficialMessageConverter(adapter.MessageConverter): - @staticmethod async def yiri2target(message_chain: platform_message.MessageChain): content_list = [] - #只实现了发文字 + # 只实现了发文字 for msg in message_chain: if type(msg) is platform_message.Plain: - content_list.append({ - "type":"text", - "content":msg.text, - }) - + content_list.append( + { + 'type': 'text', + 'content': msg.text, + } + ) + return content_list - + @staticmethod - async def target2yiri(message:str,message_id:str,pic_url:str,content_type): + async def target2yiri(message: str, message_id: str, pic_url: str, content_type): yiri_msg_list = [] yiri_msg_list.append( - platform_message.Source(id=message_id,time=datetime.datetime.now()) + platform_message.Source(id=message_id, time=datetime.datetime.now()) ) if pic_url is not None: - base64_url = await image.get_qq_official_image_base64(pic_url=pic_url,content_type=content_type) - yiri_msg_list.append( - platform_message.Image(base64=base64_url) + base64_url = await image.get_qq_official_image_base64( + pic_url=pic_url, content_type=content_type ) + yiri_msg_list.append(platform_message.Image(base64=base64_url)) yiri_msg_list.append(platform_message.Plain(text=message)) chain = platform_message.MessageChain(yiri_msg_list) return chain + class QQOfficialEventConverter(adapter.EventConverter): + @staticmethod + async def yiri2target(event: platform_events.MessageEvent) -> QQOfficialEvent: + return event.source_platform_object @staticmethod - async def yiri2target(event:platform_events.MessageEvent) -> QQOfficialEvent: - return event.source_platform_object - - @staticmethod - async def target2yiri(event:QQOfficialEvent): + async def target2yiri(event: QQOfficialEvent): """ QQ官方消息转换为LB对象 """ yiri_chain = await QQOfficialMessageConverter.target2yiri( - message=event.content,message_id=event.d_id,pic_url=event.attachments,content_type=event.content_type + message=event.content, + message_id=event.d_id, + pic_url=event.attachments, + content_type=event.content_type, ) - + if event.t == 'C2C_MESSAGE_CREATE': friend = platform_entities.Friend( - id = event.user_openid, - nickname = event.t, - remark = "", + id=event.user_openid, + nickname=event.t, + remark='', ) return platform_events.FriendMessage( - sender = friend,message_chain = yiri_chain,time = int( + sender=friend, + message_chain=yiri_chain, + time=int( datetime.datetime.strptime( - event.timestamp, "%Y-%m-%dT%H:%M:%S%z" + event.timestamp, '%Y-%m-%dT%H:%M:%S%z' ).timestamp() ), - source_platform_object=event + source_platform_object=event, ) - + if event.t == 'DIRECT_MESSAGE_CREATE': friend = platform_entities.Friend( - id = event.guild_id, - nickname = event.t, - remark = "", + id=event.guild_id, + nickname=event.t, + remark='', ) return platform_events.FriendMessage( - sender = friend,message_chain = yiri_chain, - source_platform_object=event + sender=friend, message_chain=yiri_chain, source_platform_object=event ) if event.t == 'GROUP_AT_MESSAGE_CREATE': - yiri_chain.insert(0, platform_message.At(target="justbot")) + yiri_chain.insert(0, platform_message.At(target='justbot')) sender = platform_entities.GroupMember( - id = event.group_openid, - member_name= event.t, - permission= 'MEMBER', - group = platform_entities.Group( - id = event.group_openid, - name = 'MEMBER', - permission= platform_entities.Permission.Member - ), - special_title='', - join_timestamp=0, - last_speak_timestamp=0, - mute_time_remaining=0 - ) - time = int( - datetime.datetime.strptime( - event.timestamp, "%Y-%m-%dT%H:%M:%S%z" - ).timestamp() - ) - return platform_events.GroupMessage( - sender = sender, - message_chain=yiri_chain, - time = time, - source_platform_object=event - ) - if event.t =='AT_MESSAGE_CREATE': - yiri_chain.insert(0, platform_message.At(target="justbot")) - sender = platform_entities.GroupMember( - id = event.channel_id, + id=event.group_openid, member_name=event.t, - permission= 'MEMBER', - group = platform_entities.Group( - id = event.channel_id, - name = 'MEMBER', - permission=platform_entities.Permission.Member + permission='MEMBER', + group=platform_entities.Group( + id=event.group_openid, + name='MEMBER', + permission=platform_entities.Permission.Member, ), special_title='', join_timestamp=0, last_speak_timestamp=0, - mute_time_remaining=0 + mute_time_remaining=0, ) time = int( - datetime.datetime.strptime( - event.timestamp, "%Y-%m-%dT%H:%M:%S%z" - ).timestamp() - ) + datetime.datetime.strptime( + event.timestamp, '%Y-%m-%dT%H:%M:%S%z' + ).timestamp() + ) return platform_events.GroupMessage( - sender =sender, - message_chain = yiri_chain, - time = time, - source_platform_object=event + sender=sender, + message_chain=yiri_chain, + time=time, + source_platform_object=event, + ) + if event.t == 'AT_MESSAGE_CREATE': + yiri_chain.insert(0, platform_message.At(target='justbot')) + sender = platform_entities.GroupMember( + id=event.channel_id, + member_name=event.t, + permission='MEMBER', + group=platform_entities.Group( + id=event.channel_id, + name='MEMBER', + permission=platform_entities.Permission.Member, + ), + special_title='', + join_timestamp=0, + last_speak_timestamp=0, + mute_time_remaining=0, + ) + time = int( + datetime.datetime.strptime( + event.timestamp, '%Y-%m-%dT%H:%M:%S%z' + ).timestamp() + ) + return platform_events.GroupMessage( + sender=sender, + message_chain=yiri_chain, + time=time, + source_platform_object=event, ) class QQOfficialAdapter(adapter.MessagePlatformAdapter): - bot:QQOfficialClient - ap:app.Application - config:dict - bot_account_id:str + bot: QQOfficialClient + ap: app.Application + config: dict + bot_account_id: str message_converter: QQOfficialMessageConverter = QQOfficialMessageConverter() event_converter: QQOfficialEventConverter = QQOfficialEventConverter() - def __init__(self, config:dict, ap:app.Application): + def __init__(self, config: dict, ap: app.Application): self.config = config self.ap = ap required_keys = [ - "appid", - "secret", + 'appid', + 'secret', ] missing_keys = [key for key in required_keys if key not in config] if missing_keys: - raise ParamNotEnoughError("QQ官方机器人缺少相关配置项,请查看文档或联系管理员") - + raise ParamNotEnoughError( + 'QQ官方机器人缺少相关配置项,请查看文档或联系管理员' + ) + self.bot = QQOfficialClient( - app_id=config["appid"], - secret=config["secret"], - token=config["token"], + app_id=config['appid'], + secret=config['secret'], + token=config['token'], ) async def reply_message( @@ -186,31 +189,45 @@ class QQOfficialAdapter(adapter.MessagePlatformAdapter): content_list = await QQOfficialMessageConverter.yiri2target(message) - #私聊消息 + # 私聊消息 if qq_official_event.t == 'C2C_MESSAGE_CREATE': for content in content_list: - if content["type"] == 'text': - await self.bot.send_private_text_msg(qq_official_event.user_openid,content['content'],qq_official_event.d_id) + if content['type'] == 'text': + await self.bot.send_private_text_msg( + qq_official_event.user_openid, + content['content'], + qq_official_event.d_id, + ) - #群聊消息 + # 群聊消息 if qq_official_event.t == 'GROUP_AT_MESSAGE_CREATE': for content in content_list: - if content["type"] == 'text': - await self.bot.send_group_text_msg(qq_official_event.group_openid,content['content'],qq_official_event.d_id) - - #频道群聊 + if content['type'] == 'text': + await self.bot.send_group_text_msg( + qq_official_event.group_openid, + content['content'], + qq_official_event.d_id, + ) + + # 频道群聊 if qq_official_event.t == 'AT_MESSAGE_CREATE': for content in content_list: - if content["type"] == 'text': - await self.bot.send_channle_group_text_msg(qq_official_event.channel_id,content['content'],qq_official_event.d_id) + if content['type'] == 'text': + await self.bot.send_channle_group_text_msg( + qq_official_event.channel_id, + content['content'], + qq_official_event.d_id, + ) - #频道私聊 + # 频道私聊 if qq_official_event.t == 'DIRECT_MESSAGE_CREATE': for content in content_list: - if content["type"] == 'text': - await self.bot.send_channle_private_text_msg(qq_official_event.guild_id,content['content'],qq_official_event.d_id) - - + if content['type'] == 'text': + await self.bot.send_channle_private_text_msg( + qq_official_event.guild_id, + content['content'], + qq_official_event.d_id, + ) async def send_message( self, target_type: str, target_id: str, message: platform_message.MessageChain @@ -224,22 +241,21 @@ class QQOfficialAdapter(adapter.MessagePlatformAdapter): [platform_events.Event, adapter.MessagePlatformAdapter], None ], ): - async def on_message(event:QQOfficialEvent): - self.bot_account_id = "justbot" + async def on_message(event: QQOfficialEvent): + self.bot_account_id = 'justbot' try: return await callback( - await self.event_converter.target2yiri(event),self + await self.event_converter.target2yiri(event), self ) - except: + except Exception: traceback.print_exc() - - if event_type == platform_events.FriendMessage: - self.bot.on_message("DIRECT_MESSAGE_CREATE")(on_message) - self.bot.on_message("C2C_MESSAGE_CREATE")(on_message) - elif event_type == platform_events.GroupMessage: - self.bot.on_message("GROUP_AT_MESSAGE_CREATE")(on_message) - self.bot.on_message("AT_MESSAGE_CREATE")(on_message) + if event_type == platform_events.FriendMessage: + self.bot.on_message('DIRECT_MESSAGE_CREATE')(on_message) + self.bot.on_message('C2C_MESSAGE_CREATE')(on_message) + elif event_type == platform_events.GroupMessage: + self.bot.on_message('GROUP_AT_MESSAGE_CREATE')(on_message) + self.bot.on_message('AT_MESSAGE_CREATE')(on_message) async def run_async(self): async def shutdown_trigger_placeholder(): @@ -248,17 +264,18 @@ class QQOfficialAdapter(adapter.MessagePlatformAdapter): await self.bot.run_task( host='0.0.0.0', - port=self.config["port"], + port=self.config['port'], shutdown_trigger=shutdown_trigger_placeholder, - ) - + ) + async def kill(self) -> bool: return False - + def unregister_listener( self, event_type: type, - callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, MessagePlatformAdapter], None + ], ): return super().unregister_listener(event_type, callback) - diff --git a/pkg/platform/sources/telegram.py b/pkg/platform/sources/telegram.py index 49822673..03036293 100644 --- a/pkg/platform/sources/telegram.py +++ b/pkg/platform/sources/telegram.py @@ -3,48 +3,33 @@ from __future__ import annotations import telegram import telegram.ext from telegram import Update -from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, MessageHandler, filters +from telegram.ext import ApplicationBuilder, ContextTypes, MessageHandler, filters import typing -import asyncio import traceback -import time -import re -import base64 -import uuid -import json -import datetime -import hashlib import base64 import aiohttp -from Crypto.Cipher import AES -from flask import jsonify from lark_oapi.api.im.v1 import * -from lark_oapi.api.verification.v1 import GetVerificationRequest 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 TelegramMessageConverter(adapter.MessageConverter): @staticmethod - async def yiri2target(message_chain: platform_message.MessageChain, bot: telegram.Bot) -> list[dict]: + async def yiri2target( + message_chain: platform_message.MessageChain, bot: telegram.Bot + ) -> list[dict]: components = [] for component in message_chain: if isinstance(component, platform_message.Plain): - components.append({ - "type": "text", - "text": component.text - }) + components.append({'type': 'text', 'text': component.text}) elif isinstance(component, platform_message.Image): - photo_bytes = None if component.base64: @@ -54,24 +39,25 @@ class TelegramMessageConverter(adapter.MessageConverter): async with session.get(component.url) as response: photo_bytes = await response.read() elif component.path: - with open(component.path, "rb") as f: + with open(component.path, 'rb') as f: photo_bytes = f.read() - - components.append({ - "type": "photo", - "photo": photo_bytes - }) + + components.append({'type': 'photo', 'photo': photo_bytes}) elif isinstance(component, platform_message.Forward): for node in component.node_list: - components.extend(await TelegramMessageConverter.yiri2target(node.message_chain, bot)) + components.extend( + await TelegramMessageConverter.yiri2target( + node.message_chain, bot + ) + ) return components - - @staticmethod - async def target2yiri(message: telegram.Message, bot: telegram.Bot, bot_account_id: str): - - message_components = [] + @staticmethod + async def target2yiri( + message: telegram.Message, bot: telegram.Bot, bot_account_id: str + ): + message_components = [] def parse_message_text(text: str) -> list[platform_message.MessageComponent]: msg_components = [] @@ -86,7 +72,7 @@ class TelegramMessageConverter(adapter.MessageConverter): if message.text: message_text = message.text message_components.extend(parse_message_text(message_text)) - + if message.photo: message_components.extend(parse_message_text(message.caption)) @@ -100,21 +86,26 @@ class TelegramMessageConverter(adapter.MessageConverter): file_bytes = await response.read() file_format = 'image/jpeg' - message_components.append(platform_message.Image(base64=f"data:{file_format};base64,{base64.b64encode(file_bytes).decode('utf-8')}")) - + message_components.append( + platform_message.Image( + base64=f'data:{file_format};base64,{base64.b64encode(file_bytes).decode("utf-8")}' + ) + ) + return platform_message.MessageChain(message_components) - + class TelegramEventConverter(adapter.EventConverter): @staticmethod async def yiri2target(event: platform_events.MessageEvent, bot: telegram.Bot): return event.source_platform_object - + @staticmethod async def target2yiri(event: Update, bot: telegram.Bot, bot_account_id: str): + lb_message = await TelegramMessageConverter.target2yiri( + event.message, bot, bot_account_id + ) - lb_message = await TelegramMessageConverter.target2yiri(event.message, bot, bot_account_id) - if event.effective_chat.type == 'private': return platform_events.FriendMessage( sender=platform_entities.Friend( @@ -124,7 +115,7 @@ class TelegramEventConverter(adapter.EventConverter): ), message_chain=lb_message, time=event.message.date.timestamp(), - source_platform_object=event + source_platform_object=event, ) elif event.effective_chat.type == 'group': return platform_events.GroupMessage( @@ -137,19 +128,18 @@ class TelegramEventConverter(adapter.EventConverter): name=event.effective_chat.title, permission=platform_entities.Permission.Member, ), - special_title="", + special_title='', join_timestamp=0, last_speak_timestamp=0, mute_time_remaining=0, ), message_chain=lb_message, time=event.message.date.timestamp(), - source_platform_object=event + source_platform_object=event, ) - + class TelegramAdapter(adapter.MessagePlatformAdapter): - bot: telegram.Bot application: telegram.ext.Application @@ -165,26 +155,31 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): typing.Type[platform_events.Event], typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], ] = {} - + def __init__(self, config: dict, ap: app.Application): self.config = config self.ap = ap - - async def telegram_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): + async def telegram_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): if update.message.from_user.is_bot: return try: - lb_event = await self.event_converter.target2yiri(update, self.bot, self.bot_account_id) + lb_event = await self.event_converter.target2yiri( + update, self.bot, self.bot_account_id + ) await self.listeners[type(lb_event)](lb_event, self) - except Exception as e: + except Exception: print(traceback.format_exc()) - + self.application = ApplicationBuilder().token(self.config['token']).build() self.bot = self.application.bot - self.application.add_handler(MessageHandler(filters.TEXT | (filters.COMMAND) | filters.PHOTO , telegram_callback)) - + self.application.add_handler( + MessageHandler( + filters.TEXT | (filters.COMMAND) | filters.PHOTO, telegram_callback + ) + ) + async def send_message( self, target_type: str, target_id: str, message: platform_message.MessageChain ): @@ -198,45 +193,49 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): ): assert isinstance(message_source.source_platform_object, Update) components = await TelegramMessageConverter.yiri2target(message, self.bot) - + for component in components: if component['type'] == 'text': - args = { - "chat_id": message_source.source_platform_object.effective_chat.id, - "text": component['text'], + 'chat_id': message_source.source_platform_object.effective_chat.id, + 'text': component['text'], } if quote_origin: - args['reply_to_message_id'] = message_source.source_platform_object.message.id + args['reply_to_message_id'] = ( + message_source.source_platform_object.message.id + ) await self.bot.send_message(**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.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, adapter.MessagePlatformAdapter], None + ], ): self.listeners[event_type] = callback - + def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, adapter.MessagePlatformAdapter], None + ], ): self.listeners.pop(event_type) - + async def run_async(self): await self.application.initialize() self.bot_account_id = (await self.bot.get_me()).username - await self.application.updater.start_polling( - allowed_updates=Update.ALL_TYPES - ) + await self.application.updater.start_polling(allowed_updates=Update.ALL_TYPES) await self.application.start() - + async def kill(self) -> bool: - await self.application.stop() - return True \ No newline at end of file + if self.application.running: + await self.application.stop() + return True diff --git a/pkg/platform/sources/wecom.py b/pkg/platform/sources/wecom.py index 40632595..53878062 100644 --- a/pkg/platform/sources/wecom.py +++ b/pkg/platform/sources/wecom.py @@ -9,17 +9,14 @@ from libs.wecom_api.api import WecomClient from pkg.platform.adapter import MessagePlatformAdapter from pkg.platform.types import events as platform_events, message as platform_message from libs.wecom_api.wecomevent import WecomEvent -from pkg.core import app from .. import adapter 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 ...command.errors import ParamNotEnoughError from ...utils import image -class WecomMessageConverter(adapter.MessageConverter): +class WecomMessageConverter(adapter.MessageConverter): @staticmethod async def yiri2target( message_chain: platform_message.MessageChain, bot: WecomClient @@ -28,23 +25,35 @@ class WecomMessageConverter(adapter.MessageConverter): for msg in message_chain: if type(msg) is platform_message.Plain: - content_list.append({ - "type": "text", - "content": msg.text, - }) + content_list.append( + { + 'type': 'text', + 'content': msg.text, + } + ) elif type(msg) is platform_message.Image: - content_list.append({ - "type": "image", - "media_id": await bot.get_media_id(msg), - }) + content_list.append( + { + 'type': 'image', + 'media_id': await bot.get_media_id(msg), + } + ) elif type(msg) is platform_message.Forward: for node in msg.node_list: - content_list.extend((await WecomMessageConverter.yiri2target(node.message_chain, bot))) + content_list.extend( + ( + await WecomMessageConverter.yiri2target( + node.message_chain, bot + ) + ) + ) else: - content_list.append({ - "type": "text", - "content": str(msg), - }) + content_list.append( + { + 'type': 'text', + 'content': str(msg), + } + ) return content_list @@ -67,14 +76,17 @@ class WecomMessageConverter(adapter.MessageConverter): platform_message.Source(id=message_id, time=datetime.datetime.now()) ) image_base64, image_format = await image.get_wecom_image_base64(pic_url=picurl) - yiri_msg_list.append(platform_message.Image(base64=f"data:image/{image_format};base64,{image_base64}")) + yiri_msg_list.append( + platform_message.Image( + base64=f'data:image/{image_format};base64,{image_base64}' + ) + ) chain = platform_message.MessageChain(yiri_msg_list) - + return chain class WecomEventConverter: - @staticmethod async def yiri2target( event: platform_events.Event, bot_account_id: int, bot: WecomClient @@ -85,18 +97,17 @@ class WecomEventConverter: pass if type(event) is platform_events.FriendMessage: - payload = { - "MsgType": "text", - "Content": '', - "FromUserName": event.sender.id, - "ToUserName": bot_account_id, - "CreateTime": int(datetime.datetime.now().timestamp()), - "AgentID": event.sender.nickname, + 'MsgType': 'text', + 'Content': '', + 'FromUserName': event.sender.id, + 'ToUserName': bot_account_id, + 'CreateTime': int(datetime.datetime.now().timestamp()), + 'AgentID': event.sender.nickname, } wecom_event = WecomEvent.from_payload(payload=payload) if not wecom_event: - raise ValueError("无法从 message_data 构造 WecomEvent 对象") + raise ValueError('无法从 message_data 构造 WecomEvent 对象') return wecom_event @@ -112,24 +123,24 @@ class WecomEventConverter: platform_events.FriendMessage: 转换后的 FriendMessage 对象。 """ # 转换消息链 - if event.type == "text": + if event.type == 'text': yiri_chain = await WecomMessageConverter.target2yiri( event.message, event.message_id ) friend = platform_entities.Friend( - id=f"u{event.user_id}", + id=f'u{event.user_id}', nickname=str(event.agent_id), - remark="", + remark='', ) return platform_events.FriendMessage( sender=friend, message_chain=yiri_chain, time=event.timestamp ) - elif event.type == "image": + elif event.type == 'image': friend = platform_entities.Friend( - id=f"u{event.user_id}", + id=f'u{event.user_id}', nickname=str(event.agent_id), - remark="", + remark='', ) yiri_chain = await WecomMessageConverter.target2yiri_image( @@ -142,7 +153,6 @@ class WecomEventConverter: class WecomAdapter(adapter.MessagePlatformAdapter): - bot: WecomClient ap: app.Application bot_account_id: str @@ -156,22 +166,22 @@ class WecomAdapter(adapter.MessagePlatformAdapter): self.ap = ap required_keys = [ - "corpid", - "secret", - "token", - "EncodingAESKey", - "contacts_secret", + 'corpid', + 'secret', + 'token', + 'EncodingAESKey', + 'contacts_secret', ] missing_keys = [key for key in required_keys if key not in config] if missing_keys: - raise ParamNotEnoughError("企业微信缺少相关配置项,请查看文档或联系管理员") + raise ParamNotEnoughError('企业微信缺少相关配置项,请查看文档或联系管理员') self.bot = WecomClient( - corpid=config["corpid"], - secret=config["secret"], - token=config["token"], - EncodingAESKey=config["EncodingAESKey"], - contacts_secret=config["contacts_secret"], + corpid=config['corpid'], + secret=config['secret'], + token=config['token'], + EncodingAESKey=config['EncodingAESKey'], + contacts_secret=config['contacts_secret'], ) async def reply_message( @@ -180,7 +190,6 @@ class WecomAdapter(adapter.MessagePlatformAdapter): message: platform_message.MessageChain, quote_origin: bool = False, ): - Wecom_event = await WecomEventConverter.yiri2target( message_source, self.bot_account_id, self.bot ) @@ -189,11 +198,15 @@ class WecomAdapter(adapter.MessagePlatformAdapter): # 删掉开头的u fixed_user_id = fixed_user_id[1:] for content in content_list: - if content["type"] == "text": - await self.bot.send_private_msg(fixed_user_id, Wecom_event.agent_id, content["content"]) - elif content["type"] == "image": - await self.bot.send_image(fixed_user_id, Wecom_event.agent_id, content["media_id"]) - + if content['type'] == 'text': + await self.bot.send_private_msg( + fixed_user_id, Wecom_event.agent_id, content['content'] + ) + elif content['type'] == 'image': + await self.bot.send_image( + fixed_user_id, Wecom_event.agent_id, content['media_id'] + ) + async def send_message( self, target_type: str, target_id: str, message: platform_message.MessageChain ): @@ -201,15 +214,17 @@ class WecomAdapter(adapter.MessagePlatformAdapter): 构造target_id的方式为前半部分为账户id,后半部分为agent_id,中间使用“|”符号隔开。 """ content_list = await WecomMessageConverter.yiri2target(message, self.bot) - parts = target_id.split("|") + parts = target_id.split('|') user_id = parts[0] agent_id = int(parts[1]) if target_type == 'person': for content in content_list: - if content["type"] == "text": - await self.bot.send_private_msg(user_id,agent_id,content["content"]) - if content["type"] == "image": - await self.bot.send_image(user_id,agent_id,content["media"]) + if content['type'] == 'text': + await self.bot.send_private_msg( + user_id, agent_id, content['content'] + ) + if content['type'] == 'image': + await self.bot.send_image(user_id, agent_id, content['media']) def register_listener( self, @@ -224,12 +239,12 @@ class WecomAdapter(adapter.MessagePlatformAdapter): return await callback( await self.event_converter.target2yiri(event), self ) - except: + except Exception: traceback.print_exc() if event_type == platform_events.FriendMessage: - self.bot.on_message("text")(on_message) - self.bot.on_message("image")(on_message) + self.bot.on_message('text')(on_message) + self.bot.on_message('image')(on_message) elif event_type == platform_events.GroupMessage: pass @@ -239,8 +254,8 @@ class WecomAdapter(adapter.MessagePlatformAdapter): await asyncio.sleep(1) await self.bot.run_task( - host=self.config["host"], - port=self.config["port"], + host=self.config['host'], + port=self.config['port'], shutdown_trigger=shutdown_trigger_placeholder, ) @@ -250,6 +265,8 @@ class WecomAdapter(adapter.MessagePlatformAdapter): async def unregister_listener( self, event_type: type, - callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, MessagePlatformAdapter], None + ], ): return super().unregister_listener(event_type, callback) diff --git a/pkg/platform/types/base.py b/pkg/platform/types/base.py index ce87d36c..9e31bafe 100644 --- a/pkg/platform/types/base.py +++ b/pkg/platform/types/base.py @@ -1,4 +1,3 @@ - from typing import Dict, List, Type import pydantic.v1.main as pdm @@ -25,14 +24,18 @@ class PlatformBaseModel(BaseModel, metaclass=PlatformMetaclass): 2. 允许通过别名访问字段。 3. 自动生成小驼峰风格的别名。 """ + def __init__(self, *args, **kwargs): """""" super().__init__(*args, **kwargs) def __repr__(self) -> str: - return self.__class__.__name__ + '(' + ', '.join( - (f'{k}={repr(v)}' for k, v in self.__dict__.items() if v) - ) + ')' + return ( + self.__class__.__name__ + + '(' + + ', '.join((f'{k}={repr(v)}' for k, v in self.__dict__.items() if v)) + + ')' + ) class Config: extra = 'allow' @@ -42,6 +45,7 @@ class PlatformBaseModel(BaseModel, metaclass=PlatformMetaclass): class PlatformIndexedMetaclass(PlatformMetaclass): """可以通过子类名获取子类的类的元类。""" + __indexedbases__: List[Type['PlatformIndexedModel']] = [] __indexedmodel__ = None @@ -69,6 +73,7 @@ class PlatformIndexedMetaclass(PlatformMetaclass): class PlatformIndexedModel(PlatformBaseModel, metaclass=PlatformIndexedMetaclass): """可以通过子类名获取子类的类。""" + __indexes__: Dict[str, Type['PlatformIndexedModel']] @classmethod @@ -86,7 +91,7 @@ class PlatformIndexedModel(PlatformBaseModel, metaclass=PlatformIndexedMetaclass if not (type_ and issubclass(type_, cls)): raise ValueError(f'`{name}` 不是 `{cls.__name__}` 的子类!') return type_ - except AttributeError as e: + except AttributeError: raise ValueError(f'`{name}` 不是 `{cls.__name__}` 的子类!') from None @classmethod diff --git a/pkg/platform/types/entities.py b/pkg/platform/types/entities.py index 33fbefe9..d989ffce 100644 --- a/pkg/platform/types/entities.py +++ b/pkg/platform/types/entities.py @@ -2,6 +2,7 @@ """ 此模块提供实体和配置项模型。 """ + import abc from datetime import datetime from enum import Enum @@ -12,8 +13,10 @@ import pydantic.v1 as pydantic class Entity(pydantic.BaseModel): """实体,表示一个用户或群。""" + id: int """ID。""" + @abc.abstractmethod def get_name(self) -> str: """名称。""" @@ -21,31 +24,35 @@ class Entity(pydantic.BaseModel): class Friend(Entity): """私聊对象。""" + id: typing.Union[int, str] """ID。""" nickname: typing.Optional[str] """昵称。""" remark: typing.Optional[str] """备注。""" + def get_name(self) -> str: return self.nickname or self.remark or '' - class Permission(str, Enum): """群成员身份权限。""" - Member = "MEMBER" + + Member = 'MEMBER' """成员。""" - Administrator = "ADMINISTRATOR" + Administrator = 'ADMINISTRATOR' """管理员。""" - Owner = "OWNER" + Owner = 'OWNER' """群主。""" + def __repr__(self) -> str: return repr(self.value) class Group(Entity): """群。""" + id: typing.Union[int, str] """群号。""" name: str @@ -59,6 +66,7 @@ class Group(Entity): class GroupMember(Entity): """群成员。""" + id: typing.Union[int, str] """群员 ID。""" member_name: str diff --git a/pkg/platform/types/events.py b/pkg/platform/types/events.py index 40507315..1a724beb 100644 --- a/pkg/platform/types/events.py +++ b/pkg/platform/types/events.py @@ -2,8 +2,7 @@ """ 此模块提供事件模型。 """ -from datetime import datetime -from enum import Enum + import typing import pydantic.v1 as pydantic @@ -18,15 +17,23 @@ class Event(pydantic.BaseModel): Args: type: 事件名。 """ + type: str """事件名。""" + def __repr__(self): - return self.__class__.__name__ + '(' + ', '.join( - ( - f'{k}={repr(v)}' - for k, v in self.__dict__.items() if k != 'type' and v + return ( + self.__class__.__name__ + + '(' + + ', '.join( + ( + f'{k}={repr(v)}' + for k, v in self.__dict__.items() + if k != 'type' and v + ) ) - ) + ')' + + ')' + ) @classmethod def parse_subtype(cls, obj: dict) -> 'Event': @@ -52,6 +59,7 @@ class MessageEvent(Event): type: 事件名。 message_chain: 消息内容。 """ + type: str """事件名。""" message_chain: platform_message.MessageChain @@ -74,6 +82,7 @@ class FriendMessage(MessageEvent): sender: 发送消息的好友。 message_chain: 消息内容。 """ + type: str = 'FriendMessage' """事件名。""" sender: platform_entities.Friend @@ -90,12 +99,14 @@ class GroupMessage(MessageEvent): sender: 发送消息的群成员。 message_chain: 消息内容。 """ + type: str = 'GroupMessage' """事件名。""" sender: platform_entities.GroupMember """发送消息的群成员。""" message_chain: platform_message.MessageChain """消息内容。""" + @property def group(self) -> platform_entities.Group: return self.sender.group diff --git a/pkg/platform/types/message.py b/pkg/platform/types/message.py index b99a28b3..529ce4c1 100644 --- a/pkg/platform/types/message.py +++ b/pkg/platform/types/message.py @@ -1,7 +1,6 @@ import itertools import logging from datetime import datetime -from enum import Enum from pathlib import Path import typing @@ -16,6 +15,7 @@ logger = logging.getLogger(__name__) class MessageComponentMetaclass(PlatformIndexedMetaclass): """消息组件元类。""" + __message_component__ = None def __new__(cls, name, bases, attrs, **kwargs): @@ -41,18 +41,26 @@ class MessageComponentMetaclass(PlatformIndexedMetaclass): class MessageComponent(PlatformIndexedModel, metaclass=MessageComponentMetaclass): """消息组件。""" + type: str """消息组件类型。""" + def __str__(self): return '' def __repr__(self): - return self.__class__.__name__ + '(' + ', '.join( - ( - f'{k}={repr(v)}' - for k, v in self.__dict__.items() if k != 'type' and v + return ( + self.__class__.__name__ + + '(' + + ', '.join( + ( + f'{k}={repr(v)}' + for k, v in self.__dict__.items() + if k != 'type' and v + ) ) - ) + ')' + + ')' + ) def __init__(self, *args, **kwargs): # 解析参数列表,将位置参数转化为具名参数 @@ -63,7 +71,9 @@ class MessageComponent(PlatformIndexedModel, metaclass=MessageComponentMetaclass ) for name, value in zip(parameter_names, args): if name in kwargs: - raise TypeError(f'在 `{self.type}` 中,具名参数 `{name}` 与位置参数重复。') + raise TypeError( + f'在 `{self.type}` 中,具名参数 `{name}` 与位置参数重复。' + ) kwargs[name] = value super().__init__(**kwargs) @@ -117,6 +127,7 @@ class MessageChain(PlatformBaseModel): ``` """ + __root__: typing.List[MessageComponent] @staticmethod @@ -131,10 +142,10 @@ class MessageChain(PlatformBaseModel): result.append(Plain(msg)) else: raise TypeError( - f"消息链中元素需为 dict 或 str 或 MessageComponent,当前类型:{type(msg)}" + f'消息链中元素需为 dict 或 str 或 MessageComponent,当前类型:{type(msg)}' ) return result - + @pydantic.validator('__root__', always=True, pre=True) def _parse_component(cls, msg_chain): if isinstance(msg_chain, (str, MessageComponent)): @@ -157,7 +168,7 @@ class MessageChain(PlatformBaseModel): super().__init__(__root__=__root__) def __str__(self): - return "".join(str(component) for component in self.__root__) + return ''.join(str(component) for component in self.__root__) def __repr__(self): return f'{self.__class__.__name__}({self.__root__!r})' @@ -165,8 +176,9 @@ class MessageChain(PlatformBaseModel): def __iter__(self): yield from self.__root__ - def get_first(self, - t: typing.Type[TMessageComponent]) -> typing.Optional[TMessageComponent]: + def get_first( + self, t: typing.Type[TMessageComponent] + ) -> typing.Optional[TMessageComponent]: """获取消息链中第一个符合类型的消息组件。""" for component in self: if isinstance(component, t): @@ -174,35 +186,40 @@ class MessageChain(PlatformBaseModel): return None @typing.overload - def __getitem__(self, index: int) -> MessageComponent: - ... + def __getitem__(self, index: int) -> MessageComponent: ... @typing.overload - def __getitem__(self, index: slice) -> typing.List[MessageComponent]: - ... + def __getitem__(self, index: slice) -> typing.List[MessageComponent]: ... @typing.overload - def __getitem__(self, - index: typing.Type[TMessageComponent]) -> typing.List[TMessageComponent]: - ... + def __getitem__( + self, index: typing.Type[TMessageComponent] + ) -> typing.List[TMessageComponent]: ... @typing.overload def __getitem__( self, index: typing.Tuple[typing.Type[TMessageComponent], int] - ) -> typing.List[TMessageComponent]: - ... + ) -> typing.List[TMessageComponent]: ... def __getitem__( - self, index: typing.Union[int, slice, typing.Type[TMessageComponent], - typing.Tuple[typing.Type[TMessageComponent], int]] - ) -> typing.Union[MessageComponent, typing.List[MessageComponent], - typing.List[TMessageComponent]]: + self, + index: typing.Union[ + int, + slice, + typing.Type[TMessageComponent], + typing.Tuple[typing.Type[TMessageComponent], int], + ], + ) -> typing.Union[ + MessageComponent, typing.List[MessageComponent], typing.List[TMessageComponent] + ]: return self.get(index) def __setitem__( - self, key: typing.Union[int, slice], - value: typing.Union[MessageComponent, str, typing.Iterable[typing.Union[MessageComponent, - str]]] + self, + key: typing.Union[int, slice], + value: typing.Union[ + MessageComponent, str, typing.Iterable[typing.Union[MessageComponent, str]] + ], ): if isinstance(value, str): value = Plain(value) @@ -217,8 +234,10 @@ class MessageChain(PlatformBaseModel): return reversed(self.__root__) def has( - self, sub: typing.Union[MessageComponent, typing.Type[MessageComponent], - 'MessageChain', str] + self, + sub: typing.Union[ + MessageComponent, typing.Type[MessageComponent], 'MessageChain', str + ], ) -> bool: """判断消息链中: 1. 是否有某个消息组件。 @@ -242,7 +261,7 @@ class MessageChain(PlatformBaseModel): if i == sub: return True return False - raise TypeError(f"类型不匹配,当前类型:{type(sub)}") + raise TypeError(f'类型不匹配,当前类型:{type(sub)}') def __contains__(self, sub) -> bool: return self.has(sub) @@ -293,7 +312,7 @@ class MessageChain(PlatformBaseModel): self, x: typing.Union[MessageComponent, typing.Type[MessageComponent]], i: int = 0, - j: int = -1 + j: int = -1, ) -> int: """返回 x 在消息链中首次出现项的索引号(索引号在 i 或其后且在 j 之前)。 @@ -323,12 +342,14 @@ class MessageChain(PlatformBaseModel): for index in range(i, j): if type(self[index]) is x: return index - raise ValueError("消息链中不存在该类型的组件。") + raise ValueError('消息链中不存在该类型的组件。') if isinstance(x, MessageComponent): return self.__root__.index(x, i, j) - raise TypeError(f"类型不匹配,当前类型:{type(x)}") + raise TypeError(f'类型不匹配,当前类型:{type(x)}') - def count(self, x: typing.Union[MessageComponent, typing.Type[MessageComponent]]) -> int: + def count( + self, x: typing.Union[MessageComponent, typing.Type[MessageComponent]] + ) -> int: """返回消息链中 x 出现的次数。 Args: @@ -342,7 +363,7 @@ class MessageChain(PlatformBaseModel): return sum(1 for i in self if type(i) is x) if isinstance(x, MessageComponent): return self.__root__.count(x) - raise TypeError(f"类型不匹配,当前类型:{type(x)}") + raise TypeError(f'类型不匹配,当前类型:{type(x)}') def extend(self, x: typing.Iterable[typing.Union[MessageComponent, str]]): """将另一个消息链中的元素添加到消息链末尾。 @@ -394,7 +415,7 @@ class MessageChain(PlatformBaseModel): def exclude( self, x: typing.Union[MessageComponent, typing.Type[MessageComponent]], - count: int = -1 + count: int = -1, ) -> 'MessageChain': """返回移除指定元素或指定类型的元素后剩余的消息链。 @@ -405,6 +426,7 @@ class MessageChain(PlatformBaseModel): Returns: MessageChain: 剩余的消息链。 """ + def _exclude(): nonlocal count x_is_type = isinstance(x, type) @@ -423,8 +445,7 @@ class MessageChain(PlatformBaseModel): @classmethod def join(cls, *args: typing.Iterable[typing.Union[str, MessageComponent]]): return cls( - Plain(c) if isinstance(c, str) else c - for c in itertools.chain(*args) + Plain(c) if isinstance(c, str) else c for c in itertools.chain(*args) ) @property @@ -439,14 +460,19 @@ class MessageChain(PlatformBaseModel): return source.id if source else -1 -TMessage = typing.Union[MessageChain, typing.Iterable[typing.Union[MessageComponent, str]], - MessageComponent, str] +TMessage = typing.Union[ + MessageChain, + typing.Iterable[typing.Union[MessageComponent, str]], + MessageComponent, + str, +] """可以转化为 MessageChain 的类型。""" class Source(MessageComponent): """源。包含消息的基本信息。""" - type: str = "Source" + + type: str = 'Source' """消息组件类型。""" id: typing.Union[int, str] """消息的识别号,用于引用回复(Source 类型永远为 MessageChain 的第一个元素)。""" @@ -456,10 +482,12 @@ class Source(MessageComponent): class Plain(MessageComponent): """纯文本。""" - type: str = "Plain" + + type: str = 'Plain' """消息组件类型。""" text: str """文字消息。""" + def __str__(self): return self.text @@ -469,7 +497,8 @@ class Plain(MessageComponent): class Quote(MessageComponent): """引用。""" - type: str = "Quote" + + type: str = 'Quote' """消息组件类型。""" id: typing.Optional[int] = None """被引用回复的原消息的 message_id。""" @@ -482,37 +511,42 @@ class Quote(MessageComponent): origin: MessageChain """被引用回复的原消息的消息链对象。""" - @pydantic.validator("origin", always=True, pre=True) + @pydantic.validator('origin', always=True, pre=True) def origin_formater(cls, v): return MessageChain.parse_obj(v) class At(MessageComponent): """At某人。""" - type: str = "At" + + type: str = 'At' """消息组件类型。""" target: typing.Union[int, str] """群员 ID。""" display: typing.Optional[str] = None """At时显示的文字,发送消息时无效,自动使用群名片。""" + def __eq__(self, other): return isinstance(other, At) and self.target == other.target def __str__(self): - return f"@{self.display or self.target}" + return f'@{self.display or self.target}' class AtAll(MessageComponent): """At全体。""" - type: str = "AtAll" + + type: str = 'AtAll' """消息组件类型。""" + def __str__(self): - return "@全体成员" + return '@全体成员' class Image(MessageComponent): """图片。""" - type: str = "Image" + + type: str = 'Image' """消息组件类型。""" image_id: typing.Optional[str] = None """图片的 image_id,不为空时将忽略 url 属性。""" @@ -522,10 +556,13 @@ class Image(MessageComponent): """图片的路径,发送本地图片。""" base64: typing.Optional[str] = None """图片的 Base64 编码。""" + def __eq__(self, other): - return isinstance( - other, Image - ) and self.type == other.type and self.uuid == other.uuid + return ( + isinstance(other, Image) + and self.type == other.type + and self.uuid == other.uuid + ) def __str__(self): return '[图片]' @@ -537,7 +574,7 @@ class Image(MessageComponent): try: return str(Path(path).resolve(strict=True)) except FileNotFoundError: - raise ValueError(f"无效路径:{path}") + raise ValueError(f'无效路径:{path}') else: return path @@ -554,7 +591,7 @@ class Image(MessageComponent): self, filename: typing.Union[str, Path, None] = None, directory: typing.Union[str, Path, None] = None, - determine_type: bool = True + determine_type: bool = True, ): """下载图片到本地。 @@ -568,6 +605,7 @@ class Image(MessageComponent): return import httpx + async with httpx.AsyncClient() as client: response = await client.get(self.url) response.raise_for_status() @@ -577,19 +615,20 @@ class Image(MessageComponent): path = Path(filename) if determine_type: import imghdr - path = path.with_suffix( - '.' + str(imghdr.what(None, content)) - ) + + path = path.with_suffix('.' + str(imghdr.what(None, content))) path.parent.mkdir(parents=True, exist_ok=True) elif directory: import imghdr + path = Path(directory) path.mkdir(parents=True, exist_ok=True) path = path / f'{self.uuid}.{imghdr.what(None, content)}' else: - raise ValueError("请指定文件路径或文件夹路径!") + raise ValueError('请指定文件路径或文件夹路径!') import aiofiles + async with aiofiles.open(path, 'wb') as f: await f.write(content) @@ -600,7 +639,7 @@ class Image(MessageComponent): cls, filename: typing.Union[str, Path, None] = None, content: typing.Optional[bytes] = None, - ) -> "Image": + ) -> 'Image': """从本地文件路径加载图片,以 base64 的形式传递。 Args: @@ -615,16 +654,18 @@ class Image(MessageComponent): elif filename: path = Path(filename) import aiofiles + async with aiofiles.open(path, 'rb') as f: content = await f.read() else: - raise ValueError("请指定图片路径或图片内容!") + raise ValueError('请指定图片路径或图片内容!') import base64 + img = cls(base64=base64.b64encode(content).decode()) return img @classmethod - def from_unsafe_path(cls, path: typing.Union[str, Path]) -> "Image": + def from_unsafe_path(cls, path: typing.Union[str, Path]) -> 'Image': """从不安全的路径加载图片。 Args: @@ -638,7 +679,8 @@ class Image(MessageComponent): class Unknown(MessageComponent): """未知。""" - type: str = "Unknown" + + type: str = 'Unknown' """消息组件类型。""" text: str """文本。""" @@ -646,7 +688,8 @@ class Unknown(MessageComponent): class Voice(MessageComponent): """语音。""" - type: str = "Voice" + + type: str = 'Voice' """消息组件类型。""" voice_id: typing.Optional[str] = None """语音的 voice_id,不为空时将忽略 url 属性。""" @@ -658,6 +701,7 @@ class Voice(MessageComponent): """语音的 Base64 编码。""" length: typing.Optional[int] = None """语音的长度,单位为秒。""" + @pydantic.validator('path') def validate_path(cls, path: typing.Optional[str]): """修复 path 参数的行为,使之相对于 LangBot 的启动路径。""" @@ -665,7 +709,7 @@ class Voice(MessageComponent): try: return str(Path(path).resolve(strict=True)) except FileNotFoundError: - raise ValueError(f"无效路径:{path}") + raise ValueError(f'无效路径:{path}') else: return path @@ -675,7 +719,7 @@ class Voice(MessageComponent): async def download( self, filename: typing.Union[str, Path, None] = None, - directory: typing.Union[str, Path, None] = None + directory: typing.Union[str, Path, None] = None, ): """下载语音到本地。 @@ -688,6 +732,7 @@ class Voice(MessageComponent): return import httpx + async with httpx.AsyncClient() as client: response = await client.get(self.url) response.raise_for_status() @@ -701,9 +746,10 @@ class Voice(MessageComponent): path.mkdir(parents=True, exist_ok=True) path = path / f'{self.voice_id}.silk' else: - raise ValueError("请指定文件路径或文件夹路径!") + raise ValueError('请指定文件路径或文件夹路径!') import aiofiles + async with aiofiles.open(path, 'wb') as f: await f.write(content) @@ -712,7 +758,7 @@ class Voice(MessageComponent): cls, filename: typing.Union[str, Path, None] = None, content: typing.Optional[bytes] = None, - ) -> "Voice": + ) -> 'Voice': """从本地文件路径加载语音,以 base64 的形式传递。 Args: @@ -724,17 +770,20 @@ class Voice(MessageComponent): if filename: path = Path(filename) import aiofiles + async with aiofiles.open(path, 'rb') as f: content = await f.read() else: - raise ValueError("请指定语音路径或语音内容!") + raise ValueError('请指定语音路径或语音内容!') import base64 + img = cls(base64=base64.b64encode(content).decode()) return img class ForwardMessageNode(pydantic.BaseModel): """合并转发中的一条消息。""" + sender_id: typing.Optional[typing.Union[int, str]] = None """发送人ID。""" sender_name: typing.Optional[str] = None @@ -745,6 +794,7 @@ class ForwardMessageNode(pydantic.BaseModel): """消息的 message_id。""" time: typing.Optional[datetime] = None """发送时间。""" + @pydantic.validator('message_chain', check_fields=False) def _validate_message_chain(cls, value: typing.Union[MessageChain, list]): if isinstance(value, list): @@ -753,7 +803,9 @@ class ForwardMessageNode(pydantic.BaseModel): @classmethod def create( - cls, sender: typing.Union[platform_entities.Friend, platform_entities.GroupMember], message: MessageChain + cls, + sender: typing.Union[platform_entities.Friend, platform_entities.GroupMember], + message: MessageChain, ) -> 'ForwardMessageNode': """从消息链生成转发消息。 @@ -765,28 +817,28 @@ class ForwardMessageNode(pydantic.BaseModel): ForwardMessageNode: 生成的一条消息。 """ return ForwardMessageNode( - sender_id=sender.id, - sender_name=sender.get_name(), - message_chain=message + sender_id=sender.id, sender_name=sender.get_name(), message_chain=message ) class ForwardMessageDiaplay(pydantic.BaseModel): - title: str = "群聊的聊天记录" - brief: str = "[聊天记录]" - source: str = "聊天记录" + title: str = '群聊的聊天记录' + brief: str = '[聊天记录]' + source: str = '聊天记录' preview: typing.List[str] = [] - summary: str = "查看x条转发消息" + summary: str = '查看x条转发消息' class Forward(MessageComponent): """合并转发。""" - type: str = "Forward" + + type: str = 'Forward' """消息组件类型。""" display: ForwardMessageDiaplay """显示信息""" node_list: typing.List[ForwardMessageNode] """转发消息节点列表。""" + def __init__(self, *args, **kwargs): if len(args) == 1: self.node_list = args[0] @@ -799,7 +851,8 @@ class Forward(MessageComponent): class File(MessageComponent): """文件。""" - type: str = "File" + + type: str = 'File' """消息组件类型。""" id: str """文件识别 ID。""" @@ -807,6 +860,6 @@ class File(MessageComponent): """文件名称。""" size: int """文件大小。""" + def __str__(self): return f'[文件]{self.name}' - diff --git a/pkg/plugin/__init__.py b/pkg/plugin/__init__.py index c543161a..f6bf97d7 100644 --- a/pkg/plugin/__init__.py +++ b/pkg/plugin/__init__.py @@ -1,4 +1,4 @@ """插件支持包 包含插件基类、插件宿主以及部分API接口 -""" \ No newline at end of file +""" diff --git a/pkg/plugin/context.py b/pkg/plugin/context.py index fc06b9ec..cc95adaa 100644 --- a/pkg/plugin/context.py +++ b/pkg/plugin/context.py @@ -14,13 +14,10 @@ from ..platform import adapter as platform_adapter def register( - name: str, - description: str, - version: str, - author: str + name: str, description: str, version: str, author: str ) -> typing.Callable[[typing.Type[BasePlugin]], typing.Type[BasePlugin]]: """注册插件类 - + 使用示例: @register( @@ -34,15 +31,16 @@ def register( """ pass + def handler( - event: typing.Type[events.BaseEventModel] + event: typing.Type[events.BaseEventModel], ) -> typing.Callable[[typing.Callable], typing.Callable]: """注册事件监听器 - + 使用示例: class MyPlugin(BasePlugin): - + @handler(NormalMessageResponded) async def on_normal_message_responded(self, ctx: EventContext): pass @@ -51,14 +49,14 @@ def handler( def llm_func( - name: str=None, + name: str = None, ) -> typing.Callable: """注册内容函数 - + 使用示例: class MyPlugin(BasePlugin): - + @llm_func("access_the_web_page") async def _(self, query, url: str, brief_len: int): \"""Call this function to search about the question before you answer any questions. @@ -98,7 +96,7 @@ class BasePlugin(metaclass=abc.ABCMeta): async def initialize(self): """初始化阶段被调用""" pass - + async def destroy(self): """释放/禁用插件时被调用""" pass @@ -123,12 +121,12 @@ class APIHost: def get_platform_adapters(self) -> list[platform_adapter.MessagePlatformAdapter]: """获取已启用的消息平台适配器列表 - + Returns: list[platform.adapter.MessageSourceAdapter]: 已启用的消息平台适配器列表 """ return self.ap.platform_mgr.get_running_adapters() - + async def send_active_message( self, adapter: platform_adapter.MessagePlatformAdapter, @@ -137,7 +135,7 @@ class APIHost: message: platform_message.MessageChain, ): """发送主动消息 - + Args: adapter (platform.adapter.MessageSourceAdapter): 消息平台适配器对象,调用 host.get_platform_adapters() 获取并取用其中某个 target_type (str): 目标类型,`person`或`group` @@ -153,7 +151,7 @@ class APIHost: def require_ver( self, ge: str, - le: str='v999.999.999', + le: str = 'v999.999.999', ) -> bool: """插件版本要求装饰器 @@ -164,16 +162,23 @@ class APIHost: Returns: bool: 是否满足要求, False时为无法获取版本号,True时为满足要求,报错为不满足要求 """ - langbot_version = "" + langbot_version = '' try: - langbot_version = self.ap.ver_mgr.get_current_version() # 从updater模块获取版本号 - except: + langbot_version = ( + self.ap.ver_mgr.get_current_version() + ) # 从updater模块获取版本号 + except Exception: return False - if self.ap.ver_mgr.compare_version_str(langbot_version, ge) < 0 or \ - (self.ap.ver_mgr.compare_version_str(langbot_version, le) > 0): - raise Exception("LangBot 版本不满足要求,某些功能(可能是由插件提供的)无法正常使用。(要求版本:{}-{},但当前版本:{})".format(ge, le, langbot_version)) + if self.ap.ver_mgr.compare_version_str(langbot_version, ge) < 0 or ( + self.ap.ver_mgr.compare_version_str(langbot_version, le) > 0 + ): + raise Exception( + 'LangBot 版本不满足要求,某些功能(可能是由插件提供的)无法正常使用。(要求版本:{}-{},但当前版本:{})'.format( + ge, le, langbot_version + ) + ) return True @@ -220,36 +225,30 @@ class EventContext: if key not in self.__return_value__: self.__return_value__[key] = [] self.__return_value__[key].append(ret) - + async def reply(self, message_chain: platform_message.MessageChain): """回复此次消息请求 - + Args: message_chain (platform.types.MessageChain): 源平台的消息链,若用户使用的不是源平台适配器,程序也能自动转换为目标平台消息链 """ # TODO 添加 at_sender 和 quote_origin 参数 await self.event.query.adapter.reply_message( - message_source=self.event.query.message_event, - message=message_chain + message_source=self.event.query.message_event, message=message_chain ) - + async def send_message( - self, - target_type: str, - target_id: str, - message: platform_message.MessageChain + self, target_type: str, target_id: str, message: platform_message.MessageChain ): """主动发送消息 - + Args: target_type (str): 目标类型,`person`或`group` target_id (str): 目标ID message (platform.types.MessageChain): 源平台的消息链,若用户使用的不是源平台适配器,程序也能自动转换为目标平台消息链 """ await self.event.query.adapter.send_message( - target_type=target_type, - target_id=target_id, - message=message + target_type=target_type, target_id=target_id, message=message ) def prevent_postorder(self): @@ -281,10 +280,8 @@ class EventContext: def is_prevented_postorder(self): """是否阻止后序插件执行""" return self.__prevent_postorder__ - def __init__(self, host: APIHost, event: events.BaseEventModel): - self.eid = EventContext.eid self.host = host self.event = event @@ -297,16 +294,16 @@ class EventContext: class RuntimeContainerStatus(enum.Enum): """插件容器状态""" - MOUNTED = "mounted" + MOUNTED = 'mounted' """已加载进内存,所有位于运行时记录中的 RuntimeContainer 至少是这个状态""" - INITIALIZED = "initialized" + INITIALIZED = 'initialized' """已初始化""" class RuntimeContainer(pydantic.BaseModel): """运行时的插件容器 - + 运行期间存储单个插件的信息 """ @@ -352,9 +349,10 @@ class RuntimeContainer(pydantic.BaseModel): plugin_inst: typing.Optional[BasePlugin] = None """插件实例""" - event_handlers: dict[typing.Type[events.BaseEventModel], typing.Callable[ - [BasePlugin, EventContext], typing.Awaitable[None] - ]] = {} + event_handlers: dict[ + typing.Type[events.BaseEventModel], + typing.Callable[[BasePlugin, EventContext], typing.Awaitable[None]], + ] = {} """事件处理器""" tools: list[tools_entities.LLMFunction] = [] @@ -378,7 +376,7 @@ class RuntimeContainer(pydantic.BaseModel): 'pkg_path': self.pkg_path, 'enabled': self.enabled, 'priority': self.priority, - "config_schema": self.config_schema, + 'config_schema': self.config_schema, 'event_handlers': { event_name.__name__: handler.__name__ for event_name, handler in self.event_handlers.items() diff --git a/pkg/plugin/errors.py b/pkg/plugin/errors.py index bd6199e3..8da223db 100644 --- a/pkg/plugin/errors.py +++ b/pkg/plugin/errors.py @@ -2,7 +2,6 @@ from __future__ import annotations class PluginSystemError(Exception): - message: str def __init__(self, message: str): @@ -10,15 +9,13 @@ class PluginSystemError(Exception): def __str__(self): return self.message - + class PluginNotFoundError(PluginSystemError): - def __init__(self, message: str): - super().__init__(f"未找到插件: {message}") + super().__init__(f'未找到插件: {message}') class PluginInstallerError(PluginSystemError): - def __init__(self, message: str): - super().__init__(f"安装器操作错误: {message}") + super().__init__(f'安装器操作错误: {message}') diff --git a/pkg/plugin/events.py b/pkg/plugin/events.py index 152ac39f..61e84714 100644 --- a/pkg/plugin/events.py +++ b/pkg/plugin/events.py @@ -27,7 +27,7 @@ class PersonMessageReceived(BaseEventModel): launcher_id: typing.Union[int, str] """发起对象ID(群号/QQ号)""" - + sender_id: typing.Union[int, str] """发送者ID(QQ号)""" @@ -40,7 +40,7 @@ class GroupMessageReceived(BaseEventModel): launcher_type: str launcher_id: typing.Union[int, str] - + sender_id: typing.Union[int, str] message_chain: platform_message.MessageChain @@ -52,7 +52,7 @@ class PersonNormalMessageReceived(BaseEventModel): launcher_type: str launcher_id: typing.Union[int, str] - + sender_id: typing.Union[int, str] text_message: str @@ -70,7 +70,7 @@ class PersonCommandSent(BaseEventModel): launcher_type: str launcher_id: typing.Union[int, str] - + sender_id: typing.Union[int, str] command: str @@ -94,7 +94,7 @@ class GroupNormalMessageReceived(BaseEventModel): launcher_type: str launcher_id: typing.Union[int, str] - + sender_id: typing.Union[int, str] text_message: str @@ -112,7 +112,7 @@ class GroupCommandSent(BaseEventModel): launcher_type: str launcher_id: typing.Union[int, str] - + sender_id: typing.Union[int, str] command: str @@ -136,7 +136,7 @@ class NormalMessageResponded(BaseEventModel): launcher_type: str launcher_id: typing.Union[int, str] - + sender_id: typing.Union[int, str] session: core_entities.Session diff --git a/pkg/plugin/host.py b/pkg/plugin/host.py index 2868875d..0adb0078 100644 --- a/pkg/plugin/host.py +++ b/pkg/plugin/host.py @@ -2,8 +2,8 @@ # 请从 pkg.plugin.context 引入 BasePlugin, EventContext 和 APIHost # 最早将于 v3.4 移除此模块 -from . events import * -from . context import EventContext, APIHost as PluginHost +from .events import * + def emit(*args, **kwargs): - print('插件调用了已弃用的函数 pkg.plugin.host.emit()') \ No newline at end of file + print('插件调用了已弃用的函数 pkg.plugin.host.emit()') diff --git a/pkg/plugin/installer.py b/pkg/plugin/installer.py index b9ffab8b..159967dc 100644 --- a/pkg/plugin/installer.py +++ b/pkg/plugin/installer.py @@ -1,6 +1,5 @@ from __future__ import annotations -import typing import abc from ..core import app, taskmgr @@ -23,8 +22,7 @@ class PluginInstaller(metaclass=abc.ABCMeta): plugin_source: str, task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(), ): - """安装插件 - """ + """安装插件""" raise NotImplementedError @abc.abstractmethod @@ -33,17 +31,15 @@ class PluginInstaller(metaclass=abc.ABCMeta): plugin_name: str, task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(), ): - """卸载插件 - """ + """卸载插件""" raise NotImplementedError @abc.abstractmethod async def update_plugin( self, plugin_name: str, - plugin_source: str=None, + plugin_source: str = None, task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(), ): - """更新插件 - """ + """更新插件""" raise NotImplementedError diff --git a/pkg/plugin/installers/github.py b/pkg/plugin/installers/github.py index ff36cb5b..a867b04d 100644 --- a/pkg/plugin/installers/github.py +++ b/pkg/plugin/installers/github.py @@ -2,7 +2,6 @@ from __future__ import annotations import re import os -import shutil import zipfile import ssl import certifi @@ -18,33 +17,37 @@ from ...core import taskmgr class GitHubRepoInstaller(installer.PluginInstaller): - """GitHub仓库插件安装器 - """ + """GitHub仓库插件安装器""" def get_github_plugin_repo_label(self, repo_url: str) -> list[str]: """获取username, repo""" repo = re.findall( - r"(?:https?://github\.com/|git@github\.com:)([^/]+/[^/]+?)(?:\.git|/|$)", + r'(?:https?://github\.com/|git@github\.com:)([^/]+/[^/]+?)(?:\.git|/|$)', repo_url, ) if len(repo) > 0: - return repo[0].split("/") + return repo[0].split('/') else: return None - async def download_plugin_source_code(self, repo_url: str, target_path: str, task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder()) -> str: + async def download_plugin_source_code( + self, + repo_url: str, + target_path: str, + task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(), + ) -> str: """下载插件源码(全异步)""" repo = self.get_github_plugin_repo_label(repo_url) if repo is None: raise errors.PluginInstallerError('仅支持GitHub仓库地址') - + target_path += repo[1] - self.ap.logger.debug("正在下载源码...") - task_context.trace("下载源码...", "download-plugin-source-code") - - zipball_url = f"https://api.github.com/repos/{'/'.join(repo)}/zipball/HEAD" + self.ap.logger.debug('正在下载源码...') + task_context.trace('下载源码...', 'download-plugin-source-code') + + zipball_url = f'https://api.github.com/repos/{"/".join(repo)}/zipball/HEAD' zip_resp: bytes = None - + # 创建自定义SSL上下文,使用certifi提供的根证书 ssl_context = ssl.create_default_context(cafile=certifi.where()) @@ -52,41 +55,44 @@ class GitHubRepoInstaller(installer.PluginInstaller): async with session.get( url=zipball_url, timeout=aiohttp.ClientTimeout(total=300), - ssl=ssl_context # 使用自定义SSL上下文来验证证书 + ssl=ssl_context, # 使用自定义SSL上下文来验证证书 ) as resp: if resp.status != 200: - raise errors.PluginInstallerError(f"下载源码失败: {await resp.text()}") + raise errors.PluginInstallerError( + f'下载源码失败: {await resp.text()}' + ) zip_resp = await resp.read() - - if await aiofiles_os.path.exists("temp/" + target_path): - await aioshutil.rmtree("temp/" + target_path) + + if await aiofiles_os.path.exists('temp/' + target_path): + await aioshutil.rmtree('temp/' + target_path) if await aiofiles_os.path.exists(target_path): await aioshutil.rmtree(target_path) - await aiofiles_os.makedirs("temp/" + target_path) + await aiofiles_os.makedirs('temp/' + target_path) - async with aiofiles.open("temp/" + target_path + "/source.zip", "wb") as f: + async with aiofiles.open('temp/' + target_path + '/source.zip', 'wb') as f: await f.write(zip_resp) - self.ap.logger.debug("解压中...") - task_context.trace("解压中...", "unzip-plugin-source-code") - - with zipfile.ZipFile("temp/" + target_path + "/source.zip", "r") as zip_ref: - zip_ref.extractall("temp/" + target_path) - await aiofiles_os.remove("temp/" + target_path + "/source.zip") + self.ap.logger.debug('解压中...') + task_context.trace('解压中...', 'unzip-plugin-source-code') + + with zipfile.ZipFile('temp/' + target_path + '/source.zip', 'r') as zip_ref: + zip_ref.extractall('temp/' + target_path) + await aiofiles_os.remove('temp/' + target_path + '/source.zip') import glob - unzip_dir = glob.glob("temp/" + target_path + "/*")[0] - await aioshutil.copytree(unzip_dir, target_path + "/") + + unzip_dir = glob.glob('temp/' + target_path + '/*')[0] + await aioshutil.copytree(unzip_dir, target_path + '/') await aioshutil.rmtree(unzip_dir) - - self.ap.logger.debug("源码下载完成。") + + self.ap.logger.debug('源码下载完成。') return repo[1] async def install_requirements(self, path: str): - if os.path.exists(path + "/requirements.txt"): - pkgmgr.install_requirements(path + "/requirements.txt") + if os.path.exists(path + '/requirements.txt'): + pkgmgr.install_requirements(path + '/requirements.txt') async def install_plugin( self, @@ -94,12 +100,14 @@ class GitHubRepoInstaller(installer.PluginInstaller): task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(), ): """安装插件""" - task_context.trace("下载插件源码...", "install-plugin") - repo_label = await self.download_plugin_source_code(plugin_source, "plugins/", task_context) - task_context.trace("安装插件依赖...", "install-plugin") - await self.install_requirements("plugins/" + repo_label) - task_context.trace("完成.", "install-plugin") - + task_context.trace('下载插件源码...', 'install-plugin') + repo_label = await self.download_plugin_source_code( + plugin_source, 'plugins/', task_context + ) + task_context.trace('安装插件依赖...', 'install-plugin') + await self.install_requirements('plugins/' + repo_label) + task_context.trace('完成.', 'install-plugin') + # Caution: in the v4.0, plugin without manifest will not be able to be updated # await self.ap.plugin_mgr.setting.record_installed_plugin_source( # "plugins/" + repo_label + '/', plugin_source @@ -115,9 +123,9 @@ class GitHubRepoInstaller(installer.PluginInstaller): if plugin_container is None: raise errors.PluginInstallerError('插件不存在或未成功加载') else: - task_context.trace("删除插件目录...", "uninstall-plugin") + task_context.trace('删除插件目录...', 'uninstall-plugin') await aioshutil.rmtree(plugin_container.pkg_path) - task_context.trace("完成, 重新加载以生效.", "uninstall-plugin") + task_context.trace('完成, 重新加载以生效.', 'uninstall-plugin') async def update_plugin( self, @@ -126,14 +134,14 @@ class GitHubRepoInstaller(installer.PluginInstaller): task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(), ): """更新插件""" - task_context.trace("更新插件...", "update-plugin") + task_context.trace('更新插件...', 'update-plugin') plugin_container = self.ap.plugin_mgr.get_plugin_by_name(plugin_name) if plugin_container is None: raise errors.PluginInstallerError('插件不存在或未成功加载') else: if plugin_container.plugin_repository: plugin_source = plugin_container.plugin_repository - task_context.trace("转交安装任务.", "update-plugin") + task_context.trace('转交安装任务.', 'update-plugin') await self.install_plugin(plugin_source, task_context) else: - raise errors.PluginInstallerError('插件无源码信息,无法更新') \ No newline at end of file + raise errors.PluginInstallerError('插件无源码信息,无法更新') diff --git a/pkg/plugin/loader.py b/pkg/plugin/loader.py index 44ded4ac..191d8bc1 100644 --- a/pkg/plugin/loader.py +++ b/pkg/plugin/loader.py @@ -1,11 +1,9 @@ from __future__ import annotations -from abc import ABCMeta -import typing import abc from ..core import app -from . import context, events +from . import context class PluginLoader(metaclass=abc.ABCMeta): @@ -25,4 +23,3 @@ class PluginLoader(metaclass=abc.ABCMeta): @abc.abstractmethod async def load_plugins(self): pass - diff --git a/pkg/plugin/loaders/classic.py b/pkg/plugin/loaders/classic.py index ae503ba3..857a7b9c 100644 --- a/pkg/plugin/loaders/classic.py +++ b/pkg/plugin/loaders/classic.py @@ -3,7 +3,6 @@ from __future__ import annotations import typing import pkgutil import importlib -import os import traceback from .. import loader, events, context, models @@ -11,7 +10,6 @@ from ...core import entities as core_entities from ...provider.tools import entities as tools_entities from ...utils import funcschema from ...discover import engine as discover_engine -from ...utils import pkgmgr class PluginLoader(loader.PluginLoader): @@ -36,17 +34,17 @@ class PluginLoader(loader.PluginLoader): """初始化""" def register( - self, - name: str, - description: str, - version: str, - author: str - ) -> typing.Callable[[typing.Type[context.BasePlugin]], typing.Type[context.BasePlugin]]: + self, name: str, description: str, version: str, author: str + ) -> typing.Callable[ + [typing.Type[context.BasePlugin]], typing.Type[context.BasePlugin] + ]: self.ap.logger.debug(f'注册插件 {name} {version} by {author}') container = context.RuntimeContainer( plugin_name=name, plugin_label=discover_engine.I18nString(en_US=name, zh_CN=name), - plugin_description=discover_engine.I18nString(en_US=description, zh_CN=description), + plugin_description=discover_engine.I18nString( + en_US=description, zh_CN=description + ), plugin_version=version, plugin_author=author, plugin_repository='', @@ -61,20 +59,21 @@ class PluginLoader(loader.PluginLoader): def wrapper(cls: context.BasePlugin) -> typing.Type[context.BasePlugin]: container.plugin_class = cls return cls - + return wrapper # 过时 # 最早将于 v3.4 版本移除 def on( - self, - event: typing.Type[events.BaseEventModel] + self, event: typing.Type[events.BaseEventModel] ) -> typing.Callable[[typing.Callable], typing.Callable]: """注册过时的事件处理器""" self.ap.logger.debug(f'注册事件处理器 {event.__name__}') + def wrapper(func: typing.Callable) -> typing.Callable: - - async def handler(plugin: context.BasePlugin, ctx: context.EventContext) -> None: + async def handler( + plugin: context.BasePlugin, ctx: context.EventContext + ) -> None: args = { 'host': ctx.host, 'event': ctx, @@ -82,12 +81,12 @@ class PluginLoader(loader.PluginLoader): # 把 ctx.event 所有的属性都放到 args 里 # for k, v in ctx.event.dict().items(): - # args[k] = v + # args[k] = v for attr_name in ctx.event.__dict__.keys(): args[attr_name] = getattr(ctx.event, attr_name) func(plugin, **args) - + self._current_container.event_handlers[event] = handler return func @@ -98,20 +97,21 @@ class PluginLoader(loader.PluginLoader): # 最早将于 v3.4 版本移除 def func( self, - name: str=None, + name: str = None, ) -> typing.Callable: """注册过时的内容函数""" self.ap.logger.debug(f'注册内容函数 {name}') + def wrapper(func: typing.Callable) -> typing.Callable: - function_schema = funcschema.get_func_schema(func) - function_name = self._current_container.plugin_name + '-' + (func.__name__ if name is None else name) + function_name = ( + self._current_container.plugin_name + + '-' + + (func.__name__ if name is None else name) + ) async def handler( - plugin: context.BasePlugin, - query: core_entities.Query, - *args, - **kwargs + plugin: context.BasePlugin, query: core_entities.Query, *args, **kwargs ): return func(*args, **kwargs) @@ -126,18 +126,19 @@ class PluginLoader(loader.PluginLoader): self._current_container.tools.append(llm_function) return func - + return wrapper - + def handler( - self, - event: typing.Type[events.BaseEventModel] + self, event: typing.Type[events.BaseEventModel] ) -> typing.Callable[[typing.Callable], typing.Callable]: """注册事件处理器""" self.ap.logger.debug(f'注册事件处理器 {event.__name__}') - def wrapper(func: typing.Callable) -> typing.Callable: - if self._current_container is None: # None indicates this plugin is registered through manifest, so ignore it here + def wrapper(func: typing.Callable) -> typing.Callable: + if ( + self._current_container is None + ): # None indicates this plugin is registered through manifest, so ignore it here return func self._current_container.event_handlers[event] = func @@ -148,17 +149,23 @@ class PluginLoader(loader.PluginLoader): def llm_func( self, - name: str=None, + name: str = None, ) -> typing.Callable: """注册内容函数""" self.ap.logger.debug(f'注册内容函数 {name}') + def wrapper(func: typing.Callable) -> typing.Callable: - - if self._current_container is None: # None indicates this plugin is registered through manifest, so ignore it here + if ( + self._current_container is None + ): # None indicates this plugin is registered through manifest, so ignore it here return func - + function_schema = funcschema.get_func_schema(func) - function_name = self._current_container.plugin_name + '-' + (func.__name__ if name is None else name) + function_name = ( + self._current_container.plugin_name + + '-' + + (func.__name__ if name is None else name) + ) llm_function = tools_entities.LLMFunction( name=function_name, @@ -171,43 +178,40 @@ class PluginLoader(loader.PluginLoader): self._current_container.tools.append(llm_function) return func - + return wrapper - - async def _walk_plugin_path( - self, - module, - prefix='', - path_prefix='' - ): - """遍历插件路径 - """ + + async def _walk_plugin_path(self, module, prefix='', path_prefix=''): + """遍历插件路径""" for item in pkgutil.iter_modules(module.__path__): if item.ispkg: await self._walk_plugin_path( - __import__(module.__name__ + "." + item.name, fromlist=[""]), - prefix + item.name + ".", - path_prefix + item.name + "/", + __import__(module.__name__ + '.' + item.name, fromlist=['']), + prefix + item.name + '.', + path_prefix + item.name + '/', ) else: try: - self._current_pkg_path = "plugins/" + path_prefix - self._current_module_path = "plugins/" + path_prefix + item.name + ".py" + self._current_pkg_path = 'plugins/' + path_prefix + self._current_module_path = ( + 'plugins/' + path_prefix + item.name + '.py' + ) self._current_container = None - importlib.import_module(module.__name__ + "." + item.name) + importlib.import_module(module.__name__ + '.' + item.name) if self._current_container is not None: self.plugins.append(self._current_container) self.ap.logger.debug(f'插件 {self._current_container} 已加载') - except: - self.ap.logger.error(f'加载插件模块 {prefix + item.name} 时发生错误') + except Exception: + self.ap.logger.error( + f'加载插件模块 {prefix + item.name} 时发生错误' + ) traceback.print_exc() async def load_plugins(self): - """加载插件 - """ + """加载插件""" setattr(models, 'register', self.register) setattr(models, 'on', self.on) setattr(models, 'func', self.func) @@ -215,4 +219,4 @@ class PluginLoader(loader.PluginLoader): setattr(context, 'register', self.register) setattr(context, 'handler', self.handler) setattr(context, 'llm_func', self.llm_func) - await self._walk_plugin_path(__import__("plugins", fromlist=[""])) + await self._walk_plugin_path(__import__('plugins', fromlist=[''])) diff --git a/pkg/plugin/loaders/manifest.py b/pkg/plugin/loaders/manifest.py index 101fdb3a..b634c5b5 100644 --- a/pkg/plugin/loaders/manifest.py +++ b/pkg/plugin/loaders/manifest.py @@ -1,12 +1,11 @@ from __future__ import annotations import typing -import abc import os import traceback from ...core import app -from .. import context, events, models +from .. import context, events from .. import loader from ...utils import funcschema from ...provider.tools import entities as tools_entities @@ -21,13 +20,12 @@ class PluginManifestLoader(loader.PluginLoader): super().__init__(ap) def handler( - self, - event: typing.Type[events.BaseEventModel] + self, event: typing.Type[events.BaseEventModel] ) -> typing.Callable[[typing.Callable], typing.Callable]: """注册事件处理器""" self.ap.logger.debug(f'注册事件处理器 {event.__name__}') + def wrapper(func: typing.Callable) -> typing.Callable: - self._current_container.event_handlers[event] = func return func @@ -36,14 +34,18 @@ class PluginManifestLoader(loader.PluginLoader): def llm_func( self, - name: str=None, + name: str = None, ) -> typing.Callable: """注册内容函数""" self.ap.logger.debug(f'注册内容函数 {name}') + def wrapper(func: typing.Callable) -> typing.Callable: - function_schema = funcschema.get_func_schema(func) - function_name = self._current_container.plugin_name + '-' + (func.__name__ if name is None else name) + function_name = ( + self._current_container.plugin_name + + '-' + + (func.__name__ if name is None else name) + ) llm_function = tools_entities.LLMFunction( name=function_name, @@ -56,7 +58,7 @@ class PluginManifestLoader(loader.PluginLoader): self._current_container.tools.append(llm_function) return func - + return wrapper async def load_plugins(self): @@ -68,7 +70,11 @@ class PluginManifestLoader(loader.PluginLoader): for plugin_manifest in plugin_manifests: try: - config_schema = plugin_manifest.spec['config'] if 'config' in plugin_manifest.spec else [] + config_schema = ( + plugin_manifest.spec['config'] + if 'config' in plugin_manifest.spec + else [] + ) current_plugin_container = context.RuntimeContainer( plugin_name=plugin_manifest.metadata.name, @@ -77,7 +83,9 @@ class PluginManifestLoader(loader.PluginLoader): plugin_version=plugin_manifest.metadata.version, plugin_author=plugin_manifest.metadata.author, plugin_repository=plugin_manifest.metadata.repository, - main_file=os.path.join(plugin_manifest.rel_dir, plugin_manifest.execution.python.path), + main_file=os.path.join( + plugin_manifest.rel_dir, plugin_manifest.execution.python.path + ), pkg_path=plugin_manifest.rel_dir, config_schema=config_schema, event_handlers={}, @@ -95,6 +103,8 @@ class PluginManifestLoader(loader.PluginLoader): # TODO load component extensions self.plugins.append(current_plugin_container) - except Exception as e: - self.ap.logger.error(f'加载插件 {plugin_manifest.metadata.name} 时发生错误') + except Exception: + self.ap.logger.error( + f'加载插件 {plugin_manifest.metadata.name} 时发生错误' + ) traceback.print_exc() diff --git a/pkg/plugin/manager.py b/pkg/plugin/manager.py index 2ecd01c2..2322eb5c 100644 --- a/pkg/plugin/manager.py +++ b/pkg/plugin/manager.py @@ -1,10 +1,8 @@ from __future__ import annotations -import typing import traceback import sqlalchemy -import logging from ..core import app, taskmgr from . import context, loader, events, installer, models @@ -28,28 +26,26 @@ class PluginManager: def plugins( self, - enabled: bool=None, - status: context.RuntimeContainerStatus=None, + enabled: bool = None, + status: context.RuntimeContainerStatus = None, ) -> list[context.RuntimeContainer]: - """获取插件列表 - """ + """获取插件列表""" plugins = self.plugin_containers if enabled is not None: plugins = [plugin for plugin in plugins if plugin.enabled == enabled] - + if status is not None: plugins = [plugin for plugin in plugins if plugin.status == status] return plugins - + def get_plugin( self, author: str, plugin_name: str, ) -> context.RuntimeContainer: - """通过作者和插件名获取插件 - """ + """通过作者和插件名获取插件""" for plugin in self.plugins(): if plugin.plugin_author == author and plugin.plugin_name == plugin_name: return plugin @@ -88,20 +84,24 @@ class PluginManager: self.ap.logger.debug(f'优先级排序后的插件列表 {self.plugin_containers}') async def load_plugin_settings( - self, - plugin_containers: list[context.RuntimeContainer] + self, plugin_containers: list[context.RuntimeContainer] ): for plugin_container in plugin_containers: result = await self.ap.persistence_mgr.execute_async( - sqlalchemy.select(persistence_plugin.PluginSetting) \ - .where(persistence_plugin.PluginSetting.plugin_author == plugin_container.plugin_author) - .where(persistence_plugin.PluginSetting.plugin_name == plugin_container.plugin_name) + sqlalchemy.select(persistence_plugin.PluginSetting) + .where( + persistence_plugin.PluginSetting.plugin_author + == plugin_container.plugin_author + ) + .where( + persistence_plugin.PluginSetting.plugin_name + == plugin_container.plugin_name + ) ) setting = result.first() if setting is None: - new_setting_data = { 'plugin_author': plugin_container.plugin_author, 'plugin_name': plugin_container.plugin_name, @@ -111,7 +111,9 @@ class PluginManager: } await self.ap.persistence_mgr.execute_async( - sqlalchemy.insert(persistence_plugin.PluginSetting).values(**new_setting_data) + sqlalchemy.insert(persistence_plugin.PluginSetting).values( + **new_setting_data + ) ) continue else: @@ -120,19 +122,23 @@ class PluginManager: plugin_container.plugin_config = setting.config async def dump_plugin_container_setting( - self, - plugin_container: context.RuntimeContainer + self, plugin_container: context.RuntimeContainer ): - """保存单个插件容器的设置到数据库 - """ + """保存单个插件容器的设置到数据库""" await self.ap.persistence_mgr.execute_async( sqlalchemy.update(persistence_plugin.PluginSetting) - .where(persistence_plugin.PluginSetting.plugin_author == plugin_container.plugin_author) - .where(persistence_plugin.PluginSetting.plugin_name == plugin_container.plugin_name) + .where( + persistence_plugin.PluginSetting.plugin_author + == plugin_container.plugin_author + ) + .where( + persistence_plugin.PluginSetting.plugin_name + == plugin_container.plugin_name + ) .values( enabled=plugin_container.enabled, priority=plugin_container.priority, - config=plugin_container.plugin_config + config=plugin_container.plugin_config, ) ) @@ -160,13 +166,13 @@ class PluginManager: async def destroy_plugin(self, plugin: context.RuntimeContainer): if plugin.status != context.RuntimeContainerStatus.INITIALIZED: return - + self.ap.logger.debug(f'释放插件 {plugin.plugin_name}') plugin.plugin_inst.__del__() await plugin.plugin_inst.destroy() plugin.plugin_inst = None plugin.status = context.RuntimeContainerStatus.MOUNTED - + async def destroy_plugins(self): for plugin in self.plugins(): if plugin.status != context.RuntimeContainerStatus.INITIALIZED: @@ -185,16 +191,15 @@ class PluginManager: plugin_source: str, task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(), ): - """安装插件 - """ + """安装插件""" await self.installer.install_plugin(plugin_source, task_context) await self.ap.ctr_mgr.plugin.post_install_record( { - "name": "unknown", - "remote": plugin_source, - "author": "unknown", - "version": "HEAD" + 'name': 'unknown', + 'remote': plugin_source, + 'author': 'unknown', + 'version': 'HEAD', } ) @@ -206,8 +211,7 @@ class PluginManager: plugin_name: str, task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(), ): - """卸载插件 - """ + """卸载插件""" plugin_container = self.get_plugin_by_name(plugin_name) @@ -219,10 +223,10 @@ class PluginManager: await self.ap.ctr_mgr.plugin.post_remove_record( { - "name": plugin_name, - "remote": plugin_container.plugin_repository, - "author": plugin_container.plugin_author, - "version": plugin_container.plugin_version + 'name': plugin_name, + 'remote': plugin_container.plugin_repository, + 'author': plugin_container.plugin_author, + 'version': plugin_container.plugin_version, } ) @@ -232,80 +236,82 @@ class PluginManager: async def update_plugin( self, plugin_name: str, - plugin_source: str=None, + plugin_source: str = None, task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(), ): - """更新插件 - """ + """更新插件""" await self.installer.update_plugin(plugin_name, plugin_source, task_context) - + plugin_container = self.get_plugin_by_name(plugin_name) await self.ap.ctr_mgr.plugin.post_update_record( plugin={ - "name": plugin_name, - "remote": plugin_container.plugin_repository, - "author": plugin_container.plugin_author, - "version": plugin_container.plugin_version + 'name': plugin_name, + 'remote': plugin_container.plugin_repository, + 'author': plugin_container.plugin_author, + 'version': plugin_container.plugin_version, }, old_version=plugin_container.plugin_version, - new_version="HEAD" + new_version='HEAD', ) task_context.trace('重载插件..', 'reload-plugin') await self.ap.reload(scope='plugin') def get_plugin_by_name(self, plugin_name: str) -> context.RuntimeContainer: - """通过插件名获取插件 - """ + """通过插件名获取插件""" for plugin in self.plugins(): if plugin.plugin_name == plugin_name: return plugin return None async def emit_event(self, event: events.BaseEventModel) -> context.EventContext: - """触发事件 - """ + """触发事件""" + + ctx = context.EventContext(host=self.api_host, event=event) - ctx = context.EventContext( - host=self.api_host, - event=event - ) - emitted_plugins: list[context.RuntimeContainer] = [] for plugin in self.plugins( - enabled=True, - status=context.RuntimeContainerStatus.INITIALIZED + enabled=True, status=context.RuntimeContainerStatus.INITIALIZED ): if event.__class__ in plugin.event_handlers: - self.ap.logger.debug(f'插件 {plugin.plugin_name} 处理事件 {event.__class__.__name__}') - + self.ap.logger.debug( + f'插件 {plugin.plugin_name} 处理事件 {event.__class__.__name__}' + ) + is_prevented_default_before_call = ctx.is_prevented_default() try: await plugin.event_handlers[event.__class__]( - plugin.plugin_inst, - ctx + plugin.plugin_inst, ctx ) except Exception as e: - self.ap.logger.error(f'插件 {plugin.plugin_name} 处理事件 {event.__class__.__name__} 时发生错误: {e}') - self.ap.logger.debug(f"Traceback: {traceback.format_exc()}") - + self.ap.logger.error( + f'插件 {plugin.plugin_name} 处理事件 {event.__class__.__name__} 时发生错误: {e}' + ) + self.ap.logger.debug(f'Traceback: {traceback.format_exc()}') + emitted_plugins.append(plugin) if not is_prevented_default_before_call and ctx.is_prevented_default(): - self.ap.logger.debug(f'插件 {plugin.plugin_name} 阻止了默认行为执行') + self.ap.logger.debug( + f'插件 {plugin.plugin_name} 阻止了默认行为执行' + ) if ctx.is_prevented_postorder(): - self.ap.logger.debug(f'插件 {plugin.plugin_name} 阻止了后序插件的执行') + self.ap.logger.debug( + f'插件 {plugin.plugin_name} 阻止了后序插件的执行' + ) break for key in ctx.__return_value__.keys(): if hasattr(ctx.event, key): setattr(ctx.event, key, ctx.__return_value__[key][0]) - - self.ap.logger.debug(f'事件 {event.__class__.__name__}({ctx.eid}) 处理完成,返回值 {ctx.__return_value__}') + + self.ap.logger.debug( + f'事件 {event.__class__.__name__}({ctx.eid}) 处理完成,返回值 {ctx.__return_value__}' + ) if emitted_plugins: plugins_info: list[dict] = [ @@ -313,13 +319,13 @@ class PluginManager: 'name': plugin.plugin_name, 'remote': plugin.plugin_repository, 'version': plugin.plugin_version, - 'author': plugin.plugin_author - } for plugin in emitted_plugins + 'author': plugin.plugin_author, + } + for plugin in emitted_plugins ] await self.ap.ctr_mgr.usage.post_event_record( - plugins=plugins_info, - event_name=event.__class__.__name__ + plugins=plugins_info, event_name=event.__class__.__name__ ) return ctx @@ -330,7 +336,7 @@ class PluginManager: if plugin.plugin_name == plugin_name: if plugin.enabled == new_status: return False - + # 初始化/释放插件 if new_status: await self.initialize_plugin(plugin) @@ -338,8 +344,8 @@ class PluginManager: await self.destroy_plugin(plugin) plugin.enabled = new_status - - await self.dump_plugin_container_setting(self.plugin_containers) + + await self.dump_plugin_container_setting(plugin) break @@ -348,7 +354,6 @@ class PluginManager: return False async def reorder_plugins(self, plugins: list[dict]): - for plugin in plugins: plugin_name = plugin.get('name') plugin_priority = plugin.get('priority') @@ -360,9 +365,12 @@ class PluginManager: self.plugin_containers.sort(key=lambda x: x.priority, reverse=True) - await self.dump_plugin_container_setting(self.plugin_containers) + for plugin in self.plugin_containers: + await self.dump_plugin_container_setting(plugin) - async def set_plugin_config(self, plugin_container: context.RuntimeContainer, new_config: dict): + async def set_plugin_config( + self, plugin_container: context.RuntimeContainer, new_config: dict + ): plugin_container.plugin_config = new_config plugin_container.plugin_inst.config = new_config diff --git a/pkg/plugin/models.py b/pkg/plugin/models.py index b8b499f5..dbde89a9 100644 --- a/pkg/plugin/models.py +++ b/pkg/plugin/models.py @@ -9,22 +9,20 @@ import typing from .context import BasePlugin as Plugin from .events import * + def register( - name: str, - description: str, - version: str, - author + name: str, description: str, version: str, author ) -> typing.Callable[[typing.Type[Plugin]], typing.Type[Plugin]]: pass def on( - event: typing.Type[BaseEventModel] + event: typing.Type[BaseEventModel], ) -> typing.Callable[[typing.Callable], typing.Callable]: pass def func( - name: str=None, + name: str = None, ) -> typing.Callable: pass diff --git a/pkg/provider/entities.py b/pkg/provider/entities.py index 0fb75f80..ff95b128 100644 --- a/pkg/provider/entities.py +++ b/pkg/provider/entities.py @@ -1,7 +1,6 @@ from __future__ import annotations import typing -import enum import pydantic.v1 as pydantic from pkg.provider import entities @@ -32,7 +31,6 @@ class ImageURLContentObject(pydantic.BaseModel): class ContentElement(pydantic.BaseModel): - type: str """内容类型""" @@ -57,7 +55,7 @@ class ContentElement(pydantic.BaseModel): @classmethod def from_image_url(cls, image_url: str): return cls(type='image_url', image_url=ImageURLContentObject(url=image_url)) - + @classmethod def from_image_base64(cls, image_base64: str): return cls(type='image_base64', image_base64=image_base64) @@ -82,15 +80,19 @@ class Message(pydantic.BaseModel): def readable_str(self) -> str: if self.content is not None: - return str(self.role) + ": " + str(self.get_content_platform_message_chain()) + return ( + str(self.role) + ': ' + str(self.get_content_platform_message_chain()) + ) elif self.tool_calls is not None: return f'调用工具: {self.tool_calls[0].id}' else: return '未知消息' - def get_content_platform_message_chain(self, prefix_text: str="") -> platform_message.MessageChain | None: + def get_content_platform_message_chain( + self, prefix_text: str = '' + ) -> platform_message.MessageChain | None: """将内容转换为平台消息 MessageChain 对象 - + Args: prefix_text (str): 首个文字组件的前缀文本 """ @@ -98,21 +100,22 @@ class Message(pydantic.BaseModel): if self.content is None: return None elif isinstance(self.content, str): - return platform_message.MessageChain([platform_message.Plain(prefix_text+self.content)]) + return platform_message.MessageChain( + [platform_message.Plain(prefix_text + self.content)] + ) elif isinstance(self.content, list): mc = [] for ce in self.content: if ce.type == 'text': mc.append(platform_message.Plain(ce.text)) elif ce.type == 'image_url': - if ce.image_url.url.startswith("http"): + if ce.image_url.url.startswith('http'): mc.append(platform_message.Image(url=ce.image_url.url)) else: # base64 - b64_str = ce.image_url.url - if b64_str.startswith("data:"): - b64_str = b64_str.split(",")[1] + if b64_str.startswith('data:'): + b64_str = b64_str.split(',')[1] mc.append(platform_message.Image(base64=b64_str)) @@ -120,7 +123,7 @@ class Message(pydantic.BaseModel): if prefix_text: for i, c in enumerate(mc): if isinstance(c, platform_message.Plain): - mc[i] = platform_message.Plain(prefix_text+c.text) + mc[i] = platform_message.Plain(prefix_text + c.text) break else: mc.insert(0, platform_message.Plain(prefix_text)) diff --git a/pkg/provider/modelmgr/errors.py b/pkg/provider/modelmgr/errors.py index d466cf11..dc3b35b6 100644 --- a/pkg/provider/modelmgr/errors.py +++ b/pkg/provider/modelmgr/errors.py @@ -2,4 +2,4 @@ class RequesterError(Exception): """Base class for all Requester errors.""" def __init__(self, message: str): - super().__init__("模型请求失败: "+message) + super().__init__('模型请求失败: ' + message) diff --git a/pkg/provider/modelmgr/modelmgr.py b/pkg/provider/modelmgr/modelmgr.py index 244d5753..25a79fec 100644 --- a/pkg/provider/modelmgr/modelmgr.py +++ b/pkg/provider/modelmgr/modelmgr.py @@ -1,8 +1,6 @@ from __future__ import annotations -import typing import sqlalchemy -import pydantic.v1 as pydantic from . import entities, requester from ...core import app @@ -12,10 +10,8 @@ from ..tools import entities as tools_entities from ...discover import engine from . import token from ...entity.persistence import model as persistence_model -from .requesters import bailianchatcmpl, chatcmpl, anthropicmsgs, moonshotchatcmpl, deepseekchatcmpl, ollamachat, giteeaichatcmpl, volcarkchatcmpl, xaichatcmpl, zhipuaichatcmpl, lmstudiochatcmpl, siliconflowchatcmpl, volcarkchatcmpl - -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' class ModelManager: @@ -36,7 +32,7 @@ class ModelManager: requester_components: list[engine.Component] requester_dict: dict[str, type[requester.LLMAPIRequester]] # cache - + def __init__(self, ap: app.Application): self.ap = ap self.model_list = [] @@ -45,14 +41,18 @@ class ModelManager: self.llm_models = [] self.requester_components = [] self.requester_dict = {} - + async def initialize(self): - self.requester_components = self.ap.discover.get_components_by_kind('LLMAPIRequester') + self.requester_components = self.ap.discover.get_components_by_kind( + 'LLMAPIRequester' + ) # forge requester class dict requester_dict: dict[str, type[requester.LLMAPIRequester]] = {} for component in self.requester_components: - requester_dict[component.metadata.name] = component.get_python_component_class() + requester_dict[component.metadata.name] = ( + component.get_python_component_class() + ) self.requester_dict = requester_dict @@ -74,18 +74,22 @@ class ModelManager: # load models for llm_model in llm_models: await self.load_llm_model(llm_model) - - async def load_llm_model(self, model_info: persistence_model.LLMModel | sqlalchemy.Row[persistence_model.LLMModel] | dict): + + async def load_llm_model( + self, + model_info: persistence_model.LLMModel + | sqlalchemy.Row[persistence_model.LLMModel] + | dict, + ): """加载模型""" - + if isinstance(model_info, sqlalchemy.Row): model_info = persistence_model.LLMModel(**model_info._mapping) elif isinstance(model_info, dict): model_info = persistence_model.LLMModel(**model_info) requester_inst = self.requester_dict[model_info.requester]( - ap=self.ap, - config=model_info.requester_config + ap=self.ap, config=model_info.requester_config ) await requester_inst.initialize() @@ -96,24 +100,23 @@ class ModelManager: name=model_info.uuid, tokens=model_info.api_keys, ), - requester=requester_inst + requester=requester_inst, ) self.llm_models.append(runtime_llm_model) async def get_model_by_name(self, name: str) -> entities.LLMModelInfo: # deprecated - """通过名称获取模型 - """ + """通过名称获取模型""" for model in self.model_list: if model.name == name: return model - raise ValueError(f"无法确定模型 {name} 的信息,请在元数据中配置") - + raise ValueError(f'无法确定模型 {name} 的信息,请在元数据中配置') + async def get_model_by_uuid(self, uuid: str) -> entities.LLMModelInfo: """通过uuid获取模型""" for model in self.llm_models: if model.model_entity.uuid == uuid: return model - raise ValueError(f"model {uuid} not found") + raise ValueError(f'model {uuid} not found') async def remove_llm_model(self, model_uuid: str): """移除模型""" @@ -124,10 +127,7 @@ class ModelManager: def get_available_requesters_info(self) -> list[dict]: """获取所有可用的请求器""" - return [ - component.to_plain_dict() - for component in self.requester_components - ] + return [component.to_plain_dict() for component in self.requester_components] def get_available_requester_info_by_name(self, name: str) -> dict | None: """通过名称获取请求器信息""" @@ -135,8 +135,10 @@ class ModelManager: if component.metadata.name == name: return component.to_plain_dict() return None - - def get_available_requester_manifest_by_name(self, name: str) -> engine.Component | None: + + def get_available_requester_manifest_by_name( + self, name: str + ) -> engine.Component | None: """通过名称获取请求器清单""" for component in self.requester_components: if component.metadata.name == name: @@ -151,4 +153,3 @@ class ModelManager: funcs: list[tools_entities.LLMFunction] = None, ) -> llm_entities.Message: pass - diff --git a/pkg/provider/modelmgr/requester.py b/pkg/provider/modelmgr/requester.py index 5ea8d23f..244f4c82 100644 --- a/pkg/provider/modelmgr/requester.py +++ b/pkg/provider/modelmgr/requester.py @@ -22,16 +22,21 @@ class RuntimeLLMModel: requester: LLMAPIRequester """请求器实例""" - - def __init__(self, model_entity: persistence_model.LLMModel, token_mgr: token.TokenManager, requester: LLMAPIRequester): + + def __init__( + self, + model_entity: persistence_model.LLMModel, + token_mgr: token.TokenManager, + requester: LLMAPIRequester, + ): self.model_entity = model_entity self.token_mgr = token_mgr self.requester = requester class LLMAPIRequester(metaclass=abc.ABCMeta): - """LLM API请求器 - """ + """LLM API请求器""" + name: str = None ap: app.Application @@ -42,9 +47,7 @@ class LLMAPIRequester(metaclass=abc.ABCMeta): def __init__(self, ap: app.Application, config: dict[str, typing.Any]): self.ap = ap - self.requester_cfg = { - **self.default_config - } + self.requester_cfg = {**self.default_config} self.requester_cfg.update(config) async def initialize(self): diff --git a/pkg/provider/modelmgr/requesters/anthropicmsgs.py b/pkg/provider/modelmgr/requesters/anthropicmsgs.py index 7edc4405..ca145e9e 100644 --- a/pkg/provider/modelmgr/requesters/anthropicmsgs.py +++ b/pkg/provider/modelmgr/requesters/anthropicmsgs.py @@ -2,16 +2,12 @@ from __future__ import annotations import typing import json -import traceback -import base64 import anthropic import httpx -from ....core import app -from .. import entities, errors, requester +from .. import errors, requester -from .. import entities, errors from ....core import entities as core_entities from ... import entities as llm_entities from ...tools import entities as tools_entities @@ -29,7 +25,6 @@ class AnthropicMessages(requester.LLMAPIRequester): } async def initialize(self): - httpx_client = anthropic._base_client.AsyncHttpxClientWrapper( base_url=self.requester_cfg['base_url'], # cast to a valid type because mypy doesn't understand our type narrowing @@ -40,7 +35,7 @@ class AnthropicMessages(requester.LLMAPIRequester): ) self.client = anthropic.AsyncAnthropic( - api_key="", + api_key='', http_client=httpx_client, ) @@ -55,7 +50,7 @@ class AnthropicMessages(requester.LLMAPIRequester): self.client.api_key = model.token_mgr.get_token() args = extra_args.copy() - args["model"] = model.model_entity.name + args['model'] = model.model_entity.name # 处理消息 @@ -63,14 +58,15 @@ class AnthropicMessages(requester.LLMAPIRequester): system_role_message = None for i, m in enumerate(messages): - if m.role == "system": + if m.role == 'system': system_role_message = m messages.pop(i) break - if isinstance(system_role_message, llm_entities.Message) \ - and isinstance(system_role_message.content, str): + if isinstance(system_role_message, llm_entities.Message) and isinstance( + system_role_message.content, str + ): args['system'] = system_role_message.content req_messages = [] @@ -79,67 +75,64 @@ class AnthropicMessages(requester.LLMAPIRequester): if m.role == 'tool': tool_call_id = m.tool_call_id - req_messages.append({ - "role": "user", - "content": [ - { - "type": "tool_result", - "tool_use_id": tool_call_id, - "content": m.content - } - ] - }) + req_messages.append( + { + 'role': 'user', + 'content': [ + { + 'type': 'tool_result', + 'tool_use_id': tool_call_id, + 'content': m.content, + } + ], + } + ) continue msg_dict = m.dict(exclude_none=True) - if isinstance(m.content, str) and m.content.strip() != "": - msg_dict["content"] = [ - { - "type": "text", - "text": m.content - } - ] + if isinstance(m.content, str) and m.content.strip() != '': + msg_dict['content'] = [{'type': 'text', 'text': m.content}] elif isinstance(m.content, list): - for i, ce in enumerate(m.content): - - if ce.type == "image_base64": - image_b64, image_format = await image.extract_b64_and_format(ce.image_base64) + if ce.type == 'image_base64': + image_b64, image_format = await image.extract_b64_and_format( + ce.image_base64 + ) alter_image_ele = { - "type": "image", - "source": { - "type": "base64", - "media_type": f"image/{image_format}", - "data": image_b64 - } + 'type': 'image', + 'source': { + 'type': 'base64', + 'media_type': f'image/{image_format}', + 'data': image_b64, + }, } - msg_dict["content"][i] = alter_image_ele + msg_dict['content'][i] = alter_image_ele if m.tool_calls: - for tool_call in m.tool_calls: - msg_dict["content"].append({ - "type": "tool_use", - "id": tool_call.id, - "name": tool_call.function.name, - "input": json.loads(tool_call.function.arguments) - }) + msg_dict['content'].append( + { + 'type': 'tool_use', + 'id': tool_call.id, + 'name': tool_call.function.name, + 'input': json.loads(tool_call.function.arguments), + } + ) - del msg_dict["tool_calls"] + del msg_dict['tool_calls'] req_messages.append(msg_dict) - - args["messages"] = req_messages - + args['messages'] = req_messages + if funcs: tools = await self.ap.tool_mgr.generate_tools_for_anthropic(funcs) if tools: - args["tools"] = tools + args['tools'] = tools try: # print(json.dumps(args, indent=4, ensure_ascii=False)) @@ -149,23 +142,24 @@ class AnthropicMessages(requester.LLMAPIRequester): 'content': '', 'role': resp.role, } - + assert type(resp) is anthropic.types.message.Message for block in resp.content: if block.type == 'thinking': - args['content'] = '' + block.thinking + '\n' + args['content'] + args['content'] = ( + '' + block.thinking + '\n' + args['content'] + ) elif block.type == 'text': args['content'] += block.text elif block.type == 'tool_use': assert type(block) is anthropic.types.tool_use_block.ToolUseBlock tool_call = llm_entities.ToolCall( id=block.id, - type="function", + type='function', function=llm_entities.FunctionCall( - name=block.name, - arguments=json.dumps(block.input) - ) + name=block.name, arguments=json.dumps(block.input) + ), ) if 'tool_calls' not in args: args['tool_calls'] = [] diff --git a/pkg/provider/modelmgr/requesters/bailianchatcmpl.py b/pkg/provider/modelmgr/requesters/bailianchatcmpl.py index e20e3376..287eb5b9 100644 --- a/pkg/provider/modelmgr/requesters/bailianchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/bailianchatcmpl.py @@ -4,8 +4,6 @@ import typing import openai from . import chatcmpl -from .. import requester -from ....core import app class BailianChatCompletions(chatcmpl.OpenAIChatCompletions): diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index c54d466f..e341d0fb 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -2,22 +2,15 @@ from __future__ import annotations import asyncio import typing -import json -import base64 -from typing import AsyncGenerator import openai import openai.types.chat.chat_completion as chat_completion -import openai.types.chat.chat_completion_message_tool_call as chat_completion_message_tool_call import httpx -import aiohttp -import async_lru -from .. import entities, errors, requester -from ....core import entities as core_entities, app +from .. import errors, requester +from ....core import entities as core_entities from ... import entities as llm_entities from ...tools import entities as tools_entities -from ....utils import image class OpenAIChatCompletions(requester.LLMAPIRequester): @@ -26,18 +19,17 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): client: openai.AsyncClient default_config: dict[str, typing.Any] = { - "base_url": "https://api.openai.com/v1", - "timeout": 120, + 'base_url': 'https://api.openai.com/v1', + 'timeout': 120, } async def initialize(self): - self.client = openai.AsyncClient( - api_key="", - base_url=self.requester_cfg["base_url"], - timeout=self.requester_cfg["timeout"], + api_key='', + base_url=self.requester_cfg['base_url'], + timeout=self.requester_cfg['timeout'], http_client=httpx.AsyncClient( - trust_env=True, timeout=self.requester_cfg["timeout"] + trust_env=True, timeout=self.requester_cfg['timeout'] ), ) @@ -54,8 +46,8 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): chatcmpl_message = chat_completion.choices[0].message.model_dump() # 确保 role 字段存在且不为 None - if "role" not in chatcmpl_message or chatcmpl_message["role"] is None: - chatcmpl_message["role"] = "assistant" + if 'role' not in chatcmpl_message or chatcmpl_message['role'] is None: + chatcmpl_message['role'] = 'assistant' message = llm_entities.Message(**chatcmpl_message) @@ -72,27 +64,27 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): self.client.api_key = use_model.token_mgr.get_token() args = extra_args.copy() - args["model"] = use_model.model_entity.name + args['model'] = use_model.model_entity.name if use_funcs: tools = await self.ap.tool_mgr.generate_tools_for_openai(use_funcs) if tools: - args["tools"] = tools + args['tools'] = tools # 设置此次请求中的messages messages = req_messages.copy() # 检查vision for msg in messages: - if "content" in msg and isinstance(msg["content"], list): - for me in msg["content"]: - if me["type"] == "image_base64": - me["image_url"] = {"url": me["image_base64"]} - me["type"] = "image_url" - del me["image_base64"] + if 'content' in msg and isinstance(msg['content'], list): + for me in msg['content']: + if me['type'] == 'image_base64': + me['image_url'] = {'url': me['image_base64']} + me['type'] = 'image_url' + del me['image_base64'] - args["messages"] = messages + args['messages'] = messages # 发送请求 resp = await self._req(args) @@ -113,15 +105,15 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行 for m in messages: msg_dict = m.dict(exclude_none=True) - content = msg_dict.get("content") + content = msg_dict.get('content') if isinstance(content, list): # 检查 content 列表中是否每个部分都是文本 if all( - isinstance(part, dict) and part.get("type") == "text" + isinstance(part, dict) and part.get('type') == 'text' for part in content ): # 将所有文本部分合并为一个字符串 - msg_dict["content"] = "\n".join(part["text"] for part in content) + msg_dict['content'] = '\n'.join(part['text'] for part in content) req_messages.append(msg_dict) try: @@ -133,17 +125,17 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): extra_args=extra_args, ) except asyncio.TimeoutError: - raise errors.RequesterError("请求超时") + raise errors.RequesterError('请求超时') except openai.BadRequestError as e: - if "context_length_exceeded" in e.message: - raise errors.RequesterError(f"上文过长,请重置会话: {e.message}") + if 'context_length_exceeded' in e.message: + raise errors.RequesterError(f'上文过长,请重置会话: {e.message}') else: - raise errors.RequesterError(f"请求参数错误: {e.message}") + raise errors.RequesterError(f'请求参数错误: {e.message}') except openai.AuthenticationError as e: - raise errors.RequesterError(f"无效的 api-key: {e.message}") + raise errors.RequesterError(f'无效的 api-key: {e.message}') except openai.NotFoundError as e: - raise errors.RequesterError(f"请求路径错误: {e.message}") + raise errors.RequesterError(f'请求路径错误: {e.message}') except openai.RateLimitError as e: - raise errors.RequesterError(f"请求过于频繁或余额不足: {e.message}") + raise errors.RequesterError(f'请求过于频繁或余额不足: {e.message}') except openai.APIError as e: - raise errors.RequesterError(f"请求错误: {e.message}") + raise errors.RequesterError(f'请求错误: {e.message}') diff --git a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py index ee17ac05..49457ac0 100644 --- a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py @@ -3,8 +3,8 @@ from __future__ import annotations import typing from . import chatcmpl -from .. import entities, errors, requester -from ....core import entities as core_entities, app +from .. import errors, requester +from ....core import entities as core_entities from ... import entities as llm_entities from ...tools import entities as tools_entities @@ -28,23 +28,23 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions): self.client.api_key = use_model.token_mgr.get_token() args = extra_args.copy() - args["model"] = use_model.model_entity.name + args['model'] = use_model.model_entity.name if use_funcs: tools = await self.ap.tool_mgr.generate_tools_for_openai(use_funcs) if tools: - args["tools"] = tools + args['tools'] = tools # 设置此次请求中的messages messages = req_messages # deepseek 不支持多模态,把content都转换成纯文字 for m in messages: - if 'content' in m and isinstance(m["content"], list): - m["content"] = " ".join([c["text"] for c in m["content"]]) + if 'content' in m and isinstance(m['content'], list): + m['content'] = ' '.join([c['text'] for c in m['content']]) - args["messages"] = messages + args['messages'] = messages # 发送请求 resp = await self._req(args) @@ -55,4 +55,4 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions): # 处理请求结果 message = await self._make_msg(resp) - return message \ No newline at end of file + return message diff --git a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py index 35052682..b85cc54d 100644 --- a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py +++ b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py @@ -1,17 +1,13 @@ from __future__ import annotations -import json -import asyncio -import aiohttp import typing from . import chatcmpl -from .. import entities, errors, requester -from ....core import app, entities as core_entities +from .. import requester +from ....core import entities as core_entities from ... import entities as llm_entities from ...tools import entities as tools_entities -from .. import entities as modelmgr_entities class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): @@ -33,20 +29,20 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): self.client.api_key = use_model.token_mgr.get_token() args = extra_args.copy() - args["model"] = use_model.model_entity.name + args['model'] = use_model.model_entity.name if use_funcs: tools = await self.ap.tool_mgr.generate_tools_for_openai(use_funcs) if tools: - args["tools"] = tools + args['tools'] = tools # gitee 不支持多模态,把content都转换成纯文字 for m in req_messages: - if 'content' in m and isinstance(m["content"], list): - m["content"] = " ".join([c["text"] for c in m["content"]]) + if 'content' in m and isinstance(m['content'], list): + m['content'] = ' '.join([c['text'] for c in m['content']]) - args["messages"] = req_messages + args['messages'] = req_messages resp = await self._req(args) diff --git a/pkg/provider/modelmgr/requesters/lmstudiochatcmpl.py b/pkg/provider/modelmgr/requesters/lmstudiochatcmpl.py index 6be76051..c9060c1b 100644 --- a/pkg/provider/modelmgr/requesters/lmstudiochatcmpl.py +++ b/pkg/provider/modelmgr/requesters/lmstudiochatcmpl.py @@ -4,8 +4,6 @@ import typing import openai from . import chatcmpl -from .. import requester -from ....core import app class LmStudioChatCompletions(chatcmpl.OpenAIChatCompletions): diff --git a/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py b/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py index 1ef7d9c9..ac565cdb 100644 --- a/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py @@ -2,11 +2,10 @@ from __future__ import annotations import typing -from ....core import app from . import chatcmpl -from .. import entities, errors, requester -from ....core import entities as core_entities, app +from .. import requester +from ....core import entities as core_entities from ... import entities as llm_entities from ...tools import entities as tools_entities @@ -30,26 +29,26 @@ class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions): self.client.api_key = use_model.token_mgr.get_token() args = extra_args.copy() - args["model"] = use_model.model_entity.name + args['model'] = use_model.model_entity.name if use_funcs: tools = await self.ap.tool_mgr.generate_tools_for_openai(use_funcs) if tools: - args["tools"] = tools + args['tools'] = tools # 设置此次请求中的messages messages = req_messages # deepseek 不支持多模态,把content都转换成纯文字 for m in messages: - if 'content' in m and isinstance(m["content"], list): - m["content"] = " ".join([c["text"] for c in m["content"]]) + if 'content' in m and isinstance(m['content'], list): + m['content'] = ' '.join([c['text'] for c in m['content']]) # 删除空的 - messages = [m for m in messages if m["content"].strip() != ""] + messages = [m for m in messages if m['content'].strip() != ''] - args["messages"] = messages + args['messages'] = messages # 发送请求 resp = await self._req(args) @@ -57,4 +56,4 @@ class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions): # 处理请求结果 message = await self._make_msg(resp) - return message \ No newline at end of file + return message diff --git a/pkg/provider/modelmgr/requesters/ollamachat.py b/pkg/provider/modelmgr/requesters/ollamachat.py index ee331036..995dd855 100644 --- a/pkg/provider/modelmgr/requesters/ollamachat.py +++ b/pkg/provider/modelmgr/requesters/ollamachat.py @@ -6,18 +6,15 @@ import typing from typing import Union, Mapping, Any, AsyncIterator import uuid import json -import base64 -import async_lru import ollama -from .. import entities, errors, requester +from .. import errors, requester from ... import entities as llm_entities from ...tools import entities as tools_entities -from ....core import app, entities as core_entities -from ....utils import image +from ....core import entities as core_entities -REQUESTER_NAME: str = "ollama-chat" +REQUESTER_NAME: str = 'ollama-chat' class OllamaChatCompletions(requester.LLMAPIRequester): @@ -26,13 +23,13 @@ class OllamaChatCompletions(requester.LLMAPIRequester): client: ollama.AsyncClient default_config: dict[str, typing.Any] = { - "base_url": "http://127.0.0.1:11434", - "timeout": 120, + 'base_url': 'http://127.0.0.1:11434', + 'timeout': 120, } async def initialize(self): - os.environ["OLLAMA_HOST"] = self.requester_cfg["base_url"] - self.client = ollama.AsyncClient(timeout=self.requester_cfg["timeout"]) + os.environ['OLLAMA_HOST'] = self.requester_cfg['base_url'] + self.client = ollama.AsyncClient(timeout=self.requester_cfg['timeout']) async def _req( self, @@ -49,35 +46,35 @@ class OllamaChatCompletions(requester.LLMAPIRequester): extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: args = extra_args.copy() - args["model"] = use_model.model_entity.name + args['model'] = use_model.model_entity.name messages: list[dict] = req_messages.copy() for msg in messages: - if "content" in msg and isinstance(msg["content"], list): + if 'content' in msg and isinstance(msg['content'], list): text_content: list = [] image_urls: list = [] - for me in msg["content"]: - if me["type"] == "text": - text_content.append(me["text"]) - elif me["type"] == "image_base64": - image_urls.append(me["image_base64"]) + for me in msg['content']: + if me['type'] == 'text': + text_content.append(me['text']) + elif me['type'] == 'image_base64': + image_urls.append(me['image_base64']) - msg["content"] = "\n".join(text_content) - msg["images"] = [url.split(",")[1] for url in image_urls] + msg['content'] = '\n'.join(text_content) + msg['images'] = [url.split(',')[1] for url in image_urls] if ( - "tool_calls" in msg + 'tool_calls' in msg ): # LangBot 内部以 str 存储 tool_calls 的参数,这里需要转换为 dict - for tool_call in msg["tool_calls"]: - tool_call["function"]["arguments"] = json.loads( - tool_call["function"]["arguments"] + for tool_call in msg['tool_calls']: + tool_call['function']['arguments'] = json.loads( + tool_call['function']['arguments'] ) - args["messages"] = messages + args['messages'] = messages - args["tools"] = [] + args['tools'] = [] if user_funcs: tools = await self.ap.tool_mgr.generate_tools_for_openai(user_funcs) if tools: - args["tools"] = tools + args['tools'] = tools resp = await self._req(args) message: llm_entities.Message = await self._make_msg(resp) @@ -93,7 +90,7 @@ class OllamaChatCompletions(requester.LLMAPIRequester): ret_msg: llm_entities.Message = None if message.content is not None: - ret_msg = llm_entities.Message(role="assistant", content=message.content) + ret_msg = llm_entities.Message(role='assistant', content=message.content) if message.tool_calls is not None and len(message.tool_calls) > 0: tool_calls: list[llm_entities.ToolCall] = [] @@ -101,7 +98,7 @@ class OllamaChatCompletions(requester.LLMAPIRequester): tool_calls.append( llm_entities.ToolCall( id=uuid.uuid4().hex, - type="function", + type='function', function=llm_entities.FunctionCall( name=tool_call.function.name, arguments=json.dumps(tool_call.function.arguments), @@ -123,13 +120,13 @@ class OllamaChatCompletions(requester.LLMAPIRequester): req_messages: list = [] for m in messages: msg_dict: dict = m.dict(exclude_none=True) - content: Any = msg_dict.get("content") + content: Any = msg_dict.get('content') if isinstance(content, list): if all( - isinstance(part, dict) and part.get("type") == "text" + isinstance(part, dict) and part.get('type') == 'text' for part in content ): - msg_dict["content"] = "\n".join(part["text"] for part in content) + msg_dict['content'] = '\n'.join(part['text'] for part in content) req_messages.append(msg_dict) try: return await self._closure( @@ -140,4 +137,4 @@ class OllamaChatCompletions(requester.LLMAPIRequester): extra_args=extra_args, ) except asyncio.TimeoutError: - raise errors.RequesterError("请求超时") + raise errors.RequesterError('请求超时') diff --git a/pkg/provider/modelmgr/requesters/siliconflowchatcmpl.py b/pkg/provider/modelmgr/requesters/siliconflowchatcmpl.py index dd5b9a14..3636d9d1 100644 --- a/pkg/provider/modelmgr/requesters/siliconflowchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/siliconflowchatcmpl.py @@ -4,8 +4,6 @@ import typing import openai from . import chatcmpl -from .. import requester -from ....core import app class SiliconFlowChatCompletions(chatcmpl.OpenAIChatCompletions): diff --git a/pkg/provider/modelmgr/requesters/volcarkchatcmpl.py b/pkg/provider/modelmgr/requesters/volcarkchatcmpl.py index 9b5505e1..7eb68956 100644 --- a/pkg/provider/modelmgr/requesters/volcarkchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/volcarkchatcmpl.py @@ -4,8 +4,6 @@ import typing import openai from . import chatcmpl -from .. import requester -from ....core import app class VolcArkChatCompletions(chatcmpl.OpenAIChatCompletions): diff --git a/pkg/provider/modelmgr/requesters/xaichatcmpl.py b/pkg/provider/modelmgr/requesters/xaichatcmpl.py index e08af875..db2022f1 100644 --- a/pkg/provider/modelmgr/requesters/xaichatcmpl.py +++ b/pkg/provider/modelmgr/requesters/xaichatcmpl.py @@ -4,8 +4,6 @@ import typing import openai from . import chatcmpl -from .. import requester -from ....core import app class XaiChatCompletions(chatcmpl.OpenAIChatCompletions): diff --git a/pkg/provider/modelmgr/requesters/zhipuaichatcmpl.py b/pkg/provider/modelmgr/requesters/zhipuaichatcmpl.py index 7bbca164..a1a07068 100644 --- a/pkg/provider/modelmgr/requesters/zhipuaichatcmpl.py +++ b/pkg/provider/modelmgr/requesters/zhipuaichatcmpl.py @@ -3,9 +3,7 @@ from __future__ import annotations import typing import openai -from ....core import app from . import chatcmpl -from .. import requester class ZhipuAIChatCompletions(chatcmpl.OpenAIChatCompletions): diff --git a/pkg/provider/modelmgr/token.py b/pkg/provider/modelmgr/token.py index eeec6986..9f477243 100644 --- a/pkg/provider/modelmgr/token.py +++ b/pkg/provider/modelmgr/token.py @@ -3,9 +3,8 @@ from __future__ import annotations import typing -class TokenManager(): - """鉴权 Token 管理器 - """ +class TokenManager: + """鉴权 Token 管理器""" name: str @@ -20,6 +19,6 @@ class TokenManager(): def get_token(self) -> str: return self.tokens[self.using_token_index] - + def next_token(self): self.using_token_index = (self.using_token_index + 1) % len(self.tokens) diff --git a/pkg/provider/runner.py b/pkg/provider/runner.py index 1762e546..ccfcee73 100644 --- a/pkg/provider/runner.py +++ b/pkg/provider/runner.py @@ -9,9 +9,10 @@ from . import entities as llm_entities preregistered_runners: list[typing.Type[RequestRunner]] = [] + def runner_class(name: str): - """注册一个请求运行器 - """ + """注册一个请求运行器""" + def decorator(cls: typing.Type[RequestRunner]) -> typing.Type[RequestRunner]: cls.name = name preregistered_runners.append(cls) @@ -21,8 +22,8 @@ def runner_class(name: str): class RequestRunner(abc.ABC): - """请求运行器 - """ + """请求运行器""" + name: str = None ap: app.Application @@ -34,7 +35,8 @@ class RequestRunner(abc.ABC): self.pipeline_config = pipeline_config @abc.abstractmethod - async def run(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: - """运行请求 - """ + async def run( + self, query: core_entities.Query + ) -> typing.AsyncGenerator[llm_entities.Message, None]: + """运行请求""" pass diff --git a/pkg/provider/runners/dashscopeapi.py b/pkg/provider/runners/dashscopeapi.py index d5e6a83d..92a1eb18 100644 --- a/pkg/provider/runners/dashscopeapi.py +++ b/pkg/provider/runners/dashscopeapi.py @@ -1,8 +1,6 @@ from __future__ import annotations import typing -import json -import base64 import re import dashscope @@ -10,7 +8,7 @@ import dashscope from .. import runner from ...core import app, entities as core_entities from .. import entities as llm_entities -from ...utils import image + class DashscopeAPIError(Exception): """Dashscope API 请求失败""" @@ -20,49 +18,49 @@ class DashscopeAPIError(Exception): super().__init__(self.message) -@runner.runner_class("dashscope-app-api") +@runner.runner_class('dashscope-app-api') class DashScopeAPIRunner(runner.RequestRunner): "阿里云百炼DashsscopeAPI对话请求器" - + # 运行器内部使用的配置 - app_type: str # 应用类型 - app_id: str # 应用ID - api_key: str # API Key - references_quote: str # 引用资料提示(当展示回答来源功能开启时,这个变量会作为引用资料名前的提示,可在provider.json中配置) + app_type: str # 应用类型 + app_id: str # 应用ID + api_key: str # API Key + references_quote: str # 引用资料提示(当展示回答来源功能开启时,这个变量会作为引用资料名前的提示,可在provider.json中配置) def __init__(self, ap: app.Application, pipeline_config: dict): """初始化""" self.ap = ap self.pipeline_config = pipeline_config - valid_app_types = ["agent", "workflow"] - self.app_type = self.pipeline_config["ai"]["dashscope-app-api"]["app-type"] - #检查配置文件中使用的应用类型是否支持 - if (self.app_type not in valid_app_types): - raise DashscopeAPIError( - f"不支持的 Dashscope 应用类型: {self.app_type}" - ) - - #初始化Dashscope 参数配置 - self.app_id = self.pipeline_config["ai"]["dashscope-app-api"]["app-id"] - self.api_key = self.pipeline_config["ai"]["dashscope-app-api"]["api-key"] - self.references_quote = self.pipeline_config["ai"]["dashscope-app-api"]["references_quote"] - + valid_app_types = ['agent', 'workflow'] + self.app_type = self.pipeline_config['ai']['dashscope-app-api']['app-type'] + # 检查配置文件中使用的应用类型是否支持 + if self.app_type not in valid_app_types: + raise DashscopeAPIError(f'不支持的 Dashscope 应用类型: {self.app_type}') + + # 初始化Dashscope 参数配置 + self.app_id = self.pipeline_config['ai']['dashscope-app-api']['app-id'] + self.api_key = self.pipeline_config['ai']['dashscope-app-api']['api-key'] + self.references_quote = self.pipeline_config['ai']['dashscope-app-api'][ + 'references_quote' + ] + def _replace_references(self, text, references_dict): """阿里云百炼平台的自定义应用支持资料引用,此函数可以将引用标签替换为参考资料""" - + # 匹配 [index_id] 形式的字符串 pattern = re.compile(r'\[(.*?)\]') def replacement(match): # 获取引用编号 - ref_key = match.group(1) + ref_key = match.group(1) if ref_key in references_dict: # 如果有对应的参考资料按照provider.json中的reference_quote返回提示,来自哪个参考资料文件 - return f"({self.references_quote} {references_dict[ref_key]})" + return f'({self.references_quote} {references_dict[ref_key]})' else: # 如果没有对应的参考资料,保留原样 - return match.group(0) + return match.group(0) # 使用 re.sub() 进行替换 return pattern.sub(replacement, text) @@ -71,14 +69,14 @@ class DashScopeAPIRunner(runner.RequestRunner): self, query: core_entities.Query ) -> tuple[str, list[str]]: """预处理用户消息,提取纯文本,阿里云提供的上传文件方法过于复杂,暂不支持上传文件(包括图片)""" - plain_text = "" + plain_text = '' image_ids = [] if isinstance(query.user_message.content, list): for ce in query.user_message.content: - if ce.type == "text": + if ce.type == 'text': plain_text += ce.text # 暂时不支持上传图片,保留代码以便后续扩展 - # elif ce.type == "image_base64": + # elif ce.type == "image_base64": # image_b64, image_format = await image.extract_b64_and_format(ce.image_base64) # file_bytes = base64.b64decode(image_b64) # file = ("img.png", file_bytes, f"image/{image_format}") @@ -92,147 +90,141 @@ class DashScopeAPIRunner(runner.RequestRunner): plain_text = query.user_message.content return plain_text, image_ids - - + async def _agent_messages( self, query: core_entities.Query ) -> typing.AsyncGenerator[llm_entities.Message, None]: """Dashscope 智能体对话请求""" - - #局部变量 - chunk = None # 流式传输的块 - pending_content = "" # 待处理的Agent输出内容 - references_dict = {} # 用于存储引用编号和对应的参考资料 - plain_text = "" # 用户输入的纯文本信息 - image_ids = [] # 用户输入的图片ID列表 (暂不支持) - + + # 局部变量 + chunk = None # 流式传输的块 + pending_content = '' # 待处理的Agent输出内容 + references_dict = {} # 用于存储引用编号和对应的参考资料 + plain_text = '' # 用户输入的纯文本信息 + image_ids = [] # 用户输入的图片ID列表 (暂不支持) + plain_text, image_ids = await self._preprocess_user_message(query) - - #发送对话请求 + + # 发送对话请求 response = dashscope.Application.call( - api_key=self.api_key, # 智能体应用的API Key - app_id=self.app_id, # 智能体应用的ID - prompt=plain_text, # 用户输入的文本信息 - stream=True, # 流式输出 - incremental_output=True, # 增量输出,使用流式输出需要开启增量输出 - session_id=query.session.using_conversation.uuid, # 会话ID用于,多轮对话 + api_key=self.api_key, # 智能体应用的API Key + app_id=self.app_id, # 智能体应用的ID + prompt=plain_text, # 用户输入的文本信息 + stream=True, # 流式输出 + incremental_output=True, # 增量输出,使用流式输出需要开启增量输出 + session_id=query.session.using_conversation.uuid, # 会话ID用于,多轮对话 # rag_options={ # 主要用于文件交互,暂不支持 # "session_file_ids": ["FILE_ID1"], # FILE_ID1 替换为实际的临时文件ID,逗号隔开多个 # } ) for chunk in response: - if chunk.get("status_code") != 200: + if chunk.get('status_code') != 200: raise DashscopeAPIError( - f"Dashscope API 请求失败: status_code={chunk.get('status_code')} message={chunk.get('message')} request_id={chunk.get('request_id')} " + f'Dashscope API 请求失败: status_code={chunk.get("status_code")} message={chunk.get("message")} request_id={chunk.get("request_id")} ' ) if not chunk: continue - - #获取流式传输的output - stream_output = chunk.get("output", {}) - if stream_output.get("text") is not None: - pending_content += stream_output.get("text") - - #保存当前会话的session_id用于下次对话的语境 - query.session.using_conversation.uuid = stream_output.get("session_id") - - #获取模型传出的参考资料列表 - references_dict_list = stream_output.get("doc_references", []) - - #从模型传出的参考资料信息中提取用于替换的字典 + + # 获取流式传输的output + stream_output = chunk.get('output', {}) + if stream_output.get('text') is not None: + pending_content += stream_output.get('text') + + # 保存当前会话的session_id用于下次对话的语境 + query.session.using_conversation.uuid = stream_output.get('session_id') + + # 获取模型传出的参考资料列表 + references_dict_list = stream_output.get('doc_references', []) + + # 从模型传出的参考资料信息中提取用于替换的字典 if references_dict_list is not None: for doc in references_dict_list: - if doc.get("index_id") is not None: - references_dict[doc.get("index_id")] = doc.get("doc_name") - - #将参考资料替换到文本中 + if doc.get('index_id') is not None: + references_dict[doc.get('index_id')] = doc.get('doc_name') + + # 将参考资料替换到文本中 pending_content = self._replace_references(pending_content, references_dict) - + yield llm_entities.Message( - role="assistant", + role='assistant', content=pending_content, ) - - + async def _workflow_messages( self, query: core_entities.Query ) -> typing.AsyncGenerator[llm_entities.Message, None]: """Dashscope 工作流对话请求""" - - #局部变量 - chunk = None # 流式传输的块 - pending_content = "" # 待处理的Agent输出内容 - references_dict = {} # 用于存储引用编号和对应的参考资料 - plain_text = "" # 用户输入的纯文本信息 - image_ids = [] # 用户输入的图片ID列表 (暂不支持) - + + # 局部变量 + chunk = None # 流式传输的块 + pending_content = '' # 待处理的Agent输出内容 + references_dict = {} # 用于存储引用编号和对应的参考资料 + plain_text = '' # 用户输入的纯文本信息 + image_ids = [] # 用户输入的图片ID列表 (暂不支持) + plain_text, image_ids = await self._preprocess_user_message(query) biz_params = {} biz_params.update(query.variables) - - #发送对话请求 + + # 发送对话请求 response = dashscope.Application.call( - api_key=self.api_key, # 智能体应用的API Key - app_id=self.app_id, # 智能体应用的ID - prompt=plain_text, # 用户输入的文本信息 - stream=True, # 流式输出 - incremental_output=True, # 增量输出,使用流式输出需要开启增量输出 - session_id=query.session.using_conversation.uuid, # 会话ID用于,多轮对话 - biz_params=biz_params, # 工作流应用的自定义输入参数传递 + api_key=self.api_key, # 智能体应用的API Key + app_id=self.app_id, # 智能体应用的ID + prompt=plain_text, # 用户输入的文本信息 + stream=True, # 流式输出 + incremental_output=True, # 增量输出,使用流式输出需要开启增量输出 + session_id=query.session.using_conversation.uuid, # 会话ID用于,多轮对话 + biz_params=biz_params, # 工作流应用的自定义输入参数传递 # rag_options={ # 主要用于文件交互,暂不支持 # "session_file_ids": ["FILE_ID1"], # FILE_ID1 替换为实际的临时文件ID,逗号隔开多个 # } ) - - #处理API返回的流式输出 + + # 处理API返回的流式输出 for chunk in response: - if chunk.get("status_code") != 200: + if chunk.get('status_code') != 200: raise DashscopeAPIError( - f"Dashscope API 请求失败: status_code={chunk.get('status_code')} message={chunk.get('message')} request_id={chunk.get('request_id')} " + f'Dashscope API 请求失败: status_code={chunk.get("status_code")} message={chunk.get("message")} request_id={chunk.get("request_id")} ' ) if not chunk: continue - - #获取流式传输的output - stream_output = chunk.get("output", {}) - if stream_output.get("text") is not None: - pending_content += stream_output.get("text") - - #保存当前会话的session_id用于下次对话的语境 - query.session.using_conversation.uuid = stream_output.get("session_id") - - #获取模型传出的参考资料列表 - references_dict_list = stream_output.get("doc_references", []) - - #从模型传出的参考资料信息中提取用于替换的字典 + + # 获取流式传输的output + stream_output = chunk.get('output', {}) + if stream_output.get('text') is not None: + pending_content += stream_output.get('text') + + # 保存当前会话的session_id用于下次对话的语境 + query.session.using_conversation.uuid = stream_output.get('session_id') + + # 获取模型传出的参考资料列表 + references_dict_list = stream_output.get('doc_references', []) + + # 从模型传出的参考资料信息中提取用于替换的字典 if references_dict_list is not None: for doc in references_dict_list: - if doc.get("index_id") is not None: - references_dict[doc.get("index_id")] = doc.get("doc_name") - - #将参考资料替换到文本中 + if doc.get('index_id') is not None: + references_dict[doc.get('index_id')] = doc.get('doc_name') + + # 将参考资料替换到文本中 pending_content = self._replace_references(pending_content, references_dict) - + yield llm_entities.Message( - role="assistant", + role='assistant', content=pending_content, ) - + async def run( self, query: core_entities.Query ) -> typing.AsyncGenerator[llm_entities.Message, None]: """运行""" - if self.app_type == "agent": + if self.app_type == 'agent': async for msg in self._agent_messages(query): yield msg - elif self.app_type == "workflow": + elif self.app_type == 'workflow': async for msg in self._workflow_messages(query): yield msg else: - raise DashscopeAPIError( - f"不支持的 Dashscope 应用类型: {self.app_type}" - ) - - + raise DashscopeAPIError(f'不支持的 Dashscope 应用类型: {self.app_type}') diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index f48cbd57..1d1576a6 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -5,9 +5,7 @@ import json import uuid import re import base64 -import datetime -import aiohttp from .. import runner from ...core import app, entities as core_entities @@ -17,7 +15,7 @@ from ...utils import image from libs.dify_service_api.v1 import client, errors -@runner.runner_class("dify-service-api") +@runner.runner_class('dify-service-api') class DifyServiceAPIRunner(runner.RequestRunner): """Dify Service API 对话请求器""" @@ -27,38 +25,54 @@ class DifyServiceAPIRunner(runner.RequestRunner): self.ap = ap self.pipeline_config = pipeline_config - valid_app_types = ["chat", "agent", "workflow"] + valid_app_types = ['chat', 'agent', 'workflow'] if ( - self.pipeline_config["ai"]["dify-service-api"]["app-type"] + self.pipeline_config['ai']['dify-service-api']['app-type'] not in valid_app_types ): raise errors.DifyAPIError( - f"不支持的 Dify 应用类型: {self.pipeline_config['ai']['dify-service-api']['app-type']}" + f'不支持的 Dify 应用类型: {self.pipeline_config["ai"]["dify-service-api"]["app-type"]}' ) - api_key = self.pipeline_config["ai"]["dify-service-api"]["api-key"] + api_key = self.pipeline_config['ai']['dify-service-api']['api-key'] self.dify_client = client.AsyncDifyServiceClient( api_key=api_key, - base_url=self.pipeline_config["ai"]["dify-service-api"]["base-url"], + base_url=self.pipeline_config['ai']['dify-service-api']['base-url'], ) def _try_convert_thinking(self, resp_text: str) -> str: """尝试转换 Dify 的思考提示""" - if not resp_text.startswith(" Thinking... "): + if not resp_text.startswith( + ' Thinking... ' + ): return resp_text - if self.pipeline_config["ai"]["dify-service-api"]["thinking-convert"] == "original": + if ( + self.pipeline_config['ai']['dify-service-api']['thinking-convert'] + == 'original' + ): return resp_text - - if self.pipeline_config["ai"]["dify-service-api"]["thinking-convert"] == "remove": - return re.sub(r' Thinking... .*?', '', resp_text, flags=re.DOTALL) - - if self.pipeline_config["ai"]["dify-service-api"]["thinking-convert"] == "plain": + + if ( + self.pipeline_config['ai']['dify-service-api']['thinking-convert'] + == 'remove' + ): + return re.sub( + r' Thinking... .*?', + '', + resp_text, + flags=re.DOTALL, + ) + + if ( + self.pipeline_config['ai']['dify-service-api']['thinking-convert'] + == 'plain' + ): pattern = r' Thinking... (.*?)' thinking_text = re.search(pattern, resp_text, flags=re.DOTALL) content_text = re.sub(pattern, '', resp_text, flags=re.DOTALL) - return f"{thinking_text.group(1)}\n{content_text}" + return f'{thinking_text.group(1)}\n{content_text}' async def _preprocess_user_message( self, query: core_entities.Query @@ -68,22 +82,24 @@ class DifyServiceAPIRunner(runner.RequestRunner): Returns: tuple[str, list[str]]: 纯文本和图片的 Dify 服务图片 ID """ - plain_text = "" + plain_text = '' image_ids = [] if isinstance(query.user_message.content, list): for ce in query.user_message.content: - if ce.type == "text": + if ce.type == 'text': plain_text += ce.text - elif ce.type == "image_base64": - image_b64, image_format = await image.extract_b64_and_format(ce.image_base64) + elif ce.type == 'image_base64': + image_b64, image_format = await image.extract_b64_and_format( + ce.image_base64 + ) file_bytes = base64.b64decode(image_b64) - file = ("img.png", file_bytes, f"image/{image_format}") + file = ('img.png', file_bytes, f'image/{image_format}') file_upload_resp = await self.dify_client.upload_file( file, - f"{query.session.launcher_type.value}_{query.session.launcher_id}", + f'{query.session.launcher_type.value}_{query.session.launcher_id}', ) - image_id = file_upload_resp["id"] + image_id = file_upload_resp['id'] image_ids.append(image_id) elif isinstance(query.user_message.content, str): plain_text = query.user_message.content @@ -94,116 +110,119 @@ class DifyServiceAPIRunner(runner.RequestRunner): self, query: core_entities.Query ) -> typing.AsyncGenerator[llm_entities.Message, None]: """调用聊天助手""" - cov_id = query.session.using_conversation.uuid or "" + cov_id = query.session.using_conversation.uuid or '' plain_text, image_ids = await self._preprocess_user_message(query) files = [ { - "type": "image", - "transfer_method": "local_file", - "upload_file_id": image_id, + 'type': 'image', + 'transfer_method': 'local_file', + 'upload_file_id': image_id, } for image_id in image_ids ] - mode = "basic" # 标记是基础编排还是工作流编排 + mode = 'basic' # 标记是基础编排还是工作流编排 basic_mode_pending_chunk = '' inputs = {} - + inputs.update(query.variables) async for chunk in self.dify_client.chat_messages( inputs=inputs, query=plain_text, - user=f"{query.session.launcher_type.value}_{query.session.launcher_id}", + user=f'{query.session.launcher_type.value}_{query.session.launcher_id}', conversation_id=cov_id, files=files, - timeout=self.pipeline_config["ai"]["dify-service-api"]["timeout"], + timeout=self.pipeline_config['ai']['dify-service-api']['timeout'], ): - self.ap.logger.debug("dify-chat-chunk: " + str(chunk)) + self.ap.logger.debug('dify-chat-chunk: ' + str(chunk)) if chunk['event'] == 'workflow_started': - mode = "workflow" + mode = 'workflow' - if mode == "workflow": + if mode == 'workflow': if chunk['event'] == 'node_finished': if chunk['data']['node_type'] == 'answer': yield llm_entities.Message( - role="assistant", - content=self._try_convert_thinking(chunk['data']['outputs']['answer']), + role='assistant', + content=self._try_convert_thinking( + chunk['data']['outputs']['answer'] + ), ) - elif mode == "basic": + elif mode == 'basic': if chunk['event'] == 'message': basic_mode_pending_chunk += chunk['answer'] elif chunk['event'] == 'message_end': yield llm_entities.Message( - role="assistant", + role='assistant', content=self._try_convert_thinking(basic_mode_pending_chunk), ) basic_mode_pending_chunk = '' - query.session.using_conversation.uuid = chunk["conversation_id"] + query.session.using_conversation.uuid = chunk['conversation_id'] async def _agent_chat_messages( self, query: core_entities.Query ) -> typing.AsyncGenerator[llm_entities.Message, None]: """调用聊天助手""" - cov_id = query.session.using_conversation.uuid or "" + cov_id = query.session.using_conversation.uuid or '' plain_text, image_ids = await self._preprocess_user_message(query) files = [ { - "type": "image", - "transfer_method": "local_file", - "upload_file_id": image_id, + 'type': 'image', + 'transfer_method': 'local_file', + 'upload_file_id': image_id, } for image_id in image_ids ] - ignored_events = ["agent_message"] + ignored_events = ['agent_message'] inputs = {} - + inputs.update(query.variables) async for chunk in self.dify_client.chat_messages( inputs=inputs, query=plain_text, - user=f"{query.session.launcher_type.value}_{query.session.launcher_id}", - response_mode="streaming", + user=f'{query.session.launcher_type.value}_{query.session.launcher_id}', + response_mode='streaming', conversation_id=cov_id, files=files, - timeout=self.pipeline_config["ai"]["dify-service-api"]["timeout"], + timeout=self.pipeline_config['ai']['dify-service-api']['timeout'], ): - self.ap.logger.debug("dify-agent-chunk: " + str(chunk)) + self.ap.logger.debug('dify-agent-chunk: ' + str(chunk)) - if chunk["event"] in ignored_events: + if chunk['event'] in ignored_events: continue - if chunk["event"] == "agent_thought": - - if chunk['tool'] != '' and chunk['observation'] != '': # 工具调用结果,跳过 + if chunk['event'] == 'agent_thought': + if ( + chunk['tool'] != '' and chunk['observation'] != '' + ): # 工具调用结果,跳过 continue if chunk['thought'].strip() != '': # 文字回复内容 msg = llm_entities.Message( - role="assistant", - content=chunk["thought"], + role='assistant', + content=chunk['thought'], ) yield msg if chunk['tool']: msg = llm_entities.Message( - role="assistant", + role='assistant', tool_calls=[ llm_entities.ToolCall( id=chunk['id'], - type="function", + type='function', function=llm_entities.FunctionCall( - name=chunk["tool"], + name=chunk['tool'], arguments=json.dumps({}), ), ) @@ -211,9 +230,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): ) yield msg if chunk['event'] == 'message_file': - if chunk['type'] == 'image' and chunk['belongs_to'] == 'assistant': - base_url = self.dify_client.base_url if base_url.endswith('/v1'): @@ -222,11 +239,11 @@ class DifyServiceAPIRunner(runner.RequestRunner): image_url = base_url + chunk['url'] yield llm_entities.Message( - role="assistant", + role='assistant', content=[llm_entities.ContentElement.from_image_url(image_url)], ) - query.session.using_conversation.uuid = chunk["conversation_id"] + query.session.using_conversation.uuid = chunk['conversation_id'] async def _workflow_messages( self, query: core_entities.Query @@ -235,58 +252,57 @@ class DifyServiceAPIRunner(runner.RequestRunner): if not query.session.using_conversation.uuid: query.session.using_conversation.uuid = str(uuid.uuid4()) - - query.variables["conversation_id"] = query.session.using_conversation.uuid + + query.variables['conversation_id'] = query.session.using_conversation.uuid plain_text, image_ids = await self._preprocess_user_message(query) files = [ { - "type": "image", - "transfer_method": "local_file", - "upload_file_id": image_id, + 'type': 'image', + 'transfer_method': 'local_file', + 'upload_file_id': image_id, } for image_id in image_ids ] - ignored_events = ["text_chunk", "workflow_started"] + ignored_events = ['text_chunk', 'workflow_started'] inputs = { # these variables are legacy variables, we need to keep them for compatibility - "langbot_user_message_text": plain_text, - "langbot_session_id": query.variables["session_id"], - "langbot_conversation_id": query.variables["conversation_id"], - "langbot_msg_create_time": query.variables["msg_create_time"], + 'langbot_user_message_text': plain_text, + 'langbot_session_id': query.variables['session_id'], + 'langbot_conversation_id': query.variables['conversation_id'], + 'langbot_msg_create_time': query.variables['msg_create_time'], } - + inputs.update(query.variables) async for chunk in self.dify_client.workflow_run( inputs=inputs, - user=f"{query.session.launcher_type.value}_{query.session.launcher_id}", + user=f'{query.session.launcher_type.value}_{query.session.launcher_id}', files=files, - timeout=self.pipeline_config["ai"]["dify-service-api"]["timeout"], + timeout=self.pipeline_config['ai']['dify-service-api']['timeout'], ): - self.ap.logger.debug("dify-workflow-chunk: " + str(chunk)) - if chunk["event"] in ignored_events: + self.ap.logger.debug('dify-workflow-chunk: ' + str(chunk)) + if chunk['event'] in ignored_events: continue - if chunk["event"] == "node_started": - + if chunk['event'] == 'node_started': if ( - chunk["data"]["node_type"] == "start" - or chunk["data"]["node_type"] == "end" + chunk['data']['node_type'] == 'start' + or chunk['data']['node_type'] == 'end' ): continue msg = llm_entities.Message( - role="assistant", + role='assistant', content=None, tool_calls=[ llm_entities.ToolCall( - id=chunk["data"]["node_id"], - type="function", + id=chunk['data']['node_id'], + type='function', function=llm_entities.FunctionCall( - name=chunk["data"]["title"], + name=chunk['data']['title'], arguments=json.dumps({}), ), ) @@ -295,13 +311,13 @@ class DifyServiceAPIRunner(runner.RequestRunner): yield msg - elif chunk["event"] == "workflow_finished": + elif chunk['event'] == 'workflow_finished': if chunk['data']['error']: raise errors.DifyAPIError(chunk['data']['error']) msg = llm_entities.Message( - role="assistant", - content=chunk["data"]["outputs"]["summary"], + role='assistant', + content=chunk['data']['outputs']['summary'], ) yield msg @@ -310,16 +326,16 @@ class DifyServiceAPIRunner(runner.RequestRunner): self, query: core_entities.Query ) -> typing.AsyncGenerator[llm_entities.Message, None]: """运行请求""" - if self.pipeline_config["ai"]["dify-service-api"]["app-type"] == "chat": + if self.pipeline_config['ai']['dify-service-api']['app-type'] == 'chat': async for msg in self._chat_messages(query): yield msg - elif self.pipeline_config["ai"]["dify-service-api"]["app-type"] == "agent": + elif self.pipeline_config['ai']['dify-service-api']['app-type'] == 'agent': async for msg in self._agent_chat_messages(query): yield msg - elif self.pipeline_config["ai"]["dify-service-api"]["app-type"] == "workflow": + elif self.pipeline_config['ai']['dify-service-api']['app-type'] == 'workflow': async for msg in self._workflow_messages(query): yield msg else: raise errors.DifyAPIError( - f"不支持的 Dify 应用类型: {self.pipeline_config['ai']['dify-service-api']['app-type']}" + f'不支持的 Dify 应用类型: {self.pipeline_config["ai"]["dify-service-api"]["app-type"]}' ) diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 68bb2b4f..d6b6f6cd 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -4,24 +4,28 @@ import json import typing from .. import runner -from ...core import app, entities as core_entities +from ...core import entities as core_entities from .. import entities as llm_entities -@runner.runner_class("local-agent") +@runner.runner_class('local-agent') class LocalAgentRunner(runner.RequestRunner): - """本地Agent请求运行器 - """ + """本地Agent请求运行器""" - async def run(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: - """运行请求 - """ + async def run( + self, query: core_entities.Query + ) -> typing.AsyncGenerator[llm_entities.Message, None]: + """运行请求""" pending_tool_calls = [] - req_messages = query.prompt.messages.copy() + query.messages.copy() + [query.user_message] + req_messages = ( + query.prompt.messages.copy() + query.messages.copy() + [query.user_message] + ) # 首次请求 - msg = await query.use_llm_model.requester.invoke_llm(query, query.use_llm_model, req_messages, query.use_funcs) + msg = await query.use_llm_model.requester.invoke_llm( + query, query.use_llm_model, req_messages, query.use_funcs + ) yield msg @@ -34,7 +38,7 @@ class LocalAgentRunner(runner.RequestRunner): for tool_call in pending_tool_calls: try: func = tool_call.function - + parameters = json.loads(func.arguments) func_ret = await self.ap.tool_mgr.execute_func_call( @@ -42,7 +46,9 @@ class LocalAgentRunner(runner.RequestRunner): ) msg = llm_entities.Message( - role="tool", content=json.dumps(func_ret, ensure_ascii=False), tool_call_id=tool_call.id + role='tool', + content=json.dumps(func_ret, ensure_ascii=False), + tool_call_id=tool_call.id, ) yield msg @@ -51,7 +57,7 @@ class LocalAgentRunner(runner.RequestRunner): except Exception as e: # 工具调用出错,添加一个报错信息到 req_messages err_msg = llm_entities.Message( - role="tool", content=f"err: {e}", tool_call_id=tool_call.id + role='tool', content=f'err: {e}', tool_call_id=tool_call.id ) yield err_msg @@ -59,7 +65,9 @@ class LocalAgentRunner(runner.RequestRunner): req_messages.append(err_msg) # 处理完所有调用,再次请求 - msg = await query.use_llm_model.requester.invoke_llm(query, query.use_llm_model, req_messages, query.use_funcs) + msg = await query.use_llm_model.requester.invoke_llm( + query, query.use_llm_model, req_messages, query.use_funcs + ) yield msg diff --git a/pkg/provider/session/sessionmgr.py b/pkg/provider/session/sessionmgr.py index eef723da..a0a582ad 100644 --- a/pkg/provider/session/sessionmgr.py +++ b/pkg/provider/session/sessionmgr.py @@ -3,13 +3,11 @@ from __future__ import annotations import asyncio from ...core import app, entities as core_entities -from ...plugin import context as plugin_context from ...provider import entities as provider_entities class SessionManager: - """会话管理器 - """ + """会话管理器""" ap: app.Application @@ -23,10 +21,12 @@ class SessionManager: pass async def get_session(self, query: core_entities.Query) -> core_entities.Session: - """获取会话 - """ + """获取会话""" for session in self.session_list: - if query.launcher_type == session.launcher_type and query.launcher_id == session.launcher_id: + if ( + query.launcher_type == session.launcher_type + and query.launcher_id == session.launcher_id + ): return session session_concurrency = self.ap.instance_config.data['concurrency']['session'] @@ -39,7 +39,12 @@ class SessionManager: self.session_list.append(session) return session - async def get_conversation(self, query: core_entities.Query, session: core_entities.Session, prompt_config: list[dict]) -> core_entities.Conversation: + async def get_conversation( + self, + query: core_entities.Query, + session: core_entities.Session, + prompt_config: list[dict], + ) -> core_entities.Conversation: """获取对话或创建对话""" if not session.conversations: @@ -52,7 +57,7 @@ class SessionManager: prompt_messages.append(provider_entities.Message(**prompt_message)) prompt = provider_entities.Prompt( - name="default", + name='default', messages=prompt_messages, ) diff --git a/pkg/provider/tools/entities.py b/pkg/provider/tools/entities.py index 746ffe92..102e03d3 100644 --- a/pkg/provider/tools/entities.py +++ b/pkg/provider/tools/entities.py @@ -1,13 +1,9 @@ from __future__ import annotations -import abc import typing -import asyncio import pydantic.v1 as pydantic -from ...core import entities as core_entities - class LLMFunction(pydantic.BaseModel): """函数""" diff --git a/pkg/provider/tools/loader.py b/pkg/provider/tools/loader.py index cae4a63f..25bb13eb 100644 --- a/pkg/provider/tools/loader.py +++ b/pkg/provider/tools/loader.py @@ -9,9 +9,10 @@ from . import entities as tools_entities preregistered_loaders: list[typing.Type[ToolLoader]] = [] + def loader_class(name: str): - """注册一个工具加载器 - """ + """注册一个工具加载器""" + def decorator(cls: typing.Type[ToolLoader]) -> typing.Type[ToolLoader]: cls.name = name preregistered_loaders.append(cls) @@ -22,7 +23,7 @@ def loader_class(name: str): class ToolLoader(abc.ABC): """工具加载器""" - + name: str = None ap: app.Application @@ -34,7 +35,7 @@ class ToolLoader(abc.ABC): pass @abc.abstractmethod - async def get_tools(self, enabled: bool=True) -> list[tools_entities.LLMFunction]: + async def get_tools(self, enabled: bool = True) -> list[tools_entities.LLMFunction]: """获取所有工具""" pass @@ -44,11 +45,13 @@ class ToolLoader(abc.ABC): pass @abc.abstractmethod - async def invoke_tool(self, query: core_entities.Query, name: str, parameters: dict) -> typing.Any: + async def invoke_tool( + self, query: core_entities.Query, name: str, parameters: dict + ) -> typing.Any: """执行工具调用""" pass @abc.abstractmethod async def shutdown(self): """关闭工具""" - pass \ No newline at end of file + pass diff --git a/pkg/provider/tools/loaders/mcp.py b/pkg/provider/tools/loaders/mcp.py index 6bda7f89..5377709f 100644 --- a/pkg/provider/tools/loaders/mcp.py +++ b/pkg/provider/tools/loaders/mcp.py @@ -30,7 +30,7 @@ class RuntimeMCPSession: self.server_name = server_name self.server_config = server_config self.ap = ap - + self.session = None self.exit_stack = AsyncExitStack() @@ -38,9 +38,9 @@ class RuntimeMCPSession: async def _init_stdio_python_server(self): server_params = StdioServerParameters( - command=self.server_config["command"], - args=self.server_config["args"], - env=self.server_config["env"], + command=self.server_config['command'], + args=self.server_config['args'], + env=self.server_config['env'], ) stdio_transport = await self.exit_stack.enter_async_context( @@ -58,12 +58,12 @@ class RuntimeMCPSession: async def _init_sse_server(self): sse_transport = await self.exit_stack.enter_async_context( sse_client( - self.server_config["url"], - headers=self.server_config.get("headers", {}), - timeout=self.server_config.get("timeout", 10), + self.server_config['url'], + headers=self.server_config.get('headers', {}), + timeout=self.server_config.get('timeout', 10), ) ) - + sseio, write = sse_transport self.session = await self.exit_stack.enter_async_context( @@ -73,18 +73,22 @@ class RuntimeMCPSession: await self.session.initialize() async def initialize(self): - self.ap.logger.debug(f"初始化 MCP 会话: {self.server_name} {self.server_config}") + self.ap.logger.debug( + f'初始化 MCP 会话: {self.server_name} {self.server_config}' + ) - if self.server_config["mode"] == "stdio": + if self.server_config['mode'] == 'stdio': await self._init_stdio_python_server() - elif self.server_config["mode"] == "sse": + elif self.server_config['mode'] == 'sse': await self._init_sse_server() else: - raise ValueError(f"无法识别 MCP 服务器类型: {self.server_name}: {self.server_config}") - + raise ValueError( + f'无法识别 MCP 服务器类型: {self.server_name}: {self.server_config}' + ) + tools = await self.session.list_tools() - self.ap.logger.debug(f"获取 MCP 工具: {tools}") + self.ap.logger.debug(f'获取 MCP 工具: {tools}') for tool in tools.tools: @@ -93,25 +97,28 @@ class RuntimeMCPSession: if result.isError: raise Exception(result.content[0].text) return result.content[0].text - + func.__name__ = tool.name - self.functions.append(tools_entities.LLMFunction( - name=tool.name, - human_desc=tool.description, - description=tool.description, - parameters=tool.inputSchema, - func=func, - )) + self.functions.append( + tools_entities.LLMFunction( + name=tool.name, + human_desc=tool.description, + description=tool.description, + parameters=tool.inputSchema, + func=func, + ) + ) async def shutdown(self): """关闭工具""" await self.session._exit_stack.aclose() -@loader.loader_class("mcp") + +@loader.loader_class('mcp') class MCPLoader(loader.ToolLoader): """MCP 工具加载器。 - + 在此加载器中管理所有与 MCP Server 的连接。 """ @@ -125,16 +132,17 @@ class MCPLoader(loader.ToolLoader): self._last_listed_functions = [] async def initialize(self): - - for server_config in self.ap.instance_config.data.get("mcp", {}).get("servers", []): - if not server_config["enable"]: + for server_config in self.ap.instance_config.data.get('mcp', {}).get( + 'servers', [] + ): + if not server_config['enable']: continue - session = RuntimeMCPSession(server_config["name"], server_config, self.ap) + session = RuntimeMCPSession(server_config['name'], server_config, self.ap) await session.initialize() # self.ap.event_loop.create_task(session.initialize()) - self.sessions[server_config["name"]] = session + self.sessions[server_config['name']] = session - async def get_tools(self, enabled: bool=True) -> list[tools_entities.LLMFunction]: + async def get_tools(self, enabled: bool = True) -> list[tools_entities.LLMFunction]: all_functions = [] for session in self.sessions.values(): @@ -147,13 +155,15 @@ class MCPLoader(loader.ToolLoader): async def has_tool(self, name: str) -> bool: return name in [f.name for f in self._last_listed_functions] - async def invoke_tool(self, query: core_entities.Query, name: str, parameters: dict) -> typing.Any: + async def invoke_tool( + self, query: core_entities.Query, name: str, parameters: dict + ) -> typing.Any: for server_name, session in self.sessions.items(): for function in session.functions: if function.name == name: return await function.func(query, **parameters) - raise ValueError(f"未找到工具: {name}") + raise ValueError(f'未找到工具: {name}') async def shutdown(self): """关闭工具""" diff --git a/pkg/provider/tools/loaders/plugin.py b/pkg/provider/tools/loaders/plugin.py index 5b964556..c53403af 100644 --- a/pkg/provider/tools/loaders/plugin.py +++ b/pkg/provider/tools/loaders/plugin.py @@ -4,19 +4,18 @@ import typing import traceback from .. import loader, entities as tools_entities -from ....core import app, entities as core_entities +from ....core import entities as core_entities from ....plugin import context as plugin_context -@loader.loader_class("plugin-tool-loader") +@loader.loader_class('plugin-tool-loader') class PluginToolLoader(loader.ToolLoader): """插件工具加载器。 - + 本加载器中不存储工具信息,仅负责从插件系统中获取工具信息。 """ - async def get_tools(self, enabled: bool=True) -> list[tools_entities.LLMFunction]: - + async def get_tools(self, enabled: bool = True) -> list[tools_entities.LLMFunction]: # 从插件系统获取工具(内容函数) all_functions: list[tools_entities.LLMFunction] = [] @@ -49,23 +48,23 @@ class PluginToolLoader(loader.ToolLoader): return function, plugin.plugin_inst return None, None - async def invoke_tool(self, query: core_entities.Query, name: str, parameters: dict) -> typing.Any: - + async def invoke_tool( + self, query: core_entities.Query, name: str, parameters: dict + ) -> typing.Any: try: - function, plugin = await self._get_function_and_plugin(name) if function is None: return None parameters = parameters.copy() - parameters = {"query": query, **parameters} + parameters = {'query': query, **parameters} return await function.func(plugin, **parameters) except Exception as e: - self.ap.logger.error(f"执行函数 {name} 时发生错误: {e}") + self.ap.logger.error(f'执行函数 {name} 时发生错误: {e}') traceback.print_exc() - return f"error occurred when executing function {name}: {e}" + return f'error occurred when executing function {name}: {e}' finally: plugin = None @@ -75,13 +74,12 @@ class PluginToolLoader(loader.ToolLoader): break if plugin is not None: - await self.ap.ctr_mgr.usage.post_function_record( plugin={ - "name": plugin.plugin_name, - "remote": plugin.plugin_repository, - "version": plugin.plugin_version, - "author": plugin.plugin_author, + 'name': plugin.plugin_name, + 'remote': plugin.plugin_repository, + 'version': plugin.plugin_version, + 'author': plugin.plugin_author, }, function_name=function.name, function_description=function.description, diff --git a/pkg/provider/tools/toolmgr.py b/pkg/provider/tools/toolmgr.py index 64befd8c..0f6fdac0 100644 --- a/pkg/provider/tools/toolmgr.py +++ b/pkg/provider/tools/toolmgr.py @@ -1,12 +1,13 @@ from __future__ import annotations import typing -import traceback from ...core import app, entities as core_entities from . import entities, loader as tools_loader -from ...plugin import context as plugin_context -from .loaders import plugin, mcp +from ...utils import importutil +from . import loaders + +importutil.import_modules_in_pkg(loaders) class ToolManager: @@ -22,13 +23,14 @@ class ToolManager: self.loaders = [] async def initialize(self): - for loader_cls in tools_loader.preregistered_loaders: loader_inst = loader_cls(self.ap) await loader_inst.initialize() self.loaders.append(loader_inst) - async def get_all_functions(self, plugin_enabled: bool=None) -> list[entities.LLMFunction]: + async def get_all_functions( + self, plugin_enabled: bool = None + ) -> list[entities.LLMFunction]: """获取所有函数""" all_functions: list[entities.LLMFunction] = [] @@ -37,17 +39,19 @@ class ToolManager: return all_functions - async def generate_tools_for_openai(self, use_funcs: list[entities.LLMFunction]) -> list: + async def generate_tools_for_openai( + self, use_funcs: list[entities.LLMFunction] + ) -> list: """生成函数列表""" tools = [] for function in use_funcs: function_schema = { - "type": "function", - "function": { - "name": function.name, - "description": function.description, - "parameters": function.parameters, + 'type': 'function', + 'function': { + 'name': function.name, + 'description': function.description, + 'parameters': function.parameters, }, } tools.append(function_schema) @@ -83,9 +87,9 @@ class ToolManager: for function in use_funcs: function_schema = { - "name": function.name, - "description": function.description, - "input_schema": function.parameters, + 'name': function.name, + 'description': function.description, + 'input_schema': function.parameters, } tools.append(function_schema) @@ -100,7 +104,7 @@ class ToolManager: if await loader.has_tool(name): return await loader.invoke_tool(query, name, parameters) else: - raise ValueError(f"未找到工具: {name}") + raise ValueError(f'未找到工具: {name}') async def shutdown(self): """关闭所有工具""" diff --git a/pkg/utils/announce.py b/pkg/utils/announce.py index 1fb6e166..47de9325 100644 --- a/pkg/utils/announce.py +++ b/pkg/utils/announce.py @@ -14,7 +14,7 @@ from ..core import app class Announcement(pydantic.BaseModel): """公告""" - + id: int time: str @@ -27,11 +27,11 @@ class Announcement(pydantic.BaseModel): def to_dict(self) -> dict: return { - "id": self.id, - "time": self.time, - "timestamp": self.timestamp, - "content": self.content, - "enabled": self.enabled + 'id': self.id, + 'time': self.time, + 'timestamp': self.timestamp, + 'content': self.content, + 'enabled': self.enabled, } @@ -43,30 +43,28 @@ class AnnouncementManager: def __init__(self, ap: app.Application): self.ap = ap - async def fetch_all( - self - ) -> list[Announcement]: + async def fetch_all(self) -> list[Announcement]: """获取所有公告""" resp = requests.get( - url="https://api.github.com/repos/RockChinQ/LangBot/contents/res/announcement.json", + url='https://api.github.com/repos/RockChinQ/LangBot/contents/res/announcement.json', proxies=self.ap.proxy_mgr.get_forward_proxies(), - timeout=5 + timeout=5, ) obj_json = resp.json() - b64_content = obj_json["content"] + b64_content = obj_json['content'] # 解码 - content = base64.b64decode(b64_content).decode("utf-8") + content = base64.b64decode(b64_content).decode('utf-8') return [Announcement(**item) for item in json.loads(content)] - async def fetch_saved( - self - ) -> list[Announcement]: - if not os.path.exists("data/labels/announcement_saved.json"): - with open("data/labels/announcement_saved.json", "w", encoding="utf-8") as f: - f.write("[]") + async def fetch_saved(self) -> list[Announcement]: + if not os.path.exists('data/labels/announcement_saved.json'): + with open( + 'data/labels/announcement_saved.json', 'w', encoding='utf-8' + ) as f: + f.write('[]') - with open("data/labels/announcement_saved.json", "r", encoding="utf-8") as f: + with open('data/labels/announcement_saved.json', 'r', encoding='utf-8') as f: content = f.read() if not content: @@ -74,19 +72,15 @@ class AnnouncementManager: return [Announcement(**item) for item in json.loads(content)] - async def write_saved( - self, - content: list[Announcement] - ): + async def write_saved(self, content: list[Announcement]): + with open('data/labels/announcement_saved.json', 'w', encoding='utf-8') as f: + f.write( + json.dumps( + [item.to_dict() for item in content], indent=4, ensure_ascii=False + ) + ) - with open("data/labels/announcement_saved.json", "w", encoding="utf-8") as f: - f.write(json.dumps([ - item.to_dict() for item in content - ], indent=4, ensure_ascii=False)) - - async def fetch_new( - self - ) -> list[Announcement]: + async def fetch_new(self) -> list[Announcement]: """获取新公告""" all = await self.fetch_all() saved = await self.fetch_saved() @@ -106,18 +100,15 @@ class AnnouncementManager: await self.write_saved(all) return to_show - async def show_announcements( - self - ) -> typing.Tuple[str, int]: + async def show_announcements(self) -> typing.Tuple[str, int]: """显示公告""" try: announcements = await self.fetch_new() - ann_text = "" + ann_text = '' for ann in announcements: - ann_text += f"[公告] {ann.time}: {ann.content}\n" + ann_text += f'[公告] {ann.time}: {ann.content}\n' if announcements: - await self.ap.ctr_mgr.main.post_announcement_showed( ids=[item.id for item in announcements] ) diff --git a/pkg/utils/constants.py b/pkg/utils/constants.py index f37f2151..7f3d0804 100644 --- a/pkg/utils/constants.py +++ b/pkg/utils/constants.py @@ -1,8 +1,8 @@ -semantic_version = "v4.0.0" +semantic_version = 'v4.0.0' required_database_version = 1 """标记本版本所需要的数据库结构版本,用于判断数据库迁移""" debug_mode = False -edition = 'community' \ No newline at end of file +edition = 'community' diff --git a/pkg/utils/funcschema.py b/pkg/utils/funcschema.py index c39b4886..52dd6efc 100644 --- a/pkg/utils/funcschema.py +++ b/pkg/utils/funcschema.py @@ -1,4 +1,3 @@ -import sys import re import inspect @@ -33,18 +32,18 @@ def get_func_schema(function: callable) -> dict: func_doc = function.__doc__ # Google Style Docstring if func_doc is None: - raise Exception("Function {} has no docstring.".format(function.__name__)) - func_doc = func_doc.strip().replace(' ','').replace('\t', '') + raise Exception('Function {} has no docstring.'.format(function.__name__)) + func_doc = func_doc.strip().replace(' ', '').replace('\t', '') # extract doc of args from docstring doc_spt = func_doc.split('\n\n') desc = doc_spt[0] - args = doc_spt[1] if len(doc_spt) > 1 else "" - returns = doc_spt[2] if len(doc_spt) > 2 else "" + args = doc_spt[1] if len(doc_spt) > 1 else '' + # returns = doc_spt[2] if len(doc_spt) > 2 else "" # extract args # delete the first line of args arg_lines = args.split('\n')[1:] - arg_doc_list = re.findall(r'(\w+)(\((\w+)\))?:\s*(.*)', args) + # arg_doc_list = re.findall(r'(\w+)(\((\w+)\))?:\s*(.*)', args) args_doc = {} for arg_line in arg_lines: doc_tuple = re.findall(r'(\w+)(\(([\w\[\]]+)\))?:\s*(.*)', arg_line) @@ -53,18 +52,16 @@ def get_func_schema(function: callable) -> dict: args_doc[doc_tuple[0][0]] = doc_tuple[0][3] # extract returns - return_doc_list = re.findall(r'(\w+):\s*(.*)', returns) + # return_doc_list = re.findall(r'(\w+):\s*(.*)', returns) params = enumerate(inspect.signature(function).parameters.values()) parameters = { - "type": "object", - "required": [], - "properties": {}, + 'type': 'object', + 'required': [], + 'properties': {}, } - for i, param in params: - # 排除 self, query if param.name in ['self', 'query']: continue @@ -72,24 +69,24 @@ def get_func_schema(function: callable) -> dict: param_type = param.annotation.__name__ type_name_mapping = { - "str": "string", - "int": "integer", - "float": "number", - "bool": "boolean", - "list": "array", - "dict": "object", + 'str': 'string', + 'int': 'integer', + 'float': 'number', + 'bool': 'boolean', + 'list': 'array', + 'dict': 'object', } if param_type in type_name_mapping: param_type = type_name_mapping[param_type] parameters['properties'][param.name] = { - "type": param_type, - "description": args_doc[param.name], + 'type': param_type, + 'description': args_doc[param.name], } # add schema for array - if param_type == "array": + if param_type == 'array': # extract type of array, the int of list[int] # use re array_type_tuple = re.findall(r'list\[(\w+)\]', str(param.annotation)) @@ -102,15 +99,15 @@ def get_func_schema(function: callable) -> dict: if array_type in type_name_mapping: array_type = type_name_mapping[array_type] - parameters['properties'][param.name]["items"] = { - "type": array_type, + parameters['properties'][param.name]['items'] = { + 'type': array_type, } if param.default is inspect.Parameter.empty: - parameters["required"].append(param.name) + parameters['required'].append(param.name) return { - "function": function, - "description": desc, - "parameters": parameters, - } \ No newline at end of file + 'function': function, + 'description': desc, + 'parameters': parameters, + } diff --git a/pkg/utils/image.py b/pkg/utils/image.py index 760c2128..9af766fb 100644 --- a/pkg/utils/image.py +++ b/pkg/utils/image.py @@ -8,23 +8,16 @@ import aiohttp import PIL.Image import httpx -import os -import aiofiles -import pathlib import asyncio -from urllib.parse import urlparse - - - async def get_gewechat_image_base64( - gewechat_url: str, - gewechat_file_url: str, - app_id: str, - xml_content: str, - token: str, - image_type: int = 2, + gewechat_url: str, + gewechat_file_url: str, + app_id: str, + xml_content: str, + token: str, + image_type: int = 2, ) -> typing.Tuple[str, str]: """从gewechat服务器获取图片并转换为base64格式 @@ -43,17 +36,14 @@ async def get_gewechat_image_base64( aiohttp.ClientTimeout: 请求超时(15秒)或连接超时(2秒) Exception: 其他错误 """ - headers = { - 'X-GEWE-TOKEN': token, - 'Content-Type': 'application/json' - } + headers = {'X-GEWE-TOKEN': token, 'Content-Type': 'application/json'} # 设置超时 timeout = aiohttp.ClientTimeout( total=15.0, # 总超时时间15秒 connect=2.0, # 连接超时2秒 sock_connect=2.0, # socket连接超时2秒 - sock_read=15.0 # socket读取超时15秒 + sock_read=15.0, # socket读取超时15秒 ) try: @@ -61,37 +51,37 @@ async def get_gewechat_image_base64( # 获取图片下载链接 try: async with session.post( - f"{gewechat_url}/v2/api/message/downloadImage", - headers=headers, - json={ - "appId": app_id, - "type": image_type, - "xml": xml_content - } + f'{gewechat_url}/v2/api/message/downloadImage', + headers=headers, + json={'appId': app_id, 'type': image_type, 'xml': xml_content}, ) as response: if response.status != 200: # print(response) - raise Exception(f"获取gewechat图片下载失败: {await response.text()}") + raise Exception( + f'获取gewechat图片下载失败: {await response.text()}' + ) resp_data = await response.json() - if resp_data.get("ret") != 200: - raise Exception(f"获取gewechat图片下载链接失败: {resp_data}") + if resp_data.get('ret') != 200: + raise Exception(f'获取gewechat图片下载链接失败: {resp_data}') file_url = resp_data['data']['fileUrl'] except asyncio.TimeoutError: - raise Exception("获取图片下载链接超时") + raise Exception('获取图片下载链接超时') except aiohttp.ClientError as e: - raise Exception(f"获取图片下载链接网络错误: {str(e)}") + raise Exception(f'获取图片下载链接网络错误: {str(e)}') # 解析原始URL并替换端口 base_url = gewechat_file_url - download_url = f"{base_url}/download/{file_url}" + download_url = f'{base_url}/download/{file_url}' # 下载图片 try: async with session.get(download_url) as img_response: if img_response.status != 200: - raise Exception(f"下载图片失败: {await img_response.text()}, URL: {download_url}") + raise Exception( + f'下载图片失败: {await img_response.text()}, URL: {download_url}' + ) image_data = await img_response.read() @@ -105,14 +95,11 @@ async def get_gewechat_image_base64( return base64_str, image_format except asyncio.TimeoutError: - raise Exception(f"下载图片超时, URL: {download_url}") + raise Exception(f'下载图片超时, URL: {download_url}') except aiohttp.ClientError as e: - raise Exception(f"下载图片网络错误: {str(e)}, URL: {download_url}") + raise Exception(f'下载图片网络错误: {str(e)}, URL: {download_url}') except Exception as e: - raise Exception(f"获取图片失败: {str(e)}") from e - - - + raise Exception(f'获取图片失败: {str(e)}') from e async def get_wecom_image_base64(pic_url: str) -> tuple[str, str]: @@ -124,22 +111,26 @@ async def get_wecom_image_base64(pic_url: str) -> tuple[str, str]: async with aiohttp.ClientSession() as session: async with session.get(pic_url) as response: if response.status != 200: - raise Exception(f"Failed to download image: {response.status}") - + raise Exception(f'Failed to download image: {response.status}') + # 读取图片数据 image_data = await response.read() - + # 获取图片格式 content_type = response.headers.get('Content-Type', '') image_format = content_type.split('/')[-1] # 例如 'image/jpeg' -> 'jpeg' - + # 转换为 base64 import base64 + image_base64 = base64.b64encode(image_data).decode('utf-8') - + return image_base64, image_format - -async def get_qq_official_image_base64(pic_url:str,content_type:str) -> tuple[str,str]: + + +async def get_qq_official_image_base64( + pic_url: str, content_type: str +) -> tuple[str, str]: """ 下载QQ官方图片, 并且转换为base64格式 @@ -149,18 +140,18 @@ async def get_qq_official_image_base64(pic_url:str,content_type:str) -> tuple[st response.raise_for_status() # 确保请求成功 image_data = response.content base64_data = base64.b64encode(image_data).decode('utf-8') - - return f"data:{content_type};base64,{base64_data}" + + return f'data:{content_type};base64,{base64_data}' def get_qq_image_downloadable_url(image_url: str) -> tuple[str, dict]: """获取QQ图片的下载链接""" parsed = urlparse(image_url) query = parse_qs(parsed.query) - return f"http://{parsed.netloc}{parsed.path}", query + return f'http://{parsed.netloc}{parsed.path}', query -async def get_qq_image_bytes(image_url: str, query: dict={}) -> tuple[bytes, str]: +async def get_qq_image_bytes(image_url: str, query: dict = {}) -> tuple[bytes, str]: """[弃用]获取QQ图片的bytes""" image_url, query_in_url = get_qq_image_downloadable_url(image_url) query = {**query, **query_in_url} @@ -177,14 +168,12 @@ async def get_qq_image_bytes(image_url: str, query: dict={}) -> tuple[bytes, str elif not content_type.startswith('image/'): pil_img = PIL.Image.open(io.BytesIO(file_bytes)) image_format = pil_img.format.lower() - else: + else: image_format = content_type.split('/')[-1] return file_bytes, image_format -async def qq_image_url_to_base64( - image_url: str -) -> typing.Tuple[str, str]: +async def qq_image_url_to_base64(image_url: str) -> typing.Tuple[str, str]: """[弃用]将QQ图片URL转为base64,并返回图片格式 Args: @@ -204,12 +193,13 @@ async def qq_image_url_to_base64( return base64_str, image_format + async def extract_b64_and_format(image_base64_data: str) -> typing.Tuple[str, str]: """提取base64编码和图片格式 - + data:image/jpeg;base64,xxx 提取出base64编码和图片格式 """ base64_str = image_base64_data.split(',')[-1] image_format = image_base64_data.split(':')[-1].split(';')[0].split('/')[-1] - return base64_str, image_format \ No newline at end of file + return base64_str, image_format diff --git a/pkg/utils/importutil.py b/pkg/utils/importutil.py new file mode 100644 index 00000000..87ca652a --- /dev/null +++ b/pkg/utils/importutil.py @@ -0,0 +1,43 @@ +import importlib +import importlib.util +import os +import typing + + +def import_modules_in_pkg(pkg: typing.Any) -> None: + """ + 导入一个包内的所有模块 + Args: + pkg: 要导入的包对象 + """ + pkg_path = os.path.dirname(pkg.__file__) + import_dir(pkg_path) + + +def import_modules_in_pkgs(pkgs: typing.List) -> None: + for pkg in pkgs: + import_modules_in_pkg(pkg) + + +def import_dot_style_dir(dot_sep_path: str): + sec = dot_sep_path.split('.') + + return import_dir(os.path.join(*sec)) + + +def import_dir(path: str): + for file in os.listdir(path): + if file.endswith('.py') and file != '__init__.py': + full_path = os.path.join(path, file) + rel_path = full_path.replace( + os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '' + ) + rel_path = rel_path[1:] + rel_path = rel_path.replace('/', '.')[:-3] + importlib.import_module(rel_path) + + +if __name__ == '__main__': + from pkg.platform import types + + import_modules_in_pkg(types) diff --git a/pkg/utils/ip.py b/pkg/utils/ip.py index 1250f99e..56a12086 100644 --- a/pkg/utils/ip.py +++ b/pkg/utils/ip.py @@ -1,9 +1,12 @@ import aiohttp + async def get_myip() -> str: try: - async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session: - async with session.get("https://ip.useragentinfo.com/myip") as response: + async with aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=10) + ) as session: + async with session.get('https://ip.useragentinfo.com/myip') as response: return await response.text() - except Exception as e: - return '0.0.0.0' \ No newline at end of file + except Exception: + return '0.0.0.0' diff --git a/pkg/utils/logcache.py b/pkg/utils/logcache.py index d3206e9b..84c58f55 100644 --- a/pkg/utils/logcache.py +++ b/pkg/utils/logcache.py @@ -5,8 +5,9 @@ LOG_PAGE_SIZE = 20 MAX_CACHED_PAGES = 10 -class LogPage(): +class LogPage: """日志页""" + number: int """页码""" @@ -51,12 +52,12 @@ class LogCache: start_offset: int, ) -> tuple[str, int, int]: """获取指定页码和偏移量的日志""" - final_logs_str = "" + final_logs_str = '' for page in self.log_pages: if page.number == start_page_number: - final_logs_str += "\n".join(page.logs[start_offset:]) + final_logs_str += '\n'.join(page.logs[start_offset:]) elif page.number > start_page_number: - final_logs_str += "\n".join(page.logs) + final_logs_str += '\n'.join(page.logs) return final_logs_str, page.number, len(page.logs) diff --git a/pkg/utils/pkgmgr.py b/pkg/utils/pkgmgr.py index 4f3186b6..9ce8bdb8 100644 --- a/pkg/utils/pkgmgr.py +++ b/pkg/utils/pkgmgr.py @@ -6,8 +6,17 @@ def install(package): def install_upgrade(package): - pipmain(['install', '--upgrade', package, "-i", "https://pypi.tuna.tsinghua.edu.cn/simple", - "--trusted-host", "pypi.tuna.tsinghua.edu.cn"]) + pipmain( + [ + 'install', + '--upgrade', + package, + '-i', + 'https://pypi.tuna.tsinghua.edu.cn/simple', + '--trusted-host', + 'pypi.tuna.tsinghua.edu.cn', + ] + ) def run_pip(params: list): @@ -15,5 +24,15 @@ def run_pip(params: list): def install_requirements(file, extra_params: list = []): - pipmain(['install', '-r', file, "-i", "https://pypi.tuna.tsinghua.edu.cn/simple", - "--trusted-host", "pypi.tuna.tsinghua.edu.cn"] + extra_params) + pipmain( + [ + 'install', + '-r', + file, + '-i', + 'https://pypi.tuna.tsinghua.edu.cn/simple', + '--trusted-host', + 'pypi.tuna.tsinghua.edu.cn', + ] + + extra_params + ) diff --git a/pkg/utils/proxy.py b/pkg/utils/proxy.py index 838814dd..4f3f7dec 100644 --- a/pkg/utils/proxy.py +++ b/pkg/utils/proxy.py @@ -1,14 +1,12 @@ from __future__ import annotations import os -import sys from ..core import app class ProxyManager: - """代理管理器 - """ + """代理管理器""" ap: app.Application @@ -21,14 +19,24 @@ class ProxyManager: async def initialize(self): self.forward_proxies = { - "http://": os.getenv("HTTP_PROXY") or os.getenv("http_proxy"), - "https://": os.getenv("HTTPS_PROXY") or os.getenv("https_proxy"), + 'http://': os.getenv('HTTP_PROXY') or os.getenv('http_proxy'), + 'https://': os.getenv('HTTPS_PROXY') or os.getenv('https_proxy'), } - if 'http' in self.ap.instance_config.data['proxy'] and self.ap.instance_config.data['proxy']['http']: - self.forward_proxies['http://'] = self.ap.instance_config.data['proxy']['http'] - if 'https' in self.ap.instance_config.data['proxy'] and self.ap.instance_config.data['proxy']['https']: - self.forward_proxies['https://'] = self.ap.instance_config.data['proxy']['https'] + if ( + 'http' in self.ap.instance_config.data['proxy'] + and self.ap.instance_config.data['proxy']['http'] + ): + self.forward_proxies['http://'] = self.ap.instance_config.data['proxy'][ + 'http' + ] + if ( + 'https' in self.ap.instance_config.data['proxy'] + and self.ap.instance_config.data['proxy']['https'] + ): + self.forward_proxies['https://'] = self.ap.instance_config.data['proxy'][ + 'https' + ] # 设置到环境变量 os.environ['HTTP_PROXY'] = self.forward_proxies['http://'] or '' diff --git a/pkg/utils/version.py b/pkg/utils/version.py index 9a206171..ef30c192 100644 --- a/pkg/utils/version.py +++ b/pkg/utils/version.py @@ -12,41 +12,33 @@ from . import constants class VersionManager: - """版本管理器 - """ + """版本管理器""" ap: app.Application - def __init__( - self, - ap: app.Application - ): + def __init__(self, ap: app.Application): self.ap = ap - async def initialize( - self - ): + async def initialize(self): pass - - def get_current_version( - self - ) -> str: + + def get_current_version(self) -> str: current_tag = constants.semantic_version return current_tag - + async def get_release_list(self) -> list: """获取发行列表""" rls_list_resp = requests.get( - url="https://api.github.com/repos/RockChinQ/LangBot/releases", + url='https://api.github.com/repos/RockChinQ/LangBot/releases', proxies=self.ap.proxy_mgr.get_forward_proxies(), - timeout=5 + timeout=5, ) rls_list = rls_list_resp.json() return rls_list - + async def update_all(self): """检查更新并下载源码""" start_time = time.time() @@ -58,10 +50,10 @@ class VersionManager: latest_rls = {} rls_notes = [] - latest_tag_name = "" + latest_tag_name = '' for rls in rls_list: rls_notes.append(rls['name']) # 使用发行名称作为note - if latest_tag_name == "": + if latest_tag_name == '': latest_tag_name = rls['tag_name'] if rls['tag_name'] == current_tag: @@ -69,56 +61,68 @@ class VersionManager: if latest_rls == {}: latest_rls = rls - self.ap.logger.info("更新日志: {}".format(rls_notes)) + self.ap.logger.info('更新日志: {}'.format(rls_notes)) - if latest_rls == {} and not self.is_newer(latest_tag_name, current_tag): # 没有新版本 + if latest_rls == {} and not self.is_newer( + latest_tag_name, current_tag + ): # 没有新版本 return False # 下载最新版本的zip到temp目录 - self.ap.logger.info("开始下载最新版本: {}".format(latest_rls['zipball_url'])) + self.ap.logger.info('开始下载最新版本: {}'.format(latest_rls['zipball_url'])) zip_url = latest_rls['zipball_url'] zip_resp = requests.get( - url=zip_url, - proxies=self.ap.proxy_mgr.get_forward_proxies() + url=zip_url, proxies=self.ap.proxy_mgr.get_forward_proxies() ) zip_data = zip_resp.content # 检查temp/updater目录 - if not os.path.exists("temp"): - os.mkdir("temp") - if not os.path.exists("temp/updater"): - os.mkdir("temp/updater") - with open("temp/updater/{}.zip".format(latest_rls['tag_name']), "wb") as f: + if not os.path.exists('temp'): + os.mkdir('temp') + if not os.path.exists('temp/updater'): + os.mkdir('temp/updater') + with open('temp/updater/{}.zip'.format(latest_rls['tag_name']), 'wb') as f: f.write(zip_data) - self.ap.logger.info("下载最新版本完成: {}".format("temp/updater/{}.zip".format(latest_rls['tag_name']))) + self.ap.logger.info( + '下载最新版本完成: {}'.format( + 'temp/updater/{}.zip'.format(latest_rls['tag_name']) + ) + ) # 解压zip到temp/updater// import zipfile + # 检查目标文件夹 - if os.path.exists("temp/updater/{}".format(latest_rls['tag_name'])): + if os.path.exists('temp/updater/{}'.format(latest_rls['tag_name'])): import shutil - shutil.rmtree("temp/updater/{}".format(latest_rls['tag_name'])) - os.mkdir("temp/updater/{}".format(latest_rls['tag_name'])) - with zipfile.ZipFile("temp/updater/{}.zip".format(latest_rls['tag_name']), 'r') as zip_ref: - zip_ref.extractall("temp/updater/{}".format(latest_rls['tag_name'])) + + shutil.rmtree('temp/updater/{}'.format(latest_rls['tag_name'])) + os.mkdir('temp/updater/{}'.format(latest_rls['tag_name'])) + with zipfile.ZipFile( + 'temp/updater/{}.zip'.format(latest_rls['tag_name']), 'r' + ) as zip_ref: + zip_ref.extractall('temp/updater/{}'.format(latest_rls['tag_name'])) # 覆盖源码 - source_root = "" + source_root = '' # 找到temp/updater//中的第一个子目录路径 - for root, dirs, files in os.walk("temp/updater/{}".format(latest_rls['tag_name'])): - if root != "temp/updater/{}".format(latest_rls['tag_name']): + for root, dirs, files in os.walk( + 'temp/updater/{}'.format(latest_rls['tag_name']) + ): + if root != 'temp/updater/{}'.format(latest_rls['tag_name']): source_root = root break # 覆盖源码 import shutil + for root, dirs, files in os.walk(source_root): # 覆盖所有子文件子目录 for file in files: src = os.path.join(root, file) - dst = src.replace(source_root, ".") + dst = src.replace(source_root, '.') if os.path.exists(dst): os.remove(dst) @@ -128,18 +132,18 @@ class VersionManager: # 检查目标文件是否存在 if not os.path.exists(dst): # 创建目标文件 - open(dst, "w").close() + open(dst, 'w').close() shutil.copy(src, dst) # 把current_tag写入文件 current_tag = latest_rls['tag_name'] - with open("current_tag", "w") as f: + with open('current_tag', 'w') as f: f.write(current_tag) await self.ap.ctr_mgr.main.post_update_record( - spent_seconds=int(time.time()-start_time), - infer_reason="update", + spent_seconds=int(time.time() - start_time), + infer_reason='update', old_version=old_tag, new_version=current_tag, ) @@ -155,23 +159,22 @@ class VersionManager: current_tag = self.get_current_version() # 检查是否有新版本 - latest_tag_name = "" + latest_tag_name = '' for rls in rls_list: - if latest_tag_name == "": + if latest_tag_name == '': latest_tag_name = rls['tag_name'] break return self.is_newer(latest_tag_name, current_tag) - def is_newer(self, new_tag: str, old_tag: str): """判断版本是否更新,忽略第四位版本和第一位版本""" if new_tag == old_tag: return False - new_tag = new_tag.split(".") - old_tag = old_tag.split(".") - + new_tag = new_tag.split('.') + old_tag = old_tag.split('.') + # 判断主版本是否相同 if new_tag[0] != old_tag[0]: return False @@ -180,29 +183,28 @@ class VersionManager: return True # 合成前三段,判断是否相同 - new_tag = ".".join(new_tag[:3]) - old_tag = ".".join(old_tag[:3]) + new_tag = '.'.join(new_tag[:3]) + old_tag = '.'.join(old_tag[:3]) return new_tag != old_tag - def compare_version_str(v0: str, v1: str) -> int: """比较两个版本号""" # 删除版本号前的v - if v0.startswith("v"): + if v0.startswith('v'): v0 = v0[1:] - if v1.startswith("v"): + if v1.startswith('v'): v1 = v1[1:] - v0:list = v0.split(".") - v1:list = v1.split(".") + v0: list = v0.split('.') + v1: list = v1.split('.') # 如果两个版本号节数不同,把短的后面用0补齐 if len(v0) < len(v1): - v0.extend(["0"]*(len(v1)-len(v0))) + v0.extend(['0'] * (len(v1) - len(v0))) elif len(v0) > len(v1): - v1.extend(["0"]*(len(v0)-len(v1))) + v1.extend(['0'] * (len(v0) - len(v1))) # 从高位向低位比较 for i in range(len(v0)): @@ -210,16 +212,16 @@ class VersionManager: return 1 elif int(v0[i]) < int(v1[i]): return -1 - + return 0 - async def show_version_update( - self - ) -> typing.Tuple[str, int]: + async def show_version_update(self) -> typing.Tuple[str, int]: try: - if await self.ap.ver_mgr.is_new_version_available(): - return "有新版本可用,根据文档更新:https://docs.langbot.app/deploy/update.html", logging.INFO - + return ( + '有新版本可用,根据文档更新:https://docs.langbot.app/deploy/update.html', + logging.INFO, + ) + except Exception as e: - return f"检查版本更新时出错: {e}", logging.WARNING + return f'检查版本更新时出错: {e}', logging.WARNING diff --git a/requirements.txt b/requirements.txt index e01eb373..b34c18a3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,4 +37,6 @@ mcp sqlmodel # indirect -taskgroup==0.0.0a4 \ No newline at end of file +taskgroup==0.0.0a4 +ruff +pre-commit \ No newline at end of file diff --git a/res/scripts/publish_announcement.py b/res/scripts/publish_announcement.py index 812e83d9..7d2e7d40 100644 --- a/res/scripts/publish_announcement.py +++ b/res/scripts/publish_announcement.py @@ -1,32 +1,32 @@ # 输出工作路径 import os -print("工作路径: " + os.getcwd()) -announcement = input("请输入公告内容: ") - +import time import json +print('工作路径: ' + os.getcwd()) +announcement = input('请输入公告内容: ') + # 读取现有的公告文件 res/announcement.json -with open("res/announcement.json", "r", encoding="utf-8") as f: +with open('res/announcement.json', 'r', encoding='utf-8') as f: announcement_json = json.load(f) # 将公告内容写入公告文件 # 当前自然时间 -import time -now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) +now = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) # 获取最后一个公告的id -last_id = announcement_json[-1]["id"] if len(announcement_json) > 0 else -1 +last_id = announcement_json[-1]['id'] if len(announcement_json) > 0 else -1 announcement = { - "id": last_id + 1, - "time": now, - "timestamp": int(time.time()), - "content": announcement + 'id': last_id + 1, + 'time': now, + 'timestamp': int(time.time()), + 'content': announcement, } announcement_json.append(announcement) # 将公告写入公告文件 -with open("res/announcement.json", "w", encoding="utf-8") as f: +with open('res/announcement.json', 'w', encoding='utf-8') as f: json.dump(announcement_json, f, indent=4, ensure_ascii=False) diff --git a/templates/metadata/pipeline/ai.yaml b/templates/metadata/pipeline/ai.yaml index 8b7959f6..3bd9b731 100644 --- a/templates/metadata/pipeline/ai.yaml +++ b/templates/metadata/pipeline/ai.yaml @@ -40,9 +40,8 @@ stages: label: en_US: Model zh_CN: 模型 - type: select + type: llm-model-selector required: true - scope: /provider/models/llm - name: max-round label: en_US: Max Round @@ -54,16 +53,8 @@ stages: label: en_US: Prompt zh_CN: 提示词 - type: array + type: prompt-editor required: true - items: - type: object - properties: - role: - type: string - default: user - content: - type: string - name: dify-service-api label: en_US: Dify Service API diff --git a/templates/metadata/pipeline/trigger.yaml b/templates/metadata/pipeline/trigger.yaml index 78759b11..ea7c596a 100644 --- a/templates/metadata/pipeline/trigger.yaml +++ b/templates/metadata/pipeline/trigger.yaml @@ -28,11 +28,9 @@ stages: description: en_US: The prefix of the message zh_CN: 消息前缀 - type: array + type: array[string] required: true default: [] - items: - type: string - name: regexp label: en_US: Regexp @@ -40,11 +38,9 @@ stages: description: en_US: The regexp of the message zh_CN: 消息正则表达式 - type: array + type: array[string] required: true default: [] - items: - type: string - name: random label: en_US: Random @@ -83,20 +79,16 @@ stages: label: en_US: Blacklist zh_CN: 黑名单 - type: array + type: array[string] required: true default: [] - items: - type: string - name: whitelist label: en_US: Whitelist zh_CN: 白名单 - type: array + type: array[string] required: true default: [] - items: - type: string - name: ignore-rules label: en_US: Ignore Rules @@ -109,11 +101,9 @@ stages: description: en_US: The prefix of the message zh_CN: 消息前缀 - type: array + type: array[string] required: true default: [] - items: - type: string - name: regexp label: en_US: Regexp @@ -121,8 +111,6 @@ stages: description: en_US: The regexp of the message zh_CN: 消息正则表达式 - type: array + type: array[string] required: true default: [] - items: - type: string diff --git a/web/.browserslistrc b/web/.browserslistrc deleted file mode 100644 index dc3bc09a..00000000 --- a/web/.browserslistrc +++ /dev/null @@ -1,4 +0,0 @@ -> 1% -last 2 versions -not dead -not ie 11 diff --git a/web/.editorconfig b/web/.editorconfig deleted file mode 100644 index 7053c49a..00000000 --- a/web/.editorconfig +++ /dev/null @@ -1,5 +0,0 @@ -[*.{js,jsx,ts,tsx,vue}] -indent_style = space -indent_size = 2 -trim_trailing_whitespace = true -insert_final_newline = true diff --git a/web/.eslintrc.js b/web/.eslintrc.js deleted file mode 100644 index 6e7e1b14..00000000 --- a/web/.eslintrc.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - root: true, - env: { - node: true, - }, - extends: [ - 'plugin:vue/vue3-essential', - 'eslint:recommended', - ], -} diff --git a/web/.gitignore b/web/.gitignore index 11f5d714..c77c37c5 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -1,22 +1,43 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc .DS_Store -node_modules -/dist +*.pem -# local env files -.env.local -.env.*.local - -# Log files +# debug npm-debug.log* yarn-debug.log* yarn-error.log* -pnpm-debug.log* +.pnpm-debug.log* -# Editor directories and files -.idea -.vscode -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +pnpm-lock.yaml \ No newline at end of file diff --git a/web/.husky/pre-commit b/web/.husky/pre-commit new file mode 100644 index 00000000..aa89fa4b --- /dev/null +++ b/web/.husky/pre-commit @@ -0,0 +1,2 @@ +cd web +pnpm lint-staged diff --git a/web/.lintstagedrc.json b/web/.lintstagedrc.json new file mode 100644 index 00000000..123ba0ab --- /dev/null +++ b/web/.lintstagedrc.json @@ -0,0 +1,6 @@ +{ + "*.{js,jsx,ts,tsx}": ["next lint --fix --file", "next lint --file"], + "**/*": [ + "bash -c 'cd \"$(pwd)\" && next build" + ] +} diff --git a/web/.prettierrc.mjs b/web/.prettierrc.mjs new file mode 100644 index 00000000..01ed3d48 --- /dev/null +++ b/web/.prettierrc.mjs @@ -0,0 +1,22 @@ +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + // 单行长度 + printWidth: 80, + // 缩进 + tabWidth: 2, + // 使用空格代替tab缩进 + useTabs: false, + // 句末使用分号 + semi: true, + // 使用单引号 + singleQuote: true, + // 大括号前后空格 + bracketSpacing: true, + attributeVerticalAlignment: 'auto', + trailingComma: 'all', +}; + +export default config; diff --git a/web/README.md b/web/README.md index 280a55a2..e215bc4c 100644 --- a/web/README.md +++ b/web/README.md @@ -1 +1,36 @@ -# WebUI +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/web/components.json b/web/components.json new file mode 100644 index 00000000..420be750 --- /dev/null +++ b/web/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/global.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs new file mode 100644 index 00000000..18b74c95 --- /dev/null +++ b/web/eslint.config.mjs @@ -0,0 +1,18 @@ +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { FlatCompat } from '@eslint/eslintrc'; +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends('next/core-web-vitals', 'next/typescript'), + eslintPluginPrettierRecommended, +]; + +export default eslintConfig; diff --git a/web/index.html b/web/index.html deleted file mode 100644 index 150c3979..00000000 --- a/web/index.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - LangBot 面板 - - - - - - - - diff --git a/web/jsconfig.json b/web/jsconfig.json deleted file mode 100644 index dad0634c..00000000 --- a/web/jsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "allowJs": true, - "target": "es5", - "module": "esnext", - "baseUrl": "./", - "moduleResolution": "bundler", - "paths": { - "@/*": [ - "src/*" - ] - }, - "lib": [ - "esnext", - "dom", - "dom.iterable", - "scripthost" - ] - } -} diff --git a/web/next b/web/next new file mode 100644 index 00000000..e69de29b diff --git a/web/next.config.ts b/web/next.config.ts new file mode 100644 index 00000000..1abe320c --- /dev/null +++ b/web/next.config.ts @@ -0,0 +1,8 @@ +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = { + /* config options here */ + output: 'export', +}; + +export default nextConfig; diff --git a/web/package-lock.json b/web/package-lock.json index 331b2815..6b87d740 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,623 +1,122 @@ { "name": "web", - "version": "0.0.0", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "web", - "version": "0.0.0", + "version": "0.1.0", "dependencies": { - "@koumoul/vjsf": "^3.0.0-beta.46", - "@mdi/font": "7.4.47", - "ajv": "^8.17.1", - "ajv-dist": "^8.17.1", - "ajv-errors": "^3.0.0", - "ajv-formats": "^3.0.1", - "ajv-i18n": "^4.2.0", - "ansi_up": "^6.0.2", - "axios": "^1.7.7", - "codemirror": "^5.65.18", - "core-js": "^3.37.1", - "json-editor-vue": "^0.17.3", - "roboto-fontface": "*", - "vue": "^3.4.31", - "vuedraggable": "^4.1.0", - "vuetify": "^3.6.11", - "vuex": "^4.0.2" + "@hookform/resolvers": "^5.0.1", + "@radix-ui/react-checkbox": "^1.3.1", + "@radix-ui/react-dialog": "^1.1.13", + "@radix-ui/react-label": "^2.1.6", + "@radix-ui/react-select": "^2.2.4", + "@radix-ui/react-slot": "^1.2.2", + "@radix-ui/react-switch": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.11", + "@radix-ui/react-toggle": "^1.1.8", + "@radix-ui/react-toggle-group": "^1.1.9", + "@tailwindcss/postcss": "^4.1.5", + "axios": "^1.8.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lodash": "^4.17.21", + "lucide-react": "^0.507.0", + "next": "15.2.4", + "next-themes": "^0.4.6", + "postcss": "^8.5.3", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-hook-form": "^7.56.3", + "sonner": "^2.0.3", + "tailwind-merge": "^3.2.0", + "tailwindcss": "^4.1.5", + "uuidjs": "^5.1.0", + "zod": "^3.24.4" }, "devDependencies": { - "@vitejs/plugin-vue": "^5.0.5", - "eslint": "^8.57.0", - "eslint-config-standard": "^17.1.0", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-n": "^16.6.2", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^6.4.0", - "eslint-plugin-vue": "^9.27.0", - "sass": "1.77.6", - "unplugin-fonts": "^1.1.1", - "unplugin-vue-components": "^0.27.2", - "unplugin-vue-router": "^0.10.0", - "vite": "^5.3.3", - "vite-plugin-vuetify": "^2.0.3", - "vue-router": "^4.4.0" + "@eslint/eslintrc": "^3", + "@types/lodash": "^4.17.16", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "15.2.4", + "eslint-config-prettier": "^10.1.2", + "eslint-plugin-prettier": "^5.2.6", + "husky": "^9.1.7", + "lint-staged": "^15.5.1", + "prettier": "^3.5.3", + "tw-animate-css": "^1.2.9", + "typescript": "^5.8.3", + "typescript-eslint": "^8.31.1" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@antfu/utils": { - "version": "0.7.10", - "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", - "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", - "dev": true, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", "license": "MIT", + "engines": { + "node": ">=10" + }, "funding": { - "url": "https://github.com/sponsors/antfu" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", - "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", - "license": "MIT", + "node_modules/@emnapi/core": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz", + "integrity": "sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==", + "optional": true, "dependencies": { - "@babel/types": "^7.25.6" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" + "@emnapi/wasi-threads": "1.0.1", + "tslib": "^2.4.0" } }, - "node_modules/@babel/types": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", - "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", - "license": "MIT", + "node_modules/@emnapi/runtime": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", + "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "optional": true, "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" + "tslib": "^2.4.0" } }, - "node_modules/@codemirror/autocomplete": { - "version": "6.18.2", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.2.tgz", - "integrity": "sha512-wJGylKtMFR/Ds6Gh01+OovXE/pncPiKZNNBKuC39pKnH+XK5d9+WsNqcrdxPjFPFTigRBqse0rfxw9UxrfyhPg==", - "license": "MIT", + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.1.tgz", + "integrity": "sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==", + "optional": true, "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.17.0", - "@lezer/common": "^1.0.0" - }, - "peerDependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@codemirror/commands": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.7.1.tgz", - "integrity": "sha512-llTrboQYw5H4THfhN4U3qCnSZ1SOJ60ohhz+SzU0ADGtwlc533DtklQP0vSFaQuCPDn3BPpOd1GbbnUtwNjsrw==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.4.0", - "@codemirror/view": "^6.27.0", - "@lezer/common": "^1.1.0" - } - }, - "node_modules/@codemirror/lang-json": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz", - "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@lezer/json": "^1.0.0" - } - }, - "node_modules/@codemirror/language": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.3.tgz", - "integrity": "sha512-kDqEU5sCP55Oabl6E7m5N+vZRoc0iWqgDVhEKifcHzPzjqCegcO4amfrYVL9PmPZpl4G0yjkpTpUO/Ui8CzO8A==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.23.0", - "@lezer/common": "^1.1.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0", - "style-mod": "^4.0.0" - } - }, - "node_modules/@codemirror/lint": { - "version": "6.8.2", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.2.tgz", - "integrity": "sha512-PDFG5DjHxSEjOXk9TQYYVjZDqlZTFaDBfhQixHnQOEVDDNHUbEh/hstAjcQJaA6FQdZTD1hquXTK0rVBLADR1g==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "crelt": "^1.0.5" - } - }, - "node_modules/@codemirror/search": { - "version": "6.5.7", - "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.7.tgz", - "integrity": "sha512-6+iLsXvITWKHYlkgHPCs/qiX4dNzn8N78YfhOFvPtPYCkuXqZq10rAfsUMhOq7O/1VjJqdXRflyExlfVcu/9VQ==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "crelt": "^1.0.5" - } - }, - "node_modules/@codemirror/state": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", - "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==", - "license": "MIT" - }, - "node_modules/@codemirror/view": { - "version": "6.34.2", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.34.2.tgz", - "integrity": "sha512-d6n0WFvL970A9Z+l9N2dO+Hk9ev4hDYQzIx+B9tCyBP0W5wPEszi1rhuyFesNSkLZzXbQE5FPH7F/z/TMJfoPA==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.4.0", - "style-mod": "^4.1.0", - "w3c-keyname": "^2.2.4" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "tslib": "^2.4.0" } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", + "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", "dev": true, - "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", - "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -625,87 +124,187 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.0.tgz", + "integrity": "sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.23.0.tgz", + "integrity": "sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", + "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.12.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz", + "integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==", "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.0.tgz", + "integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "license": "MIT" + }, + "node_modules/@hookform/resolvers": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.0.1.tgz", + "integrity": "sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "engines": { + "node": ">=18.18" }, "funding": { "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@fortawesome/fontawesome-common-types": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz", - "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/free-regular-svg-icons": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.6.0.tgz", - "integrity": "sha512-Yv9hDzL4aI73BEwSEh20clrY8q/uLxawaQ98lekBx6t9dQKDHcDzzV1p2YtBGTtolYtNqcWdniOnhzB+JPnQEQ==", - "license": "(CC-BY-4.0 AND MIT)", - "dependencies": { - "@fortawesome/fontawesome-common-types": "6.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/free-solid-svg-icons": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz", - "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==", - "license": "(CC-BY-4.0 AND MIT)", - "dependencies": { - "@fortawesome/fontawesome-common-types": "6.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" + "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@humanwhocodes/module-importer": { @@ -713,7 +312,6 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=12.22" }, @@ -722,222 +320,511 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", + "node_modules/@humanwhocodes/retry": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" + "engines": { + "node": ">=18.18" }, - "engines": { - "node": ">=6.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jsep-plugin/assignment": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", - "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", - "license": "MIT", - "engines": { - "node": ">= 10.16.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" } }, - "node_modules/@jsep-plugin/regex": { + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", - "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 10.16.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" - } - }, - "node_modules/@json-layout/core": { - "version": "0.32.1", - "resolved": "https://registry.npmjs.org/@json-layout/core/-/core-0.32.1.tgz", - "integrity": "sha512-/x+D8epj48MKPlDTKE7lgwjd6ThFF99+fZwxwuActV3XAFwE55bu0sK45i9yDoTkgEHHWCDlxlOdDkJP0hRQ/g==", - "license": "MIT", - "dependencies": { - "@json-layout/vocabulary": "^0.23.2", - "@types/markdown-it": "^13.0.1", - "ajv": "^8.12.0", - "ajv-errors": "^3.0.0", - "ajv-formats": "^2.1.1", - "ajv-i18n": "^4.2.0", - "debug": "^4.3.4", - "immer": "^10.0.3", - "magicast": "^0.3.3", - "markdown-it": "^13.0.2" - } - }, - "node_modules/@json-layout/core/node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" + "funding": { + "url": "https://opencollective.com/libvips" }, - "peerDependencies": { - "ajv": "^8.0.0" + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/@json-layout/vocabulary": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/@json-layout/vocabulary/-/vocabulary-0.23.2.tgz", - "integrity": "sha512-CDQ/nFZmcMdhn0Ud/f5Q3IoRemQQdw2CPm5pufRqo27T71JhIw12KluVW1jsZWlwK3v7q7yqOoVS8Ax9bUOQ4w==", - "license": "MIT", - "dependencies": { - "ajv": "^8.12.0", - "ajv-errors": "^3.0.0", - "ajv-formats": "^2.1.1", - "debug": "^4.3.4" - } - }, - "node_modules/@json-layout/vocabulary/node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" + "funding": { + "url": "https://opencollective.com/libvips" }, - "peerDependencies": { - "ajv": "^8.0.0" + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/@jsonquerylang/jsonquery": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jsonquerylang/jsonquery/-/jsonquery-3.1.1.tgz", - "integrity": "sha512-P6Qo5egd3W8TBpqQsqaZtZ9lPO7oXBM21QdkYamCAYZHv9VCPXiI8NeIuSoXdoe5zKVZPUWmqaI14uacJLmcNw==", - "license": "ISC", - "bin": { - "jsonquery": "bin/cli.js" - } - }, - "node_modules/@koumoul/vjsf": { - "version": "3.0.0-beta.46", - "resolved": "https://registry.npmjs.org/@koumoul/vjsf/-/vjsf-3.0.0-beta.46.tgz", - "integrity": "sha512-dp9EuyZrZNRHb5+8eLMHWNEI9HPhpLoeOWzDWj43tE+162mGmhi42SqVMS5fbx73ZHF2FHe4jZszgdj0MiYB2A==", - "license": "MIT", - "dependencies": { - "@json-layout/core": "0.32.1", - "@vueuse/core": "^10.5.0", - "debug": "^4.3.4", - "ejs": "^3.1.9" + "funding": { + "url": "https://opencollective.com/libvips" }, - "peerDependencies": { - "vue": "^3.4.3", - "vuetify": "^3.6.13" + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" } }, - "node_modules/@lezer/common": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", - "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", - "license": "MIT" + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } }, - "node_modules/@lezer/highlight": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", - "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", - "license": "MIT", + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "optional": true, "dependencies": { - "@lezer/common": "^1.0.0" + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@lezer/json": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.2.tgz", - "integrity": "sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==", - "license": "MIT", + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.7.tgz", + "integrity": "sha512-5yximcFK5FNompXfJFoWanu5l8v1hNGqNHh9du1xETp9HWk/B/PzvchX55WYOPaIeNglG8++68AAiauBAtbnzw==", + "optional": true, "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" + "@emnapi/core": "^1.3.1", + "@emnapi/runtime": "^1.3.1", + "@tybys/wasm-util": "^0.9.0" } }, - "node_modules/@lezer/lr": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", - "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", - "license": "MIT", + "node_modules/@next/env": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.4.tgz", + "integrity": "sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.2.4.tgz", + "integrity": "sha512-O8ScvKtnxkp8kL9TpJTTKnMqlkZnS+QxwoQnJwPGBxjBbzd6OVVPEJ5/pMNrktSyXQD/chEfzfFzYLM6JANOOQ==", + "dev": true, "dependencies": { - "@lezer/common": "^1.0.0" + "fast-glob": "3.3.1" } }, - "node_modules/@mdi/font": { - "version": "7.4.47", - "resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz", - "integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==", - "license": "Apache-2.0" + "node_modules/@next/swc-darwin-arm64": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.4.tgz", + "integrity": "sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.4.tgz", + "integrity": "sha512-3qK2zb5EwCwxnO2HeO+TRqCubeI/NgCe+kL5dTJlPldV/uwCnUgC7VbEzgmxbfrkbjehL4H9BPztWOEtsoMwew==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.4.tgz", + "integrity": "sha512-HFN6GKUcrTWvem8AZN7tT95zPb0GUGv9v0d0iyuTb303vbXkkbHDp/DxufB04jNVD+IN9yHy7y/6Mqq0h0YVaQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.4.tgz", + "integrity": "sha512-Oioa0SORWLwi35/kVB8aCk5Uq+5/ZIumMK1kJV+jSdazFm2NzPDztsefzdmzzpx5oGCJ6FkUC7vkaUseNTStNA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.4.tgz", + "integrity": "sha512-yb5WTRaHdkgOqFOZiu6rHV1fAEK0flVpaIN2HB6kxHVSy/dIajWbThS7qON3W9/SNOH2JWkVCyulgGYekMePuw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.4.tgz", + "integrity": "sha512-Dcdv/ix6srhkM25fgXiyOieFUkz+fOYkHlydWCtB0xMST6X9XYI3yPDKBZt1xuhOytONsIFJFB08xXYsxUwJLw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.4.tgz", + "integrity": "sha512-dW0i7eukvDxtIhCYkMrZNQfNicPDExt2jPb9AZPpL7cfyUo7QSNl1DjsHjmmKp6qNAqUESyT8YFl/Aw91cNJJg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.4.tgz", + "integrity": "sha512-SbnWkJmkS7Xl3kre8SdMF6F/XDh1DTFEhp0jRTj/uB8iPKoU2bb2NDfcu+iifv1+mxQEd1g2vvSxcZbXSKyWiQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, - "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -951,7 +838,6 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, - "license": "MIT", "engines": { "node": ">= 8" } @@ -961,7 +847,6 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, - "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -970,46 +855,850 @@ "node": ">= 8" } }, - "node_modules/@parcel/watcher": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", - "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" - }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.0", - "@parcel/watcher-darwin-arm64": "2.5.0", - "@parcel/watcher-darwin-x64": "2.5.0", - "@parcel/watcher-freebsd-x64": "2.5.0", - "@parcel/watcher-linux-arm-glibc": "2.5.0", - "@parcel/watcher-linux-arm-musl": "2.5.0", - "@parcel/watcher-linux-arm64-glibc": "2.5.0", - "@parcel/watcher-linux-arm64-musl": "2.5.0", - "@parcel/watcher-linux-x64-glibc": "2.5.0", - "@parcel/watcher-linux-x64-musl": "2.5.0", - "@parcel/watcher-win32-arm64": "2.5.0", - "@parcel/watcher-win32-ia32": "2.5.0", - "@parcel/watcher-win32-x64": "2.5.0" + "node": ">=12.4.0" } }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", - "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", + "node_modules/@pkgr/core": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz", + "integrity": "sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.6.tgz", + "integrity": "sha512-2JMfHJf/eVnwq+2dewT3C0acmCWD3XiVA1Da+jTDqo342UlU13WvXtqHhG+yJw5JeQmu4ue2eMy6gcEArLBlcw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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-checkbox": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.1.tgz", + "integrity": "sha512-xTaLKAO+XXMPK/BpVTSaAAhlefmvMSACjIhK9mGsImvX2ljcTDm8VGR1CuS1uYcNdR5J+oiOhoJZc5un6bh3VQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "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-collection": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.6.tgz", + "integrity": "sha512-PbhRFK4lIEw9ADonj48tiYWzkllz81TM7KVYyyMMw2cwHO7D5h4XKEblL8NlaRisTK3QTe6tBEhDccFUryxHBQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-slot": "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-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.13.tgz", + "integrity": "sha512-ARFmqUyhIVS3+riWzwGTe7JLjqwqgnODBUZdqpWar/z1WFs9z76fuOs/2BOWCR+YboRn4/WN9aoaGVwqNRr8VA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.9", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.6", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.8", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-slot": "1.2.2", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.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-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.9.tgz", + "integrity": "sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "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-focus-guards": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", + "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.6.tgz", + "integrity": "sha512-r9zpYNUQY+2jWHWZGyddQLL9YHkM/XvSFHVcWs7bdVuxMAnCwTAuy6Pf47Z4nw7dYcUou1vg/VgjjrrH03VeBw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "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-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.6.tgz", + "integrity": "sha512-S/hv1mTlgcPX2gCTJrWuTjSXf7ER3Zf7zWGtOprxhIIY93Qin3n5VgNA0Ez9AgrK/lEtlYgzLd4f5x6AVar4Yw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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-popper": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.6.tgz", + "integrity": "sha512-7iqXaOWIjDBfIG7aq8CUEeCSsQMLFdn7VEE8TaFz704DtEzpPHR7w/uuzRflvKgltqSAImgcmxQ7fFX3X7wasg==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.6", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "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-portal": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.8.tgz", + "integrity": "sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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-primitive": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz", + "integrity": "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "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-roving-focus": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.9.tgz", + "integrity": "sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.6", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.2", + "@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-select": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.4.tgz", + "integrity": "sha512-/OOm58Gil4Ev5zT8LyVzqfBcij4dTHYdeyuF5lMHZ2bIp0Lk9oETocYiJ5QC0dHekEQnK6L/FNJCceeb4AkZ6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.6", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.9", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.6", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.6", + "@radix-ui/react-portal": "1.1.8", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-slot": "1.2.2", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.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-slot": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", + "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.4.tgz", + "integrity": "sha512-yZCky6XZFnR7pcGonJkr9VyNRu46KcYAbyg1v/gVVCZUr8UJ4x+RpncC27hHtiZ15jC+3WS8Yg/JSgyIHnYYsQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "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-tabs": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.11.tgz", + "integrity": "sha512-4FiKSVoXqPP/KfzlB7lwwqoFV6EPwkrrqGp9cUYXjwDYHhvpnqq79P+EPHKcdoTE7Rl8w/+6s9rTlsfXHES9GA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-roving-focus": "1.1.9", + "@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-toggle": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.8.tgz", + "integrity": "sha512-hrpa59m3zDnsa35LrTOH5s/a3iGv/VD+KKQjjiCTo/W4r0XwPpiWQvAv6Xl1nupSoaZeNNxW6sJH9ZydsjKdYQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-primitive": "2.1.2", + "@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-toggle-group": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.9.tgz", + "integrity": "sha512-HJ6gXdYVN38q/5KDdCcd+JTuXUyFZBMJbwXaU/82/Gi+V2ps6KpiZ2sQecAeZCV80POGRfkUBdUIj6hIdF6/MQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-roving-focus": "1.1.9", + "@radix-ui/react-toggle": "1.1.8", + "@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-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.2.tgz", + "integrity": "sha512-ORCmRUbNiZIv6uV5mhFrhsIKw4UX/N3syZtyqvry61tbGm4JlgQuSn0hk5TwCARsCjkcnuRkSdCE3xfb+ADHew==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.11.0.tgz", + "integrity": "sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==", + "dev": true + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.5.tgz", + "integrity": "sha512-CBhSWo0vLnWhXIvpD0qsPephiaUYfHUX3U9anwDaHZAeuGpTiB3XmsxPAN6qX7bFhipyGBqOa1QYQVVhkOUGxg==", + "license": "MIT", + "dependencies": { + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.29.2", + "tailwindcss": "4.1.5" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.5.tgz", + "integrity": "sha512-1n4br1znquEvyW/QuqMKQZlBen+jxAbvyduU87RS8R3tUSvByAkcaMTkJepNIrTlYhD+U25K4iiCIxE6BGdRYA==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.5", + "@tailwindcss/oxide-darwin-arm64": "4.1.5", + "@tailwindcss/oxide-darwin-x64": "4.1.5", + "@tailwindcss/oxide-freebsd-x64": "4.1.5", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.5", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.5", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.5", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.5", + "@tailwindcss/oxide-linux-x64-musl": "4.1.5", + "@tailwindcss/oxide-wasm32-wasi": "4.1.5", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.5", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.5" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.5.tgz", + "integrity": "sha512-LVvM0GirXHED02j7hSECm8l9GGJ1RfgpWCW+DRn5TvSaxVsv28gRtoL4aWKGnXqwvI3zu1GABeDNDVZeDPOQrw==", "cpu": [ "arm64" ], @@ -1019,17 +1708,13 @@ "android" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", - "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.5.tgz", + "integrity": "sha512-//TfCA3pNrgnw4rRJOqavW7XUk8gsg9ddi8cwcsWXp99tzdBAZW0WXrD8wDyNbqjW316Pk2hiN/NJx/KWHl8oA==", "cpu": [ "arm64" ], @@ -1039,17 +1724,13 @@ "darwin" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", - "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.5.tgz", + "integrity": "sha512-XQorp3Q6/WzRd9OalgHgaqgEbjP3qjHrlSUb5k1EuS1Z9NE9+BbzSORraO+ecW432cbCN7RVGGL/lSnHxcd+7Q==", "cpu": [ "x64" ], @@ -1059,17 +1740,13 @@ "darwin" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", - "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.5.tgz", + "integrity": "sha512-bPrLWbxo8gAo97ZmrCbOdtlz/Dkuy8NK97aFbVpkJ2nJ2Jo/rsCbu0TlGx8joCuA3q6vMWTSn01JY46iwG+clg==", "cpu": [ "x64" ], @@ -1079,17 +1756,13 @@ "freebsd" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", - "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.5.tgz", + "integrity": "sha512-1gtQJY9JzMAhgAfvd/ZaVOjh/Ju/nCoAsvOVJenWZfs05wb8zq+GOTnZALWGqKIYEtyNpCzvMk+ocGpxwdvaVg==", "cpu": [ "arm" ], @@ -1099,37 +1772,13 @@ "linux" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", - "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", - "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.5.tgz", + "integrity": "sha512-dtlaHU2v7MtdxBXoqhxwsWjav7oim7Whc6S9wq/i/uUMTWAzq/gijq1InSgn2yTnh43kR+SFvcSyEF0GCNu1PQ==", "cpu": [ "arm64" ], @@ -1139,17 +1788,13 @@ "linux" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", - "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.5.tgz", + "integrity": "sha512-fg0F6nAeYcJ3CriqDT1iVrqALMwD37+sLzXs8Rjy8Z1ZHshJoYceodfyUwGJEsQoTyWbliFNRs2wMQNXtT7MVA==", "cpu": [ "arm64" ], @@ -1159,17 +1804,13 @@ "linux" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", - "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.5.tgz", + "integrity": "sha512-SO+F2YEIAHa1AITwc8oPwMOWhgorPzzcbhWEb+4oLi953h45FklDmM8dPSZ7hNHpIk9p/SCZKUYn35t5fjGtHA==", "cpu": [ "x64" ], @@ -1179,17 +1820,13 @@ "linux" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", - "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.5.tgz", + "integrity": "sha512-6UbBBplywkk/R+PqqioskUeXfKcBht3KU7juTi1UszJLx0KPXUo10v2Ok04iBJIaDPkIFkUOVboXms5Yxvaz+g==", "cpu": [ "x64" ], @@ -1199,280 +1836,42 @@ "linux" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", - "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.5.tgz", + "integrity": "sha512-hwALf2K9FHuiXTPqmo1KeOb83fTRNbe9r/Ixv9ZNQ/R24yw8Ge1HOWDDgTdtzntIaIUJG5dfXCf4g9AD4RiyhQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], "cpu": [ - "arm64" + "wasm32" ], "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", - "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", - "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@replit/codemirror-indentation-markers": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/@replit/codemirror-indentation-markers/-/codemirror-indentation-markers-6.5.3.tgz", - "integrity": "sha512-hL5Sfvw3C1vgg7GolLe/uxX5T3tmgOA3ZzqlMv47zjU1ON51pzNWiVbS22oh6crYhtVhv8b3gdXwoYp++2ilHw==", - "license": "MIT", - "peerDependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.2.tgz", - "integrity": "sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==", - "dev": true, - "license": "MIT", "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.9", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" }, "engines": { "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.5.tgz", - "integrity": "sha512-SU5cvamg0Eyu/F+kLeMXS7GoahL+OoizlclVFX3l5Ql6yNlywJJ0OuqTzUx0v+aHhPHEB/56CT06GQrRrGNYww==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.5.tgz", - "integrity": "sha512-S4pit5BP6E5R5C8S6tgU/drvgjtYW76FBuG6+ibG3tMvlD1h9LHVF9KmlmaUBQ8Obou7hEyS+0w+IR/VtxwNMQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.5.tgz", - "integrity": "sha512-250ZGg4ipTL0TGvLlfACkIxS9+KLtIbn7BCZjsZj88zSg2Lvu3Xdw6dhAhfe/FjjXPVNCtcSp+WZjVsD3a/Zlw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.5.tgz", - "integrity": "sha512-D8brJEFg5D+QxFcW6jYANu+Rr9SlKtTenmsX5hOSzNYVrK5oLAEMTUgKWYJP+wdKyCdeSwnapLsn+OVRFycuQg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.5.tgz", - "integrity": "sha512-PNqXYmdNFyWNg0ma5LdY8wP+eQfdvyaBAojAXgO7/gs0Q/6TQJVXAXe8gwW9URjbS0YAammur0fynYGiWsKlXw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.5.tgz", - "integrity": "sha512-kSSCZOKz3HqlrEuwKd9TYv7vxPYD77vHSUvM2y0YaTGnFc8AdI5TTQRrM1yIp3tXCKrSL9A7JLoILjtad5t8pQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.5.tgz", - "integrity": "sha512-oTXQeJHRbOnwRnRffb6bmqmUugz0glXaPyspp4gbQOPVApdpRrY/j7KP3lr7M8kTfQTyrBUzFjj5EuHAhqH4/w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.5.tgz", - "integrity": "sha512-qnOTIIs6tIGFKCHdhYitgC2XQ2X25InIbZFor5wh+mALH84qnFHvc+vmWUpyX97B0hNvwNUL4B+MB8vJvH65Fw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.5.tgz", - "integrity": "sha512-TMYu+DUdNlgBXING13rHSfUc3Ky5nLPbWs4bFnT+R6Vu3OvXkTkixvvBKk8uO4MT5Ab6lC3U7x8S8El2q5o56w==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.5.tgz", - "integrity": "sha512-PTQq1Kz22ZRvuhr3uURH+U/Q/a0pbxJoICGSprNLAoBEkyD3Sh9qP5I0Asn0y0wejXQBbsVMRZRxlbGFD9OK4A==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.5.tgz", - "integrity": "sha512-bR5nCojtpuMss6TDEmf/jnBnzlo+6n1UhgwqUvRoe4VIotC7FG1IKkyJbwsT7JDsF2jxR+NTnuOwiGv0hLyDoQ==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.5.tgz", - "integrity": "sha512-N0jPPhHjGShcB9/XXZQWuWBKZQnC1F36Ce3sDqWpujsGjDz/CQtOL9LgTrJ+rJC8MJeesMWrMWVLKKNR/tMOCA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.5.tgz", - "integrity": "sha512-uBa2e28ohzNNwjr6Uxm4XyaA1M/8aTgfF2T7UIlElLaeXkgpmIJ2EitVNQxjO9xLLLy60YqAgKn/AqSpCUkE9g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.5.tgz", - "integrity": "sha512-RXT8S1HP8AFN/Kr3tg4fuYrNxZ/pZf1HemC5Tsddc6HzgGnJm0+Lh5rAHJkDuW3StI0ynNXukidROMXYl6ew8w==", + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.5.tgz", + "integrity": "sha512-oDKncffWzaovJbkuR7/OTNFRJQVdiw/n8HnzaCItrNQUeQgjy7oUiYpsm9HUBgpmvmDpSSbGaCa2Evzvk3eFmA==", "cpu": [ "arm64" ], @@ -1480,25 +1879,15 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.5.tgz", - "integrity": "sha512-ElTYOh50InL8kzyUD6XsnPit7jYCKrphmddKAe1/Ytt74apOxDq5YEcbsiKs0fR3vff3jEneMM+3I7jbqaMyBg==", - "cpu": [ - "ia32" ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.5.tgz", - "integrity": "sha512-+lvL/4mQxSV8MukpkKyyvfwhH266COcWlXE/1qxwN08ajovta3459zrjLghYMgDerlzNwLAcFpvU+WWE5y6nAQ==", + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.5.tgz", + "integrity": "sha512-WiR4dtyrFdbb+ov0LK+7XsFOsG+0xs0PKZKkt41KDn9jYpO7baE3bXiudPVkTqUEwNfiglCygQHl2jklvSBi7Q==", "cpu": [ "x64" ], @@ -1506,322 +1895,511 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" + "node_modules/@tailwindcss/postcss": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.5.tgz", + "integrity": "sha512-5lAC2/pzuyfhsFgk6I58HcNy6vPK3dV/PoPxSDuOTVbDvCddYHzHiJZZInGIY0venvzzfrTEUAXJFULAfFmObg==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.5", + "@tailwindcss/oxide": "4.1.5", + "postcss": "^8.4.41", + "tailwindcss": "4.1.5" + } }, - "node_modules/@sphinxxxx/color-conversion": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@sphinxxxx/color-conversion/-/color-conversion-2.2.2.tgz", - "integrity": "sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw==", - "license": "ISC" + "node_modules/@tybys/wasm-util": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", + "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "license": "MIT" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/lodash": { + "version": "4.17.16", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz", + "integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.17.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", + "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", "dev": true, - "license": "MIT" - }, - "node_modules/@types/linkify-it": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz", - "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==", - "license": "MIT" - }, - "node_modules/@types/markdown-it": { - "version": "13.0.9", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-13.0.9.tgz", - "integrity": "sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==", - "license": "MIT", "dependencies": { - "@types/linkify-it": "^3", - "@types/mdurl": "^1" + "undici-types": "~6.19.2" } }, - "node_modules/@types/mdurl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz", - "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==", - "license": "MIT" - }, - "node_modules/@types/web-bluetooth": { - "version": "0.0.20", - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", - "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", - "license": "MIT" - }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@vitejs/plugin-vue": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.1.4.tgz", - "integrity": "sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "vite": "^5.0.0", - "vue": "^3.2.25" - } - }, - "node_modules/@vue-macros/common": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-1.14.0.tgz", - "integrity": "sha512-xwQhDoEXRNXobNQmdqOD20yUGdVLVLZe4zhDlT9q/E+z+mvT3wukaAoJG80XRnv/BcgOOCVpxqpkQZ3sNTgjWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.25.6", - "@rollup/pluginutils": "^5.1.0", - "@vue/compiler-sfc": "^3.5.4", - "ast-kit": "^1.1.0", - "local-pkg": "^0.5.0", - "magic-string-ast": "^0.6.2" - }, - "engines": { - "node": ">=16.14.0" - }, - "peerDependencies": { - "vue": "^2.7.0 || ^3.2.25" - }, - "peerDependenciesMeta": { - "vue": { - "optional": true - } - } - }, - "node_modules/@vue/compiler-core": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.10.tgz", - "integrity": "sha512-iXWlk+Cg/ag7gLvY0SfVucU8Kh2CjysYZjhhP70w9qI4MvSox4frrP+vDGvtQuzIcgD8+sxM6lZvCtdxGunTAA==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.25.3", - "@vue/shared": "3.5.10", - "entities": "^4.5.0", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.0" - } - }, - "node_modules/@vue/compiler-dom": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.10.tgz", - "integrity": "sha512-DyxHC6qPcktwYGKOIy3XqnHRrrXyWR2u91AjP+nLkADko380srsC2DC3s7Y1Rk6YfOlxOlvEQKa9XXmLI+W4ZA==", - "license": "MIT", - "dependencies": { - "@vue/compiler-core": "3.5.10", - "@vue/shared": "3.5.10" - } - }, - "node_modules/@vue/compiler-sfc": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.10.tgz", - "integrity": "sha512-to8E1BgpakV7224ZCm8gz1ZRSyjNCAWEplwFMWKlzCdP9DkMKhRRwt0WkCjY7jkzi/Vz3xgbpeig5Pnbly4Tow==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.25.3", - "@vue/compiler-core": "3.5.10", - "@vue/compiler-dom": "3.5.10", - "@vue/compiler-ssr": "3.5.10", - "@vue/shared": "3.5.10", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.11", - "postcss": "^8.4.47", - "source-map-js": "^1.2.0" - } - }, - "node_modules/@vue/compiler-ssr": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.10.tgz", - "integrity": "sha512-hxP4Y3KImqdtyUKXDRSxKSRkSm1H9fCvhojEYrnaoWhE4w/y8vwWhnosJoPPe2AXm5sU7CSbYYAgkt2ZPhDz+A==", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.10", - "@vue/shared": "3.5.10" - } - }, - "node_modules/@vue/devtools-api": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", - "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", - "license": "MIT" - }, - "node_modules/@vue/reactivity": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.10.tgz", - "integrity": "sha512-kW08v06F6xPSHhid9DJ9YjOGmwNDOsJJQk0ax21wKaUYzzuJGEuoKNU2Ujux8FLMrP7CFJJKsHhXN9l2WOVi2g==", - "license": "MIT", - "dependencies": { - "@vue/shared": "3.5.10" - } - }, - "node_modules/@vue/runtime-core": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.10.tgz", - "integrity": "sha512-9Q86I5Qq3swSkFfzrZ+iqEy7Vla325M7S7xc1NwKnRm/qoi1Dauz0rT6mTMmscqx4qz0EDJ1wjB+A36k7rl8mA==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.10", - "@vue/shared": "3.5.10" - } - }, - "node_modules/@vue/runtime-dom": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.10.tgz", - "integrity": "sha512-t3x7ht5qF8ZRi1H4fZqFzyY2j+GTMTDxRheT+i8M9Ph0oepUxoadmbwlFwMoW7RYCpNQLpP2Yx3feKs+fyBdpA==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.10", - "@vue/runtime-core": "3.5.10", - "@vue/shared": "3.5.10", - "csstype": "^3.1.3" - } - }, - "node_modules/@vue/server-renderer": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.10.tgz", - "integrity": "sha512-IVE97tt2kGKwHNq9yVO0xdh1IvYfZCShvDSy46JIh5OQxP1/EXSpoDqetVmyIzL7CYOWnnmMkVqd7YK2QSWkdw==", - "license": "MIT", - "dependencies": { - "@vue/compiler-ssr": "3.5.10", - "@vue/shared": "3.5.10" - }, - "peerDependencies": { - "vue": "3.5.10" - } - }, - "node_modules/@vue/shared": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.10.tgz", - "integrity": "sha512-VkkBhU97Ki+XJ0xvl4C9YJsIZ2uIlQ7HqPpZOS3m9VCvmROPaChZU6DexdMJqvz9tbgG+4EtFVrSuailUq5KGQ==", - "license": "MIT" - }, - "node_modules/@vuetify/loader-shared": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@vuetify/loader-shared/-/loader-shared-2.0.3.tgz", - "integrity": "sha512-Ss3GC7eJYkp2SF6xVzsT7FAruEmdihmn4OCk2+UocREerlXKWgOKKzTN5PN3ZVN5q05jHHrsNhTuWbhN61Bpdg==", + "node_modules/@types/react": { + "version": "19.0.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.12.tgz", + "integrity": "sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==", "devOptional": true, - "license": "MIT", "dependencies": { - "upath": "^2.0.1" - }, + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.4.tgz", + "integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==", + "devOptional": true, "peerDependencies": { - "vue": "^3.0.0", - "vuetify": "^3.0.0" + "@types/react": "^19.0.0" } }, - "node_modules/@vueuse/core": { - "version": "10.11.1", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", - "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", - "license": "MIT", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz", + "integrity": "sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ==", + "dev": true, "dependencies": { - "@types/web-bluetooth": "^0.0.20", - "@vueuse/metadata": "10.11.1", - "@vueuse/shared": "10.11.1", - "vue-demi": ">=0.14.8" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@vueuse/core/node_modules/vue-demi": { - "version": "0.14.10", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", - "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "vue-demi-fix": "bin/vue-demi-fix.js", - "vue-demi-switch": "bin/vue-demi-switch.js" + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.31.1", + "@typescript-eslint/type-utils": "8.31.1", + "@typescript-eslint/utils": "8.31.1", + "@typescript-eslint/visitor-keys": "8.31.1", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.0.1" }, "engines": { - "node": ">=12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/antfu" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@vue/composition-api": "^1.0.0-rc.1", - "vue": "^3.0.0-0 || ^2.6.0" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@vueuse/metadata": { - "version": "10.11.1", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", - "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@vueuse/shared": { - "version": "10.11.1", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", - "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", - "license": "MIT", + "node_modules/@typescript-eslint/parser": { + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.1.tgz", + "integrity": "sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q==", + "dev": true, "dependencies": { - "vue-demi": ">=0.14.8" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@vueuse/shared/node_modules/vue-demi": { - "version": "0.14.10", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", - "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "vue-demi-fix": "bin/vue-demi-fix.js", - "vue-demi-switch": "bin/vue-demi-switch.js" + "@typescript-eslint/scope-manager": "8.31.1", + "@typescript-eslint/types": "8.31.1", + "@typescript-eslint/typescript-estree": "8.31.1", + "@typescript-eslint/visitor-keys": "8.31.1", + "debug": "^4.3.4" }, "engines": { - "node": ">=12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/antfu" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@vue/composition-api": "^1.0.0-rc.1", - "vue": "^3.0.0-0 || ^2.6.0" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.31.1.tgz", + "integrity": "sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.31.1", + "@typescript-eslint/visitor-keys": "8.31.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.31.1.tgz", + "integrity": "sha512-fNaT/m9n0+dpSp8G/iOQ05GoHYXbxw81x+yvr7TArTuZuCA6VVKbqWYVZrV5dVagpDTtj/O8k5HBEE/p/HM5LA==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "8.31.1", + "@typescript-eslint/utils": "8.31.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.1.tgz", + "integrity": "sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.1.tgz", + "integrity": "sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.31.1", + "@typescript-eslint/visitor-keys": "8.31.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.31.1.tgz", + "integrity": "sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.31.1", + "@typescript-eslint/types": "8.31.1", + "@typescript-eslint/typescript-estree": "8.31.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.1.tgz", + "integrity": "sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.31.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@unrs/rspack-resolver-binding-darwin-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-darwin-arm64/-/rspack-resolver-binding-darwin-arm64-1.3.0.tgz", + "integrity": "sha512-EcjI0Hh2HiNOM0B9UuYH1PfLWgE6/SBQ4dKoHXWNloERfveha/n6aUZSBThtPGnJenmdfaJYXXZtqyNbWtJAFw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-darwin-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-darwin-x64/-/rspack-resolver-binding-darwin-x64-1.3.0.tgz", + "integrity": "sha512-3CgG+mhfudDfnaDqwEl0W1mcGTto5f5mqPyJSXcWDxrnNc7pr/p01khIgWOoOD1eCwVejmgpYvRKGBwJPwgHOQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-freebsd-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-freebsd-x64/-/rspack-resolver-binding-freebsd-x64-1.3.0.tgz", + "integrity": "sha512-ww8BwryDrpXlSajwSIEUXEv8oKDkw04L2ke3hxjaxWohuBV8pAQie9XBS4yQTyREuL2ypcqbARfoCXJJzVp7ig==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-linux-arm-gnueabihf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-linux-arm-gnueabihf/-/rspack-resolver-binding-linux-arm-gnueabihf-1.3.0.tgz", + "integrity": "sha512-WyhonI1mkuAlnG2iaMjk7uy4aWX+FWi2Au8qCCwj57wVHbAEfrN6xN2YhzbrsCC+ciumKhj5c01MqwsnYDNzWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-linux-arm-musleabihf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-linux-arm-musleabihf/-/rspack-resolver-binding-linux-arm-musleabihf-1.3.0.tgz", + "integrity": "sha512-+uCP6hIAMVWHKQnLZHESJ1U1TFVGLR3FTeaS2A4zB0k8w+IbZlWwl9FiBUOwOiqhcCCyKiUEifgnYFNGpxi3pw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-linux-arm64-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-linux-arm64-gnu/-/rspack-resolver-binding-linux-arm64-gnu-1.3.0.tgz", + "integrity": "sha512-p+s/Wp8rf75Qqs2EPw4HC0xVLLW+/60MlVAsB7TYLoeg1e1CU/QCis36FxpziLS0ZY2+wXdTnPUxr+5kkThzwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-linux-arm64-musl": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-linux-arm64-musl/-/rspack-resolver-binding-linux-arm64-musl-1.3.0.tgz", + "integrity": "sha512-cZEL9jmZ2kAN53MEk+fFCRJM8pRwOEboDn7sTLjZW+hL6a0/8JNfHP20n8+MBDrhyD34BSF4A6wPCj/LNhtOIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-linux-ppc64-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-linux-ppc64-gnu/-/rspack-resolver-binding-linux-ppc64-gnu-1.3.0.tgz", + "integrity": "sha512-IOeRhcMXTNlk2oApsOozYVcOHu4t1EKYKnTz4huzdPyKNPX0Y9C7X8/6rk4aR3Inb5s4oVMT9IVKdgNXLcpGAQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-linux-s390x-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-linux-s390x-gnu/-/rspack-resolver-binding-linux-s390x-gnu-1.3.0.tgz", + "integrity": "sha512-op54XrlEbhgVRCxzF1pHFcLamdOmHDapwrqJ9xYRB7ZjwP/zQCKzz/uAsSaAlyQmbSi/PXV7lwfca4xkv860/Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-linux-x64-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-linux-x64-gnu/-/rspack-resolver-binding-linux-x64-gnu-1.3.0.tgz", + "integrity": "sha512-orbQF7sN02N/b9QF8Xp1RBO5YkfI+AYo9VZw0H2Gh4JYWSuiDHjOPEeFPDIRyWmXbQJuiVNSB+e1pZOjPPKIyg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-linux-x64-musl": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-linux-x64-musl/-/rspack-resolver-binding-linux-x64-musl-1.3.0.tgz", + "integrity": "sha512-kpjqjIAC9MfsjmlgmgeC8U9gZi6g/HTuCqpI7SBMjsa7/9MvBaQ6TJ7dtnsV/+DXvfJ2+L5teBBXG+XxfpvIFA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-wasm32-wasi": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-wasm32-wasi/-/rspack-resolver-binding-wasm32-wasi-1.3.0.tgz", + "integrity": "sha512-JAg0hY3kGsCPk7Jgh16yMTBZ6VEnoNR1DFZxiozjKwH+zSCfuDuM5S15gr50ofbwVw9drobIP2TTHdKZ15MJZQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/rspack-resolver-binding-win32-arm64-msvc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-win32-arm64-msvc/-/rspack-resolver-binding-win32-arm64-msvc-1.3.0.tgz", + "integrity": "sha512-h5N83i407ntS3ndDkhT/3vC3Dj8oP0BIwMtekETNJcxk7IuWccSXifzCEhdxxu/FOX4OICGIHdHrxf5fJuAjfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-win32-ia32-msvc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-win32-ia32-msvc/-/rspack-resolver-binding-win32-ia32-msvc-1.3.0.tgz", + "integrity": "sha512-9QH7Gq3dRL8Q/D6PGS3Dwtjx9yw6kbCEu6iBkAUhFTDAuVUk2L0H/5NekRVA13AQaSc3OsEUKt60EOn/kq5Dug==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-win32-x64-msvc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-win32-x64-msvc/-/rspack-resolver-binding-win32-x64-msvc-1.3.0.tgz", + "integrity": "sha512-IYuXJCuwBOVV0H73l6auaZwtAPHjCPBJkxd4Co0yO6dSjDM5Na5OceaxhUmJLZ3z8kuEGhTYWIHH7PchGztnlg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", - "license": "MIT", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -1834,101 +2412,58 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, - "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-typescript": { - "version": "1.4.13", - "resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.13.tgz", - "integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==", - "license": "MIT", - "peerDependencies": { - "acorn": ">=8.9.0" - } - }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ajv-dist": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv-dist/-/ajv-dist-8.17.1.tgz", - "integrity": "sha512-KzJwANMzTTR/RERGnkx+bHzmxIfMTPMMv7+cH1d6Lx9UQ7BZyhiieq4hnO5lRuBWOtYTUL8hyWs7RJYI/45Rtg==", - "license": "MIT" - }, - "node_modules/ajv-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", - "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==", - "license": "MIT", - "peerDependencies": { - "ajv": "^8.0.1" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", + "node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, "dependencies": { - "ajv": "^8.0.0" + "environment": "^1.0.0" }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-i18n": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/ajv-i18n/-/ajv-i18n-4.2.0.tgz", - "integrity": "sha512-v/ei2UkCEeuKNXh8RToiFsUclmU+G57LO1Oo22OagNMENIw+Yb8eMwvHu7Vn9fmkjJyv6XclhJ8TbuigSglPkg==", - "license": "MIT", - "peerDependencies": { - "ajv": "^8.0.0-beta.0" - } - }, - "node_modules/ansi_up": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ansi_up/-/ansi_up-6.0.2.tgz", - "integrity": "sha512-3G3vKvl1ilEp7J1u6BmULpMA0xVoW/f4Ekqhl8RTrJrhEBkonKn5k3bUc5Xt+qDayA6iDX0jyUh3AbZjB/l0tw==", - "license": "MIT", "engines": { - "node": "*" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, - "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -1939,44 +2474,41 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "devOptional": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" + "dev": true + }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "license": "Apache-2.0", + "dev": true, "engines": { "node": ">= 0.4" } }, "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" }, "engines": { "node": ">= 0.4" @@ -1990,7 +2522,6 @@ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -2006,12 +2537,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.findlastindex": { + "node_modules/array.prototype.findlast": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", - "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -2027,17 +2557,37 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -2047,16 +2597,15 @@ } }, "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -2065,21 +2614,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, - "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" }, "engines": { "node": ">= 0.4" @@ -2088,40 +2651,21 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ast-kit": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-1.2.1.tgz", - "integrity": "sha512-h31wotR7rkFLrlmGPn0kGqOZ/n5EQFvp7dBs400chpHDhHc8BK3gpvyHDluRujuGgeoTAv3dSIMz9BI3JxAWyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.25.6", - "pathe": "^1.1.2" - }, - "engines": { - "node": ">=16.14.0" - } + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true }, - "node_modules/ast-walker-scope": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.6.2.tgz", - "integrity": "sha512-1UWOyC50xI3QZkRuDj6PqDtpm1oHWtYs+NQGwqL/2R11eN3Q81PHAHPM0SWW3BNQm53UDwS//Jv8L4CCVLM1bQ==", + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.25.3", - "ast-kit": "^1.0.1" - }, "engines": { - "node": ">=16.14.0" + "node": ">= 0.4" } }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2133,7 +2677,6 @@ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, - "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -2144,10 +2687,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axe-core": { + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", + "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -2159,7 +2711,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "license": "Apache-2.0", + "dev": true, "engines": { "node": ">= 0.4" } @@ -2168,33 +2720,13 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2204,8 +2736,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "devOptional": true, - "license": "MIT", + "dev": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -2213,54 +2744,55 @@ "node": ">=8" } }, - "node_modules/builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/builtins": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz", - "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==", - "dev": true, - "license": "MIT", + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", "dependencies": { - "semver": "^7.0.0" - } - }, - "node_modules/builtins/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "streamsearch": "^1.1.0" }, "engines": { - "node": ">=10" + "node": ">=10.16.0" } }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, - "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -2274,16 +2806,34 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001707", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", + "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2295,66 +2845,81 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "devOptional": true, - "license": "MIT", + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" + "clsx": "^2.1.1" }, "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "url": "https://polar.sh/cva" } }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "devOptional": true, - "license": "ISC", + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, "dependencies": { - "is-glob": "^4.0.1" + "restore-cursor": "^5.0.0" }, "engines": { - "node": ">= 6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/codemirror": { - "version": "5.65.18", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.18.tgz", - "integrity": "sha512-Gaz4gHnkbHMGgahNt3CA5HBk5lLQBqmD/pBgeB4kQU6OedZmqMBjlRF0LSrp2tJ4wlLNPm2FfaUd1pDy0mdlpA==", - "license": "MIT" + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/codemirror-wrapped-line-indent": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/codemirror-wrapped-line-indent/-/codemirror-wrapped-line-indent-1.0.8.tgz", - "integrity": "sha512-5UwuHCz4oAZuvot1DbfFxSxJacTESdNGa/KpJD7HfpVpDAJdgB1vV9OG4b4pkJqPWuOfIpFLTQEKS85kTpV+XA==", + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", - "peerDependencies": { - "@codemirror/language": "^6.9.0", - "@codemirror/state": "^6.2.1", - "@codemirror/view": "^6.17.1" + "engines": { + "node": ">=6" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" } }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", + "devOptional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2366,7 +2931,23 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" + "devOptional": true + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true }, "node_modules/combined-stream": { "version": "1.0.8", @@ -2380,42 +2961,26 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, - "node_modules/confbox": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", - "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", - "dev": true, - "license": "MIT" - }, - "node_modules/core-js": { - "version": "3.38.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.38.1.tgz", - "integrity": "sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==", - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/crelt": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", - "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", - "license": "MIT" + "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, - "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2425,35 +2990,27 @@ "node": ">= 8" } }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "devOptional": true + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true }, "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -2463,31 +3020,29 @@ } }, "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/inspect-js" } }, "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" }, @@ -2499,10 +3054,10 @@ } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, "dependencies": { "ms": "^2.1.3" }, @@ -2519,15 +3074,13 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, - "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -2545,7 +3098,6 @@ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, - "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -2568,120 +3120,132 @@ } }, "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "license": "Apache-2.0", - "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "engines": { - "node": ">=0.10" + "node": ">=8" } }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" }, "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, - "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, "engines": { "node": ">=0.10.0" } }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, "engines": { - "node": ">=0.12" + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "engines": { + "node": ">=18" }, "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/es-abstract": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", - "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", "dev": true, - "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" }, "engines": { "node": ">= 0.4" @@ -2691,14 +3255,9 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "engines": { "node": ">= 0.4" } @@ -2707,18 +3266,41 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", "dev": true, - "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, "engines": { "node": ">= 0.4" } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dev": true, - "license": "MIT", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dependencies": { "es-errors": "^1.3.0" }, @@ -2727,40 +3309,40 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", - "dev": true, - "license": "MIT", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, - "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, - "license": "MIT", "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" }, "engines": { "node": ">= 0.4" @@ -2769,51 +3351,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "devOptional": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -2822,118 +3364,102 @@ } }, "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.23.0.tgz", + "integrity": "sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.2", + "@eslint/config-helpers": "^0.2.0", + "@eslint/core": "^0.12.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.23.0", + "@eslint/plugin-kit": "^0.2.7", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-compat-utils": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", - "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12" + "url": "https://eslint.org/donate" }, "peerDependencies": { - "eslint": ">=6.0.0" - } - }, - "node_modules/eslint-compat-utils/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "jiti": "*" }, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-config-standard": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", - "integrity": "sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" + "peerDependenciesMeta": { + "jiti": { + "optional": true } - ], - "license": "MIT", - "engines": { - "node": ">=12.0.0" + } + }, + "node_modules/eslint-config-next": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.2.4.tgz", + "integrity": "sha512-v4gYjd4eYIme8qzaJItpR5MMBXJ0/YV07u7eb50kEnlEmX7yhOjdUdzz70v4fiINYRjLf8X8TbogF0k7wlz6sA==", + "dev": true, + "dependencies": { + "@next/eslint-plugin-next": "15.2.4", + "@rushstack/eslint-patch": "^1.10.3", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { - "eslint": "^8.0.1", - "eslint-plugin-import": "^2.25.2", - "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", - "eslint-plugin-promise": "^6.0.0" + "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.2.tgz", + "integrity": "sha512-Epgp/EofAUeEpIdZkW60MHKvPyru1ruQJxPL+WIycnaPApuseK0Zpkrh/FwL9oIpQvIhJwV7ptOy0DWUjTlCiA==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" } }, "node_modules/eslint-import-resolver-node": { @@ -2941,7 +3467,6 @@ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", @@ -2953,17 +3478,49 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.9.1.tgz", + "integrity": "sha512-euxa5rTGqHeqVxmOHT25hpk58PxkQ4mNoX6Yun4ooGaCHAxOCojJYNvjmyeOQxj/LyW+3fulH0+xtk+p2kPPTw==", + "dev": true, + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^1.3.0", + "rspack-resolver": "^1.1.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.12" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, "node_modules/eslint-module-utils": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^3.2.7" }, @@ -2981,59 +3538,15 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, - "node_modules/eslint-plugin-es": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", - "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-utils": "^2.0.0", - "regexpp": "^3.0.0" - }, - "engines": { - "node": ">=8.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=4.19.1" - } - }, - "node_modules/eslint-plugin-es-x": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz", - "integrity": "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/ota-meshi", - "https://opencollective.com/eslint" - ], - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.1.2", - "@eslint-community/regexpp": "^4.11.0", - "eslint-compat-utils": "^0.5.1" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": ">=8" - } - }, "node_modules/eslint-plugin-import": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz", - "integrity": "sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, - "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -3043,7 +3556,7 @@ "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.9.0", + "eslint-module-utils": "^2.12.0", "hasown": "^2.0.2", "is-core-module": "^2.15.1", "is-glob": "^4.0.3", @@ -3052,13 +3565,14 @@ "object.groupby": "^1.0.3", "object.values": "^1.2.0", "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "node_modules/eslint-plugin-import/node_modules/debug": { @@ -3066,238 +3580,188 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/eslint-plugin-n": { - "version": "16.6.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.6.2.tgz", - "integrity": "sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==", + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", "dev": true, - "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "builtins": "^5.0.1", - "eslint-plugin-es-x": "^7.5.0", - "get-tsconfig": "^4.7.0", - "globals": "^13.24.0", - "ignore": "^5.2.4", - "is-builtin-module": "^3.2.1", - "is-core-module": "^2.12.1", + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", "minimatch": "^3.1.2", - "resolve": "^1.22.2", - "semver": "^7.5.3" + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" }, "engines": { - "node": ">=16.0.0" + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.6.tgz", + "integrity": "sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/mysticatea" + "url": "https://opencollective.com/eslint-plugin-prettier" }, "peerDependencies": { - "eslint": ">=7.0.0" + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } } }, - "node_modules/eslint-plugin-n/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "node_modules/eslint-plugin-react": { + "version": "7.37.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", + "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, "engines": { "node": ">=10" - } - }, - "node_modules/eslint-plugin-node": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", - "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-plugin-es": "^3.0.0", - "eslint-utils": "^2.0.0", - "ignore": "^5.1.1", - "minimatch": "^3.0.4", - "resolve": "^1.10.1", - "semver": "^6.1.0" - }, - "engines": { - "node": ">=8.10.0" }, "peerDependencies": { - "eslint": ">=5.16.0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, - "node_modules/eslint-plugin-promise": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.6.0.tgz", - "integrity": "sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==", + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, - "license": "ISC", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" }, "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-plugin-vue": { - "version": "9.28.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.28.0.tgz", - "integrity": "sha512-ShrihdjIhOTxs+MfWun6oJWuk+g/LAhN+CiuOl/jjkG3l0F2AuK5NMTaWqyvBgkFtpYmyks6P4603mLmhNJW8g==", + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "globals": "^13.24.0", - "natural-compare": "^1.4.0", - "nth-check": "^2.1.1", - "postcss-selector-parser": "^6.0.15", - "semver": "^7.6.3", - "vue-eslint-parser": "^9.4.3", - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": "^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-vue/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" } }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=4" - } - }, "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, - "license": "Apache-2.0", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/eslint/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/esm-env": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.1.4.tgz", - "integrity": "sha512-oO82nKPHKkzIj/hbtuDYy/JHqBHFlMIW36SDiPCVsj87ntDLcWN+sJ1erdVryd4NxODacFTsdrIE3b7IamqbOg==", - "license": "MIT" - }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -3308,7 +3772,6 @@ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -3316,22 +3779,11 @@ "node": ">=0.10" } }, - "node_modules/esrap": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.2.2.tgz", - "integrity": "sha512-F2pSJklxx1BlQIQgooczXCPHmcWpn6EsP5oo73LQfonG9fIlIENQ8vMmfGXeojP9MrkzUNAfyU5vdFlR9shHAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15", - "@types/estree": "^1.0.1" - } - }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -3344,39 +3796,65 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "license": "MIT" - }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, - "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dev": true, - "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -3393,7 +3871,6 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -3405,81 +3882,40 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz", - "integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==", - "license": "MIT" + "dev": true }, "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, - "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, - "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" + "node": ">=16.0.0" } }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "devOptional": true, - "license": "MIT", + "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3492,7 +3928,6 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, - "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -3505,26 +3940,23 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, - "license": "MIT", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true, - "license": "ISC" + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true }, "node_modules/follow-redirects": { "version": "1.15.9", @@ -3547,71 +3979,55 @@ } }, "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, - "license": "MIT", "dependencies": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" }, "engines": { "node": ">= 6" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" }, "engines": { "node": ">= 0.4" @@ -3625,23 +4041,37 @@ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, - "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", "dev": true, - "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3650,16 +4080,48 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", - "dev": true, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dependencies": { - "call-bind": "^1.0.5", + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -3669,11 +4131,10 @@ } }, "node_modules/get-tsconfig": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", - "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", "dev": true, - "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" }, @@ -3681,34 +4142,11 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, - "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -3717,16 +4155,12 @@ } }, "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -3737,7 +4171,6 @@ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, - "license": "MIT", "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" @@ -3750,31 +4183,36 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, - "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3783,7 +4221,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", + "dev": true, "engines": { "node": ">=8" } @@ -3793,7 +4231,6 @@ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, - "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" }, @@ -3802,11 +4239,13 @@ } }, "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, - "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -3815,11 +4254,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, - "license": "MIT", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "engines": { "node": ">= 0.4" }, @@ -3831,8 +4268,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" }, @@ -3847,8 +4282,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -3856,44 +4289,44 @@ "node": ">= 0.4" } }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmmirror.com/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4" } }, - "node_modules/immer": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", - "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/immutable": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", - "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", - "license": "MIT" - }, - "node_modules/immutable-json-patch": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/immutable-json-patch/-/immutable-json-patch-6.0.1.tgz", - "integrity": "sha512-BHL/cXMjwFZlTOffiWNdY8ZTvNyYLrutCnWxrcKPHr5FqpAb6vsO6WWSPnVSys3+DruFN6lhHJJPHi8uELQL5g==", - "license": "ISC" - }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, - "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -3910,54 +4343,58 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.8.19" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" } }, "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "optional": true + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3967,40 +4404,12 @@ } }, "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, - "license": "MIT", "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "has-bigints": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4009,20 +4418,29 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-builtin-module": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", - "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, - "license": "MIT", "dependencies": { - "builtin-modules": "^3.3.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">=6" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.3.0.tgz", + "integrity": "sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==", + "dev": true, + "dependencies": { + "semver": "^7.6.3" } }, "node_modules/is-callable": { @@ -4030,7 +4448,6 @@ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4039,11 +4456,10 @@ } }, "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, - "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -4055,12 +4471,13 @@ } }, "node_modules/is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, - "license": "MIT", "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" }, "engines": { @@ -4071,13 +4488,13 @@ } }, "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, - "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4090,18 +4507,61 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "devOptional": true, - "license": "MIT", + "dev": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "devOptional": true, - "license": "MIT", + "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -4109,12 +4569,11 @@ "node": ">=0.10.0" } }, - "node_modules/is-negative-zero": { + "node_modules/is-map": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4126,20 +4585,19 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "devOptional": true, - "license": "MIT", + "dev": true, "engines": { "node": ">=0.12.0" } }, "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, - "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4148,34 +4606,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-reference": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", - "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -4184,14 +4624,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.7" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -4200,14 +4651,26 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, - "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4217,13 +4680,14 @@ } }, "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, - "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -4233,13 +4697,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, - "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -4248,14 +4711,44 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4265,49 +4758,51 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, - "license": "ISC" - }, - "node_modules/jake": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", - "license": "Apache-2.0", "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" - }, - "bin": { - "jake": "bin/cli.js" + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" }, "engines": { - "node": ">=10" + "node": ">= 0.4" } }, - "node_modules/jmespath": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", - "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", - "license": "Apache-2.0", - "engines": { - "node": ">= 0.6.0" + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -4315,93 +4810,29 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsep": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", - "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", - "license": "MIT", - "engines": { - "node": ">= 10.16.0" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-editor-vue": { - "version": "0.17.3", - "resolved": "https://registry.npmjs.org/json-editor-vue/-/json-editor-vue-0.17.3.tgz", - "integrity": "sha512-MVpD3TInIlruq9ye/J3XmYHTH+pqfyW0E1GVUV2ug5M0X/19zGslJ+FgeikDflvTVUxhVuCEnc9spMYmPj5Lyw==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "vanilla-jsoneditor": "^2.0.0", - "vue-demi": "^0.14.10" - }, - "peerDependencies": { - "@vue/composition-api": ">=1", - "vue": "2||3" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } - } - }, - "node_modules/json-editor-vue/node_modules/vue-demi": { - "version": "0.14.10", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", - "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "vue-demi-fix": "bin/vue-demi-fix.js", - "vue-demi-switch": "bin/vue-demi-switch.js" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "@vue/composition-api": "^1.0.0-rc.1", - "vue": "^3.0.0-0 || ^2.6.0" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } - } + "dev": true }, "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/json-source-map/-/json-source-map-0.6.1.tgz", - "integrity": "sha512-1QoztHPsMQqhDq0hlXY5ZqcEdUzxQEIxgFkKl4WUp2pgShObl+9ovi4kRh2TfvAfxAoHOJ9vIMEqk3k4iex7tg==", - "license": "MIT" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/json5": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, - "license": "MIT", "dependencies": { "minimist": "^1.2.0" }, @@ -4409,31 +4840,19 @@ "json5": "lib/cli.js" } }, - "node_modules/jsonpath-plus": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.2.0.tgz", - "integrity": "sha512-T9V+8iNYKFL2n2rF+w02LBOT2JjDnTjioaNFrxRy0Bv1y/hNsqR/EBK7Ojy2ythRHwmz2cRIls+9JitQGZC/sw==", - "license": "MIT", + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, "dependencies": { - "@jsep-plugin/assignment": "^1.3.0", - "@jsep-plugin/regex": "^1.0.4", - "jsep": "^1.4.0" - }, - "bin": { - "jsonpath": "bin/jsonpath-cli.js", - "jsonpath-plus": "bin/jsonpath-cli.js" + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/jsonrepair": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.10.0.tgz", - "integrity": "sha512-0Ex64Exiw0rPUEcSbhPN0ae4/5D0DZLIob9yagAF1OG5iU0mP+/t7q4gcxtQdn6i7FuQy2J/w1XbOdu/uhGV0w==", - "license": "ISC", - "bin": { - "jsonrepair": "bin/cli.js" + "node": ">=4.0" } }, "node_modules/keyv": { @@ -4441,17 +4860,33 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, - "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, - "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -4460,44 +4895,307 @@ "node": ">= 0.8.0" } }, - "node_modules/linkify-it": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", - "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", - "license": "MIT", + "node_modules/lightningcss": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", + "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==", + "license": "MPL-2.0", "dependencies": { - "uc.micro": "^1.0.1" + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.29.2", + "lightningcss-darwin-x64": "1.29.2", + "lightningcss-freebsd-x64": "1.29.2", + "lightningcss-linux-arm-gnueabihf": "1.29.2", + "lightningcss-linux-arm64-gnu": "1.29.2", + "lightningcss-linux-arm64-musl": "1.29.2", + "lightningcss-linux-x64-gnu": "1.29.2", + "lightningcss-linux-x64-musl": "1.29.2", + "lightningcss-win32-arm64-msvc": "1.29.2", + "lightningcss-win32-x64-msvc": "1.29.2" } }, - "node_modules/local-pkg": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", - "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mlly": "^1.4.2", - "pkg-types": "^1.0.3" + "node_modules/lightningcss-darwin-arm64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz", + "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz", + "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz", + "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz", + "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz", + "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz", + "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz", + "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz", + "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz", + "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz", + "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, "engines": { "node": ">=14" }, "funding": { - "url": "https://github.com/sponsors/antfu" + "url": "https://github.com/sponsors/antonk52" } }, - "node_modules/locate-character": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "license": "MIT" + "node_modules/lint-staged": { + "version": "15.5.1", + "resolved": "https://registry.npmmirror.com/lint-staged/-/lint-staged-15.5.1.tgz", + "integrity": "sha512-6m7u8mue4Xn6wK6gZvSCQwBvMBR36xfY24nF5bMTf2MHDYG6S3yhJuOgdYVw99hsjyDt2d4z168b3naI8+NWtQ==", + "dev": true, + "dependencies": { + "chalk": "^5.4.1", + "commander": "^13.1.0", + "debug": "^4.4.0", + "execa": "^8.0.1", + "lilconfig": "^3.1.3", + "listr2": "^8.2.5", + "micromatch": "^4.0.8", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.7.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/listr2": { + "version": "8.3.2", + "resolved": "https://registry.npmmirror.com/listr2/-/listr2-8.3.2.tgz", + "integrity": "sha512-vsBzcU4oE+v0lj4FhVLzr9dBTv4/fHIa57l+GCwovP8MoFNZJTOhGU8PXd4v2VJCbECAaijBiHntiekFMLvo0g==", + "dev": true, + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, - "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -4511,102 +5209,116 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "license": "MIT" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" + "dev": true }, - "node_modules/magic-string": { - "version": "0.30.11", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", - "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/magic-string-ast": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-0.6.2.tgz", - "integrity": "sha512-oN3Bcd7ZVt+0VGEs7402qR/tjgjbM7kPlH/z7ufJnzTLVBzXJITRHOJiwMmmYMgZfdoWQsfQcY+iKlxiBppnMA==", + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", "dev": true, - "license": "MIT", "dependencies": { - "magic-string": "^0.30.10" + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=16.14.0" - } - }, - "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" - } - }, - "node_modules/markdown-it": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.2.tgz", - "integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1", - "entities": "~3.0.1", - "linkify-it": "^4.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - }, - "bin": { - "markdown-it": "bin/markdown-it.js" - } - }, - "node_modules/markdown-it/node_modules/entities": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", - "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" + "node": ">=18" }, "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", - "license": "MIT" + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } }, - "node_modules/memoize-one": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", - "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", - "license": "MIT" + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lucide-react": { + "version": "0.507.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.507.0.tgz", + "integrity": "sha512-XfgE6gvAHwAtnbUvWiTTHx4S3VGR+cUJHEc0vrh9Ogu672I1Tue2+Cp/8JJqpytgcBHAB1FVI297W4XGNwc2dQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 8" } @@ -4615,8 +5327,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "devOptional": true, - "license": "MIT", + "dev": true, "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -4646,11 +5357,35 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -4663,41 +5398,26 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, - "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/mlly": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz", - "integrity": "sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.3", - "pathe": "^1.1.2", - "pkg-types": "^1.1.1", - "ufo": "^1.5.3" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "dev": true }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -4709,51 +5429,140 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/next": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/next/-/next-15.2.4.tgz", + "integrity": "sha512-VwL+LAaPSxEkd3lU2xWbgEOtrM8oedmyhBqaVNmgKB+GvZlCy9rgaEc+y2on0wv+l0oSFqLtYD6dcC1eAedUaQ==", + "dependencies": { + "@next/env": "15.2.4", + "@swc/counter": "0.1.3", + "@swc/helpers": "0.5.15", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.2.4", + "@next/swc-darwin-x64": "15.2.4", + "@next/swc-linux-arm64-gnu": "15.2.4", + "@next/swc-linux-arm64-musl": "15.2.4", + "@next/swc-linux-x64-gnu": "15.2.4", + "@next/swc-linux-x64-musl": "15.2.4", + "@next/swc-win32-arm64-msvc": "15.2.4", + "@next/swc-win32-x64-msvc": "15.2.4", + "sharp": "^0.33.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", "dev": true, - "license": "MIT" + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "license": "MIT" + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT", - "optional": true - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "devOptional": true, - "license": "MIT", + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4766,21 +5575,21 @@ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", "object-keys": "^1.1.1" }, "engines": { @@ -4790,12 +5599,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/object.fromentries": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -4814,7 +5637,6 @@ "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -4825,13 +5647,13 @@ } }, "node_modules/object.values": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", - "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, @@ -4842,14 +5664,19 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", "dev": true, - "license": "ISC", "dependencies": { - "wrappy": "1" + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/optionator": { @@ -4857,7 +5684,6 @@ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, - "license": "MIT", "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -4870,12 +5696,28 @@ "node": ">= 0.8.0" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, - "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -4891,7 +5733,6 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, - "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -4907,7 +5748,6 @@ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, - "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -4920,27 +5760,15 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } @@ -4949,28 +5777,18 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", - "license": "ISC" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "devOptional": true, - "license": "MIT", + "dev": true, "engines": { "node": ">=8.6" }, @@ -4978,32 +5796,31 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkg-types": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.0.tgz", - "integrity": "sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==", + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.7", - "mlly": "^1.7.1", - "pathe": "^1.1.2" + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" } }, "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "funding": [ { "type": "opencollective", @@ -5020,38 +5837,61 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -5063,7 +5903,6 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } @@ -5086,33 +5925,132 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "MIT" + ] }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, + "node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", "engines": { - "node": ">=8.10.0" + "node": ">=0.10.0" } }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", - "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", - "dev": true, + "node_modules/react-dom": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "dependencies": { + "scheduler": "^0.25.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.56.3", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.3.tgz", + "integrity": "sha512-IK18V6GVbab4TAo1/cz3kqajxbDPGofdF0w7VHdCo0Nt8PrPlOZcuuDq9YYIV1BtjcX78x0XsldbQRQnQXWXmw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/react-remove-scroll": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", + "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", "es-errors": "^1.3.0", - "set-function-name": "^2.0.1" + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -5121,42 +6059,42 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, - "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dev": true, - "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5166,7 +6104,6 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } @@ -5176,79 +6113,81 @@ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "dev": true, - "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, - "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/rspack-resolver": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rspack-resolver/-/rspack-resolver-1.3.0.tgz", + "integrity": "sha512-az/PLDwa1xijNv4bAFBS8mtqqJC1Y3lVyFag4cuyIUOHq/ft5kSZlHbqYaLZLpsQtPWv4ZGDo5ycySKJzUvU/A==", "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/roboto-fontface": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.10.0.tgz", - "integrity": "sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g==", - "license": "Apache-2.0" - }, - "node_modules/rollup": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.5.tgz", - "integrity": "sha512-WoinX7GeQOFMGznEcWA1WrTQCd/tpEbMkc3nuMs9BT0CPjMdSjPMTVClwWd4pgSQwJdP65SK9mTCNvItlr5o7w==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.6" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "url": "https://github.com/sponsors/JounQin" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.22.5", - "@rollup/rollup-android-arm64": "4.22.5", - "@rollup/rollup-darwin-arm64": "4.22.5", - "@rollup/rollup-darwin-x64": "4.22.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.22.5", - "@rollup/rollup-linux-arm-musleabihf": "4.22.5", - "@rollup/rollup-linux-arm64-gnu": "4.22.5", - "@rollup/rollup-linux-arm64-musl": "4.22.5", - "@rollup/rollup-linux-powerpc64le-gnu": "4.22.5", - "@rollup/rollup-linux-riscv64-gnu": "4.22.5", - "@rollup/rollup-linux-s390x-gnu": "4.22.5", - "@rollup/rollup-linux-x64-gnu": "4.22.5", - "@rollup/rollup-linux-x64-musl": "4.22.5", - "@rollup/rollup-win32-arm64-msvc": "4.22.5", - "@rollup/rollup-win32-ia32-msvc": "4.22.5", - "@rollup/rollup-win32-x64-msvc": "4.22.5", - "fsevents": "~2.3.2" + "@unrs/rspack-resolver-binding-darwin-arm64": "1.3.0", + "@unrs/rspack-resolver-binding-darwin-x64": "1.3.0", + "@unrs/rspack-resolver-binding-freebsd-x64": "1.3.0", + "@unrs/rspack-resolver-binding-linux-arm-gnueabihf": "1.3.0", + "@unrs/rspack-resolver-binding-linux-arm-musleabihf": "1.3.0", + "@unrs/rspack-resolver-binding-linux-arm64-gnu": "1.3.0", + "@unrs/rspack-resolver-binding-linux-arm64-musl": "1.3.0", + "@unrs/rspack-resolver-binding-linux-ppc64-gnu": "1.3.0", + "@unrs/rspack-resolver-binding-linux-s390x-gnu": "1.3.0", + "@unrs/rspack-resolver-binding-linux-x64-gnu": "1.3.0", + "@unrs/rspack-resolver-binding-linux-x64-musl": "1.3.0", + "@unrs/rspack-resolver-binding-wasm32-wasi": "1.3.0", + "@unrs/rspack-resolver-binding-win32-arm64-msvc": "1.3.0", + "@unrs/rspack-resolver-binding-win32-ia32-msvc": "1.3.0", + "@unrs/rspack-resolver-binding-win32-x64-msvc": "1.3.0" } }, "node_modules/run-parallel": { @@ -5270,21 +6209,20 @@ "url": "https://feross.org/support" } ], - "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } }, "node_modules/safe-array-concat": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", - "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, "engines": { @@ -5294,16 +6232,14 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-regex-test": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", - "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", "es-errors": "^1.3.0", - "is-regex": "^1.1.4" + "isarray": "^2.0.5" }, "engines": { "node": ">= 0.4" @@ -5312,39 +6248,38 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/sass": { - "version": "1.77.6", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz", - "integrity": "sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==", - "devOptional": true, - "license": "MIT", + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" }, "engines": { - "node": ">=14.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/scule": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", - "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", - "dev": true, - "license": "MIT" + "node_modules/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==" }, "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "devOptional": true, "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/set-function-length": { @@ -5352,7 +6287,6 @@ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, - "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -5370,7 +6304,6 @@ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, - "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -5381,12 +6314,64 @@ "node": ">= 0.4" } }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, - "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -5399,22 +6384,21 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5423,32 +6407,236 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/sortablejs": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz", - "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==", - "license": "MIT" + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/sonner": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.3.tgz", + "integrity": "sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, - "node_modules/string.prototype.trim": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", - "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5458,16 +6646,19 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", - "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5477,7 +6668,6 @@ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -5491,16 +6681,18 @@ } }, "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, - "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-bom": { @@ -5508,17 +6700,27 @@ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" }, @@ -5526,17 +6728,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/style-mod": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", - "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", - "license": "MIT" + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -5549,7 +6767,6 @@ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5557,52 +6774,94 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/svelte": { - "version": "5.1.13", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.1.13.tgz", - "integrity": "sha512-xVNk8yLsZNfkyqWzVg8+nfU9ewiSjVW0S4qyTxfKa6Y7P5ZBhA+LDsh2cHWIXJQMltikQAk6W3sqGdQZSH58PA==", - "license": "MIT", + "node_modules/synckit": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.4.tgz", + "integrity": "sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==", + "dev": true, "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@jridgewell/sourcemap-codec": "^1.5.0", - "@types/estree": "^1.0.5", - "acorn": "^8.12.1", - "acorn-typescript": "^1.4.13", - "aria-query": "^5.3.1", - "axobject-query": "^4.1.0", - "esm-env": "^1.0.0", - "esrap": "^1.2.2", - "is-reference": "^3.0.2", - "locate-character": "^3.0.0", - "magic-string": "^0.30.11", - "zimmerframe": "^1.1.2" + "@pkgr/core": "^0.2.3", + "tslib": "^2.8.1" }, "engines": { - "node": ">=18" + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, + "node_modules/tailwind-merge": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.2.0.tgz", + "integrity": "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.5.tgz", + "integrity": "sha512-nYtSPfWGDiWgCkwQG/m+aX83XCwf62sBgg3bIlNiiOcggnS1x3uVRDAuyelBFL+vJdOPPCGElxv9DjHJjRHiVA==", "license": "MIT" }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "license": "MIT", "engines": { - "node": ">=4" + "node": ">=6" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", + "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==", + "dev": true, + "dependencies": { + "fdir": "^6.4.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", + "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "dev": true, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "devOptional": true, - "license": "MIT", + "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -5610,12 +6869,23 @@ "node": ">=8.0" } }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, - "license": "MIT", "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", @@ -5623,12 +6893,26 @@ "strip-bom": "^3.0.0" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/tw-animate-css": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.9.tgz", + "integrity": "sha512-9O4k1at9pMQff9EAcCEuy1UNO43JmaPQvq+0lwza9Y0BQ6LB38NiMj+qHqjoQf40355MX+gs6wtlR6H9WsSXFg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, - "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -5636,46 +6920,31 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -5685,18 +6954,18 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", - "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, - "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" }, "engines": { "node": ">= 0.4" @@ -5706,18 +6975,17 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", - "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-proto": "^1.0.3", "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" }, "engines": { "node": ">= 0.4" @@ -5726,533 +6994,130 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", - "license": "MIT" - }, - "node_modules/ufo": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", - "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, - "license": "MIT" + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.31.1.tgz", + "integrity": "sha512-j6DsEotD/fH39qKzXTQRwYYWlt7D+0HmfpOK+DVhwJOFLcdmn92hq3mBb7HlKJHbjjI/gTOqEcc9d6JfpFf/VA==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.31.1", + "@typescript-eslint/parser": "8.31.1", + "@typescript-eslint/utils": "8.31.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } }, "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", + "call-bound": "^1.0.3", "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/unplugin": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.14.1.tgz", - "integrity": "sha512-lBlHbfSFPToDYp9pjXlUEFVxYLaue9f9T1HC+4OHlmj+HnMDdz9oZY+erXfoCe/5V/7gKUSY2jpXPb9S7f0f/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.12.1", - "webpack-virtual-modules": "^0.6.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "webpack-sources": "^3" - }, - "peerDependenciesMeta": { - "webpack-sources": { - "optional": true - } - } - }, - "node_modules/unplugin-fonts": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unplugin-fonts/-/unplugin-fonts-1.1.1.tgz", - "integrity": "sha512-/Aw/rL9D2aslGGM0vi+2R2aG508RSwawLnnBuo+JDSqYc4cHJO1R1phllhN6GysEhBp/6a4B6+vSFPVapWyAAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-glob": "^3.2.12", - "unplugin": "^1.3.1" - }, - "peerDependencies": { - "@nuxt/kit": "^3.0.0", - "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "@nuxt/kit": { - "optional": true - } - } - }, - "node_modules/unplugin-vue-components": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.27.4.tgz", - "integrity": "sha512-1XVl5iXG7P1UrOMnaj2ogYa5YTq8aoh5jwDPQhemwO/OrXW+lPQKDXd1hMz15qxQPxgb/XXlbgo3HQ2rLEbmXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@antfu/utils": "^0.7.10", - "@rollup/pluginutils": "^5.1.0", - "chokidar": "^3.6.0", - "debug": "^4.3.6", - "fast-glob": "^3.3.2", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.11", - "minimatch": "^9.0.5", - "mlly": "^1.7.1", - "unplugin": "^1.12.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "@babel/parser": "^7.15.8", - "@nuxt/kit": "^3.2.2", - "vue": "2 || 3" - }, - "peerDependenciesMeta": { - "@babel/parser": { - "optional": true - }, - "@nuxt/kit": { - "optional": true - } - } - }, - "node_modules/unplugin-vue-components/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/unplugin-vue-components/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/unplugin-vue-router": { - "version": "0.10.8", - "resolved": "https://registry.npmjs.org/unplugin-vue-router/-/unplugin-vue-router-0.10.8.tgz", - "integrity": "sha512-xi+eLweYAqolIoTRSmumbi6Yx0z5M0PLvl+NFNVWHJgmE2ByJG1SZbrn+TqyuDtIyln20KKgq8tqmL7aLoiFjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.25.4", - "@rollup/pluginutils": "^5.1.0", - "@vue-macros/common": "^1.12.2", - "ast-walker-scope": "^0.6.2", - "chokidar": "^3.6.0", - "fast-glob": "^3.3.2", - "json5": "^2.2.3", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.11", - "mlly": "^1.7.1", - "pathe": "^1.1.2", - "scule": "^1.3.0", - "unplugin": "^1.12.2", - "yaml": "^2.5.0" - }, - "peerDependencies": { - "vue-router": "^4.4.0" - }, - "peerDependenciesMeta": { - "vue-router": { - "optional": true - } - } - }, - "node_modules/unplugin-vue-router/node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/upath": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz", - "integrity": "sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=4", - "yarn": "*" - } + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/vanilla-jsoneditor": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/vanilla-jsoneditor/-/vanilla-jsoneditor-2.0.2.tgz", - "integrity": "sha512-/qsSp2B/sQsyW7SO8wq7vvEeq4Wqs5JT1j5SSfqlID7CPB9S95+vhzUjrKMpUk0YtxsySlxDecAcXK5Lzg22Sw==", - "license": "ISC", - "dependencies": { - "@codemirror/autocomplete": "^6.18.1", - "@codemirror/commands": "^6.7.1", - "@codemirror/lang-json": "^6.0.1", - "@codemirror/language": "^6.10.3", - "@codemirror/lint": "^6.8.2", - "@codemirror/search": "^6.5.6", - "@codemirror/state": "^6.4.1", - "@codemirror/view": "^6.34.1", - "@fortawesome/free-regular-svg-icons": "^6.6.0", - "@fortawesome/free-solid-svg-icons": "^6.6.0", - "@jsonquerylang/jsonquery": "^3.1.1", - "@lezer/highlight": "^1.2.1", - "@replit/codemirror-indentation-markers": "^6.5.3", - "ajv": "^8.17.1", - "codemirror-wrapped-line-indent": "^1.0.8", - "diff-sequences": "^29.6.3", - "immutable-json-patch": "^6.0.1", - "jmespath": "^0.16.0", - "json-source-map": "^0.6.1", - "jsonpath-plus": "^9.0.0 || ^10.1.0", - "jsonrepair": "^3.0.0", - "lodash-es": "^4.17.21", - "memoize-one": "^6.0.0", - "natural-compare-lite": "^1.4.0", - "sass": "^1.80.4", - "svelte": "^3.54.0 || ^4.0.0 || ^5.0.0", - "vanilla-picker": "^2.12.3" - } - }, - "node_modules/vanilla-jsoneditor/node_modules/chokidar": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", - "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", "license": "MIT", "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/vanilla-jsoneditor/node_modules/readdirp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", - "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", - "license": "MIT", - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/vanilla-jsoneditor/node_modules/sass": { - "version": "1.80.6", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.80.6.tgz", - "integrity": "sha512-ccZgdHNiBF1NHBsWvacvT5rju3y1d/Eu+8Ex6c21nHp2lZGLBEtuwc415QfiI1PJa1TpCo3iXwwSRjRpn2Ckjg==", - "license": "MIT", - "dependencies": { - "chokidar": "^4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=14.0.0" - }, - "optionalDependencies": { - "@parcel/watcher": "^2.4.1" - } - }, - "node_modules/vanilla-picker": { - "version": "2.12.3", - "resolved": "https://registry.npmjs.org/vanilla-picker/-/vanilla-picker-2.12.3.tgz", - "integrity": "sha512-qVkT1E7yMbUsB2mmJNFmaXMWE2hF8ffqzMMwe9zdAikd8u2VfnsVY2HQcOUi2F38bgbxzlJBEdS1UUhOXdF9GQ==", - "license": "ISC", - "dependencies": { - "@sphinxxxx/color-conversion": "^2.2.2" - } - }, - "node_modules/vite": { - "version": "5.4.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", - "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite-plugin-vuetify": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/vite-plugin-vuetify/-/vite-plugin-vuetify-2.0.4.tgz", - "integrity": "sha512-A4cliYUoP/u4AWSRVRvAPKgpgR987Pss7LpFa7s1GvOe8WjgDq92Rt3eVXrvgxGCWvZsPKziVqfHHdCMqeDhfw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@vuetify/loader-shared": "^2.0.3", - "debug": "^4.3.3", - "upath": "^2.0.1" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "vite": ">=5", - "vue": "^3.0.0", - "vuetify": "^3.0.0" - } - }, - "node_modules/vue": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.10.tgz", - "integrity": "sha512-Vy2kmJwHPlouC/tSnIgXVg03SG+9wSqT1xu1Vehc+ChsXsRd7jLkKgMltVEFOzUdBr3uFwBCG+41LJtfAcBRng==", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.10", - "@vue/compiler-sfc": "3.5.10", - "@vue/runtime-dom": "3.5.10", - "@vue/server-renderer": "3.5.10", - "@vue/shared": "3.5.10" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/vue-eslint-parser": { - "version": "9.4.3", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", - "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "eslint-scope": "^7.1.1", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", - "esquery": "^1.4.0", - "lodash": "^4.17.21", - "semver": "^7.3.6" - }, - "engines": { - "node": "^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=6.0.0" - } - }, - "node_modules/vue-eslint-parser/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "tslib": "^2.0.0" }, "engines": { "node": ">=10" - } - }, - "node_modules/vue-router": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.5.tgz", - "integrity": "sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vue/devtools-api": "^6.6.4" - }, - "funding": { - "url": "https://github.com/sponsors/posva" }, "peerDependencies": { - "vue": "^3.2.0" - } - }, - "node_modules/vuedraggable": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz", - "integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==", - "license": "MIT", - "dependencies": { - "sortablejs": "1.14.0" - }, - "peerDependencies": { - "vue": "^3.0.1" - } - }, - "node_modules/vuetify": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.7.2.tgz", - "integrity": "sha512-q0WTcRG977+a9Dqhb8TOaPm+Xmvj0oVhnBJhAdHWFSov3HhHTTxlH2nXP/GBTXZuuMHDbBeIWFuUR2/1Fx0PPw==", - "license": "MIT", - "engines": { - "node": "^12.20 || >=14.13" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/johnleider" - }, - "peerDependencies": { - "typescript": ">=4.7", - "vite-plugin-vuetify": ">=1.0.0", - "vue": "^3.3.0", - "webpack-plugin-vuetify": ">=2.0.0" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "vite-plugin-vuetify": { - "optional": true - }, - "webpack-plugin-vuetify": { + "@types/react": { "optional": true } } }, - "node_modules/vuex": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/vuex/-/vuex-4.0.2.tgz", - "integrity": "sha512-M6r8uxELjZIK8kTKDGgZTYX/ahzblnzC4isU1tpmEuOIIKmV+TRdc+H4s8ds2NuZ7wpUTdGRzJRtoj+lI+pc0Q==", + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", "license": "MIT", "dependencies": { - "@vue/devtools-api": "^6.0.0-beta.11" + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" }, "peerDependencies": { - "vue": "^3.0.2" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/w3c-keyname": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", - "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", - "license": "MIT" - }, - "node_modules/webpack-virtual-modules": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", - "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", - "dev": true, - "license": "MIT" + "node_modules/uuidjs": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/uuidjs/-/uuidjs-5.1.0.tgz", + "integrity": "sha512-HAQPtUkr7t5Ud3uCwRcqtBRNagu/2aerrrBQE6PzgSluGijvFF75UaOq22Xw545GGviRjSLhc4c8CaSMI5h4Ng==", + "bin": { + "uuidjs": "dist/cli.js" + } }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, - "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -6264,33 +7129,81 @@ } }, "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, - "license": "MIT", "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, - "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, "engines": { @@ -6305,34 +7218,44 @@ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", "dev": true, - "license": "ISC" + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } }, - "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/yaml": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", - "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "version": "2.7.1", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", "dev": true, - "license": "ISC", "bin": { "yaml": "bin.mjs" }, @@ -6345,7 +7268,6 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -6353,11 +7275,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zimmerframe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", - "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", - "license": "MIT" + "node_modules/zod": { + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", + "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/web/package.json b/web/package.json index fa14b869..2e6aed98 100644 --- a/web/package.json +++ b/web/package.json @@ -1,46 +1,59 @@ { "name": "web", - "version": "0.0.0", + "version": "0.1.0", + "private": true, "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview", - "lint": "eslint . --fix --ignore-path .gitignore" + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "next lint", + "prepare": "cd .. && husky web/.husky", + "lint-staged": "lint-staged" }, "dependencies": { - "@koumoul/vjsf": "^3.0.0-beta.46", - "@mdi/font": "7.4.47", - "ajv": "^8.17.1", - "ajv-dist": "^8.17.1", - "ajv-errors": "^3.0.0", - "ajv-formats": "^3.0.1", - "ajv-i18n": "^4.2.0", - "ansi_up": "^6.0.2", - "axios": "^1.7.7", - "codemirror": "^5.65.18", - "core-js": "^3.37.1", - "json-editor-vue": "^0.17.3", - "roboto-fontface": "*", - "vue": "^3.4.31", - "vuedraggable": "^4.1.0", - "vuetify": "^3.6.11", - "vuex": "^4.0.2" + "@hookform/resolvers": "^5.0.1", + "@radix-ui/react-checkbox": "^1.3.1", + "@radix-ui/react-dialog": "^1.1.13", + "@radix-ui/react-label": "^2.1.6", + "@radix-ui/react-select": "^2.2.4", + "@radix-ui/react-slot": "^1.2.2", + "@radix-ui/react-switch": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.11", + "@radix-ui/react-toggle": "^1.1.8", + "@radix-ui/react-toggle-group": "^1.1.9", + "@tailwindcss/postcss": "^4.1.5", + "axios": "^1.8.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lodash": "^4.17.21", + "lucide-react": "^0.507.0", + "next": "15.2.4", + "next-themes": "^0.4.6", + "postcss": "^8.5.3", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-hook-form": "^7.56.3", + "sonner": "^2.0.3", + "tailwind-merge": "^3.2.0", + "tailwindcss": "^4.1.5", + "uuidjs": "^5.1.0", + "zod": "^3.24.4" }, "devDependencies": { - "@vitejs/plugin-vue": "^5.0.5", - "eslint": "^8.57.0", - "eslint-config-standard": "^17.1.0", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-n": "^16.6.2", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^6.4.0", - "eslint-plugin-vue": "^9.27.0", - "sass": "1.77.6", - "unplugin-fonts": "^1.1.1", - "unplugin-vue-components": "^0.27.2", - "unplugin-vue-router": "^0.10.0", - "vite": "^5.3.3", - "vite-plugin-vuetify": "^2.0.3", - "vue-router": "^4.4.0" + "@eslint/eslintrc": "^3", + "@types/lodash": "^4.17.16", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "15.2.4", + "eslint-config-prettier": "^10.1.2", + "eslint-plugin-prettier": "^5.2.6", + "husky": "^9.1.7", + "lint-staged": "^15.5.1", + "prettier": "^3.5.3", + "tw-animate-css": "^1.2.9", + "typescript": "^5.8.3", + "typescript-eslint": "^8.31.1" } } diff --git a/web/postcss.config.mjs b/web/postcss.config.mjs new file mode 100644 index 00000000..bc9198c3 --- /dev/null +++ b/web/postcss.config.mjs @@ -0,0 +1,6 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, + }; + export default config; \ No newline at end of file diff --git a/web/public/file.svg b/web/public/file.svg new file mode 100644 index 00000000..004145cd --- /dev/null +++ b/web/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/globe.svg b/web/public/globe.svg new file mode 100644 index 00000000..567f17b0 --- /dev/null +++ b/web/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/next.svg b/web/public/next.svg new file mode 100644 index 00000000..5174b28c --- /dev/null +++ b/web/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/vercel.svg b/web/public/vercel.svg new file mode 100644 index 00000000..77053960 --- /dev/null +++ b/web/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/window.svg b/web/public/window.svg new file mode 100644 index 00000000..b2b2a44f --- /dev/null +++ b/web/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/App.vue b/web/src/App.vue deleted file mode 100644 index 926ad3a7..00000000 --- a/web/src/App.vue +++ /dev/null @@ -1,329 +0,0 @@ - - - - {{ text }} - - - - - - - - - - - - {{ proxy.$store.state.version }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 关于 LangBot - - - - - - - - - - - - 查看文档 - - - - - - 重载消息平台 - - - - - - 重载插件 - - - - - - 重载 LLM 管理器 - - - - - - - - - - - - - - - - - - - - 正在加载... - - - - - - - - - - - - - - - - diff --git a/web/src/app/assets/langbot-logo.webp b/web/src/app/assets/langbot-logo.webp new file mode 100644 index 00000000..09226adc Binary files /dev/null and b/web/src/app/assets/langbot-logo.webp differ diff --git a/web/public/favicon.ico b/web/src/app/favicon.ico similarity index 100% rename from web/public/favicon.ico rename to web/src/app/favicon.ico diff --git a/web/src/app/global.css b/web/src/app/global.css new file mode 100644 index 00000000..d030c747 --- /dev/null +++ b/web/src/app/global.css @@ -0,0 +1,120 @@ + +:root { + /* 适用于 Firefox 的滚动条 */ + scrollbar-color: rgba(0, 0, 0, 0.2) transparent; /* 滑块颜色 + 轨道颜色 */ + scrollbar-width: thin; /* auto | thin | none */ --radius: 0.625rem; --background: oklch(1 0 0); --foreground: oklch(0.141 0.005 285.823); --card: oklch(1 0 0); --card-foreground: oklch(0.141 0.005 285.823); --popover: oklch(1 0 0); --popover-foreground: oklch(0.141 0.005 285.823); --primary: oklch(0.21 0.006 285.885); --primary-foreground: oklch(0.985 0 0); --secondary: oklch(0.967 0.001 286.375); --secondary-foreground: oklch(0.21 0.006 285.885); --muted: oklch(0.967 0.001 286.375); --muted-foreground: oklch(0.552 0.016 285.938); --accent: oklch(0.967 0.001 286.375); --accent-foreground: oklch(0.21 0.006 285.885); --destructive: oklch(0.577 0.245 27.325); --border: oklch(0.92 0.004 286.32); --input: oklch(0.92 0.004 286.32); --ring: oklch(0.705 0.015 286.067); --chart-1: oklch(0.646 0.222 41.116); --chart-2: oklch(0.6 0.118 184.704); --chart-3: oklch(0.398 0.07 227.392); --chart-4: oklch(0.828 0.189 84.429); --chart-5: oklch(0.769 0.188 70.08); --sidebar: oklch(0.985 0 0); --sidebar-foreground: oklch(0.141 0.005 285.823); --sidebar-primary: oklch(0.21 0.006 285.885); --sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-accent: oklch(0.967 0.001 286.375); --sidebar-accent-foreground: oklch(0.21 0.006 285.885); --sidebar-border: oklch(0.92 0.004 286.32); --sidebar-ring: oklch(0.705 0.015 286.067); +} + +/* WebKit 内核浏览器定制 */ +::-webkit-scrollbar { + width: 6px; /* 垂直滚动条宽度 */ + height: 6px; /* 水平滚动条高度 */ +} + +::-webkit-scrollbar-track { + background: transparent; /* 隐藏轨道背景 */ +} + +::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.2); /* 半透明黑色 */ + border-radius: 3px; + transition: background 0.3s; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.35); /* 悬停加深 */ +} + +/* 兼容 Edge */ +@supports (-ms-ime-align:auto) { + body { + -ms-overflow-style: -ms-autohiding-scrollbar; /* 自动隐藏滚动条 */ + } +} + +@import "tailwindcss"; + +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +.dark { + --background: oklch(0.141 0.005 285.823); + --foreground: oklch(0.985 0 0); + --card: oklch(0.21 0.006 285.885); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.21 0.006 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); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.274 0.006 286.033); + --muted-foreground: oklch(0.705 0.015 286.067); + --accent: oklch(0.274 0.006 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%); + --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-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-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.552 0.016 285.938); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/web/src/app/home/bots/ICreateBotField.ts b/web/src/app/home/bots/ICreateBotField.ts new file mode 100644 index 00000000..e69de29b diff --git a/web/src/app/home/bots/botConfig.module.css b/web/src/app/home/bots/botConfig.module.css new file mode 100644 index 00000000..f65512dd --- /dev/null +++ b/web/src/app/home/bots/botConfig.module.css @@ -0,0 +1,10 @@ +.botListContainer { + width: 100%; + padding-left: 0.8rem; + padding-right: 0.8rem; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(24rem, 1fr)); + gap: 2rem; + justify-items: stretch; + align-items: start; +} \ No newline at end of file diff --git a/web/src/app/home/bots/components/bot-card/BotCard.tsx b/web/src/app/home/bots/components/bot-card/BotCard.tsx new file mode 100644 index 00000000..924032c9 --- /dev/null +++ b/web/src/app/home/bots/components/bot-card/BotCard.tsx @@ -0,0 +1,53 @@ +import { BotCardVO } from '@/app/home/bots/components/bot-card/BotCardVO'; +import styles from './botCard.module.css'; + +export default function BotCard({ botCardVO }: { botCardVO: BotCardVO }) { + return ( + + + + + + + {botCardVO.name} + + {botCardVO.description} + + + + + + + + + {botCardVO.adapterLabel} + + + + + + + + + {botCardVO.usePipelineName} + + + + + + ); +} diff --git a/web/src/app/home/bots/components/bot-card/BotCardVO.ts b/web/src/app/home/bots/components/bot-card/BotCardVO.ts new file mode 100644 index 00000000..c0f7c19e --- /dev/null +++ b/web/src/app/home/bots/components/bot-card/BotCardVO.ts @@ -0,0 +1,26 @@ +export interface IBotCardVO { + id: string; + iconURL: string; + name: string; + description: string; + adapterLabel: string; + usePipelineName: string; +} + +export class BotCardVO implements IBotCardVO { + id: string; + iconURL: string; + name: string; + description: string; + adapterLabel: string; + usePipelineName: string; + + constructor(props: IBotCardVO) { + this.id = props.id; + this.iconURL = props.iconURL; + this.name = props.name; + this.description = props.description; + this.adapterLabel = props.adapterLabel; + this.usePipelineName = props.usePipelineName; + } +} 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 new file mode 100644 index 00000000..f8dc9305 --- /dev/null +++ b/web/src/app/home/bots/components/bot-card/botCard.module.css @@ -0,0 +1,90 @@ +.cardContainer { + width: 100%; + height: 10rem; + background-color: #fff; + border-radius: 10px; + box-shadow: 0px 2px 2px 0 rgba(0, 0, 0, 0.2); + padding: 1.2rem; + cursor: pointer; +} + +.cardContainer:hover { + box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1); +} + +.iconBasicInfoContainer { + width: 100%; + height: 100%; + display: flex; + flex-direction: row; + gap: 0.8rem; + user-select: none; + /* background-color: aqua; */ +} + +.iconImage { + width: 4rem; + height: 4rem; + margin: 0.2rem; + /* border-radius: 50%; */ +} + +.basicInfoContainer { + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.basicInfoNameContainer { + display: flex; + flex-direction: column; +} + +.basicInfoName { + font-size: 1.4rem; + font-weight: 500; +} + +.basicInfoDescription { + font-size: 1rem; + font-weight: 300; + color: #b1b1b1; +} + +.basicInfoAdapterContainer { + display: flex; + flex-direction: row; + gap: 0.4rem; +} + +.basicInfoAdapterIcon { + width: 1.2rem; + height: 1.2rem; + margin-top: 0.2rem; + color: #626262; +} + +.basicInfoAdapterLabel { + font-size: 1.2rem; + font-weight: 500; + color: #626262; +} + +.basicInfoPipelineContainer { + display: flex; + flex-direction: row; + gap: 0.4rem; +} + +.basicInfoPipelineIcon { + width: 1.2rem; + height: 1.2rem; + color: #626262; + margin-top: 0.2rem; +} + +.basicInfoPipelineLabel { + font-size: 1.2rem; + font-weight: 500; + color: #626262; +} \ No newline at end of file diff --git a/web/src/app/home/bots/components/bot-form/BotForm.tsx b/web/src/app/home/bots/components/bot-form/BotForm.tsx new file mode 100644 index 00000000..e2e12778 --- /dev/null +++ b/web/src/app/home/bots/components/bot-form/BotForm.tsx @@ -0,0 +1,543 @@ +import { useEffect, useState } from 'react'; +import { + IChooseAdapterEntity, + IPipelineEntity, +} from '@/app/home/bots/components/bot-form/ChooseEntity'; +import { + DynamicFormItemConfig, + getDefaultValues, + parseDynamicFormItemType, +} from '@/app/home/components/dynamic-form/DynamicFormItemConfig'; +import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic'; +import { UUID } from 'uuidjs'; +import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent'; +import { httpClient } from '@/app/infra/http/HttpClient'; +import { Bot } from '@/app/infra/entities/api'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import { toast } from 'sonner'; + +import { + Dialog, + DialogContent, + DialogDescription, + 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 { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Switch } from '@/components/ui/switch'; + +const formSchema = z.object({ + name: z.string().min(1, { message: '机器人名称不能为空' }), + description: z.string().min(1, { message: '机器人描述不能为空' }), + adapter: z.string().min(1, { message: '适配器不能为空' }), + adapter_config: z.record(z.string(), z.any()), + enable: z.boolean().optional(), + use_pipeline_uuid: z.string().optional(), +}); + +export default function BotForm({ + initBotId, + onFormSubmit, + onFormCancel, + onBotDeleted, +}: { + initBotId?: string; + onFormSubmit: (value: z.infer) => void; + onFormCancel: () => void; + onBotDeleted: () => void; +}) { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: '', + description: '一个机器人', + adapter: '', + adapter_config: {}, + enable: true, + use_pipeline_uuid: '', + }, + }); + + const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false); + + const [adapterNameToDynamicConfigMap, setAdapterNameToDynamicConfigMap] = + useState(new Map()); + // const [form] = Form.useForm(); + const [showDynamicForm, setShowDynamicForm] = useState(false); + // const [dynamicForm] = Form.useForm(); + const [adapterNameList, setAdapterNameList] = useState< + IChooseAdapterEntity[] + >([]); + const [adapterIconList, setAdapterIconList] = useState< + Record + >({}); + const [adapterDescriptionList, setAdapterDescriptionList] = useState< + Record + >({}); + + const [pipelineNameList, setPipelineNameList] = useState( + [], + ); + + const [dynamicFormConfigList, setDynamicFormConfigList] = useState< + IDynamicFormItemSchema[] + >([]); + const [, setIsLoading] = useState(false); + + useEffect(() => { + initBotFormComponent().then(() => { + // 拉取初始化表单信息 + if (initBotId) { + getBotConfig(initBotId) + .then((val) => { + form.setValue('name', val.name); + form.setValue('description', val.description); + form.setValue('adapter', val.adapter); + form.setValue('adapter_config', val.adapter_config); + form.setValue('enable', val.enable); + form.setValue('use_pipeline_uuid', val.use_pipeline_uuid || ''); + console.log('form', form.getValues()); + handleAdapterSelect(val.adapter); + // dynamicForm.setFieldsValue(val.adapter_config); + }) + .catch((err) => { + toast.error('获取机器人配置失败:' + err.message); + }); + } else { + form.reset(); + } + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + async function initBotFormComponent() { + // 初始化流水线列表 + const pipelinesRes = await httpClient.getPipelines(); + console.log('rawPipelineList', pipelinesRes); + setPipelineNameList( + pipelinesRes.pipelines.map((item) => { + return { + label: item.name, + value: item.uuid ?? '', + }; + }), + ); + + // 拉取adapter + const adaptersRes = await httpClient.getAdapters(); + console.log('rawAdapterList', adaptersRes); + setAdapterNameList( + adaptersRes.adapters.map((item) => { + return { + label: item.label.zh_CN, + value: item.name, + }; + }), + ); + + // 初始化适配器图标列表 + setAdapterIconList( + adaptersRes.adapters.reduce( + (acc, item) => { + acc[item.name] = httpClient.getAdapterIconURL(item.name); + return acc; + }, + {} as Record, + ), + ); + + // 初始化适配器描述列表 + setAdapterDescriptionList( + adaptersRes.adapters.reduce( + (acc, item) => { + acc[item.name] = item.description.zh_CN; + return acc; + }, + {} as Record, + ), + ); + + // 初始化适配器表单map + adaptersRes.adapters.forEach((rawAdapter) => { + adapterNameToDynamicConfigMap.set( + rawAdapter.name, + rawAdapter.spec.config.map( + (item) => + new DynamicFormItemConfig({ + default: item.default, + id: UUID.generate(), + label: item.label, + name: item.name, + required: item.required, + type: parseDynamicFormItemType(item.type), + }), + ), + ); + }); + setAdapterNameToDynamicConfigMap(adapterNameToDynamicConfigMap); + } + async function getBotConfig( + botId: string, + ): Promise> { + return new Promise((resolve, reject) => { + httpClient + .getBot(botId) + .then((res) => { + const bot = res.bot; + resolve({ + adapter: bot.adapter, + description: bot.description, + name: bot.name, + adapter_config: bot.adapter_config, + enable: bot.enable ?? true, + use_pipeline_uuid: bot.use_pipeline_uuid ?? '', + }); + }) + .catch((err) => { + reject(err); + }); + }); + } + + function handleAdapterSelect(adapterName: string) { + if (adapterName) { + const dynamicFormConfigList = + adapterNameToDynamicConfigMap.get(adapterName); + if (dynamicFormConfigList) { + setDynamicFormConfigList(dynamicFormConfigList); + if (!initBotId) { + form.setValue( + 'adapter_config', + getDefaultValues(dynamicFormConfigList), + ); + } + } + setShowDynamicForm(true); + } else { + setShowDynamicForm(false); + } + } + + // 只有通过外层固定表单验证才会走到这里,真正的提交逻辑在这里 + function onDynamicFormSubmit(value: object) { + setIsLoading(true); + console.log('set loading', true); + if (initBotId) { + // 编辑提交 + // console.log('submit edit', form.getFieldsValue(), value); + const updateBot: Bot = { + uuid: initBotId, + name: form.getValues().name, + description: form.getValues().description, + adapter: form.getValues().adapter, + adapter_config: form.getValues().adapter_config, + enable: form.getValues().enable, + use_pipeline_uuid: form.getValues().use_pipeline_uuid, + }; + httpClient + .updateBot(initBotId, updateBot) + .then((res) => { + console.log('update bot success', res); + onFormSubmit(form.getValues()); + toast.success('保存成功'); + }) + .catch((err) => { + toast.error('保存失败:' + err.message); + }) + .finally(() => { + setIsLoading(false); + form.reset(); + // dynamicForm.resetFields(); + }); + } else { + // 创建提交 + console.log('submit create', form.getValues(), value); + const newBot: Bot = { + name: form.getValues().name, + description: form.getValues().description, + adapter: form.getValues().adapter, + adapter_config: form.getValues().adapter_config, + }; + httpClient + .createBot(newBot) + .then((res) => { + console.log(res); + onFormSubmit(form.getValues()); + toast.success('创建成功'); + }) + .catch((err) => { + toast.error('创建失败:' + err.message); + }) + .finally(() => { + setIsLoading(false); + form.reset(); + // dynamicForm.resetFields(); + }); + } + setShowDynamicForm(false); + console.log('set loading', false); + } + + function deleteBot() { + if (initBotId) { + httpClient + .deleteBot(initBotId) + .then(() => { + onBotDeleted(); + toast.success('删除成功'); + }) + .catch((err) => { + toast.error('删除失败:' + err.message); + }); + } + } + + return ( + + + + + 删除确认 + + 你确定要删除这个机器人吗? + + setShowDeleteConfirmModal(false)} + > + 取消 + + { + deleteBot(); + setShowDeleteConfirmModal(false); + }} + > + 确认删除 + + + + + + + + + {/* 是否启用 & 绑定流水线 仅在编辑模式 */} + {initBotId && ( + + ( + + 是否启用 + + + + + )} + /> + + ( + + 绑定流水线 + + + + + + + + {pipelineNameList.map((item) => ( + + {item.label} + + ))} + + + + + + )} + /> + + )} + + ( + + + 机器人名称* + + + + + + + )} + /> + ( + + + 机器人描述* + + + + + + + )} + /> + + ( + + + 平台/适配器选择* + + + + { + field.onChange(value); + handleAdapterSelect(value); + }} + value={field.value} + > + + + + + + {adapterNameList.map((item) => ( + + {item.label} + + ))} + + + + + + + + )} + /> + + {form.watch('adapter') && ( + + + + + { + adapterNameList.find( + (item) => item.value === form.watch('adapter'), + )?.label + } + + + {adapterDescriptionList[form.watch('adapter')]} + + + + )} + + {showDynamicForm && dynamicFormConfigList.length > 0 && ( + + 适配器配置 + { + form.setValue('adapter_config', values); + }} + /> + + )} + + + + + {!initBotId && ( + + 提交 + + )} + {initBotId && ( + <> + setShowDeleteConfirmModal(true)} + > + 删除 + + + 保存 + + > + )} + onFormCancel()} + > + 取消 + + + + + + + ); +} diff --git a/web/src/app/home/bots/components/bot-form/ChooseEntity.ts b/web/src/app/home/bots/components/bot-form/ChooseEntity.ts new file mode 100644 index 00000000..492b4226 --- /dev/null +++ b/web/src/app/home/bots/components/bot-form/ChooseEntity.ts @@ -0,0 +1,9 @@ +export interface IChooseAdapterEntity { + label: string; + value: string; +} + +export interface IPipelineEntity { + label: string; + value: string; +} diff --git a/web/src/app/home/bots/page.tsx b/web/src/app/home/bots/page.tsx new file mode 100644 index 00000000..1b9409c4 --- /dev/null +++ b/web/src/app/home/bots/page.tsx @@ -0,0 +1,129 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import styles from './botConfig.module.css'; +import { BotCardVO } from '@/app/home/bots/components/bot-card/BotCardVO'; +import BotForm from '@/app/home/bots/components/bot-form/BotForm'; +import BotCard from '@/app/home/bots/components/bot-card/BotCard'; +import CreateCardComponent from '@/app/infra/basic-component/create-card-component/CreateCardComponent'; +import { httpClient } from '@/app/infra/http/HttpClient'; +import { Bot, Adapter } from '@/app/infra/entities/api'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { toast } from 'sonner'; +export default function BotConfigPage() { + const [modalOpen, setModalOpen] = useState(false); + const [botList, setBotList] = useState([]); + const [isEditForm, setIsEditForm] = useState(false); + const [nowSelectedBotCard, setNowSelectedBotCard] = useState(); + + useEffect(() => { + getBotList(); + }, []); + + async function getBotList() { + const adapterListResp = await httpClient.getAdapters(); + const adapterList = adapterListResp.adapters.map((adapter: Adapter) => { + return { + label: adapter.label.zh_CN, + value: adapter.name, + }; + }); + + httpClient + .getBots() + .then((resp) => { + const botList: BotCardVO[] = resp.bots.map((bot: Bot) => { + return new BotCardVO({ + id: bot.uuid || '', + iconURL: httpClient.getAdapterIconURL(bot.adapter), + name: bot.name, + description: bot.description, + adapterLabel: + adapterList.find((item) => item.value === bot.adapter)?.label || + bot.adapter.substring(0, 10), + usePipelineName: bot.use_pipeline_name || '', + }); + }); + setBotList(botList); + }) + .catch((err) => { + console.error('get bot list error', err); + toast.error('获取机器人列表失败:' + err.message); + }) + .finally(() => { + // setIsLoading(false); + }); + } + + function handleCreateBotClick() { + setIsEditForm(false); + setNowSelectedCard(undefined); + setModalOpen(true); + } + + function setNowSelectedCard(cardVO: BotCardVO | undefined) { + setNowSelectedBotCard(cardVO); + } + + function selectBot(cardVO: BotCardVO) { + setIsEditForm(true); + setNowSelectedCard(cardVO); + console.log('set now vo', cardVO); + setModalOpen(true); + } + + return ( + + + + + + {isEditForm ? '编辑机器人' : '创建机器人'} + + + + { + getBotList(); + setModalOpen(false); + }} + onFormCancel={() => setModalOpen(false)} + onBotDeleted={() => { + getBotList(); + setModalOpen(false); + }} + /> + + + + + {/* 注意:其余的返回内容需要保持在Spin组件外部 */} + + + {botList.map((cardVO) => { + return ( + { + selectBot(cardVO); + }} + > + + + ); + })} + + + ); +} diff --git a/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx b/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx new file mode 100644 index 00000000..465f2ae1 --- /dev/null +++ b/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx @@ -0,0 +1,163 @@ +import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import DynamicFormItemComponent from '@/app/home/components/dynamic-form/DynamicFormItemComponent'; +import { useEffect } from 'react'; + +export default function DynamicFormComponent({ + itemConfigList, + onSubmit, + initialValues, +}: { + itemConfigList: IDynamicFormItemSchema[]; + onSubmit?: (val: object) => unknown; + initialValues?: Record; +}) { + // 根据 itemConfigList 动态生成 zod schema + const formSchema = z.object( + itemConfigList.reduce( + (acc, item) => { + let fieldSchema; + switch (item.type) { + case 'integer': + fieldSchema = z.number(); + break; + case 'float': + fieldSchema = z.number(); + break; + case 'boolean': + fieldSchema = z.boolean(); + break; + case 'string': + fieldSchema = z.string(); + break; + case 'array[string]': + fieldSchema = z.array(z.string()); + break; + case 'select': + fieldSchema = z.string(); + break; + case 'llm-model-selector': + fieldSchema = z.string(); + break; + case 'prompt-editor': + fieldSchema = z.array( + z.object({ + content: z.string(), + role: z.string(), + }), + ); + break; + default: + fieldSchema = z.string(); + } + + if ( + item.required && + (fieldSchema instanceof z.ZodString || + fieldSchema instanceof z.ZodArray) + ) { + fieldSchema = fieldSchema.min(1, { message: '此字段为必填项' }); + } + + return { + ...acc, + [item.name]: fieldSchema, + }; + }, + {} as Record, + ), + ); + + type FormValues = z.infer; + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: itemConfigList.reduce((acc, item) => { + // 优先使用 initialValues,如果没有则使用默认值 + const value = initialValues?.[item.name] ?? item.default; + return { + ...acc, + [item.name]: value, + }; + }, {} as FormValues), + }); + + // 当 initialValues 变化时更新表单值 + useEffect(() => { + console.log('initialValues', initialValues); + if (initialValues) { + // 合并默认值和初始值 + const mergedValues = itemConfigList.reduce( + (acc, item) => { + acc[item.name] = initialValues[item.name] ?? item.default; + return acc; + }, + {} as Record, + ); + + Object.entries(mergedValues).forEach(([key, value]) => { + form.setValue(key as keyof FormValues, value); + }); + } + }, [initialValues, form, itemConfigList]); + + // 监听表单值变化 + useEffect(() => { + const subscription = form.watch(() => { + // 获取完整的表单值,确保包含所有默认值 + const formValues = form.getValues(); + console.log('formValues', formValues); + const finalValues = itemConfigList.reduce( + (acc, item) => { + acc[item.name] = formValues[item.name] ?? item.default; + return acc; + }, + {} as Record, + ); + console.log('finalValues', finalValues); + onSubmit?.(finalValues); + }); + return () => subscription.unsubscribe(); + }, [form, onSubmit, itemConfigList]); + + return ( + + + {itemConfigList.map((config) => ( + ( + + + {config.label.zh_CN}{' '} + {config.required && *} + + + + + {config.description && ( + + {config.description.zh_CN} + + )} + + + )} + /> + ))} + + + ); +} diff --git a/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx b/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx new file mode 100644 index 00000000..03412dc6 --- /dev/null +++ b/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx @@ -0,0 +1,230 @@ +import { + DynamicFormItemType, + IDynamicFormItemSchema, +} from '@/app/infra/entities/form/dynamic'; +import { Input } from '@/components/ui/input'; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Switch } from '@/components/ui/switch'; +import { ControllerRenderProps } from 'react-hook-form'; +import { Button } from '@/components/ui/button'; +import { useEffect, useState } from 'react'; +import { httpClient } from '@/app/infra/http/HttpClient'; +import { LLMModel } from '@/app/infra/entities/api'; +import { toast } from 'sonner'; + +export default function DynamicFormItemComponent({ + config, + field, +}: { + config: IDynamicFormItemSchema; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + field: ControllerRenderProps; +}) { + const [llmModels, setLlmModels] = useState([]); + + useEffect(() => { + if (config.type === DynamicFormItemType.LLM_MODEL_SELECTOR) { + httpClient + .getProviderLLMModels() + .then((resp) => { + setLlmModels(resp.models); + }) + .catch((err) => { + toast.error('获取 LLM 模型列表失败:' + err.message); + }); + } + }, [config.type]); + + switch (config.type) { + case DynamicFormItemType.INT: + case DynamicFormItemType.FLOAT: + return ( + field.onChange(Number(e.target.value))} + /> + ); + + case DynamicFormItemType.STRING: + return ; + + case DynamicFormItemType.BOOLEAN: + return ; + + case DynamicFormItemType.STRING_ARRAY: + return ( + + {field.value.map((item: string, index: number) => ( + + { + const newValue = [...field.value]; + newValue[index] = e.target.value; + field.onChange(newValue); + }} + /> + { + const newValue = field.value.filter( + (_: string, i: number) => i !== index, + ); + field.onChange(newValue); + }} + > + + + + + + ))} + { + field.onChange([...field.value, '']); + }} + > + 添加 + + + ); + + case DynamicFormItemType.SELECT: + return ( + + + + + + + {config.options?.map((option) => ( + + {option.label.zh_CN} + + ))} + + + + ); + + case DynamicFormItemType.LLM_MODEL_SELECTOR: + return ( + + + + + + + {llmModels.map((model) => ( + + {model.name} + + ))} + + + + ); + + case DynamicFormItemType.PROMPT_EDITOR: + return ( + + {field.value.map( + (item: { role: string; content: string }, index: number) => ( + + {/* 角色选择 */} + {index === 0 ? ( + + system + + ) : ( + { + const newValue = [...field.value]; + newValue[index] = { ...newValue[index], role: value }; + field.onChange(newValue); + }} + > + + + + + + user + assistant + + + + )} + {/* 内容输入 */} + { + const newValue = [...field.value]; + newValue[index] = { + ...newValue[index], + content: e.target.value, + }; + field.onChange(newValue); + }} + /> + {/* 删除按钮,第一轮不显示 */} + {index !== 0 && ( + { + const newValue = field.value.filter( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (_: any, i: number) => i !== index, + ); + field.onChange(newValue); + }} + > + + + + + )} + + ), + )} + { + field.onChange([...field.value, { role: 'user', content: '' }]); + }} + > + 添加回合 + + + ); + + default: + return ; + } +} diff --git a/web/src/app/home/components/dynamic-form/DynamicFormItemConfig.ts b/web/src/app/home/components/dynamic-form/DynamicFormItemConfig.ts new file mode 100644 index 00000000..74fd4a0b --- /dev/null +++ b/web/src/app/home/components/dynamic-form/DynamicFormItemConfig.ts @@ -0,0 +1,54 @@ +import { + IDynamicFormItemSchema, + DynamicFormItemType, + IDynamicFormItemOption, +} from '@/app/infra/entities/form/dynamic'; +import { I18nLabel } from '@/app/infra/entities/common'; + +export class DynamicFormItemConfig implements IDynamicFormItemSchema { + id: string; + name: string; + default: string | number | boolean | Array; + label: I18nLabel; + required: boolean; + type: DynamicFormItemType; + description?: I18nLabel; + options?: IDynamicFormItemOption[]; + + constructor(params: IDynamicFormItemSchema) { + this.id = params.id; + this.name = params.name; + this.default = params.default; + this.label = params.label; + this.required = params.required; + this.type = params.type; + this.description = params.description; + this.options = params.options; + } +} + +export function isDynamicFormItemType( + value: string, +): value is DynamicFormItemType { + return Object.values(DynamicFormItemType).includes( + value as DynamicFormItemType, + ); +} + +export function parseDynamicFormItemType(value: string): DynamicFormItemType { + return isDynamicFormItemType(value) ? value : DynamicFormItemType.UNKNOWN; +} + +export function getDefaultValues( + itemConfigList: IDynamicFormItemSchema[], + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): Record { + return itemConfigList.reduce( + (acc, item) => { + acc[item.name] = item.default; + return acc; + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as Record, + ); +} diff --git a/web/src/app/home/components/dynamic-form/testDynamicConfigList.ts b/web/src/app/home/components/dynamic-form/testDynamicConfigList.ts new file mode 100644 index 00000000..ca19b8b2 --- /dev/null +++ b/web/src/app/home/components/dynamic-form/testDynamicConfigList.ts @@ -0,0 +1,41 @@ +import { + DynamicFormItemType, + IDynamicFormItemSchema, +} from '@/app/infra/entities/form/dynamic'; +import { DynamicFormItemConfig } from '@/app/home/components/dynamic-form/DynamicFormItemConfig'; + +export const testDynamicConfigList: IDynamicFormItemSchema[] = [ + new DynamicFormItemConfig({ + default: '', + id: '111', + label: { + zh_CN: '测试字段string', + en_US: 'eng test', + }, + name: 'string_test', + required: false, + type: DynamicFormItemType.STRING, + }), + new DynamicFormItemConfig({ + default: '', + id: '222', + label: { + zh_CN: '测试字段int', + en_US: 'int eng test', + }, + name: 'int_test', + required: true, + type: DynamicFormItemType.INT, + }), + new DynamicFormItemConfig({ + default: '', + id: '333', + label: { + zh_CN: '测试字段boolean', + en_US: 'boolean eng test', + }, + name: 'boolean_test', + required: false, + type: DynamicFormItemType.BOOLEAN, + }), +]; 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 new file mode 100644 index 00000000..b55b4cee --- /dev/null +++ b/web/src/app/home/components/empty-and-create-component/EmptyAndCreateComponent.tsx @@ -0,0 +1,27 @@ +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 new file mode 100644 index 00000000..ba3d041f --- /dev/null +++ b/web/src/app/home/components/empty-and-create-component/emptyAndCreate.module.css @@ -0,0 +1,54 @@ +.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; +} \ No newline at end of file diff --git a/web/src/app/home/components/home-sidebar/HomeSidebar.module.css b/web/src/app/home/components/home-sidebar/HomeSidebar.module.css new file mode 100644 index 00000000..7f08d649 --- /dev/null +++ b/web/src/app/home/components/home-sidebar/HomeSidebar.module.css @@ -0,0 +1,106 @@ +.sidebarContainer { + box-sizing: border-box; + width: 200px; + height: 100vh; + background-color: #eee; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: space-between; + padding-block: 1rem; + user-select: none; + /* box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.1); */ +} + +.langbotIconContainer { + width: 200px; + height: 70px; + display: flex; + flex-direction: row; + 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; + } + + .langbotTextContainer { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + gap: 0.1rem; + } + + .langbotText { + font-size: 1.4rem; + font-weight: 500; + } + + .langbotVersion { + font-size: 0.8rem; + font-weight: 700; + color: #6C6C6C; + } +} + +.sidebarTopContainer { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.5rem; +} + + +.sidebarChildContainer { + width: 10rem; + height: 3rem; + margin: 0.8rem 0; + padding-left: 2rem; + font-size: 1rem; + border-radius: 12px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + cursor: pointer; + gap: 0.5rem; +} + +.sidebarSelected { + background-color: #2288ee; + color: white; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); +} + +.sidebarUnselected { + color: #6C6C6C; +} + +.sidebarChildIcon { + width: 20px; + height: 20px; + background-color: rgba(96, 149, 209, 0); +} + +.sidebarBottomContainer { + width: 100%; + height: 100px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.sidebarBottomChildContainer { + width: 100%; + height: 50px; + display: flex; + flex-direction: row; +} \ No newline at end of file diff --git a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx new file mode 100644 index 00000000..848813c0 --- /dev/null +++ b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx @@ -0,0 +1,157 @@ +'use client'; + +import styles from './HomeSidebar.module.css'; +import { useEffect, useState } from 'react'; +import { + SidebarChild, + SidebarChildVO, +} from '@/app/home/components/home-sidebar/HomeSidebarChild'; +import { useRouter, usePathname } from 'next/navigation'; +import { sidebarConfigList } from '@/app/home/components/home-sidebar/sidbarConfigList'; +import langbotIcon from '@/app/assets/langbot-logo.webp'; + +// TODO 侧边导航栏要加动画 +export default function HomeSidebar({ + onSelectedChangeAction, +}: { + onSelectedChangeAction: (sidebarChild: SidebarChildVO) => void; +}) { + // 路由相关 + const router = useRouter(); + const pathname = usePathname(); + // 路由被动变化时处理 + useEffect(() => { + handleRouteChange(pathname); + }, [pathname]); + + const [selectedChild, setSelectedChild] = useState(); + + useEffect(() => { + console.log('HomeSidebar挂载完成'); + initSelect(); + return () => console.log('HomeSidebar卸载'); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + function handleChildClick(child: SidebarChildVO) { + setSelectedChild(child); + handleRoute(child); + onSelectedChangeAction(child); + } + + function initSelect() { + // 根据当前路径选择对应的菜单项 + const currentPath = pathname; + const matchedChild = sidebarConfigList.find( + (childConfig) => childConfig.route === currentPath, + ); + if (matchedChild) { + handleChildClick(matchedChild); + } else { + // 如果没有匹配的路径,则默认选择第一个 + handleChildClick(sidebarConfigList[0]); + } + } + + function handleRoute(child: SidebarChildVO) { + console.log(child); + router.push(`${child.route}`); + } + + function handleRouteChange(pathname: string) { + // TODO 这段逻辑并不好,未来router封装好后改掉 + // 判断在home下,并且路由更改的是自己的路由子组件则更新UI + const routeList = pathname.split('/'); + if ( + routeList[1] === 'home' && + sidebarConfigList.find((childConfig) => childConfig.route === pathname) + ) { + console.log('find success'); + const routeSelectChild = sidebarConfigList.find( + (childConfig) => childConfig.route === pathname, + ); + if (routeSelectChild) { + setSelectedChild(routeSelectChild); + } + } + } + + return ( + + + {/* LangBot、ICON区域 */} + + {/* icon */} + + {/* 文字 */} + + LangBot + v4.0.0 + + + {/* 菜单列表,后期可升级成配置驱动 */} + + {sidebarConfigList.map((config) => { + return ( + { + console.log('click:', config.id); + handleChildClick(config); + }} + > + {}} + isSelected={ + selectedChild !== undefined && + selectedChild.id === config.id + } + icon={config.icon} + name={config.name} + /> + + ); + })} + + + + + {}} + isSelected={false} + icon={ + + + + } + name="系统设置" + /> + { + // open docs.langbot.app + window.open('https://docs.langbot.app', '_blank'); + }} + isSelected={false} + icon={ + + + + } + name="帮助文档" + /> + + + ); +} diff --git a/web/src/app/home/components/home-sidebar/HomeSidebarChild.tsx b/web/src/app/home/components/home-sidebar/HomeSidebarChild.tsx new file mode 100644 index 00000000..f0ae353b --- /dev/null +++ b/web/src/app/home/components/home-sidebar/HomeSidebarChild.tsx @@ -0,0 +1,44 @@ +import styles from './HomeSidebar.module.css'; + +export interface ISidebarChildVO { + id: string; + icon: React.ReactNode; + name: string; + route: string; +} + +export class SidebarChildVO { + id: string; + icon: React.ReactNode; + name: string; + route: string; + + constructor(props: ISidebarChildVO) { + this.id = props.id; + this.icon = props.icon; + this.name = props.name; + this.route = props.route; + } +} + +export function SidebarChild({ + icon, + name, + isSelected, + onClick, +}: { + icon: React.ReactNode; + name: string; + isSelected: boolean; + onClick: () => void; +}) { + return ( + + {icon} + {name} + + ); +} diff --git a/web/src/app/home/components/home-sidebar/sidbarConfigList.tsx b/web/src/app/home/components/home-sidebar/sidbarConfigList.tsx new file mode 100644 index 00000000..9d051534 --- /dev/null +++ b/web/src/app/home/components/home-sidebar/sidbarConfigList.tsx @@ -0,0 +1,65 @@ +import { SidebarChildVO } from '@/app/home/components/home-sidebar/HomeSidebarChild'; +import styles from './HomeSidebar.module.css'; + +export const sidebarConfigList = [ + new SidebarChildVO({ + id: 'bots', + name: '机器人', + icon: ( + + + + ), + route: '/home/bots', + }), + new SidebarChildVO({ + id: 'models', + name: '模型配置', + icon: ( + + + + ), + route: '/home/models', + }), + new SidebarChildVO({ + id: 'pipelines', + name: '流水线', + icon: ( + + + + ), + route: '/home/pipelines', + }), + new SidebarChildVO({ + id: 'plugins', + name: '插件管理', + icon: ( + + + + ), + route: '/home/plugins', + }), +]; diff --git a/web/src/app/home/components/home-titlebar/HomeTitleBar.tsx b/web/src/app/home/components/home-titlebar/HomeTitleBar.tsx new file mode 100644 index 00000000..a00c961c --- /dev/null +++ b/web/src/app/home/components/home-titlebar/HomeTitleBar.tsx @@ -0,0 +1,9 @@ +import styles from './HomeTittleBar.module.css'; + +export default function HomeTitleBar({ title }: { title: string }) { + return ( + + {title} + + ); +} diff --git a/web/src/app/home/components/home-titlebar/HomeTittleBar.module.css b/web/src/app/home/components/home-titlebar/HomeTittleBar.module.css new file mode 100644 index 00000000..8a9a7da4 --- /dev/null +++ b/web/src/app/home/components/home-titlebar/HomeTittleBar.module.css @@ -0,0 +1,18 @@ +.titleBarContainer { + width: 100%; + padding-top: 0.8rem; + height: 4rem; + opacity: 1; + font-size: 20px; + display: flex; + flex-direction: row; + align-items: center; + background-color: #fafafa; +} + +.titleText { + margin-left: 3.2rem; + font-size: 1.6rem; + font-weight: 500; + color: #585858; +} \ No newline at end of file diff --git a/web/src/app/home/layout.module.css b/web/src/app/home/layout.module.css new file mode 100644 index 00000000..0c48d4f3 --- /dev/null +++ b/web/src/app/home/layout.module.css @@ -0,0 +1,32 @@ +/* 主布局容器 */ +.homeLayoutContainer { + width: 100vw; + height: 100vh; + display: flex; + flex-direction: row; + background-color: #eee; +} + +/* 主内容区域 */ +.main { + background-color: #fafafa; + flex: 1; + display: flex; + flex-direction: column; + /* height: 100vh; */ + width: calc(100% - 1.2rem); + height: calc(100% - 1.2rem); + overflow: hidden; + border-radius: 1.5rem 0 0 1.5rem; + margin-left: 0.6rem; + margin-top: 0.6rem; + box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.05); +} + +.mainContent { + padding: 1.5rem; + padding-left: 2rem; + flex: 1; + overflow-y: auto; + background-color: #fafafa; +} \ No newline at end of file diff --git a/web/src/app/home/layout.tsx b/web/src/app/home/layout.tsx new file mode 100644 index 00000000..ae4bc723 --- /dev/null +++ b/web/src/app/home/layout.tsx @@ -0,0 +1,32 @@ +'use client'; + +import styles from './layout.module.css'; +import HomeSidebar from '@/app/home/components/home-sidebar/HomeSidebar'; +import HomeTitleBar from '@/app/home/components/home-titlebar/HomeTitleBar'; +import React, { useState } from 'react'; +import { SidebarChildVO } from '@/app/home/components/home-sidebar/HomeSidebarChild'; + +export default function HomeLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + const [title, setTitle] = useState(''); + const onSelectedChangeAction = (child: SidebarChildVO) => { + setTitle(child.name); + }; + + return ( + + + + + + + {children} + + + ); +} diff --git a/web/src/app/home/models/ICreateLLMField.ts b/web/src/app/home/models/ICreateLLMField.ts new file mode 100644 index 00000000..4ded490b --- /dev/null +++ b/web/src/app/home/models/ICreateLLMField.ts @@ -0,0 +1,8 @@ +export interface ICreateLLMField { + name: string; + model_provider: string; + url: string; + api_key: string; + abilities: string[]; + extra_args: string[]; +} diff --git a/web/src/app/home/models/LLMConfig.module.css b/web/src/app/home/models/LLMConfig.module.css new file mode 100644 index 00000000..cac172b8 --- /dev/null +++ b/web/src/app/home/models/LLMConfig.module.css @@ -0,0 +1,19 @@ +.modelListContainer { + width: 100%; + padding-left: 0.8rem; + padding-right: 0.8rem; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(24rem, 1fr)); + gap: 2rem; + justify-items: stretch; + align-items: start; +} + +.emptyContainer { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} 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 new file mode 100644 index 00000000..05a50662 --- /dev/null +++ b/web/src/app/home/models/component/llm-card/LLMCard.module.css @@ -0,0 +1,120 @@ +.cardContainer { + width: 100%; + height: 10rem; + background-color: #fff; + border-radius: 10px; + box-shadow: 0px 2px 2px 0 rgba(0, 0, 0, 0.2); + padding: 1.2rem; + cursor: pointer; +} + +.cardContainer:hover { + box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1); +} + +.iconBasicInfoContainer { + width: 100%; + height: 100%; + display: flex; + flex-direction: row; + gap: 0.8rem; + user-select: none; + /* background-color: aqua; */ +} + +.iconImage { + width: 3.8rem; + height: 3.8rem; + margin: 0.2rem; + border-radius: 50%; +} + +.basicInfoContainer { + display: flex; + flex-direction: column; + gap: 0.2rem; + width: 100%; +} + +.basicInfoText { + font-size: 1.4rem; + font-weight: bold; +} + +.providerContainer { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + gap: 0.2rem; +} + +.providerIcon { + width: 1.2rem; + height: 1.2rem; + margin-top: 0.2rem; + color: #626262; +} + +.providerLabel { + font-size: 1.2rem; + font-weight: 600; + color: #626262; +} + +.baseURLContainer { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + gap: 0.2rem; + width: calc(100% - 3rem); +} + +.baseURLIcon { + width: 1.2rem; + height: 1.2rem; + color: #626262; +} + +.baseURLText { + font-size: 1rem; + width: 100%; + color: #626262; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; +} + +.abilitiesContainer { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + gap: 0.4rem; +} + +.abilityBadge { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + gap: 0.2rem; + height: 1.5rem; + padding: 0.5rem; + border-radius: 0.8rem; + background-color: #66baff80; +} + +.abilityIcon { + width: 1rem; + height: 1rem; + color: #2288ee; +} + +.abilityLabel { + font-size: 0.8rem; + font-weight: 400; + color: #2288ee; +} \ No newline at end of file diff --git a/web/src/app/home/models/component/llm-card/LLMCard.tsx b/web/src/app/home/models/component/llm-card/LLMCard.tsx new file mode 100644 index 00000000..bd7c2f5e --- /dev/null +++ b/web/src/app/home/models/component/llm-card/LLMCard.tsx @@ -0,0 +1,92 @@ +import styles from './LLMCard.module.css'; +import { LLMCardVO } from '@/app/home/models/component/llm-card/LLMCardVO'; + +function checkAbilityBadges(abilities: string[]) { + const abilityBadges = { + vision: ( + + + + + 视觉能力 + + ), + func_call: ( + + + + + 函数调用 + + ), + }; + + return abilities.map((ability) => { + return abilityBadges[ability as keyof typeof abilityBadges]; + }); +} + +export default function LLMCard({ cardVO }: { cardVO: LLMCardVO }) { + return ( + + + + + + {/* 名称 */} + + {cardVO.name} + + {/* 厂商 */} + + + + + + {cardVO.providerLabel} + + + {/* baseURL */} + + + + + {cardVO.baseURL} + + {/* 能力 */} + + {checkAbilityBadges(cardVO.abilities)} + + + + + ); +} diff --git a/web/src/app/home/models/component/llm-card/LLMCardVO.ts b/web/src/app/home/models/component/llm-card/LLMCardVO.ts new file mode 100644 index 00000000..274cede1 --- /dev/null +++ b/web/src/app/home/models/component/llm-card/LLMCardVO.ts @@ -0,0 +1,26 @@ +export interface ILLMCardVO { + id: string; + iconURL: string; + name: string; + providerLabel: string; + baseURL: string; + abilities: string[]; +} + +export class LLMCardVO implements ILLMCardVO { + id: string; + iconURL: string; + providerLabel: string; + name: string; + baseURL: string; + abilities: string[]; + + constructor(props: ILLMCardVO) { + this.id = props.id; + this.iconURL = props.iconURL; + this.providerLabel = props.providerLabel; + this.name = props.name; + this.baseURL = props.baseURL; + this.abilities = props.abilities; + } +} diff --git a/web/src/app/home/models/component/llm-form/ChooseRequesterEntity.ts b/web/src/app/home/models/component/llm-form/ChooseRequesterEntity.ts new file mode 100644 index 00000000..5728c1ce --- /dev/null +++ b/web/src/app/home/models/component/llm-form/ChooseRequesterEntity.ts @@ -0,0 +1,4 @@ +export interface IChooseRequesterEntity { + label: string; + value: string; +} diff --git a/web/src/app/home/models/component/llm-form/LLMForm.tsx b/web/src/app/home/models/component/llm-form/LLMForm.tsx new file mode 100644 index 00000000..fd6bd998 --- /dev/null +++ b/web/src/app/home/models/component/llm-form/LLMForm.tsx @@ -0,0 +1,549 @@ +import { ICreateLLMField } from '@/app/home/models/ICreateLLMField'; +import { useEffect, useState } from 'react'; +import { IChooseRequesterEntity } from '@/app/home/models/component/llm-form/ChooseRequesterEntity'; +import { httpClient } from '@/app/infra/http/HttpClient'; +import { LLMModel } from '@/app/infra/entities/api'; +import { UUID } from 'uuidjs'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Checkbox } from '@/components/ui/checkbox'; +import { toast } from 'sonner'; +const extraArgSchema = z + .object({ + key: z.string().min(1, { message: '键名不能为空' }), + type: z.enum(['string', 'number', 'boolean']), + value: z.string(), + }) + .superRefine((data, ctx) => { + if (data.type === 'number' && isNaN(Number(data.value))) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: '必须是有效的数字', + path: ['value'], + }); + } + if ( + data.type === 'boolean' && + data.value !== 'true' && + data.value !== 'false' + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: '必须是 true 或 false', + path: ['value'], + }); + } + }); + +const formSchema = z.object({ + name: z.string().min(1, { message: '模型名称不能为空' }), + model_provider: z.string().min(1, { message: '模型供应商不能为空' }), + url: z.string().min(1, { message: '请求URL不能为空' }), + api_key: z.string().min(1, { message: 'API Key不能为空' }), + abilities: z.array(z.string()), + extra_args: z.array(extraArgSchema).optional(), +}); + +export default function LLMForm({ + editMode, + initLLMId, + onFormSubmit, + onFormCancel, + onLLMDeleted, +}: { + editMode: boolean; + initLLMId?: string; + onFormSubmit: (value: z.infer) => void; + onFormCancel: () => void; + onLLMDeleted: () => void; +}) { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: '', + model_provider: '', + url: '', + api_key: '', + abilities: [], + extra_args: [], + }, + }); + + const [extraArgs, setExtraArgs] = useState< + { key: string; type: 'string' | 'number' | 'boolean'; value: string }[] + >([]); + + const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false); + const abilityOptions: { label: string; value: string }[] = [ + { + label: '视觉能力', + value: 'vision', + }, + { + label: '函数调用', + value: 'func_call', + }, + ]; + const [requesterNameList, setRequesterNameList] = useState< + IChooseRequesterEntity[] + >([]); + const [requesterDefaultURLList, setRequesterDefaultURLList] = useState< + string[] + >([]); + + useEffect(() => { + initLLMModelFormComponent(); + if (editMode && initLLMId) { + getLLMConfig(initLLMId).then((val) => { + form.setValue('name', val.name); + form.setValue('model_provider', val.model_provider); + form.setValue('url', val.url); + form.setValue('api_key', val.api_key); + form.setValue('abilities', val.abilities as ('vision' | 'func_call')[]); + // 转换extra_args为新格式 + if (val.extra_args) { + const args = val.extra_args.map((arg) => { + const [key, value] = arg.split(':'); + let type: 'string' | 'number' | 'boolean' = 'string'; + if (!isNaN(Number(value))) { + type = 'number'; + } else if (value === 'true' || value === 'false') { + type = 'boolean'; + } + return { + key, + type, + value, + }; + }); + setExtraArgs(args); + form.setValue('extra_args', args); + } + }); + } else { + form.reset(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const addExtraArg = () => { + setExtraArgs([...extraArgs, { key: '', type: 'string', value: '' }]); + }; + + const updateExtraArg = ( + index: number, + field: 'key' | 'type' | 'value', + value: string, + ) => { + const newArgs = [...extraArgs]; + newArgs[index] = { + ...newArgs[index], + [field]: value, + }; + setExtraArgs(newArgs); + form.setValue('extra_args', newArgs); + }; + + const removeExtraArg = (index: number) => { + const newArgs = extraArgs.filter((_, i) => i !== index); + setExtraArgs(newArgs); + form.setValue('extra_args', newArgs); + }; + + async function initLLMModelFormComponent() { + const requesterNameList = await httpClient.getProviderRequesters(); + setRequesterNameList( + requesterNameList.requesters.map((item) => { + return { + label: item.label.zh_CN, + value: item.name, + }; + }), + ); + setRequesterDefaultURLList( + requesterNameList.requesters.map((item) => { + const config = item.spec.config; + for (let i = 0; i < config.length; i++) { + if (config[i].name == 'base_url') { + return config[i].default?.toString() || ''; + } + } + return ''; + }), + ); + } + + async function getLLMConfig(id: string): Promise { + const llmModel = await httpClient.getProviderLLMModel(id); + + const fakeExtraArgs = []; + const extraArgs = llmModel.model.extra_args as Record; + for (const key in extraArgs) { + fakeExtraArgs.push(`${key}:${extraArgs[key]}`); + } + return { + name: llmModel.model.name, + model_provider: llmModel.model.requester, + url: llmModel.model.requester_config?.base_url, + api_key: llmModel.model.api_keys[0], + abilities: llmModel.model.abilities || [], + extra_args: fakeExtraArgs, + }; + } + + function handleFormSubmit(value: z.infer) { + if (editMode) { + // 暂不支持更改模型 + // onSaveEdit(value) + } else { + onCreateLLM(value); + } + form.reset(); + } + + function onCreateLLM(value: z.infer) { + console.log('create llm', value); + // 转换extra_args为对象格式 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const extraArgsObj: Record = {}; + value.extra_args?.forEach((arg) => { + if (arg.type === 'number') { + extraArgsObj[arg.key] = Number(arg.value); + } else if (arg.type === 'boolean') { + extraArgsObj[arg.key] = arg.value === 'true'; + } else { + extraArgsObj[arg.key] = arg.value; + } + }); + + const requestParam: LLMModel = { + uuid: UUID.generate(), + name: value.name, + description: '', + requester: value.model_provider, + requester_config: { + base_url: value.url, + timeout: 120, + }, + extra_args: extraArgsObj, + api_keys: [value.api_key], + abilities: value.abilities, + }; + httpClient + .createProviderLLMModel(requestParam) + .then(() => { + onFormSubmit(value); + toast.success('创建成功'); + }) + .catch((err) => { + toast.error('创建失败:' + err.message); + }); + } + + function deleteModel() { + if (initLLMId) { + httpClient + .deleteProviderLLMModel(initLLMId) + .then(() => { + onLLMDeleted(); + toast.success('删除成功'); + }) + .catch((err) => { + toast.error('删除失败:' + err.message); + }); + } + } + + return ( + + + + + 删除确认 + + 你确定要删除这个模型吗? + + setShowDeleteConfirmModal(false)} + > + 取消 + + { + deleteModel(); + setShowDeleteConfirmModal(false); + }} + > + 确认删除 + + + + + + + + + ( + + + 模型名称* + + + + + + + 请填写供应商向您提供的模型名称 + + + )} + /> + + ( + + + 模型供应商* + + + { + field.onChange(value); + const index = requesterNameList.findIndex( + (item) => item.value === value, + ); + if (index !== -1) { + form.setValue('url', requesterDefaultURLList[index]); + } + }} + value={field.value} + > + + + + + + {requesterNameList.map((item) => ( + + {item.label} + + ))} + + + + + + + )} + /> + + ( + + + 请求URL* + + + + + + + )} + /> + ( + + + API Key* + + + + + + + )} + /> + ( + + 能力 + + 选择模型能力 + + {abilityOptions.map((item) => ( + { + return ( + + + { + return checked + ? field.onChange([ + ...(field.value || []), + item.value, + ]) + : field.onChange( + field.value?.filter( + (value) => value !== item.value, + ), + ); + }} + /> + + + {item.label} + + + ); + }} + /> + ))} + + + )} + /> + + + 额外参数 + + {extraArgs.map((arg, index) => ( + + + updateExtraArg(index, 'key', e.target.value) + } + /> + + updateExtraArg(index, 'type', value) + } + > + + + + + 字符串 + 数字 + 布尔值 + + + + updateExtraArg(index, 'value', e.target.value) + } + /> + removeExtraArg(index)} + > + + + + + + ))} + + 添加参数 + + + + 将在请求时附加到请求体中,如 max_tokens, temperature, top_p 等 + + + + + + {!editMode && 提交} + {editMode && ( + setShowDeleteConfirmModal(true)} + > + 删除 + + )} + onFormCancel()} + > + 取消 + + + + + + ); +} diff --git a/web/src/app/home/models/page.tsx b/web/src/app/home/models/page.tsx new file mode 100644 index 00000000..30fcb889 --- /dev/null +++ b/web/src/app/home/models/page.tsx @@ -0,0 +1,121 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { LLMCardVO } from '@/app/home/models/component/llm-card/LLMCardVO'; +import styles from './LLMConfig.module.css'; +import LLMCard from '@/app/home/models/component/llm-card/LLMCard'; +import LLMForm from '@/app/home/models/component/llm-form/LLMForm'; +import CreateCardComponent from '@/app/infra/basic-component/create-card-component/CreateCardComponent'; +import { httpClient } from '@/app/infra/http/HttpClient'; +import { LLMModel } from '@/app/infra/entities/api'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { toast } from 'sonner'; + +export default function LLMConfigPage() { + const [cardList, setCardList] = useState([]); + const [modalOpen, setModalOpen] = useState(false); + const [isEditForm, setIsEditForm] = useState(false); + const [nowSelectedLLM, setNowSelectedLLM] = useState(null); + + useEffect(() => { + getLLMModelList(); + }, []); + + async function getLLMModelList() { + const requesterNameListResp = await httpClient.getProviderRequesters(); + const requesterNameList = requesterNameListResp.requesters.map((item) => { + return { + label: item.label.zh_CN, + value: item.name, + }; + }); + + httpClient + .getProviderLLMModels() + .then((resp) => { + const llmModelList: LLMCardVO[] = resp.models.map((model: LLMModel) => { + console.log('model', model); + return new LLMCardVO({ + id: model.uuid, + iconURL: httpClient.getProviderRequesterIconURL(model.requester), + name: model.name, + providerLabel: + requesterNameList.find((item) => item.value === model.requester) + ?.label || model.requester.substring(0, 10), + baseURL: model.requester_config?.base_url, + abilities: model.abilities || [], + }); + }); + console.log('get llmModelList', llmModelList); + setCardList(llmModelList); + }) + .catch((err) => { + console.error('get LLM model list error', err); + toast.error('获取模型列表失败:' + err.message); + }); + } + + function selectLLM(cardVO: LLMCardVO) { + setIsEditForm(true); + setNowSelectedLLM(cardVO); + console.log('set now vo', cardVO); + setModalOpen(true); + } + function handleCreateModelClick() { + setIsEditForm(false); + setNowSelectedLLM(null); + setModalOpen(true); + } + + return ( + + + + + {isEditForm ? '预览模型' : '创建模型'} + + { + setModalOpen(false); + getLLMModelList(); + }} + onFormCancel={() => { + setModalOpen(false); + }} + onLLMDeleted={() => { + setModalOpen(false); + getLLMModelList(); + }} + /> + + + + + {cardList.map((cardVO) => { + return ( + { + selectLLM(cardVO); + }} + > + + + ); + })} + + + ); +} diff --git a/web/src/app/home/page.tsx b/web/src/app/home/page.tsx new file mode 100644 index 00000000..29814463 --- /dev/null +++ b/web/src/app/home/page.tsx @@ -0,0 +1,3 @@ +export default function Home() { + return ; +} diff --git a/web/src/app/home/pipelines/components/pipeline-card/PipelineCard.tsx b/web/src/app/home/pipelines/components/pipeline-card/PipelineCard.tsx new file mode 100644 index 00000000..59c6ef18 --- /dev/null +++ b/web/src/app/home/pipelines/components/pipeline-card/PipelineCard.tsx @@ -0,0 +1,49 @@ +import styles from './pipelineCard.module.css'; +import { PipelineCardVO } from '@/app/home/pipelines/components/pipeline-card/PipelineCardVO'; + +export default function PipelineCard({ cardVO }: { cardVO: PipelineCardVO }) { + return ( + + + + + {cardVO.name} + + + {cardVO.description} + + + + + + + + + 更新于{cardVO.lastUpdatedTimeAgo} + + + + + + {cardVO.isDefault && ( + + + + + 默认 + + )} + + + ); +} diff --git a/web/src/app/home/pipelines/components/pipeline-card/PipelineCardVO.ts b/web/src/app/home/pipelines/components/pipeline-card/PipelineCardVO.ts new file mode 100644 index 00000000..4ac80938 --- /dev/null +++ b/web/src/app/home/pipelines/components/pipeline-card/PipelineCardVO.ts @@ -0,0 +1,23 @@ +export interface IPipelineCardVO { + id: string; + name: string; + description: string; + lastUpdatedTimeAgo: string; + isDefault: boolean; +} + +export class PipelineCardVO implements IPipelineCardVO { + id: string; + description: string; + name: string; + lastUpdatedTimeAgo: string; + isDefault: boolean; + + constructor(props: IPipelineCardVO) { + this.id = props.id; + this.name = props.name; + this.description = props.description; + this.lastUpdatedTimeAgo = props.lastUpdatedTimeAgo; + this.isDefault = props.isDefault; + } +} 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 new file mode 100644 index 00000000..0722a3c8 --- /dev/null +++ b/web/src/app/home/pipelines/components/pipeline-card/pipelineCard.module.css @@ -0,0 +1,90 @@ +.cardContainer { + width: 100%; + height: 10rem; + background-color: #fff; + border-radius: 10px; + box-shadow: 0px 2px 2px 0 rgba(0, 0, 0, 0.2); + padding: 1.2rem; + cursor: pointer; + display: flex; + flex-direction: row; + justify-content: space-between; + gap:0.5rem; +} + +.cardContainer:hover { + box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1); +} + +.basicInfoContainer { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 0.4rem; +} + +.basicInfoNameContainer { + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.basicInfoNameText { + font-size: 1.4rem; + font-weight: 500; +} + +.basicInfoDescriptionText { + font-size: 0.9rem; + font-weight: 400; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + color: #b1b1b1; +} + +.basicInfoLastUpdatedTimeContainer { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.5rem; +} + +.basicInfoUpdateTimeIcon { + width: 1.2rem; + height: 1.2rem; +} + +.basicInfoUpdateTimeText { + font-size: 1rem; + font-weight: 400; +} + +.operationContainer { + display: flex; + flex-direction: row; + gap: 0.5rem; + width: 5rem; +} + +.operationDefaultBadge { + display: flex; + flex-direction: row; + gap: 0.5rem; +} + +.operationDefaultBadgeIcon { + width: 1.2rem; + height: 1.2rem; + color: #ffcd27; +} + +.operationDefaultBadgeText { + font-size: 1rem; + font-weight: 400; + color: #ffcd27; +} diff --git a/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx b/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx new file mode 100644 index 00000000..5e20fe3c --- /dev/null +++ b/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx @@ -0,0 +1,454 @@ +import { useEffect, useState } from 'react'; +import { httpClient } from '@/app/infra/http/HttpClient'; +import { Pipeline } from '@/app/infra/entities/api'; +import { + PipelineFormEntity, + PipelineConfigTab, + PipelineConfigStage, +} from '@/app/infra/entities/pipeline'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent'; +import { Button } from '@/components/ui/button'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { Input } from '@/components/ui/input'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { toast } from 'sonner'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from '@/components/ui/dialog'; + +export default function PipelineFormComponent({ + initValues, + isDefaultPipeline, + onFinish, + onNewPipelineCreated, + isEditMode, + pipelineId, +}: { + pipelineId?: string; + isDefaultPipeline: boolean; + isEditMode: boolean; + disableForm: boolean; + // 这里的写法很不安全不规范,未来流水线需要重新整理 + initValues?: PipelineFormEntity; + onFinish: () => void; + onNewPipelineCreated: (pipelineId: string) => void; +}) { + const formSchema = isEditMode + ? z.object({ + basic: z.object({ + name: z.string().min(1, { message: '名称不能为空' }), + description: z.string().min(1, { message: '描述不能为空' }), + }), + ai: z.record(z.string(), z.any()), + trigger: z.record(z.string(), z.any()), + safety: z.record(z.string(), z.any()), + output: z.record(z.string(), z.any()), + }) + : z.object({ + basic: z.object({ + name: z.string().min(1, { message: '名称不能为空' }), + description: z.string().min(1, { message: '描述不能为空' }), + }), + ai: z.record(z.string(), z.any()).optional(), + trigger: z.record(z.string(), z.any()).optional(), + safety: z.record(z.string(), z.any()).optional(), + output: z.record(z.string(), z.any()).optional(), + }); + + type FormValues = z.infer; + // 这里不好,可以改成enum等 + const formLabelList: FormLabel[] = isEditMode + ? [ + { label: '基础信息', name: 'basic' }, + { label: 'AI能力', name: 'ai' }, + { label: '触发条件', name: 'trigger' }, + { label: '安全能力', name: 'safety' }, + { label: '输出处理', name: 'output' }, + ] + : [{ label: '基础信息', name: 'basic' }]; + // const [basicForm] = Form.useForm(); + // const [aiForm] = Form.useForm(); + // const [triggerForm] = Form.useForm(); + // const [safetyForm] = Form.useForm(); + // const [outputForm] = Form.useForm(); + const [aiConfigTabSchema, setAIConfigTabSchema] = + useState(); + const [triggerConfigTabSchema, setTriggerConfigTabSchema] = + useState(); + const [safetyConfigTabSchema, setSafetyConfigTabSchema] = + useState(); + const [outputConfigTabSchema, setOutputConfigTabSchema] = + useState(); + const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + basic: {}, + ai: {}, + trigger: {}, + safety: {}, + output: {}, + }, + }); + + useEffect(() => { + // get config schema from metadata + httpClient.getGeneralPipelineMetadata().then((resp) => { + for (const config of resp.configs) { + if (config.name === 'ai') { + setAIConfigTabSchema(config); + } else if (config.name === 'trigger') { + setTriggerConfigTabSchema(config); + } else if (config.name === 'safety') { + setSafetyConfigTabSchema(config); + } else if (config.name === 'output') { + setOutputConfigTabSchema(config); + } + } + }); + }, []); + + useEffect(() => { + if (initValues) { + form.reset(initValues); + } + + if (!isEditMode) { + form.reset({ + basic: { + name: '', + description: '', + }, + }); + } + }, [initValues, form, isEditMode]); + + function handleFormSubmit(values: FormValues) { + console.log('handleFormSubmit', values); + if (isEditMode) { + handleModify(values); + } else { + handleCreate(values); + } + } + + function handleCreate(values: FormValues) { + console.log('handleCreate', values); + const pipeline: Pipeline = { + config: {}, + description: values.basic.description, + name: values.basic.name, + }; + httpClient + .createPipeline(pipeline) + .then((resp) => { + onFinish(); + onNewPipelineCreated(resp.uuid); + toast.success('创建成功 请编辑流水线详细参数'); + }) + .catch((err) => { + toast.error('创建失败:' + err.message); + }); + } + + function handleModify(values: FormValues) { + const realConfig = { + ai: values.ai, + trigger: values.trigger, + safety: values.safety, + output: values.output, + }; + + const pipeline: Pipeline = { + config: realConfig, + // created_at: '', + description: values.basic.description, + // for_version: '', + name: values.basic.name, + // stages: [], + // updated_at: '', + // uuid: pipelineId || '', + // is_default: false, + }; + httpClient + .updatePipeline(pipelineId || '', pipeline) + .then(() => { + onFinish(); + toast.success('保存成功'); + }) + .catch((err) => { + toast.error('保存失败:' + err.message); + }); + } + + function renderDynamicForms( + stage: PipelineConfigStage, + formName: keyof FormValues, + ) { + // 如果是 AI 配置,需要特殊处理 + if (formName === 'ai') { + // 获取当前选择的 runner + const currentRunner = form.watch('ai.runner.runner'); + + // 如果是 runner 配置项,直接渲染 + if (stage.name === 'runner') { + return ( + + {stage.label.zh_CN} + {stage.description && ( + + {stage.description.zh_CN} + + )} + )?.[stage.name] || + {} + } + onSubmit={(values) => { + const currentValues = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (form.getValues(formName) as Record) || {}; + form.setValue(formName, { + ...currentValues, + [stage.name]: values, + }); + }} + /> + + ); + } + + // 如果不是当前选择的 runner 对应的配置项,则不渲染 + if (stage.name !== currentRunner) { + return null; + } + } + + return ( + + {stage.label.zh_CN} + {stage.description && ( + {stage.description.zh_CN} + )} + )?.[stage.name] || {} + } + onSubmit={(values) => { + const currentValues = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (form.getValues(formName) as Record) || {}; + form.setValue(formName, { + ...currentValues, + [stage.name]: values, + }); + }} + /> + + ); + } + + function deletePipeline() { + httpClient + .deletePipeline(pipelineId || '') + .then(() => { + onFinish(); + toast.success('删除成功'); + }) + .catch((err) => { + toast.error('删除失败:' + err.message); + }); + } + + return ( + + + + + 删除确认 + + + 你确定要删除这个流水线吗?已绑定此流水线的机器人将无法使用。 + + + setShowDeleteConfirmModal(false)} + > + 取消 + + { + deletePipeline(); + setShowDeleteConfirmModal(false); + }} + > + 确认删除 + + + + + + + + + + {formLabelList.map((formLabel) => ( + + {formLabel.label} + + ))} + + + {formLabelList.map((formLabel) => ( + + {formLabel.label} + + {formLabel.name === 'basic' && ( + + ( + + + 名称* + + + + + + + )} + /> + + ( + + + 描述* + + + + + + + )} + /> + + )} + + {isEditMode && ( + <> + {formLabel.name === 'ai' && aiConfigTabSchema && ( + + {aiConfigTabSchema.stages.map((stage) => + renderDynamicForms(stage, 'ai'), + )} + + )} + + {formLabel.name === 'trigger' && triggerConfigTabSchema && ( + + {triggerConfigTabSchema.stages.map((stage) => + renderDynamicForms(stage, 'trigger'), + )} + + )} + + {formLabel.name === 'safety' && safetyConfigTabSchema && ( + + {safetyConfigTabSchema.stages.map((stage) => + renderDynamicForms(stage, 'safety'), + )} + + )} + + {formLabel.name === 'output' && outputConfigTabSchema && ( + + {outputConfigTabSchema.stages.map((stage) => + renderDynamicForms(stage, 'output'), + )} + + )} + > + )} + + ))} + + + + + {isEditMode && isDefaultPipeline && ( + + 默认流水线不可删除 + + )} + + {isEditMode && !isDefaultPipeline && ( + { + setShowDeleteConfirmModal(true); + }} + className="cursor-pointer" + > + 删除 + + )} + + + {isEditMode ? '保存' : '提交'} + + + 取消 + + + + + + + ); +} + +interface FormLabel { + label: string; + name: string; +} diff --git a/web/src/app/home/pipelines/components/pipeline-form/pipelineFormStyle.module.css b/web/src/app/home/pipelines/components/pipeline-form/pipelineFormStyle.module.css new file mode 100644 index 00000000..f1b052f4 --- /dev/null +++ b/web/src/app/home/pipelines/components/pipeline-form/pipelineFormStyle.module.css @@ -0,0 +1,12 @@ +.formItemSubtitle { + font-size: 18px; + font-weight: bold; + margin-bottom: 10px; +} + +.changeFormButtonGroupContainer { + width: 320px; + display: flex; + flex-direction: row; + justify-content: space-between; +} diff --git a/web/src/app/home/pipelines/page.tsx b/web/src/app/home/pipelines/page.tsx new file mode 100644 index 00000000..03031bdd --- /dev/null +++ b/web/src/app/home/pipelines/page.tsx @@ -0,0 +1,153 @@ +'use client'; +import { useState, useEffect } from 'react'; +import CreateCardComponent from '@/app/infra/basic-component/create-card-component/CreateCardComponent'; +import PipelineFormComponent from './components/pipeline-form/PipelineFormComponent'; +import { httpClient } from '@/app/infra/http/HttpClient'; +import { PipelineCardVO } from '@/app/home/pipelines/components/pipeline-card/PipelineCardVO'; +import PipelineCard from '@/app/home/pipelines/components/pipeline-card/PipelineCard'; +import { PipelineFormEntity } from '@/app/infra/entities/pipeline'; +import styles from './pipelineConfig.module.css'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { toast } from 'sonner'; +export default function PluginConfigPage() { + const [modalOpen, setModalOpen] = useState(false); + const [isEditForm, setIsEditForm] = useState(false); + const [pipelineList, setPipelineList] = useState([]); + const [selectedPipelineId, setSelectedPipelineId] = useState(''); + const [selectedPipelineFormValue, setSelectedPipelineFormValue] = + useState({ + basic: {}, + ai: {}, + trigger: {}, + safety: {}, + output: {}, + }); + const [disableForm, setDisableForm] = useState(false); + const [selectedPipelineIsDefault, setSelectedPipelineIsDefault] = + useState(false); + + useEffect(() => { + getPipelines(); + }, []); + + function getPipelines() { + httpClient + .getPipelines() + .then((value) => { + const currentTime = new Date(); + const pipelineList = value.pipelines.map((pipeline) => { + const lastUpdatedTimeAgo = Math.floor( + (currentTime.getTime() - + new Date( + pipeline.updated_at ?? currentTime.getTime(), + ).getTime()) / + 1000 / + 60 / + 60 / + 24, + ); + + const lastUpdatedTimeAgoText = + lastUpdatedTimeAgo > 0 ? ` ${lastUpdatedTimeAgo} 天前` : '今天'; + + return new PipelineCardVO({ + lastUpdatedTimeAgo: lastUpdatedTimeAgoText, + description: pipeline.description, + id: pipeline.uuid ?? '', + name: pipeline.name, + isDefault: pipeline.is_default ?? false, + }); + }); + setPipelineList(pipelineList); + }) + .catch((error) => { + console.log(error); + toast.error('获取流水线列表失败:' + error.message); + }); + } + + function getSelectedPipelineForm(id?: string) { + httpClient.getPipeline(id ?? selectedPipelineId).then((value) => { + setSelectedPipelineFormValue({ + ai: value.pipeline.config.ai, + basic: { + description: value.pipeline.description, + name: value.pipeline.name, + }, + output: value.pipeline.config.output, + safety: value.pipeline.config.safety, + trigger: value.pipeline.config.trigger, + }); + setSelectedPipelineIsDefault(value.pipeline.is_default ?? false); + setDisableForm(false); + }); + } + + return ( + + + + + + {isEditForm ? '编辑流水线' : '创建流水线'} + + + + { + setDisableForm(true); + setIsEditForm(true); + setModalOpen(true); + setSelectedPipelineId(pipelineId); + getSelectedPipelineForm(pipelineId); + }} + onFinish={() => { + getPipelines(); + setModalOpen(false); + }} + isEditMode={isEditForm} + pipelineId={selectedPipelineId} + disableForm={disableForm} + initValues={selectedPipelineFormValue} + isDefaultPipeline={selectedPipelineIsDefault} + /> + + + + + + { + setIsEditForm(false); + setModalOpen(true); + }} + /> + + {pipelineList.map((pipeline) => { + return ( + { + setDisableForm(true); + setIsEditForm(true); + setModalOpen(true); + setSelectedPipelineId(pipeline.id); + getSelectedPipelineForm(pipeline.id); + }} + > + + + ); + })} + + + ); +} diff --git a/web/src/app/home/pipelines/pipelineConfig.module.css b/web/src/app/home/pipelines/pipelineConfig.module.css new file mode 100644 index 00000000..8330b40c --- /dev/null +++ b/web/src/app/home/pipelines/pipelineConfig.module.css @@ -0,0 +1,16 @@ +.configPageContainer { + width: 100%; + height: 100%; +} + + +.pipelineListContainer { + width: 100%; + padding-left: 0.8rem; + padding-right: 0.8rem; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(24rem, 1fr)); + gap: 2rem; + justify-items: stretch; + align-items: start; +} diff --git a/web/src/app/home/plugins/page.tsx b/web/src/app/home/plugins/page.tsx new file mode 100644 index 00000000..c66f3a25 --- /dev/null +++ b/web/src/app/home/plugins/page.tsx @@ -0,0 +1,170 @@ +'use client'; +import PluginInstalledComponent, { + PluginInstalledComponentRef, +} from '@/app/home/plugins/plugin-installed/PluginInstalledComponent'; +import PluginMarketComponent from '@/app/home/plugins/plugin-market/PluginMarketComponent'; +import styles from './plugins.module.css'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Button } from '@/components/ui/button'; +import { PlusIcon } from 'lucide-react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { GithubIcon } from 'lucide-react'; +import { useState, useRef } from 'react'; +import { httpClient } from '@/app/infra/http/HttpClient'; +import { toast } from 'sonner'; +enum PluginInstallStatus { + WAIT_INPUT = 'wait_input', + INSTALLING = 'installing', + ERROR = 'error', +} + +export default function PluginConfigPage() { + const [modalOpen, setModalOpen] = useState(false); + const [pluginInstallStatus, setPluginInstallStatus] = + useState(PluginInstallStatus.WAIT_INPUT); + const [installError, setInstallError] = useState(null); + const [githubURL, setGithubURL] = useState(''); + const pluginInstalledRef = useRef(null); + + function handleModalConfirm() { + installPlugin(githubURL); + } + function installPlugin(url: string) { + setPluginInstallStatus(PluginInstallStatus.INSTALLING); + httpClient + .installPluginFromGithub(url) + .then((resp) => { + const taskId = resp.task_id; + + let alreadySuccess = false; + + // 每秒拉取一次任务状态 + const interval = setInterval(() => { + httpClient.getAsyncTask(taskId).then((resp) => { + console.log('task status:', resp); + if (resp.runtime.done) { + clearInterval(interval); + if (resp.runtime.exception) { + setInstallError(resp.runtime.exception); + setPluginInstallStatus(PluginInstallStatus.ERROR); + } else { + // success + if (!alreadySuccess) { + toast.success('插件安装成功'); + alreadySuccess = true; + } + setGithubURL(''); + setModalOpen(false); + pluginInstalledRef.current?.refreshPluginList(); + } + } + }); + }, 1000); + }) + .catch((err) => { + console.log('error when install plugin:', err); + setInstallError(err.message); + setPluginInstallStatus(PluginInstallStatus.ERROR); + }); + } + + return ( + + + + + + 已安装 + + + 插件市场 + + + + + { + setModalOpen(true); + setPluginInstallStatus(PluginInstallStatus.WAIT_INPUT); + setInstallError(null); + }} + > + + 安装 + + + + + + + + { + setGithubURL(githubURL); + setModalOpen(true); + setPluginInstallStatus(PluginInstallStatus.WAIT_INPUT); + setInstallError(null); + }} + /> + + + + + + + + + 从 GitHub 安装插件 + + + {pluginInstallStatus === PluginInstallStatus.WAIT_INPUT && ( + + 目前仅支持从 GitHub 安装 + setGithubURL(e.target.value)} + className="mb-4" + /> + + )} + {pluginInstallStatus === PluginInstallStatus.INSTALLING && ( + + 正在安装插件... + + )} + {pluginInstallStatus === PluginInstallStatus.ERROR && ( + + 插件安装失败: + {installError} + + )} + + {pluginInstallStatus === PluginInstallStatus.WAIT_INPUT && ( + <> + setModalOpen(false)}> + 取消 + + 确认 + > + )} + {pluginInstallStatus === PluginInstallStatus.ERROR && ( + setModalOpen(false)}> + 关闭 + + )} + + + + + ); +} diff --git a/web/src/app/home/plugins/plugin-installed/PluginCardVO.ts b/web/src/app/home/plugins/plugin-installed/PluginCardVO.ts new file mode 100644 index 00000000..0e880543 --- /dev/null +++ b/web/src/app/home/plugins/plugin-installed/PluginCardVO.ts @@ -0,0 +1,38 @@ +export interface IPluginCardVO { + author: string; + name: string; + description: string; + version: string; + enabled: boolean; + priority: number; + status: string; + tools: object[]; + event_handlers: object; + repository: string; +} + +export class PluginCardVO implements IPluginCardVO { + author: string; + name: string; + description: string; + version: string; + enabled: boolean; + priority: number; + status: string; + tools: object[]; + event_handlers: object; + repository: string; + + constructor(prop: IPluginCardVO) { + this.author = prop.author; + this.description = prop.description; + this.enabled = prop.enabled; + this.event_handlers = prop.event_handlers; + this.name = prop.name; + this.priority = prop.priority; + this.repository = prop.repository; + this.status = prop.status; + this.tools = prop.tools; + this.version = prop.version; + } +} diff --git a/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx b/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx new file mode 100644 index 00000000..21002be7 --- /dev/null +++ b/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx @@ -0,0 +1,108 @@ +'use client'; + +import { useState, useEffect, forwardRef, useImperativeHandle } from 'react'; +import { PluginCardVO } from '@/app/home/plugins/plugin-installed/PluginCardVO'; +import PluginCardComponent from '@/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent'; +import PluginForm from '@/app/home/plugins/plugin-installed/plugin-form/PluginForm'; +import styles from '@/app/home/plugins/plugins.module.css'; +import { httpClient } from '@/app/infra/http/HttpClient'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; + +export interface PluginInstalledComponentRef { + refreshPluginList: () => void; +} + +// eslint-disable-next-line react/display-name +const PluginInstalledComponent = forwardRef( + (props, ref) => { + const [pluginList, setPluginList] = useState([]); + const [modalOpen, setModalOpen] = useState(false); + const [selectedPlugin, setSelectedPlugin] = useState( + null, + ); + + useEffect(() => { + initData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + function initData() { + getPluginList(); + } + + function getPluginList() { + httpClient.getPlugins().then((value) => { + setPluginList( + value.plugins.map((plugin) => { + return new PluginCardVO({ + author: plugin.author, + description: plugin.description.zh_CN, + enabled: plugin.enabled, + name: plugin.name, + version: plugin.version, + status: plugin.status, + tools: plugin.tools, + event_handlers: plugin.event_handlers, + repository: plugin.repository, + priority: plugin.priority, + }); + }), + ); + }); + } + + useImperativeHandle(ref, () => ({ + refreshPluginList: getPluginList, + })); + + function handlePluginClick(plugin: PluginCardVO) { + setSelectedPlugin(plugin); + setModalOpen(true); + } + + return ( + + + + + 插件配置 + + + {selectedPlugin && ( + { + setModalOpen(false); + getPluginList(); + }} + onFormCancel={() => { + setModalOpen(false); + }} + /> + )} + + + + + {pluginList.map((vo, index) => { + return ( + + handlePluginClick(vo)} + /> + + ); + })} + + ); + }, +); + +export default PluginInstalledComponent; diff --git a/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx b/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx new file mode 100644 index 00000000..eecba2f0 --- /dev/null +++ b/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx @@ -0,0 +1,117 @@ +import { PluginCardVO } from '@/app/home/plugins/plugin-installed/PluginCardVO'; +import { useState } from 'react'; +import { httpClient } from '@/app/infra/http/HttpClient'; +import { Badge } from '@/components/ui/badge'; +import { Switch } from '@/components/ui/switch'; +import { toast } from 'sonner'; + +export default function PluginCardComponent({ + cardVO, + onCardClick, +}: { + cardVO: PluginCardVO; + onCardClick: () => void; +}) { + const [enabled, setEnabled] = useState(cardVO.enabled); + const [switchEnable, setSwitchEnable] = useState(true); + + function handleEnable(e: React.MouseEvent) { + e.stopPropagation(); // 阻止事件冒泡 + setSwitchEnable(false); + httpClient + .togglePlugin(cardVO.author, cardVO.name, !enabled) + .then(() => { + setEnabled(!enabled); + }) + .catch((err) => { + toast.error('修改失败:' + err.message); + }) + .finally(() => { + setSwitchEnable(true); + }); + } + return ( + + + + + + + + + + + {cardVO.author} /{' '} + + + {cardVO.name} + + v{cardVO.version} + + + + + + {cardVO.description} + + + + + + + + + + 事件 {Object.keys(cardVO.event_handlers).length} + + + + + + + + + 工具 {cardVO.tools.length} + + + + + + + + handleEnable(e)} + disabled={!switchEnable} + /> + + + + {/* + + */} + + + + + ); +} diff --git a/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx b/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx new file mode 100644 index 00000000..060afa06 --- /dev/null +++ b/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx @@ -0,0 +1,234 @@ +import { useState, useEffect } from 'react'; +import { ApiRespPluginConfig, Plugin } from '@/app/infra/entities/api'; +import { httpClient } from '@/app/infra/http/HttpClient'; +import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog'; +import { toast } from 'sonner'; + +enum PluginRemoveStatus { + WAIT_INPUT = 'WAIT_INPUT', + REMOVING = 'REMOVING', + ERROR = 'ERROR', +} + +export default function PluginForm({ + pluginAuthor, + pluginName, + onFormSubmit, + onFormCancel, +}: { + pluginAuthor: string; + pluginName: string; + onFormSubmit: () => void; + onFormCancel: () => void; +}) { + const [pluginInfo, setPluginInfo] = useState(); + const [pluginConfig, setPluginConfig] = useState(); + const [isSaving, setIsLoading] = useState(false); + + const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false); + const [pluginRemoveStatus, setPluginRemoveStatus] = + useState(PluginRemoveStatus.WAIT_INPUT); + const [pluginRemoveError, setPluginRemoveError] = useState( + null, + ); + + useEffect(() => { + // 获取插件信息 + httpClient.getPlugin(pluginAuthor, pluginName).then((res) => { + setPluginInfo(res.plugin); + }); + // 获取插件配置 + httpClient.getPluginConfig(pluginAuthor, pluginName).then((res) => { + setPluginConfig(res); + }); + }, [pluginAuthor, pluginName]); + + const handleSubmit = async (values: object) => { + setIsLoading(true); + httpClient + .updatePluginConfig(pluginAuthor, pluginName, values) + .then(() => { + onFormSubmit(); + toast.success('保存成功'); + }) + .catch((error) => { + toast.error('保存失败:' + error.message); + }) + .finally(() => { + setIsLoading(false); + }); + }; + + if (!pluginInfo || !pluginConfig) { + return 加载中...; + } + + function deletePlugin() { + setPluginRemoveStatus(PluginRemoveStatus.REMOVING); + httpClient + .removePlugin(pluginAuthor, pluginName) + .then((res) => { + const taskId = res.task_id; + + let alreadySuccess = false; + + const interval = setInterval(() => { + httpClient.getAsyncTask(taskId).then((res) => { + if (res.runtime.done) { + clearInterval(interval); + if (res.runtime.exception) { + setPluginRemoveError(res.runtime.exception); + setPluginRemoveStatus(PluginRemoveStatus.ERROR); + } else { + // success + if (!alreadySuccess) { + toast.success('插件删除成功'); + alreadySuccess = true; + } + setPluginRemoveStatus(PluginRemoveStatus.WAIT_INPUT); + setShowDeleteConfirmModal(false); + onFormSubmit(); + } + } + }); + }, 1000); + }) + .catch((error) => { + setPluginRemoveError(error.message); + setPluginRemoveStatus(PluginRemoveStatus.ERROR); + }); + } + + return ( + + + + + 删除确认 + + + {pluginRemoveStatus === PluginRemoveStatus.WAIT_INPUT && ( + + 你确定要删除插件({pluginAuthor}/{pluginName})吗? + + )} + {pluginRemoveStatus === PluginRemoveStatus.REMOVING && ( + 删除中... + )} + {pluginRemoveStatus === PluginRemoveStatus.ERROR && ( + + 删除失败: + {pluginRemoveError} + + )} + + + {pluginRemoveStatus === PluginRemoveStatus.WAIT_INPUT && ( + { + setShowDeleteConfirmModal(false); + setPluginRemoveStatus(PluginRemoveStatus.WAIT_INPUT); + }} + > + 取消 + + )} + {pluginRemoveStatus === PluginRemoveStatus.WAIT_INPUT && ( + { + deletePlugin(); + }} + > + 确认删除 + + )} + {pluginRemoveStatus === PluginRemoveStatus.REMOVING && ( + + 删除中... + + )} + {pluginRemoveStatus === PluginRemoveStatus.ERROR && ( + { + setShowDeleteConfirmModal(false); + // setPluginRemoveStatus(PluginRemoveStatus.WAIT_INPUT); + }} + > + 关闭 + + )} + + + + + + {pluginInfo.name} + + {pluginInfo.description.zh_CN} + + {pluginInfo.config_schema.length > 0 && ( + } + onSubmit={(values) => { + let config = pluginConfig.config; + config = { + ...config, + ...values, + }; + setPluginConfig({ + config: config, + }); + }} + /> + )} + {pluginInfo.config_schema.length === 0 && ( + 该插件没有配置项。 + )} + + + + + { + setShowDeleteConfirmModal(true); + setPluginRemoveStatus(PluginRemoveStatus.WAIT_INPUT); + }} + disabled={pluginRemoveStatus === PluginRemoveStatus.REMOVING} + > + {pluginRemoveStatus === PluginRemoveStatus.REMOVING + ? '删除中...' + : '删除插件'} + + + handleSubmit(pluginConfig.config)} + disabled={isSaving} + > + {isSaving ? '保存中...' : '保存配置'} + + + 取消 + + + + + ); +} diff --git a/web/src/app/home/plugins/plugin-market/PluginMarketComponent.tsx b/web/src/app/home/plugins/plugin-market/PluginMarketComponent.tsx new file mode 100644 index 00000000..970f1efd --- /dev/null +++ b/web/src/app/home/plugins/plugin-market/PluginMarketComponent.tsx @@ -0,0 +1,245 @@ +'use client'; + +import { useEffect, useState, useRef } from 'react'; +import styles from '@/app/home/plugins/plugins.module.css'; +import { PluginMarketCardVO } from '@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardVO'; +import PluginMarketCardComponent from '@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardComponent'; +import { spaceClient } from '@/app/infra/http/HttpClient'; +import { Input } from '@/components/ui/input'; +import { + Pagination, + PaginationContent, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from '@/components/ui/pagination'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; + +export default function PluginMarketComponent({ + askInstallPlugin, +}: { + askInstallPlugin: (githubURL: string) => void; +}) { + const [marketPluginList, setMarketPluginList] = useState< + PluginMarketCardVO[] + >([]); + const [totalCount, setTotalCount] = useState(0); + const [nowPage, setNowPage] = useState(1); + const [searchKeyword, setSearchKeyword] = useState(''); + const [loading, setLoading] = useState(false); + const [sortByValue, setSortByValue] = useState('pushed_at'); + const [sortOrderValue, setSortOrderValue] = useState('DESC'); + const searchTimeout = useRef(null); + const pageSize = 10; + + useEffect(() => { + initData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + function initData() { + getPluginList(); + } + + function onInputSearchKeyword(keyword: string) { + setSearchKeyword(keyword); + + // 清除之前的定时器 + if (searchTimeout.current) { + clearTimeout(searchTimeout.current); + } + + // 设置新的定时器 + searchTimeout.current = setTimeout(() => { + setNowPage(1); + getPluginList(1, keyword); + }, 500); + } + + function getPluginList( + page: number = nowPage, + keyword: string = searchKeyword, + sortBy: string = sortByValue, + sortOrder: string = sortOrderValue, + ) { + setLoading(true); + spaceClient + .getMarketPlugins(page, pageSize, keyword, sortBy, sortOrder) + .then((res) => { + setMarketPluginList( + res.plugins.map((marketPlugin) => { + let repository = marketPlugin.repository; + if (repository.startsWith('https://github.com/')) { + repository = repository.replace('https://github.com/', ''); + } + + if (repository.startsWith('github.com/')) { + repository = repository.replace('github.com/', ''); + } + + const author = repository.split('/')[0]; + const name = repository.split('/')[1]; + return new PluginMarketCardVO({ + author: author, + description: marketPlugin.description, + githubURL: `https://github.com/${repository}`, + name: name, + pluginId: String(marketPlugin.ID), + starCount: marketPlugin.stars, + version: + 'version' in marketPlugin + ? String(marketPlugin.version) + : '1.0.0', // Default version if not provided + }); + }), + ); + setTotalCount(res.total); + setLoading(false); + console.log('market plugins:', res); + }) + .catch((error) => { + console.error('获取插件列表失败:', error); + setLoading(false); + }); + } + + function handlePageChange(page: number) { + setNowPage(page); + getPluginList(page); + } + + function handleSortChange(value: string) { + const [newSortBy, newSortOrder] = value.split(',').map((s) => s.trim()); + setSortByValue(newSortBy); + setSortOrderValue(newSortOrder); + setNowPage(1); + getPluginList(1, searchKeyword, newSortBy, newSortOrder); + } + + return ( + + + onInputSearchKeyword(e.target.value)} + /> + + + + + + + 最多星标 + 最近新增 + 最近更新 + + + + + {totalCount > 0 && ( + + + + handlePageChange(nowPage - 1)} + className={ + nowPage <= 1 ? 'pointer-events-none opacity-50' : '' + } + /> + + + {/* 如果总页数大于5,则只显示5页,如果总页数小于5,则显示所有页 */} + {(() => { + const totalPages = Math.ceil(totalCount / pageSize); + const maxVisiblePages = 5; + let startPage = Math.max( + 1, + nowPage - Math.floor(maxVisiblePages / 2), + ); + const endPage = Math.min( + totalPages, + startPage + maxVisiblePages - 1, + ); + + if (endPage - startPage + 1 < maxVisiblePages) { + startPage = Math.max(1, endPage - maxVisiblePages + 1); + } + + return Array.from( + { length: endPage - startPage + 1 }, + (_, i) => { + const pageNum = startPage + i; + return ( + + handlePageChange(pageNum)} + > + + {pageNum} + + + + ); + }, + ); + })()} + + + handlePageChange(nowPage + 1)} + className={ + nowPage >= Math.ceil(totalCount / pageSize) + ? 'pointer-events-none opacity-50' + : '' + } + /> + + + + )} + + + + + {loading ? ( + + {/* 加载中... */} + + ) : marketPluginList.length === 0 ? ( + + {/* 没有找到匹配的插件 */} + + ) : ( + marketPluginList.map((vo, index) => ( + + { + askInstallPlugin(githubURL); + }} + /> + + )) + )} + + + ); +} diff --git a/web/src/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardComponent.tsx b/web/src/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardComponent.tsx new file mode 100644 index 00000000..990701f0 --- /dev/null +++ b/web/src/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardComponent.tsx @@ -0,0 +1,84 @@ +import { PluginMarketCardVO } from '@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardVO'; +import { Button } from '@/components/ui/button'; + +export default function PluginMarketCardComponent({ + cardVO, + installPlugin, +}: { + cardVO: PluginMarketCardVO; + installPlugin: (pluginURL: string) => void; +}) { + function handleInstallClick(pluginURL: string) { + installPlugin(pluginURL); + } + + return ( + + + + + + + + + + + {cardVO.author} /{' '} + + + {cardVO.name} + + + + + {cardVO.description} + + + + + + + + + + 星标 {cardVO.starCount} + + + + + window.open(cardVO.githubURL, '_blank')} + > + + + { + handleInstallClick(cardVO.githubURL); + }} + className="cursor-pointer" + > + 安装 + + + + + + + ); +} diff --git a/web/src/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardVO.ts b/web/src/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardVO.ts new file mode 100644 index 00000000..fe0a1e75 --- /dev/null +++ b/web/src/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardVO.ts @@ -0,0 +1,29 @@ +export interface IPluginMarketCardVO { + pluginId: string; + author: string; + name: string; + description: string; + starCount: number; + githubURL: string; + version: string; +} + +export class PluginMarketCardVO implements IPluginMarketCardVO { + pluginId: string; + description: string; + name: string; + author: string; + githubURL: string; + starCount: number; + version: string; + + constructor(prop: IPluginMarketCardVO) { + this.description = prop.description; + this.name = prop.name; + this.author = prop.author; + this.githubURL = prop.githubURL; + this.starCount = prop.starCount; + this.pluginId = prop.pluginId; + this.version = prop.version; + } +} diff --git a/web/src/app/home/plugins/plugins.module.css b/web/src/app/home/plugins/plugins.module.css new file mode 100644 index 00000000..e2018423 --- /dev/null +++ b/web/src/app/home/plugins/plugins.module.css @@ -0,0 +1,21 @@ +.pageContainer { + width: 100%; +} + + +.marketComponentBody { + width: 100%; + height: calc(100% - 60px); +} + +.pluginListContainer { + width: 100%; + padding-left: 0.8rem; + padding-right: 0.8rem; + padding-top: 2rem; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(24rem, 1fr)); + gap: 2rem; + justify-items: stretch; + align-items: start; +} \ No newline at end of file diff --git a/web/src/app/infra/basic-component/create-card-component/CreateCardComponent.tsx b/web/src/app/infra/basic-component/create-card-component/CreateCardComponent.tsx new file mode 100644 index 00000000..dd65e889 --- /dev/null +++ b/web/src/app/infra/basic-component/create-card-component/CreateCardComponent.tsx @@ -0,0 +1,27 @@ +import styles from './createCartComponent.module.css'; + +export default function CreateCardComponent({ + height, + plusSize, + onClick, + width = '100%', +}: { + height: string; + plusSize: string; + onClick: () => void; + width?: string; +}) { + return ( + + + + + ); +} diff --git a/web/src/app/infra/basic-component/create-card-component/createCartComponent.module.css b/web/src/app/infra/basic-component/create-card-component/createCartComponent.module.css new file mode 100644 index 00000000..180f79db --- /dev/null +++ b/web/src/app/infra/basic-component/create-card-component/createCartComponent.module.css @@ -0,0 +1,19 @@ +.cardContainer { + background-color: #FFF; + border-radius: 9px; + box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-evenly; + cursor: pointer; +} + +.cardContainer:hover { + box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.05); +} + +.createCardContainer { + font-size: 90px; + color: #acacac; +} \ No newline at end of file diff --git a/web/src/app/infra/entities/api/index.ts b/web/src/app/infra/entities/api/index.ts new file mode 100644 index 00000000..28c9de7f --- /dev/null +++ b/web/src/app/infra/entities/api/index.ts @@ -0,0 +1,309 @@ +import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic'; +import { PipelineConfigTab } from '@/app/infra/entities/pipeline'; + +export interface ApiResponse { + code: number; + data: T; + msg: string; +} + +export interface I18nText { + en_US: string; + zh_CN: string; +} + +export interface AsyncTaskCreatedResp { + task_id: number; +} + +export interface ApiRespProviderRequesters { + requesters: Requester[]; +} + +export interface ApiRespProviderRequester { + requester: Requester; +} + +export interface Requester { + name: string; + label: I18nText; + description: I18nText; + icon?: string; + spec: { + config: IDynamicFormItemSchema[]; + }; +} + +export interface ApiRespProviderLLMModels { + models: LLMModel[]; +} + +export interface ApiRespProviderLLMModel { + model: LLMModel; +} + +export interface LLMModel { + name: string; + description: string; + uuid: string; + requester: string; + requester_config: { + base_url: string; + timeout: number; + }; + extra_args?: object; + api_keys: string[]; + abilities?: string[]; + // created_at: string; + // updated_at: string; +} + +export interface ApiRespPipelines { + pipelines: Pipeline[]; +} + +export interface Pipeline { + uuid?: string; + name: string; + description: string; + for_version?: string; + config: object; + stages?: string[]; + is_default?: boolean; + created_at?: string; + updated_at?: string; +} + +export interface ApiRespPlatformAdapters { + adapters: Adapter[]; +} + +export interface ApiRespPlatformAdapter { + adapter: Adapter; +} + +export interface Adapter { + name: string; + label: I18nText; + description: I18nText; + icon?: string; + spec: { + config: AdapterSpecConfig[]; + }; +} + +export interface AdapterSpecConfig { + default: string | number | boolean | Array; + label: I18nText; + name: string; + required: boolean; + type: string; +} + +export interface ApiRespPlatformBots { + bots: Bot[]; +} + +export interface ApiRespPlatformBot { + bot: Bot; +} + +export interface Bot { + uuid?: string; + name: string; + description: string; + enable?: boolean; + adapter: string; + adapter_config: object; + use_pipeline_name?: string; + use_pipeline_uuid?: string; + created_at?: string; + updated_at?: string; +} + +// plugins +export interface ApiRespPlugins { + plugins: Plugin[]; +} + +export interface ApiRespPlugin { + plugin: Plugin; +} + +export interface Plugin { + author: string; + name: string; + description: I18nText; + label: I18nText; + version: string; + enabled: boolean; + priority: number; + status: string; + tools: object[]; + event_handlers: object; + main_file: string; + pkg_path: string; + repository: string; + config_schema: IDynamicFormItemSchema[]; +} + +export interface ApiRespPluginConfig { + config: object; +} + +export interface PluginReorderElement { + author: string; + name: string; + priority: number; +} + +// system +export interface ApiRespSystemInfo { + debug: boolean; + version: string; +} + +export interface ApiRespAsyncTasks { + tasks: AsyncTask[]; +} + +export interface AsyncTaskRuntimeInfo { + done: boolean; + exception?: string; + result?: object; + state: string; +} + +export interface AsyncTaskTaskContext { + current_action: string; + log: string; +} + +export interface AsyncTask { + id: number; + kind: string; + name: string; + task_type: string; // system or user + runtime: AsyncTaskRuntimeInfo; + task_context: AsyncTaskTaskContext; +} + +export interface ApiRespUserToken { + token: string; +} + +export interface MarketPlugin { + ID: number; + CreatedAt: string; // ISO 8601 格式日期 + UpdatedAt: string; + DeletedAt: string | null; + name: string; + author: string; + description: string; + repository: string; // GitHub 仓库路径 + artifacts_path: string; + stars: number; + downloads: number; + status: 'initialized' | 'mounted'; // 可根据实际状态值扩展联合类型 + synced_at: string; + pushed_at: string; // 最后一次代码推送时间 +} + +export interface MarketPluginResponse { + plugins: MarketPlugin[]; + total: number; +} + +interface GetPipelineConfig { + ai: { + 'dashscope-app-api': { + 'api-key': string; + 'app-id': string; + 'app-type': 'agent' | 'workflow'; + 'references-quote'?: string; + }; + 'dify-service-api': { + 'api-key': string; + 'app-type': 'chat' | 'agent' | 'workflow'; + 'base-url': string; + 'thinking-convert': 'plain' | 'original' | 'remove'; + timeout?: number; + }; + 'local-agent': { + 'max-round': number; + model: string; + prompt: Array<{ + content: string; + role: string; + }>; + }; + runner: { + runner: 'local-agent' | 'dify-service-api' | 'dashscope-app-api'; + }; + }; + output: { + 'force-delay': { + max: number; + min: number; + }; + 'long-text-processing': { + 'font-path': string; + strategy: 'forward' | 'image'; + threshold: number; + }; + misc: { + 'at-sender': boolean; + 'hide-exception': boolean; + 'quote-origin': boolean; + 'track-function-calls': boolean; + }; + }; + safety: { + 'content-filter': { + 'check-sensitive-words': boolean; + scope: 'all' | 'income-msg' | 'output-msg'; + }; + 'rate-limit': { + limitation: number; + strategy: 'drop' | 'wait'; + 'window-length': number; + }; + }; + trigger: { + 'access-control': { + blacklist: string[]; + mode: 'blacklist' | 'whitelist'; + whitelist: string[]; + }; + 'group-respond-rules': { + at: boolean; + prefix: string[]; + random: number; + regexp: string[]; + }; + 'ignore-rules': { + prefix: string[]; + regexp: string[]; + }; + }; +} + +interface GetPipeline { + config: GetPipelineConfig; + created_at: string; + description: string; + for_version: string; + is_default: boolean; + name: string; + stages: string[]; + updated_at: string; + uuid: string; +} + +export interface GetPipelineResponseData { + pipeline: GetPipeline; +} + +export interface GetPipelineMetadataResponseData { + configs: PipelineConfigTab[]; +} diff --git a/web/src/app/infra/entities/common.ts b/web/src/app/infra/entities/common.ts new file mode 100644 index 00000000..3408c105 --- /dev/null +++ b/web/src/app/infra/entities/common.ts @@ -0,0 +1,5 @@ +export interface I18nLabel { + en_US: string; + zh_CN: string; + ja_JP?: string; +} diff --git a/web/src/app/infra/entities/form/dynamic.ts b/web/src/app/infra/entities/form/dynamic.ts new file mode 100644 index 00000000..6a185c8b --- /dev/null +++ b/web/src/app/infra/entities/form/dynamic.ts @@ -0,0 +1,29 @@ +import { I18nLabel } from '@/app/infra/entities/common'; + +export interface IDynamicFormItemSchema { + id: string; + default: string | number | boolean | Array; + label: I18nLabel; + name: string; + required: boolean; + type: DynamicFormItemType; + description?: I18nLabel; + options?: IDynamicFormItemOption[]; +} + +export enum DynamicFormItemType { + INT = 'integer', + FLOAT = 'float', + BOOLEAN = 'boolean', + STRING = 'string', + STRING_ARRAY = 'array[string]', + SELECT = 'select', + LLM_MODEL_SELECTOR = 'llm-model-selector', + PROMPT_EDITOR = 'prompt-editor', + UNKNOWN = 'unknown', +} + +export interface IDynamicFormItemOption { + name: string; + label: I18nLabel; +} diff --git a/web/src/app/infra/entities/pipeline/index.ts b/web/src/app/infra/entities/pipeline/index.ts new file mode 100644 index 00000000..29a5f6af --- /dev/null +++ b/web/src/app/infra/entities/pipeline/index.ts @@ -0,0 +1,23 @@ +import { I18nLabel } from '@/app/infra/entities/common'; +import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic'; + +export interface PipelineFormEntity { + basic: object; + ai: object; + trigger: object; + safety: object; + output: object; +} + +export interface PipelineConfigTab { + name: string; + label: I18nLabel; + stages: PipelineConfigStage[]; +} + +export interface PipelineConfigStage { + name: string; + label: I18nLabel; + description?: I18nLabel; + config: IDynamicFormItemSchema[]; +} diff --git a/web/src/app/infra/http/HttpClient.ts b/web/src/app/infra/http/HttpClient.ts new file mode 100644 index 00000000..2533765b --- /dev/null +++ b/web/src/app/infra/http/HttpClient.ts @@ -0,0 +1,438 @@ +import axios, { + AxiosInstance, + AxiosRequestConfig, + AxiosResponse, + AxiosError, +} from 'axios'; +import { + ApiRespProviderRequesters, + ApiRespProviderRequester, + ApiRespProviderLLMModels, + ApiRespProviderLLMModel, + LLMModel, + ApiRespPipelines, + Pipeline, + ApiRespPlatformAdapters, + ApiRespPlatformAdapter, + ApiRespPlatformBots, + ApiRespPlatformBot, + Bot, + ApiRespPlugins, + ApiRespPlugin, + ApiRespPluginConfig, + PluginReorderElement, + AsyncTaskCreatedResp, + ApiRespSystemInfo, + ApiRespAsyncTasks, + ApiRespUserToken, + MarketPluginResponse, + GetPipelineResponseData, + GetPipelineMetadataResponseData, + AsyncTask, +} from '@/app/infra/entities/api'; + +type JSONValue = string | number | boolean | JSONObject | JSONArray | null; +interface JSONObject { + [key: string]: JSONValue; +} +type JSONArray = Array; + +export interface ResponseData { + code: number; + message: string; + data: T; + timestamp: number; +} + +export interface RequestConfig extends AxiosRequestConfig { + isSSR?: boolean; // 服务端渲染标识 + retry?: number; // 重试次数 +} + +class HttpClient { + private instance: AxiosInstance; + private disableToken: boolean = false; + // 暂不需要SSR + // private ssrInstance: AxiosInstance | null = null + + constructor(baseURL?: string, disableToken?: boolean) { + this.instance = axios.create({ + baseURL: baseURL || this.getBaseUrl(), + timeout: 15000, + headers: { + 'Content-Type': 'application/json', + }, + }); + this.disableToken = disableToken || false; + this.initInterceptors(); + } + + // 兜底URL,如果使用未配置会走到这里 + private getBaseUrl(): string { + // NOT IMPLEMENT + if (typeof window === 'undefined') { + // 服务端环境 + return ''; + } + // 客户端环境 + return ''; + } + + // 获取Session + private async getSession() { + // NOT IMPLEMENT + return ''; + } + + // 同步获取Session + private getSessionSync() { + // NOT IMPLEMENT + return localStorage.getItem('token'); + } + + // 拦截器配置 + private initInterceptors() { + // 请求拦截 + this.instance.interceptors.request.use( + async (config) => { + // 服务端请求自动携带 cookie, Langbot暂时用不到SSR相关 + // if (typeof window === 'undefined' && config.isSSR) { } + // cookie not required + // const { cookies } = await import('next/headers') + // config.headers.Cookie = cookies().toString() + + // 客户端添加认证头 + if (typeof window !== 'undefined' && !this.disableToken) { + const session = this.getSessionSync(); + config.headers.Authorization = `Bearer ${session}`; + } + + return config; + }, + (error) => Promise.reject(error), + ); + + // 响应拦截 + this.instance.interceptors.response.use( + (response: AxiosResponse) => { + // 响应拦截处理写在这里,暂无业务需要 + + return response; + }, + (error: AxiosError) => { + // 统一错误处理 + if (error.response) { + const { status, data } = error.response; + const errMessage = data?.message || error.message; + + switch (status) { + case 401: + console.log('401 error: ', errMessage, error.request); + console.log('responseURL', error.request.responseURL); + localStorage.removeItem('token'); + if (!error.request.responseURL.includes('/check-token')) { + window.location.href = '/login'; + } + break; + case 403: + console.error('Permission denied:', errMessage); + break; + case 500: + // NOTE: move to component layer for customized message? + // toast.error(errMessage); + console.error('Server error:', errMessage); + break; + } + + return Promise.reject({ + code: data?.code || status, + message: errMessage, + data: data?.data || null, + }); + } + + return Promise.reject({ + code: -1, + message: error.message || 'Network Error', + data: null, + }); + }, + ); + } + + // 转换下划线为驼峰 + private convertKeysToCamel(obj: JSONValue): JSONValue { + if (Array.isArray(obj)) { + return obj.map((v) => this.convertKeysToCamel(v)); + } else if (obj !== null && typeof obj === 'object') { + return Object.keys(obj).reduce((acc, key) => { + const camelKey = key.replace(/_([a-z])/g, (_, letter) => + letter.toUpperCase(), + ); + acc[camelKey] = this.convertKeysToCamel((obj as JSONObject)[key]); + return acc; + }, {} as JSONObject); + } + return obj; + } + + // 核心请求方法 + public async request(config: RequestConfig): Promise { + try { + // 这里未来如果需要SSR可以将前面替换为SSR的instance + const instance = config.isSSR ? this.instance : this.instance; + const response = await instance.request>(config); + return response.data.data; + } catch (error) { + return this.handleError(error as object); + } + } + + private handleError(error: object): never { + if (axios.isCancel(error)) { + throw { code: -2, message: 'Request canceled', data: null }; + } + throw error; + } + + // 快捷方法 + public get( + url: string, + params?: object, + config?: RequestConfig, + ) { + return this.request({ method: 'get', url, params, ...config }); + } + + public post(url: string, data?: object, config?: RequestConfig) { + return this.request({ method: 'post', url, data, ...config }); + } + + public put(url: string, data?: object, config?: RequestConfig) { + return this.request({ method: 'put', url, data, ...config }); + } + + public delete(url: string, config?: RequestConfig) { + return this.request({ method: 'delete', url, ...config }); + } + + // real api request implementation + // ============ Provider API ============ + public getProviderRequesters(): Promise { + return this.get('/api/v1/provider/requesters'); + } + + public getProviderRequester(name: string): Promise { + return this.get(`/api/v1/provider/requesters/${name}`); + } + + public getProviderRequesterIconURL(name: string): string { + if (this.instance.defaults.baseURL === '/') { + // 获取用户访问的URL + const url = window.location.href; + const baseURL = url.split('/').slice(0, 3).join('/'); + return `${baseURL}/api/v1/provider/requesters/${name}/icon`; + } + return ( + this.instance.defaults.baseURL + + `/api/v1/provider/requesters/${name}/icon` + ); + } + + // ============ Provider Model LLM ============ + public getProviderLLMModels(): Promise { + return this.get('/api/v1/provider/models/llm'); + } + + public getProviderLLMModel(uuid: string): Promise { + return this.get(`/api/v1/provider/models/llm/${uuid}`); + } + + public createProviderLLMModel(model: LLMModel): Promise { + return this.post('/api/v1/provider/models/llm', model); + } + + public deleteProviderLLMModel(uuid: string): Promise { + return this.delete(`/api/v1/provider/models/llm/${uuid}`); + } + + // ============ Pipeline API ============ + public getGeneralPipelineMetadata(): Promise { + // as designed, this method will be deprecated, and only for developer to check the prefered config schema + return this.get('/api/v1/pipelines/_/metadata'); + } + + public getPipelines(): Promise { + return this.get('/api/v1/pipelines'); + } + + public getPipeline(uuid: string): Promise { + return this.get(`/api/v1/pipelines/${uuid}`); + } + + public createPipeline(pipeline: Pipeline): Promise<{ + uuid: string; + }> { + return this.post('/api/v1/pipelines', pipeline); + } + + public updatePipeline(uuid: string, pipeline: Pipeline): Promise { + return this.put(`/api/v1/pipelines/${uuid}`, pipeline); + } + + public deletePipeline(uuid: string): Promise { + return this.delete(`/api/v1/pipelines/${uuid}`); + } + + // ============ Platform API ============ + public getAdapters(): Promise { + return this.get('/api/v1/platform/adapters'); + } + + public getAdapter(name: string): Promise { + return this.get(`/api/v1/platform/adapters/${name}`); + } + + public getAdapterIconURL(name: string): string { + if (this.instance.defaults.baseURL === '/') { + // 获取用户访问的URL + const url = window.location.href; + const baseURL = url.split('/').slice(0, 3).join('/'); + return `${baseURL}/api/v1/platform/adapters/${name}/icon`; + } + return ( + this.instance.defaults.baseURL + `/api/v1/platform/adapters/${name}/icon` + ); + } + + // ============ Platform Bots ============ + public getBots(): Promise { + return this.get('/api/v1/platform/bots'); + } + + public getBot(uuid: string): Promise { + return this.get(`/api/v1/platform/bots/${uuid}`); + } + + public createBot(bot: Bot): Promise { + return this.post('/api/v1/platform/bots', bot); + } + + public updateBot(uuid: string, bot: Bot): Promise { + return this.put(`/api/v1/platform/bots/${uuid}`, bot); + } + + public deleteBot(uuid: string): Promise { + return this.delete(`/api/v1/platform/bots/${uuid}`); + } + + // ============ Plugins API ============ + public getPlugins(): Promise { + return this.get('/api/v1/plugins'); + } + + public getPlugin(author: string, name: string): Promise { + return this.get(`/api/v1/plugins/${author}/${name}`); + } + + public getPluginConfig( + author: string, + name: string, + ): Promise { + return this.get(`/api/v1/plugins/${author}/${name}/config`); + } + + public updatePluginConfig( + author: string, + name: string, + config: object, + ): Promise { + return this.put(`/api/v1/plugins/${author}/${name}/config`, config); + } + + public togglePlugin( + author: string, + name: string, + target_enabled: boolean, + ): Promise { + return this.put(`/api/v1/plugins/${author}/${name}/toggle`, { + target_enabled, + }); + } + + public reorderPlugins(plugins: PluginReorderElement[]): Promise { + return this.post('/api/v1/plugins/reorder', plugins); + } + + public updatePlugin( + author: string, + name: string, + ): Promise { + return this.post(`/api/v1/plugins/${author}/${name}/update`); + } + + public getMarketPlugins( + page: number, + page_size: number, + query: string, + sort_by: string = 'stars', + sort_order: string = 'DESC', + ): Promise { + return this.post(`/api/v1/market/plugins`, { + page, + page_size, + query, + sort_by, + sort_order, + }); + } + public installPluginFromGithub( + source: string, + ): Promise { + return this.post('/api/v1/plugins/install/github', { source }); + } + + public removePlugin( + author: string, + name: string, + ): Promise { + return this.delete(`/api/v1/plugins/${author}/${name}`); + } + + // ============ System API ============ + public getSystemInfo(): Promise { + return this.get('/api/v1/system/info'); + } + + public getAsyncTasks(): Promise { + return this.get('/api/v1/system/tasks'); + } + + public getAsyncTask(id: number): Promise { + return this.get(`/api/v1/system/tasks/${id}`); + } + + // ============ User API ============ + public checkIfInited(): Promise<{ initialized: boolean }> { + return this.get('/api/v1/user/init'); + } + + public initUser(user: string, password: string): Promise { + return this.post('/api/v1/user/init', { user, password }); + } + + public authUser(user: string, password: string): Promise { + return this.post('/api/v1/user/auth', { user, password }); + } + + public checkUserToken(): Promise { + return this.get('/api/v1/user/check-token'); + } +} + +// export const httpClient = new HttpClient("https://version-4.langbot.dev"); +// export const httpClient = new HttpClient('http://localhost:5300'); +export const httpClient = new HttpClient('/'); + +// 临时写法,未来两种Client都继承自HttpClient父类,不允许共享方法 +export const spaceClient = new HttpClient('https://space.langbot.app'); diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx new file mode 100644 index 00000000..8aebbeb8 --- /dev/null +++ b/web/src/app/layout.tsx @@ -0,0 +1,23 @@ +import './global.css'; +import type { Metadata } from 'next'; +import { Toaster } from '@/components/ui/sonner'; + +export const metadata: Metadata = { + title: 'LangBot', + description: 'LangBot 是大模型原生即时通信机器人平台', +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + + ); +} diff --git a/web/src/app/login/layout.tsx b/web/src/app/login/layout.tsx new file mode 100644 index 00000000..4996a7ac --- /dev/null +++ b/web/src/app/login/layout.tsx @@ -0,0 +1,15 @@ +'use client'; + +import React from 'react'; + +export default function LoginLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/web/src/app/login/page.tsx b/web/src/app/login/page.tsx new file mode 100644 index 00000000..297f8b83 --- /dev/null +++ b/web/src/app/login/page.tsx @@ -0,0 +1,165 @@ +'use client'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardDescription, +} from '@/components/ui/card'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as z from 'zod'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { useEffect } from 'react'; +import { httpClient } from '@/app/infra/http/HttpClient'; +import { useRouter } from 'next/navigation'; +import { Mail, Lock } from 'lucide-react'; +import langbotIcon from '@/app/assets/langbot-logo.webp'; +import { toast } from 'sonner'; + +const formSchema = z.object({ + email: z.string().email('请输入有效的邮箱地址'), + password: z.string().min(1, '请输入密码'), +}); + +export default function Login() { + const router = useRouter(); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: '', + password: '', + }, + }); + + useEffect(() => { + getIsInitialized(); + checkIfAlreadyLoggedIn(); + }, []); + + function getIsInitialized() { + httpClient + .checkIfInited() + .then((res) => { + if (!res.initialized) { + router.push('/register'); + } + }) + .catch((err) => { + console.log('error at getIsInitialized: ', err); + }); + } + + function checkIfAlreadyLoggedIn() { + httpClient + .checkUserToken() + .then((res) => { + if (res.token) { + localStorage.setItem('token', res.token); + router.push('/home'); + } + }) + .catch((err) => { + console.log('error at checkIfAlreadyLoggedIn: ', err); + }); + } + function onSubmit(values: z.infer) { + handleLogin(values.email, values.password); + } + + function handleLogin(username: string, password: string) { + httpClient + .authUser(username, password) + .then((res) => { + localStorage.setItem('token', res.token); + console.log('login success: ', res); + router.push('/home'); + toast.success('登录成功'); + }) + .catch((err) => { + console.log('login error: ', err); + + toast.error('登录失败,请检查邮箱和密码是否正确'); + }); + } + + return ( + + + + + + 欢迎回到 LangBot 👋 + + 登录以继续 + + + + + ( + + 邮箱 + + + + + + + + + )} + /> + + ( + + 密码 + + + + + + + + + )} + /> + + + 登录 + + + + + + + ); +} diff --git a/web/src/app/not-found.tsx b/web/src/app/not-found.tsx new file mode 100644 index 00000000..90941c7a --- /dev/null +++ b/web/src/app/not-found.tsx @@ -0,0 +1,63 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { Button } from '@/components/ui/button'; + +export default function NotFound() { + const router = useRouter(); + + return ( + + + + {/* 404 图标 */} + + 404 + + + {/* 错误文本 */} + + + 页面不存在 + + + 您要查找的页面似乎不存在。请检查您输入的 URL + 是否正确,或者返回首页。 + + + + {/* 按钮组 */} + + router.back()} + className="h-9 px-4 cursor-pointer" + > + 上一级 + + router.push('/home')} + className="h-9 px-4 cursor-pointer" + > + 返回主页 + + + + {/* 帮助文档链接 */} + + + 查看 + + 帮助文档 + + + + + + + ); +} diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx new file mode 100644 index 00000000..c2943fe9 --- /dev/null +++ b/web/src/app/page.tsx @@ -0,0 +1,12 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { useEffect } from 'react'; + +export default function Home() { + const router = useRouter(); + useEffect(() => { + router.push('/login'); + }, []); + return ; +} diff --git a/web/src/app/register/layout.tsx b/web/src/app/register/layout.tsx new file mode 100644 index 00000000..c93e0bde --- /dev/null +++ b/web/src/app/register/layout.tsx @@ -0,0 +1,15 @@ +'use client'; + +import React from 'react'; + +export default function RegisterLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/web/src/app/register/page.tsx b/web/src/app/register/page.tsx new file mode 100644 index 00000000..c1a38f58 --- /dev/null +++ b/web/src/app/register/page.tsx @@ -0,0 +1,153 @@ +'use client'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardDescription, +} from '@/components/ui/card'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as z from 'zod'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { useEffect } from 'react'; +import { httpClient } from '@/app/infra/http/HttpClient'; +import { useRouter } from 'next/navigation'; +import { Mail, Lock } from 'lucide-react'; +import langbotIcon from '@/app/assets/langbot-logo.webp'; +import { toast } from 'sonner'; + +const formSchema = z.object({ + email: z.string().email('请输入有效的邮箱地址'), + password: z.string().min(1, '请输入密码'), +}); + +export default function Register() { + const router = useRouter(); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: '', + password: '', + }, + }); + + useEffect(() => { + getIsInitialized(); + }, []); + + function getIsInitialized() { + httpClient + .checkIfInited() + .then((res) => { + if (res.initialized) { + router.push('/login'); + } + }) + .catch((err) => { + console.log('error at getIsInitialized: ', err); + }); + } + + function onSubmit(values: z.infer) { + handleRegister(values.email, values.password); + } + + function handleRegister(username: string, password: string) { + httpClient + .initUser(username, password) + .then((res) => { + console.log('init user success: ', res); + toast.success('初始化成功 请登录'); + router.push('/login'); + }) + .catch((err) => { + console.log('init user error: ', err); + toast.error('初始化失败:' + err.message); + }); + } + + return ( + + + + + + 初始化 LangBot 👋 + + + 这是您首次启动 LangBot + + 您填写的邮箱和密码将作为初始管理员账号 + + + + + + ( + + 邮箱 + + + + + + + + + )} + /> + + ( + + 密码 + + + + + + + + + )} + /> + + + 注册 + + + + + + + ); +} diff --git a/web/src/assets/langbot-logo-block.png b/web/src/assets/langbot-logo-block.png deleted file mode 100644 index b2caca7b..00000000 Binary files a/web/src/assets/langbot-logo-block.png and /dev/null differ diff --git a/web/src/assets/langbot-logo.png b/web/src/assets/langbot-logo.png deleted file mode 100644 index 567a2e4f..00000000 Binary files a/web/src/assets/langbot-logo.png and /dev/null differ diff --git a/web/src/components/AboutDialog.vue b/web/src/components/AboutDialog.vue deleted file mode 100644 index c7f1037f..00000000 --- a/web/src/components/AboutDialog.vue +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - - - - LangBot - - - - 版本 {{ proxy.$store.state.version }} - - - - 版权所有 © 2024 RockChinQ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/src/components/InitDialog.vue b/web/src/components/InitDialog.vue deleted file mode 100644 index e25ff56d..00000000 --- a/web/src/components/InitDialog.vue +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - 系统初始化 - - - - 请输入初始管理员邮箱和密码。 - - - - - - - - - 初始化 - - - - - - - - diff --git a/web/src/components/LoginDialog.vue b/web/src/components/LoginDialog.vue deleted file mode 100644 index 087bb3e7..00000000 --- a/web/src/components/LoginDialog.vue +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - 登录 LangBot - - - - - - - - - - 登录 - - - - - - - - \ No newline at end of file diff --git a/web/src/components/MarketPluginCard.vue b/web/src/components/MarketPluginCard.vue deleted file mode 100644 index ad1379f8..00000000 --- a/web/src/components/MarketPluginCard.vue +++ /dev/null @@ -1,181 +0,0 @@ - - - - - {{ plugin.author }} / - {{ plugin.name }} - - - - - - {{ plugin.description }} - - - - - {{ plugin.stars }} - - 安装 - - - - - - - diff --git a/web/src/components/Marketplace.vue b/web/src/components/Marketplace.vue deleted file mode 100644 index 89c0ad29..00000000 --- a/web/src/components/Marketplace.vue +++ /dev/null @@ -1,228 +0,0 @@ - - - - - - - - - - - - - - 共 {{ proxy.$store.state.marketplaceTotalPluginsCount }} 个插件 - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/src/components/NumberFieldData.vue b/web/src/components/NumberFieldData.vue deleted file mode 100644 index fbdc86e7..00000000 --- a/web/src/components/NumberFieldData.vue +++ /dev/null @@ -1,75 +0,0 @@ - - - - {{ number }} - - - - {{ title }} - - {{ linkText }} - - - - - - \ No newline at end of file diff --git a/web/src/components/PageTitle.vue b/web/src/components/PageTitle.vue deleted file mode 100644 index a4cfb4f0..00000000 --- a/web/src/components/PageTitle.vue +++ /dev/null @@ -1,51 +0,0 @@ - - - {{ title }} - - - - - - - \ No newline at end of file diff --git a/web/src/components/PluginCard.vue b/web/src/components/PluginCard.vue deleted file mode 100644 index 899e0329..00000000 --- a/web/src/components/PluginCard.vue +++ /dev/null @@ -1,257 +0,0 @@ - - - - - {{ plugin.author }} / - {{ plugin.name }} - - - - 已禁用 - v{{ plugin.version - }} - - - {{ plugin.description }} - - - - - - - - {{ Object.keys(plugin.event_handlers).length }} - - - - - - - - {{ plugin.content_functions.length }} - - - - - - - - - - - - {{ item.title }} - - - - - - - - - - - diff --git a/web/src/components/SettingWindow.vue b/web/src/components/SettingWindow.vue deleted file mode 100644 index ff4f2735..00000000 --- a/web/src/components/SettingWindow.vue +++ /dev/null @@ -1,287 +0,0 @@ - - - - - - - - - mdi-view-dashboard-edit-outline - - - mdi-code-json - - - - - - - 重置 - 应用 - - - - - - - - - - - - - - - - *配置文件格式请查看 - 文档 - - - - - - - \ No newline at end of file diff --git a/web/src/components/TaskCard.vue b/web/src/components/TaskCard.vue deleted file mode 100644 index 167f6f41..00000000 --- a/web/src/components/TaskCard.vue +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - - - - - - {{ task.kind }} - {{ task.label }} - 正在执行: {{ task.task_context.current_action }} - 完成 - - - - {{ task.runtime.exception }} - - - - - - - - 关闭 - - - - - - - - - - - - - - - - - - - - - - - - 关闭 - - - - - - - - - - - diff --git a/web/src/components/TaskListDialog.vue b/web/src/components/TaskListDialog.vue deleted file mode 100644 index 94ae2790..00000000 --- a/web/src/components/TaskListDialog.vue +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/src/components/ui/alert.tsx b/web/src/components/ui/alert.tsx new file mode 100644 index 00000000..93a76f8c --- /dev/null +++ b/web/src/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +const alertVariants = cva( + 'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current', + { + variants: { + variant: { + default: 'bg-card text-card-foreground', + destructive: + 'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +); + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<'div'> & VariantProps) { + return ( + + ); +} + +function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) { + return ( + + ); +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( + + ); +} + +export { Alert, AlertTitle, AlertDescription }; diff --git a/web/src/components/ui/badge.tsx b/web/src/components/ui/badge.tsx new file mode 100644 index 00000000..b0c1fc93 --- /dev/null +++ b/web/src/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +const badgeVariants = cva( + 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden', + { + variants: { + variant: { + default: + 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90', + secondary: + 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', + destructive: + 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +); + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<'span'> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : 'span'; + + return ( + + ); +} + +export { Badge, badgeVariants }; diff --git a/web/src/components/ui/button.tsx b/web/src/components/ui/button.tsx new file mode 100644 index 00000000..477fab8b --- /dev/null +++ b/web/src/components/ui/button.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +const buttonVariants = cva( + "cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + 'bg-[#2288ee] text-primary-foreground shadow-xs hover:bg-[#2277e0]', + destructive: + 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', + secondary: + 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', + ghost: + 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-9 px-4 py-2 has-[>svg]:px-3', + sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + icon: 'size-9', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +); + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<'button'> & + VariantProps & { + asChild?: boolean; + }) { + const Comp = asChild ? Slot : 'button'; + + return ( + + ); +} + +export { Button, buttonVariants }; diff --git a/web/src/components/ui/card.tsx b/web/src/components/ui/card.tsx new file mode 100644 index 00000000..4898dfa6 --- /dev/null +++ b/web/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +function Card({ className, ...props }: React.ComponentProps<'div'>) { + return ( + + ); +} + +function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( + + ); +} + +function CardTitle({ className, ...props }: React.ComponentProps<'div'>) { + return ( + + ); +} + +function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { + return ( + + ); +} + +function CardAction({ className, ...props }: React.ComponentProps<'div'>) { + return ( + + ); +} + +function CardContent({ className, ...props }: React.ComponentProps<'div'>) { + return ( + + ); +} + +function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( + + ); +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +}; diff --git a/web/src/components/ui/checkbox.tsx b/web/src/components/ui/checkbox.tsx new file mode 100644 index 00000000..6905d3c9 --- /dev/null +++ b/web/src/components/ui/checkbox.tsx @@ -0,0 +1,32 @@ +'use client'; + +import * as React from 'react'; +import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; +import { CheckIcon } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ); +} + +export { Checkbox }; diff --git a/web/src/components/ui/dialog.tsx b/web/src/components/ui/dialog.tsx new file mode 100644 index 00000000..a544a9ce --- /dev/null +++ b/web/src/components/ui/dialog.tsx @@ -0,0 +1,135 @@ +'use client'; + +import * as React from 'react'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { XIcon } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +function Dialog({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DialogContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + {children} + + + Close + + + + ); +} + +function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( + + ); +} + +function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( + + ); +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; diff --git a/web/src/components/ui/form.tsx b/web/src/components/ui/form.tsx new file mode 100644 index 00000000..657c64a6 --- /dev/null +++ b/web/src/components/ui/form.tsx @@ -0,0 +1,168 @@ +'use client'; + +import * as React from 'react'; +import * as LabelPrimitive from '@radix-ui/react-label'; +import { Slot } from '@radix-ui/react-slot'; +import { + Controller, + FormProvider, + useFormContext, + useFormState, + type ControllerProps, + type FieldPath, + type FieldValues, +} from 'react-hook-form'; + +import { cn } from '@/lib/utils'; +import { Label } from '@/components/ui/label'; + +const Form = FormProvider; + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName; +}; + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue, +); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState } = useFormContext(); + const formState = useFormState({ name: fieldContext.name }); + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error('useFormField should be used within '); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; + +type FormItemContextValue = { + id: string; +}; + +const FormItemContext = React.createContext( + {} as FormItemContextValue, +); + +function FormItem({ className, ...props }: React.ComponentProps<'div'>) { + const id = React.useId(); + + return ( + + + + ); +} + +function FormLabel({ + className, + ...props +}: React.ComponentProps) { + const { error, formItemId } = useFormField(); + + return ( + + ); +} + +function FormControl({ ...props }: React.ComponentProps) { + const { error, formItemId, formDescriptionId, formMessageId } = + useFormField(); + + return ( + + ); +} + +function FormDescription({ className, ...props }: React.ComponentProps<'p'>) { + const { formDescriptionId } = useFormField(); + + return ( + + ); +} + +function FormMessage({ className, ...props }: React.ComponentProps<'p'>) { + const { error, formMessageId } = useFormField(); + const body = error ? String(error?.message ?? '') : props.children; + + if (!body) { + return null; + } + + return ( + + {body} + + ); +} + +export { + useFormField, + Form, + FormItem, + FormLabel, + FormControl, + FormDescription, + FormMessage, + FormField, +}; diff --git a/web/src/components/ui/input.tsx b/web/src/components/ui/input.tsx new file mode 100644 index 00000000..3c1cfcaf --- /dev/null +++ b/web/src/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +function Input({ className, type, ...props }: React.ComponentProps<'input'>) { + return ( + + ); +} + +export { Input }; diff --git a/web/src/components/ui/label.tsx b/web/src/components/ui/label.tsx new file mode 100644 index 00000000..1f7ae512 --- /dev/null +++ b/web/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +'use client'; + +import * as React from 'react'; +import * as LabelPrimitive from '@radix-ui/react-label'; + +import { cn } from '@/lib/utils'; + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Label }; diff --git a/web/src/components/ui/pagination.tsx b/web/src/components/ui/pagination.tsx new file mode 100644 index 00000000..012eadc3 --- /dev/null +++ b/web/src/components/ui/pagination.tsx @@ -0,0 +1,127 @@ +import * as React from 'react'; +import { + ChevronLeftIcon, + ChevronRightIcon, + MoreHorizontalIcon, +} from 'lucide-react'; + +import { cn } from '@/lib/utils'; +import { Button, buttonVariants } from '@/components/ui/button'; + +function Pagination({ className, ...props }: React.ComponentProps<'nav'>) { + return ( + + ); +} + +function PaginationContent({ + className, + ...props +}: React.ComponentProps<'ul'>) { + return ( + + ); +} + +function PaginationItem({ ...props }: React.ComponentProps<'li'>) { + return ; +} + +type PaginationLinkProps = { + isActive?: boolean; +} & Pick, 'size'> & + React.ComponentProps<'a'>; + +function PaginationLink({ + className, + isActive, + size = 'icon', + ...props +}: PaginationLinkProps) { + return ( + + ); +} + +function PaginationPrevious({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ); +} + +function PaginationNext({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ); +} + +function PaginationEllipsis({ + className, + ...props +}: React.ComponentProps<'span'>) { + return ( + + + More pages + + ); +} + +export { + Pagination, + PaginationContent, + PaginationLink, + PaginationItem, + PaginationPrevious, + PaginationNext, + PaginationEllipsis, +}; diff --git a/web/src/components/ui/select.tsx b/web/src/components/ui/select.tsx new file mode 100644 index 00000000..a5d19d35 --- /dev/null +++ b/web/src/components/ui/select.tsx @@ -0,0 +1,185 @@ +'use client'; + +import * as React from 'react'; +import * as SelectPrimitive from '@radix-ui/react-select'; +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +function Select({ + ...props +}: React.ComponentProps) { + return ; +} + +function SelectGroup({ + ...props +}: React.ComponentProps) { + return ; +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return ; +} + +function SelectTrigger({ + className, + size = 'default', + children, + ...props +}: React.ComponentProps & { + size?: 'sm' | 'default'; +}) { + return ( + + {children} + + + + + ); +} + +function SelectContent({ + className, + children, + position = 'popper', + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ); +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +}; diff --git a/web/src/components/ui/sonner.tsx b/web/src/components/ui/sonner.tsx new file mode 100644 index 00000000..564a6547 --- /dev/null +++ b/web/src/components/ui/sonner.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { useTheme } from 'next-themes'; +import { Toaster as Sonner, ToasterProps } from 'sonner'; + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = 'system' } = useTheme(); + + return ( + + ); +}; + +export { Toaster }; diff --git a/web/src/components/ui/switch.tsx b/web/src/components/ui/switch.tsx new file mode 100644 index 00000000..62d855d4 --- /dev/null +++ b/web/src/components/ui/switch.tsx @@ -0,0 +1,31 @@ +'use client'; + +import * as React from 'react'; +import * as SwitchPrimitive from '@radix-ui/react-switch'; + +import { cn } from '@/lib/utils'; + +function Switch({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +export { Switch }; diff --git a/web/src/components/ui/tabs.tsx b/web/src/components/ui/tabs.tsx new file mode 100644 index 00000000..3512331a --- /dev/null +++ b/web/src/components/ui/tabs.tsx @@ -0,0 +1,66 @@ +'use client'; + +import * as React from 'react'; +import * as TabsPrimitive from '@radix-ui/react-tabs'; + +import { cn } from '@/lib/utils'; + +function Tabs({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function TabsList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function TabsTrigger({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/web/src/components/ui/toggle-group.tsx b/web/src/components/ui/toggle-group.tsx new file mode 100644 index 00000000..a15b9bcb --- /dev/null +++ b/web/src/components/ui/toggle-group.tsx @@ -0,0 +1,73 @@ +'use client'; + +import * as React from 'react'; +import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group'; +import { type VariantProps } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; +import { toggleVariants } from '@/components/ui/toggle'; + +const ToggleGroupContext = React.createContext< + VariantProps +>({ + size: 'default', + variant: 'default', +}); + +function ToggleGroup({ + className, + variant, + size, + children, + ...props +}: React.ComponentProps & + VariantProps) { + return ( + + + {children} + + + ); +} + +function ToggleGroupItem({ + className, + children, + variant, + size, + ...props +}: React.ComponentProps & + VariantProps) { + const context = React.useContext(ToggleGroupContext); + + return ( + + {children} + + ); +} + +export { ToggleGroup, ToggleGroupItem }; diff --git a/web/src/components/ui/toggle.tsx b/web/src/components/ui/toggle.tsx new file mode 100644 index 00000000..0fdbf91f --- /dev/null +++ b/web/src/components/ui/toggle.tsx @@ -0,0 +1,47 @@ +'use client'; + +import * as React from 'react'; +import * as TogglePrimitive from '@radix-ui/react-toggle'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +const toggleVariants = cva( + "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap", + { + variants: { + variant: { + default: 'bg-transparent', + outline: + 'border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground', + }, + size: { + default: 'h-9 px-2 min-w-9', + sm: 'h-8 px-1.5 min-w-8', + lg: 'h-10 px-2.5 min-w-10', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +); + +function Toggle({ + className, + variant, + size, + ...props +}: React.ComponentProps & + VariantProps) { + return ( + + ); +} + +export { Toggle, toggleVariants }; diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts new file mode 100644 index 00000000..2819a830 --- /dev/null +++ b/web/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/web/src/main.js b/web/src/main.js deleted file mode 100644 index d31eee22..00000000 --- a/web/src/main.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * main.js - * - * Bootstraps Vuetify and other plugins then mounts the App` - */ - -// Plugins -import { registerPlugins } from '@/plugins' - -// Components -import App from './App.vue' - -// Composables -import { createApp } from 'vue' - -const app = createApp(App) - -registerPlugins(app) - -app.mount('#app') diff --git a/web/src/pages/DashBoard.vue b/web/src/pages/DashBoard.vue deleted file mode 100644 index 407d3870..00000000 --- a/web/src/pages/DashBoard.vue +++ /dev/null @@ -1,151 +0,0 @@ - - - - - - - - - 基础数据 - - - - - - - - - - - - - 消息平台 - - - - - - - - - - - 插件数量 - - - - - - - - - - * 更多图表将在数据持久化功能更新后可用。 - - - - - - - \ No newline at end of file diff --git a/web/src/pages/Logs.vue b/web/src/pages/Logs.vue deleted file mode 100644 index e85d7adc..00000000 --- a/web/src/pages/Logs.vue +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/web/src/pages/Plugins.vue b/web/src/pages/Plugins.vue deleted file mode 100644 index fcdade39..00000000 --- a/web/src/pages/Plugins.vue +++ /dev/null @@ -1,325 +0,0 @@ - - - - - - 已安装 - 插件市场 - - - - - - - 编排 - - - - - - - - - - - {{ element.author }} / - - - {{ element.name }} - - - - - mdi-drag - - - - - - - - - 应用 - - - - - - - - - - 安装 - - - - - - - - - 目前仅支持从 GitHub 安装,插件列表:LangBot 插件 - - - - - - - 安装 - - - - - - - - - - - - - - - - - - - - diff --git a/web/src/pages/Settings.vue b/web/src/pages/Settings.vue deleted file mode 100644 index 58a39699..00000000 --- a/web/src/pages/Settings.vue +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - - - {{ manager.name }} - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/src/plugins/index.js b/web/src/plugins/index.js deleted file mode 100644 index 3ca203de..00000000 --- a/web/src/plugins/index.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * plugins/index.js - * - * Automatically included in `./src/main.js` - */ - -// Plugins -import vuetify from './vuetify' -import router from '@/router' -import store from '@/store' -import axios from 'axios' - -export function registerPlugins (app) { - app - .use(vuetify) - .use(router) - .use(store) - - // 读取用户令牌 - const token = localStorage.getItem('user-token') - - if (token) { - store.state.user.jwtToken = token - } - - // 所有axios请求均携带用户令牌 - axios.defaults.headers.common['Authorization'] = `Bearer ${store.state.user.jwtToken}` - - app.config.globalProperties.$axios = axios - store.commit('initializeFetch') -} diff --git a/web/src/plugins/vuetify.js b/web/src/plugins/vuetify.js deleted file mode 100644 index 1db02919..00000000 --- a/web/src/plugins/vuetify.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * plugins/vuetify.js - * - * Framework documentation: https://vuetifyjs.com` - */ - -// Styles -import '@mdi/font/css/materialdesignicons.css' -import 'vuetify/styles' - -// Composables -import { createVuetify } from 'vuetify' - -// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides -export default createVuetify({ - theme: { - defaultTheme: 'light', - }, -}) diff --git a/web/src/router/index.js b/web/src/router/index.js deleted file mode 100644 index b8572783..00000000 --- a/web/src/router/index.js +++ /dev/null @@ -1,46 +0,0 @@ - -/** - * router/index.ts - * - * Automatic routes for `./src/pages/*.vue` - */ - -// Composables -import { createRouter, createWebHashHistory } from 'vue-router/auto' -import DashBoard from '../pages/DashBoard.vue' -import Settings from '../pages/Settings.vue' -import Logs from '../pages/Logs.vue' -import Plugins from '../pages/Plugins.vue' - -const routes = [ - { path: '/', component: DashBoard }, - { path: '/settings', component: Settings }, - { path: '/logs', component: Logs }, - { path: '/plugins', component: Plugins }, -] - -const router = createRouter({ - history: createWebHashHistory(), - routes, -}) - -// Workaround for https://github.com/vitejs/vite/issues/11804 -router.onError((err, to) => { - if (err?.message?.includes?.('Failed to fetch dynamically imported module')) { - if (!localStorage.getItem('vuetify:dynamic-reload')) { - console.log('Reloading page to fix dynamic import error') - localStorage.setItem('vuetify:dynamic-reload', 'true') - location.assign(to.fullPath) - } else { - console.error('Dynamic import error, reloading page did not fix it', err) - } - } else { - console.error(err) - } -}) - -router.isReady().then(() => { - localStorage.removeItem('vuetify:dynamic-reload') -}) - -export default router diff --git a/web/src/store/index.js b/web/src/store/index.js deleted file mode 100644 index 5374ff1e..00000000 --- a/web/src/store/index.js +++ /dev/null @@ -1,53 +0,0 @@ -import { createStore } from 'vuex' -import router from '@/router' -import axios from 'axios' - -export default createStore({ - state: { - // 开发时使用 - // apiBaseUrl: 'http://localhost:5300/api/v1', - apiBaseUrl: '/api/v1', - autoRefreshLog: false, - autoScrollLog: true, - settingsPageTab: '', - version: 'v0.0.0', - debug: false, - enabledPlatformCount: 0, - user: { - tokenChecked: false, - tokenValid: false, - systemInitialized: true, - jwtToken: '', - }, - pluginsView: 'installed', - marketplaceParams: { - query: '', - page: 1, - per_page: 10, - sort_by: 'pushed_at', - sort_order: 'DESC', - }, - marketplacePlugins: [], - marketplaceTotalPages: 0, - marketplaceTotalPluginsCount: 0, - }, - mutations: { - initializeFetch() { - axios.defaults.baseURL = this.state.apiBaseUrl - - axios.get('/system/info').then(response => { - this.state.version = response.data.data.version - this.state.debug = response.data.data.debug - this.state.enabledPlatformCount = response.data.data.enabled_platform_count - }) - }, - fetchSystemInfo() { - axios.get('/system/info').then(response => { - this.state.version = response.data.data.version - this.state.debug = response.data.data.debug - this.state.enabledPlatformCount = response.data.data.enabled_platform_count - }) - } - }, - actions: {}, -}) diff --git a/web/src/styles/settings.scss b/web/src/styles/settings.scss deleted file mode 100644 index 3e36a279..00000000 --- a/web/src/styles/settings.scss +++ /dev/null @@ -1,10 +0,0 @@ -/** - * src/styles/settings.scss - * - * Configures SASS variables and Vuetify overwrites - */ - -// https://vuetifyjs.com/features/sass-variables/` -// @use 'vuetify/settings' with ( -// $color-pack: false -// ); diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 00000000..c1334095 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/web/vite.config.mjs b/web/vite.config.mjs deleted file mode 100644 index 31052bbe..00000000 --- a/web/vite.config.mjs +++ /dev/null @@ -1,64 +0,0 @@ -// Plugins -import Components from 'unplugin-vue-components/vite' -import Vue from '@vitejs/plugin-vue' -import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify' -import ViteFonts from 'unplugin-fonts/vite' -import VueRouter from 'unplugin-vue-router/vite' - -import { commonjsDeps } from '@koumoul/vjsf/utils/build.js' - -// Utilities -import { defineConfig } from 'vite' -import { fileURLToPath, URL } from 'node:url' - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [ - VueRouter(), - Vue({ - template: { transformAssetUrls } - }), - // https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme - Vuetify({ - autoImport: true, - styles: { - configFile: 'src/styles/settings.scss', - }, - }), - Components(), - ViteFonts({ - google: { - families: [{ - name: 'Roboto', - styles: 'wght@100;300;400;500;700;900', - }], - }, - }), - ], - define: { 'process.env': {} }, - resolve: { - alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)) - }, - extensions: [ - '.js', - '.json', - '.jsx', - '.mjs', - '.ts', - '.tsx', - '.vue', - ], - }, - server: { - port: 3002, - }, - optimizeDeps: { - include: commonjsDeps, - }, - build: { - commonjsOptions: { - transformMixedEsModules: true, - }, - } -}) diff --git a/web/web@0.1.0 b/web/web@0.1.0 new file mode 100644 index 00000000..e69de29b
+ {config.description.zh_CN} +
目前仅支持从 GitHub 安装
正在安装插件...
插件安装失败:
{installError}
+ 您要查找的页面似乎不存在。请检查您输入的 URL + 是否正确,或者返回首页。 +
+ 查看 + + 帮助文档 + +
请输入初始管理员邮箱和密码。
+ {body} +