diff --git a/.gitignore b/.gitignore index 71e0ef8e..48303861 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..68e11c6e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.11.7 + hooks: + # Run the linter of backend. + - id: ruff + args: [--fix] + # Run the formatter of backend. + - id: ruff-format + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.1.0 + hooks: + - id: prettier + types_or: [javascript, jsx, ts, tsx, css, scss] + additional_dependencies: + - prettier@3.1.0 + + - repo: local + hooks: + - id: lint-staged + name: lint-staged + entry: cd web && pnpm lint-staged + language: system + types: [javascript, jsx, ts, tsx] + pass_filenames: false diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 00000000..5fb48089 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,42 @@ + +line-length = 120 + +[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 355eec6e..951b1396 100644 --- a/README.md +++ b/README.md @@ -142,10 +142,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 d9906ac9..c9c449a1 100644 --- a/README_EN.md +++ b/README_EN.md @@ -126,10 +126,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 bb7f6208..c4ca5621 100644 --- a/README_JP.md +++ b/README_JP.md @@ -125,10 +125,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/components.yaml b/components.yaml index f4e2b7bc..fc2084c6 100644 --- a/components.yaml +++ b/components.yaml @@ -17,3 +17,7 @@ spec: LLMAPIRequester: fromDirs: - path: pkg/provider/modelmgr/requesters/ + Plugin: + fromDirs: + - path: plugins/ + maxDepth: 2 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 789b2e95..b66d72a5 100644 --- a/libs/dingtalk_api/api.py +++ b/libs/dingtalk_api/api.py @@ -10,7 +10,14 @@ import traceback class DingTalkClient: - def __init__(self, client_id: str, client_secret: str,robot_name:str,robot_code:str,markdown_card:bool): + def __init__( + self, + client_id: str, + client_secret: str, + robot_name: str, + robot_code: str, + markdown_card: bool, + ): """初始化 WebSocket 连接并自动启动""" self.credential = dingtalk_stream.Credential(client_id, client_secret) self.client = dingtalk_stream.DingTalkStreamClient(self.credential) @@ -20,7 +27,7 @@ class DingTalkClient: self.EchoTextHandler = EchoTextHandler(self) 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 @@ -28,97 +35,78 @@ class DingTalkClient: self.access_token_expiry_time = '' self.markdown_card = markdown_card - - 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') # 返回字符串格式 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) @@ -126,27 +114,28 @@ class DingTalkClient: event = DingTalkEvent.from_payload(message_data) if event: await self._handle_message(event) - - async def send_message(self,content:str,incoming_message): + async def send_message(self, content: str, incoming_message): if self.markdown_card: - self.EchoTextHandler.reply_markdown(title=self.robot_name+'的回答',text=content,incoming_message=incoming_message) + self.EchoTextHandler.reply_markdown( + title=self.robot_name + '的回答', + text=content, + incoming_message=incoming_message, + ) else: - self.EchoTextHandler.reply_text(content,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): @@ -158,37 +147,33 @@ 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['Type'] = 'image' elif incoming_message.message_type == 'audio': message_data['Audio'] = await self.get_audio_url(incoming_message.to_dict()['content']['downloadCode']) @@ -200,56 +185,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..094aeb36 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,122 @@ 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 +164,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 +184,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,40 +200,37 @@ 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) 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]: @@ -239,7 +240,7 @@ class OAClientForLongerResponse(): to_user=from_user, from_user=to_user, create_time=int(time.time()), - content=queue_content + content=queue_content, ) return response_xml @@ -248,65 +249,60 @@ 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 +315,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..dbdbcf4a 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,130 @@ 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", []) + 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)] + 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/slack_api/api.py b/libs/slack_api/api.py index 86239ce9..441692ab 100644 --- a/libs/slack_api/api.py +++ b/libs/slack_api/api.py @@ -1,59 +1,57 @@ import json -from quart import Quart, jsonify,request +from quart import Quart, jsonify, request from slack_sdk.web.async_client import AsyncWebClient from .slackevent import SlackEvent -from typing import Callable, Dict, Any -from pkg.platform.types import events as platform_events, message as platform_message - -class SlackClient(): - - def __init__(self,bot_token:str,signing_secret:str): - - self.bot_token = bot_token - self.signing_secret = signing_secret - self.app = Quart(__name__) - self.client = AsyncWebClient(self.bot_token) - self.app.add_url_rule('/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']) - self._message_handlers = { - "example":[], - } - self.bot_user_id = None # 避免机器人回复自己的消息 - - async def handle_callback_request(self): - try: - body = await request.get_data() - data = json.loads(body) - if 'type' in data: - if data['type'] == 'url_verification': - return data['challenge'] - - bot_user_id = data.get("event",{}).get("bot_id","") - - if self.bot_user_id and bot_user_id == self.bot_user_id: - return jsonify({'status': 'ok'}) - - - # 处理私信 - if data and data.get("event", {}).get("channel_type") in ["im"]: - event = SlackEvent.from_payload(data) - await self._handle_message(event) - return jsonify({'status': 'ok'}) - - #处理群聊 - if data.get("event",{}).get("type") == 'app_mention': - data.setdefault("event", {})["channel_type"] = "channel" - event = SlackEvent.from_payload(data) - await self._handle_message(event) - return jsonify({'status':'ok'}) - - return jsonify({'status': 'ok'}) - - except Exception as e: - raise(e) - +from typing import Callable +from pkg.platform.types import events as platform_events - async def _handle_message(self, event: SlackEvent): +class SlackClient: + def __init__(self, bot_token: str, signing_secret: str): + self.bot_token = bot_token + self.signing_secret = signing_secret + self.app = Quart(__name__) + self.client = AsyncWebClient(self.bot_token) + self.app.add_url_rule( + '/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST'] + ) + self._message_handlers = { + 'example': [], + } + self.bot_user_id = None # 避免机器人回复自己的消息 + + async def handle_callback_request(self): + try: + body = await request.get_data() + data = json.loads(body) + if 'type' in data: + if data['type'] == 'url_verification': + return data['challenge'] + + bot_user_id = data.get('event', {}).get('bot_id', '') + + if self.bot_user_id and bot_user_id == self.bot_user_id: + return jsonify({'status': 'ok'}) + + # 处理私信 + if data and data.get('event', {}).get('channel_type') in ['im']: + event = SlackEvent.from_payload(data) + await self._handle_message(event) + return jsonify({'status': 'ok'}) + + # 处理群聊 + if data.get('event', {}).get('type') == 'app_mention': + data.setdefault('event', {})['channel_type'] = 'channel' + event = SlackEvent.from_payload(data) + await self._handle_message(event) + return jsonify({'status': 'ok'}) + + return jsonify({'status': 'ok'}) + + except Exception as e: + raise (e) + + async def _handle_message(self, event: SlackEvent): """ 处理消息事件。 """ @@ -62,50 +60,38 @@ class SlackClient(): for handler in self._message_handlers[msg_type]: await handler(event) - def on_message(self, msg_type: str): + def on_message(self, msg_type: str): """注册消息类型处理器""" + def decorator(func: Callable[[platform_events.Event], 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 send_message_to_channel(self,text:str,channel_id:str): - try: - response = await self.client.chat_postMessage( - channel=channel_id, - text=text - ) - if self.bot_user_id is None and response.get("ok"): - self.bot_user_id = response["message"]["bot_id"] - return - except Exception as e: - raise e + async def send_message_to_channel(self, text: str, channel_id: str): + try: + response = await self.client.chat_postMessage(channel=channel_id, text=text) + if self.bot_user_id is None and response.get('ok'): + self.bot_user_id = response['message']['bot_id'] + return + except Exception as e: + raise e - async def send_message_to_one(self,text:str,user_id:str): - try: - response = await self.client.chat_postMessage( - channel = '@'+user_id, - text= text - ) - if self.bot_user_id is None and response.get("ok"): - self.bot_user_id = response["message"]["bot_id"] - - return - except Exception as e: - raise e - - async def run_task(self, host: str, port: int, *args, **kwargs): - """ - 启动 Quart 应用。 - """ - await self.app.run_task(host=host, port=port, *args, **kwargs) - - - - - - + async def send_message_to_one(self, text: str, user_id: str): + try: + response = await self.client.chat_postMessage(channel='@' + user_id, text=text) + if self.bot_user_id is None and response.get('ok'): + self.bot_user_id = response['message']['bot_id'] + return + except Exception as e: + raise e + async def run_task(self, host: str, port: int, *args, **kwargs): + """ + 启动 Quart 应用。 + """ + await self.app.run_task(host=host, port=port, *args, **kwargs) diff --git a/libs/slack_api/slackevent.py b/libs/slack_api/slackevent.py index 5a6e9f90..77177816 100644 --- a/libs/slack_api/slackevent.py +++ b/libs/slack_api/slackevent.py @@ -1,86 +1,82 @@ from typing import Dict, Any, Optional + class SlackEvent(dict): @staticmethod - def from_payload(payload: Dict[str, Any]) -> Optional["SlackEvent"]: + def from_payload(payload: Dict[str, Any]) -> Optional['SlackEvent']: try: event = SlackEvent(payload) return event except KeyError: return None - + @property def text(self) -> str: - - if self.get("event", {}).get("channel_type") == "im": - blocks = self.get("event", {}).get("blocks", []) - if not blocks: - return "" + if self.get('event', {}).get('channel_type') == 'im': + blocks = self.get('event', {}).get('blocks', []) + if not blocks: + return '' - elements = blocks[0].get("elements", []) - if not elements: - return "" + elements = blocks[0].get('elements', []) + if not elements: + return '' - elements = elements[0].get("elements", []) - text = "" + elements = elements[0].get('elements', []) + text = '' - for el in elements: - if el.get("type") == "text": - text += el.get("text", "") - elif el.get("type") == "link": - text += el.get("url", "") + for el in elements: + if el.get('type') == 'text': + text += el.get('text', '') + elif el.get('type') == 'link': + text += el.get('url', '') - return text + return text - - if self.get("event",{}).get("channel_type") == 'channel': - message_text = "" - for block in self.get("event", {}).get("blocks", []): - if block.get("type") == "rich_text": - for element in block.get("elements", []): - if element.get("type") == "rich_text_section": + if self.get('event', {}).get('channel_type') == 'channel': + message_text = '' + for block in self.get('event', {}).get('blocks', []): + if block.get('type') == 'rich_text': + for element in block.get('elements', []): + if element.get('type') == 'rich_text_section': parts = [] - for el in element.get("elements", []): - if el.get("type") == "text": - parts.append(el["text"]) - elif el.get("type") == "link": - parts.append(el["url"]) - message_text = "".join(parts) - + for el in element.get('elements', []): + if el.get('type') == 'text': + parts.append(el['text']) + elif el.get('type') == 'link': + parts.append(el['url']) + message_text = ''.join(parts) + return message_text - - @property def user_id(self) -> Optional[str]: - return self.get("event", {}).get("user","") - + return self.get('event', {}).get('user', '') + @property def channel_id(self) -> Optional[str]: - return self.get("event", {}).get("channel","") - + return self.get('event', {}).get('channel', '') + @property def type(self) -> str: - """ message对应私聊,app_mention对应频道at """ - return self.get("event", {}).get("channel_type", "") - + """message对应私聊,app_mention对应频道at""" + return self.get('event', {}).get('channel_type', '') + @property def message_id(self) -> str: - return self.get("event_id","") - + return self.get('event_id', '') + @property def pic_url(self) -> str: """提取 Slack 事件中的图片 URL""" - files = self.get("event", {}).get("files", []) + files = self.get('event', {}).get('files', []) if files: - return files[0].get("url_private", "") + return files[0].get('url_private', '') return None - - + @property def sender_name(self) -> str: - return self.get("event", {}).get("user","") - + return self.get('event', {}).get('user', '') + def __getattr__(self, key: str) -> Optional[Any]: return self.get(key) @@ -88,4 +84,4 @@ class SlackEvent(dict): self[key] = value def __repr__(self) -> str: - return f"" + return f'' diff --git a/libs/wecom_api/WXBizMsgCrypt3.py b/libs/wecom_api/WXBizMsgCrypt3.py index 0123c7d1..ceb5e71a 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,7 @@ 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 +183,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 +196,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 +206,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..f4a62be0 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,24 @@ 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()) - 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,135 +55,134 @@ 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) - 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): + 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) - url = self.base_url+'/message/send?access_token='+self.access_token_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") + 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) if ret != 0: - raise Exception(f"消息解密失败,错误码: {ret}") + raise Exception(f'消息解密失败,错误码: {ret}') # 解析消息并处理 message_data = await self.get_message(xml_msg) @@ -180,9 +191,9 @@ class WecomClient(): 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 +205,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 +229,37 @@ 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): """ @@ -258,7 +270,7 @@ class WecomClient(): 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 +289,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 +314,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/libs/wecom_customer_service_api/api.py b/libs/wecom_customer_service_api/api.py index 04e5398b..965fefcd 100644 --- a/libs/wecom_customer_service_api/api.py +++ b/libs/wecom_customer_service_api/api.py @@ -6,60 +6,61 @@ import httpx import traceback from quart import Quart import xml.etree.ElementTree as ET -from typing import Callable, Dict, Any +from typing import Callable from .wecomcsevent import WecomCSEvent -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 WecomCSClient(): - def __init__(self,corpid:str,secret:str,token:str,EncodingAESKey:str): +class WecomCSClient: + def __init__(self, corpid: str, secret: str, token: str, EncodingAESKey: 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' self.access_token = '' 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': [], } async def get_pic_url(self, media_id: str): if not await self.check_access_token(): self.access_token = await self.get_access_token(self.secret) - url = f"{self.base_url}/media/get?access_token={self.access_token}&media_id={media_id}" + url = f'{self.base_url}/media/get?access_token={self.access_token}&media_id={media_id}' async with httpx.AsyncClient() as client: response = await client.get(url) - if response.headers.get("Content-Type", "").startswith("application/json"): + if response.headers.get('Content-Type', '').startswith('application/json'): data = response.json() if data.get('errcode') in [40014, 42001]: self.access_token = await self.get_access_token(self.secret) return await self.get_pic_url(media_id) else: - raise Exception("Failed to get image: " + str(data)) + raise Exception('Failed to get image: ' + str(data)) # 否则是图片,转成 base64 image_bytes = response.content - content_type = response.headers.get("Content-Type", "") - base64_str = base64.b64encode(image_bytes).decode("utf-8") - base64_str = f"data:{content_type};base64,{base64_str}" + content_type = response.headers.get('Content-Type', '') + base64_str = base64.b64encode(image_bytes).decode('utf-8') + base64_str = f'data:{content_type};base64,{base64_str}' return base64_str - - #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()) - 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) @@ -67,118 +68,115 @@ class WecomCSClient(): if 'access_token' in data: return data['access_token'] else: - raise Exception(f"未获取access token: {data}") - - async def get_detailed_message_list(self,xml_msg:str): + raise Exception(f'未获取access token: {data}') + + async def get_detailed_message_list(self, xml_msg: str): # 在本方法中解析消息,并且获得消息的具体内容 if isinstance(xml_msg, bytes): xml_msg = xml_msg.decode('utf-8') root = ET.fromstring(xml_msg) - token = root.find("Token").text - open_kfid = root.find("OpenKfId").text - + token = root.find('Token').text + open_kfid = root.find('OpenKfId').text + # if open_kfid in self.openkfid_list: # return None # else: # self.openkfid_list.append(open_kfid) - + if not await self.check_access_token(): self.access_token = await self.get_access_token(self.secret) - - url = self.base_url+'/kf/sync_msg?access_token='+ self.access_token + + url = self.base_url + '/kf/sync_msg?access_token=' + self.access_token async with httpx.AsyncClient() as client: params = { - "token": token, - "voice_format": 0, - "open_kfid": open_kfid, + 'token': token, + 'voice_format': 0, + 'open_kfid': open_kfid, } - 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.get_detailed_message_list(xml_msg) if data['errcode'] != 0: - raise Exception("Failed to get message") - + raise Exception('Failed to get message') + last_msg_data = data['msg_list'][-1] - open_kfid = last_msg_data.get("open_kfid") + open_kfid = last_msg_data.get('open_kfid') # 进行获取图片操作 - if last_msg_data.get("msgtype") == "image": - media_id = last_msg_data.get("image").get("media_id") + if last_msg_data.get('msgtype') == 'image': + media_id = last_msg_data.get('image').get('media_id') picurl = await self.get_pic_url(media_id) - last_msg_data["picurl"] = picurl + last_msg_data['picurl'] = picurl # await self.change_service_status(userid=external_userid,openkfid=open_kfid,servicer=servicer) return last_msg_data - - - async def change_service_status(self,userid:str,openkfid:str,servicer:str): + + async def change_service_status(self, userid: str, openkfid: str, servicer: str): if not await self.check_access_token(): self.access_token = await self.get_access_token(self.secret) - url = self.base_url+"/kf/service_state/get?access_token="+self.access_token + url = self.base_url + '/kf/service_state/get?access_token=' + self.access_token async with httpx.AsyncClient() as client: params = { - "open_kfid" : openkfid, - "external_userid" : userid, - "service_state" : 1, - "servicer_userid" : servicer, + 'open_kfid': openkfid, + 'external_userid': userid, + 'service_state': 1, + 'servicer_userid': servicer, } - 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.change_service_status(userid,openkfid) + return await self.change_service_status(userid, openkfid) if data['errcode'] != 0: - raise Exception("Failed to change service status: "+str(data)) - + raise Exception('Failed to change service status: ' + 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)) - + raise Exception('Failed to send image: ' + str(data)) - async def send_text_msg(self, open_kfid: str, external_userid: str, msgid: str,content:str): + async def send_text_msg(self, open_kfid: str, external_userid: str, msgid: str, content: str): if not await self.check_access_token(): self.access_token = await self.get_access_token(self.secret) - url = f"https://qyapi.weixin.qq.com/cgi-bin/kf/send_msg?access_token={self.access_token}" + url = f'https://qyapi.weixin.qq.com/cgi-bin/kf/send_msg?access_token={self.access_token}' payload = { - "touser": external_userid, - "open_kfid": open_kfid, - "msgid": msgid, - "msgtype": "text", - "text": { - "content": content, - } + 'touser': external_userid, + 'open_kfid': open_kfid, + 'msgid': msgid, + 'msgtype': 'text', + 'text': { + 'content': content, + }, } async with httpx.AsyncClient() as client: @@ -187,46 +185,44 @@ class WecomCSClient(): 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_text_msg(open_kfid,external_userid,msgid,content) + return await self.send_text_msg(open_kfid, external_userid, msgid, content) if data['errcode'] != 0: - raise Exception("Failed to send message") + raise Exception('Failed to send message') return 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") + 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) if ret != 0: - raise Exception(f"消息解密失败,错误码: {ret}") + raise Exception(f'消息解密失败,错误码: {ret}') # 解析消息并处理 message_data = await self.get_detailed_message_list(xml_msg) if message_data is not None: - event = WecomCSEvent.from_payload(message_data) + event = WecomCSEvent.from_payload(message_data) if event: await self._handle_message(event) - return "success" + return 'success' except Exception as e: traceback.print_exc() - 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): """ @@ -238,11 +234,13 @@ class WecomCSClient(): """ 注册消息类型处理器。 """ + def decorator(func: Callable[[WecomCSEvent], 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: WecomCSEvent): @@ -254,25 +252,23 @@ class WecomCSClient(): for handler in self._message_handlers[msg_type]: await handler(event) - @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): """ @@ -283,7 +279,7 @@ class WecomCSClient(): 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: @@ -302,20 +298,22 @@ class WecomCSClient(): 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: @@ -325,19 +323,18 @@ class WecomCSClient(): 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_customer_service_api/wecomcsevent.py b/libs/wecom_customer_service_api/wecomcsevent.py index 8dc0e30d..ee830a73 100644 --- a/libs/wecom_customer_service_api/wecomcsevent.py +++ b/libs/wecom_customer_service_api/wecomcsevent.py @@ -9,7 +9,7 @@ class WecomCSEvent(dict): """ @staticmethod - def from_payload(payload: Dict[str, Any]) -> Optional["WecomCSEvent"]: + def from_payload(payload: Dict[str, Any]) -> Optional['WecomCSEvent']: """ 从企业微信(客服会话)事件数据构造 `WecomEvent` 对象。 @@ -21,7 +21,7 @@ class WecomCSEvent(dict): """ try: event = WecomCSEvent(payload) - _ = event.type, + _ = (event.type,) return event except KeyError: return None @@ -34,8 +34,8 @@ class WecomCSEvent(dict): Returns: str: 事件类型。 """ - return self.get("msgtype", "") - + return self.get('msgtype', '') + @property def user_id(self) -> Optional[str]: """ @@ -44,7 +44,7 @@ class WecomCSEvent(dict): Returns: Optional[str]: 用户 ID。 """ - return self.get("external_userid") + return self.get('external_userid') @property def receiver_id(self) -> Optional[str]: @@ -54,8 +54,8 @@ class WecomCSEvent(dict): Returns: Optional[str]: 接收者 ID。 """ - return self.get("open_kfid","") - + return self.get('open_kfid', '') + @property def picurl(self) -> Optional[str]: """ @@ -65,7 +65,7 @@ class WecomCSEvent(dict): Optional[str]: 图片 URL。 """ - return self.get("picurl","") + return self.get('picurl', '') @property def message_id(self) -> Optional[str]: @@ -75,7 +75,7 @@ class WecomCSEvent(dict): Returns: Optional[str]: 消息 ID。 """ - return self.get("msgid") + return self.get('msgid') @property def message(self) -> Optional[str]: @@ -85,12 +85,11 @@ class WecomCSEvent(dict): Returns: Optional[str]: 消息内容。 """ - if self.get("msgtype") == 'text': - return self.get("text").get("content","") + if self.get('msgtype') == 'text': + return self.get('text').get('content', '') else: return None - @property def timestamp(self) -> Optional[int]: """ @@ -99,8 +98,7 @@ class WecomCSEvent(dict): Returns: Optional[int]: 时间戳。 """ - return self.get("send_time") - + return self.get('send_time') def __getattr__(self, key: str) -> Optional[Any]: """ @@ -131,4 +129,4 @@ class WecomCSEvent(dict): Returns: str: 字符串表示。 """ - return f"" + return f'' diff --git a/main.py b/main.py index c297c2a4..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,17 +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 # 检查配置文件 @@ -49,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) @@ -63,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字符串 @@ -75,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 5a6ab97e..ce366539 100644 --- a/pkg/api/http/controller/group.py +++ b/pkg/api/http/controller/group.py @@ -4,6 +4,7 @@ import abc import typing import enum import quart +import traceback from quart.typing import RouteCallable from ....core import app @@ -12,6 +13,7 @@ from ....core import app preregistered_groups: list[type[RouterGroup]] = [] """RouterGroup 的预注册列表""" + def group_class(name: str, path: str) -> None: """注册一个 RouterGroup""" @@ -26,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 @@ -48,14 +50,19 @@ 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 ', '') @@ -66,6 +73,11 @@ class RouterGroup(abc.ABC): try: user_email = await self.ap.user_service.verify_jwt_token(token) + # check if this account exists + user = await self.ap.user_service.get_user_by_email(user_email) + if not user: + return self.http_status(401, -1, '用户不存在') + # 检查f是否接受user_email参数 if 'user_email' in f.__code__.co_varnames: kwargs['user_email'] = user_email @@ -74,9 +86,11 @@ class RouterGroup(abc.ABC): try: return await f(*args, **kwargs) - except Exception as e: # 自动 500 - return self.http_status(500, -2, str(e)) - + 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__ @@ -88,20 +102,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..e3bff9db 100644 --- a/pkg/api/http/controller/groups/logs.py +++ b/pkg/api/http/controller/groups/logs.py @@ -1,32 +1,27 @@ 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 + 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 new file mode 100644 index 00000000..1a8036cc --- /dev/null +++ b/pkg/api/http/controller/groups/pipelines.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +import quart + +from .. import group + + +@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()}) + elif quart.request.method == 'POST': + json_data = await quart.request.json + + pipeline_uuid = await self.ap.pipeline_service.create_pipeline(json_data) + + 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()}) + + @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}) + elif quart.request.method == 'PUT': + json_data = await quart.request.json + + await self.ap.pipeline_service.update_pipeline(pipeline_uuid, json_data) + + return self.success() + elif quart.request.method == 'DELETE': + await self.ap.pipeline_service.delete_pipeline(pipeline_uuid) + + return self.success() diff --git a/pkg/audit/center/__init__.py b/pkg/api/http/controller/groups/platform/__init__.py similarity index 100% rename from pkg/audit/center/__init__.py rename to pkg/api/http/controller/groups/platform/__init__.py diff --git a/pkg/api/http/controller/groups/platform/adapters.py b/pkg/api/http/controller/groups/platform/adapters.py new file mode 100644 index 00000000..4136791c --- /dev/null +++ b/pkg/api/http/controller/groups/platform/adapters.py @@ -0,0 +1,34 @@ +import quart + +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()}) + + @self.route('/', methods=['GET']) + async def _(adapter_name: str) -> str: + 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'], 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') + + icon_path = adapter_manifest.icon_rel_path + + if icon_path is None: + return self.http_status(404, -1, 'icon not found') + + 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 new file mode 100644 index 00000000..af248fac --- /dev/null +++ b/pkg/api/http/controller/groups/platform/bots.py @@ -0,0 +1,31 @@ +import quart + +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()}) + 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}) + + @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}) + 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() diff --git a/pkg/api/http/controller/groups/plugins.py b/pkg/api/http/controller/groups/plugins.py index 00951550..daf6ea7d 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,66 +16,94 @@ 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 - ) - return self.success(data={ - 'task_id': wrapper.id - }) - - @self.route('//', methods=['DELETE'], 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.uninstall_plugin(plugin_name, task_context=ctx), - kind="plugin-operation", - name=f'plugin-remove-{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}) - 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()}) + 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', + name=f'plugin-remove-{plugin_name}', + label=f'删除插件 {plugin_name}', + context=ctx, + ) + + 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}) + elif quart.request.method == 'PUT': + data = await quart.request.json + + await self.ap.plugin_mgr.set_plugin_config(plugin, data) + + return self.success(data={}) @self.route('/reorder', methods=['PUT'], auth_type=group.AuthType.USER_TOKEN) async def _() -> str: 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) 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/audit/center/groups/__init__.py b/pkg/api/http/controller/groups/provider/__init__.py similarity index 100% rename from pkg/audit/center/groups/__init__.py rename to pkg/api/http/controller/groups/provider/__init__.py diff --git a/pkg/api/http/controller/groups/provider/models.py b/pkg/api/http/controller/groups/provider/models.py new file mode 100644 index 00000000..683fac01 --- /dev/null +++ b/pkg/api/http/controller/groups/provider/models.py @@ -0,0 +1,38 @@ +import quart + +from ... import group + + +@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()}) + 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}) + + @self.route('/', methods=['GET', 'PUT', 'DELETE']) + async def _(model_uuid: str) -> str: + if quart.request.method == 'GET': + model = await self.ap.model_service.get_llm_model(model_uuid) + + if model is None: + return self.http_status(404, -1, 'model not found') + + return self.success(data={'model': model}) + elif quart.request.method == 'PUT': + json_data = await quart.request.json + + await self.ap.model_service.update_llm_model(model_uuid, json_data) + + return self.success() + elif quart.request.method == 'DELETE': + await self.ap.model_service.delete_llm_model(model_uuid) + + return self.success() diff --git a/pkg/api/http/controller/groups/provider/requesters.py b/pkg/api/http/controller/groups/provider/requesters.py new file mode 100644 index 00000000..0f999288 --- /dev/null +++ b/pkg/api/http/controller/groups/provider/requesters.py @@ -0,0 +1,34 @@ +import quart + +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()}) + + @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) + + 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'], 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) + + if requester_manifest is None: + return self.http_status(404, -1, 'requester not found') + + icon_path = requester_manifest.icon_rel_path + + if icon_path is None: + return self.http_status(404, -1, 'icon not found') + + return await quart.send_file(icon_path) diff --git a/pkg/api/http/controller/groups/settings.py b/pkg/api/http/controller/groups/settings.py deleted file mode 100644 index 835d86ad..00000000 --- a/pkg/api/http/controller/groups/settings.py +++ /dev/null @@ -1,62 +0,0 @@ -import quart - -from .....core import app -from .. import group - - -@group.group_class('settings', '/api/v1/settings') -class SettingsRouterGroup(group.RouterGroup): - - async def initialize(self) -> None: - - @self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN) - async def _() -> str: - return self.success( - data={ - "managers": [ - { - "name": m.name, - "description": m.description, - } - for m in self.ap.settings_mgr.get_manager_list() - ] - } - ) - - @self.route('/', methods=['GET'], auth_type=group.AuthType.USER_TOKEN) - async def _(manager_name: str) -> str: - - manager = self.ap.settings_mgr.get_manager(manager_name) - - if manager is None: - return self.fail(1, '配置管理器不存在') - - return self.success( - data={ - "manager": { - "name": manager.name, - "description": manager.description, - "schema": manager.schema, - "file": manager.file.config_file_name, - "data": manager.data, - "doc_link": manager.doc_link - } - } - ) - - @self.route('//data', methods=['PUT'], auth_type=group.AuthType.USER_TOKEN) - async def _(manager_name: str) -> str: - data = await quart.request.json - manager = self.ap.settings_mgr.get_manager(manager_name) - - if manager is None: - return self.fail(code=1, msg='配置管理器不存在') - - # manager.data = data['data'] - for k, v in data['data'].items(): - manager.data[k] = v - - await manager.dump_config() - return self.success(data={ - "data": manager.data - }) diff --git a/pkg/api/http/controller/groups/stats.py b/pkg/api/http/controller/groups/stats.py index 43d56f27..8c8e9113 100644 --- a/pkg/api/http/controller/groups/stats.py +++ b/pkg/api/http/controller/groups/stats.py @@ -1,23 +1,19 @@ -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 []) - 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 71d0d8df..c4cab602 100644 --- a/pkg/api/http/controller/groups/system.py +++ b/pkg/api/http/controller/groups/system.py @@ -1,63 +1,56 @@ 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.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) - ) - + 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) 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 ce8e7448..498efaa4 100644 --- a/pkg/api/http/controller/groups/user.py +++ b/pkg/api/http/controller/groups/user.py @@ -1,22 +1,17 @@ import quart -import sqlalchemy import argon2 from .. import group -from .....persistence.entities 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,7 +23,7 @@ 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 @@ -38,10 +33,10 @@ class UserRouterGroup(group.RouterGroup): 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']) - async def _() -> str: - return self.success() + @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}) diff --git a/pkg/api/http/controller/main.py b/pkg/api/http/controller/main.py index 70a1a546..3c8097b8 100644 --- a/pkg/api/http/controller/main.py +++ b/pkg/api/http/controller/main.py @@ -7,12 +7,19 @@ import quart import quart_cors from ....core import app, entities as core_entities -from .groups import logs, system, settings, plugins, stats, user +from ....utils import importutil + +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 @@ -20,13 +27,13 @@ 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() async def run(self) -> None: - if self.ap.system_cfg.data["http-api"]["enable"]: + if True: async def shutdown_trigger_placeholder(): while True: @@ -34,74 +41,70 @@ 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( - host=self.ap.system_cfg.data["http-api"]["host"], - port=self.ap.system_cfg.data["http-api"]["port"], + host='0.0.0.0', + 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", - mimetype="text/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): + 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" + 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 - ) \ No newline at end of file + 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 new file mode 100644 index 00000000..e562a310 --- /dev/null +++ b/pkg/api/http/service/bot.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +import uuid +import sqlalchemy + +from ....core import app +from ....entity.persistence import bot as persistence_bot +from ....entity.persistence import pipeline as persistence_pipeline + + +class BotService: + """机器人服务""" + + ap: app.Application + + def __init__(self, ap: app.Application) -> None: + self.ap = ap + + async def get_bots(self) -> list[dict]: + """获取所有机器人""" + result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_bot.Bot)) + + bots = result.all() + + return [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) + ) + + bot = result.first() + + if bot is None: + return None + + return self.ap.persistence_mgr.serialize_model(persistence_bot.Bot, bot) + + async def create_bot(self, bot_data: dict) -> str: + """创建机器人""" + # TODO: 检查配置信息格式 + bot_data['uuid'] = str(uuid.uuid4()) + + # checkout the default pipeline + result = await self.ap.persistence_mgr.execute_async( + sqlalchemy.select(persistence_pipeline.LegacyPipeline).where( + persistence_pipeline.LegacyPipeline.is_default == True + ) + ) + pipeline = result.first() + if pipeline is not None: + bot_data['use_pipeline_uuid'] = pipeline.uuid + bot_data['use_pipeline_name'] = pipeline.name + + await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_bot.Bot).values(bot_data)) + + 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: + """更新机器人""" + if 'uuid' in bot_data: + del bot_data['uuid'] + + # 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'] + ) + ) + pipeline = result.first() + if pipeline is not None: + bot_data['use_pipeline_name'] = pipeline.name + else: + 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) + ) + await self.ap.platform_mgr.remove_bot(bot_uuid) + + # select from db + bot = await self.get_bot(bot_uuid) + + runtime_bot = await self.ap.platform_mgr.load_bot(bot) + + if runtime_bot.enable: + await runtime_bot.run() + + async def delete_bot(self, bot_uuid: str) -> None: + """删除机器人""" + 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) + ) diff --git a/pkg/api/http/service/model.py b/pkg/api/http/service/model.py new file mode 100644 index 00000000..080abb9d --- /dev/null +++ b/pkg/api/http/service/model.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +import uuid +import sqlalchemy + +from ....core import app +from ....entity.persistence import model as persistence_model +from ....entity.persistence import pipeline as persistence_pipeline + + +class ModelsService: + ap: app.Application + + def __init__(self, ap: app.Application) -> None: + self.ap = ap + + async def get_llm_models(self) -> list[dict]: + result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_model.LLMModel)) + + models = result.all() + return [self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, model) for model in models] + + 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)) + + llm_model = await self.get_llm_model(model_data['uuid']) + + await self.ap.model_mgr.load_llm_model(llm_model) + + # 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 + ) + ) + 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) + + 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) + ) + + model = result.first() + + if model is None: + return None + + 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) + ) + + await self.ap.model_mgr.remove_llm_model(model_uuid) + + llm_model = await self.get_llm_model(model_uuid) + + await self.ap.model_mgr.load_llm_model(llm_model) + + 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) + ) + + 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 new file mode 100644 index 00000000..ee648db0 --- /dev/null +++ b/pkg/api/http/service/pipeline.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +import uuid +import json +import sqlalchemy + +from ....core import app +from ....entity.persistence import pipeline as persistence_pipeline + + +default_stage_order = [ + '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, + ] + + 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) + 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 + ) + ) + + pipeline = result.first() + + if pipeline is None: + return None + + 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')) + + await self.ap.persistence_mgr.execute_async( + sqlalchemy.insert(persistence_pipeline.LegacyPipeline).values(**pipeline_data) + ) + + pipeline = await self.get_pipeline(pipeline_data['uuid']) + + await self.ap.pipeline_mgr.load_pipeline(pipeline) + + return pipeline_data['uuid'] + + async def update_pipeline(self, pipeline_uuid: str, pipeline_data: dict) -> None: + if 'uuid' in pipeline_data: + del pipeline_data['uuid'] + if 'for_version' in pipeline_data: + del pipeline_data['for_version'] + if 'stages' in pipeline_data: + del pipeline_data['stages'] + if 'is_default' in pipeline_data: + 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) + ) + + await self.ap.pipeline_mgr.remove_pipeline(pipeline_uuid) + + pipeline = await self.get_pipeline(pipeline_uuid) + + await self.ap.pipeline_mgr.load_pipeline(pipeline) + + 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 + ) + ) + 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 93774778..782aad75 100644 --- a/pkg/api/http/service/user.py +++ b/pkg/api/http/service/user.py @@ -6,37 +6,39 @@ import jwt import datetime from ....core import app -from ....persistence.entities import user +from ....entity.persistence import user from ....utils import constants class UserService: - ap: app.Application def __init__(self, ap: app.Application) -> None: self.ap = ap async def is_initialized(self) -> bool: - result = await self.ap.persistence_mgr.execute_async( - sqlalchemy.select(user.User).limit(1) - ) + result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(user.User).limit(1)) 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() hashed_password = ph.hash(password) await self.ap.persistence_mgr.execute_async( - sqlalchemy.insert(user.User).values( - user=user_email, - password=hashed_password - ) + sqlalchemy.insert(user.User).values(user=user_email, password=hashed_password) ) + async def get_user_by_email(self, user_email: str) -> user.User | None: + result = await self.ap.persistence_mgr.execute_async( + sqlalchemy.select(user.User).where(user.User.user == user_email) + ) + + result_list = result.all() + return result_list[0] if result_list is not None and len(result_list) > 0 else None + async def authenticate(self, user_email: str, password: str) -> str | None: result = await self.ap.persistence_mgr.execute_async( sqlalchemy.select(user.User).where(user.User.user == user_email) @@ -56,18 +58,18 @@ class UserService: return await self.generate_jwt_token(user_email) async def generate_jwt_token(self, user_email: str) -> str: - jwt_secret = self.ap.instance_secret_meta.data['jwt_secret'] - jwt_expire = self.ap.system_cfg.data['http-api']['jwt-expire'] + jwt_secret = self.ap.instance_config.data['system']['jwt']['secret'] + jwt_expire = self.ap.instance_config.data['system']['jwt']['expire'] 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_secret_meta.data['jwt_secret'] + jwt_secret = self.ap.instance_config.data['system']['jwt']['secret'] return jwt.decode(token, jwt_secret, algorithms=['HS256'])['user'] diff --git a/pkg/audit/__init__.py b/pkg/audit/__init__.py deleted file mode 100644 index c1a8353b..00000000 --- a/pkg/audit/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -审计相关操作 -""" \ No newline at end of file diff --git a/pkg/audit/center/apigroup.py b/pkg/audit/center/apigroup.py deleted file mode 100644 index 4b20a09a..00000000 --- a/pkg/audit/center/apigroup.py +++ /dev/null @@ -1,88 +0,0 @@ -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 - - -class APIGroup(metaclass=abc.ABCMeta): - """API 组抽象类""" - - _basic_info: dict = None - _runtime_info: dict = None - - prefix = None - - ap: app.Application - - def __init__(self, prefix: str, ap: app.Application): - self.prefix = prefix - self.ap = ap - - async def _do( - self, - method: str, - path: str, - data: dict = None, - params: dict = None, - headers: dict = {}, - **kwargs, - ): - """ - 执行请求 - """ - self._runtime_info["account_id"] = "-1" - - url = self.prefix + path - data = json.dumps(data) - 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()) - - except Exception as e: - self.ap.logger.debug(f"上报失败: {e}") - - async def do( - self, - method: str, - path: str, - data: dict = None, - params: dict = None, - headers: dict = {}, - **kwargs, - ) -> asyncio.Task: - """执行请求""" - - return self.ap.task_mgr.create_task( - self._do(method, path, data, params, headers, **kwargs), - kind="telemetry-operation", - name=f"{method} {path}", - scopes=[core_entities.LifecycleControlScope.APPLICATION], - ).task - - def gen_rid(self): - """生成一个请求 ID""" - return str(uuid.uuid4()) - - def basic_info(self): - """获取基本信息""" - basic_info = APIGroup._basic_info.copy() - basic_info["rid"] = self.gen_rid() - return basic_info - - def runtime_info(self): - """获取运行时信息""" - return APIGroup._runtime_info diff --git a/pkg/audit/center/groups/main.py b/pkg/audit/center/groups/main.py deleted file mode 100644 index 3a31a65b..00000000 --- a/pkg/audit/center/groups/main.py +++ /dev/null @@ -1,55 +0,0 @@ -from __future__ import annotations - -from .. import apigroup -from ....core import app - - -class V2MainDataAPI(apigroup.APIGroup): - """主程序相关 数据API""" - - def __init__(self, prefix: str, ap: app.Application): - self.ap = ap - super().__init__(prefix+"/main", ap) - - async def do(self, *args, **kwargs): - if not self.ap.system_cfg.data['report-usage']: - return None - return await super().do(*args, **kwargs) - - async def post_update_record( - self, - spent_seconds: int, - infer_reason: str, - old_version: str, - new_version: str, - ): - """提交更新记录""" - return await self.do( - "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, - } - } - ) - - async def post_announcement_showed( - self, - ids: list[int], - ): - """提交公告已阅""" - return await self.do( - "POST", - "/announcement", - data={ - "basic": self.basic_info(), - "announcement_info": { - "ids": ids, - } - } - ) diff --git a/pkg/audit/center/groups/plugin.py b/pkg/audit/center/groups/plugin.py deleted file mode 100644 index 627b116c..00000000 --- a/pkg/audit/center/groups/plugin.py +++ /dev/null @@ -1,65 +0,0 @@ -from __future__ import annotations - -from ....core import app -from .. import apigroup - - -class V2PluginDataAPI(apigroup.APIGroup): - """插件数据相关 API""" - - def __init__(self, prefix: str, ap: app.Application): - self.ap = ap - super().__init__(prefix+"/plugin", ap) - - async def do(self, *args, **kwargs): - if not self.ap.system_cfg.data['report-usage']: - return None - return await super().do(*args, **kwargs) - - async def post_install_record( - self, - plugin: dict - ): - """提交插件安装记录""" - return await self.do( - "POST", - "/install", - data={ - "basic": self.basic_info(), - "plugin": plugin, - } - ) - - async def post_remove_record( - self, - plugin: dict - ): - """提交插件卸载记录""" - return await self.do( - "POST", - "/remove", - data={ - "basic": self.basic_info(), - "plugin": plugin, - } - ) - - async def post_update_record( - self, - plugin: dict, - old_version: str, - new_version: str, - ): - """提交插件更新记录""" - return await self.do( - "POST", - "/update", - data={ - "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 deleted file mode 100644 index 8a8bdf04..00000000 --- a/pkg/audit/center/groups/usage.py +++ /dev/null @@ -1,88 +0,0 @@ -from __future__ import annotations - -from .. import apigroup -from ....core import app - - -class V2UsageDataAPI(apigroup.APIGroup): - """使用量数据相关 API""" - - def __init__(self, prefix: str, ap: app.Application): - self.ap = ap - super().__init__(prefix+"/usage", ap) - - async def do(self, *args, **kwargs): - if not self.ap.system_cfg.data['report-usage']: - return None - return await super().do(*args, **kwargs) - - async def post_query_record( - self, - session_type: str, - session_id: str, - query_ability_provider: str, - usage: int, - model_name: str, - response_seconds: int, - retry_times: int, - ): - """提交请求记录""" - return await self.do( - "POST", - "/query", - data={ - "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, - } - } - ) - - async def post_event_record( - self, - plugins: list[dict], - event_name: str, - ): - """提交事件触发记录""" - return await self.do( - "POST", - "/event", - data={ - "basic": self.basic_info(), - "runtime": self.runtime_info(), - "plugins": plugins, - "event_info": { - "name": event_name, - } - } - ) - - async def post_function_record( - self, - plugin: dict, - function_name: str, - function_description: str, - ): - """提交内容函数使用记录""" - return await self.do( - "POST", - "/function", - data={ - "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 deleted file mode 100644 index 234e6d22..00000000 --- a/pkg/audit/center/v2.py +++ /dev/null @@ -1,35 +0,0 @@ -from __future__ import annotations - -import logging - -from . import apigroup -from .groups import main -from .groups import usage -from .groups import plugin -from ...core import app - - -class V2CenterAPI: - """中央服务器 v2 API 交互类""" - - main: main.V2MainDataAPI = None - """主 API 组""" - - usage: usage.V2UsageDataAPI = None - """使用量 API 组""" - - plugin: plugin.V2PluginDataAPI = None - """插件 API 组""" - - 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) - - 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 deleted file mode 100644 index 3e2ec57d..00000000 --- a/pkg/audit/identifier.py +++ /dev/null @@ -1,85 +0,0 @@ -# 实例 识别码 控制 - -import os -import uuid -import json -import time - - -identifier = { - 'host_id': '', - 'instance_id': '', - 'host_create_ts': 0, - 'instance_create_ts': 0, -} - -HOST_ID_FILE = os.path.expanduser('~/.langbot/host_id.json') -INSTANCE_ID_FILE = 'data/labels/instance_id.json' - -def init(): - global identifier - - if not os.path.exists(os.path.expanduser('~/.langbot')): - os.mkdir(os.path.expanduser('~/.langbot')) - - if not os.path.exists(HOST_ID_FILE): - 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) - - identifier['host_id'] = new_host_id - identifier['host_create_ts'] = new_host_create_ts - else: - loaded_host_id = '' - loaded_host_create_ts = 0 - - with open(HOST_ID_FILE, 'r') as f: - file_content = json.load(f) - loaded_host_id = file_content['host_id'] - loaded_host_create_ts = file_content['host_create_ts'] - - identifier['host_id'] = loaded_host_id - identifier['host_create_ts'] = loaded_host_create_ts - - # 检查实例 id - if os.path.exists(INSTANCE_ID_FILE): - instance_id = {} - with open(INSTANCE_ID_FILE, 'r') as f: - instance_id = json.load(f) - - 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_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) - - identifier['instance_id'] = new_instance_id - identifier['instance_create_ts'] = new_instance_create_ts - else: - loaded_instance_id = '' - loaded_instance_create_ts = 0 - - with open(INSTANCE_ID_FILE, 'r') as f: - file_content = json.load(f) - loaded_instance_id = file_content['instance_id'] - loaded_instance_create_ts = file_content['instance_create_ts'] - - 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 8d442fdb..1bd03fcf 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, default, 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,22 +26,21 @@ 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, []) # 应用命令权限配置 for cls in operator.preregistered_operators: - if cls.path in self.ap.command_cfg.data['privilege']: - cls.lowest_privilege = self.ap.command_cfg.data['privilege'][cls.path] + if cls.path in self.ap.instance_config.data['command']['privilege']: + cls.lowest_privilege = self.ap.instance_config.data['command']['privilege'][cls.path] # 实例化所有类 self.cmd_list = [cls(self.ap) for cls in operator.preregistered_operators] @@ -58,57 +57,46 @@ 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 if not found: # 如果下一个参数未在此节点的子节点中找到,则执行此节点或者报错 if operator is None: - yield entities.CommandReturn( - error=errors.CommandNotFoundError(context.crt_params[0]) - ) + yield entities.CommandReturn(error=errors.CommandNotFoundError(context.crt_params[0])) else: if operator.lowest_privilege > context.privilege: - yield entities.CommandReturn( - error=errors.CommandPrivilegeError(operator.name) - ) + yield entities.CommandReturn(error=errors.CommandPrivilegeError(operator.name)) else: 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.system_cfg.data['admin-sessions']: + if f'{query.launcher_type.value}_{query.launcher_id}' in self.ap.instance_config.data['admins']: privilege = 2 ctx = entities.ExecuteContext( @@ -119,11 +107,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..9ee3de37 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 @@ -95,15 +95,12 @@ class CommandOperator(metaclass=abc.ABCMeta): pass @abc.abstractmethod - async def execute( - self, - context: entities.ExecuteContext - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: + async def execute(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..f5a69a7b 100644 --- a/pkg/command/operators/cmd.py +++ b/pkg/command/operators/cmd.py @@ -2,35 +2,26 @@ 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 - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - """执行 - """ + async def execute(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] @@ -44,7 +35,7 @@ class CmdOperator(operator.CommandOperator): if cmd is None: 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/default.py b/pkg/command/operators/default.py deleted file mode 100644 index ee46c7d0..00000000 --- a/pkg/command/operators/default.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import annotations - -import typing -import traceback - -from .. import operator, entities, cmdmgr, errors - - -@operator.operator_class( - name="default", - help="操作情景预设", - usage='!default\n!default set <指定情景预设为默认>' -) -class DefaultOperator(operator.CommandOperator): - - async def execute( - self, - context: entities.ExecuteContext - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - - reply_str = "当前所有情景预设: \n\n" - - for prompt in self.ap.prompt_mgr.get_all_prompts(): - - content = "" - for msg in prompt.messages: - content += f" {msg.readable_str()}\n" - - reply_str += f"名称: {prompt.name}\n内容: \n{content}\n\n" - - reply_str += f"当前会话使用的是: {context.session.use_prompt_name}" - - yield entities.CommandReturn(text=reply_str.strip()) - - -@operator.operator_class( - name="set", - help="设置当前会话默认情景预设", - parent_class=DefaultOperator -) -class DefaultSetOperator(operator.CommandOperator): - - async def execute( - self, - context: entities.ExecuteContext - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - - if len(context.crt_params) == 0: - yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供情景预设名称')) - else: - prompt_name = context.crt_params[0] - - try: - prompt = await self.ap.prompt_mgr.get_prompt_by_prefix(prompt_name) - if prompt is None: - yield entities.CommandReturn(error=errors.CommandError("设置当前会话默认情景预设失败: 未找到情景预设 {}".format(prompt_name))) - else: - context.session.use_prompt_name = prompt.name - yield entities.CommandReturn(text=f"已设置当前会话默认情景预设为 {prompt_name}, !reset 后生效") - except Exception as e: - traceback.print_exc() - yield entities.CommandReturn(error=errors.CommandError("设置当前会话默认情景预设失败: "+str(e))) diff --git a/pkg/command/operators/delc.py b/pkg/command/operators/delc.py index db865ff7..7e72ff3c 100644 --- a/pkg/command/operators/delc.py +++ b/pkg/command/operators/delc.py @@ -1,62 +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="del", - help="删除当前会话的历史记录", - usage='!del <序号>\n!del all' -) +@operator.operator_class(name='del', help='删除当前会话的历史记录', usage='!del <序号>\n!del all') class DelOperator(operator.CommandOperator): - - async def execute( - self, - context: entities.ExecuteContext - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - + async def execute(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: + 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 + 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('当前没有对话')) -@operator.operator_class( - name="all", - help="删除此会话的所有历史记录", - parent_class=DelOperator -) +@operator.operator_class(name='all', help='删除此会话的所有历史记录', parent_class=DelOperator) class DelAllOperator(operator.CommandOperator): - - async def execute( - self, - context: entities.ExecuteContext - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - + async def execute(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..648cc5e2 100644 --- a/pkg/command/operators/func.py +++ b/pkg/command/operators/func.py @@ -1,16 +1,13 @@ 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" + async def execute(self, context: entities.ExecuteContext) -> AsyncGenerator[entities.CommandReturn, None]: + reply_str = '当前已启用的内容函数: \n\n' index = 1 @@ -19,7 +16,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 570e103c..91ad66dc 100644 --- a/pkg/command/operators/help.py +++ b/pkg/command/operators/help.py @@ -2,21 +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 - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - help = self.ap.system_cfg.data['help-message'] + async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + help = 'LangBot - 大语言模型原生即时通信机器人平台\n链接:https://langbot.app' help += '\n发送命令 !cmd 可查看命令列表' diff --git a/pkg/command/operators/last.py b/pkg/command/operators/last.py index e7a14c83..25b1fc6a 100644 --- a/pkg/command/operators/last.py +++ b/pkg/command/operators/last.py @@ -1,36 +1,28 @@ 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 - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - + async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: if context.session.conversations: # 找到当前会话的上一个会话 - for index in range(len(context.session.conversations)-1, -1, -1): + 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('已经是第一个对话了')) 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..70ff3945 100644 --- a/pkg/command/operators/list.py +++ b/pkg/command/operators/list.py @@ -1,29 +1,19 @@ 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 <页码>' -) +@operator.operator_class(name='list', help='列出此会话中的所有历史对话', usage='!list\n!list <页码>') class ListOperator(operator.CommandOperator): - - async def execute( - self, - context: entities.ExecuteContext - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - + async def execute(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: + page = int(context.crt_params[0] - 1) + except Exception: yield entities.CommandReturn(error=errors.CommandOperationError('页码应为整数')) return @@ -36,21 +26,23 @@ 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 deleted file mode 100644 index 692e2728..00000000 --- a/pkg/command/operators/model.py +++ /dev/null @@ -1,86 +0,0 @@ -from __future__ import annotations - -import typing - -from .. import operator, entities, cmdmgr, errors - -@operator.operator_class( - name="model", - help='显示和切换模型列表', - usage='!model\n!model show <模型名>\n!model set <模型名>', - privilege=2 -) -class ModelOperator(operator.CommandOperator): - """Model命令""" - - 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当前对话使用模型: {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 -) -class ModelShowOperator(operator.CommandOperator): - """Model Show命令""" - - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: - model_name = context.crt_params[0] - - model = None - for _model in self.ap.model_mgr.model_list: - if model_name == _model.name: - model = _model - break - - if model is None: - yield entities.CommandReturn(error=errors.CommandError(f"未找到模型 {model_name}")) - else: - content = f"模型详情\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.provider}\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 -) -class ModelSetOperator(operator.CommandOperator): - """Model Set命令""" - - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: - model_name = context.crt_params[0] - - model = None - for _model in self.ap.model_mgr.model_list: - if model_name == _model.name: - model = _model - break - - if model is None: - 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},重置会话以生效") diff --git a/pkg/command/operators/next.py b/pkg/command/operators/next.py index 8f4b5a5a..938c8331 100644 --- a/pkg/command/operators/next.py +++ b/pkg/command/operators/next.py @@ -1,35 +1,27 @@ 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 - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - + async def execute(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: + 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 deleted file mode 100644 index f5ed382d..00000000 --- a/pkg/command/operators/ollama.py +++ /dev/null @@ -1,121 +0,0 @@ -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 <模型名>" -) -class OllamaOperator(operator.CommandOperator): - async def execute( - 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 服务正常")) - - -def bytes_to_mb(num_bytes): - mb: float = num_bytes / 1024 / 1024 - return format(mb, '.2f') - - -@operator.operator_class( - name="show", - help="ollama模型详情", - privilege=2, - parent_class=OllamaOperator -) -class OllamaShowOperator(operator.CommandOperator): - async def execute( - self, context: entities.ExecuteContext - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - content: str = '模型详情:\n' - try: - show: dict = ollama.show(model=context.crt_params[0]) - model_info: dict = show.get('model_info', {}) - ignore_show: str = 'too long to show...' - - for key in ['license', 'modelfile']: - show[key] = ignore_show - - 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 服务正常")) - -@operator.operator_class( - name="pull", - help="ollama模型拉取", - privilege=2, - parent_class=OllamaOperator -) -class OllamaPullOperator(operator.CommandOperator): - async def execute( - 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="模型已存在") - return - except ollama.ResponseError as e: - yield entities.CommandReturn(error=errors.CommandError(f"无法获取模型列表,请确认 Ollama 服务正常")) - return - - on_progress: bool = False - progress_count: int = 0 - try: - for resp in ollama.pull(model=context.crt_params[0], stream=True): - total: typing.Any = resp.get('total') - if not on_progress: - if total is not None: - on_progress = True - yield entities.CommandReturn(text=resp.get('status')) - else: - if total is None: - on_progress = False - - completed: typing.Any = resp.get('completed') - if isinstance(completed, int) and isinstance(total, int): - percentage_completed = (completed / total) * 100 - if percentage_completed > progress_count: - progress_count += 10 - yield entities.CommandReturn( - text=f"下载进度: {completed}/{total} ({percentage_completed:.2f}%)") - except ollama.ResponseError as e: - yield entities.CommandReturn(text=f"拉取失败: {e.error}") - - -@operator.operator_class( - name="del", - help="ollama模型删除", - privilege=2, - parent_class=OllamaOperator -) -class OllamaDelOperator(operator.CommandOperator): - async def execute( - 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}" - yield entities.CommandReturn(text=ret) diff --git a/pkg/command/operators/plugin.py b/pkg/command/operators/plugin.py index e50d0ba2..40ec0e3a 100644 --- a/pkg/command/operators/plugin.py +++ b/pkg/command/operators/plugin.py @@ -2,80 +2,55 @@ 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 - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - + async def execute(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) - - # TODO 从元数据调远程地址 + 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 yield entities.CommandReturn(text=reply_str) -@operator.operator_class( - name="get", - help="安装插件", - privilege=2, - parent_class=PluginOperator -) +@operator.operator_class(name='get', help='安装插件', privilege=2, parent_class=PluginOperator) class PluginGetOperator(operator.CommandOperator): - - async def execute( - self, - context: entities.ExecuteContext - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - + async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: if len(context.crt_params) == 0: 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 -) +@operator.operator_class(name='update', help='更新插件', privilege=2, parent_class=PluginOperator) class PluginUpdateOperator(operator.CommandOperator): - - async def execute( - self, - context: entities.ExecuteContext - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - + async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: if len(context.crt_params) == 0: yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称')) else: @@ -85,36 +60,24 @@ 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 -) + +@operator.operator_class(name='all', help='更新所有插件', privilege=2, parent_class=PluginUpdateOperator) class PluginUpdateAllOperator(operator.CommandOperator): - - async def execute( - self, - context: entities.ExecuteContext - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - + async def execute(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: @@ -122,28 +85,18 @@ 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 -) +@operator.operator_class(name='del', help='删除插件', privilege=2, parent_class=PluginOperator) class PluginDelOperator(operator.CommandOperator): - - async def execute( - self, - context: entities.ExecuteContext - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - + async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: if len(context.crt_params) == 0: yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称')) else: @@ -153,29 +106,19 @@ 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 -) +@operator.operator_class(name='on', help='启用插件', privilege=2, parent_class=PluginOperator) class PluginEnableOperator(operator.CommandOperator): - - async def execute( - self, - context: entities.ExecuteContext - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - + async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: if len(context.crt_params) == 0: yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称')) else: @@ -183,27 +126,19 @@ class PluginEnableOperator(operator.CommandOperator): 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 -) +@operator.operator_class(name='off', help='禁用插件', privilege=2, parent_class=PluginOperator) class PluginDisableOperator(operator.CommandOperator): - - async def execute( - self, - context: entities.ExecuteContext - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - + async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: if len(context.crt_params) == 0: yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称')) else: @@ -211,9 +146,11 @@ class PluginDisableOperator(operator.CommandOperator): 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..fdcba2bd 100644 --- a/pkg/command/operators/prompt.py +++ b/pkg/command/operators/prompt.py @@ -2,28 +2,19 @@ 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 - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - """执行 - """ + async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + """执行""" if context.session.using_conversation is None: 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..39789fef 100644 --- a/pkg/command/operators/resend.py +++ b/pkg/command/operators/resend.py @@ -2,26 +2,18 @@ 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' -) +@operator.operator_class(name='resend', help='重发当前会话的最后一条消息', usage='!resend') class ResendOperator(operator.CommandOperator): - - async def execute( - self, - context: entities.ExecuteContext - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: + async def execute(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 +23,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..008143a1 100644 --- a/pkg/command/operators/reset.py +++ b/pkg/command/operators/reset.py @@ -2,22 +2,13 @@ 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 - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - """执行 - """ + async def execute(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..29b8f560 100644 --- a/pkg/command/operators/update.py +++ b/pkg/command/operators/update.py @@ -1,30 +1,11 @@ from __future__ import annotations import typing -import traceback -from .. import operator, entities, cmdmgr, errors +from .. import operator, entities -@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 - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - - try: - yield entities.CommandReturn(text="正在进行更新...") - if await self.ap.ver_mgr.update_all(): - yield entities.CommandReturn(text="更新完成,请重启程序以应用更新") - else: - 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 + async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + yield entities.CommandReturn(text='不再支持通过命令更新,请查看 LangBot 文档。') diff --git a/pkg/command/operators/version.py b/pkg/command/operators/version.py index a5d7a81b..200875aa 100644 --- a/pkg/command/operators/version.py +++ b/pkg/command/operators/version.py @@ -2,26 +2,18 @@ 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 - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - reply_str = f"当前版本: \n{self.ap.ver_mgr.get_current_version()}" + async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + 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..c2e6bdf4 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,9 @@ 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 +52,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 +60,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 +82,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 +99,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/config/settings.py b/pkg/config/settings.py deleted file mode 100644 index 1f21e926..00000000 --- a/pkg/config/settings.py +++ /dev/null @@ -1,75 +0,0 @@ -from __future__ import annotations - -from . import manager as config_manager -from ..core import app - - -class SettingsManager: - """设置管理器 - 保存、管理多个配置文件管理器 - """ - - ap: app.Application - - managers: list[config_manager.ConfigManager] = [] - """配置文件管理器列表""" - - def __init__(self, ap: app.Application) -> None: - self.ap = ap - self.managers = [] - - async def initialize(self) -> None: - pass - - def register_manager( - self, - name: str, - description: str, - manager: config_manager.ConfigManager, - schema: dict=None, - doc_link: str=None, - ) -> None: - """注册配置管理器 - - Args: - name (str): 配置管理器名 - description (str): 配置管理器描述 - manager (ConfigManager): 配置管理器 - schema (dict): 配置文件 schema,符合 JSON Schema Draft 7 规范 - """ - - for m in self.managers: - if m.name == name: - raise ValueError(f'配置管理器名 {name} 已存在') - - manager.name = name - manager.description = description - manager.schema = schema - manager.doc_link = doc_link - self.managers.append(manager) - - def get_manager(self, name: str) -> config_manager.ConfigManager | None: - """获取配置管理器 - - Args: - name (str): 配置管理器名 - - Returns: - ConfigManager: 配置管理器 - """ - - for m in self.managers: - if m.name == name: - return m - - return None - - def get_manager_list(self) -> list[config_manager.ConfigManager]: - """获取配置管理器列表 - - Returns: - list[ConfigManager]: 配置管理器列表 - """ - - return self.managers - diff --git a/pkg/core/app.py b/pkg/core/app.py index 8fd36d63..ad1d45c7 100644 --- a/pkg/core/app.py +++ b/pkg/core/app.py @@ -2,34 +2,30 @@ from __future__ import annotations import logging import asyncio -import threading import traceback -import enum import sys import os -from ..platform import manager as im_mgr +from ..platform import botmgr as im_mgr from ..provider.session import sessionmgr as llm_session_mgr from ..provider.modelmgr import modelmgr as llm_model_mgr -from ..provider.sysprompt import sysprompt as llm_prompt_mgr from ..provider.tools import toolmgr as llm_tool_mgr -from ..provider import runnermgr from ..config import manager as config_mgr -from ..config import settings as settings_mgr -from ..audit.center import v2 as center_mgr from ..command import cmdmgr from ..plugin import manager as plugin_mgr from ..pipeline import pool -from ..pipeline import controller, stagemgr +from ..pipeline import controller, pipelinemgr from ..utils import version as version_mgr, proxy as proxy_mgr, announce as announce_mgr from ..persistence import mgr as persistencemgr from ..api.http.controller import main as http_controller from ..api.http.service import user as user_service +from ..api.http.service import model as model_service +from ..api.http.service import pipeline as pipeline_service +from ..api.http.service import bot as bot_service 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: @@ -50,49 +46,41 @@ class Application: model_mgr: llm_model_mgr.ModelManager = None - prompt_mgr: llm_prompt_mgr.PromptManager = None - + # TODO 移动到 pipeline 里 tool_mgr: llm_tool_mgr.ToolManager = None - runner_mgr: runnermgr.RunnerManager = None - - settings_mgr: settings_mgr.SettingsManager = None - # ======= 配置管理器 ======= - command_cfg: config_mgr.ConfigManager = None + command_cfg: config_mgr.ConfigManager = None # deprecated - pipeline_cfg: config_mgr.ConfigManager = None + pipeline_cfg: config_mgr.ConfigManager = None # deprecated - platform_cfg: config_mgr.ConfigManager = None + platform_cfg: config_mgr.ConfigManager = None # deprecated - provider_cfg: config_mgr.ConfigManager = None + provider_cfg: config_mgr.ConfigManager = None # deprecated - system_cfg: config_mgr.ConfigManager = None + system_cfg: config_mgr.ConfigManager = None # deprecated + + instance_config: config_mgr.ConfigManager = None # ======= 元数据配置管理器 ======= sensitive_meta: config_mgr.ConfigManager = None - adapter_qq_botpy_meta: config_mgr.ConfigManager = None - - plugin_setting_meta: config_mgr.ConfigManager = None - - llm_models_meta: config_mgr.ConfigManager = None - - instance_secret_meta: config_mgr.ConfigManager = None + pipeline_config_meta_trigger: config_mgr.ConfigManager = None + pipeline_config_meta_safety: config_mgr.ConfigManager = None + pipeline_config_meta_ai: config_mgr.ConfigManager = None + pipeline_config_meta_output: config_mgr.ConfigManager = None # ========================= - ctr_mgr: center_mgr.V2CenterAPI = None - plugin_mgr: plugin_mgr.PluginManager = None query_pool: pool.QueryPool = None ctrl: controller.Controller = None - stage_mgr: stagemgr.StageManager = None + pipeline_mgr: pipelinemgr.PipelineManager = None ver_mgr: version_mgr.VersionManager = None @@ -112,6 +100,12 @@ class Application: user_service: user_service.UserService = None + model_service: model_service.ModelsService = None + + pipeline_service: pipeline_service.PipelineService = None + + bot_service: bot_service.BotService = None + def __init__(self): pass @@ -121,37 +115,57 @@ 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() - port = self.system_cfg.data['http-api']['port'] + port = self.instance_config.data['api']['port'] tips = f""" ======================================= @@ -168,7 +182,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( @@ -177,21 +191,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) @@ -202,12 +223,10 @@ 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() - latest_llm_model_config = await config.load_json_config("data/metadata/llm-models.json", "templates/metadata/llm-models.json") - self.llm_models_meta = latest_llm_model_config llm_model_mgr_inst = llm_model_mgr.ModelManager(self) await llm_model_mgr_inst.initialize() self.model_mgr = llm_model_mgr_inst @@ -216,16 +235,8 @@ class Application: await llm_session_mgr_inst.initialize() self.sess_mgr = llm_session_mgr_inst - llm_prompt_mgr_inst = llm_prompt_mgr.PromptManager(self) - await llm_prompt_mgr_inst.initialize() - self.prompt_mgr = llm_prompt_mgr_inst - llm_tool_mgr_inst = llm_tool_mgr.ToolManager(self) await llm_tool_mgr_inst.initialize() self.tool_mgr = llm_tool_mgr_inst - - runner_mgr_inst = runnermgr.RunnerManager(self) - await runner_mgr_inst.initialize() - self.runner_mgr = runner_mgr_inst case _: - pass \ No newline at end of file + pass diff --git a/pkg/core/boot.py b/pkg/core/boot.py index e6a0e3eb..b8c5a974 100644 --- a/pkg/core/boot.py +++ b/pkg/core/boot.py @@ -5,30 +5,28 @@ import asyncio 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 +from . import stages + +importutil.import_modules_in_pkg(stages) stage_order = [ - "LoadConfigStage", - "MigrationStage", - "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() @@ -49,21 +47,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 940a6132..cea4af45 100644 --- a/pkg/core/bootutils/config.py +++ b/pkg/core/bootutils/config.py @@ -1,10 +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 diff --git a/pkg/core/bootutils/deps.py b/pkg/core/bootutils/deps.py index 71639d08..cf41a7b2 100644 --- a/pkg/core/bootutils/deps.py +++ b/pkg/core/bootutils/deps.py @@ -1,41 +1,45 @@ import pip +import os +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", - "telegramify_markdown":"telegramify-markdown", + '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', + 'telegramify_markdown': 'telegramify-markdown', + 'slack_sdk': 'slack_sdk', } @@ -50,8 +54,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...') + + # 只有在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 f52f7b09..3599e41b 100644 --- a/pkg/core/bootutils/files.py +++ b/pkg/core/bootutils/files.py @@ -2,32 +2,23 @@ from __future__ import annotations import os import shutil -import sys required_files = { - "plugins/__init__.py": "templates/__init__.py", - "plugins/plugins.json": "templates/plugin-settings.json", - "data/config/command.json": "templates/command.json", - "data/config/pipeline.json": "templates/pipeline.json", - "data/config/platform.json": "templates/platform.json", - "data/config/provider.json": "templates/provider.json", - "data/config/system.json": "templates/system.json", - "data/scenario/default.json": "templates/scenario-template.json", + 'plugins/__init__.py': 'templates/__init__.py', + 'data/config.yaml': 'templates/config.yaml', } required_paths = [ - "temp", - "data", - "data/metadata", - "data/prompts", - "data/scenario", - "data/logs", - "data/config", - "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 62359dec..eb6806fa 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,17 +26,15 @@ 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("qcg") + 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, ) @@ -46,7 +43,10 @@ async def init_logging(extra_handlers: list[logging.Handler] = None) -> logging. # stream_handler.setFormatter(color_formatter) 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 +54,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 71ec995b..e2ea3d45 100644 --- a/pkg/core/entities.py +++ b/pkg/core/entities.py @@ -8,22 +8,18 @@ import asyncio import pydantic.v1 as pydantic from ..provider import entities as llm_entities -from ..provider.modelmgr import entities -from ..provider.sysprompt import entities as sysprompt_entities +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): @@ -57,6 +53,15 @@ class Query(pydantic.BaseModel): message_chain: platform_message.MessageChain """消息链,platform收到的原始消息链""" + bot_uuid: typing.Optional[str] = None + """机器人UUID。""" + + pipeline_uuid: typing.Optional[str] = None + """流水线UUID。""" + + pipeline_config: typing.Optional[dict[str, typing.Any]] = None + """流水线配置,由 Pipeline 在运行开始时设置。""" + adapter: msadapter.MessagePlatformAdapter """消息平台适配器对象,单个app中可能启用了多个消息平台适配器,此对象表明发起此query的适配器""" @@ -66,7 +71,7 @@ class Query(pydantic.BaseModel): messages: typing.Optional[list[llm_entities.Message]] = [] """历史消息列表,由前置处理器阶段设置""" - prompt: typing.Optional[sysprompt_entities.Prompt] = None + prompt: typing.Optional[llm_entities.Prompt] = None """情景预设内容,由前置处理器阶段设置""" user_message: typing.Optional[llm_entities.Message] = None @@ -75,20 +80,22 @@ class Query(pydantic.BaseModel): variables: typing.Optional[dict[str, typing.Any]] = None """变量,由前置处理器阶段设置。在prompt中嵌入或由 Runner 传递到 LLMOps 平台。""" - use_model: typing.Optional[entities.LLMModelInfo] = None - """使用的模型,由前置处理器阶段设置""" + use_llm_model: typing.Optional[requester.RuntimeLLMModel] = None + """使用的对话模型,由前置处理器阶段设置""" 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.stagemgr.StageInstContainer" = None + current_stage: typing.Optional['pkg.pipeline.pipelinemgr.StageInstContainer'] = None """当前所处阶段""" class Config: @@ -101,13 +108,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: @@ -118,7 +125,7 @@ class Query(pydantic.BaseModel): class Conversation(pydantic.BaseModel): """对话,包含于 Session 中,一个 Session 可以有多个历史 Conversation,但只有一个当前使用的 Conversation""" - prompt: sysprompt_entities.Prompt + prompt: llm_entities.Prompt messages: list[llm_entities.Message] @@ -126,16 +133,20 @@ class Conversation(pydantic.BaseModel): update_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now) - use_model: entities.LLMModelInfo + use_llm_model: requester.RuntimeLLMModel use_funcs: typing.Optional[list[tools_entities.LLMFunction]] uuid: typing.Optional[str] = None """该对话的 uuid,在创建时不会自动生成。而是当使用 Dify API 等由外部管理对话信息的服务时,用于绑定外部的会话。具体如何使用,取决于 Runner。""" + class Config: + arbitrary_types_allowed = True + class Session(pydantic.BaseModel): """会话,一个 Session 对应一个 {launcher_type.value}_{launcher_id}""" + launcher_type: LauncherTypes launcher_id: typing.Union[int, str] 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..35cb076f 100644 --- a/pkg/core/migrations/m001_sensitive_word_migration.py +++ b/pkg/core/migrations/m001_sensitive_word_migration.py @@ -1,26 +1,24 @@ 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..9a35370c 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: @@ -35,7 +32,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 +41,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..2783e079 100644 --- a/pkg/core/migrations/m007_qcg_center_url.py +++ b/pkg/core/migrations/m007_qcg_center_url.py @@ -3,18 +3,18 @@ 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..964e819b 100644 --- a/pkg/core/migrations/m008_ad_fixwin_config_migrate.py +++ b/pkg/core/migrations/m008_ad_fixwin_config_migrate.py @@ -3,27 +3,23 @@ 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 - ) + return isinstance(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..80e7b74f 100644 --- a/pkg/core/migrations/m013_http_api_config.py +++ b/pkg/core/migrations/m013_http_api_config.py @@ -3,29 +3,27 @@ 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..67635fb5 100644 --- a/pkg/core/migrations/m017_dify_api_timeout_params.py +++ b/pkg/core/migrations/m017_dify_api_timeout_params.py @@ -3,22 +3,25 @@ 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..31b9ee8e 100644 --- a/pkg/core/migrations/m023_siliconflow_config.py +++ b/pkg/core/migrations/m023_siliconflow_config.py @@ -3,25 +3,23 @@ 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'] 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..7269765a 100644 --- a/pkg/core/migrations/m033_dify_thinking_config.py +++ b/pkg/core/migrations/m033_dify_thinking_config.py @@ -3,24 +3,22 @@ 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..512b75b1 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,6 @@ 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/migrations/m038_tg_dingtalk_markdown.py b/pkg/core/migrations/m038_tg_dingtalk_markdown.py index 1123c6b2..c0a85a44 100644 --- a/pkg/core/migrations/m038_tg_dingtalk_markdown.py +++ b/pkg/core/migrations/m038_tg_dingtalk_markdown.py @@ -3,24 +3,23 @@ from __future__ import annotations from .. import migration -@migration.migration_class("tg-dingtalk-markdown", 38) +@migration.migration_class('tg-dingtalk-markdown', 38) class TgDingtalkMarkdownMigration(migration.Migration): """迁移""" async def need_migrate(self) -> bool: """判断当前环境是否需要运行此迁移""" - + for adapter in self.ap.platform_cfg.data['platform-adapters']: - if adapter['adapter'] in ['dingtalk','telegram']: + if adapter['adapter'] in ['dingtalk', 'telegram']: if 'markdown_card' not in adapter: return True return False - + async def run(self): """执行迁移""" for adapter in self.ap.platform_cfg.data['platform-adapters']: - if adapter['adapter'] in ['dingtalk','telegram']: + if adapter['adapter'] in ['dingtalk', 'telegram']: if 'markdown_card' not in adapter: adapter['markdown_card'] = False await self.ap.platform_cfg.dump_config() - \ No newline at end of file diff --git a/pkg/core/migrations/m039_modelscope_cfg_completion.py b/pkg/core/migrations/m039_modelscope_cfg_completion.py index 8e574911..9eec0344 100644 --- a/pkg/core/migrations/m039_modelscope_cfg_completion.py +++ b/pkg/core/migrations/m039_modelscope_cfg_completion.py @@ -3,20 +3,19 @@ from __future__ import annotations from .. import migration -@migration.migration_class("modelscope-config-completion", 39) +@migration.migration_class('modelscope-config-completion', 39) class ModelScopeConfigCompletionMigration(migration.Migration): - """ModelScope配置迁移 - """ + """ModelScope配置迁移""" async def need_migrate(self) -> bool: - """判断当前环境是否需要运行此迁移 - """ - return 'modelscope-chat-completions' not in self.ap.provider_cfg.data['requester'] \ + """判断当前环境是否需要运行此迁移""" + return ( + 'modelscope-chat-completions' not in self.ap.provider_cfg.data['requester'] or 'modelscope' not in self.ap.provider_cfg.data['keys'] + ) async def run(self): - """执行迁移 - """ + """执行迁移""" if 'modelscope-chat-completions' not in self.ap.provider_cfg.data['requester']: self.ap.provider_cfg.data['requester']['modelscope-chat-completions'] = { 'base-url': 'https://api-inference.modelscope.cn/v1', diff --git a/pkg/core/migrations/m040_ppio_config.py b/pkg/core/migrations/m040_ppio_config.py index cd218d87..d4d82b98 100644 --- a/pkg/core/migrations/m040_ppio_config.py +++ b/pkg/core/migrations/m040_ppio_config.py @@ -3,20 +3,19 @@ from __future__ import annotations from .. import migration -@migration.migration_class("ppio-config", 40) +@migration.migration_class('ppio-config', 40) class PPIOConfigMigration(migration.Migration): - """PPIO配置迁移 - """ + """PPIO配置迁移""" async def need_migrate(self) -> bool: - """判断当前环境是否需要运行此迁移 - """ - return 'ppio-chat-completions' not in self.ap.provider_cfg.data['requester'] \ + """判断当前环境是否需要运行此迁移""" + return ( + 'ppio-chat-completions' not in self.ap.provider_cfg.data['requester'] or 'ppio' not in self.ap.provider_cfg.data['keys'] + ) async def run(self): - """执行迁移 - """ + """执行迁移""" if 'ppio-chat-completions' not in self.ap.provider_cfg.data['requester']: self.ap.provider_cfg.data['requester']['ppio-chat-completions'] = { 'base-url': 'https://api.ppinfra.com/v3/openai', 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 6eed21d6..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""" async def need_show(self) -> bool: return True async def yield_note(self) -> typing.AsyncGenerator[typing.Tuple[str, int], None]: - - yield f"当前版本:{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 e4fd0ea5..2cce2bc5 100644 --- a/pkg/core/stages/build_app.py +++ b/pkg/core/stages/build_app.py @@ -1,71 +1,46 @@ from __future__ import annotations -import sys from .. import stage, app -from ...utils import version, proxy, announce, platform -from ...audit.center import v2 as center_v2 -from ...audit import identifier -from ...pipeline import pool, controller, stagemgr +from ...utils import version, proxy, announce +from ...pipeline import pool, controller, pipelinemgr from ...plugin import manager as plugin_mgr from ...command import cmdmgr from ...provider.session import sessionmgr as llm_session_mgr from ...provider.modelmgr import modelmgr as llm_model_mgr -from ...provider.sysprompt import sysprompt as llm_prompt_mgr from ...provider.tools import toolmgr as llm_tool_mgr -from ...provider import runnermgr -from ...platform import manager as im_mgr +from ...platform import botmgr as im_mgr from ...persistence import mgr as persistencemgr from ...api.http.controller import main as http_controller from ...api.http.service import user as user_service +from ...api.http.service import model as model_service +from ...api.http.service import pipeline as pipeline_service +from ...api.http.service import bot as bot_service from ...discover import engine as discover_engine 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 - center_v2_api = center_v2.V2CenterAPI( - ap, - backend_url=ap.system_cfg.data["qcg-center-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(), - }, - runtime_info={ - "admin_id": "{}".format(ap.system_cfg.data["admin-sessions"]), - "msg_source": str([ - adapter_cfg['adapter'] if 'adapter' in adapter_cfg else 'unknown' - for adapter_cfg in ap.platform_cfg.data['platform-adapters'] if adapter_cfg['enable'] - ]), - }, - ) - ap.ctr_mgr = center_v2_api - # 发送公告 ann_mgr = announce.AnnouncementManager(ap) ap.ann_mgr = ann_mgr @@ -76,8 +51,8 @@ class BuildAppStage(stage.BootingStage): ap.log_cache = log_cache persistence_mgr_inst = persistencemgr.PersistenceManager(ap) - await persistence_mgr_inst.initialize() ap.persistence_mgr = persistence_mgr_inst + await persistence_mgr_inst.initialize() plugin_mgr_inst = plugin_mgr.PluginManager(ap) await plugin_mgr_inst.initialize() @@ -96,25 +71,17 @@ class BuildAppStage(stage.BootingStage): await llm_session_mgr_inst.initialize() ap.sess_mgr = llm_session_mgr_inst - llm_prompt_mgr_inst = llm_prompt_mgr.PromptManager(ap) - await llm_prompt_mgr_inst.initialize() - ap.prompt_mgr = llm_prompt_mgr_inst - llm_tool_mgr_inst = llm_tool_mgr.ToolManager(ap) await llm_tool_mgr_inst.initialize() ap.tool_mgr = llm_tool_mgr_inst - runner_mgr_inst = runnermgr.RunnerManager(ap) - await runner_mgr_inst.initialize() - ap.runner_mgr = runner_mgr_inst - im_mgr_inst = im_mgr.PlatformManager(ap=ap) await im_mgr_inst.initialize() ap.platform_mgr = im_mgr_inst - stage_mgr = stagemgr.StageManager(ap) - await stage_mgr.initialize() - ap.stage_mgr = stage_mgr + pipeline_mgr = pipelinemgr.PipelineManager(ap) + await pipeline_mgr.initialize() + ap.pipeline_mgr = pipeline_mgr http_ctrl = http_controller.HTTPController(ap) await http_ctrl.initialize() @@ -123,5 +90,14 @@ class BuildAppStage(stage.BootingStage): user_service_inst = user_service.UserService(ap) ap.user_service = user_service_inst + model_service_inst = model_service.ModelsService(ap) + ap.model_service = model_service_inst + + pipeline_service_inst = pipeline_service.PipelineService(ap) + ap.pipeline_service = pipeline_service_inst + + bot_service_inst = bot_service.BotService(ap) + ap.bot_service = bot_service_inst + ctrl = controller.Controller(ap) ap.ctrl = ctrl diff --git a/pkg/core/stages/genkeys.py b/pkg/core/stages/genkeys.py new file mode 100644 index 00000000..c24ebd70 --- /dev/null +++ b/pkg/core/stages/genkeys.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +import secrets + +from .. import stage, app + + +@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 cc154a7c..ef5f611b 100644 --- a/pkg/core/stages/load_config.py +++ b/pkg/core/stages/load_config.py @@ -1,84 +1,79 @@ from __future__ import annotations -import secrets +import os from .. import stage, app from ..bootutils import config -from ...config import settings as settings_mgr -from ...utils import schema -@stage.stage_class("LoadConfigStage") +@stage.stage_class('LoadConfigStage') class LoadConfigStage(stage.BootingStage): - """加载配置文件阶段 - """ + """加载配置文件阶段""" async def run(self, ap: app.Application): - """启动 - """ - - ap.settings_mgr = settings_mgr.SettingsManager(ap) - await ap.settings_mgr.initialize() + """启动""" - ap.command_cfg = await config.load_json_config("data/config/command.json", "templates/command.json", completion=False) - ap.pipeline_cfg = await config.load_json_config("data/config/pipeline.json", "templates/pipeline.json", completion=False) - ap.platform_cfg = await config.load_json_config("data/config/platform.json", "templates/platform.json", completion=False) - ap.provider_cfg = await config.load_json_config("data/config/provider.json", "templates/provider.json", completion=False) - ap.system_cfg = await config.load_json_config("data/config/system.json", "templates/system.json", completion=False) + # ======= 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, + ) - ap.settings_mgr.register_manager( - name="command.json", - description="命令配置", - manager=ap.command_cfg, - schema=schema.CONFIG_COMMAND_SCHEMA, - doc_link="https://docs.langbot.app/config/function/command.html" + 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/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, + ) + + # ======= deprecated ======= + + ap.instance_config = await config.load_yaml_config( + 'data/config.yaml', 'templates/config.yaml', completion=False ) + await ap.instance_config.dump_config() - ap.settings_mgr.register_manager( - name="pipeline.json", - description="消息处理流水线配置", - manager=ap.pipeline_cfg, - schema=schema.CONFIG_PIPELINE_SCHEMA, - doc_link="https://docs.langbot.app/config/function/pipeline.html" + ap.sensitive_meta = await config.load_json_config( + 'data/metadata/sensitive-words.json', + 'templates/metadata/sensitive-words.json', ) - - ap.settings_mgr.register_manager( - name="platform.json", - description="消息平台配置", - manager=ap.platform_cfg, - schema=schema.CONFIG_PLATFORM_SCHEMA, - doc_link="https://docs.langbot.app/config/function/platform.html" - ) - - ap.settings_mgr.register_manager( - name="provider.json", - description="大模型能力配置", - manager=ap.provider_cfg, - schema=schema.CONFIG_PROVIDER_SCHEMA, - doc_link="https://docs.langbot.app/config/function/provider.html" - ) - - ap.settings_mgr.register_manager( - name="system.json", - description="系统配置", - manager=ap.system_cfg, - schema=schema.CONFIG_SYSTEM_SCHEMA, - doc_link="https://docs.langbot.app/config/function/system.html" - ) - - ap.plugin_setting_meta = await config.load_json_config("plugins/plugins.json", "templates/plugin-settings.json") - await ap.plugin_setting_meta.dump_config() - - 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.adapter_qq_botpy_meta = await config.load_json_config("data/metadata/adapter-qq-botpy.json", "templates/metadata/adapter-qq-botpy.json") - await ap.adapter_qq_botpy_meta.dump_config() - - ap.llm_models_meta = await config.load_json_config("data/metadata/llm-models.json", "templates/metadata/llm-models.json") - await ap.llm_models_meta.dump_config() - - 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() + 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 8d4366bf..02b03256 100644 --- a/pkg/core/stages/migrate.py +++ b/pkg/core/stages/migrate.py @@ -1,28 +1,31 @@ from __future__ import annotations -import importlib 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, m038_tg_dingtalk_markdown, m039_modelscope_cfg_completion, m040_ppio_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 + 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 2c029c03..0f756118 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 @@ -36,17 +35,15 @@ class TaskContext: if action is not None: self.set_current_action(action) - self._log( - f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')} | {self.current_action} | {msg}" - ) + self._log(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 +66,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,10 +97,10 @@ 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], ): @@ -115,7 +112,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 +122,44 @@ 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,10 +180,10 @@ 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], ) -> TaskWrapper: @@ -196,13 +194,13 @@ class AsyncTaskManager: 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], ) -> 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 +213,10 @@ class AsyncTaskManager: type: str = None, ) -> dict: return { - "tasks": [ - t.to_dict() for t in self.tasks if type is None or t.task_type == type - ], - "id_index": TaskWrapper._id_index, + 'tasks': [t.to_dict() for t in self.tasks if type is None or t.task_type == type], + '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,7 +225,12 @@ 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): + for wrapper in self.tasks: + if wrapper.id == task_id: + if not wrapper.task.done(): + wrapper.task.cancel() + return diff --git a/pkg/discover/engine.py b/pkg/discover/engine.py index 297e6515..2224ba48 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 yaml import pydantic @@ -23,6 +21,17 @@ class I18nString(pydantic.BaseModel): ja_JP: typing.Optional[str] = None """日文""" + def to_dict(self) -> dict: + """转换为字典""" + dic = {} + if self.en_US is not None: + dic['en_US'] = self.en_US + if self.zh_CN is not None: + dic['zh_CN'] = self.zh_CN + if self.ja_JP is not None: + dic['ja_JP'] = self.ja_JP + return dic + class Metadata(pydantic.BaseModel): """元数据""" @@ -36,9 +45,27 @@ class Metadata(pydantic.BaseModel): description: typing.Optional[I18nString] = None """描述""" + version: typing.Optional[str] = None + """版本""" + icon: typing.Optional[str] = None """图标""" + author: typing.Optional[str] = None + """作者""" + + repository: typing.Optional[str] = None + """仓库""" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + if self.description is None: + self.description = I18nString(en_US='') + + if self.icon is None: + self.icon = '' + class PythonExecution(pydantic.BaseModel): """Python执行""" @@ -75,6 +102,9 @@ class Component(pydantic.BaseModel): rel_path: str """组件清单相对main.py的路径""" + rel_dir: str + """组件清单相对main.py的目录""" + _metadata: Metadata """组件元数据""" @@ -88,42 +118,66 @@ class Component(pydantic.BaseModel): super().__init__( owner=owner, manifest=manifest, - rel_path=rel_path + rel_path=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 + @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 + @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 + ) + def get_python_component_class(self) -> typing.Type[typing.Any]: """获取Python组件类""" - parent_path = os.path.dirname(self.rel_path) - module_path = os.path.join(parent_path, self.execution.python.path) + module_path = os.path.join(self.rel_dir, self.execution.python.path) if module_path.endswith('.py'): module_path = module_path[:-3] 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 { + 'name': self.metadata.name, + 'label': self.metadata.label.to_dict(), + 'description': self.metadata.description.to_dict(), + 'icon': self.metadata.icon, + 'spec': self.spec, + } + class ComponentDiscoveryEngine: """组件发现引擎""" @@ -137,64 +191,93 @@ 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: + 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) - comp = Component( - owner=owner, - manifest=manifest, - rel_path=path - ) + if not Component.is_component_manifest(manifest): + return None + 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) -> 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] = [] - for file in os.listdir(path): - if file.endswith('.yaml') or file.endswith('.yml'): - components.append(self.load_component_manifest(os.path.join(path, file), owner, no_save)) + + def recursive_load_component_manifests_in_dir(path: str, depth: int = 1): + 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 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(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: for file in group['fromFiles']: - components.append(self.load_component_manifest(file, owner, no_save)) + comp = self.load_component_manifest(file, owner, no_save) + if comp is not None: + components.append(comp) if 'fromDirs' in group: for dir in group['fromDirs']: path = dir['path'] - # depth = dir['depth'] - components.extend(self.load_component_manifests_in_dir(path, owner, no_save)) + max_depth = dir['maxDepth'] if 'maxDepth' in dir else 1 + 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) + if blueprint_manifest is None: + raise ValueError(f'Invalid blueprint manifest: {blueprint_manifest_path}') assert blueprint_manifest.kind == 'Blueprint', '`Kind` must be `Blueprint`' components: typing.Dict[str, typing.List[Component]] = {} # 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 - def get_components_by_kind(self, kind: str) -> typing.List[Component]: """获取指定类型的组件""" if kind not in self.components: - raise ValueError(f'No components found for kind: {kind}') + return [] return self.components[kind] + + def find_components(self, kind: str, component_list: typing.List[Component]) -> typing.List[Component]: + """查找组件""" + result: typing.List[Component] = [] + for component in component_list: + if component.kind == kind: + result.append(component) + return result diff --git a/pkg/persistence/entities/__init__.py b/pkg/entity/__init__.py similarity index 100% rename from pkg/persistence/entities/__init__.py rename to pkg/entity/__init__.py diff --git a/pkg/provider/sysprompt/__init__.py b/pkg/entity/persistence/__init__.py similarity index 100% rename from pkg/provider/sysprompt/__init__.py rename to pkg/entity/persistence/__init__.py diff --git a/pkg/persistence/entities/base.py b/pkg/entity/persistence/base.py similarity index 100% rename from pkg/persistence/entities/base.py rename to pkg/entity/persistence/base.py diff --git a/pkg/entity/persistence/bot.py b/pkg/entity/persistence/bot.py new file mode 100644 index 00000000..3c08f4ec --- /dev/null +++ b/pkg/entity/persistence/bot.py @@ -0,0 +1,25 @@ +import sqlalchemy + +from .base import Base + + +class Bot(Base): + """机器人""" + + __tablename__ = 'bots' + + 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) + adapter = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) + adapter_config = sqlalchemy.Column(sqlalchemy.JSON, nullable=False) + 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(), + ) diff --git a/pkg/entity/persistence/metadata.py b/pkg/entity/persistence/metadata.py new file mode 100644 index 00000000..d9e03663 --- /dev/null +++ b/pkg/entity/persistence/metadata.py @@ -0,0 +1,20 @@ +import sqlalchemy + +from .base import Base + + +initial_metadata = [ + { + 'key': 'database_version', + 'value': '0', + }, +] + + +class Metadata(Base): + """数据库元数据""" + + __tablename__ = 'metadata' + + key = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True) + value = sqlalchemy.Column(sqlalchemy.String(255)) diff --git a/pkg/entity/persistence/model.py b/pkg/entity/persistence/model.py new file mode 100644 index 00000000..9eb2ccef --- /dev/null +++ b/pkg/entity/persistence/model.py @@ -0,0 +1,25 @@ +import sqlalchemy + +from .base import Base + + +class LLMModel(Base): + """LLM 模型""" + + __tablename__ = 'llm_models' + + 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) + requester = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) + requester_config = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={}) + 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(), + ) diff --git a/pkg/entity/persistence/pipeline.py b/pkg/entity/persistence/pipeline.py new file mode 100644 index 00000000..56e2cae9 --- /dev/null +++ b/pkg/entity/persistence/pipeline.py @@ -0,0 +1,45 @@ +import sqlalchemy + +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(), + ) + for_version = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) + is_default = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=False) + + stages = sqlalchemy.Column(sqlalchemy.JSON, nullable=False) + config = sqlalchemy.Column(sqlalchemy.JSON, nullable=False) + + +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(), + ) + 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 new file mode 100644 index 00000000..30db6bd6 --- /dev/null +++ b/pkg/entity/persistence/plugin.py @@ -0,0 +1,22 @@ +import sqlalchemy + +from .base import Base + + +class PluginSetting(Base): + """插件配置""" + + __tablename__ = 'plugin_settings' + + plugin_author = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True) + plugin_name = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True) + 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(), + ) diff --git a/pkg/persistence/entities/user.py b/pkg/entity/persistence/user.py similarity index 50% rename from pkg/persistence/entities/user.py rename to pkg/entity/persistence/user.py index 55597b4f..04a5b374 100644 --- a/pkg/persistence/entities/user.py +++ b/pkg/entity/persistence/user.py @@ -9,3 +9,10 @@ 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(), + ) 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 14f89092..7b095e61 100644 --- a/pkg/persistence/databases/sqlite.py +++ b/pkg/persistence/databases/sqlite.py @@ -5,9 +5,10 @@ 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: - self.engine = sqlalchemy_asyncio.create_async_engine(f"sqlite+aiosqlite:///{self.ap.system_cfg.data['persistence']['sqlite']['path']}") + sqlite_path = 'data/langbot.db' + 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 0eef4800..f0d7459b 100644 --- a/pkg/persistence/mgr.py +++ b/pkg/persistence/mgr.py @@ -1,15 +1,24 @@ from __future__ import annotations -import asyncio import datetime +import typing +import json +import uuid import sqlalchemy.ext.asyncio as sqlalchemy_asyncio import sqlalchemy -from . import database -from .entities import user, base +from . import database, migration +from ..entity.persistence import base, pipeline, metadata +from ..entity import persistence from ..core import app -from .databases import sqlite +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: @@ -27,7 +36,8 @@ class PersistenceManager: self.meta = base.Base.metadata async def initialize(self): - + self.ap.logger.info('Initializing database...') + for manager in database.preregistered_managers: self.db = manager(self.ap) await self.db.initialize() @@ -35,19 +45,78 @@ class PersistenceManager: await self.create_tables() async def create_tables(self): - # TODO: 对扩展友好 - - # 日志 + # create tables async with self.get_db_engine().connect() as conn: await conn.run_sync(self.meta.create_all) await conn.commit() - async def execute_async( - self, - *args, - **kwargs - ) -> sqlalchemy.engine.cursor.CursorResult: + # ======= write initial data ======= + + # write 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']) + ) + 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')) + + pipeline_data = { + 'uuid': str(uuid.uuid4()), + 'for_version': self.ap.ver_mgr.get_current_version(), + 'stages': pipeline_service.default_stage_order, + 'is_default': True, + 'name': 'ChatPipeline', + 'description': '默认提供的流水线,您配置的机器人、第一个模型将自动绑定到此流水线', + 'config': pipeline_config, + } + + await self.execute_async(sqlalchemy.insert(pipeline.LegacyPipeline).values(pipeline_data)) + # ================================= + + # run migrations + database_version = await self.execute_async( + sqlalchemy.select(metadata.Metadata).where(metadata.Metadata.key == 'database_version') + ) + + database_version = int(database_version.fetchone()[1]) + required_database_version = constants.required_database_version + + if database_version < required_database_version: + migrations = migration.preregistered_db_migrations + migrations.sort(key=lambda x: x.number) + + last_migration_number = database_version + + for migration_cls in migrations: + migration_instance = migration_cls(self.ap) + + 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)}) + ) + 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}.') + + async def execute_async(self, *args, **kwargs) -> sqlalchemy.engine.cursor.CursorResult: async with self.get_db_engine().connect() as conn: result = await conn.execute(*args, **kwargs) await conn.commit() @@ -55,3 +124,11 @@ 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: + return { + 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 new file mode 100644 index 00000000..c191b686 --- /dev/null +++ b/pkg/persistence/migration.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +import typing +import abc + +from ..core import app + + +preregistered_db_migrations: list[typing.Type[DBMigration]] = [] + + +def migration_class(number: int): + """迁移类装饰器""" + + def wrapper(cls: typing.Type[DBMigration]) -> typing.Type[DBMigration]: + cls.number = number + preregistered_db_migrations.append(cls) + return cls + + return wrapper + + +class DBMigration(abc.ABC): + """数据库迁移""" + + number: int + """迁移号""" + + def __init__(self, ap: app.Application): + self.ap = ap + + @abc.abstractmethod + async def upgrade(self): + """升级""" + pass + + @abc.abstractmethod + async def downgrade(self): + """降级""" + pass diff --git a/pkg/provider/sysprompt/loaders/__init__.py b/pkg/persistence/migrations/__init__.py similarity index 100% rename from pkg/provider/sysprompt/loaders/__init__.py rename to pkg/persistence/migrations/__init__.py diff --git a/pkg/persistence/migrations/dbm001_migrate_v3_config.py b/pkg/persistence/migrations/dbm001_migrate_v3_config.py new file mode 100644 index 00000000..a1145527 --- /dev/null +++ b/pkg/persistence/migrations/dbm001_migrate_v3_config.py @@ -0,0 +1,249 @@ +from .. import migration +from copy import deepcopy +import uuid +import os +import sqlalchemy +import shutil + +from ...config import manager as config_manager +from ...entity.persistence import ( + model as persistence_model, + pipeline as persistence_pipeline, + bot as persistence_bot, +) + + +@migration.migration_class(1) +class DBMigrateV3Config(migration.DBMigration): + """从 v3 的配置迁移到 v4 的数据库""" + + async def upgrade(self): + """升级""" + """ + 将 data/config 下的所有配置文件进行迁移。 + 迁移后,之前的配置文件都保存到 data/legacy/config 下。 + 迁移后,data/metadata/ 下的所有配置文件都保存到 data/legacy/metadata 下。 + """ + + if self.ap.provider_cfg is None: + return + + # ======= 迁移模型 ======= + # 只迁移当前选中的模型 + model_name = self.ap.provider_cfg.data.get('model', 'gpt-4o') + + model_requester = 'openai-chat-completions' + model_requester_config = {} + model_api_keys = ['sk-proj-1234567890'] + model_abilities = [] + model_extra_args = {} + + if os.path.exists('data/metadata/llm-models.json'): + _llm_model_meta = await config_manager.load_json_config('data/metadata/llm-models.json', completion=False) + + for item in _llm_model_meta.data.get('list', []): + if item.get('name') == model_name: + if 'model_name' in item: + model_name = item['model_name'] + if 'requester' in item: + model_requester = item['requester'] + if 'token_mgr' in item: + _token_mgr = item['token_mgr'] + + if _token_mgr in self.ap.provider_cfg.data.get('keys', {}): + model_api_keys = self.ap.provider_cfg.data.get('keys', {})[_token_mgr] + + if 'tool_call_supported' in item and item['tool_call_supported']: + model_abilities.append('func_call') + + if 'vision_supported' in item and item['vision_supported']: + model_abilities.append('vision') + + if ( + model_requester in self.ap.provider_cfg.data.get('requester', {}) + and 'args' in self.ap.provider_cfg.data.get('requester', {})[model_requester] + ): + model_extra_args = self.ap.provider_cfg.data.get('requester', {})[model_requester]['args'] + + if model_requester in self.ap.provider_cfg.data.get('requester', {}): + model_requester_config = self.ap.provider_cfg.data.get('requester', {})[model_requester] + model_requester_config = { + 'base_url': model_requester_config['base-url'], + 'timeout': model_requester_config['timeout'], + } + + break + + model_uuid = str(uuid.uuid4()) + + llm_model_data = { + 'uuid': model_uuid, + 'name': model_name, + 'description': '由 LangBot v3 迁移而来', + 'requester': model_requester, + 'requester_config': model_requester_config, + 'api_keys': model_api_keys, + 'abilities': model_abilities, + 'extra_args': model_extra_args, + } + + await self.ap.persistence_mgr.execute_async( + sqlalchemy.insert(persistence_model.LLMModel).values(**llm_model_data) + ) + + # ======= 迁移流水线配置 ======= + # 修改到默认流水线 + default_pipeline = [ + self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline) + for pipeline in ( + await self.ap.persistence_mgr.execute_async( + sqlalchemy.select(persistence_pipeline.LegacyPipeline).where( + persistence_pipeline.LegacyPipeline.is_default == True + ) + ) + ).all() + ][0] + + pipeline_uuid = str(uuid.uuid4()) + pipeline_name = 'ChatPipeline' + + if default_pipeline: + pipeline_name = default_pipeline['name'] + pipeline_uuid = default_pipeline['uuid'] + + pipeline_config = default_pipeline['config'] + + # ai + pipeline_config['ai']['runner'] = { + 'runner': self.ap.provider_cfg.data['runner'], + } + pipeline_config['ai']['local-agent']['model'] = model_uuid + pipeline_config['ai']['local-agent']['max-round'] = self.ap.pipeline_cfg.data['msg-truncate']['round'][ + 'max-round' + ] + + pipeline_config['ai']['local-agent']['prompt'] = [ + { + 'role': 'system', + 'content': self.ap.provider_cfg.data['prompt']['default'], + } + ] + pipeline_config['ai']['dify-service-api'] = { + 'base-url': self.ap.provider_cfg.data['dify-service-api']['base-url'], + 'app-type': self.ap.provider_cfg.data['dify-service-api']['app-type'], + 'api-key': self.ap.provider_cfg.data['dify-service-api'][ + self.ap.provider_cfg.data['dify-service-api']['app-type'] + ]['api-key'], + 'thinking-convert': self.ap.provider_cfg.data['dify-service-api']['options']['convert-thinking-tips'], + 'timeout': self.ap.provider_cfg.data['dify-service-api'][ + self.ap.provider_cfg.data['dify-service-api']['app-type'] + ]['timeout'], + } + pipeline_config['ai']['dashscope-app-api'] = { + 'app-type': self.ap.provider_cfg.data['dashscope-app-api']['app-type'], + 'api-key': self.ap.provider_cfg.data['dashscope-app-api']['api-key'], + 'references_quote': self.ap.provider_cfg.data['dashscope-app-api'][ + self.ap.provider_cfg.data['dashscope-app-api']['app-type'] + ]['references_quote'], + } + + # trigger + pipeline_config['trigger']['group-respond-rules'] = self.ap.pipeline_cfg.data['respond-rules']['default'] + pipeline_config['trigger']['access-control'] = self.ap.pipeline_cfg.data['access-control'] + pipeline_config['trigger']['ignore-rules'] = self.ap.pipeline_cfg.data['ignore-rules'] + + # safety + pipeline_config['safety']['content-filter'] = { + 'scope': 'all', + 'check-sensitive-words': self.ap.pipeline_cfg.data['check-sensitive-words'], + } + pipeline_config['safety']['rate-limit'] = { + 'window-length': self.ap.pipeline_cfg.data['rate-limit']['fixwin']['default']['window-size'], + 'limitation': self.ap.pipeline_cfg.data['rate-limit']['fixwin']['default']['limit'], + 'strategy': self.ap.pipeline_cfg.data['rate-limit']['strategy'], + } + + # output + pipeline_config['output']['long-text-processing'] = self.ap.platform_cfg.data['long-text-process'] + pipeline_config['output']['force-delay'] = self.ap.platform_cfg.data['force-delay'] + pipeline_config['output']['misc'] = { + 'hide-exception': self.ap.platform_cfg.data['hide-exception-info'], + 'quote-origin': self.ap.platform_cfg.data['quote-origin'], + 'at-sender': self.ap.platform_cfg.data['at-sender'], + 'track-function-calls': self.ap.platform_cfg.data['track-function-calls'], + } + + default_pipeline['description'] = default_pipeline['description'] + ' [已迁移 LangBot v3 配置]' + default_pipeline['config'] = pipeline_config + default_pipeline.pop('created_at') + default_pipeline.pop('updated_at') + + await self.ap.persistence_mgr.execute_async( + sqlalchemy.update(persistence_pipeline.LegacyPipeline) + .values(default_pipeline) + .where(persistence_pipeline.LegacyPipeline.uuid == default_pipeline['uuid']) + ) + + # ======= 迁移机器人 ======= + # 只迁移启用的机器人 + for adapter in self.ap.platform_cfg.data.get('platform-adapters', []): + if not adapter.get('enable'): + continue + + args = deepcopy(adapter) + args.pop('adapter') + args.pop('enable') + + bot_data = { + 'uuid': str(uuid.uuid4()), + 'name': adapter.get('adapter'), + 'description': '由 LangBot v3 迁移而来', + 'adapter': adapter.get('adapter'), + 'adapter_config': args, + 'enable': True, + 'use_pipeline_uuid': pipeline_uuid, + 'use_pipeline_name': pipeline_name, + } + + await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_bot.Bot).values(**bot_data)) + + # ======= 迁移系统设置 ======= + self.ap.instance_config.data['admins'] = self.ap.system_cfg.data['admin-sessions'] + self.ap.instance_config.data['api']['port'] = self.ap.system_cfg.data['http-api']['port'] + self.ap.instance_config.data['command'] = { + 'prefix': self.ap.command_cfg.data['command-prefix'], + 'privilege': self.ap.command_cfg.data['privilege'], + } + self.ap.instance_config.data['concurrency']['pipeline'] = self.ap.system_cfg.data['pipeline-concurrency'] + self.ap.instance_config.data['concurrency']['session'] = self.ap.system_cfg.data['session-concurrency'][ + 'default' + ] + self.ap.instance_config.data['mcp'] = self.ap.provider_cfg.data['mcp'] + self.ap.instance_config.data['proxy'] = self.ap.system_cfg.data['network-proxies'] + await self.ap.instance_config.dump_config() + + # ======= move files ======= + # 迁移 data/config 下的所有配置文件 + all_legacy_dir_name = [ + 'config', + # 'metadata', + 'prompts', + 'scenario', + ] + + def move_legacy_files(dir_name: str): + if not os.path.exists(f'data/legacy/{dir_name}'): + os.makedirs(f'data/legacy/{dir_name}') + + if os.path.exists(f'data/{dir_name}'): + for file in os.listdir(f'data/{dir_name}'): + if file.endswith('.json'): + shutil.move(f'data/{dir_name}/{file}', f'data/legacy/{dir_name}/{file}') + + os.rmdir(f'data/{dir_name}') + + for dir_name in all_legacy_dir_name: + move_legacy_files(dir_name) + + async def downgrade(self): + """降级""" diff --git a/pkg/pipeline/bansess/bansess.py b/pkg/pipeline/bansess/bansess.py index 9c041385..3b927a55 100644 --- a/pkg/pipeline/bansess/bansess.py +++ b/pkg/pipeline/bansess/bansess.py @@ -1,42 +1,36 @@ from __future__ import annotations -import re -from .. import stage, entities, stagemgr +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中群号或个人号是否在访问控制列表中。 """ - async def initialize(self): + async def initialize(self, pipeline_config: dict): pass - 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: found = False - mode = self.ap.pipeline_cfg.data['access-control']['mode'] + mode = query.pipeline_config['trigger']['access-control']['mode'] - sess_list = self.ap.pipeline_cfg.data['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': @@ -47,5 +41,5 @@ class BanSessionCheckStage(stage.PipelineStage): return entities.StageProcessResult( 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 f7376b61..879b1295 100644 --- a/pkg/pipeline/cntfilter/cntfilter.py +++ b/pkg/pipeline/cntfilter/cntfilter.py @@ -2,22 +2,23 @@ from __future__ import annotations from ...core import app -from .. import stage, entities, stagemgr +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): """内容过滤阶段 - + 前置: 检查消息是否符合规则,不符合则拦截。 改写: @@ -35,23 +36,21 @@ class ContentFilterStage(stage.PipelineStage): self.filter_chain = [] super().__init__(ap) - async def initialize(self): - + async def initialize(self, pipeline_config: dict): filters_required = [ - "content-ignore", + 'content-ignore', ] - if self.ap.pipeline_cfg.data['check-sensitive-words']: - filters_required.append("ban-word-filter") + if pipeline_config['safety']['content-filter']['check-sensitive-words']: + filters_required.append('ban-word-filter') - if self.ap.pipeline_cfg.data['baidu-cloud-examine']['enable']: - filters_required.append("baidu-cloud-examine") + # TODO revert it + # if self.ap.pipeline_cfg.data['baidu-cloud-examine']['enable']: + # filters_required.append("baidu-cloud-examine") 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() @@ -65,38 +64,30 @@ class ContentFilterStage(stage.PipelineStage): 只要有一个不通过就不放行,只放行 PASS 的消息 """ - if not self.ap.pipeline_cfg.data['income-msg-check']: - return entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query - ) + if query.pipeline_config['safety']['content-filter']['scope'] == 'output-msg': + return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) else: for filter in self.filter_chain: if filter_entities.EnableStage.PRE in filter.enable_stages: - result = await filter.process(message) + result = await filter.process(query, message) 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 - ) - + query.message_chain = platform_message.MessageChain(platform_message.Plain(message)) + + return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) + async def _post_process( self, message: str, @@ -105,46 +96,34 @@ class ContentFilterStage(stage.PipelineStage): """请求llm后处理响应 只要是 PASS 或者 MASKED 的就通过此 filter,将其 replacement 设置为message,进入下一个 filter """ - if message is None: - return entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query - ) + if query.pipeline_config['safety']['content-filter']['scope'] == 'income-msg': + return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) else: message = message.strip() for filter in self.filter_chain: if filter_entities.EnableStage.POST in filter.enable_stages: - result = await filter.process(message) + result = await filter.process(query, message) if result.level == filter_entities.ResultLevel.BLOCK: 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 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 - ) + return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) - 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 stage_inst_name == 'PreContentFilterStage': - contain_non_text = False text_components = [platform_message.Plain, platform_message.Source] @@ -155,28 +134,20 @@ class ContentFilterStage(stage.PipelineStage): break if contain_non_text: - self.ap.logger.debug(f"消息中包含非文本消息,跳过内容过滤器检查。") - return entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query - ) + self.ap.logger.debug('消息中包含非文本消息,跳过内容过滤器检查。') + return entities.StageProcessResult(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 类型,跳过内容过滤器检查。") - return entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query + 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) 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 8eceb877..0a3ceaae 100644 --- a/pkg/pipeline/cntfilter/filter.py +++ b/pkg/pipeline/cntfilter/filter.py @@ -3,16 +3,15 @@ from __future__ import annotations import abc import typing -from ...core import app +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,19 @@ 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, 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 8c5b77cd..9637aec2 100644 --- a/pkg/pipeline/cntfilter/filters/baiduexamine.py +++ b/pkg/pipeline/cntfilter/filters/baiduexamine.py @@ -4,13 +4,14 @@ import aiohttp from .. import entities 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): """百度云内容审核""" @@ -19,44 +20,46 @@ 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, 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 1430c2ed..916a1bc1 100644 --- a/pkg/pipeline/cntfilter/filters/banwords.py +++ b/pkg/pipeline/cntfilter/filters/banwords.py @@ -3,17 +3,17 @@ import re from .. import filter as filter_model from .. import entities -from ....config import manager as cfg_mgr +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, 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,18 +23,17 @@ 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( - match[i], self.ap.sensitive_meta.data['mask_word'] - ) + message = message.replace(match[i], self.ap.sensitive_meta.data['mask_word']) return entities.FilterResult( 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 781f6397..5e410e31 100644 --- a/pkg/pipeline/cntfilter/filters/cntignore.py +++ b/pkg/pipeline/cntfilter/filters/cntignore.py @@ -3,9 +3,10 @@ import re from .. import entities 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): """根据内容忽略消息""" @@ -15,30 +16,30 @@ class ContentIgnore(filter_model.ContentFilter): entities.EnableStage.PRE, ] - async def process(self, message: str) -> entities.FilterResult: - if 'prefix' in self.ap.pipeline_cfg.data['ignore-rules']: - for rule in self.ap.pipeline_cfg.data['ignore-rules']['prefix']: + 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): return entities.FilterResult( level=entities.ResultLevel.BLOCK, replacement='', user_notice='', - console_notice='根据 ignore_rules 中的 prefix 规则,忽略消息' + console_notice='根据 ignore_rules 中的 prefix 规则,忽略消息', ) - - if 'regexp' in self.ap.pipeline_cfg.data['ignore-rules']: - for rule in self.ap.pipeline_cfg.data['ignore-rules']['regexp']: + + if 'regexp' in query.pipeline_config['trigger']['ignore-rules']: + for rule in query.pipeline_config['trigger']['ignore-rules']['regexp']: if re.search(rule, message): return entities.FilterResult( 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 807bec05..052187a2 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,10 @@ class Controller: def __init__(self, ap: app.Application): self.ap = ap - self.semaphore = asyncio.Semaphore(self.ap.system_cfg.data['pipeline-concurrency']) + 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 +30,7 @@ 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 @@ -50,148 +45,40 @@ class Controller: continue if selected_query: - async def _process_query(selected_query): + + async def _process_query(selected_query: entities.Query): async with self.semaphore: # 总并发上限 - await self.process_query(selected_query) - + # 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) + if bot: + 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() # 通知其他协程,有新的请求可以处理了 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()}") - - async def _check_output(self, query: entities.Query, result: pipeline_entities.StageProcessResult): - """检查输出 - """ - if result.user_notice: - # 处理str类型 - - if isinstance(result.user_notice, str): - result.user_notice = platform_message.MessageChain( - platform_message.Plain(result.user_notice) - ) - elif isinstance(result.user_notice, list): - result.user_notice = platform_message.MessageChain( - *result.user_notice - ) - - await self.ap.platform_mgr.send( - query.message_event, - result.user_notice, - query.adapter - ) - if result.debug_notice: - self.ap.logger.debug(result.debug_notice) - if result.console_notice: - self.ap.logger.info(result.console_notice) - if result.error_notice: - self.ap.logger.error(result.error_notice) - - async def _execute_from_stage( - self, - stage_index: int, - query: entities.Query, - ): - """从指定阶段开始执行,实现了责任链模式和基于生成器的阶段分叉功能。 - - 如何看懂这里为什么这么写? - 去问 GPT-4: - Q1: 现在有一个责任链,其中有多个stage,query对象在其中传递,stage.process可能返回Result也有可能返回typing.AsyncGenerator[Result, None], - 如果返回的是生成器,需要挨个生成result,检查是否result中是否要求继续,如果要求继续就进行下一个stage。如果此次生成器产生的result处理完了,就继续生成下一个result, - 调用后续的stage,直到该生成器全部生成完。责任链中可能有多个stage会返回生成器 - Q2: 不是这样的,你可能理解有误。如果我们责任链上有这些Stage: - - A B C D E F G - - 如果所有的stage都返回Result,且所有Result都要求继续,那么执行顺序是: - - A B C D E F G - - 现在假设C返回的是AsyncGenerator,那么执行顺序是: - - A B C D E F G C D E F G C D E F G ... - Q3: 但是如果不止一个stage会返回生成器呢? - """ - i = stage_index - - while i < len(self.ap.stage_mgr.stage_containers): - stage_container = self.ap.stage_mgr.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}") - 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}") - 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") - - async for sub_result in 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}") - break - elif sub_result.result_type == pipeline_entities.ResultType.CONTINUE: - query = sub_result.new_query - await self._execute_from_stage(i + 1, query) - break - - 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_ctx = await self.ap.plugin_mgr.emit_event( - event=event_type( - launcher_type=query.launcher_type.value, - launcher_id=query.launcher_id, - sender_id=query.sender_id, - message_chain=query.message_chain, - query=query - ) - ) - - if event_ctx.is_prevented_default(): - return - - 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()}") - finally: - self.ap.logger.debug(f"Query {query} processed") + 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 ecb745d0..5be20650 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, stagemgr +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): """长消息处理阶段 @@ -23,41 +24,49 @@ class LongTextProcessStage(stage.PipelineStage): strategy_impl: strategy.LongTextStrategy - async def initialize(self): - config = self.ap.platform_cfg.data['long-text-process'] + async def initialize(self, pipeline_config: dict): + config = pipeline_config['output']['long-text-processing'] if config['strategy'] == 'image': use_font = config['font-path'] try: # 检查是否存在 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( + '未找到字体文件,且无法使用系统自带字体,更换为转发消息组件以发送长消息,您可以在配置文件中调整相关设置。' + ) - self.ap.platform_cfg.data['long-text-process']['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 + ) + ) - self.ap.platform_cfg.data['long-text-process']['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: # 检查是否包含非 Plain 组件 contains_non_plain = False @@ -66,13 +75,15 @@ 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])) > self.ap.platform_cfg.data['long-text-process']['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 - ) + 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) + ) + + return entities.StageProcessResult(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..6228d580 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,25 @@ 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]: 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 b9675074..3716e7c2 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 @@ -8,30 +7,34 @@ import re from PIL import Image, ImageDraw, ImageFont +import functools from ....platform.types import message as platform_message 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): - - text_render_font: ImageFont.FreeTypeFont - async def initialize(self): - self.text_render_font = ImageFont.truetype(self.ap.platform_cfg.data['long-text-process']['font-path'], 32, encoding="utf-8") - + 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]: img_path = self.text_to_image( text_str=message, - save_as='temp/{}.png'.format(int(time.time())) + save_as='temp/{}.png'.format(int(time.time())), + query=query, ) - compressed_path, size = self.compress_image( - img_path, - outfile="temp/{}_compressed.png".format(int(time.time())) - ) + compressed_path, size = self.compress_image(img_path, outfile='temp/{}_compressed.png'.format(int(time.time()))) with open(compressed_path, 'rb') as f: img = f.read() @@ -89,13 +92,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 @@ -103,7 +104,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: 压缩源文件 @@ -126,24 +126,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): - - 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.text_render_font.getlength(line) - self.ap.logger.debug("line_width: {}".format(line_width)) + line_width = self.get_font(query).getlength(line) + self.ap.logger.debug('line_width: {}'.format(line_width)) if line_width < text_width: final_lines.append(line) continue @@ -173,13 +177,18 @@ class Text2ImageStrategy(strategy_model.LongTextStrategy): 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: @@ -192,7 +201,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..0ddec0c6 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,10 +44,10 @@ 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]: """处理长文本 diff --git a/pkg/pipeline/msgtrun/msgtrun.py b/pkg/pipeline/msgtrun/msgtrun.py index e56c551f..c64f67fc 100644 --- a/pkg/pipeline/msgtrun/msgtrun.py +++ b/pkg/pipeline/msgtrun/msgtrun.py @@ -1,35 +1,36 @@ from __future__ import annotations -from .. import stage, entities, stagemgr +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): - use_method = self.ap.pipeline_cfg.data['msg-truncate']['method'] + async def initialize(self, pipeline_config: dict): + 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: - """处理 - """ + """处理""" query = await self.trun.truncate(query) - return entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query - ) \ No newline at end of file + return entities.StageProcessResult(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 646f2856..fa72a0e1 100644 --- a/pkg/pipeline/msgtrun/truncators/round.py +++ b/pkg/pipeline/msgtrun/truncators/round.py @@ -4,15 +4,13 @@ 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 = self.ap.pipeline_cfg.data['msg-truncate']['round']['max-round'] + """截断""" + 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 new file mode 100644 index 00000000..b61e34ad --- /dev/null +++ b/pkg/pipeline/pipelinemgr.py @@ -0,0 +1,269 @@ +from __future__ import annotations + +import typing +import traceback + +import sqlalchemy + +from ..core import app, entities +from . import entities as pipeline_entities +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 . 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: + """阶段实例容器""" + + inst_name: str + + inst: stage.PipelineStage + + def __init__(self, inst_name: str, inst: stage.PipelineStage): + self.inst_name = inst_name + self.inst = inst + + +class RuntimePipeline: + """运行时流水线""" + + 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 + + async def run(self, query: entities.Query): + query.pipeline_config = self.pipeline_entity.config + await self.process_query(query) + + async def _check_output(self, query: entities.Query, result: pipeline_entities.StageProcessResult): + """检查输出""" + if result.user_notice: + # 处理str类型 + + if isinstance(result.user_notice, str): + result.user_notice = platform_message.MessageChain(platform_message.Plain(result.user_notice)) + elif isinstance(result.user_notice, list): + 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 + ): + result.user_notice.insert(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'], + ) + if result.debug_notice: + self.ap.logger.debug(result.debug_notice) + if result.console_notice: + self.ap.logger.info(result.console_notice) + if result.error_notice: + self.ap.logger.error(result.error_notice) + + async def _execute_from_stage( + self, + stage_index: int, + query: entities.Query, + ): + """从指定阶段开始执行,实现了责任链模式和基于生成器的阶段分叉功能。 + + 如何看懂这里为什么这么写? + 去问 GPT-4: + Q1: 现在有一个责任链,其中有多个stage,query对象在其中传递,stage.process可能返回Result也有可能返回typing.AsyncGenerator[Result, None], + 如果返回的是生成器,需要挨个生成result,检查是否result中是否要求继续,如果要求继续就进行下一个stage。如果此次生成器产生的result处理完了,就继续生成下一个result, + 调用后续的stage,直到该生成器全部生成完。责任链中可能有多个stage会返回生成器 + Q2: 不是这样的,你可能理解有误。如果我们责任链上有这些Stage: + + A B C D E F G + + 如果所有的stage都返回Result,且所有Result都要求继续,那么执行顺序是: + + A B C D E F G + + 现在假设C返回的是AsyncGenerator,那么执行顺序是: + + A B C D E F G C D E F G C D E F G ... + Q3: 但是如果不止一个stage会返回生成器呢? + """ + i = stage_index + + while i < len(self.stage_containers): + 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}') + 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}') + 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') + + async for sub_result in 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}') + break + elif sub_result.result_type == pipeline_entities.ResultType.CONTINUE: + query = sub_result.new_query + await self._execute_from_stage(i + 1, query) + break + + 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_ctx = await self.ap.plugin_mgr.emit_event( + event=event_type( + launcher_type=query.launcher_type.value, + launcher_id=query.launcher_id, + sender_id=query.sender_id, + message_chain=query.message_chain, + query=query, + ) + ) + + if event_ctx.is_prevented_default(): + return + + 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.error(f'Traceback: {traceback.format_exc()}') + finally: + self.ap.logger.debug(f'Query {query} processed') + + +class PipelineManager: + """流水线管理器""" + + # ====== 4.0 ====== + + ap: app.Application + + pipelines: list[RuntimePipeline] + + stage_dict: dict[str, type[stage.PipelineStage]] + + def __init__(self, ap: app.Application): + self.ap = ap + self.pipelines = [] + + async def initialize(self): + self.stage_dict = {name: cls for name, cls in stage.preregistered_stages.items()} + + await self.load_pipelines_from_db() + + async def load_pipelines_from_db(self): + self.ap.logger.info('Loading pipelines from db...') + + result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline)) + + pipelines = result.all() + + # load pipelines + 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, + ): + if isinstance(pipeline_entity, sqlalchemy.Row): + 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))) + + 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) + + async def get_pipeline_by_uuid(self, uuid: str) -> RuntimePipeline | None: + for pipeline in self.pipelines: + if pipeline.pipeline_entity.uuid == uuid: + return pipeline + return None + + async def remove_pipeline(self, uuid: str): + for pipeline in self.pipelines: + if pipeline.pipeline_entity.uuid == uuid: + self.pipelines.remove(pipeline) + return diff --git a/pkg/pipeline/pool.py b/pkg/pipeline/pool.py index e358d249..3da4e19b 100644 --- a/pkg/pipeline/pool.py +++ b/pkg/pipeline/pool.py @@ -28,15 +28,17 @@ class QueryPool: async def add_query( self, + bot_uuid: str, launcher_type: entities.LauncherTypes, launcher_id: typing.Union[int, str], sender_id: typing.Union[int, str], message_event: platform_events.MessageEvent, message_chain: platform_message.MessageChain, - adapter: msadapter.MessagePlatformAdapter + adapter: msadapter.MessagePlatformAdapter, ) -> entities.Query: async with self.condition: query = entities.Query( + bot_uuid=bot_uuid, query_id=self.query_id_counter, launcher_type=launcher_type, launcher_id=launcher_id, @@ -45,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 299aea5e..29371adc 100644 --- a/pkg/pipeline/preproc/preproc.py +++ b/pkg/pipeline/preproc/preproc.py @@ -2,14 +2,14 @@ from __future__ import annotations import datetime -from .. import stage, entities, stagemgr +from .. import stage, entities from ...core import entities as core_entities from ...provider import entities as llm_entities 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,29 +29,37 @@ 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(session) + conversation = await self.ap.sess_mgr.get_conversation( + query, session, query.pipeline_config['ai']['local-agent']['prompt'] + ) # 设置query query.session = session query.prompt = conversation.prompt.copy() query.messages = conversation.messages.copy() - query.use_model = conversation.use_model + query.use_llm_model = conversation.use_llm_model - query.use_funcs = conversation.use_funcs if query.use_model.tool_call_supported 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()), } - # 检查vision是否启用,没启用就删除所有图片 - if not self.ap.provider_cfg.data['enable-vision'] or (self.ap.provider_cfg.data['runner'] == 'local-agent' and not query.use_model.vision_supported): + # 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'): for msg in query.messages: if isinstance(msg.content, list): for me in msg.content: @@ -60,27 +68,22 @@ 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 self.ap.provider_cfg.data['enable-vision'] and (self.ap.provider_cfg.data['runner'] != 'local-agent' or query.use_model.vision_supported): + 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) - ) + content_list.append(llm_entities.ContentElement.from_image_base64(me.base64)) 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( @@ -88,14 +91,11 @@ 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, ) ) query.prompt.messages = event_ctx.event.default_prompt query.messages = event_ctx.event.prompt - return entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query - ) + return entities.StageProcessResult(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 83bb3335..35fa1611 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -1,33 +1,37 @@ 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, runnermgr +from ....provider import runner as runner_module 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( @@ -35,7 +39,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, ) ) @@ -45,34 +49,23 @@ class ChatMessageHandler(handler.MessageHandler): query.resp_messages.append(mc) - yield entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query - ) + yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) else: - yield entities.StageProcessResult( - result_type=entities.ResultType.INTERRUPT, - new_query=query - ) + yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, new_query=query) else: - - if not self.ap.provider_cfg.data['enable-chat']: - yield entities.StageProcessResult( - result_type=entities.ResultType.INTERRUPT, - new_query=query, - ) - if event_ctx.event.alter is not None: # if isinstance(event_ctx.event, str): # 现在暂时不考虑多模态alter query.user_message.content = event_ctx.event.alter text_length = 0 - start_time = time.time() - try: - - runner = self.ap.runner_mgr.get_runner() + for r in runner_module.preregistered_runners: + 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"]}') async for result in runner.run(query): query.resp_messages.append(result) @@ -82,32 +75,22 @@ class ChatMessageHandler(handler.MessageHandler): if result.content is not None: text_length += len(result.content) - yield entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query - ) + yield entities.StageProcessResult(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)}') + hide_exception_info = query.pipeline_config['output']['misc']['hide-exception'] + yield entities.StageProcessResult( result_type=entities.ResultType.INTERRUPT, new_query=query, - user_notice='请求失败' if self.ap.platform_cfg.data['hide-exception-info'] else f'{e}', + 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", - usage=text_length, - model_name=query.use_model.name, - response_seconds=int(time.time() - start_time), - retry_times=-1, - ) + # TODO statistics + pass diff --git a/pkg/pipeline/process/handlers/command.py b/pkg/pipeline/process/handlers/command.py index cec64a45..cc0e9314 100644 --- a/pkg/pipeline/process/handlers/command.py +++ b/pkg/pipeline/process/handlers/command.py @@ -11,24 +11,26 @@ 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.system_cfg.data['admin-sessions']: + + 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,42 +40,28 @@ 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 - ) + yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) else: - yield entities.StageProcessResult( - result_type=entities.ResultType.INTERRUPT, - new_query=query - ) + yield entities.StageProcessResult(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 - ): + async for ret in self.ap.cmd_mgr.execute(command_text=command_text, query=query, session=session): if ret.error is not None: query.resp_messages.append( llm_entities.Message( @@ -84,23 +72,15 @@ class CommandHandler(handler.MessageHandler): 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 - ) + yield entities.StageProcessResult(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( - llm_entities.ContentElement.from_image_url(ret.image_url) - ) + content.append(llm_entities.ContentElement.from_image_url(ret.image_url)) query.resp_messages.append( llm_entities.Message( @@ -111,12 +91,6 @@ 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 - ) + yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) else: - yield entities.StageProcessResult( - result_type=entities.ResultType.INTERRUPT, - new_query=query - ) + yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, new_query=query) diff --git a/pkg/pipeline/process/process.py b/pkg/pipeline/process/process.py index 362ece01..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, stagemgr -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): """请求实际处理阶段 - + 通过命令处理器和聊天处理器处理消息。 改写: @@ -23,7 +21,7 @@ class Processor(stage.PipelineStage): chat_handler: handler.MessageHandler - async def initialize(self): + async def initialize(self, pipeline_config: dict): self.cmd_handler = command.CommandHandler(self.ap) self.chat_handler = chat.ChatMessageHandler(self.ap) @@ -35,14 +33,15 @@ 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.command_cfg.data['command-prefix'] + cmd_prefix = self.ap.instance_config.data['command']['prefix'] if any(message_text.startswith(prefix) for prefix in cmd_prefix): async for result in self.cmd_handler.handle(query): @@ -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 9b418dd2..3bcc347a 100644 --- a/pkg/pipeline/ratelimit/algo.py +++ b/pkg/pipeline/ratelimit/algo.py @@ -2,24 +2,24 @@ from __future__ import annotations import abc import typing -from ...core import app +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, 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, 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 3cc1ab94..cc816f73 100644 --- a/pkg/pipeline/ratelimit/algos/fixedwin.py +++ b/pkg/pipeline/ratelimit/algos/fixedwin.py @@ -3,10 +3,11 @@ import asyncio import time import typing from .. import algo +from ....core import entities as core_entities + # 固定窗口算法 class SessionContainer: - wait_lock: asyncio.Lock records: dict[int, int] @@ -17,9 +18,8 @@ class SessionContainer: self.records = {} -@algo.algo_class("fixwin") +@algo.algo_class('fixwin') class FixedWindowAlgo(algo.ReteLimitAlgo): - containers_lock: asyncio.Lock """访问记录容器锁""" @@ -30,7 +30,12 @@ class FixedWindowAlgo(algo.ReteLimitAlgo): self.containers_lock = asyncio.Lock() self.containers = {} - async def require_access(self, 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 @@ -45,14 +50,14 @@ class FixedWindowAlgo(algo.ReteLimitAlgo): # 等待锁 async with container.wait_lock: - # 获取窗口大小和限制 - window_size = self.ap.pipeline_cfg.data['rate-limit']['fixwin']['default']['window-size'] - limitation = self.ap.pipeline_cfg.data['rate-limit']['fixwin']['default']['limit'] + window_size = query.pipeline_config['safety']['rate-limit']['window-length'] + limitation = query.pipeline_config['safety']['rate-limit']['limitation'] - if session_name in self.ap.pipeline_cfg.data['rate-limit']['fixwin']: - window_size = self.ap.pipeline_cfg.data['rate-limit']['fixwin'][session_name]['window-size'] - limitation = self.ap.pipeline_cfg.data['rate-limit']['fixwin'][session_name]['limit'] + # TODO revert it + # if session_name in self.ap.pipeline_cfg.data['rate-limit']['fixwin']: + # window_size = self.ap.pipeline_cfg.data['rate-limit']['fixwin'][session_name]['window-size'] + # limitation = self.ap.pipeline_cfg.data['rate-limit']['fixwin'][session_name]['limit'] # 获取当前时间戳 now = int(time.time()) @@ -65,15 +70,15 @@ class FixedWindowAlgo(algo.ReteLimitAlgo): # 如果访问次数超过了限制 if count >= limitation: - if self.ap.pipeline_cfg.data['rate-limit']['strategy'] == 'drop': + if query.pipeline_config['safety']['rate-limit']['strategy'] == 'drop': return False - elif self.ap.pipeline_cfg.data['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 @@ -83,6 +88,11 @@ class FixedWindowAlgo(algo.ReteLimitAlgo): # 返回True return True - - async def release_access(self, 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 cd39b85c..23de4ec6 100644 --- a/pkg/pipeline/ratelimit/ratelimit.py +++ b/pkg/pipeline/ratelimit/ratelimit.py @@ -2,25 +2,28 @@ from __future__ import annotations import typing -from .. import entities, stagemgr, stage +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): - - algo_name = self.ap.pipeline_cfg.data['rate-limit']['algo'] + async def initialize(self, pipeline_config: dict): + algo_name = 'fixwin' algo_class = None @@ -42,10 +45,10 @@ 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, query.launcher_id, ): @@ -57,11 +60,12 @@ 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, query.launcher_id, ) diff --git a/pkg/pipeline/respback/respback.py b/pkg/pipeline/respback/respback.py index 08b335d5..39d3abb1 100644 --- a/pkg/pipeline/respback/respback.py +++ b/pkg/pipeline/respback/respback.py @@ -4,40 +4,42 @@ 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, stagemgr +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: - """处理 - """ - - random_range = (self.ap.platform_cfg.data['force-delay']['min'], self.ap.platform_cfg.data['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) - await self.ap.platform_mgr.send( - query.message_event, - query.resp_message_chain[-1], - adapter=query.adapter + 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)) + + quote_origin = query.pipeline_config['output']['misc']['quote-origin'] + + await query.adapter.reply_message( + message_source=query.message_event, + message=query.resp_message_chain[-1], + quote_origin=quote_origin, ) - return entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query - ) \ No newline at end of file + return entities.StageProcessResult(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 77858f0d..0193f2ce 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, stagemgr +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): """群组响应规则检查器 @@ -20,9 +22,8 @@ class GroupRespondRuleCheckStage(stage.PipelineStage): rule_matchers: list[rule.GroupRespondRule] """检查器实例""" - async def initialize(self): - """初始化检查器 - """ + async def initialize(self, pipeline_config: dict): + """初始化检查器""" self.rule_matchers = [] @@ -32,19 +33,16 @@ class GroupRespondRuleCheckStage(stage.PipelineStage): self.rule_matchers.append(rule_inst) 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 - ) + return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) - rules = self.ap.pipeline_cfg.data['respond-rules'] + rules = query.pipeline_config['trigger']['group-respond-rules'] - use_rule = rules['default'] + use_rule = rules - if str(query.launcher_id) in rules: - use_rule = rules[str(query.launcher_id)] + # TODO revert it + # if str(query.launcher_id) in rules: + # 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) @@ -55,8 +53,5 @@ class GroupRespondRuleCheckStage(stage.PipelineStage): result_type=entities.ResultType.CONTINUE, new_query=query, ) - - return entities.StageProcessResult( - result_type=entities.ResultType.INTERRUPT, - new_query=query - ) + + return entities.StageProcessResult(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..340b92c7 100644 --- a/pkg/pipeline/resprule/rules/atbot.py +++ b/pkg/pipeline/resprule/rules/atbot.py @@ -7,21 +7,21 @@ 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']: 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 +29,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..d2f782ab 100644 --- a/pkg/pipeline/resprule/rules/random.py +++ b/pkg/pipeline/resprule/rules/random.py @@ -7,19 +7,15 @@ 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 + + return entities.RuleJudgeResult(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 56c092b5..18636e9f 100644 --- a/pkg/pipeline/stage.py +++ b/pkg/pipeline/stage.py @@ -7,30 +7,27 @@ from ..core import app, entities as core_entities from . import entities -_stage_classes: dict[str, PipelineStage] = {} +preregistered_stages: dict[str, PipelineStage] = {} def stage_class(name: str): - def decorator(cls): - _stage_classes[name] = cls + preregistered_stages[name] = cls return cls - + return decorator class PipelineStage(metaclass=abc.ABCMeta): - """流水线阶段 - """ + """流水线阶段""" ap: app.Application def __init__(self, ap: app.Application): self.ap = ap - async def initialize(self): - """初始化 - """ + 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/stagemgr.py b/pkg/pipeline/stagemgr.py deleted file mode 100644 index 2bd685d6..00000000 --- a/pkg/pipeline/stagemgr.py +++ /dev/null @@ -1,71 +0,0 @@ -from __future__ import annotations - -from ..core import app -from . import stage -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 - - -# 请求处理阶段顺序 -stage_order = [ - "GroupRespondRuleCheckStage", # 群响应规则检查 - "BanSessionCheckStage", # 封禁会话检查 - "PreContentFilterStage", # 内容过滤前置阶段 - "PreProcessor", # 预处理器 - "ConversationMessageTruncator", # 会话消息截断器 - "RequireRateLimitOccupancy", # 请求速率限制占用 - "MessageProcessor", # 处理器 - "ReleaseRateLimitOccupancy", # 释放速率限制占用 - "PostContentFilterStage", # 内容过滤后置阶段 - "ResponseWrapper", # 响应包装器 - "LongTextProcessStage", # 长文本处理 - "SendResponseBackStage", # 发送响应 -] - - -class StageInstContainer(): - """阶段实例容器 - """ - - inst_name: str - - inst: stage.PipelineStage - - def __init__(self, inst_name: str, inst: stage.PipelineStage): - self.inst_name = inst_name - self.inst = inst - - -class StageManager: - ap: app.Application - - stage_containers: list[StageInstContainer] - - def __init__(self, ap: app.Application): - self.ap = ap - - self.stage_containers = [] - - async def initialize(self): - """初始化 - """ - - for name, cls in stage._stage_classes.items(): - self.stage_containers.append(StageInstContainer( - inst_name=name, - inst=cls(self.ap) - )) - - for stage_containers in self.stage_containers: - await stage_containers.inst.initialize() - - # 按照 stage_order 排序 - self.stage_containers.sort(key=lambda x: stage_order.index(x.inst_name)) diff --git a/pkg/pipeline/wrapper/wrapper.py b/pkg/pipeline/wrapper/wrapper.py index a06e4a80..3299a226 100644 --- a/pkg/pipeline/wrapper/wrapper.py +++ b/pkg/pipeline/wrapper/wrapper.py @@ -3,26 +3,24 @@ from __future__ import annotations import typing -from ...core import app, entities as core_entities -from .. import entities -from .. import stage, entities, stagemgr 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 """ - async def initialize(self): + async def initialize(self, pipeline_config: dict): pass async def process( @@ -30,36 +28,26 @@ 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 - ) + yield entities.StageProcessResult(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] ')) - - yield entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query + 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) elif query.resp_messages[-1].role == 'plugin': 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 - ) + yield entities.StageProcessResult(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 +67,39 @@ 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)) else: - 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: # 有函数调用 - 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 self.ap.platform_cfg.data['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 +109,30 @@ 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..c0fd15c5 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,9 @@ 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 +46,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 +64,23 @@ 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 +93,7 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): async def kill(self) -> bool: """关闭适配器 - + Returns: bool: 是否成功关闭,热重载时若此函数返回False则不会重载MessageSource底层 """ @@ -107,6 +102,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 new file mode 100644 index 00000000..0af7e394 --- /dev/null +++ b/pkg/platform/botmgr.py @@ -0,0 +1,255 @@ +from __future__ import annotations + +import sys +import asyncio +import traceback +import sqlalchemy + + +# FriendMessage, Image, MessageChain, Plain +from . import adapter as msadapter + +from ..core import app, entities as core_entities, taskmgr +from .types import events as platform_events + +from ..discover import engine + +from ..entity.persistence import bot as persistence_bot + +# 处理 3.4 移除了 YiriMirai 之后,插件的兼容性问题 +from . import types as mirai + +sys.modules['mirai'] = mirai + + +class RuntimeBot: + """运行时机器人""" + + ap: app.Application + + bot_entity: persistence_bot.Bot + + enable: bool + + adapter: msadapter.MessagePlatformAdapter + + task_wrapper: taskmgr.TaskWrapper + + task_context: taskmgr.TaskContext + + 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 + self.adapter = adapter + self.task_context = taskmgr.TaskContext() + + async def initialize(self): + 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, + launcher_id=event.sender.id, + sender_id=event.sender.id, + message_event=event, + message_chain=event.message_chain, + adapter=adapter, + ) + + 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, + launcher_id=event.group.id, + sender_id=event.sender.id, + message_event=event, + message_chain=event.message_chain, + adapter=adapter, + ) + + 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...') + await self.adapter.run_async() + self.task_context.set_current_action('Exited.') + except Exception as e: + if isinstance(e, asyncio.CancelledError): + self.task_context.set_current_action('Exited.') + 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.ap.logger.error(f'平台适配器运行出错: {e}') + 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__}', + context=self.task_context, + scopes=[ + core_entities.LifecycleControlScope.APPLICATION, + core_entities.LifecycleControlScope.PLATFORM, + ], + ) + + async def shutdown(self): + await self.adapter.kill() + + self.ap.task_mgr.cancel_task(self.task_wrapper.id) + + +# 控制QQ消息输入输出的类 +class PlatformManager: + # ====== 4.0 ====== + ap: app.Application = None + + bots: list[RuntimeBot] + + adapter_components: list[engine.Component] + + 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') + adapter_dict: dict[str, type[msadapter.MessagePlatformAdapter]] = {} + for component in self.adapter_components: + adapter_dict[component.metadata.name] = component.get_python_component_class() + self.adapter_dict = adapter_dict + + await self.load_bots_from_db() + + def get_running_adapters(self) -> list[msadapter.MessagePlatformAdapter]: + return [bot.adapter for bot in self.bots if bot.enable] + + async def load_bots_from_db(self): + self.ap.logger.info('Loading bots from db...') + + self.bots = [] + + result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_bot.Bot)) + + 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: + """加载机器人""" + if isinstance(bot_entity, sqlalchemy.Row): + bot_entity = persistence_bot.Bot(**bot_entity._mapping) + elif isinstance(bot_entity, dict): + bot_entity = persistence_bot.Bot(**bot_entity) + + adapter_inst = self.adapter_dict[bot_entity.adapter](bot_entity.adapter_config, self.ap) + + runtime_bot = RuntimeBot(ap=self.ap, bot_entity=bot_entity, adapter=adapter_inst) + + await runtime_bot.initialize() + + 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: + return bot + return None + + async def remove_bot(self, bot_uuid: str): + for bot in self.bots: + if bot.bot_entity.uuid == bot_uuid: + if bot.enable: + await bot.shutdown() + self.bots.remove(bot) + return + + def get_available_adapters_info(self) -> list[dict]: + 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: + 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, + ): + # index = -2 + + # for i, adapter in enumerate(self.adapters): + # if adapter == adapter_inst: + # index = i + # break + + # if index == -2: + # raise Exception('平台适配器未找到') + + # # 只修改启用的适配器 + # real_index = -1 + + # for i, adapter in enumerate(self.ap.platform_cfg.data['platform-adapters']): + # if adapter['enable']: + # index -= 1 + # if index == -1: + # real_index = i + # break + + # new_cfg = { + # 'adapter': adapter_name, + # 'enable': True, + # **config + # } + # self.ap.platform_cfg.data['platform-adapters'][real_index] = new_cfg + # await self.ap.platform_cfg.dump_config() + + # TODO implement this + pass + + async def run(self): + # This method will only be called when the application launching + for bot in self.bots: + if bot.enable: + await bot.run() + + async def shutdown(self): + for bot in self.bots: + if bot.enable: + await bot.shutdown() + self.ap.task_mgr.cancel_by_scope(core_entities.LifecycleControlScope.PLATFORM) diff --git a/pkg/platform/manager.py b/pkg/platform/manager.py deleted file mode 100644 index 96c50602..00000000 --- a/pkg/platform/manager.py +++ /dev/null @@ -1,190 +0,0 @@ -from __future__ import annotations - -import json -import os -import sys -import logging -import asyncio -import traceback - -from .sources import qqofficial - -# FriendMessage, Image, MessageChain, Plain -from ..platform import adapter as msadapter - -from ..core import app, entities as core_entities -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 - -# 处理 3.4 移除了 YiriMirai 之后,插件的兼容性问题 -from . import types as mirai -sys.modules['mirai'] = mirai - - -# 控制QQ消息输入输出的类 -class PlatformManager: - - # adapter: msadapter.MessageSourceAdapter = None - adapters: list[msadapter.MessagePlatformAdapter] = [] - - message_platform_adapter_components: list[engine.Component] = [] - - # modern - ap: app.Application = None - - def __init__(self, ap: app.Application = None): - - self.ap = ap - self.adapters = [] - - async def initialize(self): - - components = self.ap.discover.get_components_by_kind('MessagePlatformAdapter') - - self.message_platform_adapter_components = components - - # from .sources import nakuru, aiocqhttp, qqbotpy, qqofficial, wecom, lark, discord, gewechat, officialaccount, telegram, dingtalk - - async def on_friend_message(event: platform_events.FriendMessage, adapter: msadapter.MessagePlatformAdapter): - - await self.ap.query_pool.add_query( - launcher_type=core_entities.LauncherTypes.PERSON, - launcher_id=event.sender.id, - sender_id=event.sender.id, - message_event=event, - message_chain=event.message_chain, - adapter=adapter - ) - - async def on_group_message(event: platform_events.GroupMessage, adapter: msadapter.MessagePlatformAdapter): - - await self.ap.query_pool.add_query( - launcher_type=core_entities.LauncherTypes.GROUP, - launcher_id=event.group.id, - sender_id=event.sender.id, - message_event=event, - message_chain=event.message_chain, - adapter=adapter - ) - - index = 0 - - for adap_cfg in self.ap.platform_cfg.data['platform-adapters']: - if adap_cfg['enable']: - self.ap.logger.info(f'初始化平台适配器 {index}: {adap_cfg["adapter"]}') - index += 1 - cfg_copy = adap_cfg.copy() - del cfg_copy['enable'] - adapter_name = cfg_copy['adapter'] - del cfg_copy['adapter'] - - found = False - - for adapter in self.message_platform_adapter_components: - if adapter.metadata.name == adapter_name: - found = True - adapter_cls = adapter.get_python_component_class() - - adapter_inst = adapter_cls( - cfg_copy, - self.ap - ) - self.adapters.append(adapter_inst) - - adapter_inst.register_listener( - platform_events.FriendMessage, - on_friend_message - ) - adapter_inst.register_listener( - platform_events.GroupMessage, - on_group_message - ) - - if not found: - raise Exception('platform.json 中启用了未知的平台适配器: ' + adapter_name) - - if len(self.adapters) == 0: - self.ap.logger.warning('未运行平台适配器,请根据文档配置并启用平台适配器。') - - def write_back_config(self, adapter_name: str, adapter_inst: msadapter.MessagePlatformAdapter, config: dict): - index = -2 - - for i, adapter in enumerate(self.adapters): - if adapter == adapter_inst: - index = i - break - - if index == -2: - raise Exception('平台适配器未找到') - - # 只修改启用的适配器 - real_index = -1 - - for i, adapter in enumerate(self.ap.platform_cfg.data['platform-adapters']): - if adapter['enable']: - index -= 1 - if index == -1: - real_index = i - break - - new_cfg = { - 'adapter': adapter_name, - 'enable': True, - **config - } - self.ap.platform_cfg.data['platform-adapters'][real_index] = new_cfg - self.ap.platform_cfg.dump_config_sync() - - async def send(self, event: platform_events.MessageEvent, msg: platform_message.MessageChain, adapter: msadapter.MessagePlatformAdapter): - - if self.ap.platform_cfg.data['at-sender'] and isinstance(event, platform_events.GroupMessage): - - msg.insert( - 0, - platform_message.At( - event.sender.id - ) - ) - - await adapter.reply_message( - event, - msg, - quote_origin=True if self.ap.platform_cfg.data['quote-origin'] else False - ) - - async def run(self): - try: - tasks = [] - for adapter in self.adapters: - async def exception_wrapper(adapter: msadapter.MessagePlatformAdapter): - try: - await adapter.run_async() - except Exception as e: - if isinstance(e, asyncio.CancelledError): - return - self.ap.logger.error('平台适配器运行出错: ' + str(e)) - self.ap.logger.debug(f"Traceback: {traceback.format_exc()}") - - tasks.append(exception_wrapper(adapter)) - - - for task in tasks: - self.ap.task_mgr.create_task( - task, - kind="platform-adapter", - name=f"platform-adapter-{adapter.__class__.__name__}", - scopes=[core_entities.LifecycleControlScope.APPLICATION, core_entities.LifecycleControlScope.PLATFORM], - ) - - except Exception as e: - self.ap.logger.error('平台适配器运行出错: ' + str(e)) - self.ap.logger.debug(f"Traceback: {traceback.format_exc()}") - - async def shutdown(self): - for adapter in self.adapters: - await adapter.kill() - self.ap.task_mgr.cancel_by_scope(core_entities.LifecycleControlScope.PLATFORM) \ No newline at end of file diff --git a/pkg/platform/sources/aiocqhttp.py b/pkg/platform/sources/aiocqhttp.py index af14372a..bee97f57 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,9 @@ 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]) - + else: msg_list.append(aiocqhttp.MessageSegment.text(str(msg))) @@ -73,25 +71,23 @@ class AiocqhttpMessageConverter(adapter.MessageConverter): yiri_msg_list = [] - 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 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": + 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}")) + yiri_msg_list.append(platform_message.Image(base64=f'data:image/{image_format};base64,{image_base64}')) chain = platform_message.MessageChain(yiri_msg_list) @@ -99,60 +95,56 @@ 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 @staticmethod async def target2yiri(event: aiocqhttp.Event): - yiri_chain = await AiocqhttpMessageConverter.target2yiri( - event.message, event.message_id - ) + yiri_chain = await AiocqhttpMessageConverter.target2yiri(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,25 +162,23 @@ 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() - 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): 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 +186,13 @@ 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_msg = (await AiocqhttpMessageConverter.yiri2target(message))[0] if quote_origin: 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 @@ -219,13 +206,13 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter): self.bot_account_id = event.self_id try: return await callback(await self.event_converter.target2yiri(event), self) - except: + 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, @@ -238,4 +225,6 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter): await self.bot._server_app.run_task(**self.config) async def kill(self) -> bool: + # Current issue: existing connection will not be closed + # self.should_shutdown = True return False diff --git a/pkg/platform/sources/aiocqhttp.yaml b/pkg/platform/sources/aiocqhttp.yaml index a2c230a7..c915e464 100644 --- a/pkg/platform/sources/aiocqhttp.yaml +++ b/pkg/platform/sources/aiocqhttp.yaml @@ -3,17 +3,21 @@ 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 适配器 + zh_CN: OneBot v11 适配器,请查看文档了解使用方式 + icon: onebot.png spec: config: - name: host label: en_US: Host zh_CN: 主机 + description: + en_US: The host that OneBot v11 listens on for reverse WebSocket connections. Unless you know what you're doing, use 0.0.0.0 + zh_CN: OneBot v11 监听的反向 WS 主机,除非你知道自己在做什么,否则请写 0.0.0.0 type: string required: true default: 0.0.0.0 @@ -21,13 +25,19 @@ spec: label: en_US: Port zh_CN: 端口 - type: int + description: + en_US: Port + zh_CN: 监听的端口 + type: integer required: true default: 2280 - name: access-token label: en_US: Access Token zh_CN: 访问令牌 + description: + en_US: Custom connection token for the protocol endpoint. If the protocol endpoint is not set, don't fill it + zh_CN: 自定义的与协议端的连接令牌,若协议端未设置,则不填 type: string required: false default: "" diff --git a/pkg/platform/sources/dingtalk.py b/pkg/platform/sources/dingtalk.py index 94a7d249..433ef836 100644 --- a/pkg/platform/sources/dingtalk.py +++ b/pkg/platform/sources/dingtalk.py @@ -1,37 +1,28 @@ - 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 +30,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 +38,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,29 +94,29 @@ 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"], - markdown_card=config["markdown_card"] + client_id=config['client_id'], + client_secret=config['client_secret'], + robot_name=config['robot_name'], + robot_code=config['robot_code'], + markdown_card=config['markdown_card'], ) - + async def reply_message( self, message_source: platform_events.MessageEvent, @@ -147,37 +129,33 @@ 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 - ): + 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, 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: 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() @@ -191,4 +169,3 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], ): return super().unregister_listener(event_type, callback) - diff --git a/pkg/platform/sources/dingtalk.svg b/pkg/platform/sources/dingtalk.svg new file mode 100644 index 00000000..b60653b7 --- /dev/null +++ b/pkg/platform/sources/dingtalk.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/pkg/platform/sources/dingtalk.yaml b/pkg/platform/sources/dingtalk.yaml index b4c1c692..e251bf25 100644 --- a/pkg/platform/sources/dingtalk.yaml +++ b/pkg/platform/sources/dingtalk.yaml @@ -7,7 +7,8 @@ metadata: zh_CN: 钉钉 description: en_US: DingTalk Adapter - zh_CN: 钉钉适配器 + zh_CN: 钉钉适配器,请查看文档了解使用方式 + icon: dingtalk.svg spec: config: - name: client_id @@ -38,6 +39,13 @@ spec: type: string required: true default: "" + - name: markdown_card + label: + en_US: Markdown Card + zh_CN: 是否使用 Markdown 卡片 + type: boolean + required: false + default: true execution: python: path: ./dingtalk.py diff --git a/pkg/platform/sources/discord.py b/pkg/platform/sources/discord.py index 961b031a..f5be422d 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,45 @@ 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()) - ) + 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 +88,15 @@ 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 +104,23 @@ 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 +131,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 +142,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 +154,6 @@ class DiscordEventConverter(adapter.EventConverter): class DiscordAdapter(adapter.MessagePlatformAdapter): - bot: discord.Client bot_account_id: str # 用于在流水线中识别at是否是本bot,直接以bot_name作为标识 @@ -191,12 +174,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,14 +191,12 @@ 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 - ): + + async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): pass async def reply_message( @@ -229,17 +209,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) @@ -262,7 +242,7 @@ class DiscordAdapter(adapter.MessagePlatformAdapter): 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/discord.svg b/pkg/platform/sources/discord.svg new file mode 100644 index 00000000..177a0591 --- /dev/null +++ b/pkg/platform/sources/discord.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/pkg/platform/sources/discord.yaml b/pkg/platform/sources/discord.yaml index fd523057..c5aa24cc 100644 --- a/pkg/platform/sources/discord.yaml +++ b/pkg/platform/sources/discord.yaml @@ -7,7 +7,8 @@ metadata: zh_CN: Discord description: en_US: Discord Adapter - zh_CN: Discord 适配器 + zh_CN: Discord 适配器,请查看文档了解使用方式 + icon: discord.svg spec: config: - name: client_id diff --git a/pkg/platform/sources/gewechat.png b/pkg/platform/sources/gewechat.png new file mode 100644 index 00000000..32b2fa32 Binary files /dev/null and b/pkg/platform/sources/gewechat.png differ diff --git a/pkg/platform/sources/gewechat.py b/pkg/platform/sources/gewechat.py index 28407400..efa58f3d 100644 --- a/pkg/platform/sources/gewechat.py +++ b/pkg/platform/sources/gewechat.py @@ -5,62 +5,88 @@ import asyncio 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 from ..types import entities as platform_entities from ...utils import image import xml.etree.ElementTree as ET -from typing import Optional, List, Tuple +from typing import Optional, Tuple from functools import partial -class GewechatMessageConverter(adapter.MessageConverter): +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}) + elif isinstance(component, platform_message.Forward): + for node in component.node_list: + content_list.extend(await GewechatMessageConverter.yiri2target(node.message_chain)) + content_list.append({'type': 'image', 'image': component.url}) elif isinstance(component, platform_message.WeChatMiniPrograms): - content_list.append({"type": 'WeChatMiniPrograms', 'mini_app_id': component.mini_app_id, 'display_name': component.display_name, - 'page_path': component.page_path, 'cover_img_url': component.image_url, 'title': component.title, - 'user_name': component.user_name}) + content_list.append( + { + 'type': 'WeChatMiniPrograms', + 'mini_app_id': component.mini_app_id, + 'display_name': component.display_name, + 'page_path': component.page_path, + 'cover_img_url': component.image_url, + 'title': component.title, + 'user_name': component.user_name, + } + ) elif isinstance(component, platform_message.WeChatForwardMiniPrograms): - content_list.append({"type": 'WeChatForwardMiniPrograms', 'xml_data': component.xml_data, 'image_url': component.image_url}) + content_list.append( + { + 'type': 'WeChatForwardMiniPrograms', + 'xml_data': component.xml_data, + 'image_url': component.image_url, + } + ) elif isinstance(component, platform_message.WeChatEmoji): - content_list.append({'type': 'WeChatEmoji', 'emoji_md5': component.emoji_md5, 'emoji_size': component.emoji_size}) + content_list.append( + { + 'type': 'WeChatEmoji', + 'emoji_md5': component.emoji_md5, + 'emoji_size': component.emoji_size, + } + ) elif isinstance(component, platform_message.WeChatLink): - content_list.append({'type': 'WeChatLink', 'link_title': component.link_title, 'link_desc': component.link_desc, - 'link_thumb_url': component.link_thumb_url, 'link_url': component.link_url}) + content_list.append( + { + 'type': 'WeChatLink', + 'link_title': component.link_title, + 'link_desc': component.link_desc, + 'link_thumb_url': component.link_thumb_url, + 'link_url': component.link_url, + } + ) elif isinstance(component, platform_message.WeChatForwardLink): content_list.append({'type': 'WeChatForwardLink', 'xml_data': component.xml_data}) 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.WeChatForwardImage): content_list.append({'type': 'WeChatForwardImage', 'xml_data': component.xml_data}) elif isinstance(component, platform_message.WeChatForwardFile): @@ -77,28 +103,23 @@ class GewechatMessageConverter(adapter.MessageConverter): return content_list - - async def target2yiri( - self, - message: dict, - bot_account_id: str - ) -> platform_message.MessageChain: + async def target2yiri(self, message: dict, bot_account_id: str) -> platform_message.MessageChain: """外部消息转平台消息""" # 数据预处理 message_list = [] - ats_bot = False # 是否被@ - content = message["Data"]["Content"]["string"] + ats_bot = False # 是否被@ + content = message['Data']['Content']['string'] content_no_preifx = content # 群消息则去掉前缀 is_group_message = self._is_group_message(message) if is_group_message: ats_bot = self._ats_bot(message, bot_account_id) - if "@所有人" in content: + if '@所有人' in content: message_list.append(platform_message.AtAll()) elif ats_bot: message_list.append(platform_message.At(target=bot_account_id)) content_no_preifx, _ = self._extract_content_and_sender(content) - msg_type = message["Data"]["MsgType"] + msg_type = message['Data']['MsgType'] # 映射消息类型到处理器方法 handler_map = { @@ -111,99 +132,80 @@ class GewechatMessageConverter(adapter.MessageConverter): # 分派处理 handler = handler_map.get(msg_type, self._handler_default) handler_result = await handler( - message = message, # 原始的message - content_no_preifx = content_no_preifx, # 处理后的content + message=message, # 原始的message + content_no_preifx=content_no_preifx, # 处理后的content ) - + if handler_result and len(handler_result) > 0: message_list.extend(handler_result) - + return platform_message.MessageChain(message_list) - async def _handler_text( - self, - message: Optional[dict], - content_no_preifx: str - ) -> platform_message.MessageChain: + async def _handler_text(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain: """处理文本消息 (msg_type=1)""" if message and self._is_group_message(message): pattern = r'@\S{1,20}' content_no_preifx = re.sub(pattern, '', content_no_preifx) - + return platform_message.MessageChain([platform_message.Plain(content_no_preifx)]) - - async def _handler_image( - self, - message: Optional[dict], - content_no_preifx: str - ) -> platform_message.MessageChain: + + async def _handler_image(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain: """处理图像消息 (msg_type=3)""" try: image_xml = content_no_preifx if not image_xml: - return platform_message.MessageChain([platform_message.Unknown("[图片内容为空]")]) - + return platform_message.MessageChain([platform_message.Unknown('[图片内容为空]')]) + 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, ) elements = [ - platform_message.Image(base64=f"data:image/{image_format};base64,{base64_str}"), - platform_message.WeChatForwardImage(xml_data=image_xml) # 微信消息转发 + platform_message.Image(base64=f'data:image/{image_format};base64,{base64_str}'), + platform_message.WeChatForwardImage(xml_data=image_xml), # 微信消息转发 ] return platform_message.MessageChain(elements) except Exception as e: - print(f"处理图片失败: {str(e)}") - return platform_message.MessageChain([platform_message.Unknown("[图片处理失败]")]) - - async def _handler_voice( - self, - message: Optional[dict], - content_no_preifx: str - ) -> platform_message.MessageChain: + print(f'处理图片失败: {str(e)}') + return platform_message.MessageChain([platform_message.Unknown('[图片处理失败]')]) + + async def _handler_voice(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain: """处理语音消息 (msg_type=34)""" message_List = [] try: # 从消息中提取语音数据(需根据实际数据结构调整字段名) - audio_base64 = message["Data"]["ImgBuf"]["buffer"] - + audio_base64 = message['Data']['ImgBuf']['buffer'] + # 验证语音数据有效性 if not audio_base64: - message_List.append(platform_message.Unknown(text="[语音内容为空]")) + message_List.append(platform_message.Unknown(text='[语音内容为空]')) return platform_message.MessageChain(message_List) - + # 转换为平台支持的语音格式(如 Silk 格式) - voice_element = platform_message.Voice( - base64=f"data:audio/silk;base64,{audio_base64}" - ) + voice_element = platform_message.Voice(base64=f'data:audio/silk;base64,{audio_base64}') message_List.append(voice_element) - + except KeyError as e: - print(f"语音数据字段缺失: {str(e)}") - message_List.append(platform_message.Unknown(text="[语音数据解析失败]")) + print(f'语音数据字段缺失: {str(e)}') + message_List.append(platform_message.Unknown(text='[语音数据解析失败]')) except Exception as e: - print(f"处理语音消息异常: {str(e)}") - message_List.append(platform_message.Unknown(text="[语音处理失败]")) - + print(f'处理语音消息异常: {str(e)}') + message_List.append(platform_message.Unknown(text='[语音处理失败]')) + return platform_message.MessageChain(message_List) - - async def _handler_compound( - self, - message: Optional[dict], - content_no_preifx: str - ) -> platform_message.MessageChain: + async def _handler_compound(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain: """处理复合消息 (msg_type=49),根据子类型分派""" try: xml_data = ET.fromstring(content_no_preifx) appmsg_data = xml_data.find('.//appmsg') if appmsg_data: - data_type = appmsg_data.findtext('.//type', "") + data_type = appmsg_data.findtext('.//type', '') # 二次分派处理器 sub_handler_map = { @@ -212,67 +214,59 @@ class GewechatMessageConverter(adapter.MessageConverter): '6': self._handler_compound_file, '33': self._handler_compound_mini_program, '36': self._handler_compound_mini_program, - '2000': partial(self._handler_compound_unsupported, text="[转账消息]"), - '2001': partial(self._handler_compound_unsupported, text="[红包消息]"), - '51': partial(self._handler_compound_unsupported, text="[视频号消息]"), + '2000': partial(self._handler_compound_unsupported, text='[转账消息]'), + '2001': partial(self._handler_compound_unsupported, text='[红包消息]'), + '51': partial(self._handler_compound_unsupported, text='[视频号消息]'), } - + handler = sub_handler_map.get(data_type, self._handler_compound_unsupported) return await handler( - message=message, #原始msg - xml_data=xml_data, # xml数据 + message=message, # 原始msg + xml_data=xml_data, # xml数据 ) else: return platform_message.MessageChain([platform_message.Unknown(text=content_no_preifx)]) except Exception as e: - print(f"解析复合消息失败: {str(e)}") + print(f'解析复合消息失败: {str(e)}') return platform_message.MessageChain([platform_message.Unknown(text=content_no_preifx)]) - + async def _handler_compound_quote( - self, - message: Optional[dict], - xml_data: ET.Element + self, message: Optional[dict], xml_data: ET.Element ) -> platform_message.MessageChain: """处理引用消息 (data_type=57)""" message_list = [] # print("_handler_compound_quote", ET.tostring(xml_data, encoding='unicode')) appmsg_data = xml_data.find('.//appmsg') - quote_data = "" # 引用原文 - quote_id = None # 引用消息的原发送者 - tousername = None # 接收方: 所属微信的wxid - user_data = "" # 用户消息 - sender_id = xml_data.findtext('.//fromusername') # 发送方:单聊用户/群member + quote_data = '' # 引用原文 + user_data = '' # 用户消息 + sender_id = xml_data.findtext('.//fromusername') # 发送方:单聊用户/群member if appmsg_data: - user_data = appmsg_data.findtext('.//title') or "" + user_data = appmsg_data.findtext('.//title') or '' quote_data = appmsg_data.find('.//refermsg').findtext('.//content') - quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') message_list.append( - platform_message.WeChatForwardQuote( - app_msg=ET.tostring(appmsg_data, encoding='unicode')) - ) - if message: - tousername = message['Wxid'] + platform_message.WeChatForwardQuote(app_msg=ET.tostring(appmsg_data, encoding='unicode')) + ) # quote_data原始的消息 if quote_data: quote_data_message_list = platform_message.MessageChain() # 文本消息 try: - if "" not in quote_data: + if '' not in quote_data: quote_data_message_list.append(platform_message.Plain(quote_data)) else: # 引用消息展开 quote_data_xml = ET.fromstring(quote_data) - if quote_data_xml.find("img"): + if quote_data_xml.find('img'): quote_data_message_list.extend(await self._handler_image(None, quote_data)) - elif quote_data_xml.find("voicemsg"): + elif quote_data_xml.find('voicemsg'): quote_data_message_list.extend(await self._handler_voice(None, quote_data)) - elif quote_data_xml.find("videomsg"): - quote_data_message_list.extend(await self._handler_default(None, quote_data)) # 先不处理 + elif quote_data_xml.find('videomsg'): + quote_data_message_list.extend(await self._handler_default(None, quote_data)) # 先不处理 else: # appmsg quote_data_message_list.extend(await self._handler_compound(None, quote_data)) except Exception as e: - print(f"处理引用消息异常 expcetion:{e}") + print(f'处理引用消息异常 expcetion:{e}') quote_data_message_list.append(platform_message.Plain(quote_data)) message_list.append( platform_message.Quote( @@ -284,7 +278,7 @@ class GewechatMessageConverter(adapter.MessageConverter): pattern = r'@\S{1,20}' user_data = re.sub(pattern, '', user_data) message_list.append(platform_message.Plain(user_data)) - + # for comp in message_list: # if isinstance(comp, platform_message.Quote): # print(f"quote_message_chain len={len(message_list)}") @@ -295,22 +289,12 @@ class GewechatMessageConverter(adapter.MessageConverter): # print(f"quote_message_chain plain [msg_type={comp.type}][message={comp.text}]") return platform_message.MessageChain(message_list) - async def _handler_compound_file( - self, - message: dict, - xml_data: ET.Element - ) -> platform_message.MessageChain: + async def _handler_compound_file(self, message: dict, xml_data: ET.Element) -> platform_message.MessageChain: """处理文件消息 (data_type=6)""" xml_data_str = ET.tostring(xml_data, encoding='unicode') - return platform_message.MessageChain([ - platform_message.WeChatForwardFile(xml_data=xml_data_str) - ]) + return platform_message.MessageChain([platform_message.WeChatForwardFile(xml_data=xml_data_str)]) - async def _handler_compound_link( - self, - message: dict, - xml_data: ET.Element - ) -> platform_message.MessageChain: + async def _handler_compound_link(self, message: dict, xml_data: ET.Element) -> platform_message.MessageChain: """处理链接消息(如公众号文章、外部网页)""" message_list = [] try: @@ -320,185 +304,161 @@ class GewechatMessageConverter(adapter.MessageConverter): return platform_message.MessageChain() message_list.append( platform_message.WeChatLink( - link_title = appmsg.findtext('title', ''), - link_desc = appmsg.findtext('des', ''), - link_url = appmsg.findtext('url', ''), - link_thumb_url = appmsg.findtext("thumburl", '') # 这个字段拿不到 + link_title=appmsg.findtext('title', ''), + link_desc=appmsg.findtext('des', ''), + link_url=appmsg.findtext('url', ''), + link_thumb_url=appmsg.findtext('thumburl', ''), # 这个字段拿不到 ) ) # 转发消息 xml_data_str = ET.tostring(xml_data, encoding='unicode') # print(xml_data_str) - message_list.append( - platform_message.WeChatForwardLink( - xml_data=xml_data_str - ) - ) + message_list.append(platform_message.WeChatForwardLink(xml_data=xml_data_str)) except Exception as e: - print(f"解析链接消息失败: {str(e)}") + print(f'解析链接消息失败: {str(e)}') return platform_message.MessageChain(message_list) async def _handler_compound_mini_program( - self, - message: dict, - xml_data: ET.Element + self, message: dict, xml_data: ET.Element ) -> platform_message.MessageChain: """处理小程序消息(如小程序卡片、服务通知)""" xml_data_str = ET.tostring(xml_data, encoding='unicode') - return platform_message.MessageChain([ - platform_message.WeChatForwardMiniPrograms(xml_data=xml_data_str) - ]) + return platform_message.MessageChain([platform_message.WeChatForwardMiniPrograms(xml_data=xml_data_str)]) - async def _handler_default( - self, - message: Optional[dict], - content_no_preifx: str - ) -> platform_message.MessageChain: + async def _handler_default(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain: """处理未知消息类型""" if message: - msg_type = message["Data"]["MsgType"] + msg_type = message['Data']['MsgType'] else: - msg_type = "" - return platform_message.MessageChain([ - platform_message.Unknown(text=f"[未知消息类型 msg_type:{msg_type}]") - ]) + msg_type = '' + return platform_message.MessageChain([platform_message.Unknown(text=f'[未知消息类型 msg_type:{msg_type}]')]) def _handler_compound_unsupported( - self, - message: dict, - xml_data: str, - text: Optional[str] = None + self, message: dict, xml_data: str, text: Optional[str] = None ) -> platform_message.MessageChain: """处理未支持复合消息类型(msg_type=49)子类型""" if not text: - text = f"[xml_data={xml_data}]" + text = f'[xml_data={xml_data}]' content_list = [] - content_list.append( - platform_message.Unknown(text=f"[处理未支持复合消息类型[msg_type=49]|{text}")) - + content_list.append(platform_message.Unknown(text=f'[处理未支持复合消息类型[msg_type=49]|{text}')) + return platform_message.MessageChain(content_list) # 返回是否被艾特 - def _ats_bot(self, message: dict, bot_account_id:str) -> bool: + def _ats_bot(self, message: dict, bot_account_id: str) -> bool: ats_bot = False try: - to_user_name = message['Wxid'] # 接收方: 所属微信的wxid - raw_content = message["Data"]["Content"]["string"] # 原始消息内容 + to_user_name = message['Wxid'] # 接收方: 所属微信的wxid + raw_content = message['Data']['Content']['string'] # 原始消息内容 content_no_prefix, _ = self._extract_content_and_sender(raw_content) # 直接艾特机器人(这个有bug,当被引用的消息里面有@bot,会套娃 # ats_bot = ats_bot or (f"@{bot_account_id}" in content_no_prefix) # 文本类@bot push_content = message.get('Data', {}).get('PushContent', '') - ats_bot = ats_bot or ('在群聊中@了你' in push_content) + ats_bot = ats_bot or ('在群聊中@了你' in push_content) # 引用别人时@bot msg_source = message.get('Data', {}).get('MsgSource', '') or '' if len(msg_source) > 0: msg_source_data = ET.fromstring(msg_source) - at_user_list = msg_source_data.findtext("atuserlist") or "" + at_user_list = msg_source_data.findtext('atuserlist') or '' ats_bot = ats_bot or (to_user_name in at_user_list) # 引用bot - if message.get('Data', {}).get('MsgType', 0) == 49: + if message.get('Data', {}).get('MsgType', 0) == 49: xml_data = ET.fromstring(content_no_prefix) appmsg_data = xml_data.find('.//appmsg') tousername = message['Wxid'] if appmsg_data: # 接收方: 所属微信的wxid quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') # 引用消息的原发送者 - ats_bot = ats_bot or (quote_id == tousername) + ats_bot = ats_bot or (quote_id == tousername) except Exception as e: - print(f"_ats_bot got except: {e}") + print(f'_ats_bot got except: {e}') finally: return ats_bot - + # 提取一下content前面的sender_id, 和去掉前缀的内容 def _extract_content_and_sender(self, raw_content: str) -> Tuple[str, Optional[str]]: try: # 检查消息开头,如果有 wxid_sbitaz0mt65n22:\n 则删掉 - # add: 有些用户的wxid不是上述格式。换成user_name: - regex = re.compile(r"^[a-zA-Z0-9_\-]{5,20}:") - line_split = raw_content.split("\n") + # add: 有些用户的wxid不是上述格式。换成user_name: + regex = re.compile(r'^[a-zA-Z0-9_\-]{5,20}:') + line_split = raw_content.split('\n') if len(line_split) > 0 and regex.match(line_split[0]): - raw_content = "\n".join(line_split[1:]) - sender_id = line_split[0].strip(":") + raw_content = '\n'.join(line_split[1:]) + sender_id = line_split[0].strip(':') return raw_content, sender_id except Exception as e: - print(f"_extract_content_and_sender got except: {e}") + print(f'_extract_content_and_sender got except: {e}') finally: return raw_content, None # 是否是群消息 - def _is_group_message(self, message: dict)->bool: + def _is_group_message(self, message: dict) -> bool: from_user_name = message['Data']['FromUserName']['string'] - return from_user_name.endswith("@chatroom") + return from_user_name.endswith('@chatroom') + 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 - ) -> platform_events.MessageEvent: + async def target2yiri(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) 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 @@ -516,7 +476,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 @@ -529,24 +489,20 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): async def gewechat_callback(): data = await quart.request.json # print(json.dumps(data, indent=4, ensure_ascii=False)) - self.ap.logger.debug( - f"Gewechat callback event: {data}" - ) - + self.ap.logger.debug(f'Gewechat callback event: {data}') + 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: + except Exception: traceback.print_exc() if event.__class__ in self.listeners: @@ -554,24 +510,18 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): return 'ok' - async def _handle_message( - self, - message: platform_message.MessageChain, - target_id: str - ): + async def _handle_message(self, message: platform_message.MessageChain, target_id: str): """统一消息处理核心逻辑""" content_list = await self.message_converter.yiri2target(message) - at_targets = [item["target"] for item in content_list if item["type"] == "at"] + at_targets = [item['target'] for item in content_list if item['type'] == 'at'] # 处理@逻辑 at_targets = at_targets or [] member_info = [] if at_targets: - member_info = self.bot.get_chatroom_member_detail( - self.config["app_id"], - target_id, - at_targets[::-1] - )["data"] + member_info = self.bot.get_chatroom_member_detail(self.config['app_id'], target_id, at_targets[::-1])[ + 'data' + ] # 处理消息组件 for msg in content_list: @@ -586,24 +536,24 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): app_id=self.config['app_id'], to_wxid=target_id, content=msg['content'], - ats=",".join(at_targets) + ats=','.join(at_targets), ), 'image': lambda msg: self.bot.post_image( app_id=self.config['app_id'], to_wxid=target_id, - img_url=msg["image"] + img_url=msg['image'], ), 'WeChatForwardMiniPrograms': lambda msg: self.bot.forward_mini_app( app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data'], - cover_img_url=msg.get('image_url') + cover_img_url=msg.get('image_url'), ), 'WeChatEmoji': lambda msg: self.bot.post_emoji( app_id=self.config['app_id'], to_wxid=target_id, emoji_md5=msg['emoji_md5'], - emoji_size=msg['emoji_size'] + emoji_size=msg['emoji_size'], ), 'WeChatLink': lambda msg: self.bot.post_link( app_id=self.config['app_id'], @@ -621,49 +571,38 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): page_path=msg['page_path'], cover_img_url=msg['cover_img_url'], title=msg['title'], - user_name=msg['user_name'] + user_name=msg['user_name'], ), 'WeChatForwardLink': lambda msg: self.bot.forward_url( - app_id=self.config['app_id'], - to_wxid=target_id, - xml=msg['xml_data'] + app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data'] ), 'WeChatForwardImage': lambda msg: self.bot.forward_image( - app_id=self.config['app_id'], - to_wxid=target_id, - xml=msg['xml_data'] + app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data'] ), 'WeChatForwardFile': lambda msg: self.bot.forward_file( - app_id=self.config['app_id'], - to_wxid=target_id, - xml=msg['xml_data'] + app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data'] ), 'voice': lambda msg: self.bot.post_voice( app_id=self.config['app_id'], to_wxid=target_id, voice_url=msg['url'], - voice_duration=msg['length'] + voice_duration=msg['length'], ), 'WeChatAppMsg': lambda msg: self.bot.post_app_msg( app_id=self.config['app_id'], to_wxid=target_id, - appmsg=msg['app_msg'] + appmsg=msg['app_msg'], ), - 'at': lambda msg: None + 'at': lambda msg: None, } if handler := handler_map.get(msg['type']): handler(msg) else: - self.ap.logger.warning(f"未处理的消息类型: {msg['type']}") + self.ap.logger.warning(f'未处理的消息类型: {msg["type"]}') continue - 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): """主动发送消息""" return await self._handle_message(message, target_id) @@ -671,11 +610,11 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): self, message_source: platform_events.MessageEvent, message: platform_message.MessageChain, - quote_origin: bool = False + quote_origin: bool = False, ): """回复消息""" if message_source.source_platform_object: - target_id = message_source.source_platform_object["Data"]["FromUserName"]["string"] + target_id = message_source.source_platform_object['Data']['FromUserName']['string'] return await self._handle_message(message, target_id) async def is_muted(self, group_id: int) -> bool: @@ -684,59 +623,53 @@ 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"] - ) + self.bot = gewechat_client.GewechatClient(f'{self.config["gewechat_url"]}/v2/api', self.config['token']) def gewechat_login_process(): - - 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}') 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'] time.sleep(2) try: # gewechat-server容器重启, token会变,但是还会登录成功 # 换新token也会收不到回调,要重新登陆下。 - ret = self.bot.set_callback(self.config["token"], self.config["callback_url"]) - except Exception as e: - raise Exception(f"设置 Gewechat 回调失败, token失效: {e}") - + self.bot.set_callback(self.config['token'], self.config['callback_url']) + except Exception as e: + raise Exception(f'设置 Gewechat 回调失败, token失效: {e}') threading.Thread(target=gewechat_login_process).start() @@ -746,9 +679,9 @@ 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, ) async def kill(self) -> bool: - pass \ No newline at end of file + pass diff --git a/pkg/platform/sources/gewechat.yaml b/pkg/platform/sources/gewechat.yaml index 01967ffc..d9473bc9 100644 --- a/pkg/platform/sources/gewechat.yaml +++ b/pkg/platform/sources/gewechat.yaml @@ -7,7 +7,8 @@ metadata: zh_CN: GeWeChat(个人微信) description: en_US: GeWeChat Adapter - zh_CN: GeWeChat 适配器 + zh_CN: GeWeChat 适配器,请查看文档了解使用方式 + icon: gewechat.png spec: config: - name: gewechat_url @@ -28,7 +29,7 @@ spec: label: en_US: Port zh_CN: 端口 - type: int + type: integer required: true default: 2286 - name: callback_url diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index 2857f5c5..0bf19a23 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -1,61 +1,57 @@ from __future__ import annotations import lark_oapi -from lark_oapi.api.im.v1 import CreateImageRequest, CreateImageRequestBody, CreateImageResponse +from lark_oapi.api.im.v1 import CreateImageRequest, CreateImageRequestBody import traceback 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 @@ -67,22 +63,20 @@ class LarkMessageConverter(adapter.MessageConverter): # Ensure text is valid UTF-8 try: text = msg.text.encode('utf-8').decode('utf-8') - pending_paragraph.append({"tag": "md", "text": text}) + pending_paragraph.append({'tag': 'md', 'text': text}) except UnicodeError: # If text is not valid UTF-8, try to decode with other encodings try: text = msg.text.encode('latin1').decode('utf-8') - pending_paragraph.append({"tag": "md", "text": text}) + pending_paragraph.append({'tag': 'md', 'text': text}) except UnicodeError: # If still fails, replace invalid characters text = msg.text.encode('utf-8', errors='replace').decode('utf-8') - pending_paragraph.append({"tag": "md", "text": text}) + pending_paragraph.append({'tag': 'md', 'text': text}) elif isinstance(msg, platform_message.At): - pending_paragraph.append( - {"tag": "at", "user_id": msg.target, "style": []} - ) + pending_paragraph.append({'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 @@ -92,7 +86,7 @@ class LarkMessageConverter(adapter.MessageConverter): if msg.base64.startswith('data:'): msg.base64 = msg.base64.split(',', 1)[1] image_bytes = base64.b64decode(msg.base64) - except Exception as e: + except Exception: traceback.print_exc() continue elif msg.url: @@ -104,14 +98,14 @@ class LarkMessageConverter(adapter.MessageConverter): else: traceback.print_exc() continue - except Exception as e: + except Exception: traceback.print_exc() continue elif msg.path: try: - with open(msg.path, "rb") as f: + with open(msg.path, 'rb') as f: image_bytes = f.read() - except Exception as e: + except Exception: traceback.print_exc() continue @@ -121,45 +115,49 @@ class LarkMessageConverter(adapter.MessageConverter): try: # Create a temporary file to store the image bytes import tempfile + with tempfile.NamedTemporaryFile(delete=False) as temp_file: temp_file.write(image_bytes) temp_file.flush() - + # Create image request using the temporary file - request = CreateImageRequest.builder()\ + request = ( + CreateImageRequest.builder() .request_body( CreateImageRequestBody.builder() - .image_type("message") - .image(open(temp_file.name, "rb")) + .image_type('message') + .image(open(temp_file.name, 'rb')) .build() - )\ + ) .build() - + ) + response = await api_client.im.v1.image.acreate(request) - + 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 message_elements.append(pending_paragraph) message_elements.append( [ { - "tag": "img", - "image_key": image_key, + 'tag': 'img', + 'image_key': image_key, } ] ) pending_paragraph = [] - except Exception as e: + except Exception: traceback.print_exc() continue finally: # Clean up the temporary file import os + if 'temp_file' in locals(): os.unlink(temp_file.name) elif isinstance(msg, platform_message.Forward): @@ -180,23 +178,19 @@ class LarkMessageConverter(adapter.MessageConverter): lb_msg_list = [] - msg_create_time = datetime.datetime.fromtimestamp( - int(message.create_time) / 1000 - ) + msg_create_time = datetime.datetime.fromtimestamp(int(message.create_time) / 1000) - lb_msg_list.append( - platform_message.Source(id=message.message_id, time=msg_create_time) - ) + lb_msg_list.append(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: @@ -209,94 +203,79 @@ 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 = [] left_text = text_split[0] right_text = text_split[1] - new_list.extend( - text_element_recur({"tag": "text", "text": left_text, "style": []}) - ) + new_list.extend(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": []}) - ) + new_list.extend(text_element_recur({'tag': 'text', 'text': right_text, 'style': []})) return new_list - element_list = text_element_recur( - {"tag": "text", "text": message_content["text"], "style": []} - ) + element_list = text_element_recur({'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() ) - response: GetMessageResourceResponse = ( - await api_client.im.v1.message_resource.aget(request) - ) + response: GetMessageResourceResponse = await api_client.im.v1.message_resource.aget(request) 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}" - ) - ) + lb_msg_list.append(platform_message.Image(base64=f'data:{image_format};base64,{image_base64}')) return platform_message.MessageChain(lb_msg_list) class LarkEventConverter(adapter.EventConverter): - @staticmethod async def yiri2target( event: platform_events.MessageEvent, @@ -307,21 +286,19 @@ class LarkEventConverter(adapter.EventConverter): async def target2yiri( event: lark_oapi.im.v1.P2ImMessageReceiveV1, api_client: lark_oapi.Client ) -> platform_events.Event: - message_chain = await LarkMessageConverter.target2yiri( - event.event.message, api_client - ) + message_chain = await LarkMessageConverter.target2yiri(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, @@ -329,10 +306,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, @@ -343,7 +320,6 @@ class LarkEventConverter(adapter.EventConverter): class LarkAdapter(adapter.MessagePlatformAdapter): - bot: lark_oapi.ws.Client api_client: lark_oapi.Client @@ -372,25 +348,21 @@ class LarkAdapter(adapter.MessagePlatformAdapter): try: data = await quart.request.json - self.ap.logger.debug( - f"Lark callback event: {data}" - ) + self.ap.logger.debug(f'Lark callback event: {data}') if 'encrypt' in data: cipher = AESCipher(self.config['encrypt-key']) 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: # todo 验证verification token - return { - "challenge": data.get("challenge") - } + return {'challenge': data.get('challenge')} context = EventContext(data) type = context.header.event_type p2v1 = P2ImMessageReceiveV1() @@ -403,19 +375,18 @@ class LarkAdapter(adapter.MessagePlatformAdapter): if 'im.message.receive_v1' == type: try: event = await self.event_converter.target2yiri(p2v1, self.api_client) - except Exception as e: + 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) @@ -424,26 +395,15 @@ class LarkAdapter(adapter.MessagePlatformAdapter): asyncio.create_task(on_message(event)) event_handler = ( - lark_oapi.EventDispatcherHandler.builder("", "") - .register_p2_im_message_receive_v1(sync_on_message) - .build() + 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 - ) - self.api_client = ( - lark_oapi.Client.builder() - .app_id(config["app_id"]) - .app_secret(config["app_secret"]) - .build() - ) + self.bot = lark_oapi.ws.Client(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']).build() - 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): pass async def reply_message( @@ -452,17 +412,14 @@ 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( - message, self.api_client - ) + lark_message = await self.message_converter.yiri2target(message, self.api_client) final_content = { - "zh_cn": { - "title": "", - "content": lark_message, + 'zh_cn': { + 'title': '', + 'content': lark_message, }, } @@ -472,7 +429,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() @@ -480,13 +437,11 @@ class LarkAdapter(adapter.MessagePlatformAdapter): .build() ) - response: ReplyMessageResponse = await self.api_client.im.v1.message.areply( - request - ) + response: ReplyMessageResponse = await self.api_client.im.v1.message.areply(request) 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: @@ -495,18 +450,14 @@ class LarkAdapter(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) @@ -526,6 +477,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): else: raise e else: + async def shutdown_trigger_placeholder(): while True: await asyncio.sleep(1) @@ -535,5 +487,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/lark.svg b/pkg/platform/sources/lark.svg new file mode 100644 index 00000000..bf3c202a --- /dev/null +++ b/pkg/platform/sources/lark.svg @@ -0,0 +1 @@ + diff --git a/pkg/platform/sources/lark.yaml b/pkg/platform/sources/lark.yaml index 6170e367..c9bcbc69 100644 --- a/pkg/platform/sources/lark.yaml +++ b/pkg/platform/sources/lark.yaml @@ -7,7 +7,8 @@ metadata: zh_CN: 飞书 description: en_US: Lark Adapter - zh_CN: 飞书适配器 + zh_CN: 飞书适配器,请查看文档了解使用方式 + icon: lark.svg spec: config: - name: app_id @@ -35,6 +36,9 @@ spec: label: en_US: Enable Webhook Mode zh_CN: 启用Webhook模式 + description: + en_US: If enabled, the bot will use webhook mode to receive messages. Otherwise, it will use WS long connection mode + zh_CN: 如果启用,机器人将使用 Webhook 模式接收消息。否则,将使用 WS 长连接模式 type: boolean required: true default: false @@ -42,13 +46,19 @@ spec: label: en_US: Webhook Port zh_CN: Webhook端口 - type: int + description: + en_US: Only valid when webhook mode is enabled, please fill in the webhook port + zh_CN: 仅在启用 Webhook 模式时有效,请填写 Webhook 端口 + type: integer required: true default: 2285 - name: encrypt-key label: en_US: Encrypt Key zh_CN: 加密密钥 + description: + en_US: Only valid when webhook mode is enabled, please fill in the encrypt key + zh_CN: 仅在启用 Webhook 模式时有效,请填写加密密钥 type: string required: true default: "" diff --git a/pkg/platform/sources/nakuru.png b/pkg/platform/sources/nakuru.png new file mode 100644 index 00000000..0101afc4 Binary files /dev/null and b/pkg/platform/sources/nakuru.png differ diff --git a/pkg/platform/sources/nakuru.py b/pkg/platform/sources/nakuru.py index 8dcf6e52..44e2d301 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,10 @@ 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: @@ -65,18 +65,21 @@ class NakuruProjectMessageConverter(adapter_model.MessageConverter): 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 @@ -86,6 +89,7 @@ class NakuruProjectMessageConverter(adapter_model.MessageConverter): yiri_msg_list = [] import datetime + # 添加Source组件以标记message_id等信息 yiri_msg_list.append(platform_message.Source(id=message_id, time=datetime.datetime.now())) for component in message_chain: @@ -106,6 +110,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,7 +118,7 @@ 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: @@ -123,18 +128,18 @@ class NakuruProjectEventConverter(adapter_model.EventConverter): 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 +149,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 +157,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 +192,12 @@ 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 - + # 检查是否有转发消息 has_forward = False for msg in converted_msg: @@ -200,19 +206,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,34 +226,27 @@ 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 - ) + await self.send_message('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 - ) + await self.send_message('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)) return group_member_info.shut_up_timestamp > int(time.time()) @@ -255,10 +254,9 @@ class NakuruAdapter(adapter_model.MessagePlatformAdapter): 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 +266,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 +281,7 @@ 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 +290,13 @@ 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 +307,22 @@ 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']), - headers={ - 'Authorization': "Bearer " + self.cfg['token'] if 'token' in self.cfg else "" - }, + 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 ''}, 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/nakuru.yaml b/pkg/platform/sources/nakuru.yaml index b64e191b..4d1bdeff 100644 --- a/pkg/platform/sources/nakuru.yaml +++ b/pkg/platform/sources/nakuru.yaml @@ -7,7 +7,8 @@ metadata: zh_CN: Nakuru description: en_US: Nakuru Adapter - zh_CN: Nakuru 适配器(go-cqhttp) + zh_CN: Nakuru 适配器(go-cqhttp),请查看文档了解使用方式 + icon: nakuru.png spec: config: - name: host @@ -21,14 +22,14 @@ spec: label: en_US: HTTP Port zh_CN: HTTP端口 - type: int + type: integer required: true default: 5700 - name: ws_port label: en_US: WebSocket Port zh_CN: WebSocket端口 - type: int + type: integer required: true default: 8080 - name: token diff --git a/pkg/platform/sources/officialaccount.png b/pkg/platform/sources/officialaccount.png new file mode 100644 index 00000000..24746e1d Binary files /dev/null and b/pkg/platform/sources/officialaccount.png differ diff --git a/pkg/platform/sources/officialaccount.py b/pkg/platform/sources/officialaccount.py index 0816824f..8c7831a5 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,117 +21,112 @@ 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()) - ) + yiri_msg_list.append(platform_message.Source(id=message_id, time=datetime.datetime.now())) yiri_msg_list.append(platform_message.Plain(text=message)) chain = platform_message.MessageChain(yiri_msg_list) return chain - + class OAEventConverter(adapter.EventConverter): @staticmethod - async def target2yiri(event:OAEvent): - if event.type == "text": - yiri_chain = await OAMessageConverter.target2yiri( - event.message, event.message_id - ) + async def target2yiri(event: OAEvent): + if event.type == 'text': + yiri_chain = await OAMessageConverter.target2yiri(event.message, event.message_id) 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: - from_user = message_source.sender.id - 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 + 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) + + 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: + return await callback(await self.event_converter.target2yiri(event), self) + 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 +136,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, ) @@ -162,5 +150,3 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter): 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/officialaccount.yaml b/pkg/platform/sources/officialaccount.yaml index dbd84a77..09337bb9 100644 --- a/pkg/platform/sources/officialaccount.yaml +++ b/pkg/platform/sources/officialaccount.yaml @@ -7,7 +7,8 @@ metadata: zh_CN: 微信公众号 description: en_US: Official Account Adapter - zh_CN: 微信公众号适配器 + zh_CN: 微信公众号适配器,请查看文档了解使用方式 + icon: officialaccount.png spec: config: - name: token @@ -38,6 +39,20 @@ spec: type: string required: true default: "" + - name: Mode + label: + en_US: Mode + zh_CN: 接入模式 + type: string + required: true + default: "drop" + - name: LoadingMessage + label: + en_US: Loading Message + zh_CN: 加载消息 + type: string + required: true + default: "AI正在思考中,请发送任意内容获取回复。" - name: host label: en_US: Host @@ -49,7 +64,7 @@ spec: label: en_US: Port zh_CN: 监听端口 - type: int + type: integer required: true default: 2287 execution: diff --git a/pkg/platform/sources/onebot.png b/pkg/platform/sources/onebot.png new file mode 100644 index 00000000..5144648c Binary files /dev/null and b/pkg/platform/sources/onebot.png differ diff --git a/pkg/platform/sources/qqbotpy.py b/pkg/platform/sources/qqbotpy.py index 9f407b7f..74699961 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 @@ -134,9 +147,7 @@ class OfficialMessageConverter(adapter_model.MessageConverter): elif type(message_chain) is str: msg_list = [platform_message.Plain(text=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))) offcial_messages: list[dict] = [] """ @@ -154,24 +165,18 @@ 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} - ) + offcial_messages.append({'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 不支持此消息类型,忽略此消息。" - ) + print('上层组件要求发送 AtAll 消息,但 QQ 官方 API 不支持此消息类型,忽略此消息。') elif type(component) is platform_message.Voice: - print( - "上层组件要求发送 Voice 消息,但 QQ 官方 API 不支持此消息类型,忽略此消息。" - ) + print('上层组件要求发送 Voice 消息,但 QQ 官方 API 不支持此消息类型,忽略此消息。') elif type(component) is forward.Forward: # 转发消息 yiri_forward_node_list = component.node_list @@ -182,10 +187,8 @@ class OfficialMessageConverter(adapter_model.MessageConverter): message_chain = yiri_forward_node.message_chain # 平铺 - offcial_messages.extend( - OfficialMessageConverter.yiri2target(message_chain) - ) - except Exception as e: + offcial_messages.extend(OfficialMessageConverter.yiri2target(message_chain)) + except Exception: import traceback traceback.print_exc() @@ -194,23 +197,24 @@ 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: yiri_msg_list = [] # 存id - yiri_msg_list.append( - platform_message.Source( - id=save_msg_id(message_id), time=datetime.datetime.now() - ) - ) + yiri_msg_list.append(platform_message.Source(id=save_msg_id(message_id), time=datetime.datetime.now())) 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 +222,13 @@ 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 + ",忽略此附件。" - ) + logging.warning('不支持的附件类型:' + 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) @@ -237,12 +239,8 @@ class OfficialMessageConverter(adapter_model.MessageConverter): class OfficialEventConverter(adapter_model.EventConverter): """事件转换器""" - member_openid_mapping: OpenIDMapping[str, int] - group_openid_mapping: OpenIDMapping[str, int] - - def __init__(self, member_openid_mapping: OpenIDMapping[str, int], group_openid_mapping: OpenIDMapping[str, int]): - self.member_openid_mapping = member_openid_mapping - self.group_openid_mapping = group_openid_mapping + def __init__(self): + pass def yiri2target(self, event: typing.Type[platform_events.Event]): if event == platform_events.GroupMessage: @@ -250,22 +248,24 @@ class OfficialEventConverter(adapter_model.EventConverter): elif event == platform_events.FriendMessage: return botpy_message.DirectMessage else: - raise Exception( - "未支持转换的事件类型(YiriMirai -> Official): " + str(event) - ) + raise Exception('未支持转换的事件类型(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( @@ -277,71 +277,49 @@ 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" - ).timestamp() + datetime.datetime.strptime(event.member.joined_at, '%Y-%m-%dT%H:%M:%S%z').timestamp() ), last_speak_timestamp=datetime.datetime.now().timestamp(), mute_time_remaining=0, ), - message_chain=OfficialMessageConverter.extract_message_chain_from_obj( - event, event.id - ), - time=int( - datetime.datetime.strptime( - event.timestamp, "%Y-%m-%dT%H:%M:%S%z" - ).timestamp() - ), + message_chain=OfficialMessageConverter.extract_message_chain_from_obj(event, event.id), + time=int(datetime.datetime.strptime(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, nickname=event.author.username, remark=event.author.username, ), - message_chain=OfficialMessageConverter.extract_message_chain_from_obj( - event, event.id - ), - time=int( - datetime.datetime.strptime( - event.timestamp, "%Y-%m-%dT%H:%M:%S%z" - ).timestamp() - ), + message_chain=OfficialMessageConverter.extract_message_chain_from_obj(event, event.id), + time=int(datetime.datetime.strptime(event.timestamp, '%Y-%m-%dT%H:%M:%S%z').timestamp()), ) - elif type(event) == botpy_message.GroupMessage: # 群聊,转群聊事件 - - replacing_member_id = self.member_openid_mapping.save_openid(event.author.member_openid) + elif isinstance(event, botpy_message.GroupMessage): # 群聊,转群聊事件 + author_member_id = event.author.member_openid return OfficialGroupMessage( sender=platform_entities.GroupMember( - id=replacing_member_id, - member_name=replacing_member_id, - permission="MEMBER", + id=author_member_id, + member_name=author_member_id, + permission='MEMBER', group=platform_entities.Group( - id=self.group_openid_mapping.save_openid(event.group_openid), - name=replacing_member_id, + 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, ), - message_chain=OfficialMessageConverter.extract_message_chain_from_obj( - event, event.id - ), - time=int( - datetime.datetime.strptime( - event.timestamp, "%Y-%m-%dT%H:%M:%S%z" - ).timestamp() - ), + message_chain=OfficialMessageConverter.extract_message_chain_from_obj(event, event.id), + time=int(datetime.datetime.strptime(event.timestamp, '%Y-%m-%dT%H:%M:%S%z').timestamp()), ) - elif type(event) == botpy_message.C2CMessage: # 私聊,转私聊事件 - - user_id_alter = self.member_openid_mapping.save_openid(event.author.user_openid) # 实测这里的user_openid与group的member_openid是一样的 + elif isinstance(event, botpy_message.C2CMessage): # 私聊,转私聊事件 + user_id_alter = event.author.user_openid return OfficialFriendMessage( sender=platform_entities.Friend( @@ -349,14 +327,8 @@ class OfficialEventConverter(adapter_model.EventConverter): nickname=user_id_alter, remark=user_id_alter, ), - message_chain=OfficialMessageConverter.extract_message_chain_from_obj( - event, event.id - ), - time=int( - datetime.datetime.strptime( - event.timestamp, "%Y-%m-%dT%H:%M:%S%z" - ).timestamp() - ), + message_chain=OfficialMessageConverter.extract_message_chain_from_obj(event, event.id), + time=int(datetime.datetime.strptime(event.timestamp, '%Y-%m-%dT%H:%M:%S%z').timestamp()), ) @@ -382,9 +354,6 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter): metadata: cfg_mgr.ConfigManager = None - member_openid_mapping: OpenIDMapping[str, int] = None - group_openid_mapping: OpenIDMapping[str, int] = None - group_msg_seq = None c2c_msg_seq = None @@ -398,38 +367,36 @@ 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) self.bot = botpy.Client(intents=intents) - 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): 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 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) @@ -439,90 +406,72 @@ 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( - message_id=cached_message_ids[ - str(message_source.message_chain.message_id) - ] + 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[ - str(message_source.message_chain.message_id) - ] + 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[ - str(message_source.message_chain.message_id) - ] + 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"] = self.group_openid_mapping.getkey( - 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[ - str(message_source.message_chain.message_id) - ] - args["msg_seq"] = self.group_msg_seq + args['msg_id'] = cached_message_ids[str(message_source.message_chain.message_id)] + 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"] = self.member_openid_mapping.getkey( - 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[ - str(message_source.message_chain.message_id) - ] + 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) @@ -533,11 +482,8 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter): 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: async def wrapper( @@ -545,7 +491,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) @@ -559,34 +505,19 @@ class OfficialAdapter(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], ): delattr(self.bot, event_handler_mapping[event_type]) async def run_async(self): - self.metadata = self.ap.adapter_qq_botpy_meta - self.member_openid_mapping = OpenIDMapping( - map=self.metadata.data["mapping"]["members"], - dump_func=self.metadata.dump_config_sync, - ) - - self.group_openid_mapping = OpenIDMapping( - map=self.metadata.data["mapping"]["groups"], - dump_func=self.metadata.dump_config_sync, - ) - self.message_converter = OfficialMessageConverter() - self.event_converter = OfficialEventConverter( - self.member_openid_mapping, self.group_openid_mapping - ) + self.event_converter = OfficialEventConverter() 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/qqbotpy.svg b/pkg/platform/sources/qqbotpy.svg new file mode 100644 index 00000000..d0a07bcb --- /dev/null +++ b/pkg/platform/sources/qqbotpy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pkg/platform/sources/qqbotpy.yaml b/pkg/platform/sources/qqbotpy.yaml index 79653194..524e7cdc 100644 --- a/pkg/platform/sources/qqbotpy.yaml +++ b/pkg/platform/sources/qqbotpy.yaml @@ -7,7 +7,8 @@ metadata: zh_CN: QQBotPy description: en_US: QQ Official API (WebSocket) - zh_CN: QQ 官方 API (WebSocket) + zh_CN: QQ 官方 API (WebSocket),请查看文档了解使用方式 + icon: qqbotpy.svg spec: config: - name: appid @@ -28,9 +29,11 @@ spec: label: en_US: Intents zh_CN: 权限 - type: array[string] + type: array required: true default: [] + items: + type: string execution: python: path: ./qqbotpy.py diff --git a/pkg/platform/sources/qqofficial.py b/pkg/platform/sources/qqofficial.py index bfef2135..f9795bcd 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,144 @@ 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()) - ) + yiri_msg_list.append(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( - datetime.datetime.strptime( - event.timestamp, "%Y-%m-%dT%H:%M:%S%z" - ).timestamp() - ), - source_platform_object=event + sender=friend, + message_chain=yiri_chain, + time=int(datetime.datetime.strptime(event.timestamp, '%Y-%m-%dT%H:%M:%S%z').timestamp()), + source_platform_object=event, ) - + if event.t == 'DIRECT_MESSAGE_CREATE': friend = platform_entities.Friend( - id = event.guild_id, - nickname = event.t, - remark = "", - ) - return platform_events.FriendMessage( - sender = friend,message_chain = yiri_chain, - source_platform_object=event + id=event.guild_id, + nickname=event.t, + remark='', ) + return platform_events.FriendMessage(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() - ) + 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 + 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,60 +169,67 @@ 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 - ): + async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): pass 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: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 - ) - except: + return await callback(await self.event_converter.target2yiri(event), self) + 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 +238,16 @@ 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], ): return super().unregister_listener(event_type, callback) - diff --git a/pkg/platform/sources/qqofficial.svg b/pkg/platform/sources/qqofficial.svg new file mode 100644 index 00000000..d0a07bcb --- /dev/null +++ b/pkg/platform/sources/qqofficial.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pkg/platform/sources/qqofficial.yaml b/pkg/platform/sources/qqofficial.yaml index 4d7430ea..61881e29 100644 --- a/pkg/platform/sources/qqofficial.yaml +++ b/pkg/platform/sources/qqofficial.yaml @@ -7,7 +7,8 @@ metadata: zh_CN: QQ 官方 API description: en_US: QQ Official API (Webhook) - zh_CN: QQ 官方 API (Webhook) + zh_CN: QQ 官方 API (Webhook),请查看文档了解使用方式 + icon: qqofficial.svg spec: config: - name: appid @@ -28,7 +29,7 @@ spec: label: en_US: Port zh_CN: 监听端口 - type: int + type: integer required: true default: 2284 - name: token diff --git a/pkg/platform/sources/slack.png b/pkg/platform/sources/slack.png new file mode 100644 index 00000000..91d92fe2 Binary files /dev/null and b/pkg/platform/sources/slack.png differ diff --git a/pkg/platform/sources/slack.py b/pkg/platform/sources/slack.py index bc4e4d8e..62ef4137 100644 --- a/pkg/platform/sources/slack.py +++ b/pkg/platform/sources/slack.py @@ -11,37 +11,32 @@ from pkg.platform.types import events as platform_events, message as platform_me from libs.slack_api.slackevent import SlackEvent 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 SlackMessageConverter(adapter.MessageConverter): - @staticmethod - async def yiri2target(message_chain:platform_message.MessageChain): + 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({ - "content":msg.text, - }) - + content_list.append( + { + 'content': msg.text, + } + ) + return content_list @staticmethod - async def target2yiri(message:str,message_id:str,pic_url:str,bot:SlackClient): + async def target2yiri(message: str, message_id: str, pic_url: str, bot: SlackClient): yiri_msg_list = [] - 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())) if pic_url is not None: - base64_url = await image.get_slack_image_to_base64(pic_url=pic_url,bot_token=bot.bot_token) - yiri_msg_list.append( - platform_message.Image(base64=base64_url) - ) + base64_url = await image.get_slack_image_to_base64(pic_url=pic_url, bot_token=bot.bot_token) + 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) @@ -49,55 +44,43 @@ class SlackMessageConverter(adapter.MessageConverter): class SlackEventConverter(adapter.EventConverter): - @staticmethod - async def yiri2target(event:platform_events.MessageEvent) -> SlackEvent: + async def yiri2target(event: platform_events.MessageEvent) -> SlackEvent: return event.source_platform_object - + @staticmethod - async def target2yiri(event:SlackEvent,bot:SlackClient): + async def target2yiri(event: SlackEvent, bot: SlackClient): yiri_chain = await SlackMessageConverter.target2yiri( - message=event.text,message_id=event.message_id,pic_url=event.pic_url,bot=bot + message=event.text, message_id=event.message_id, pic_url=event.pic_url, bot=bot ) if event.type == 'channel': - yiri_chain.insert(0, platform_message.At(target="SlackBot")) + yiri_chain.insert(0, platform_message.At(target='SlackBot')) sender = platform_entities.GroupMember( - id = event.user_id, - member_name= str(event.sender_name), - permission= 'MEMBER', - group = platform_entities.Group( - id = event.channel_id, - name = 'MEMBER', - permission= platform_entities.Permission.Member + id=event.user_id, + member_name=str(event.sender_name), + 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 + mute_time_remaining=0, ) time = int(datetime.datetime.utcnow().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.type == 'im': return platform_events.FriendMessage( - sender=platform_entities.Friend( - id=event.user_id, - nickname = event.sender_name, - remark="" - ), - message_chain = yiri_chain, - time = float(datetime.datetime.now().timestamp()), + sender=platform_entities.Friend(id=event.user_id, nickname=event.sender_name, remark=''), + message_chain=yiri_chain, + time=float(datetime.datetime.now().timestamp()), source_platform_object=event, ) - - class SlackAdapter(adapter.MessagePlatformAdapter): @@ -108,21 +91,18 @@ class SlackAdapter(adapter.MessagePlatformAdapter): event_converter: SlackEventConverter = SlackEventConverter() 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 = [ - "bot_token", - "signing_secret", + 'bot_token', + 'signing_secret', ] missing_keys = [key for key in required_keys if key not in config] if missing_keys: - raise ParamNotEnoughError("Slack机器人缺少相关配置项,请查看文档或联系管理员") + raise ParamNotEnoughError('Slack机器人缺少相关配置项,请查看文档或联系管理员') - self.bot = SlackClient( - bot_token=self.config["bot_token"], - signing_secret=self.config["signing_secret"] - ) + self.bot = SlackClient(bot_token=self.config['bot_token'], signing_secret=self.config['signing_secret']) async def reply_message( self, @@ -130,52 +110,40 @@ class SlackAdapter(adapter.MessagePlatformAdapter): message: platform_message.MessageChain, quote_origin: bool = False, ): - slack_event = await SlackEventConverter.yiri2target( - message_source - ) + slack_event = await SlackEventConverter.yiri2target(message_source) - content_list = await SlackMessageConverter.yiri2target(message) + content_list = await SlackMessageConverter.yiri2target(message) for content in content_list: if slack_event.type == 'channel': - await self.bot.send_message_to_channel( - content['content'],slack_event.channel_id - ) + await self.bot.send_message_to_channel(content['content'], slack_event.channel_id) if slack_event.type == 'im': - await self.bot.send_message_to_one( - content['content'],slack_event.user_id - ) - + await self.bot.send_message_to_one(content['content'], slack_event.user_id) + async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): content_list = await SlackMessageConverter.yiri2target(message) for content in content_list: if target_type == 'person': - await self.bot.send_message_to_one(content['content'],target_id) + await self.bot.send_message_to_one(content['content'], target_id) if target_type == 'group': - await self.bot.send_message_to_channel(content['content'],target_id) - + await self.bot.send_message_to_channel(content['content'], target_id) 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:SlackEvent): + async def on_message(event: SlackEvent): self.bot_account_id = 'SlackBot' try: - return await callback( - await self.event_converter.target2yiri(event,self.bot),self - ) + return await callback(await self.event_converter.target2yiri(event, self.bot), self) except: traceback.print_exc() - - if event_type == platform_events.FriendMessage: - self.bot.on_message("im")(on_message) - elif event_type == platform_events.GroupMessage: - self.bot.on_message("channel")(on_message) + if event_type == platform_events.FriendMessage: + self.bot.on_message('im')(on_message) + elif event_type == platform_events.GroupMessage: + self.bot.on_message('channel')(on_message) async def run_async(self): async def shutdown_trigger_placeholder(): @@ -183,8 +151,8 @@ class SlackAdapter(adapter.MessagePlatformAdapter): await asyncio.sleep(1) await self.bot.run_task( - host="0.0.0.0", - port=self.config["port"], + host='0.0.0.0', + port=self.config['port'], shutdown_trigger=shutdown_trigger_placeholder, ) @@ -197,8 +165,3 @@ class SlackAdapter(adapter.MessagePlatformAdapter): callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], ): return super().unregister_listener(event_type, callback) - - - - - diff --git a/pkg/platform/sources/slack.yaml b/pkg/platform/sources/slack.yaml index 7b16960c..078b37f5 100644 --- a/pkg/platform/sources/slack.yaml +++ b/pkg/platform/sources/slack.yaml @@ -3,11 +3,12 @@ kind: MessagePlatformAdapter metadata: name: slack label: - en_US: Slack API - zh_CN: Slack API + en_US: Slack + zh_CN: Slack description: - en_US: Slack API - zh_CN: Slack API + en_US: Slack Adapter + zh_CN: Slack 适配器,请查看文档了解使用方式 + icon: slack.png spec: config: - name: bot_token diff --git a/pkg/platform/sources/telegram.py b/pkg/platform/sources/telegram.py index b463c6b3..5d318cbb 100644 --- a/pkg/platform/sources/telegram.py +++ b/pkg/platform/sources/telegram.py @@ -3,33 +3,20 @@ 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 telegramify_markdown 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): @@ -39,12 +26,8 @@ class TelegramMessageConverter(adapter.MessageConverter): 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,25 +37,20 @@ 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)) return 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 = [] @@ -101,21 +79,24 @@ 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) - + if event.effective_chat.type == 'private': return platform_events.FriendMessage( sender=platform_entities.Friend( @@ -125,9 +106,9 @@ 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' or 'supergroup' : + elif event.effective_chat.type == 'group' or 'supergroup': return platform_events.GroupMessage( sender=platform_entities.GroupMember( id=event.effective_chat.id, @@ -138,19 +119,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 @@ -166,29 +146,28 @@ 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) 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)) - - async def send_message( - self, target_type: str, target_id: str, message: platform_message.MessageChain - ): + 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): pass async def reply_message( @@ -199,52 +178,50 @@ 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': if self.config['markdown_card'] is True: content = telegramify_markdown.markdownify( - content= component['text'], + content=component['text'], ) else: content = component['text'] args = { - "chat_id": message_source.source_platform_object.effective_chat.id, - "text": content, + 'chat_id': message_source.source_platform_object.effective_chat.id, + 'text': content, } if self.config['markdown_card'] is True: - args["parse_mode"] = "MarkdownV2" + args['parse_mode'] = 'MarkdownV2' if quote_origin: 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], ): 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], ): 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/telegram.svg b/pkg/platform/sources/telegram.svg new file mode 100644 index 00000000..acd7fce6 --- /dev/null +++ b/pkg/platform/sources/telegram.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pkg/platform/sources/telegram.yaml b/pkg/platform/sources/telegram.yaml index 67dfb5bc..a83c5fa8 100644 --- a/pkg/platform/sources/telegram.yaml +++ b/pkg/platform/sources/telegram.yaml @@ -7,7 +7,8 @@ metadata: zh_CN: 电报 description: en_US: Telegram Adapter - zh_CN: 电报适配器 + zh_CN: 电报适配器,请查看文档了解使用方式 + icon: telegram.svg spec: config: - name: token @@ -17,6 +18,13 @@ spec: type: string required: true default: "" + - name: markdown_card + label: + en_US: Markdown Card + zh_CN: 是否使用 Markdown 卡片 + type: boolean + required: false + default: true execution: python: path: ./telegram.py diff --git a/pkg/platform/sources/wecom.png b/pkg/platform/sources/wecom.png new file mode 100644 index 00000000..8588c20d Binary files /dev/null and b/pkg/platform/sources/wecom.png differ diff --git a/pkg/platform/sources/wecom.py b/pkg/platform/sources/wecom.py index 40632595..5c02a632 100644 --- a/pkg/platform/sources/wecom.py +++ b/pkg/platform/sources/wecom.py @@ -9,51 +9,50 @@ 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 - ): + async def yiri2target(message_chain: platform_message.MessageChain, bot: WecomClient): 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, + } + ) 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))) else: - content_list.append({ - "type": "text", - "content": str(msg), - }) + content_list.append( + { + 'type': 'text', + 'content': str(msg), + } + ) return content_list @staticmethod async def target2yiri(message: str, message_id: int = -1): yiri_msg_list = [] - 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())) yiri_msg_list.append(platform_message.Plain(text=message)) chain = platform_message.MessageChain(yiri_msg_list) @@ -63,40 +62,34 @@ class WecomMessageConverter(adapter.MessageConverter): @staticmethod async def target2yiri_image(picurl: str, message_id: int = -1): yiri_msg_list = [] - 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())) 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 - ) -> WecomEvent: + async def yiri2target(event: platform_events.Event, bot_account_id: int, bot: WecomClient) -> WecomEvent: # only for extracting user information if type(event) is platform_events.GroupMessage: 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,37 +105,28 @@ class WecomEventConverter: platform_events.FriendMessage: 转换后的 FriendMessage 对象。 """ # 转换消息链 - if event.type == "text": - yiri_chain = await WecomMessageConverter.target2yiri( - event.message, event.message_id - ) + 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": + return platform_events.FriendMessage(sender=friend, message_chain=yiri_chain, time=event.timestamp) + 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( - picurl=event.picurl, message_id=event.message_id - ) + yiri_chain = await WecomMessageConverter.target2yiri_image(picurl=event.picurl, message_id=event.message_id) - return platform_events.FriendMessage( - sender=friend, message_chain=yiri_chain, time=event.timestamp - ) + return platform_events.FriendMessage(sender=friend, message_chain=yiri_chain, time=event.timestamp) class WecomAdapter(adapter.MessagePlatformAdapter): - bot: WecomClient ap: app.Application bot_account_id: str @@ -156,22 +140,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,56 +164,47 @@ 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 - ) + Wecom_event = await WecomEventConverter.yiri2target(message_source, self.bot_account_id, self.bot) content_list = await WecomMessageConverter.yiri2target(message, self.bot) fixed_user_id = Wecom_event.user_id # 删掉开头的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"]) - - async def send_message( - self, target_type: str, target_id: str, message: platform_message.MessageChain - ): + 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): """企业微信目前只有发送给个人的方法, 构造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, 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: WecomEvent): self.bot_account_id = event.receiver_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.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 +214,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, ) diff --git a/pkg/platform/sources/wecom.yaml b/pkg/platform/sources/wecom.yaml index 6b6c26eb..68f73e8a 100644 --- a/pkg/platform/sources/wecom.yaml +++ b/pkg/platform/sources/wecom.yaml @@ -7,13 +7,17 @@ metadata: zh_CN: 企业微信 description: en_US: WeCom Adapter - zh_CN: 企业微信适配器 + zh_CN: 企业微信适配器,请查看文档了解使用方式 + icon: wecom.png spec: config: - name: host label: en_US: Host zh_CN: 监听主机 + description: + en_US: Webhook host, unless you know what you're doing, please write 0.0.0.0 + zh_CN: Webhook 监听主机,除非你知道自己在做什么,否则请写 0.0.0.0 type: string required: true default: "0.0.0.0" @@ -21,7 +25,7 @@ spec: label: en_US: Port zh_CN: 监听端口 - type: int + type: integer required: true default: 2290 - name: corpid diff --git a/pkg/platform/sources/wecomcs.py b/pkg/platform/sources/wecomcs.py index 532d7470..94d0e450 100644 --- a/pkg/platform/sources/wecomcs.py +++ b/pkg/platform/sources/wecomcs.py @@ -11,49 +11,47 @@ from pkg.platform.types import events as platform_events, message as platform_me from libs.wecom_customer_service_api.wecomcsevent import WecomCSEvent 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): - @staticmethod - async def yiri2target( - message_chain: platform_message.MessageChain, bot: WecomCSClient - ): + async def yiri2target(message_chain: platform_message.MessageChain, bot: WecomCSClient): 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, + } + ) 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))) else: - content_list.append({ - "type": "text", - "content": str(msg), - }) + content_list.append( + { + 'type': 'text', + 'content': str(msg), + } + ) return content_list @staticmethod async def target2yiri(message: str, message_id: int = -1): yiri_msg_list = [] - 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())) yiri_msg_list.append(platform_message.Plain(text=message)) chain = platform_message.MessageChain(yiri_msg_list) @@ -63,21 +61,16 @@ class WecomMessageConverter(adapter.MessageConverter): @staticmethod async def target2yiri_image(picurl: str, message_id: int = -1): yiri_msg_list = [] - 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())) yiri_msg_list.append(platform_message.Image(base64=picurl)) chain = platform_message.MessageChain(yiri_msg_list) - + return chain class WecomEventConverter: - @staticmethod - async def yiri2target( - event: platform_events.Event, bot_account_id: int, bot: WecomCSClient - ) -> WecomCSEvent: + async def yiri2target(event: platform_events.Event, bot_account_id: int, bot: WecomCSClient) -> WecomCSEvent: # only for extracting user information if type(event) is platform_events.GroupMessage: @@ -98,29 +91,25 @@ class WecomEventConverter: platform_events.FriendMessage: 转换后的 FriendMessage 对象。 """ # 转换消息链 - if event.type == "text": - yiri_chain = await WecomMessageConverter.target2yiri( - event.message, event.message_id - ) + 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.user_id), - remark="", + remark='', ) return platform_events.FriendMessage( sender=friend, message_chain=yiri_chain, time=event.timestamp, source_platform_object=event ) - 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.user_id), - remark="", + remark='', ) - yiri_chain = await WecomMessageConverter.target2yiri_image( - picurl=event.picurl, message_id=event.message_id - ) + yiri_chain = await WecomMessageConverter.target2yiri_image(picurl=event.picurl, message_id=event.message_id) return platform_events.FriendMessage( sender=friend, message_chain=yiri_chain, time=event.timestamp, source_platform_object=event @@ -128,7 +117,6 @@ class WecomEventConverter: class WecomCSAdapter(adapter.MessagePlatformAdapter): - bot: WecomCSClient ap: app.Application bot_account_id: str @@ -142,20 +130,20 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter): self.ap = ap required_keys = [ - "corpid", - "secret", - "token", - "EncodingAESKey", + 'corpid', + 'secret', + 'token', + 'EncodingAESKey', ] missing_keys = [key for key in required_keys if key not in config] if missing_keys: - raise ParamNotEnoughError("企业微信客服缺少相关配置项,请查看文档或联系管理员") + raise ParamNotEnoughError('企业微信客服缺少相关配置项,请查看文档或联系管理员') self.bot = WecomCSClient( - corpid=config["corpid"], - secret=config["secret"], - token=config["token"], - EncodingAESKey=config["EncodingAESKey"], + corpid=config['corpid'], + secret=config['secret'], + token=config['token'], + EncodingAESKey=config['EncodingAESKey'], ) async def reply_message( @@ -164,40 +152,36 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter): message: platform_message.MessageChain, quote_origin: bool = False, ): - - Wecom_event = await WecomEventConverter.yiri2target( - message_source, self.bot_account_id, self.bot - ) + Wecom_event = await WecomEventConverter.yiri2target(message_source, self.bot_account_id, self.bot) content_list = await WecomMessageConverter.yiri2target(message, self.bot) - + for content in content_list: - if content["type"] == "text": - await self.bot.send_text_msg(open_kfid=Wecom_event.receiver_id,external_userid=Wecom_event.user_id,msgid=Wecom_event.message_id,content=content["content"]) - - async def send_message( - self, target_type: str, target_id: str, message: platform_message.MessageChain - ): + if content['type'] == 'text': + await self.bot.send_text_msg( + open_kfid=Wecom_event.receiver_id, + external_userid=Wecom_event.user_id, + msgid=Wecom_event.message_id, + content=content['content'], + ) + + async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): pass 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: WecomCSEvent): self.bot_account_id = event.receiver_id try: - return await callback( - await self.event_converter.target2yiri(event), self - ) + return await callback(await self.event_converter.target2yiri(event), self) except: 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 @@ -207,8 +191,8 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter): await asyncio.sleep(1) await self.bot.run_task( - host="0.0.0.0", - port=self.config["port"], + host='0.0.0.0', + port=self.config['port'], shutdown_trigger=shutdown_trigger_placeholder, ) @@ -220,4 +204,4 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter): event_type: type, callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], ): - return super().unregister_listener(event_type, callback) \ No newline at end of file + return super().unregister_listener(event_type, callback) diff --git a/pkg/platform/sources/wecomcs.yaml b/pkg/platform/sources/wecomcs.yaml index fb93d0b6..c542e188 100644 --- a/pkg/platform/sources/wecomcs.yaml +++ b/pkg/platform/sources/wecomcs.yaml @@ -8,6 +8,7 @@ metadata: description: en_US: WeComCSAdapter zh_CN: 企业微信客服适配器 + icon: wecom.png spec: config: - name: port diff --git a/pkg/platform/types/base.py b/pkg/platform/types/base.py index ce87d36c..da58d4ed 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,15 @@ 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 +42,7 @@ class PlatformBaseModel(BaseModel, metaclass=PlatformMetaclass): class PlatformIndexedMetaclass(PlatformMetaclass): """可以通过子类名获取子类的类的元类。""" + __indexedbases__: List[Type['PlatformIndexedModel']] = [] __indexedmodel__ = None @@ -69,6 +70,7 @@ class PlatformIndexedMetaclass(PlatformMetaclass): class PlatformIndexedModel(PlatformBaseModel, metaclass=PlatformIndexedMetaclass): """可以通过子类名获取子类的类。""" + __indexes__: Dict[str, Type['PlatformIndexedModel']] @classmethod @@ -86,7 +88,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..5ffccb9b 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,17 @@ 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 +53,7 @@ class MessageEvent(Event): type: 事件名。 message_chain: 消息内容。 """ + type: str """事件名。""" message_chain: platform_message.MessageChain @@ -74,6 +76,7 @@ class FriendMessage(MessageEvent): sender: 发送消息的好友。 message_chain: 消息内容。 """ + type: str = 'FriendMessage' """事件名。""" sender: platform_entities.Friend @@ -90,12 +93,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 c0b97671..8412e8a4 100644 --- a/pkg/platform/types/message.py +++ b/pkg/platform/types/message.py @@ -2,7 +2,6 @@ import itertools import logging import typing from datetime import datetime -from enum import Enum from pathlib import Path import pydantic.v1 as pydantic @@ -15,6 +14,7 @@ logger = logging.getLogger(__name__) class MessageComponentMetaclass(PlatformIndexedMetaclass): """消息组件元类。""" + __message_component__ = None def __new__(cls, name, bases, attrs, **kwargs): @@ -40,26 +40,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): # 解析参数列表,将位置参数转化为具名参数 parameter_names = self.__parameter_names__ if len(args) > len(parameter_names): - raise TypeError( - f'`{self.type}`需要{len(parameter_names)}个参数,但传入了{len(args)}个。' - ) + raise TypeError(f'`{self.type}`需要{len(parameter_names)}个参数,但传入了{len(args)}个。') for name, value in zip(parameter_names, args): if name in kwargs: raise TypeError(f'在 `{self.type}` 中,具名参数 `{name}` 与位置参数重复。') @@ -116,6 +116,7 @@ class MessageChain(PlatformBaseModel): ``` """ + __root__: typing.List[MessageComponent] @staticmethod @@ -129,11 +130,9 @@ class MessageChain(PlatformBaseModel): elif isinstance(msg, str): result.append(Plain(msg)) else: - raise TypeError( - f"消息链中元素需为 dict 或 str 或 MessageComponent,当前类型:{type(msg)}" - ) + raise TypeError(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)): @@ -156,7 +155,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})' @@ -164,8 +163,7 @@ 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): @@ -173,35 +171,34 @@ 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) @@ -216,8 +213,8 @@ 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. 是否有某个消息组件。 @@ -241,7 +238,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) @@ -252,9 +249,7 @@ class MessageChain(PlatformBaseModel): def __len__(self) -> int: return len(self.__root__) - def __add__( - self, other: typing.Union['MessageChain', MessageComponent, str] - ) -> 'MessageChain': + def __add__(self, other: typing.Union['MessageChain', MessageComponent, str]) -> 'MessageChain': if isinstance(other, MessageChain): return self.__class__(self.__root__ + other.__root__) if isinstance(other, str): @@ -267,9 +262,7 @@ class MessageChain(PlatformBaseModel): if isinstance(other, MessageComponent): return self.__class__([other] + self.__root__) if isinstance(other, str): - return self.__class__( - [typing.cast(MessageComponent, Plain(other))] + self.__root__ - ) + return self.__class__([typing.cast(MessageComponent, Plain(other))] + self.__root__) return NotImplemented def __mul__(self, other: int): @@ -292,7 +285,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 之前)。 @@ -322,10 +315,10 @@ 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: """返回消息链中 x 出现的次数。 @@ -341,7 +334,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]]): """将另一个消息链中的元素添加到消息链末尾。 @@ -393,7 +386,7 @@ class MessageChain(PlatformBaseModel): def exclude( self, x: typing.Union[MessageComponent, typing.Type[MessageComponent]], - count: int = -1 + count: int = -1, ) -> 'MessageChain': """返回移除指定元素或指定类型的元素后剩余的消息链。 @@ -404,6 +397,7 @@ class MessageChain(PlatformBaseModel): Returns: MessageChain: 剩余的消息链。 """ + def _exclude(): nonlocal count x_is_type = isinstance(x, type) @@ -421,10 +415,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) - ) + return cls(Plain(c) if isinstance(c, str) else c for c in itertools.chain(*args)) @property def source(self) -> typing.Optional['Source']: @@ -438,14 +429,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 的第一个元素)。""" @@ -455,10 +451,12 @@ class Source(MessageComponent): class Plain(MessageComponent): """纯文本。""" - type: str = "Plain" + + type: str = 'Plain' """消息组件类型。""" text: str """文字消息。""" + def __str__(self): return self.text @@ -468,7 +466,8 @@ class Plain(MessageComponent): class Quote(MessageComponent): """引用。""" - type: str = "Quote" + + type: str = 'Quote' """消息组件类型。""" id: typing.Optional[int] = None """被引用回复的原消息的 message_id。""" @@ -481,37 +480,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 属性。""" @@ -521,10 +525,9 @@ 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 '[图片]' @@ -536,7 +539,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 @@ -553,7 +556,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, ): """下载图片到本地。 @@ -567,6 +570,7 @@ class Image(MessageComponent): return import httpx + async with httpx.AsyncClient() as client: response = await client.get(self.url) response.raise_for_status() @@ -576,19 +580,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) @@ -599,7 +604,7 @@ class Image(MessageComponent): cls, filename: typing.Union[str, Path, None] = None, content: typing.Optional[bytes] = None, - ) -> "Image": + ) -> 'Image': """从本地文件路径加载图片,以 base64 的形式传递。 Args: @@ -614,16 +619,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: @@ -637,16 +644,20 @@ class Image(MessageComponent): class Unknown(MessageComponent): """未知。""" - type: str = "Unknown" + + type: str = 'Unknown' """消息组件类型。""" text: str """文本。""" + def __str__(self): return f'Unknown Message: {self.text}' + class Voice(MessageComponent): """语音。""" - type: str = "Voice" + + type: str = 'Voice' """消息组件类型。""" voice_id: typing.Optional[str] = None """语音的 voice_id,不为空时将忽略 url 属性。""" @@ -658,6 +669,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 +677,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 +687,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 +700,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 +714,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 +726,7 @@ class Voice(MessageComponent): cls, filename: typing.Union[str, Path, None] = None, content: typing.Optional[bytes] = None, - ) -> "Voice": + ) -> 'Voice': """从本地文件路径加载语音,以 base64 的形式传递。 Args: @@ -724,17 +738,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 +762,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 +771,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': """从消息链生成转发消息。 @@ -764,29 +784,27 @@ class ForwardMessageNode(pydantic.BaseModel): Returns: ForwardMessageNode: 生成的一条消息。 """ - return ForwardMessageNode( - sender_id=sender.id, - sender_name=sender.get_name(), - message_chain=message - ) + return ForwardMessageNode(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 +817,8 @@ class Forward(MessageComponent): class File(MessageComponent): """文件。""" - type: str = "File" + + type: str = 'File' """消息组件类型。""" id: str """文件识别 ID。""" @@ -811,10 +830,13 @@ class File(MessageComponent): def __str__(self): return f'[文件]{self.name}' + # ================ 个人微信专用组件 ================ + class WeChatMiniPrograms(MessageComponent): """小程序。个人微信专用组件。""" + type: str = 'WeChatMiniPrograms' """小程序id""" mini_app_id: str @@ -832,17 +854,20 @@ class WeChatMiniPrograms(MessageComponent): class WeChatForwardMiniPrograms(MessageComponent): """转发小程序。个人微信专用组件。""" + type: str = 'WeChatForwardMiniPrograms' """xml数据""" xml_data: str """首页图片""" image_url: typing.Optional[str] = None + def __str__(self): return self.xml_data class WeChatEmoji(MessageComponent): """emoji表情。个人微信专用组件。""" + type: str = 'WeChatEmoji' """emojimd5""" emoji_md5: str @@ -852,6 +877,7 @@ class WeChatEmoji(MessageComponent): class WeChatLink(MessageComponent): """发送链接。个人微信专用组件。""" + type: str = 'WeChatLink' """标题""" link_title: str = '' @@ -865,40 +891,54 @@ class WeChatLink(MessageComponent): class WeChatForwardLink(MessageComponent): """转发链接。个人微信专用组件。""" + type: str = 'WeChatForwardLink' """xml数据""" xml_data: str + def __str__(self): return self.xml_data + class WeChatForwardImage(MessageComponent): """转发图片。个人微信专用组件。""" + type: str = 'WeChatForwardImage' """xml数据""" xml_data: str + def __str__(self): return self.xml_data + class WeChatForwardFile(MessageComponent): """转发文件。个人微信专用组件。""" + type: str = 'WeChatForwardFile' """xml数据""" xml_data: str + def __str__(self): return self.xml_data + class WeChatAppMsg(MessageComponent): """通用appmsg发送。个人微信专用组件。""" + type: str = 'WeChatAppMsg' """xml数据""" app_msg: str + def __str__(self): return self.app_msg + class WeChatForwardQuote(MessageComponent): """转发引用消息。个人微信专用组件。""" + type: str = 'WeChatForwardQuote' """xml数据""" app_msg: str + def __str__(self): return self.app_msg 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 76a49bf4..dfd691f3 100644 --- a/pkg/plugin/context.py +++ b/pkg/plugin/context.py @@ -8,18 +8,16 @@ import enum from . import events from ..provider.tools import entities as tools_entities from ..core import app +from ..discover import engine as discover_engine from ..platform.types import message as platform_message 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( @@ -33,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 @@ -50,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. @@ -86,14 +85,18 @@ class BasePlugin(metaclass=abc.ABCMeta): ap: app.Application """应用程序对象""" + config: dict + """插件配置""" + def __init__(self, host: APIHost): """初始化阶段被调用""" self.host = host + self.config = {} async def initialize(self): """初始化阶段被调用""" pass - + async def destroy(self): """释放/禁用插件时被调用""" pass @@ -118,12 +121,12 @@ class APIHost: def get_platform_adapters(self) -> list[platform_adapter.MessagePlatformAdapter]: """获取已启用的消息平台适配器列表 - + Returns: list[platform.adapter.MessageSourceAdapter]: 已启用的消息平台适配器列表 """ - return self.ap.platform_mgr.adapters - + return self.ap.platform_mgr.get_running_adapters() + async def send_active_message( self, adapter: platform_adapter.MessagePlatformAdapter, @@ -132,7 +135,7 @@ class APIHost: message: platform_message.MessageChain, ): """发送主动消息 - + Args: adapter (platform.adapter.MessageSourceAdapter): 消息平台适配器对象,调用 host.get_platform_adapters() 获取并取用其中某个 target_type (str): 目标类型,`person`或`group` @@ -148,7 +151,7 @@ class APIHost: def require_ver( self, ge: str, - le: str='v999.999.999', + le: str = 'v999.999.999', ) -> bool: """插件版本要求装饰器 @@ -159,16 +162,21 @@ class APIHost: Returns: bool: 是否满足要求, False时为无法获取版本号,True时为满足要求,报错为不满足要求 """ - langbot_version = "" + langbot_version = '' try: langbot_version = self.ap.ver_mgr.get_current_version() # 从updater模块获取版本号 - except: + 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 @@ -215,37 +223,27 @@ 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): 源平台的消息链,若用户使用的不是源平台适配器,程序也能自动转换为目标平台消息链 """ - await self.host.ap.platform_mgr.send( - event=self.event.query.message_event, - msg=message_chain, - adapter=self.event.query.adapter, + # TODO 添加 at_sender 和 quote_origin 参数 + await self.event.query.adapter.reply_message( + 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 - ): + + 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 message (platform.types.MessageChain): 源平台的消息链,若用户使用的不是源平台适配器,程序也能自动转换为目标平台消息链 """ - await self.event.query.adapter.send_message( - target_type=target_type, - target_id=target_id, - message=message - ) + await self.event.query.adapter.send_message(target_type=target_type, target_id=target_id, message=message) def prevent_postorder(self): """阻止后续插件执行""" @@ -276,10 +274,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 @@ -292,23 +288,26 @@ class EventContext: class RuntimeContainerStatus(enum.Enum): """插件容器状态""" - MOUNTED = "mounted" + MOUNTED = 'mounted' """已加载进内存,所有位于运行时记录中的 RuntimeContainer 至少是这个状态""" - INITIALIZED = "initialized" + INITIALIZED = 'initialized' """已初始化""" class RuntimeContainer(pydantic.BaseModel): """运行时的插件容器 - + 运行期间存储单个插件的信息 """ plugin_name: str """插件名称""" - plugin_description: str + plugin_label: discover_engine.I18nString + """插件标签""" + + plugin_description: discover_engine.I18nString """插件描述""" plugin_version: str @@ -317,7 +316,7 @@ class RuntimeContainer(pydantic.BaseModel): plugin_author: str """插件作者""" - plugin_source: str + plugin_repository: str """插件源码地址""" main_file: str @@ -335,15 +334,22 @@ class RuntimeContainer(pydantic.BaseModel): priority: typing.Optional[int] = 0 """优先级""" + config_schema: typing.Optional[list[dict]] = [] + """插件配置模板""" + + plugin_config: typing.Optional[dict] = {} + """插件配置""" + 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]], + ] = {} """事件处理器""" - content_functions: list[tools_entities.LLMFunction] = [] + tools: list[tools_entities.LLMFunction] = [] """内容函数""" status: RuntimeContainerStatus = RuntimeContainerStatus.MOUNTED @@ -352,43 +358,23 @@ class RuntimeContainer(pydantic.BaseModel): class Config: arbitrary_types_allowed = True - def to_setting_dict(self): - return { - 'name': self.plugin_name, - 'description': self.plugin_description, - 'version': self.plugin_version, - 'author': self.plugin_author, - 'source': self.plugin_source, - 'main_file': self.main_file, - 'pkg_path': self.pkg_path, - 'priority': self.priority, - 'enabled': self.enabled, - } - - def set_from_setting_dict( - self, - setting: dict - ): - self.plugin_source = setting['source'] - self.priority = setting['priority'] - self.enabled = setting['enabled'] - def model_dump(self, *args, **kwargs): return { 'name': self.plugin_name, - 'description': self.plugin_description, + 'label': self.plugin_label.to_dict(), + 'description': self.plugin_description.to_dict(), 'version': self.plugin_version, 'author': self.plugin_author, - 'source': self.plugin_source, + 'repository': self.plugin_repository, 'main_file': self.main_file, 'pkg_path': self.pkg_path, 'enabled': self.enabled, 'priority': self.priority, + 'config_schema': self.config_schema, 'event_handlers': { - event_name.__name__: handler.__name__ - for event_name, handler in self.event_handlers.items() + event_name.__name__: handler.__name__ for event_name, handler in self.event_handlers.items() }, - 'content_functions': [ + 'tools': [ { 'name': function.name, 'human_desc': function.human_desc, @@ -396,7 +382,7 @@ class RuntimeContainer(pydantic.BaseModel): 'parameters': function.parameters, 'func': function.func.__name__, } - for function in self.content_functions + for function in self.tools ], 'status': self.status.value, } 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 039ff196..df247219 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,42 @@ 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,14 +98,16 @@ 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") - await self.ap.plugin_mgr.setting.record_installed_plugin_source( - "plugins/" + repo_label + '/', plugin_source - ) + 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 + # ) async def uninstall_plugin( self, @@ -113,9 +119,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, @@ -124,14 +130,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_source: - plugin_source = plugin_container.plugin_source - task_context.trace("转交安装任务.", "update-plugin") + if plugin_container.plugin_repository: + plugin_source = plugin_container.plugin_repository + 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 b3710c9e..8aa7382b 100644 --- a/pkg/plugin/loaders/classic.py +++ b/pkg/plugin/loaders/classic.py @@ -9,6 +9,7 @@ from .. import loader, events, context, models 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 class PluginLoader(loader.PluginLoader): @@ -31,32 +32,22 @@ class PluginLoader(loader.PluginLoader): async def initialize(self): """初始化""" - setattr(models, 'register', self.register) - setattr(models, 'on', self.on) - setattr(models, 'func', self.func) - - setattr(context, 'register', self.register) - setattr(context, 'handler', self.handler) - setattr(context, 'llm_func', self.llm_func) def register( - self, - name: str, - description: str, - version: str, - author: str + 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_description=description, + plugin_label=discover_engine.I18nString(en_US=name, zh_CN=name), + plugin_description=discover_engine.I18nString(en_US=description, zh_CN=description), plugin_version=version, plugin_author=author, - plugin_source='', + plugin_repository='', pkg_path=self._current_pkg_path, main_file=self._current_module_path, event_handlers={}, - content_functions=[], + tools=[], ) self._current_container = container @@ -64,19 +55,16 @@ 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] - ) -> typing.Callable[[typing.Callable], typing.Callable]: + def on(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: args = { 'host': ctx.host, @@ -85,12 +73,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 @@ -101,21 +89,16 @@ 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) - async def handler( - plugin: context.BasePlugin, - query: core_entities.Query, - *args, - **kwargs - ): + async def handler(plugin: context.BasePlugin, query: core_entities.Query, *args, **kwargs): return func(*args, **kwargs) llm_function = tools_entities.LLMFunction( @@ -126,20 +109,22 @@ class PluginLoader(loader.PluginLoader): func=handler, ) - self._current_container.content_functions.append(llm_function) + self._current_container.tools.append(llm_function) return func - + return wrapper - - def handler( - self, - event: typing.Type[events.BaseEventModel] - ) -> typing.Callable[[typing.Callable], typing.Callable]: + + def handler(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 + return func + self._current_container.event_handlers[event] = func return func @@ -148,12 +133,17 @@ 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 + return func + function_schema = funcschema.get_func_schema(func) function_name = self._current_container.plugin_name + '-' + (func.__name__ if name is None else name) @@ -165,44 +155,44 @@ class PluginLoader(loader.PluginLoader): func=func, ) - self._current_container.content_functions.append(llm_function) + 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: + except Exception: self.ap.logger.error(f'加载插件模块 {prefix + item.name} 时发生错误') traceback.print_exc() async def load_plugins(self): - """加载插件 - """ - await self._walk_plugin_path(__import__("plugins", fromlist=[""])) + """加载插件""" + setattr(models, 'register', self.register) + setattr(models, 'on', self.on) + setattr(models, 'func', self.func) + + 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=[''])) diff --git a/pkg/plugin/loaders/manifest.py b/pkg/plugin/loaders/manifest.py new file mode 100644 index 00000000..cce6c9e3 --- /dev/null +++ b/pkg/plugin/loaders/manifest.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +import typing +import os +import traceback + +from ...core import app +from .. import context, events +from .. import loader +from ...utils import funcschema +from ...provider.tools import entities as tools_entities + + +class PluginManifestLoader(loader.PluginLoader): + """通过插件清单发现插件""" + + _current_container: context.RuntimeContainer = None + + def __init__(self, ap: app.Application): + super().__init__(ap) + + def handler(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 + + return wrapper + + def llm_func( + self, + 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) + + llm_function = tools_entities.LLMFunction( + name=function_name, + human_desc='', + description=function_schema['description'], + parameters=function_schema['parameters'], + func=func, + ) + + self._current_container.tools.append(llm_function) + + return func + + return wrapper + + async def load_plugins(self): + """加载插件""" + setattr(context, 'handler', self.handler) + setattr(context, 'llm_func', self.llm_func) + + plugin_manifests = self.ap.discover.get_components_by_kind('Plugin') + + for plugin_manifest in plugin_manifests: + try: + config_schema = plugin_manifest.spec['config'] if 'config' in plugin_manifest.spec else [] + + current_plugin_container = context.RuntimeContainer( + plugin_name=plugin_manifest.metadata.name, + plugin_label=plugin_manifest.metadata.label, + plugin_description=plugin_manifest.metadata.description, + 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), + pkg_path=plugin_manifest.rel_dir, + config_schema=config_schema, + event_handlers={}, + tools=[], + ) + + self._current_container = current_plugin_container + + # extract the plugin class + # this step will load the plugin module, + # so the event handlers and tools will be registered + plugin_class = plugin_manifest.get_python_component_class() + current_plugin_container.plugin_class = plugin_class + + # TODO load component extensions + + self.plugins.append(current_plugin_container) + 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 2b8e887d..f813d2e2 100644 --- a/pkg/plugin/manager.py +++ b/pkg/plugin/manager.py @@ -1,12 +1,14 @@ from __future__ import annotations -import typing import traceback +import sqlalchemy + from ..core import app, taskmgr -from . import context, loader, events, installer, setting, models -from .loaders import classic +from . import context, loader, events, installer, models +from .loaders import classic, manifest from .installers import github +from ..entity.persistence import plugin as persistence_plugin class PluginManager: @@ -14,59 +16,118 @@ class PluginManager: ap: app.Application - loader: loader.PluginLoader + loaders: list[loader.PluginLoader] installer: installer.PluginInstaller - setting: setting.SettingManager - api_host: context.APIHost + plugin_containers: list[context.RuntimeContainer] + def plugins( self, - enabled: bool=None, - status: context.RuntimeContainerStatus=None, + enabled: bool = None, + status: context.RuntimeContainerStatus = None, ) -> list[context.RuntimeContainer]: - """获取插件列表 - """ - plugins = self.loader.plugins + """获取插件列表""" + 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 + return None + def __init__(self, ap: app.Application): self.ap = ap - self.loader = classic.PluginLoader(ap) + self.loaders = [ + classic.PluginLoader(ap), + manifest.PluginManifestLoader(ap), + ] self.installer = github.GitHubRepoInstaller(ap) - self.setting = setting.SettingManager(ap) self.api_host = context.APIHost(ap) + self.plugin_containers = [] async def initialize(self): - await self.loader.initialize() + for loader in self.loaders: + await loader.initialize() await self.installer.initialize() - await self.setting.initialize() await self.api_host.initialize() setattr(models, 'require_ver', self.api_host.require_ver) async def load_plugins(self): - await self.loader.load_plugins() + self.ap.logger.info('Loading all plugins...') - await self.setting.sync_setting(self.loader.plugins) + for loader in self.loaders: + await loader.load_plugins() + self.plugin_containers.extend(loader.plugins) + + await self.load_plugin_settings(self.plugin_containers) # 按优先级倒序 - self.loader.plugins.sort(key=lambda x: x.priority, reverse=True) + self.plugin_containers.sort(key=lambda x: x.priority, reverse=True) - self.ap.logger.debug(f'优先级排序后的插件列表 {self.loader.plugins}') + self.ap.logger.debug(f'优先级排序后的插件列表 {self.plugin_containers}') + + async def load_plugin_settings(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) + ) + + setting = result.first() + + if setting is None: + new_setting_data = { + 'plugin_author': plugin_container.plugin_author, + 'plugin_name': plugin_container.plugin_name, + 'enabled': plugin_container.enabled, + 'priority': plugin_container.priority, + 'config': plugin_container.plugin_config, + } + + await self.ap.persistence_mgr.execute_async( + sqlalchemy.insert(persistence_plugin.PluginSetting).values(**new_setting_data) + ) + continue + else: + plugin_container.enabled = setting.enabled + plugin_container.priority = setting.priority + plugin_container.plugin_config = setting.config + + async def dump_plugin_container_setting(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) + .values( + enabled=plugin_container.enabled, + priority=plugin_container.priority, + config=plugin_container.plugin_config, + ) + ) async def initialize_plugin(self, plugin: context.RuntimeContainer): self.ap.logger.debug(f'初始化插件 {plugin.plugin_name}') plugin.plugin_inst = plugin.plugin_class(self.api_host) + plugin.plugin_inst.config = plugin.plugin_config plugin.plugin_inst.ap = self.ap plugin.plugin_inst.host = self.api_host await plugin.plugin_inst.initialize() @@ -87,13 +148,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: @@ -112,18 +173,10 @@ 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" - } - ) + # TODO statistics task_context.trace('重载插件..', 'reload-plugin') await self.ap.reload(scope='plugin') @@ -133,8 +186,7 @@ class PluginManager: plugin_name: str, task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(), ): - """卸载插件 - """ + """卸载插件""" plugin_container = self.get_plugin_by_name(plugin_name) @@ -144,14 +196,7 @@ class PluginManager: await self.destroy_plugin(plugin_container) await self.installer.uninstall_plugin(plugin_name, task_context) - await self.ap.ctr_mgr.plugin.post_remove_record( - { - "name": plugin_name, - "remote": plugin_container.plugin_source, - "author": plugin_container.plugin_author, - "version": plugin_container.plugin_version - } - ) + # TODO statistics task_context.trace('重载插件..', 'reload-plugin') await self.ap.reload(scope='plugin') @@ -159,66 +204,45 @@ 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_source, - "author": plugin_container.plugin_author, - "version": plugin_container.plugin_version - }, - old_version=plugin_container.plugin_version, - new_version="HEAD" - ) + # TODO statistics 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 - ): + for plugin in self.plugins(enabled=True, status=context.RuntimeContainerStatus.INITIALIZED): if event.__class__ in plugin.event_handlers: 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 - ) + await plugin.event_handlers[event.__class__](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(): @@ -231,23 +255,10 @@ class PluginManager: 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__}') - if emitted_plugins: - plugins_info: list[dict] = [ - { - 'name': plugin.plugin_name, - 'remote': plugin.plugin_source, - 'version': plugin.plugin_version, - '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__ - ) + # TODO statistics return ctx @@ -257,7 +268,7 @@ class PluginManager: if plugin.plugin_name == plugin_name: if plugin.enabled == new_status: return False - + # 初始化/释放插件 if new_status: await self.initialize_plugin(plugin) @@ -265,8 +276,8 @@ class PluginManager: await self.destroy_plugin(plugin) plugin.enabled = new_status - - await self.setting.dump_container_setting(self.loader.plugins) + + await self.dump_plugin_container_setting(plugin) break @@ -275,16 +286,23 @@ 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') - for plugin in self.loader.plugins: + for plugin in self.plugin_containers: if plugin.plugin_name == plugin_name: plugin.priority = plugin_priority break - self.loader.plugins.sort(key=lambda x: x.priority, reverse=True) + self.plugin_containers.sort(key=lambda x: x.priority, reverse=True) - await self.setting.dump_container_setting(self.loader.plugins) + 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): + plugin_container.plugin_config = new_config + + plugin_container.plugin_inst.config = new_config + + await self.dump_plugin_container_setting(plugin_container) 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/plugin/setting.py b/pkg/plugin/setting.py deleted file mode 100644 index bd50603f..00000000 --- a/pkg/plugin/setting.py +++ /dev/null @@ -1,101 +0,0 @@ -from __future__ import annotations - -from ..core import app -from ..config import manager as cfg_mgr -from . import context - - -class SettingManager: - """插件设置管理器""" - - ap: app.Application - - settings: cfg_mgr.ConfigManager - - def __init__(self, ap: app.Application): - self.ap = ap - - async def initialize(self): - self.settings = self.ap.plugin_setting_meta - - async def sync_setting( - self, - plugin_containers: list[context.RuntimeContainer], - ): - """同步设置 - """ - - not_matched_source_record = [] - - for value in self.settings.data['plugins']: - - if 'name' not in value: # 只有远程地址的,应用到pkg_path相同的插件容器上 - matched = False - - for plugin_container in plugin_containers: - if plugin_container.pkg_path == value['pkg_path']: - matched = True - - plugin_container.plugin_source = value['source'] - break - - if not matched: - not_matched_source_record.append(value) - else: # 正常的插件设置 - for plugin_container in plugin_containers: - if plugin_container.plugin_name == value['name']: - plugin_container.set_from_setting_dict(value) - break - - self.settings.data = { - 'plugins': [ - p.to_setting_dict() - for p in plugin_containers - ] - } - - self.settings.data['plugins'].extend(not_matched_source_record) - - await self.settings.dump_config() - - async def dump_container_setting( - self, - plugin_containers: list[context.RuntimeContainer] - ): - """保存插件容器设置 - """ - - for plugin in plugin_containers: - for ps in self.settings.data['plugins']: - if ps['name'] == plugin.plugin_name: - plugin_dict = plugin.to_setting_dict() - - for key in plugin_dict: - ps[key] = plugin_dict[key] - - break - - await self.settings.dump_config() - - async def record_installed_plugin_source( - self, - pkg_path: str, - source: str - ): - found = False - - for value in self.settings.data['plugins']: - if value['pkg_path'] == pkg_path: - value['source'] = source - found = True - break - - if not found: - - self.settings.data['plugins'].append( - { - 'pkg_path': pkg_path, - 'source': source - } - ) - await self.settings.dump_config() \ No newline at end of file diff --git a/pkg/provider/entities.py b/pkg/provider/entities.py index dce55fd5..94b812d9 100644 --- a/pkg/provider/entities.py +++ b/pkg/provider/entities.py @@ -1,9 +1,10 @@ from __future__ import annotations import typing -import enum import pydantic.v1 as pydantic +from pkg.provider import entities + from ..platform.types import message as platform_message @@ -30,7 +31,6 @@ class ImageURLContentObject(pydantic.BaseModel): class ContentElement(pydantic.BaseModel): - type: str """内容类型""" @@ -55,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) @@ -80,15 +80,15 @@ 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): 首个文字组件的前缀文本 """ @@ -96,21 +96,20 @@ 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)) @@ -118,9 +117,19 @@ 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)) return platform_message.MessageChain(mc) + + +class Prompt(pydantic.BaseModel): + """供AI使用的Prompt""" + + name: str + """名称""" + + messages: list[entities.Message] + """消息列表""" 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 a5ffe6bc..e37e21cb 100644 --- a/pkg/provider/modelmgr/modelmgr.py +++ b/pkg/provider/modelmgr/modelmgr.py @@ -1,121 +1,143 @@ from __future__ import annotations -import aiohttp +import sqlalchemy from . import entities, requester from ...core import app +from ...core import entities as core_entities +from .. import entities as llm_entities +from ..tools import entities as tools_entities from ...discover import engine from . import token -from .requesters import bailianchatcmpl, chatcmpl, anthropicmsgs, moonshotchatcmpl, deepseekchatcmpl, ollamachat, giteeaichatcmpl, volcarkchatcmpl, xaichatcmpl, zhipuaichatcmpl, lmstudiochatcmpl, siliconflowchatcmpl, volcarkchatcmpl, modelscopechatcmpl +from ...entity.persistence import model as persistence_model -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: """模型管理器""" + model_list: list[entities.LLMModelInfo] # deprecated + + requesters: dict[str, requester.LLMAPIRequester] # deprecated + + token_mgrs: dict[str, token.TokenManager] # deprecated + + # ====== 4.0 ====== + ap: app.Application + llm_models: list[requester.RuntimeLLMModel] + requester_components: list[engine.Component] - model_list: list[entities.LLMModelInfo] + requester_dict: dict[str, type[requester.LLMAPIRequester]] # cache - requesters: dict[str, requester.LLMAPIRequester] - - token_mgrs: dict[str, token.TokenManager] - def __init__(self, ap: app.Application): self.ap = ap self.model_list = [] self.requesters = {} self.token_mgrs = {} + self.llm_models = [] + self.requester_components = [] + self.requester_dict = {} - async def get_model_by_name(self, name: str) -> entities.LLMModelInfo: - """通过名称获取模型 - """ + async def initialize(self): + 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() + + self.requester_dict = requester_dict + + await self.load_models_from_db() + + async def load_models_from_db(self): + """从数据库加载模型""" + self.ap.logger.info('Loading models from db...') + + self.llm_models = [] + + # llm models + result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_model.LLMModel)) + + llm_models = result.all() + + # 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, + ): + """加载模型""" + + 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) + + await requester_inst.initialize() + + runtime_llm_model = requester.RuntimeLLMModel( + model_entity=model_info, + token_mgr=token.TokenManager( + name=model_info.uuid, + tokens=model_info.api_keys, + ), + 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} 的信息,请在元数据中配置") - - async def initialize(self): + raise ValueError(f'无法确定模型 {name} 的信息,请在元数据中配置') - self.requester_components = self.ap.discover.get_components_by_kind('LLMAPIRequester') + 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') - # 初始化token_mgr, requester - for k, v in self.ap.provider_cfg.data['keys'].items(): - self.token_mgrs[k] = token.TokenManager(k, v) + async def remove_llm_model(self, model_uuid: str): + """移除模型""" + for model in self.llm_models: + if model.model_entity.uuid == model_uuid: + self.llm_models.remove(model) + return - # for api_cls in requester.preregistered_requesters: - # api_inst = api_cls(self.ap) - # await api_inst.initialize() - # self.requesters[api_inst.name] = api_inst + def get_available_requesters_info(self) -> list[dict]: + """获取所有可用的请求器""" + return [component.to_plain_dict() for component in self.requester_components] + + def get_available_requester_info_by_name(self, name: str) -> dict | None: + """通过名称获取请求器信息""" for component in self.requester_components: - api_cls = component.get_python_component_class() - api_inst = api_cls(self.ap) - await api_inst.initialize() - self.requesters[component.metadata.name] = api_inst + if component.metadata.name == name: + return component.to_plain_dict() + return None - # 尝试从api获取最新的模型信息 - try: - async with aiohttp.ClientSession() as session: - async with session.request( - method="GET", - url=FETCH_MODEL_LIST_URL, - # 参数 - params={ - "version": self.ap.ver_mgr.get_current_version() - }, - ) as resp: - model_list = (await resp.json())['data']['list'] + def get_available_requester_manifest_by_name(self, name: str) -> engine.Component | None: + """通过名称获取请求器清单""" + for component in self.requester_components: + if component.metadata.name == name: + return component + return None - for model in model_list: - - for index, local_model in enumerate(self.ap.llm_models_meta.data['list']): - if model['name'] == local_model['name']: - self.ap.llm_models_meta.data['list'][index] = model - break - else: - self.ap.llm_models_meta.data['list'].append(model) - - await self.ap.llm_models_meta.dump_config() - - except Exception as e: - self.ap.logger.debug(f'获取最新模型列表失败: {e}') - - default_model_info: entities.LLMModelInfo = None - - for model in self.ap.llm_models_meta.data['list']: - if model['name'] == 'default': - default_model_info = entities.LLMModelInfo( - name=model['name'], - model_name=None, - token_mgr=self.token_mgrs[model['token_mgr']], - requester=self.requesters[model['requester']], - tool_call_supported=model['tool_call_supported'], - vision_supported=model['vision_supported'] - ) - break - - for model in self.ap.llm_models_meta.data['list']: - - try: - - model_name = model.get('model_name', default_model_info.model_name) - token_mgr = self.token_mgrs[model['token_mgr']] if 'token_mgr' in model else default_model_info.token_mgr - req = self.requesters[model['requester']] if 'requester' in model else default_model_info.requester - tool_call_supported = model.get('tool_call_supported', default_model_info.tool_call_supported) - vision_supported = model.get('vision_supported', default_model_info.vision_supported) - - model_info = entities.LLMModelInfo( - name=model['name'], - model_name=model_name, - token_mgr=token_mgr, - requester=req, - tool_call_supported=tool_call_supported, - vision_supported=vision_supported - ) - self.model_list.append(model_info) - - except Exception as e: - self.ap.logger.error(f"初始化模型 {model['name']} 失败: {type(e)} {e} ,请检查配置文件") + async def invoke_llm( + self, + query: core_entities.Query, + model_uuid: str, + messages: list[llm_entities.Message], + 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 147a97c4..244f4c82 100644 --- a/pkg/provider/modelmgr/requester.py +++ b/pkg/provider/modelmgr/requester.py @@ -6,47 +6,69 @@ import typing from ...core import app from ...core import entities as core_entities from .. import entities as llm_entities -from . import entities as modelmgr_entities from ..tools import entities as tools_entities +from ...entity.persistence import model as persistence_model +from . import token + + +class RuntimeLLMModel: + """运行时模型""" + + model_entity: persistence_model.LLMModel + """模型数据""" + + token_mgr: token.TokenManager + """api key管理器""" + + 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 - def __init__(self, ap: app.Application): + default_config: dict[str, typing.Any] = {} + + requester_cfg: dict[str, typing.Any] = {} + + def __init__(self, ap: app.Application, config: dict[str, typing.Any]): self.ap = ap + self.requester_cfg = {**self.default_config} + self.requester_cfg.update(config) async def initialize(self): pass - async def preprocess( - self, - query: core_entities.Query, - ): - """预处理 - - 在这里处理特定API对Query对象的兼容性问题。 - """ - pass - @abc.abstractmethod - async def call( + async def invoke_llm( self, query: core_entities.Query, - model: modelmgr_entities.LLMModelInfo, + model: RuntimeLLMModel, messages: typing.List[llm_entities.Message], funcs: typing.List[tools_entities.LLMFunction] = None, + extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: """调用API Args: - model (modelmgr_entities.LLMModelInfo): 使用的模型信息 + model (RuntimeLLMModel): 使用的模型信息 messages (typing.List[llm_entities.Message]): 消息对象列表 funcs (typing.List[tools_entities.LLMFunction], optional): 使用的工具函数列表. Defaults to None. + extra_args (dict[str, typing.Any], optional): 额外的参数. Defaults to {}. Returns: llm_entities.Message: 返回消息对象 diff --git a/pkg/provider/modelmgr/requesters/anthropic.svg b/pkg/provider/modelmgr/requesters/anthropic.svg new file mode 100644 index 00000000..d852f044 --- /dev/null +++ b/pkg/provider/modelmgr/requesters/anthropic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/pkg/provider/modelmgr/requesters/anthropicmsgs.py b/pkg/provider/modelmgr/requesters/anthropicmsgs.py index b6052633..38573854 100644 --- a/pkg/provider/modelmgr/requesters/anthropicmsgs.py +++ b/pkg/provider/modelmgr/requesters/anthropicmsgs.py @@ -2,17 +2,13 @@ from __future__ import annotations import typing import json -import traceback -import base64 import platform import socket - import anthropic import httpx -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 @@ -24,39 +20,44 @@ class AnthropicMessages(requester.LLMAPIRequester): client: anthropic.AsyncAnthropic + default_config: dict[str, typing.Any] = { + 'base_url': 'https://api.anthropic.com/v1', + 'timeout': 120, + } + async def initialize(self): # 兼容 Windows 缺失 TCP_KEEPINTVL 和 TCP_KEEPCNT 的问题 - if platform.system() == "Windows": - if not hasattr(socket, "TCP_KEEPINTVL"): + if platform.system() == 'Windows': + if not hasattr(socket, 'TCP_KEEPINTVL'): socket.TCP_KEEPINTVL = 0 - if not hasattr(socket, "TCP_KEEPCNT"): + if not hasattr(socket, 'TCP_KEEPCNT'): socket.TCP_KEEPCNT = 0 - httpx_client = anthropic._base_client.AsyncHttpxClientWrapper( - base_url=self.ap.provider_cfg.data['requester']['anthropic-messages']['base-url'].replace(' ', ''), + base_url=self.requester_cfg['base_url'], # cast to a valid type because mypy doesn't understand our type narrowing - timeout=typing.cast(httpx.Timeout, self.ap.provider_cfg.data['requester']['anthropic-messages']['timeout']), + timeout=typing.cast(httpx.Timeout, self.requester_cfg['timeout']), limits=anthropic._constants.DEFAULT_CONNECTION_LIMITS, follow_redirects=True, trust_env=True, ) self.client = anthropic.AsyncAnthropic( - api_key="", + api_key='', http_client=httpx_client, ) - async def call( + async def invoke_llm( self, query: core_entities.Query, - model: entities.LLMModelInfo, + model: requester.RuntimeLLMModel, messages: typing.List[llm_entities.Message], funcs: typing.List[tools_entities.LLMFunction] = None, + extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: self.client.api_key = model.token_mgr.get_token() - args = self.ap.provider_cfg.data['requester']['anthropic-messages']['args'].copy() - args["model"] = model.name if model.model_name is None else model.model_name + args = extra_args.copy() + args['model'] = model.model_entity.name # 处理消息 @@ -64,7 +65,7 @@ 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 break @@ -72,8 +73,7 @@ class AnthropicMessages(requester.LLMAPIRequester): if system_role_message: messages.pop(i) - 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 = [] @@ -82,67 +82,62 @@ 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": + 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)) @@ -152,7 +147,7 @@ class AnthropicMessages(requester.LLMAPIRequester): 'content': '', 'role': resp.role, } - + assert type(resp) is anthropic.types.message.Message for block in resp.content: @@ -164,11 +159,8 @@ class AnthropicMessages(requester.LLMAPIRequester): assert type(block) is anthropic.types.tool_use_block.ToolUseBlock tool_call = llm_entities.ToolCall( id=block.id, - type="function", - function=llm_entities.FunctionCall( - name=block.name, - arguments=json.dumps(block.input) - ) + type='function', + function=llm_entities.FunctionCall(name=block.name, arguments=json.dumps(block.input)), ) if 'tool_calls' not in args: args['tool_calls'] = [] diff --git a/pkg/provider/modelmgr/requesters/anthropicmsgs.yaml b/pkg/provider/modelmgr/requesters/anthropicmsgs.yaml index 934db905..07aca1fe 100644 --- a/pkg/provider/modelmgr/requesters/anthropicmsgs.yaml +++ b/pkg/provider/modelmgr/requesters/anthropicmsgs.yaml @@ -5,27 +5,21 @@ metadata: label: en_US: Anthropic zh_CN: Anthropic + icon: anthropic.svg spec: config: - - name: base-url + - name: base_url label: en_US: Base URL zh_CN: 基础 URL type: string required: true default: "https://api.anthropic.com/v1" - - name: args - label: - en_US: Args - zh_CN: 附加参数 - type: object - required: true - default: {} - name: timeout label: en_US: Timeout zh_CN: 超时时间 - type: int + type: integer required: true default: 120 execution: diff --git a/pkg/provider/modelmgr/requesters/bailian.png b/pkg/provider/modelmgr/requesters/bailian.png new file mode 100644 index 00000000..c1aff40e Binary files /dev/null and b/pkg/provider/modelmgr/requesters/bailian.png differ diff --git a/pkg/provider/modelmgr/requesters/bailianchatcmpl.py b/pkg/provider/modelmgr/requesters/bailianchatcmpl.py index 5504b151..8689008d 100644 --- a/pkg/provider/modelmgr/requesters/bailianchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/bailianchatcmpl.py @@ -1,10 +1,9 @@ from __future__ import annotations +import typing import openai -from . import chatcmpl, modelscopechatcmpl -from .. import requester -from ....core import app +from . import modelscopechatcmpl class BailianChatCompletions(modelscopechatcmpl.ModelScopeChatCompletions): @@ -12,10 +11,7 @@ class BailianChatCompletions(modelscopechatcmpl.ModelScopeChatCompletions): client: openai.AsyncClient - requester_cfg: dict - - def __init__(self, ap: app.Application): - self.ap = ap - - self.requester_cfg = self.ap.provider_cfg.data['requester']['bailian-chat-completions'] - \ No newline at end of file + default_config: dict[str, typing.Any] = { + 'base_url': 'https://dashscope.aliyuncs.com/compatible-mode/v1', + 'timeout': 120, + } diff --git a/pkg/provider/modelmgr/requesters/bailianchatcmpl.yaml b/pkg/provider/modelmgr/requesters/bailianchatcmpl.yaml index 0107d1a6..f288df53 100644 --- a/pkg/provider/modelmgr/requesters/bailianchatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/bailianchatcmpl.yaml @@ -5,27 +5,21 @@ metadata: label: en_US: Aliyun Bailian zh_CN: 阿里云百炼 + icon: bailian.png spec: config: - - name: base-url + - name: base_url label: en_US: Base URL zh_CN: 基础 URL type: string required: true default: "https://dashscope.aliyuncs.com/compatible-mode/v1" - - name: args - label: - en_US: Args - zh_CN: 附加参数 - type: object - required: true - default: {} - name: timeout label: en_US: Timeout zh_CN: 超时时间 - type: int + type: integer required: true default: 120 execution: diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index f83a4909..513086e5 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): @@ -25,23 +18,17 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): client: openai.AsyncClient - requester_cfg: dict - - def __init__(self, ap: app.Application): - self.ap = ap - - self.requester_cfg = self.ap.provider_cfg.data['requester']['openai-chat-completions'] + default_config: dict[str, typing.Any] = { + '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'].replace(' ', ''), + api_key='', + base_url=self.requester_cfg['base_url'].replace(' ', ''), timeout=self.requester_cfg['timeout'], - http_client=httpx.AsyncClient( - trust_env=True, - timeout=self.requester_cfg['timeout'] - ) + http_client=httpx.AsyncClient(trust_env=True, timeout=self.requester_cfg['timeout']), ) async def _req( @@ -55,17 +42,17 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): self, chat_completion: chat_completion.ChatCompletion, ) -> llm_entities.Message: - chatcmpl_message = chat_completion.choices[0].message.dict() + 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' reasoning_content = chatcmpl_message['reasoning_content'] if 'reasoning_content' in chatcmpl_message else None - + # deepseek的reasoner模型 if reasoning_content is not None: - chatcmpl_message['content'] = "\n" + reasoning_content + "\n\n"+ chatcmpl_message['content'] + chatcmpl_message['content'] = '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] message = llm_entities.Message(**chatcmpl_message) @@ -75,64 +62,70 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): self, query: core_entities.Query, req_messages: list[dict], - use_model: entities.LLMModelInfo, + use_model: requester.RuntimeLLMModel, use_funcs: list[tools_entities.LLMFunction] = None, + extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() args = {} - args["model"] = use_model.name if use_model.model_name is None else use_model.model_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, extra_body=self.requester_cfg['args']) + resp = await self._req(args, extra_body=extra_args) # 处理请求结果 message = await self._make_msg(resp) return message - - async def call( + + async def invoke_llm( self, query: core_entities.Query, - model: entities.LLMModelInfo, + model: requester.RuntimeLLMModel, messages: typing.List[llm_entities.Message], funcs: typing.List[tools_entities.LLMFunction] = None, + extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: 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" for part in content): + if all(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(query=query, req_messages=req_messages, use_model=model, use_funcs=funcs) + return await self._closure( + query=query, + req_messages=req_messages, + use_model=model, + use_funcs=funcs, + extra_args=extra_args, + ) except asyncio.TimeoutError: raise errors.RequesterError('请求超时') except openai.BadRequestError as e: diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.yaml b/pkg/provider/modelmgr/requesters/chatcmpl.yaml index 9e623542..bbf31e9a 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/chatcmpl.yaml @@ -5,27 +5,21 @@ metadata: label: en_US: OpenAI zh_CN: OpenAI + icon: openai.svg spec: config: - - name: base-url + - name: base_url label: en_US: Base URL zh_CN: 基础 URL type: string required: true default: "https://api.openai.com/v1" - - name: args - label: - en_US: Args - zh_CN: 附加参数 - type: object - required: true - default: {} - name: timeout label: en_US: Timeout zh_CN: 超时时间 - type: int + type: integer required: true default: 120 execution: diff --git a/pkg/provider/modelmgr/requesters/deepseek.svg b/pkg/provider/modelmgr/requesters/deepseek.svg new file mode 100644 index 00000000..aa854a75 --- /dev/null +++ b/pkg/provider/modelmgr/requesters/deepseek.svg @@ -0,0 +1,3 @@ + + + diff --git a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py index eb466b65..30848df9 100644 --- a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py @@ -1,8 +1,10 @@ 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 @@ -10,37 +12,39 @@ from ...tools import entities as tools_entities class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions): """Deepseek ChatCompletion API 请求器""" - def __init__(self, ap: app.Application): - self.requester_cfg = ap.provider_cfg.data['requester']['deepseek-chat-completions'] - self.ap = ap + default_config: dict[str, typing.Any] = { + 'base_url': 'https://api.deepseek.com', + 'timeout': 120, + } async def _closure( self, query: core_entities.Query, req_messages: list[dict], - use_model: entities.LLMModelInfo, + use_model: requester.RuntimeLLMModel, use_funcs: list[tools_entities.LLMFunction] = None, + extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() - args = {} - args["model"] = use_model.name if use_model.model_name is None else use_model.model_name + args = extra_args.copy() + 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, extra_body=self.requester_cfg['args']) @@ -51,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/deepseekchatcmpl.yaml b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.yaml index 2eb56be4..48095697 100644 --- a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.yaml @@ -5,27 +5,21 @@ metadata: label: en_US: DeepSeek zh_CN: 深度求索 + icon: deepseek.svg spec: config: - - name: base-url + - name: base_url label: en_US: Base URL zh_CN: 基础 URL type: string required: true default: "https://api.deepseek.com" - - name: args - label: - en_US: Args - zh_CN: 附加参数 - type: object - required: true - default: {} - name: timeout label: en_US: Timeout zh_CN: 超时时间 - type: int + type: integer required: true default: 120 execution: diff --git a/pkg/provider/modelmgr/requesters/giteeai.svg b/pkg/provider/modelmgr/requesters/giteeai.svg new file mode 100644 index 00000000..1f51187f --- /dev/null +++ b/pkg/provider/modelmgr/requesters/giteeai.svg @@ -0,0 +1,3 @@ + + + diff --git a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py index fd9f66c8..050a04bc 100644 --- a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py +++ b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py @@ -1,50 +1,48 @@ 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): """Gitee AI ChatCompletions API 请求器""" - def __init__(self, ap: app.Application): - self.ap = ap - self.requester_cfg = ap.provider_cfg.data['requester']['gitee-ai-chat-completions'].copy() + default_config: dict[str, typing.Any] = { + 'base_url': 'https://ai.gitee.com/v1', + 'timeout': 120, + } async def _closure( self, query: core_entities.Query, req_messages: list[dict], - use_model: entities.LLMModelInfo, + use_model: requester.RuntimeLLMModel, use_funcs: list[tools_entities.LLMFunction] = None, + extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() - args = {} - args["model"] = use_model.name if use_model.model_name is None else use_model.model_name + args = extra_args.copy() + 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, extra_body=self.requester_cfg['args']) diff --git a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.yaml b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.yaml index 67178cd1..22d48501 100644 --- a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.yaml @@ -5,27 +5,21 @@ metadata: label: en_US: Gitee AI zh_CN: Gitee AI + icon: giteeai.svg spec: config: - - name: base-url + - name: base_url label: en_US: Base URL zh_CN: 基础 URL type: string required: true default: "https://ai.gitee.com/v1" - - name: args - label: - en_US: Args - zh_CN: 附加参数 - type: object - required: true - default: {} - name: timeout label: en_US: Timeout zh_CN: 超时时间 - type: int + type: integer required: true default: 120 execution: diff --git a/pkg/provider/modelmgr/requesters/lmstudio.webp b/pkg/provider/modelmgr/requesters/lmstudio.webp new file mode 100644 index 00000000..66bc8163 Binary files /dev/null and b/pkg/provider/modelmgr/requesters/lmstudio.webp differ diff --git a/pkg/provider/modelmgr/requesters/lmstudiochatcmpl.py b/pkg/provider/modelmgr/requesters/lmstudiochatcmpl.py index d2a9bcb7..c9060c1b 100644 --- a/pkg/provider/modelmgr/requesters/lmstudiochatcmpl.py +++ b/pkg/provider/modelmgr/requesters/lmstudiochatcmpl.py @@ -1,10 +1,9 @@ from __future__ import annotations +import typing import openai from . import chatcmpl -from .. import requester -from ....core import app class LmStudioChatCompletions(chatcmpl.OpenAIChatCompletions): @@ -12,9 +11,7 @@ class LmStudioChatCompletions(chatcmpl.OpenAIChatCompletions): client: openai.AsyncClient - requester_cfg: dict - - def __init__(self, ap: app.Application): - self.ap = ap - - self.requester_cfg = self.ap.provider_cfg.data['requester']['lmstudio-chat-completions'] + default_config: dict[str, typing.Any] = { + 'base_url': 'http://127.0.0.1:1234/v1', + 'timeout': 120, + } diff --git a/pkg/provider/modelmgr/requesters/lmstudiochatcmpl.yaml b/pkg/provider/modelmgr/requesters/lmstudiochatcmpl.yaml index 2b18e92c..a1e02584 100644 --- a/pkg/provider/modelmgr/requesters/lmstudiochatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/lmstudiochatcmpl.yaml @@ -5,27 +5,21 @@ metadata: label: en_US: LM Studio zh_CN: LM Studio + icon: lmstudio.webp spec: config: - - name: base-url + - name: base_url label: en_US: Base URL zh_CN: 基础 URL type: string required: true default: "http://127.0.0.1:1234/v1" - - name: args - label: - en_US: Args - zh_CN: 附加参数 - type: object - required: true - default: {} - name: timeout label: en_US: Timeout zh_CN: 超时时间 - type: int + type: integer required: true default: 120 execution: diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py index 8f51241e..c8be8a01 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -1,23 +1,17 @@ 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 entities as llm_entities from ...tools import entities as tools_entities -from ....utils import image class ModelScopeChatCompletions(requester.LLMAPIRequester): @@ -33,26 +27,22 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): self.requester_cfg = self.ap.provider_cfg.data['requester']['modelscope-chat-completions'] async def initialize(self): - self.client = openai.AsyncClient( - api_key="", + 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'] - ) + http_client=httpx.AsyncClient(trust_env=True, timeout=self.requester_cfg['timeout']), ) async def _req( self, args: dict, ) -> chat_completion.ChatCompletion: - args["stream"] = True + args['stream'] = True chunk = None - pending_content = "" + pending_content = '' tool_calls = [] @@ -74,7 +64,7 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): break else: tool_calls.append(tool_call) - + if chunk.choices[0].finish_reason is not None: break @@ -82,36 +72,41 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): for tc in tool_calls: function = chat_completion_message_tool_call.Function( - name=tc.function.name, - arguments=tc.function.arguments + name=tc.function.name, arguments=tc.function.arguments ) - real_tool_calls.append(chat_completion_message_tool_call.ChatCompletionMessageToolCall( - id=tc.id, - function=function, - type="function" - )) - - return chat_completion.ChatCompletion( - id=chunk.id, - object="chat.completion", - created=chunk.created, - choices=[ - chat_completion.Choice( - index=0, - message=chat_completion.ChatCompletionMessage( - role="assistant", - content=pending_content, - tool_calls=real_tool_calls if len(real_tool_calls) > 0 else None - ), - finish_reason=chunk.choices[0].finish_reason if hasattr(chunk.choices[0], 'finish_reason') and chunk.choices[0].finish_reason is not None else 'stop', - logprobs=chunk.choices[0].logprobs, + real_tool_calls.append( + chat_completion_message_tool_call.ChatCompletionMessageToolCall( + id=tc.id, function=function, type='function' ) - ], - model=chunk.model, - service_tier=chunk.service_tier if hasattr(chunk, 'service_tier') else None, - system_fingerprint=chunk.system_fingerprint if hasattr(chunk, 'system_fingerprint') else None, - usage=chunk.usage if hasattr(chunk, 'usage') else None - ) if chunk else None + ) + + return ( + chat_completion.ChatCompletion( + id=chunk.id, + object='chat.completion', + created=chunk.created, + choices=[ + chat_completion.Choice( + index=0, + message=chat_completion.ChatCompletionMessage( + role='assistant', + content=pending_content, + tool_calls=real_tool_calls if len(real_tool_calls) > 0 else None, + ), + finish_reason=chunk.choices[0].finish_reason + if hasattr(chunk.choices[0], 'finish_reason') and chunk.choices[0].finish_reason is not None + else 'stop', + logprobs=chunk.choices[0].logprobs, + ) + ], + model=chunk.model, + service_tier=chunk.service_tier if hasattr(chunk, 'service_tier') else None, + system_fingerprint=chunk.system_fingerprint if hasattr(chunk, 'system_fingerprint') else None, + usage=chunk.usage if hasattr(chunk, 'usage') else None, + ) + if chunk + else None + ) return await self.client.chat.completions.create(**args) async def _make_msg( @@ -138,29 +133,27 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): self.client.api_key = use_model.token_mgr.get_token() args = self.requester_cfg['args'].copy() - args["model"] = use_model.name if use_model.model_name is None else use_model.model_name + args['model'] = use_model.name if use_model.model_name is None else use_model.model_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) @@ -180,12 +173,12 @@ class ModelScopeChatCompletions(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" for part in content): + if all(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: @@ -204,4 +197,4 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): except openai.RateLimitError as e: raise errors.RequesterError(f'请求过于频繁或余额不足: {e.message}') except openai.APIError as e: - raise errors.RequesterError(f'请求错误: {e.message}') \ No newline at end of file + raise errors.RequesterError(f'请求错误: {e.message}') diff --git a/pkg/provider/modelmgr/requesters/moonshot.png b/pkg/provider/modelmgr/requesters/moonshot.png new file mode 100644 index 00000000..58ba4b46 Binary files /dev/null and b/pkg/provider/modelmgr/requesters/moonshot.png differ diff --git a/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py b/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py index 5389d132..cb843fed 100644 --- a/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py @@ -1,10 +1,11 @@ from __future__ import annotations -from ....core import app +import typing + 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 @@ -12,40 +13,42 @@ from ...tools import entities as tools_entities class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions): """Moonshot ChatCompletion API 请求器""" - def __init__(self, ap: app.Application): - self.requester_cfg = ap.provider_cfg.data['requester']['moonshot-chat-completions'] - self.ap = ap + default_config: dict[str, typing.Any] = { + 'base_url': 'https://api.moonshot.cn/v1', + 'timeout': 120, + } async def _closure( self, query: core_entities.Query, req_messages: list[dict], - use_model: entities.LLMModelInfo, + use_model: requester.RuntimeLLMModel, use_funcs: list[tools_entities.LLMFunction] = None, + extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() - args = {} - args["model"] = use_model.name if use_model.model_name is None else use_model.model_name + args = extra_args.copy() + 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() != "" and ('tool_calls' not in m or not m['tool_calls'])] - args["messages"] = messages + args['messages'] = messages # 发送请求 resp = await self._req(args, extra_body=self.requester_cfg['args']) @@ -53,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/moonshotchatcmpl.yaml b/pkg/provider/modelmgr/requesters/moonshotchatcmpl.yaml index 2680cea1..7e50130e 100644 --- a/pkg/provider/modelmgr/requesters/moonshotchatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/moonshotchatcmpl.yaml @@ -5,27 +5,21 @@ metadata: label: en_US: Moonshot zh_CN: 月之暗面 + icon: moonshot.png spec: config: - - name: base-url + - name: base_url label: en_US: Base URL zh_CN: 基础 URL type: string required: true default: "https://api.moonshot.com/v1" - - name: args - label: - en_US: Args - zh_CN: 附加参数 - type: object - required: true - default: {} - name: timeout label: en_US: Timeout zh_CN: 超时时间 - type: int + type: integer required: true default: 120 execution: diff --git a/pkg/provider/modelmgr/requesters/ollama.svg b/pkg/provider/modelmgr/requesters/ollama.svg new file mode 100644 index 00000000..f8482a96 --- /dev/null +++ b/pkg/provider/modelmgr/requesters/ollama.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/pkg/provider/modelmgr/requesters/ollamachat.py b/pkg/provider/modelmgr/requesters/ollamachat.py index 0ac2915f..00793f82 100644 --- a/pkg/provider/modelmgr/requesters/ollamachat.py +++ b/pkg/provider/modelmgr/requesters/ollamachat.py @@ -6,80 +6,77 @@ 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): """Ollama平台 ChatCompletion API请求器""" - client: ollama.AsyncClient - request_cfg: dict - def __init__(self, ap: app.Application): - super().__init__(ap) - self.ap = ap - self.request_cfg = self.ap.provider_cfg.data['requester'][REQUESTER_NAME] + client: ollama.AsyncClient + + default_config: dict[str, typing.Any] = { + 'base_url': 'http://127.0.0.1:11434', + 'timeout': 120, + } async def initialize(self): - os.environ['OLLAMA_HOST'] = self.request_cfg['base-url'] - self.client = ollama.AsyncClient( - timeout=self.request_cfg['timeout'] - ) + os.environ['OLLAMA_HOST'] = self.requester_cfg['base_url'] + self.client = ollama.AsyncClient(timeout=self.requester_cfg['timeout']) - async def _req(self, - args: dict, - ) -> Union[Mapping[str, Any], AsyncIterator[Mapping[str, Any]]]: - return await self.client.chat( - **args - ) + async def _req( + self, + args: dict, + ) -> Union[Mapping[str, Any], AsyncIterator[Mapping[str, Any]]]: + return await self.client.chat(**args) - async def _closure(self, query: core_entities.Query, req_messages: list[dict], use_model: entities.LLMModelInfo, - user_funcs: list[tools_entities.LLMFunction] = None) -> ( - llm_entities.Message): - args: Any = self.request_cfg['args'].copy() - args["model"] = use_model.name if use_model.model_name is None else use_model.model_name + async def _closure( + self, + query: core_entities.Query, + req_messages: list[dict], + use_model: requester.RuntimeLLMModel, + user_funcs: list[tools_entities.LLMFunction] = None, + extra_args: dict[str, typing.Any] = {}, + ) -> llm_entities.Message: + args = extra_args.copy() + 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"]) - - msg["content"] = "\n".join(text_content) - msg["images"] = [url.split(',')[1] for url in image_urls] + 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] if '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']) - 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) return message - async def _make_msg( - self, - chat_completions: ollama.ChatResponse) -> llm_entities.Message: + async def _make_msg(self, chat_completions: ollama.ChatResponse) -> llm_entities.Message: message: ollama.Message = chat_completions.message if message is None: raise ValueError("chat_completions must contain a 'message' field") @@ -87,42 +84,48 @@ 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] = [] for tool_call in message.tool_calls: - tool_calls.append(llm_entities.ToolCall( - id=uuid.uuid4().hex, - type="function", - function=llm_entities.FunctionCall( - name=tool_call.function.name, - arguments=json.dumps(tool_call.function.arguments) + tool_calls.append( + llm_entities.ToolCall( + id=uuid.uuid4().hex, + type='function', + function=llm_entities.FunctionCall( + name=tool_call.function.name, + arguments=json.dumps(tool_call.function.arguments), + ), ) - )) + ) ret_msg.tool_calls = tool_calls return ret_msg - async def call( - self, - query: core_entities.Query, - model: entities.LLMModelInfo, - messages: typing.List[llm_entities.Message], - funcs: typing.List[tools_entities.LLMFunction] = None, + async def invoke_llm( + self, + query: core_entities.Query, + model: requester.RuntimeLLMModel, + messages: typing.List[llm_entities.Message], + funcs: typing.List[tools_entities.LLMFunction] = None, + extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: 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' 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(query, req_messages, model, funcs) + return await self._closure( + query=query, + req_messages=req_messages, + use_model=model, + use_funcs=funcs, + extra_args=extra_args, + ) except asyncio.TimeoutError: raise errors.RequesterError('请求超时') diff --git a/pkg/provider/modelmgr/requesters/ollamachat.yaml b/pkg/provider/modelmgr/requesters/ollamachat.yaml index 9c2e83f3..c62cb1ea 100644 --- a/pkg/provider/modelmgr/requesters/ollamachat.yaml +++ b/pkg/provider/modelmgr/requesters/ollamachat.yaml @@ -5,27 +5,21 @@ metadata: label: en_US: Ollama zh_CN: Ollama + icon: ollama.svg spec: config: - - name: base-url + - name: base_url label: en_US: Base URL zh_CN: 基础 URL type: string required: true default: "http://127.0.0.1:11434" - - name: args - label: - en_US: Args - zh_CN: 附加参数 - type: object - required: true - default: {} - name: timeout label: en_US: Timeout zh_CN: 超时时间 - type: int + type: integer required: true default: 120 execution: diff --git a/pkg/provider/modelmgr/requesters/openai.svg b/pkg/provider/modelmgr/requesters/openai.svg new file mode 100644 index 00000000..70686f9b --- /dev/null +++ b/pkg/provider/modelmgr/requesters/openai.svg @@ -0,0 +1,4 @@ + + + + diff --git a/pkg/provider/modelmgr/requesters/ppiochatcmpl.py b/pkg/provider/modelmgr/requesters/ppiochatcmpl.py index d0149a80..67c1701a 100644 --- a/pkg/provider/modelmgr/requesters/ppiochatcmpl.py +++ b/pkg/provider/modelmgr/requesters/ppiochatcmpl.py @@ -1,12 +1,11 @@ - from __future__ import annotations import openai -from . import chatcmpl, modelscopechatcmpl -from .. import requester +from . import chatcmpl from ....core import app + class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): """欧派云 ChatCompletion API 请求器""" @@ -17,4 +16,4 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): def __init__(self, ap: app.Application): self.ap = ap - self.requester_cfg = self.ap.provider_cfg.data['requester']['ppio-chat-completions'] \ No newline at end of file + self.requester_cfg = self.ap.provider_cfg.data['requester']['ppio-chat-completions'] diff --git a/pkg/provider/modelmgr/requesters/siliconflow.svg b/pkg/provider/modelmgr/requesters/siliconflow.svg new file mode 100644 index 00000000..ad6b384f --- /dev/null +++ b/pkg/provider/modelmgr/requesters/siliconflow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pkg/provider/modelmgr/requesters/siliconflowchatcmpl.py b/pkg/provider/modelmgr/requesters/siliconflowchatcmpl.py index c763556f..3636d9d1 100644 --- a/pkg/provider/modelmgr/requesters/siliconflowchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/siliconflowchatcmpl.py @@ -1,10 +1,9 @@ from __future__ import annotations +import typing import openai from . import chatcmpl -from .. import requester -from ....core import app class SiliconFlowChatCompletions(chatcmpl.OpenAIChatCompletions): @@ -12,9 +11,7 @@ class SiliconFlowChatCompletions(chatcmpl.OpenAIChatCompletions): client: openai.AsyncClient - requester_cfg: dict - - def __init__(self, ap: app.Application): - self.ap = ap - - self.requester_cfg = self.ap.provider_cfg.data['requester']['siliconflow-chat-completions'] + default_config: dict[str, typing.Any] = { + 'base_url': 'https://api.siliconflow.cn/v1', + 'timeout': 120, + } diff --git a/pkg/provider/modelmgr/requesters/siliconflowchatcmpl.yaml b/pkg/provider/modelmgr/requesters/siliconflowchatcmpl.yaml index 02b08fea..c8dfe770 100644 --- a/pkg/provider/modelmgr/requesters/siliconflowchatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/siliconflowchatcmpl.yaml @@ -5,27 +5,21 @@ metadata: label: en_US: SiliconFlow zh_CN: 硅基流动 + icon: siliconflow.svg spec: config: - - name: base-url + - name: base_url label: en_US: Base URL zh_CN: 基础 URL type: string required: true default: "https://api.siliconflow.cn/v1" - - name: args - label: - en_US: Args - zh_CN: 附加参数 - type: object - required: true - default: {} - name: timeout label: en_US: Timeout zh_CN: 超时时间 - type: int + type: integer required: true default: 120 execution: diff --git a/pkg/provider/modelmgr/requesters/volcark.svg b/pkg/provider/modelmgr/requesters/volcark.svg new file mode 100644 index 00000000..e6454a89 --- /dev/null +++ b/pkg/provider/modelmgr/requesters/volcark.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/pkg/provider/modelmgr/requesters/volcarkchatcmpl.py b/pkg/provider/modelmgr/requesters/volcarkchatcmpl.py index f2a58789..7eb68956 100644 --- a/pkg/provider/modelmgr/requesters/volcarkchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/volcarkchatcmpl.py @@ -1,10 +1,9 @@ from __future__ import annotations +import typing import openai from . import chatcmpl -from .. import requester -from ....core import app class VolcArkChatCompletions(chatcmpl.OpenAIChatCompletions): @@ -12,9 +11,7 @@ class VolcArkChatCompletions(chatcmpl.OpenAIChatCompletions): client: openai.AsyncClient - requester_cfg: dict - - def __init__(self, ap: app.Application): - self.ap = ap - - self.requester_cfg = self.ap.provider_cfg.data['requester']['volcark-chat-completions'] + default_config: dict[str, typing.Any] = { + 'base_url': 'https://ark.cn-beijing.volces.com/api/v3', + 'timeout': 120, + } diff --git a/pkg/provider/modelmgr/requesters/volcarkchatcmpl.yaml b/pkg/provider/modelmgr/requesters/volcarkchatcmpl.yaml index 9beee799..bc639b86 100644 --- a/pkg/provider/modelmgr/requesters/volcarkchatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/volcarkchatcmpl.yaml @@ -5,27 +5,21 @@ metadata: label: en_US: Volc Engine Ark zh_CN: 火山方舟 + icon: volcark.svg spec: config: - - name: base-url + - name: base_url label: en_US: Base URL zh_CN: 基础 URL type: string required: true default: "https://ark.cn-beijing.volces.com/api/v3" - - name: args - label: - en_US: Args - zh_CN: 附加参数 - type: object - required: true - default: {} - name: timeout label: en_US: Timeout zh_CN: 超时时间 - type: int + type: integer required: true default: 120 execution: diff --git a/pkg/provider/modelmgr/requesters/xai.svg b/pkg/provider/modelmgr/requesters/xai.svg new file mode 100644 index 00000000..f8b745cb --- /dev/null +++ b/pkg/provider/modelmgr/requesters/xai.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pkg/provider/modelmgr/requesters/xaichatcmpl.py b/pkg/provider/modelmgr/requesters/xaichatcmpl.py index 217b142f..db2022f1 100644 --- a/pkg/provider/modelmgr/requesters/xaichatcmpl.py +++ b/pkg/provider/modelmgr/requesters/xaichatcmpl.py @@ -1,10 +1,9 @@ from __future__ import annotations +import typing import openai from . import chatcmpl -from .. import requester -from ....core import app class XaiChatCompletions(chatcmpl.OpenAIChatCompletions): @@ -12,9 +11,7 @@ class XaiChatCompletions(chatcmpl.OpenAIChatCompletions): client: openai.AsyncClient - requester_cfg: dict - - def __init__(self, ap: app.Application): - self.ap = ap - - self.requester_cfg = self.ap.provider_cfg.data['requester']['xai-chat-completions'] + default_config: dict[str, typing.Any] = { + 'base_url': 'https://api.x.ai/v1', + 'timeout': 120, + } diff --git a/pkg/provider/modelmgr/requesters/xaichatcmpl.yaml b/pkg/provider/modelmgr/requesters/xaichatcmpl.yaml index a9eee84d..99588dab 100644 --- a/pkg/provider/modelmgr/requesters/xaichatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/xaichatcmpl.yaml @@ -5,27 +5,21 @@ metadata: label: en_US: xAI zh_CN: xAI + icon: xai.svg spec: config: - - name: base-url + - name: base_url label: en_US: Base URL zh_CN: 基础 URL type: string required: true default: "https://api.x.ai/v1" - - name: args - label: - en_US: Args - zh_CN: 附加参数 - type: object - required: true - default: {} - name: timeout label: en_US: Timeout zh_CN: 超时时间 - type: int + type: integer required: true default: 120 execution: diff --git a/pkg/provider/modelmgr/requesters/zhipuai.svg b/pkg/provider/modelmgr/requesters/zhipuai.svg new file mode 100644 index 00000000..016f97dd --- /dev/null +++ b/pkg/provider/modelmgr/requesters/zhipuai.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/pkg/provider/modelmgr/requesters/zhipuaichatcmpl.py b/pkg/provider/modelmgr/requesters/zhipuaichatcmpl.py index 18edd36d..a1a07068 100644 --- a/pkg/provider/modelmgr/requesters/zhipuaichatcmpl.py +++ b/pkg/provider/modelmgr/requesters/zhipuaichatcmpl.py @@ -1,10 +1,9 @@ from __future__ import annotations +import typing import openai -from ....core import app from . import chatcmpl -from .. import requester class ZhipuAIChatCompletions(chatcmpl.OpenAIChatCompletions): @@ -12,9 +11,7 @@ class ZhipuAIChatCompletions(chatcmpl.OpenAIChatCompletions): client: openai.AsyncClient - requester_cfg: dict - - def __init__(self, ap: app.Application): - self.ap = ap - - self.requester_cfg = self.ap.provider_cfg.data['requester']['zhipuai-chat-completions'] + default_config: dict[str, typing.Any] = { + 'base_url': 'https://open.bigmodel.cn/api/paas/v4', + 'timeout': 120, + } diff --git a/pkg/provider/modelmgr/requesters/zhipuaichatcmpl.yaml b/pkg/provider/modelmgr/requesters/zhipuaichatcmpl.yaml index 76ab63e9..68bc3fe3 100644 --- a/pkg/provider/modelmgr/requesters/zhipuaichatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/zhipuaichatcmpl.yaml @@ -5,27 +5,21 @@ metadata: label: en_US: ZhipuAI zh_CN: 智谱 AI + icon: zhipuai.svg spec: config: - - name: base-url + - name: base_url label: en_US: Base URL zh_CN: 基础 URL type: string required: true default: "https://open.bigmodel.cn/api/paas/v4" - - name: args - label: - en_US: Args - zh_CN: 附加参数 - type: object - required: true - default: {} - name: timeout label: en_US: Timeout zh_CN: 超时时间 - type: int + type: integer required: true default: 120 execution: diff --git a/pkg/provider/modelmgr/token.py b/pkg/provider/modelmgr/token.py index f6f9436d..9f477243 100644 --- a/pkg/provider/modelmgr/token.py +++ b/pkg/provider/modelmgr/token.py @@ -3,23 +3,22 @@ from __future__ import annotations import typing -class TokenManager(): - """鉴权 Token 管理器 - """ +class TokenManager: + """鉴权 Token 管理器""" - provider: str + name: str tokens: list[str] using_token_index: typing.Optional[int] = 0 - def __init__(self, provider: str, tokens: list[str]): - self.provider = provider + def __init__(self, name: str, tokens: list[str]): + self.name = name self.tokens = tokens self.using_token_index = 0 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 5a5cf6ef..a74a2dc5 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,20 +22,19 @@ def runner_class(name: str): class RequestRunner(abc.ABC): - """请求运行器 - """ + """请求运行器""" + name: str = None ap: app.Application - def __init__(self, ap: app.Application): - self.ap = ap + pipeline_config: dict - async def initialize(self): - pass + def __init__(self, ap: app.Application, pipeline_config: dict): + self.ap = ap + self.pipeline_config = pipeline_config @abc.abstractmethod async def run(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: - """运行请求 - """ + """运行请求""" pass diff --git a/pkg/provider/runnermgr.py b/pkg/provider/runnermgr.py deleted file mode 100644 index 52e1d8d2..00000000 --- a/pkg/provider/runnermgr.py +++ /dev/null @@ -1,30 +0,0 @@ -from __future__ import annotations - -from . import runner -from ..core import app - -from .runners import localagent -from .runners import difysvapi -from .runners import dashscopeapi - -class RunnerManager: - - ap: app.Application - - using_runner: runner.RequestRunner - - def __init__(self, ap: app.Application): - self.ap = ap - - async def initialize(self): - - for r in runner.preregistered_runners: - if r.name == self.ap.provider_cfg.data['runner']: - self.using_runner = r(self.ap) - await self.using_runner.initialize() - break - else: - raise ValueError(f"未找到请求运行器: {self.ap.provider_cfg.data['runner']}") - - def get_runner(self) -> runner.RequestRunner: - return self.using_runner diff --git a/pkg/provider/runners/dashscopeapi.py b/pkg/provider/runners/dashscopeapi.py index 0bb09822..02cb0b51 100644 --- a/pkg/provider/runners/dashscopeapi.py +++ b/pkg/provider/runners/dashscopeapi.py @@ -1,16 +1,14 @@ from __future__ import annotations import typing -import json -import base64 import re import dashscope from .. import runner -from ...core import entities as core_entities +from ...core import app, entities as core_entities from .. import entities as llm_entities -from ...utils import image + class DashscopeAPIError(Exception): """Dashscope API 请求失败""" @@ -20,64 +18,63 @@ 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中配置) - biz_params: dict = {} # 工作流应用参数(仅在工作流应用中生效) - async def initialize(self): + # 运行器内部使用的配置 + 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): """初始化""" - valid_app_types = ["agent", "workflow"] - self.app_type = self.ap.provider_cfg.data["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.ap.provider_cfg.data["dashscope-app-api"][self.app_type]["app-id"] - self.api_key = self.ap.provider_cfg.data["dashscope-app-api"]["api-key"] - self.references_quote = self.ap.provider_cfg.data["dashscope-app-api"][self.app_type]["references_quote"] - self.biz_params = self.ap.provider_cfg.data["dashscope-app-api"]["workflow"]["biz_params"] - + 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'] + 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) - async def _preprocess_user_message( - self, query: core_entities.Query - ) -> tuple[str, list[str]]: + async def _preprocess_user_message(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}") @@ -91,150 +88,135 @@ 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]: + + 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]: + + 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(self.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]: + + async def run(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: """运行""" - if self.ap.provider_cfg.data["dashscope-app-api"]["app-type"] == "agent": + if self.app_type == 'agent': async for msg in self._agent_messages(query): yield msg - elif self.ap.provider_cfg.data["dashscope-app-api"]["app-type"] == "workflow": + elif self.app_type == 'workflow': async for msg in self._workflow_messages(query): yield msg else: - raise DashscopeAPIError( - f"不支持的 Dashscope 应用类型: {self.ap.provider_cfg.data['dashscope-app-api']['app-type']}" - ) - - + raise DashscopeAPIError(f'不支持的 Dashscope 应用类型: {self.app_type}') diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index 863359f1..26556851 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -5,166 +5,164 @@ import json import uuid import re import base64 -import datetime -import aiohttp from .. import runner -from ...core import entities as core_entities +from ...core import app, entities as core_entities from .. import entities as llm_entities 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 对话请求器""" dify_client: client.AsyncDifyServiceClient - async def initialize(self): - """初始化""" - valid_app_types = ["chat", "agent", "workflow"] - if ( - self.ap.provider_cfg.data["dify-service-api"]["app-type"] - not in valid_app_types - ): + def __init__(self, ap: app.Application, pipeline_config: dict): + self.ap = ap + self.pipeline_config = pipeline_config + + valid_app_types = ['chat', 'agent', 'workflow'] + if self.pipeline_config['ai']['dify-service-api']['app-type'] not in valid_app_types: raise errors.DifyAPIError( - f"不支持的 Dify 应用类型: {self.ap.provider_cfg.data['dify-service-api']['app-type']}" + f'不支持的 Dify 应用类型: {self.pipeline_config["ai"]["dify-service-api"]["app-type"]}' ) - api_key = self.ap.provider_cfg.data["dify-service-api"][ - self.ap.provider_cfg.data["dify-service-api"]["app-type"] - ]["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.ap.provider_cfg.data["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.ap.provider_cfg.data["dify-service-api"]["options"]["convert-thinking-tips"] == "original": + if self.pipeline_config['ai']['dify-service-api']['thinking-convert'] == 'original': return resp_text - - if self.ap.provider_cfg.data["dify-service-api"]["options"]["convert-thinking-tips"] == "remove": - return re.sub(r'
Thinking... .*?
', '', resp_text, flags=re.DOTALL) - - if self.ap.provider_cfg.data["dify-service-api"]["options"]["convert-thinking-tips"] == "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 - ) -> tuple[str, list[str]]: + async def _preprocess_user_message(self, query: core_entities.Query) -> tuple[str, list[str]]: """预处理用户消息,提取纯文本,并将图片上传到 Dify 服务 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": + 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 return plain_text, image_ids - async def _chat_messages( - self, query: core_entities.Query - ) -> typing.AsyncGenerator[llm_entities.Message, None]: + async def _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 ] - mode = "basic" # 标记是基础编排还是工作流编排 + mode = 'basic' # 标记是基础编排还是工作流编排 basic_mode_pending_chunk = '' inputs = {} - + inputs.update(query.variables) - + chunk = None # 初始化chunk变量,防止在没有响应时引用错误 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.ap.provider_cfg.data["dify-service-api"]["chat"]["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", + 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 = '' - - if chunk is None: - raise errors.DifyAPIError("Dify API 没有返回任何响应,请检查网络连接和API配置") - query.session.using_conversation.uuid = chunk["conversation_id"] + if chunk is None: + raise errors.DifyAPIError('Dify API 没有返回任何响应,请检查网络连接和API配置') + + 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 ] @@ -172,25 +170,25 @@ class DifyServiceAPIRunner(runner.RequestRunner): ignored_events = [] inputs = {} - + inputs.update(query.variables) pending_agent_message = '' - + chunk = None # 初始化chunk变量,防止在没有响应时引用错误 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.ap.provider_cfg.data["dify-service-api"]["chat"]["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_message': @@ -199,25 +197,24 @@ class DifyServiceAPIRunner(runner.RequestRunner): if pending_agent_message.strip() != '': pending_agent_message = pending_agent_message.replace('
Action:', '
') yield llm_entities.Message( - role="assistant", + role='assistant', content=self._try_convert_thinking(pending_agent_message), ) pending_agent_message = '' - if chunk["event"] == "agent_thought": - + if chunk['event'] == 'agent_thought': if chunk['tool'] != '' and chunk['observation'] != '': # 工具调用结果,跳过 continue 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({}), ), ) @@ -225,9 +222,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'): @@ -236,76 +231,70 @@ 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)], ) if chunk['event'] == 'error': - raise errors.DifyAPIError("dify 服务错误: " + chunk['message']) - + raise errors.DifyAPIError('dify 服务错误: ' + chunk['message']) + if chunk is None: - raise errors.DifyAPIError("Dify API 没有返回任何响应,请检查网络连接和API配置") + raise errors.DifyAPIError('Dify API 没有返回任何响应,请检查网络连接和API配置') - 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 - ) -> typing.AsyncGenerator[llm_entities.Message, None]: + async def _workflow_messages(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: """调用工作流""" 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.ap.provider_cfg.data["dify-service-api"]["workflow"]["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["data"]["node_type"] == "start" - or chunk["data"]["node_type"] == "end" - ): + if chunk['event'] == 'node_started': + if 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({}), ), ) @@ -314,35 +303,29 @@ 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=self._try_convert_thinking(chunk["data"]["outputs"][ - self.ap.provider_cfg.data["dify-service-api"]["workflow"][ - "output-key" - ] - ]), + role='assistant', + content=chunk['data']['outputs']['summary'], ) yield msg - 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]: """运行请求""" - if self.ap.provider_cfg.data["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.ap.provider_cfg.data["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.ap.provider_cfg.data["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.ap.provider_cfg.data['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 f05c82e3..7d5e04c5 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -4,26 +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]: - """运行请求 - """ - await query.use_model.requester.preprocess(query) - + """运行请求""" pending_tool_calls = [] req_messages = query.prompt.messages.copy() + query.messages.copy() + [query.user_message] # 首次请求 - msg = await query.use_model.requester.call(query, query.use_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, + extra_args=query.use_llm_model.model_entity.extra_args, + ) yield msg @@ -36,15 +38,15 @@ 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( - query, func.name, parameters - ) + func_ret = await self.ap.tool_mgr.execute_func_call(query, func.name, parameters) 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 @@ -52,16 +54,20 @@ class LocalAgentRunner(runner.RequestRunner): req_messages.append(msg) except Exception as e: # 工具调用出错,添加一个报错信息到 req_messages - err_msg = llm_entities.Message( - role="tool", content=f"err: {e}", tool_call_id=tool_call.id - ) + err_msg = llm_entities.Message(role='tool', content=f'err: {e}', tool_call_id=tool_call.id) yield err_msg req_messages.append(err_msg) # 处理完所有调用,再次请求 - msg = await query.use_model.requester.call(query, query.use_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, + extra_args=query.use_llm_model.model_entity.extra_args, + ) yield msg diff --git a/pkg/provider/session/sessionmgr.py b/pkg/provider/session/sessionmgr.py index 00523472..91bec826 100644 --- a/pkg/provider/session/sessionmgr.py +++ b/pkg/provider/session/sessionmgr.py @@ -3,12 +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 @@ -22,16 +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: return session - session_concurrency = self.ap.system_cfg.data['session-concurrency']['default'] - - if f'{query.launcher_type.value}_{query.launcher_id}' in self.ap.system_cfg.data['session-concurrency']: - session_concurrency = self.ap.system_cfg.data['session-concurrency'][f'{query.launcher_type.value}_{query.launcher_id}'] + session_concurrency = self.ap.instance_config.data['concurrency']['session'] session = core_entities.Session( launcher_type=query.launcher_type, @@ -41,17 +36,35 @@ class SessionManager: self.session_list.append(session) return session - async def get_conversation(self, session: core_entities.Session) -> 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: session.conversations = [] + # set prompt + prompt_messages = [] + + for prompt_message in prompt_config: + prompt_messages.append(provider_entities.Message(**prompt_message)) + + prompt = provider_entities.Prompt( + name='default', + messages=prompt_messages, + ) + if session.using_conversation is None: conversation = core_entities.Conversation( - prompt=await self.ap.prompt_mgr.get_prompt(session.use_prompt_name), + prompt=prompt, messages=[], - use_model=await self.ap.model_mgr.get_model_by_name(self.ap.provider_cfg.data['model']), + use_llm_model=await self.ap.model_mgr.get_model_by_uuid( + query.pipeline_config['ai']['local-agent']['model'] + ), use_funcs=await self.ap.tool_mgr.get_all_functions( plugin_enabled=True, ), diff --git a/pkg/provider/sysprompt/entities.py b/pkg/provider/sysprompt/entities.py deleted file mode 100644 index 5442e809..00000000 --- a/pkg/provider/sysprompt/entities.py +++ /dev/null @@ -1,16 +0,0 @@ -from __future__ import annotations - -import typing -import pydantic.v1 as pydantic - -from ...provider import entities - - -class Prompt(pydantic.BaseModel): - """供AI使用的Prompt""" - - name: str - """名称""" - - messages: list[entities.Message] - """消息列表""" diff --git a/pkg/provider/sysprompt/loader.py b/pkg/provider/sysprompt/loader.py deleted file mode 100644 index 855728e2..00000000 --- a/pkg/provider/sysprompt/loader.py +++ /dev/null @@ -1,46 +0,0 @@ -from __future__ import annotations -import abc -import typing - -from ...core import app -from . import entities - - -preregistered_loaders: list[typing.Type[PromptLoader]] = [] - -def loader_class(name: str): - - def decorator(cls: typing.Type[PromptLoader]) -> typing.Type[PromptLoader]: - cls.name = name - preregistered_loaders.append(cls) - return cls - - return decorator - - -class PromptLoader(metaclass=abc.ABCMeta): - """Prompt加载器抽象类 - """ - name: str - - ap: app.Application - - prompts: list[entities.Prompt] - - def __init__(self, ap: app.Application): - self.ap = ap - self.prompts = [] - - async def initialize(self): - pass - - @abc.abstractmethod - async def load(self): - """加载Prompt,存放到prompts列表中 - """ - raise NotImplementedError - - def get_prompts(self) -> list[entities.Prompt]: - """获取Prompt列表 - """ - return self.prompts diff --git a/pkg/provider/sysprompt/loaders/scenario.py b/pkg/provider/sysprompt/loaders/scenario.py deleted file mode 100644 index f907a51c..00000000 --- a/pkg/provider/sysprompt/loaders/scenario.py +++ /dev/null @@ -1,39 +0,0 @@ -from __future__ import annotations - -import json -import os - -from .. import loader -from .. import entities -from ....provider import entities as llm_entities - - -@loader.loader_class("full-scenario") -class ScenarioPromptLoader(loader.PromptLoader): - """加载scenario目录下的json""" - - async def load(self): - """加载Prompt - """ - for file in os.listdir("data/scenario"): - with open("data/scenario/{}".format(file), "r", encoding="utf-8") as f: - file_str = f.read() - file_name = file.split(".")[0] - file_json = json.loads(file_str) - messages = [] - for msg in file_json["prompt"]: - role = 'system' - if "role" in msg: - role = msg['role'] - messages.append( - llm_entities.Message( - role=role, - content=msg['content'], - ) - ) - prompt = entities.Prompt( - name=file_name, - messages=messages - ) - self.prompts.append(prompt) - \ No newline at end of file diff --git a/pkg/provider/sysprompt/loaders/single.py b/pkg/provider/sysprompt/loaders/single.py deleted file mode 100644 index 3ac9c262..00000000 --- a/pkg/provider/sysprompt/loaders/single.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import annotations -import os - -from .. import loader -from .. import entities -from ....provider import entities as llm_entities - - -@loader.loader_class("normal") -class SingleSystemPromptLoader(loader.PromptLoader): - """配置文件中的单条system prompt的prompt加载器 - """ - - async def load(self): - """加载Prompt - """ - - for name, cnt in self.ap.provider_cfg.data['prompt'].items(): - prompt = entities.Prompt( - name=name, - messages=[ - llm_entities.Message( - role='system', - content=cnt - ) - ] - ) - self.prompts.append(prompt) - - for file in os.listdir("data/prompts"): - with open("data/prompts/{}".format(file), "r", encoding="utf-8") as f: - file_str = f.read() - file_name = file.split(".")[0] - prompt = entities.Prompt( - name=file_name, - messages=[ - llm_entities.Message( - role='system', - content=file_str - ) - ] - ) - self.prompts.append(prompt) diff --git a/pkg/provider/sysprompt/sysprompt.py b/pkg/provider/sysprompt/sysprompt.py deleted file mode 100644 index c7695f5a..00000000 --- a/pkg/provider/sysprompt/sysprompt.py +++ /dev/null @@ -1,56 +0,0 @@ -from __future__ import annotations - -from ...core import app -from . import loader -from .loaders import single, scenario - - -class PromptManager: - """Prompt管理器 - """ - - ap: app.Application - - loader_inst: loader.PromptLoader - - default_prompt: str = 'default' - - def __init__(self, ap: app.Application): - self.ap = ap - - async def initialize(self): - - mode_name = self.ap.provider_cfg.data['prompt-mode'] - - loader_class = None - - for loader_cls in loader.preregistered_loaders: - if loader_cls.name == mode_name: - loader_class = loader_cls - break - else: - raise ValueError(f'未知的 Prompt 加载器: {mode_name}') - - self.loader_inst: loader.PromptLoader = loader_class(self.ap) - - await self.loader_inst.initialize() - await self.loader_inst.load() - - def get_all_prompts(self) -> list[loader.entities.Prompt]: - """获取所有Prompt - """ - return self.loader_inst.get_prompts() - - async def get_prompt(self, name: str) -> loader.entities.Prompt: - """获取Prompt - """ - for prompt in self.get_all_prompts(): - if prompt.name == name: - return prompt - - async def get_prompt_by_prefix(self, prefix: str) -> loader.entities.Prompt: - """通过前缀获取Prompt - """ - for prompt in self.get_all_prompts(): - if prompt.name.startswith(prefix): - return prompt 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..76b7d248 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 @@ -51,4 +52,4 @@ class ToolLoader(abc.ABC): @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 a475f9b7..5c030994 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,53 +38,47 @@ 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( - stdio_client(server_params) - ) + stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params)) stdio, write = stdio_transport - self.session = await self.exit_stack.enter_async_context( - ClientSession(stdio, write) - ) + self.session = await self.exit_stack.enter_async_context(ClientSession(stdio, write)) await self.session.initialize() 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( - ClientSession(sseio, write) - ) + self.session = await self.exit_stack.enter_async_context(ClientSession(sseio, write)) 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 +87,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 +122,15 @@ class MCPLoader(loader.ToolLoader): self._last_listed_functions = [] async def initialize(self): - - for server_config in self.ap.provider_cfg.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(): @@ -153,7 +149,7 @@ class MCPLoader(loader.ToolLoader): 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 08211334..b7df2d67 100644 --- a/pkg/provider/tools/loaders/plugin.py +++ b/pkg/provider/tools/loaders/plugin.py @@ -4,26 +4,25 @@ 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] = [] for plugin in self.ap.plugin_mgr.plugins( enabled=enabled, status=plugin_context.RuntimeContainerStatus.INITIALIZED ): - all_functions.extend(plugin.content_functions) + all_functions.extend(plugin.tools) return all_functions @@ -32,7 +31,7 @@ class PluginToolLoader(loader.ToolLoader): for plugin in self.ap.plugin_mgr.plugins( enabled=True, status=plugin_context.RuntimeContainerStatus.INITIALIZED ): - for function in plugin.content_functions: + for function in plugin.tools: if function.name == name: return True return False @@ -44,48 +43,35 @@ class PluginToolLoader(loader.ToolLoader): for plugin in self.ap.plugin_mgr.plugins( enabled=True, status=plugin_context.RuntimeContainerStatus.INITIALIZED ): - for function in plugin.content_functions: + for function in plugin.tools: if function.name == name: return function, plugin.plugin_inst return None, None 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 for p in self.ap.plugin_mgr.plugins(): - if function in p.content_functions: + if function in p.tools: plugin = p break - if plugin is not None: - - await self.ap.ctr_mgr.usage.post_function_record( - plugin={ - "name": plugin.plugin_name, - "remote": plugin.plugin_source, - "version": plugin.plugin_version, - "author": plugin.plugin_author, - }, - function_name=function.name, - function_description=function.description, - ) + # TODO statistics async def shutdown(self): """关闭工具""" diff --git a/pkg/provider/tools/toolmgr.py b/pkg/provider/tools/toolmgr.py index 64befd8c..b1d43d08 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,12 @@ 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] = [] @@ -43,20 +43,18 @@ class ToolManager: 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) return tools - async def generate_tools_for_anthropic( - self, use_funcs: list[entities.LLMFunction] - ) -> list: + async def generate_tools_for_anthropic(self, use_funcs: list[entities.LLMFunction]) -> list: """为anthropic生成函数列表 e.g. @@ -83,24 +81,22 @@ 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) return tools - async def execute_func_call( - self, query: core_entities.Query, name: str, parameters: dict - ) -> typing.Any: + async def execute_func_call(self, query: core_entities.Query, name: str, parameters: dict) -> typing.Any: """执行函数调用""" for loader in self.loaders: 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..7108a08c 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,26 @@ 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 +70,11 @@ 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,21 +94,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] - ) + # TODO statistics return ann_text, logging.INFO except Exception as e: diff --git a/pkg/utils/constants.py b/pkg/utils/constants.py index 16886956..7f3d0804 100644 --- a/pkg/utils/constants.py +++ b/pkg/utils/constants.py @@ -1,5 +1,8 @@ -semantic_version = "v3.4.14.3" +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 8f395c35..86230df8 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,33 @@ 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 +91,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 +107,24 @@ 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 +134,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 +162,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,9 +187,10 @@ 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编码和图片格式 """ @@ -215,16 +199,12 @@ async def extract_b64_and_format(image_base64_data: str) -> typing.Tuple[str, st return base64_str, image_format - -async def get_slack_image_to_base64(pic_url:str, bot_token:str): - headers = {"Authorization": f"Bearer {bot_token}"} +async def get_slack_image_to_base64(pic_url: str, bot_token: str): + headers = {'Authorization': f'Bearer {bot_token}'} try: async with aiohttp.ClientSession() as session: async with session.get(pic_url, headers=headers) as resp: image_data = await resp.read() return base64.b64encode(image_data).decode('utf-8') except Exception as e: - raise(e) - - - + raise (e) diff --git a/pkg/utils/importutil.py b/pkg/utils/importutil.py new file mode 100644 index 00000000..ad93e9f7 --- /dev/null +++ b/pkg/utils/importutil.py @@ -0,0 +1,41 @@ +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..c67fe687 100644 --- a/pkg/utils/ip.py +++ b/pkg/utils/ip.py @@ -1,9 +1,10 @@ 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 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 9c0f8b72..9ce8bdb8 100644 --- a/pkg/utils/pkgmgr.py +++ b/pkg/utils/pkgmgr.py @@ -1,24 +1,38 @@ from pip._internal import main as pipmain -# from . import log - def install(package): pipmain(['install', package]) - # log.reset_logging() + def install_upgrade(package): - pipmain(['install', '--upgrade', package, "-i", "https://pypi.tuna.tsinghua.edu.cn/simple", - "--trusted-host", "pypi.tuna.tsinghua.edu.cn"]) - # log.reset_logging() + pipmain( + [ + 'install', + '--upgrade', + package, + '-i', + 'https://pypi.tuna.tsinghua.edu.cn/simple', + '--trusted-host', + 'pypi.tuna.tsinghua.edu.cn', + ] + ) def run_pip(params: list): pipmain(params) - # log.reset_logging() -def install_requirements(file): - pipmain(['install', '-r', file, "-i", "https://pypi.tuna.tsinghua.edu.cn/simple", - "--trusted-host", "pypi.tuna.tsinghua.edu.cn"]) - # log.reset_logging() +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 + ) diff --git a/pkg/utils/proxy.py b/pkg/utils/proxy.py index db03ad93..04160082 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,14 @@ 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.system_cfg.data['network-proxies'] and self.ap.system_cfg.data['network-proxies']['http']: - self.forward_proxies['http://'] = self.ap.system_cfg.data['network-proxies']['http'] - if 'https' in self.ap.system_cfg.data['network-proxies'] and self.ap.system_cfg.data['network-proxies']['https']: - self.forward_proxies['https://'] = self.ap.system_cfg.data['network-proxies']['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/schema.py b/pkg/utils/schema.py deleted file mode 100644 index 378cdf5b..00000000 --- a/pkg/utils/schema.py +++ /dev/null @@ -1,14 +0,0 @@ -import os -import json - - -def load_schema(schema_path: str) -> dict: - with open(schema_path, 'r', encoding='utf-8') as f: - return json.load(f) - - -CONFIG_SYSTEM_SCHEMA = load_schema("templates/schema/system.json") -CONFIG_PIPELINE_SCHEMA = load_schema("templates/schema/pipeline.json") -CONFIG_COMMAND_SCHEMA = load_schema("templates/schema/command.json") -CONFIG_PLATFORM_SCHEMA = load_schema("templates/schema/platform.json") -CONFIG_PROVIDER_SCHEMA = load_schema("templates/schema/provider.json") diff --git a/pkg/utils/version.py b/pkg/utils/version.py index 5e5741c6..46c1aad6 100644 --- a/pkg/utils/version.py +++ b/pkg/utils/version.py @@ -3,7 +3,6 @@ from __future__ import annotations import os import typing import logging -import time import requests @@ -12,56 +11,46 @@ 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() current_tag = self.get_current_version() - old_tag = current_tag rls_list = await self.get_release_list() 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 +58,56 @@ 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): # 没有新版本 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() - ) + zip_resp = requests.get(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,21 +117,16 @@ 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", - old_version=old_tag, - new_version=current_tag, - ) + # TODO statistics async def is_new_version_available(self) -> bool: """检查是否有新版本""" @@ -155,23 +139,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 +163,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 +192,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 "有新版本可用,请使用管理员账号发送 !update 命令更新", 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 d0adc126..dafa6985 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,8 +34,12 @@ dashscope python-telegram-bot certifi mcp +sqlmodel slack_sdk telegramify-markdown + # indirect taskgroup==0.0.0a4 +ruff +pre-commit python-socks \ 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/config.yaml b/templates/config.yaml new file mode 100644 index 00000000..109cd8d7 --- /dev/null +++ b/templates/config.yaml @@ -0,0 +1,20 @@ +admins: [] +api: + port: 5300 +command: + prefix: + - '!' + - ! + privilege: {} +concurrency: + pipeline: 20 + session: 1 +mcp: + servers: [] +proxy: + http: '' + https: '' +system: + jwt: + expire: 604800 + secret: '' diff --git a/templates/default-pipeline-config.json b/templates/default-pipeline-config.json new file mode 100644 index 00000000..ead811be --- /dev/null +++ b/templates/default-pipeline-config.json @@ -0,0 +1,77 @@ +{ + "trigger": { + "group-respond-rules": { + "at": true, + "prefix": [ + "ai" + ], + "regexp": [], + "random": 0.0 + }, + "access-control": { + "mode": "blacklist", + "blacklist": [], + "whitelist": [] + }, + "ignore-rules": { + "prefix": [], + "regexp": [] + } + }, + "safety": { + "content-filter": { + "scope": "all", + "check-sensitive-words": true + }, + "rate-limit": { + "window-length": 60, + "limitation": 60, + "strategy": "drop" + } + }, + "ai": { + "runner": { + "runner": "local-agent" + }, + "local-agent": { + "model": "", + "max-round": 10, + "prompt": [ + { + "role": "system", + "content": "You are a helpful assistant." + } + ] + }, + "dify-service-api": { + "base-url": "https://api.dify.ai/v1", + "app-type": "chat", + "api-key": "your-api-key", + "thinking-convert": "plain", + "timeout": 30 + }, + "dashscope-app-api": { + "app-type": "agent", + "api-key": "your-api-key", + "app-id": "your-app-id", + "references-quote": "参考资料来自:" + } + }, + "output": { + "long-text-processing": { + "threshold": 1000, + "strategy": "forward", + "font-path": "" + }, + "force-delay": { + "min": 0, + "max": 0 + }, + "misc": { + "hide-exception": true, + "at-sender": true, + "quote-origin": true, + "track-function-calls": false + } + } +} \ No newline at end of file diff --git a/templates/command.json b/templates/legacy/command.json similarity index 100% rename from templates/command.json rename to templates/legacy/command.json diff --git a/templates/pipeline.json b/templates/legacy/pipeline.json similarity index 100% rename from templates/pipeline.json rename to templates/legacy/pipeline.json diff --git a/templates/platform.json b/templates/legacy/platform.json similarity index 100% rename from templates/platform.json rename to templates/legacy/platform.json diff --git a/templates/provider.json b/templates/legacy/provider.json similarity index 100% rename from templates/provider.json rename to templates/legacy/provider.json diff --git a/templates/system.json b/templates/legacy/system.json similarity index 94% rename from templates/system.json rename to templates/legacy/system.json index c090ea0e..f3e69feb 100644 --- a/templates/system.json +++ b/templates/legacy/system.json @@ -20,7 +20,7 @@ }, "persistence": { "sqlite": { - "path": "data/persistence.db" + "path": "data/langbot.db" }, "use": "sqlite" } diff --git a/templates/metadata/adapter-qq-botpy.json b/templates/metadata/adapter-qq-botpy.json deleted file mode 100644 index 765f12c9..00000000 --- a/templates/metadata/adapter-qq-botpy.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "mapping": { - "groups": {}, - "members": {} - } -} \ No newline at end of file diff --git a/templates/metadata/llm-models.json b/templates/metadata/llm-models.json deleted file mode 100644 index a12c7687..00000000 --- a/templates/metadata/llm-models.json +++ /dev/null @@ -1,237 +0,0 @@ -{ - "list": [ - { - "name": "default", - "requester": "openai-chat-completions", - "token_mgr": "openai", - "tool_call_supported": false, - "vision_supported": false - }, - { - "name": "gpt-4o", - "tool_call_supported": true, - "vision_supported": true - }, - { - "name": "gpt-4o-2024-11-20", - "tool_call_supported": true, - "vision_supported": true - }, - { - "name": "gpt-4o-2024-08-06", - "tool_call_supported": true, - "vision_supported": true - }, - { - "name": "gpt-4o-2024-05-13", - "tool_call_supported": true, - "vision_supported": true - }, - { - "name": "chatgpt-4o-latest", - "tool_call_supported": true, - "vision_supported": true - }, - { - "name": "gpt-4o-mini", - "tool_call_supported": true, - "vision_supported": true - }, - { - "name": "o1-preview", - "tool_call_supported": true, - "vision_supported": true - }, - { - "name": "o1-mini", - "tool_call_supported": true, - "vision_supported": true - }, - { - "name": "gpt-4-turbo", - "tool_call_supported": true, - "vision_supported": true - }, - { - "name": "gpt-4", - "tool_call_supported": true, - "vision_supported": true - }, - { - "name": "gpt-3.5-turbo", - "tool_call_supported": true, - "vision_supported": false - }, - { - "model_name": "SparkDesk", - "name": "OneAPI/SparkDesk" - }, - { - "model_name": "gemini-pro", - "name": "OneAPI/gemini-pro" - }, - { - "name": "claude-3-opus-latest", - "requester": "anthropic-messages", - "token_mgr": "anthropic", - "vision_supported": true, - "tool_call_supported": true - }, - { - "name": "claude-3-5-sonnet-latest", - "requester": "anthropic-messages", - "token_mgr": "anthropic", - "vision_supported": true, - "tool_call_supported": true - }, - { - "name": "claude-3-5-haiku-latest", - "requester": "anthropic-messages", - "token_mgr": "anthropic", - "vision_supported": true, - "tool_call_supported": true - }, - { - "name": "claude-3-7-sonnet-latest", - "requester": "anthropic-messages", - "token_mgr": "anthropic", - "vision_supported": true, - "tool_call_supported": true - }, - { - "name": "moonshot-v1-8k", - "requester": "moonshot-chat-completions", - "token_mgr": "moonshot", - "tool_call_supported": true - }, - { - "name": "moonshot-v1-32k", - "requester": "moonshot-chat-completions", - "token_mgr": "moonshot", - "tool_call_supported": true - }, - { - "name": "moonshot-v1-128k", - "requester": "moonshot-chat-completions", - "token_mgr": "moonshot", - "tool_call_supported": true - }, - { - "name": "deepseek-chat", - "requester": "deepseek-chat-completions", - "token_mgr": "deepseek" - }, - { - "name": "deepseek-coder", - "requester": "deepseek-chat-completions", - "token_mgr": "deepseek" - }, - { - "name": "deepseek-reasoner", - "requester": "deepseek-chat-completions", - "token_mgr": "deepseek" - }, - { - "name": "grok-2-latest", - "requester": "xai-chat-completions", - "token_mgr": "xai" - }, - { - "name": "grok-2", - "requester": "xai-chat-completions", - "token_mgr": "xai" - }, - { - "name": "grok-2-vision-1212", - "requester": "xai-chat-completions", - "token_mgr": "xai", - "vision_supported": true - }, - { - "name": "grok-2-1212", - "requester": "xai-chat-completions", - "token_mgr": "xai" - }, - { - "name": "grok-vision-beta", - "requester": "xai-chat-completions", - "token_mgr": "xai", - "vision_supported": true - }, - { - "name": "grok-beta", - "requester": "xai-chat-completions", - "token_mgr": "xai" - }, - { - "name": "glm-4-plus", - "requester": "zhipuai-chat-completions", - "token_mgr": "zhipuai", - "tool_call_supported": true - }, - { - "name": "glm-4-0520", - "requester": "zhipuai-chat-completions", - "token_mgr": "zhipuai", - "tool_call_supported": true - }, - { - "name": "glm-4-air", - "requester": "zhipuai-chat-completions", - "token_mgr": "zhipuai", - "tool_call_supported": true - }, - { - "name": "glm-4-airx", - "requester": "zhipuai-chat-completions", - "token_mgr": "zhipuai", - "tool_call_supported": true - }, - { - "name": "glm-4-long", - "requester": "zhipuai-chat-completions", - "token_mgr": "zhipuai", - "tool_call_supported": true - }, - { - "name": "glm-4-flashx", - "requester": "zhipuai-chat-completions", - "token_mgr": "zhipuai", - "tool_call_supported": true - }, - { - "name": "glm-4-flash", - "requester": "zhipuai-chat-completions", - "token_mgr": "zhipuai", - "tool_call_supported": true - }, - { - "name": "glm-4v-plus", - "requester": "zhipuai-chat-completions", - "token_mgr": "zhipuai", - "vision_supported": true, - "tool_call_supported": true - }, - { - "name": "glm-4v", - "requester": "zhipuai-chat-completions", - "token_mgr": "zhipuai", - "vision_supported": true, - "tool_call_supported": true - }, - { - "name": "glm-4v-flash", - "requester": "zhipuai-chat-completions", - "token_mgr": "zhipuai", - "vision_supported": true, - "tool_call_supported": true - }, - { - "name": "glm-zero-preview", - "requester": "zhipuai-chat-completions", - "token_mgr": "zhipuai", - "vision_supported": true, - "tool_call_supported": true - } - ] -} \ No newline at end of file diff --git a/templates/metadata/pipeline/ai.yaml b/templates/metadata/pipeline/ai.yaml new file mode 100644 index 00000000..a0305cce --- /dev/null +++ b/templates/metadata/pipeline/ai.yaml @@ -0,0 +1,172 @@ +name: ai +label: + en_US: AI Feature + zh_CN: AI 能力 +stages: + - name: runner + label: + en_US: Runner + zh_CN: 运行方式 + description: + en_US: Strategy to call AI to process messages + zh_CN: 调用 AI 处理消息的方式 + config: + - name: runner + label: + en_US: Runner + zh_CN: 运行器 + type: select + required: true + default: local-agent + options: + - name: local-agent + label: + en_US: Embedded Agent + zh_CN: 内置 Agent + - name: dify-service-api + label: + en_US: Dify Service API + zh_CN: Dify 服务 API + - name: dashscope-app-api + label: + en_US: Aliyun Dashscope App API + zh_CN: 阿里云百炼平台 API + - name: local-agent + label: + en_US: Embedded Agent + zh_CN: 内置 Agent + description: + en_US: Configure the embedded agent of the pipeline + zh_CN: 配置内置 Agent + config: + - name: model + label: + en_US: Model + zh_CN: 模型 + type: llm-model-selector + required: true + - name: max-round + label: + en_US: Max Round + zh_CN: 最大回合数 + description: + en_US: The maximum number of previous messages that the agent can remember + zh_CN: 最大前文消息回合数 + type: integer + required: true + default: 10 + - name: prompt + label: + en_US: Prompt + zh_CN: 提示词 + description: + en_US: The prompt of the agent + zh_CN: 除非您了解消息结构,否则请只使用 system 单提示词 + type: prompt-editor + required: true + - name: dify-service-api + label: + en_US: Dify Service API + zh_CN: Dify 服务 API + description: + en_US: Configure the Dify service API of the pipeline + zh_CN: 配置 Dify 服务 API + config: + - name: base-url + label: + en_US: Base URL + zh_CN: 基础 URL + type: string + required: true + - name: app-type + label: + en_US: App Type + zh_CN: 应用类型 + type: select + required: true + default: chat + options: + - name: chat + label: + en_US: Chat + zh_CN: 聊天(包括Chatflow) + - name: agent + label: + en_US: Agent + zh_CN: Agent + - name: workflow + label: + en_US: Workflow + zh_CN: 工作流 + - name: api-key + label: + en_US: API Key + zh_CN: API 密钥 + type: string + required: true + - name: thinking-convert + label: + en_US: CoT Convert + zh_CN: 思维链转换策略 + type: select + required: true + default: plain + options: + - name: plain + label: + en_US: Convert to ... + zh_CN: 转换成 ... + - name: original + label: + en_US: Original + zh_CN: 原始 + - name: remove + label: + en_US: Remove + zh_CN: 移除 + - name: dashscope-app-api + label: + en_US: Aliyun Dashscope App API + zh_CN: 阿里云百炼平台 API + description: + en_US: Configure the Aliyun Dashscope App API of the pipeline + zh_CN: 配置阿里云百炼平台 API + config: + - name: app-type + label: + en_US: App Type + zh_CN: 应用类型 + type: select + required: true + default: agent + options: + - name: agent + label: + en_US: Agent + zh_CN: Agent + - name: workflow + label: + en_US: Workflow + zh_CN: 工作流 + - name: api-key + label: + en_US: API Key + zh_CN: API 密钥 + type: string + required: true + - name: app-id + label: + en_US: App ID + zh_CN: 应用 ID + type: string + required: true + - name: references_quote + label: + en_US: References Quote + zh_CN: 引用文本 + description: + en_US: The text prompt when the references are included + zh_CN: 包含引用资料时的文本提示 + type: string + required: false + default: '参考资料来自:' diff --git a/templates/metadata/pipeline/output.yaml b/templates/metadata/pipeline/output.yaml new file mode 100644 index 00000000..8f035cd1 --- /dev/null +++ b/templates/metadata/pipeline/output.yaml @@ -0,0 +1,107 @@ +name: output +label: + en_US: Output Processing + zh_CN: 输出处理 +stages: + - name: long-text-processing + label: + en_US: Long Text Processing + zh_CN: 长文本处理 + config: + - name: threshold + label: + en_US: Threshold + zh_CN: 阈值 + description: + en_US: The threshold of the long text + zh_CN: 超过此长度的文本将被处理 + type: integer + required: true + default: 1000 + - name: strategy + label: + en_US: Strategy + zh_CN: 策略 + description: + en_US: The strategy of the long text + zh_CN: 长文本的处理策略 + type: select + required: true + default: forward + options: + - name: forward + label: + en_US: Forward Message Component + zh_CN: 转换为转发消息组件(部分平台不支持) + - name: image + label: + en_US: Convert to Image + zh_CN: 转换为图片 + - name: font-path + label: + en_US: Font Path + zh_CN: 字体路径 + description: + en_US: The path of the font to be used when converting to image + zh_CN: 选用转换为图片时,所使用的字体路径 + type: string + required: false + default: '' + - name: force-delay + label: + en_US: Force Delay + zh_CN: 强制延迟 + description: + en_US: Force the output to be delayed for a while + zh_CN: 强制延迟一段时间后再回复给用户 + config: + - name: min + label: + en_US: Min Seconds + zh_CN: 最小秒数 + type: integer + required: true + default: 0 + - name: max + label: + en_US: Max Seconds + zh_CN: 最大秒数 + type: integer + required: true + default: 0 + - name: misc + label: + en_US: Misc + zh_CN: 杂项 + config: + - name: hide-exception + label: + en_US: Hide Exception + zh_CN: 不输出异常信息给用户 + type: boolean + required: true + default: true + - name: at-sender + label: + en_US: At Sender + zh_CN: 在群聊回复中@发送者 + type: boolean + required: true + default: true + - name: quote-origin + label: + en_US: Quote Origin Message + zh_CN: 引用原消息 + type: boolean + required: true + default: false + - name: track-function-calls + label: + en_US: Track Function Calls + zh_CN: 跟踪函数调用 + description: + en_US: If enabled, the function calls will be tracked and output to the user + zh_CN: 启用后,Agent 每次调用工具时都会输出一个提示给用户 + type: boolean + required: true + default: false diff --git a/templates/metadata/pipeline/safety.yaml b/templates/metadata/pipeline/safety.yaml new file mode 100644 index 00000000..975b34f9 --- /dev/null +++ b/templates/metadata/pipeline/safety.yaml @@ -0,0 +1,75 @@ +name: safety +label: + en_US: Safety Control + zh_CN: 安全控制 +stages: + - name: content-filter + label: + en_US: Content Filter + zh_CN: 内容过滤 + config: + - name: scope + label: + en_US: Scope + zh_CN: 检查范围 + type: select + required: true + default: all + options: + - name: all + label: + en_US: All + zh_CN: 全部 + - name: income-msg + label: + en_US: Income Message + zh_CN: 传入消息(用户消息) + - name: output-msg + label: + en_US: Output Message + zh_CN: 传出消息(机器人消息) + - name: check-sensitive-words + label: + en_US: Check Sensitive Words + zh_CN: 检查敏感词 + description: + en_US: Sensitive words can be configured in data/metadata/sensitive-words.json + zh_CN: 敏感词内容可以在 data/metadata/sensitive-words.json 中配置 + type: boolean + required: true + default: false + - name: rate-limit + label: + en_US: Rate Limit + zh_CN: 速率限制 + config: + - name: window-length + label: + en_US: Window Length + zh_CN: 窗口长度(秒) + type: integer + required: true + default: 60 + - name: limitation + label: + en_US: Limitation + zh_CN: 限制次数 + type: integer + required: true + default: 60 + - name: strategy + label: + en_US: Strategy + zh_CN: 策略 + type: select + required: true + default: drop + options: + - name: drop + label: + en_US: Drop + zh_CN: 丢弃 + - name: wait + label: + en_US: Wait + zh_CN: 等待 \ No newline at end of file diff --git a/templates/metadata/pipeline/trigger.yaml b/templates/metadata/pipeline/trigger.yaml new file mode 100644 index 00000000..69e54363 --- /dev/null +++ b/templates/metadata/pipeline/trigger.yaml @@ -0,0 +1,119 @@ +name: trigger +label: + en_US: Trigger + zh_CN: 触发条件 +stages: + - name: group-respond-rules + label: + en_US: Group Respond Rule + zh_CN: 群响应规则 + description: + en_US: The respond rule of the messages in the groups + zh_CN: 群内消息的响应规则 + config: + - name: at + label: + en_US: At + zh_CN: '@' + description: + en_US: Whether to trigger when the message mentions the bot + zh_CN: 是否在消息@机器人时触发 + type: boolean + required: true + default: false + - name: prefix + label: + en_US: Prefix + zh_CN: 前缀 + description: + en_US: Messages with these prefixes will be responded (the prefixes will be removed automatically when sending to AI) + zh_CN: 具有这些前缀的消息将被响应(发送给 AI 时会自动去除对应前缀) + type: array[string] + required: true + default: [] + - name: regexp + label: + en_US: Regexp + zh_CN: 正则表达式 + description: + en_US: Messages with these regular expressions will be responded + zh_CN: 符合这些正则表达式的消息将被响应 + type: array[string] + required: true + default: [] + - name: random + label: + en_US: Random + zh_CN: 随机 + description: + en_US: The probability of the random response, range from 0.0 to 1.0 + zh_CN: 随机响应概率,范围为 0.0-1.0,对应 0% 到 100% + type: float + required: false + default: 0 + - name: access-control + label: + en_US: Access Control + zh_CN: 访问控制 + config: + - name: mode + label: + en_US: Mode + zh_CN: 模式 + description: + en_US: The mode of the access control + zh_CN: 访问控制模式 + type: select + required: true + default: blacklist + options: + - name: blacklist + label: + en_US: Blacklist + zh_CN: 黑名单 + - name: whitelist + label: + en_US: Whitelist + zh_CN: 白名单 + - name: blacklist + label: + en_US: Blacklist + zh_CN: 黑名单 + type: array[string] + required: true + default: [] + - name: whitelist + label: + en_US: Whitelist + zh_CN: 白名单 + type: array[string] + required: true + default: [] + - name: ignore-rules + label: + en_US: Ignore Rules + zh_CN: 消息忽略规则 + description: + en_US: Ignore rules that apply to both group and private messages + zh_CN: 对群聊、私聊消息均适用的忽略规则(优先级高于群响应规则) + config: + - name: prefix + label: + en_US: Prefix + zh_CN: 前缀 + description: + en_US: Messages with these prefixes will be ignored + zh_CN: 包含这些前缀的消息将被忽略 + type: array[string] + required: true + default: [] + - name: regexp + label: + en_US: Regexp + zh_CN: 正则表达式 + description: + en_US: Messages with these regular expressions will be ignored + zh_CN: 符合这些正则表达式的消息将被忽略 + type: array[string] + required: true + default: [] diff --git a/templates/plugin-settings.json b/templates/plugin-settings.json deleted file mode 100644 index 1d807ed1..00000000 --- a/templates/plugin-settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "plugins": [] -} \ No newline at end of file diff --git a/templates/scenario-template.json b/templates/scenario-template.json deleted file mode 100644 index d9b7267a..00000000 --- a/templates/scenario-template.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "prompt": [ - { - "role": "system", - "content": "You are a helpful assistant. 如果我需要帮助,你要说“输入!help获得帮助”" - }, - { - "role": "assistant", - "content": "好的,我是一个能干的AI助手。 如果你需要帮助,我会说“输入!help获得帮助”" - } - ] -} \ No newline at end of file diff --git a/templates/schema/command.json b/templates/schema/command.json deleted file mode 100644 index 1cfc2541..00000000 --- a/templates/schema/command.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "object", - "layout": "expansion-panels", - "properties": { - "command-prefix": { - "type": "array", - "title": "命令前缀", - "description": "以数组形式设置,程序将前缀符合设置的消息视为命令(群内需要符合群响应规则)", - "items": { - "type": "string" - }, - "default": [ - "!", - "!" - ] - }, - "privilege": { - "type": "object", - "title": "权限管理", - "description": "设置每个命令的权限配置。普通用户权限级别为 1,管理员(system.json中设置的)权限级别为 2;在这里设置每个命令的最低权限级别,若设置为1,则用户和管理员均可用,若为2,则仅管理员可用;设置子命令时,以点号间隔,如\"plugin.on\"", - "properties": { - "placeholder": { - "type": "integer", - "minimum": 1, - "maximum": 2, - "const": 1 - } - }, - "patternProperties": { - "^[a-zA-Z0-9_.]+$": { - "type": "integer", - "minimum": 1, - "maximum": 2 - } - }, - "default": {} - } - } -} \ No newline at end of file diff --git a/templates/schema/pipeline.json b/templates/schema/pipeline.json deleted file mode 100644 index 787c9e71..00000000 --- a/templates/schema/pipeline.json +++ /dev/null @@ -1,326 +0,0 @@ -{ - "type": "object", - "layout": "expansion-panels", - "properties": { - "access-control": { - "type": "object", - "title": "访问控制", - "properties": { - "mode": { - "type": "string", - "title": "访问控制模式", - "description": "访问控制模式,支持黑名单和白名单", - "enum": [ - "blacklist", - "whitelist" - ], - "default": "blacklist" - }, - "blacklist": { - "type": "array", - "title": "黑名单", - "description": "黑名单中的会话将无法使用机器人,仅在访问控制模式为黑名单时有效。格式:{type}_{id},示例:group_12345678 或 person_12341234", - "items": { - "type": "string", - "format": "regex", - "pattern": "^(person|group)_.*$" - }, - "default": [] - }, - "whitelist": { - "type": "array", - "title": "白名单", - "description": "仅白名单中的会话可以使用机器人,仅在访问控制模式为白名单时有效。格式:{type}_{id},示例:group_12345678 或 person_12341234", - "items": { - "type": "string", - "format": "regex", - "pattern": "^(person|group)_.*$" - }, - "default": [] - } - }, - "required": [ - "mode" - ] - }, - "respond-rules": { - "type": "object", - "title": "群消息响应规则", - "description": "仅处理 访问控制 允许的会话的消息。所有未指定的群使用 默认响应规则,若需指定特定的群的规则,请输入 群号 并添加,并设置响应规则", - "properties": { - "default": { - "type": "object", - "title": "默认响应规则", - "properties": { - "at": { - "type": "boolean", - "title": "是否响应 @ 消息", - "layout": { - "comp": "switch", - "props": { - "color": "primary" - } - } - }, - "prefix": { - "type": "array", - "title": "响应前缀", - "description": "带有指定前缀的消息即使没有 at 机器人也会被响应,发送给 AI 时会删除前缀", - "items": { - "type": "string" - }, - "default": [] - }, - "regexp": { - "type": "array", - "title": "响应正则表达式", - "description": "正则表达式教程:https://www.runoob.com/regexp/regexp-syntax.html", - "items": { - "type": "string", - "format": "regex" - }, - "default": [] - }, - "random": { - "type": "number", - "title": "随机响应概率", - "description": "数值范围是0.0-1.0,对应概率0%-100%,为1.0时所有消息都响应", - "minimum": 0, - "maximum": 1, - "step": 0.01, - "layout": { - "comp": "slider", - "props": { - "color": "primary" - } - } - } - } - } - }, - "patternProperties": { - "^.*$": { - "type": "object", - "properties": { - "at": { - "type": "boolean", - "title": "是否响应 @ 消息", - "layout": { - "comp": "switch", - "props": { - "color": "primary" - } - } - }, - "prefix": { - "type": "array", - "title": "响应前缀", - "description": "带有指定前缀的消息即使没有 at 机器人也会被响应,发送给 AI 时会删除前缀", - "items": { - "type": "string" - }, - "default": [] - }, - "regexp": { - "type": "array", - "title": "响应正则表达式", - "description": "正则表达式教程:https://www.runoob.com/regexp/regexp-syntax.html", - "items": { - "type": "string", - "format": "regex" - }, - "default": [] - }, - "random": { - "type": "number", - "title": "随机响应概率", - "description": "数值范围是0.0-1.0,对应概率0%-100%,为1.0时所有消息都响应", - "minimum": 0, - "maximum": 1, - "step": 0.01, - "layout": { - "comp": "slider", - "props": { - "color": "primary" - } - } - } - } - } - } - }, - "income-msg-check": { - "type": "boolean", - "title": "检查传入消息内容", - "description": "是否对传入的消息(用户消息)进行检查,需配合审核策略使用(AI 响应内容一定会通过检查策略)", - "layout": { - "comp": "switch", - "props": { - "color": "primary" - } - } - }, - "ignore-rules": { - "type": "object", - "title": "传入消息忽略规则", - "description": "符合规则的传入消息将被忽略,仅传入消息检查被启用时生效", - "properties": { - "prefix": { - "type": "array", - "title": "忽略前缀", - "description": "具有指定前缀的消息将被忽略", - "items": { - "type": "string" - }, - "default": [] - }, - "regexp": { - "type": "array", - "title": "忽略正则表达式", - "description": "正则表达式教程:https://www.runoob.com/regexp/regexp-syntax.html", - "items": { - "type": "string", - "format": "regex" - }, - "default": [] - } - } - }, - "check-sensitive-words": { - "type": "boolean", - "title": "本地敏感词检查", - "description": "是否启用本地敏感词检查", - "layout": { - "comp": "switch", - "props": { - "color": "primary" - } - } - }, - "baidu-cloud-examine": { - "type": "object", - "title": "百度云内容审核配置", - "description": "百度云内容审核配置,前往:https://cloud.baidu.com/doc/ANTIPORN/index.html 获取 API Key 和 API Secret", - "properties": { - "enable": { - "type": "boolean", - "title": "是否启用", - "layout": { - "comp": "switch", - "props": { - "color": "primary" - } - } - }, - "api-key": { - "type": "string", - "title": "API Key", - "default": "" - }, - "api-secret": { - "type": "string", - "title": "API Secret", - "default": "" - } - } - }, - "rate-limit": { - "type": "object", - "title": "请求限速规则", - "properties": { - "strategy": { - "type": "string", - "title": "限速策略", - "description": "会话中的请求速率超过限制时的处理策略,drop为丢弃新请求,wait为等待请求速率降到限制以下", - "enum": [ - "drop", - "wait" - ], - "default": "drop" - }, - "algo": { - "type": "string", - "title": "限速算法", - "description": "目前仅支持 fixwin(固定窗口),支持插件扩展", - "enum": [ - "fixwin" - ], - "default": "fixwin" - }, - "fixwin": { - "type": "object", - "title": "固定窗口限速策略配置", - "description": "所有会话使用默认限速策略,若需指定特定会话的限速策略,请输入 会话名称(格式为 {type}_{id},示例:group_123456 或 person_123456) 并添加,以设置特定会话的限速参数", - "properties": { - "default": { - "type": "object", - "title": "默认限速策略", - "properties": { - "window-size": { - "type": "integer", - "title": "窗口大小(秒)", - "minimum": 1, - "default": 60 - }, - "limit": { - "type": "integer", - "title": "窗口期间允许的最大消息数", - "minimum": 1, - "default": 60 - } - } - } - }, - "patternProperties": { - "^(person|group).*$": { - "type": "object", - "title": "会话限速", - "properties": { - "window-size": { - "type": "integer", - "title": "窗口大小(秒)", - "minimum": 1, - "default": 60 - }, - "limit": { - "type": "integer", - "title": "窗口期间允许的最大消息数", - "minimum": 1, - "default": 60 - } - } - } - } - } - } - }, - "msg-truncate": { - "type": "object", - "title": "对话历史记录截断", - "description": "将在发送消息给模型之前对当前会话的历史消息进行截断,以限制传给模型的消息长度", - "properties": { - "method": { - "type": "string", - "title": "截断方法", - "description": "目前仅支持 round(按回合截断),支持插件扩展", - "enum": [ - "round" - ], - "default": "round" - }, - "round": { - "type": "object", - "title": "轮次截断策略配置", - "properties": { - "max-round": { - "type": "integer", - "title": "最大保留前文回合数", - "minimum": 1, - "default": 10 - } - } - } - } - } - } -} \ No newline at end of file diff --git a/templates/schema/platform.json b/templates/schema/platform.json deleted file mode 100644 index ffc735a0..00000000 --- a/templates/schema/platform.json +++ /dev/null @@ -1,608 +0,0 @@ -{ - "type": "object", - "layout": "expansion-panels", - "properties": { - "platform-adapters": { - "type": "array", - "title": "消息平台适配器", - "default": {}, - "items": { - "type": "object", - "oneOf": [ - { - "title": "Nakuru 适配器", - "description": "用于接入 go-cqhttp", - "properties": { - "adapter": { - "type": "string", - "const": "nakuru" - }, - "enable": { - "type": "boolean", - "default": false, - "description": "是否启用此适配器", - "layout": { - "comp": "switch", - "props": { - "color": "primary" - } - } - }, - "host": { - "type": "string", - "default": "127.0.0.1" - }, - "ws_port": { - "type": "integer", - "default": 8080 - }, - "http_port": { - "type": "integer", - "default": 5700 - }, - "token": { - "type": "string", - "default": "" - } - } - }, - { - "title": "aiocqhttp 适配器", - "description": "用于接入 Lagrange 等兼容 OneBot v11 协议的机器人框架(仅支持反向ws)", - "properties": { - "adapter": { - "type": "string", - "const": "aiocqhttp" - }, - "enable": { - "type": "boolean", - "default": false, - "description": "是否启用此适配器", - "layout": { - "comp": "switch", - "props": { - "color": "primary" - } - } - }, - "host": { - "type": "string", - "default": "0.0.0.0", - "description": "监听的 IP 地址,一般就保持 0.0.0.0 就可以了。使用 aiocqhttp 时,LangBot 作为服务端被动等待框架连接,请在 Lagrange 等框架中设置被动 ws 地址或者反向 ws 地址(具体视框架而定)为 LangBot 监听的地址,且路径为/ws,例如:ws://127.0.0.1:2280/ws" - }, - "port": { - "type": "integer", - "default": 2290, - "description": "设置监听的端口,默认2280,需在 Lagrange 等框架中设置为与此处一致的端口" - }, - "access-token": { - "type": "string", - "default": "", - "description": "设置访问密钥,与 Lagrange 等框架中设置的保持一致" - } - } - }, - { - "title": "qq-botpy 适配器(WebSocket)", - "description": "用于接入 QQ 官方机器人 API", - "properties": { - "adapter": { - "type": "string", - "const": "qq-botpy" - }, - "enable": { - "type": "boolean", - "default": false, - "description": "是否启用此适配器", - "layout": { - "comp": "switch", - "props": { - "color": "primary" - } - } - }, - "appid": { - "type": "string", - "default": "", - "description": "申请到的QQ官方机器人的appid" - }, - "secret": { - "type": "string", - "default": "", - "description": "申请到的QQ官方机器人的secret" - }, - "intents": { - "type": "array", - "description": "控制监听的事件类型,需要填写才能接收到对应消息,目前支持的事件类型有:public_guild_messages(QQ 频道消息)、direct_message(QQ 频道私聊消息)、public_messages(QQ 群 和 列表私聊消息)", - "default": [ - "public_guild_messages", - "direct_message", - "public_messages" - ] - } - } - }, - { - "title": "QQ 官方适配器(WebHook)", - "description": "用于接入 QQ 官方机器人 API", - "properties": { - "adapter": { - "type": "string", - "const": "qqofficial" - }, - "enable": { - "type": "boolean", - "default": false, - "description": "是否启用此适配器", - "layout": { - "comp": "switch", - "props": { - "color": "primary" - } - } - }, - "appid": { - "type": "string", - "default": "", - "description": "申请到的QQ官方机器人的appid" - }, - "secret": { - "type": "string", - "default": "", - "description": "申请到的QQ官方机器人的secret" - }, - "port": { - "type": "integer", - "default": 2284, - "description": "监听的端口" - }, - "token": { - "type": "string", - "default": "", - "description": "申请到的QQ官方机器人的token" - } - } - }, - { - "title": "企业微信适配器", - "description": "用于接入企业微信", - "properties": { - "adapter": { - "type": "string", - "const": "wecom" - }, - "enable": { - "type": "boolean", - "default": false, - "description": "是否启用此适配器", - "layout": { - "comp": "switch", - "props": { - "color": "primary" - } - } - }, - "host": { - "type": "string", - "default": "0.0.0.0", - "description": "监听的IP地址" - }, - "port": { - "type": "integer", - "default": 2290, - "description": "监听的端口" - }, - "corpid": { - "type": "string", - "default": "", - "description": "企业微信的corpid" - }, - "secret": { - "type": "string", - "default": "", - "description": "企业微信的secret" - }, - "token": { - "type": "string", - "default": "", - "description": "企业微信的token" - }, - "EncodingAESKey": { - "type": "string", - "default": "", - "description": "企业微信的EncodingAESKey" - }, - "contacts_secret": { - "type": "string", - "default": "", - "description": "企业微信的contacts_secret" - } - } - }, - { - "title": "飞书适配器", - "description": "用于接入飞书", - "properties": { - "adapter": { - "type": "string", - "const": "lark" - }, - "enable": { - "type": "boolean", - "default": false, - "description": "是否启用此适配器", - "layout": { - "comp": "switch", - "props": { - "color": "primary" - } - } - }, - "app_id": { - "type": "string", - "default": "", - "description": "飞书的app_id" - }, - "app_secret": { - "type": "string", - "default": "", - "description": "飞书的app_secret" - }, - "bot_name": { - "type": "string", - "default": "", - "description": "飞书的bot_name" - }, - "enable-webhook": { - "type": "boolean", - "default": false, - "description": "是否启用webhook模式" - }, - "port": { - "type": "integer", - "description": "设置监听的端口,开启callback event时需要设置", - "default": 2285 - }, - "encrypt-key": { - "type": "string", - "default": "", - "description": "设置加密密钥" - } - } - }, - { - "title": "Discord 适配器", - "description": "用于接入 Discord", - "properties": { - "adapter": { - "type": "string", - "const": "discord" - }, - "enable": { - "type": "boolean", - "default": false, - "description": "是否启用此适配器", - "layout": { - "comp": "switch", - "props": { - "color": "primary" - } - } - }, - "client_id": { - "type": "string", - "default": "", - "description": "Discord 的 client_id" - }, - "token": { - "type": "string", - "default": "", - "description": "Discord 的 token" - } - } - }, - { - "title": "gewechat 适配器", - "description": "用于接入个人微信", - "properties": { - "adapter": { - "type": "string", - "const": "gewechat" - }, - "enable": { - "type": "boolean", - "default": false, - "description": "是否启用此适配器", - "layout": { - "comp": "switch", - "props": { - "color": "primary" - } - } - }, - "gewechat_url": { - "type": "string", - "default": "", - "description": "gewechat 的 url" - }, - "gewechat_file_url": { - "type": "string", - "default": "", - "description": "gewechat 文件下载URL" - }, - "port": { - "type": "integer", - "default": 2286, - "description": "gewechat 的端口" - }, - "callback_url": { - "type": "string", - "default": "", - "description": "回调地址(LangBot主机相对于gewechat服务器的地址)" - }, - "app_id": { - "type": "string", - "default": "", - "description": "gewechat 的 app_id" - }, - "token": { - "type": "string", - "default": "", - "description": "gewechat 的 token" - } - } - }, - { - "title": "微信公众号适配器", - "description": "用于接入微信公众号", - "properties": { - "adapter": { - "type": "string", - "const": "officialaccount" - }, - "enable": { - "type": "boolean", - "default": false, - "description": "是否启用此适配器" - }, - "token": { - "type": "string", - "default": "", - "description": "微信公众号的token" - }, - "EncodingAESKey": { - "type": "string", - "default": "", - "description": "微信公众号的EncodingAESKey" - }, - "AppID": { - "type": "string", - "default": "", - "description": "微信公众号的AppID" - }, - "AppSecret": { - "type": "string", - "default": "", - "description": "微信公众号的AppSecret" - }, - "Mode": { - "type": "string", - "default": "drop", - "description": "对于超过15s的响应的处理模式", - "enum": ["drop", "passive"] - }, - "LoadingMessage": { - "type": "string", - "default": "AI正在思考中,请发送任意内容获取回复。", - "description": "当使用被动模式时,显示给用户的提示信息" - }, - "host": { - "type": "string", - "default": "0.0.0.0", - "description": "监听的IP地址" - }, - "port": { - "type": "integer", - "default": 2287, - "description": "监听的端口" - } - } - }, - { - "title": "钉钉适配器", - "description": "用于接入钉钉", - "properties": { - "adapter": { - "type": "string", - "const": "dingtalk" - }, - "enable": { - "type": "boolean", - "default": false, - "description": "是否启用此适配器", - "layout": { - "comp": "switch", - "props": { - "color": "primary" - } - } - }, - "client_id": { - "type": "string", - "default": "", - "description": "钉钉的client_id" - }, - "client_secret": { - "type": "string", - "default": "", - "description": "钉钉的client_secret" - }, - "robot_code": { - "type": "string", - "default": "", - "description": "钉钉的robot_code" - }, - "robot_name": { - "type": "string", - "default": "", - "description": "钉钉的robot_name" - }, - "markdown_card": { - "type": "boolean", - "default": false, - "description": "是否使用 Markdown 卡片发送消息" - } - } - }, - { - "title": "Telegram 适配器", - "description": "用于接入 Telegram", - "properties": { - "adapter": { - "type": "string", - "const": "telegram" - }, - "enable": { - "type": "boolean", - "default": false, - "description": "是否启用此适配器" - }, - "token": { - "type": "string", - "default": "", - "description": "Telegram 的 token" - }, - "markdown_card": { - "type": "boolean", - "default": false, - "description": "是否使用 Markdown 卡片发送消息" - } - } - }, - { - "title": "Slack 适配器", - "description": "用于接入 Slack", - "properties": { - "adapter": { - "type": "string", - "const": "slack" - }, - "enable": { - "type": "boolean", - "default": false, - "description": "是否启用此适配器" - }, - "bot_token": { - "type": "string", - "default": "", - "description": "Slack 的 bot_token" - }, - "signing_secret": { - "type": "string", - "default": "", - "description": "Slack 的 signing_secret" - }, - "port": { - "type": "integer", - "default": 2288, - "description": "监听的端口" - } - } - } - ] - } - }, - "track-function-calls": { - "type": "boolean", - "default": true, - "layout": { - "comp": "switch", - "props": { - "color": "primary" - } - }, - "title": "跟踪内容函数调用", - "description": "开启之后,在对话中调用的内容函数记录也会发给用户,关闭后(false)仅会发给用户最终结果" - }, - "quote-origin": { - "type": "boolean", - "default": false, - "layout": { - "comp": "switch", - "props": { - "color": "primary" - } - }, - "title": "引用原消息", - "description": "在群内回复时是否引用原消息" - }, - "at-sender": { - "type": "boolean", - "default": false, - "layout": { - "comp": "switch", - "props": { - "color": "primary" - } - }, - "title": "是否 at 原用户", - "description": "在群内回复时是否@发送者" - }, - "force-delay": { - "type": "object", - "default": { - "min": 0, - "max": 0 - }, - "title": "强制消息延迟范围", - "description": "在将响应内容发回给用户前的强制消息随机延迟时间范围,以防风控,单位是秒", - "properties": { - "min": { - "type": "integer", - "default": 0, - "description": "最小值,单位是秒" - }, - "max": { - "type": "integer", - "default": 0, - "description": "最大值,单位是秒" - } - } - }, - "long-text-process": { - "type": "object", - "title": "长消息处理策略", - "properties": { - "threshold": { - "type": "integer", - "default": 256, - "title": "长消息处理阈值", - "description": "当消息长度超过此阈值时,将启用长消息处理策略" - }, - "strategy": { - "type": "string", - "default": "forward", - "title": "长消息处理策略", - "description": "长消息处理策略,目前支持forward(转发消息组件)和image(文字转图片)。aiocqhttp 和 qq-botpy 不支持 forward 策略" - }, - "font-path": { - "type": "string", - "description": "image的渲染字体。未设置时,如果在windows下,会尝试寻找系统的微软雅黑字体,若找不到,则转为forward策略。未设置时,若不是windows系统,则直接转为forward策略", - "default": "" - } - } - }, - "hide-exception-info": { - "type": "boolean", - "default": true, - "layout": { - "comp": "switch", - "props": { - "color": "primary" - } - }, - "title": "向用户隐藏AI接口的异常信息", - "description": "是否向用户隐藏AI的异常信息,如果为true,当请求AI接口出现异常时,会返回一个错误的提示给用户。而把报错详情输出在控制台。" - } - } -} \ No newline at end of file diff --git a/templates/schema/provider.json b/templates/schema/provider.json deleted file mode 100644 index af36a19b..00000000 --- a/templates/schema/provider.json +++ /dev/null @@ -1,606 +0,0 @@ -{ - "type": "object", - "layout": "expansion-panels", - "properties": { - "enable-chat": { - "type": "boolean", - "default": true, - "title": "启用聊天功能", - "description": "是否启用 AI 聊天功能" - }, - "enable-vision": { - "type": "boolean", - "default": true, - "title": "启用视觉功能", - "description": "是否开启AI视觉功能。需要使用的模型同时支持视觉功能,详情见元数据板块" - }, - "keys": { - "type": "object", - "title": "模型接口密钥", - "description": "以字典的形式设置若干个密钥组,每个密钥组的键为密钥组名称,值为密钥列表。模型与密钥组的对应关系,请查看元数据板块", - "properties": { - "openai": { - "type": "array", - "title": "OpenAI API 密钥", - "items": { - "type": "string" - }, - "default": [] - }, - "anthropic": { - "type": "array", - "title": "Anthropic API 密钥", - "items": { - "type": "string" - }, - "default": [] - }, - "moonshot": { - "type": "array", - "title": "Moonshot API 密钥", - "items": { - "type": "string" - }, - "default": [] - }, - "deepseek": { - "type": "array", - "title": "DeepSeek API 密钥", - "items": { - "type": "string" - }, - "default": [] - }, - "gitee": { - "type": "array", - "title": "Gitee AI API 密钥", - "items": { - "type": "string" - }, - "default": [] - }, - "xai": { - "type": "array", - "title": "xAI API 密钥", - "items": { - "type": "string" - }, - "default": [] - }, - "zhipuai": { - "type": "array", - "title": "智谱AI API 密钥", - "items": { - "type": "string" - }, - "default": [] - }, - "siliconflow": { - "type": "array", - "title": "SiliconFlow API 密钥", - "items": { - "type": "string" - }, - "default": [] - }, - "bailian": { - "type": "array", - "title": "阿里云百炼大模型平台 API 密钥", - "items": { - "type": "string" - }, - "default": [] - }, - "volcark": { - "type": "array", - "title": "火山引擎大模型平台 API 密钥", - "items": { - "type": "string" - }, - "default": [] - } - } - }, - "requester": { - "type": "object", - "title": "大模型请求器", - "description": "以字典的形式设置若干个请求器,每个请求器的键为请求器名称,值为请求器配置。模型与请求器的对应关系,请查看元数据板块。实现请求器的方式,请查看插件编写教程", - "properties": { - "openai-chat-completions": { - "type": "object", - "title": "OpenAI API 请求配置", - "description": "仅可编辑 URL 和 超时时间,额外请求参数不支持可视化编辑,请到编辑器编辑", - "properties": { - "base-url": { - "type": "string", - "title": "API URL" - }, - "args": { - "type": "object", - "default": {} - }, - "timeout": { - "type": "number", - "title": "API 请求超时时间", - "default": 120 - } - } - }, - "anthropic-messages": { - "type": "object", - "title": "Anthropic API 请求配置", - "description": "仅可编辑 URL 和 超时时间,额外请求参数不支持可视化编辑,请到编辑器编辑", - "properties": { - "base-url": { - "type": "string", - "title": "API URL" - }, - "args": { - "type": "object", - "default": {} - }, - "timeout": { - "type": "number", - "title": "API 请求超时时间", - "default": 120 - } - } - }, - "moonshot-chat-completions": { - "type": "object", - "title": "Moonshot API 请求配置", - "description": "仅可编辑 URL 和 超时时间,额外请求参数不支持可视化编辑,请到编辑器编辑", - "properties": { - "base-url": { - "type": "string", - "title": "API URL" - }, - "args": { - "type": "object", - "default": {} - }, - "timeout": { - "type": "number", - "title": "API 请求超时时间", - "default": 120 - } - } - }, - "deepseek-chat-completions": { - "type": "object", - "title": "DeepSeek API 请求配置", - "description": "仅可编辑 URL 和 超时时间,额外请求参数不支持可视化编辑,请到编辑器编辑", - "properties": { - "base-url": { - "type": "string", - "title": "API URL" - }, - "args": { - "type": "object", - "default": {} - }, - "timeout": { - "type": "number", - "title": "API 请求超时时间", - "default": 120 - } - } - }, - "ollama-chat": { - "type": "object", - "title": "Ollama API 请求配置", - "description": "仅可编辑 URL 和 超时时间,额外请求参数不支持可视化编辑,请到编辑器编辑", - "properties": { - "base-url": { - "type": "string", - "title": "API URL" - }, - "args": { - "type": "object", - "default": {} - }, - "timeout": { - "type": "number", - "title": "API 请求超时时间", - "default": 600 - } - } - }, - "gitee-ai-chat-completions": { - "type": "object", - "title": "Gitee AI API 请求配置", - "description": "仅可编辑 URL 和 超时时间,额外请求参数不支持可视化编辑,请到编辑器编辑", - "properties": { - "base-url": { - "type": "string", - "title": "API URL" - }, - "args": { - "type": "object", - "default": {} - }, - "timeout": { - "type": "number", - "title": "API 请求超时时间", - "default": 120 - } - } - }, - "xai-chat-completions": { - "type": "object", - "title": "xAI API 请求配置", - "description": "仅可编辑 URL 和 超时时间,额外请求参数不支持可视化编辑,请到编辑器编辑", - "properties": { - "base-url": { - "type": "string", - "title": "API URL" - }, - "args": { - "type": "object", - "default": {} - }, - "timeout": { - "type": "number", - "title": "API 请求超时时间", - "default": 120 - } - } - }, - "zhipuai-chat-completions": { - "type": "object", - "title": "智谱AI API 请求配置", - "description": "仅可编辑 URL 和 超时时间,额外请求参数不支持可视化编辑,请到编辑器编辑", - "properties": { - "base-url": { - "type": "string", - "title": "API URL" - }, - "args": { - "type": "object", - "default": {} - }, - "timeout": { - "type": "number", - "default": 120 - } - } - }, - "lmstudio-chat-completions": { - "type": "object", - "title": "LMStudio API 请求配置", - "description": "仅可编辑 URL 和 超时时间,额外请求参数不支持可视化编辑,请到编辑器编辑", - "properties": { - "base-url": { - "type": "string", - "title": "API URL" - }, - "args": { - "type": "object", - "default": {} - }, - "timeout": { - "type": "number", - "title": "API 请求超时时间", - "default": 120 - } - } - }, - "siliconflow-chat-completions": { - "type": "object", - "title": "SiliconFlow API 请求配置", - "description": "仅可编辑 URL 和 超时时间,额外请求参数不支持可视化编辑,请到编辑器编辑", - "properties": { - "base-url": { - "type": "string", - "title": "API URL" - }, - "args": { - "type": "object", - "default": {} - }, - "timeout": { - "type": "number", - "title": "API 请求超时时间", - "default": 120 - } - } - }, - "bailian-chat-completions": { - "type": "object", - "title": "阿里云百炼大模型平台 API 请求配置", - "description": "仅可编辑 URL 和 超时时间,额外请求参数不支持可视化编辑,请到编辑器编辑", - "properties": { - "base-url": { - "type": "string", - "title": "API URL" - }, - "args": { - "type": "object", - "default": {} - }, - "timeout": { - "type": "number", - "title": "API 请求超时时间", - "default": 120 - } - } - }, - "volcark-chat-completions": { - "type": "object", - "title": "火山方舟大模型平台 API 请求配置", - "description": "仅可编辑 URL 和 超时时间,额外请求参数不支持可视化编辑,请到编辑器编辑", - "properties": { - "base-url": { - "type": "string", - "title": "API URL" - }, - "args": { - "type": "object", - "default": {} - }, - "timeout": { - "type": "number", - "title": "API 请求超时时间", - "default": 120 - } - } - } - } - }, - "model": { - "type": "string", - "title": "所使用的模型名称", - "description": "设置要使用的模型名称。通常来说直接填写模型名称即可,但如果要使用原生接口不是 ChatCompletion 但以 ChatCompletion 接口格式接入的模型,请在模型名称前方加一个 OneAPI/ 前缀以进行区分。 简单来说可以认为是:现阶段非 OpenAI 的模型接入都需要在模型名称前方加一个 OneAPI/ 前缀。\n\n例如:\n\n1. 通过 OneAPI 等中转服务接入了 OpenAI 的 gpt-4 模型,由于 gpt-4 也是使用 ChatCompletion 接口格式进行请求,则可以直接填入 gpt-4;\n2. 通过 OneAPI 等中转服务接入了 Google 的 gemini-pro 模型,由于 gemini-pro 原生接口格式并非 ChatCompletion,因此需要填入 OneAPI/gemini-pro。\n具体支持的模型列表和各个模型对应的请求器和密钥组,请查看元数据板块 llm-models.json " - }, - "prompt-mode": { - "type": "string", - "title": "情景预设(人格)模式", - "description": "值为normal(单预设模式)和full-scenario(完整历史对话模式);normal模式时,使用下方设置的情景预设,也支持读取data/prompts目录下的文件内容作为单个 System Prompt,文件名即为prompt的名称;full-scenario模式时,读取 data/scenario/ 下的完整历史对话作为情景预设", - "enum": ["normal", "full-scenario"], - "default": "normal" - }, - "prompt": { - "type": "object", - "title": "情景预设(人格)", - "description": "设置情景预设(人格)。值为空字符串时,将不使用情景预设(人格)。normal模式时,使用下方设置的情景预设,也支持读取data/prompts目录下的文件内容作为单个 System Prompt,文件名即为prompt的名称;full-scenario模式时,读取 data/scenario/ 下的完整历史对话作为情景预设", - "properties": { - "default": { - "type": "string", - "title": "默认情景预设", - "description": "设置默认情景预设。值为空字符串时,将不使用情景预设(人格)", - "default": "You are a helpful assistant." - } - }, - "patternProperties": { - "^.*$": { - "type": "string", - "title": "情景预设", - "description": "设置情景预设。值为空字符串时,将不使用情景预设(人格)", - "default": "" - } - }, - "required": ["default"] - }, - "runner": { - "type": "string", - "title": "请求运行器", - "description": "设置请求运行器。值为local-agent时,使用内置默认运行器;支持插件扩展", - "default": "local-agent" - }, - "dify-service-api": { - "type": "object", - "title": "Dify Service API 配置", - "properties": { - "base-url": { - "type": "string", - "title": "API URL", - "description": "Dify Service API 的 基础URL,可以在 Dify 应用 API 页面查看", - "default": "https://api.dify.ai/v1" - }, - "app-type": { - "type": "string", - "title": "应用类型", - "description": "支持 chat 和 workflow,chat:聊天助手(含高级编排)和 Agent;workflow:工作流;请填写下方对应的应用类型 API 参数", - "enum": ["chat", "workflow", "agent"], - "default": "chat" - }, - "options": { - "type": "object", - "title": "Dify Service API 配置选项", - "properties": { - "convert-thinking-tips": { - "type": "string", - "title": "转换思考提示", - "description": "设置转换思考提示。值为 original 时,不转换思考提示;值为 plain 时,将思考提示转换为类似 DeepSeek 官方的...格式;值为 remove 时,删除思考提示", - "enum": ["original", "plain", "remove"], - "default": "plain" - } - } - }, - - "chat": { - "type": "object", - "title": "聊天助手 API 参数", - "properties": { - "api-key": { - "type": "string", - "title": "API 密钥" - }, - "timeout": { - "type": "number", - "title":"API 请求超时时间" - } - } - }, - "agent": { - "type": "object", - "title": "Agent API 参数", - "properties": { - "api-key": { - "type": "string", - "title": "API 密钥" - }, - "timeout": { - "type": "number", - "title":"API 请求超时时间" - } - } - }, - "workflow": { - "type": "object", - "title": "工作流 API 参数", - "properties": { - "api-key": { - "type": "string", - "title": "API 密钥" - }, - "output-key": { - "type": "string", - "title": "工作流输出键", - "description": "设置工作流输出键,用于从 Dify Workflow 结束节点返回的 JSON 数据中提取输出内容", - "default": "summary" - }, - "timeout": { - "type": "number", - "title": "API 请求超时时间" - } - } - } - } - }, - "dashscope-app-api": { - "type": "object", - "title": "阿里百炼平台自建应用 API 配置", - "properties": { - "app-type": { - "type": "string", - "title": "应用类型", - "description": "支持 workflow 和 agent,workflow:智能体编排;agent:普通智能体;请填写下方对应的应用类型 API 参数", - "enum": ["workflow", "agent"], - "default": "agent" - }, - "api-key": { - "type": "string", - "title": "API 密钥" - }, - "agent": { - "type": "object", - "title": "Agent API 参数", - "properties": { - "app-id": { - "type": "string", - "title": "应用 ID" - }, - "references_quote": { - "type": "string", - "title": "参考资料引用", - "description": "设置参考资料引用,用于从 Dashscope App API 结束节点返回的 JSON 数据中提取引用内容", - "default": "参考资料来自:" - } - } - }, - "workflow": { - "type": "object", - "title": "工作流 API 参数", - "properties": { - "app-id": { - "type": "string", - "title": "应用 ID" - }, - "references_quote": { - "type": "string", - "title": "参考资料引用", - "default": "参考资料来自:" - }, - "biz_params": { - "type": "object", - "title": "传入参数", - "default": {} - } - } - } - } - }, - "mcp": { - "type": "object", - "title": "MCP 配置", - "properties": { - "servers": { - "type": "array", - "title": "MCP 服务器配置", - "default": [], - "items": { - "type": "object", - "oneOf": [ - { - "title": "Stdio 模式服务器", - "properties": { - "mode": { - "type": "string", - "title": "模式", - "const": "stdio" - }, - "enable": { - "type": "boolean", - "title": "启用" - }, - "name": { - "type": "string", - "title": "名称" - }, - "command": { - "type": "string", - "title": "启动命令" - }, - "args": { - "type": "array", - "title": "启动参数", - "items": { - "type": "string" - }, - "default": [] - }, - "env": { - "type": "object", - "default": {} - } - } - }, - { - "title": "SSE 模式服务器", - "properties": { - "mode": { - "type": "string", - "title": "模式", - "const": "sse" - }, - "enable": { - "type": "boolean", - "title": "启用" - }, - "name": { - "type": "string", - "title": "名称" - }, - "url": { - "type": "string", - "title": "URL" - }, - "headers": { - "type": "object", - "default": {} - }, - "timeout": { - "type": "number", - "title": "请求超时时间", - "default": 10 - } - } - } - ] - } - } - } - } - } -} \ No newline at end of file diff --git a/templates/schema/system.json b/templates/schema/system.json deleted file mode 100644 index 85f46e71..00000000 --- a/templates/schema/system.json +++ /dev/null @@ -1,126 +0,0 @@ -{ - "type": "object", - "layout": "expansion-panels", - "properties": { - "admin-sessions": { - "type": "array", - "title": "管理员会话", - "description": "设置管理员会话,格式为 {type}_{id},type 为 \"group\" 或 \"person\",如:group_123456 或 person_123456", - "items": { - "type": "string", - "format": "regex", - "pattern": "^(person|group)_.*$" - }, - "default": [] - }, - "network-proxies": { - "type": "object", - "title": "网络代理", - "description": "正向代理,http和https都要填,例如:http://127.0.0.1:7890 https://127.0.0.1:7890 。不使用代理请留空。正向代理也可以用环境变量设置:http_proxy 和 https_proxy", - "properties": { - "http": { - "type": "string" - }, - "https": { - "type": "string" - } - } - }, - "report-usage": { - "type": "boolean", - "title": "上报遥测数据", - "description": "遥测数据用于统计和分析项目使用情况,不包含任何隐私信息,不建议禁用", - "layout": { - "comp": "switch", - "props": { - "color": "primary" - } - } - }, - "logging-level": { - "type": "string", - "title": "日志等级", - "description": "目前无效,启用调试模式请设置环境变量:export DEBUG=true" - }, - "session-concurrency": { - "type": "object", - "title": "会话消息处理并发数", - "description": "粒度是单个会话,所有会话使用默认并发数,若需指定特定会话的并发数,请输入 会话名称(格式为 {type}_{id},示例:group_123456 或 person_123456) 并添加,以设置特定会话的并发数", - "properties": { - "default": { - "type": "integer" - } - }, - "patternProperties": { - "^(person|group)_.*$": { - "type": "integer" - } - } - }, - "pipeline-concurrency": { - "type": "integer", - "title": "流水线消息处理并发数", - "description": "粒度是整个程序,目前使用 FCFS 算法调度各个请求" - }, - "qcg-center-url": { - "type": "string", - "title": "遥测服务器地址", - "description": "运行期间推送遥测数据的目标地址,默认为官方地址,若您自己部署了 https://github.com/RockChinQ/qcg-center,可以改为你的地址。" - }, - "help-message": { - "type": "string", - "title": "帮助消息", - "description": "用户发送 !help 命令时的输出", - "layout": "textarea" - }, - "http-api": { - "type": "object", - "title": "HTTP 接口", - "properties": { - "enable": { - "type": "boolean", - "layout": { - "comp": "switch", - "props": { - "color": "primary" - } - }, - "title": "是否启用" - }, - "host": { - "type": "string" - }, - "port": { - "type": "integer" - }, - "jwt-expire": { - "type": "integer", - "title": "JWT 过期时间", - "description": "单位:秒" - } - } - }, - "persistence": { - "type": "object", - "title": "持久化设置", - "properties": { - "sqlite": { - "type": "object", - "title": "sqlite", - "properties": { - "path": { - "type": "string" - } - } - }, - "use": { - "type": "string", - "title": "所使用的数据库", - "enum": [ - "sqlite" - ] - } - } - } - } -} \ No newline at end of file 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/.lintstagedrc.json b/web/.lintstagedrc.json new file mode 100644 index 00000000..ab33c2b3 --- /dev/null +++ b/web/.lintstagedrc.json @@ -0,0 +1,4 @@ +{ + "*.{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..b37ee514 --- /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" +} 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..8894e743 100644 --- a/web/package.json +++ b/web/package.json @@ -1,46 +1,63 @@ { "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", + "lint-staged": "lint-staged" + }, + "lint-staged": { + "*.{js,jsx,ts,tsx}": [ + "next lint --fix", + "prettier --write" + ] }, "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", + "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..95d885ee --- /dev/null +++ b/web/postcss.config.mjs @@ -0,0 +1,6 @@ +const config = { + plugins: { + '@tailwindcss/postcss': {}, + }, +}; +export default config; 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 @@ - - - - - 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..079437e8 --- /dev/null +++ b/web/src/app/global.css @@ -0,0 +1,151 @@ +: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; + } +} 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..0e3d18e5 --- /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; +} 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 ( +
+
+ icon + +
+
+
{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..30a6cef2 --- /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; +} 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..2ab46bdb --- /dev/null +++ b/web/src/app/home/bots/components/bot-form/BotForm.tsx @@ -0,0 +1,554 @@ +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, + onNewBotCreated, +}: { + initBotId?: string; + onFormSubmit: (value: z.infer) => void; + onFormCancel: () => void; + onBotDeleted: () => void; + onNewBotCreated: (botId: string) => 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(() => { + setBotFormValues(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + function setBotFormValues() { + 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(); + } + }); + } + + 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('create bot success', res); + toast.success('创建成功 请启用或修改绑定流水线'); + initBotId = res.uuid; + + setBotFormValues(); + + onNewBotCreated(res.uuid); + }) + .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 ( +
+ + + + 删除确认 + + 你确定要删除这个机器人吗? + + + + + + + +
+ +
+ {/* 是否启用 & 绑定流水线 仅在编辑模式 */} + {initBotId && ( +
+ ( + + 是否启用 + + + + + )} + /> + + ( + + 绑定流水线 + + + + + )} + /> +
+ )} + + ( + + + 机器人名称* + + + + + + + )} + /> + ( + + + 机器人描述* + + + + + + + )} + /> + + ( + + + 平台/适配器选择* + + +
+ +
+
+ +
+ )} + /> + + {form.watch('adapter') && ( +
+ adapter icon +
+
+ { + adapterNameList.find( + (item) => item.value === form.watch('adapter'), + )?.label + } +
+
+ {adapterDescriptionList[form.watch('adapter')]} +
+
+
+ )} + + {showDynamicForm && dynamicFormConfigList.length > 0 && ( +
+
适配器配置
+ { + form.setValue('adapter_config', values); + }} + /> +
+ )} +
+ +
+
+ {!initBotId && ( + + )} + {initBotId && ( + <> + + + + )} + +
+
+
+ +
+ ); +} 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..34d0b3cf --- /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 [nowSelectedBotUUID, setNowSelectedBotUUID] = 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); + setNowSelectedBotUUID(''); + setModalOpen(true); + } + + function selectBot(botUUID: string) { + setNowSelectedBotUUID(botUUID); + setIsEditForm(true); + setModalOpen(true); + } + + return ( +
+ + + + + {isEditForm ? '编辑机器人' : '创建机器人'} + + +
+ { + getBotList(); + setModalOpen(false); + }} + onFormCancel={() => setModalOpen(false)} + onBotDeleted={() => { + getBotList(); + setModalOpen(false); + }} + onNewBotCreated={(botId) => { + console.log('new bot created', botId); + getBotList(); + selectBot(botId); + }} + /> +
+
+
+ + {/* 注意:其余的返回内容需要保持在Spin组件外部 */} +
+ + {botList.map((cardVO) => { + return ( +
{ + selectBot(cardVO.id); + }} + > + +
+ ); + })} +
+
+ ); +} 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); + }} + /> + +
+ ))} + +
+ ); + + case DynamicFormItemType.SELECT: + return ( + + ); + + case DynamicFormItemType.LLM_MODEL_SELECTOR: + return ( + + ); + + 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], + content: e.target.value, + }; + field.onChange(newValue); + }} + /> + {/* 删除按钮,第一轮不显示 */} + {index !== 0 && ( + + )} +
+ ), + )} + +
+ ); + + 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..3504d7a3 --- /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; +} 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..c1ac4ff8 --- /dev/null +++ b/web/src/app/home/components/home-sidebar/HomeSidebar.module.css @@ -0,0 +1,105 @@ +.sidebarContainer { + box-sizing: border-box; + width: 11rem; + 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: 9rem; + height: 3rem; + margin: 0.8rem 0; + padding-left: 1.6rem; + 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; +} 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..d9752c3a --- /dev/null +++ b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx @@ -0,0 +1,160 @@ +'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'; +import { httpClient } from '@/app/infra/http/HttpClient'; + +// 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-icon + {/* 文字 */} +
+
LangBot
+
+ {httpClient.systemInfo?.version} +
+
+
+ {/* 菜单列表,后期可升级成配置驱动 */} +
+ {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..0e98dcc0 --- /dev/null +++ b/web/src/app/home/components/home-sidebar/HomeSidebarChild.tsx @@ -0,0 +1,52 @@ +import styles from './HomeSidebar.module.css'; + +export interface ISidebarChildVO { + id: string; + icon: React.ReactNode; + name: string; + route: string; + description: string; + helpLink: string; +} + +export class SidebarChildVO { + id: string; + icon: React.ReactNode; + name: string; + route: string; + description: string; + helpLink: string; + + constructor(props: ISidebarChildVO) { + this.id = props.id; + this.icon = props.icon; + this.name = props.name; + this.route = props.route; + this.description = props.description; + this.helpLink = props.helpLink; + } +} + +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..fb343f55 --- /dev/null +++ b/web/src/app/home/components/home-sidebar/sidbarConfigList.tsx @@ -0,0 +1,73 @@ +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', + description: '创建和管理机器人,这是 LangBot 与各个平台连接的入口', + helpLink: 'https://docs.langbot.app/zh/deploy/platforms/readme.html', + }), + new SidebarChildVO({ + id: 'models', + name: '模型配置', + icon: ( + + + + ), + route: '/home/models', + description: '配置和管理可在流水线中使用的模型', + helpLink: 'https://docs.langbot.app/zh/deploy/models/readme.html', + }), + new SidebarChildVO({ + id: 'pipelines', + name: '流水线', + icon: ( + + + + ), + route: '/home/pipelines', + description: '流水线定义了对消息事件的处理流程,用于绑定到机器人', + helpLink: 'https://docs.langbot.app/zh/deploy/pipelines/readme.html', + }), + new SidebarChildVO({ + id: 'plugins', + name: '插件管理', + icon: ( + + + + ), + route: '/home/plugins', + description: '安装和配置用于扩展 LangBot 功能的插件', + helpLink: 'https://docs.langbot.app/zh/plugin/plugin-intro.html', + }), +]; 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..99e7339f --- /dev/null +++ b/web/src/app/home/components/home-titlebar/HomeTitleBar.tsx @@ -0,0 +1,32 @@ +import styles from './HomeTittleBar.module.css'; + +export default function HomeTitleBar({ + title, + subtitle, + helpLink, +}: { + title: string; + subtitle: string; + helpLink: string; +}) { + return ( +
+
{title}
+
+ {subtitle} + + + + + + + +
+
+ ); +} 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..bc740231 --- /dev/null +++ b/web/src/app/home/components/home-titlebar/HomeTittleBar.module.css @@ -0,0 +1,32 @@ +.titleBarContainer { + width: 100%; + padding-top: 1rem; + height: 4rem; + opacity: 1; + font-size: 20px; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; +} + +.titleText { + margin-left: 3.2rem; + font-size: 1.4rem; + font-weight: 500; + color: #585858; +} + +.subtitleText { + margin-left: 3.2rem; + font-size: 0.8rem; + color: #808080; + display: flex; + align-items: center; +} + +.helpLink { + margin-left: 0.2rem; + font-size: 0.8rem; + color: #8b8b8b; +} diff --git a/web/src/app/home/layout.module.css b/web/src/app/home/layout.module.css new file mode 100644 index 00000000..78a11beb --- /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; +} diff --git a/web/src/app/home/layout.tsx b/web/src/app/home/layout.tsx new file mode 100644 index 00000000..6435bddf --- /dev/null +++ b/web/src/app/home/layout.tsx @@ -0,0 +1,36 @@ +'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 [subtitle, setSubtitle] = useState(''); + const [helpLink, setHelpLink] = useState(''); + const onSelectedChangeAction = (child: SidebarChildVO) => { + setTitle(child.name); + setSubtitle(child.description); + setHelpLink(child.helpLink); + }; + + 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..ce6c689a --- /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..f33fea99 --- /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; +} 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 ( +
+
+ icon + +
+ {/* 名称 */} +
+ {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..7c52eb5a --- /dev/null +++ b/web/src/app/home/models/component/llm-form/LLMForm.tsx @@ -0,0 +1,559 @@ +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: () => 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) { + 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 llmModel: LLMModel = { + uuid: editMode ? initLLMId || '' : 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, + }; + + if (editMode) { + onSaveEdit(llmModel).then(() => { + form.reset(); + }); + } else { + onCreateLLM(llmModel).then(() => { + form.reset(); + }); + } + } + + async function onCreateLLM(llmModel: LLMModel) { + try { + await httpClient.createProviderLLMModel(llmModel); + onFormSubmit(); + toast.success('创建成功'); + } catch (err) { + toast.error('创建失败:' + (err as Error).message); + } + } + + async function onSaveEdit(llmModel: LLMModel) { + try { + await httpClient.updateProviderLLMModel(initLLMId || '', llmModel); + onFormSubmit(); + toast.success('保存成功'); + } catch (err) { + toast.error('保存失败:' + (err as Error).message); + } + } + + function deleteModel() { + if (initLLMId) { + httpClient + .deleteProviderLLMModel(initLLMId) + .then(() => { + onLLMDeleted(); + toast.success('删除成功'); + }) + .catch((err) => { + toast.error('删除失败:' + err.message); + }); + } + } + + return ( +
+ + + + 删除确认 + + 你确定要删除这个模型吗? + + + + + + + +
+ +
+ ( + + + 模型名称* + + + + + + + 请填写供应商向您提供的模型名称 + + + )} + /> + + ( + + + 模型供应商* + + + + + + + )} + /> + + ( + + + 请求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, 'value', e.target.value) + } + /> + +
+ ))} + +
+ + 将在请求时附加到请求体中,如 max_tokens, temperature, top_p 等 + + +
+
+ + {editMode && ( + + )} + + + + + +
+ +
+ ); +} 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..43e14b4c --- /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..6b73a9db --- /dev/null +++ b/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx @@ -0,0 +1,450 @@ +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 [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 ( +
+ + + + 删除确认 + + + 你确定要删除这个流水线吗?已绑定此流水线的机器人将无法使用。 + + + + + + + + +
+ + + + {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 && ( + + )} + + + +
+
+
+ +
+ ); +} + +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..a0e86c89 --- /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..a5ef835c --- /dev/null +++ b/web/src/app/home/pipelines/pipelineConfig.module.css @@ -0,0 +1,15 @@ +.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..4be10db3 --- /dev/null +++ b/web/src/app/home/plugins/page.tsx @@ -0,0 +1,171 @@ +'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; + console.log('taskId:', taskId); + + // 每秒拉取一次任务状态 + 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 ( +
+ +
+ + + 已安装 + + + 插件市场 + + + +
+ +
+
+ + + + + { + 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 && ( + <> + + + + )} + {pluginInstallStatus === PluginInstallStatus.ERROR && ( + + )} + +
+
+
+ ); +} 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..0918b7db --- /dev/null +++ b/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx @@ -0,0 +1,124 @@ +'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 ( + <> + {pluginList.length === 0 ? ( +
+ + + +
暂未安装任何插件
+
+ ) : ( +
+ + + + 插件配置 + +
+ {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 && ( + + )} + {pluginRemoveStatus === PluginRemoveStatus.WAIT_INPUT && ( + + )} + {pluginRemoveStatus === PluginRemoveStatus.REMOVING && ( + + )} + {pluginRemoveStatus === PluginRemoveStatus.ERROR && ( + + )} + +
+
+ +
+
{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 && ( +
该插件没有配置项。
+ )} +
+ +
+
+ + + + +
+
+
+ ); +} 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')} + > + + + +
+
+
+
+
+ ); +} 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..54ede1c6 --- /dev/null +++ b/web/src/app/home/plugins/plugins.module.css @@ -0,0 +1,20 @@ +.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; +} 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..2c7242e6 --- /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; +} 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..2c328c42 --- /dev/null +++ b/web/src/app/infra/http/HttpClient.ts @@ -0,0 +1,452 @@ +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 + public systemInfo: ApiRespSystemInfo | 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(); + + if (this.systemInfo === null) { + this.getSystemInfo().then((res) => { + this.systemInfo = res; + }); + } + } + + // 兜底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}`); + } + + public updateProviderLLMModel( + uuid: string, + model: LLMModel, + ): Promise { + return this.put(`/api/v1/provider/models/llm/${uuid}`, model); + } + + // ============ 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<{ uuid: string }> { + 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 + + 欢迎回到 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 + 是否正确,或者返回首页。 +

+
+ + {/* 按钮组 */} +
+ + +
+ + {/* 帮助文档链接 */} +
+

+ 查看 + + 帮助文档 + +

+
+
+
+
+ ); +} 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 👋 + + + 这是您首次启动 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 @@ - - - - - \ 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 @@ - - - - - \ 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 @@ - - - - - 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 @@ - - - - - \ 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 @@ - - - - - \ 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 @@ - - - - - \ 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 @@ - - - - - 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 @@ - - - - - \ 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - \ 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