From 74183ff07941be4b2cf27af3402b6c9fd62f32fa Mon Sep 17 00:00:00 2001 From: foxplaying <166147707+foxplaying@users.noreply.github.com> Date: Sun, 21 Sep 2025 20:56:35 +0800 Subject: [PATCH 01/26] =?UTF-8?q?=E5=B0=9D=E8=AF=95=E4=BC=A0=E5=85=A5?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89base=5Furl(=E5=AE=9E=E9=AA=8C?= =?UTF-8?q?=E6=80=A7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/llm_models/model_client/gemini_client.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/llm_models/model_client/gemini_client.py b/src/llm_models/model_client/gemini_client.py index e58466d1..237d0724 100644 --- a/src/llm_models/model_client/gemini_client.py +++ b/src/llm_models/model_client/gemini_client.py @@ -4,6 +4,7 @@ import base64 from typing import Callable, AsyncIterator, Optional, Coroutine, Any, List from google import genai +from google.genai import _base_url as genai_base_url from google.genai.types import ( Content, Part, @@ -349,6 +350,11 @@ class GeminiClient(BaseClient): api_key=api_provider.api_key, ) # 这里和openai不一样,gemini会自己决定自己是否需要retry + # 尝试传入自定义base_url(实验性,必须为Gemini格式) + if hasattr(api_provider, "base_url") and api_provider.base_url: + self.base_url = api_provider.base_url + self.client._api_client._base_url = self.base_url + @staticmethod def clamp_thinking_budget(tb: int, model_id: str) -> int: """ From be3b80f90e09d1ae8cbc33b36adcbe072cc97544 Mon Sep 17 00:00:00 2001 From: foxplaying <166147707+foxplaying@users.noreply.github.com> Date: Sun, 21 Sep 2025 21:08:07 +0800 Subject: [PATCH 02/26] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/llm_models/model_client/gemini_client.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/llm_models/model_client/gemini_client.py b/src/llm_models/model_client/gemini_client.py index 237d0724..4ff7d9d6 100644 --- a/src/llm_models/model_client/gemini_client.py +++ b/src/llm_models/model_client/gemini_client.py @@ -4,7 +4,6 @@ import base64 from typing import Callable, AsyncIterator, Optional, Coroutine, Any, List from google import genai -from google.genai import _base_url as genai_base_url from google.genai.types import ( Content, Part, @@ -352,8 +351,7 @@ class GeminiClient(BaseClient): # 尝试传入自定义base_url(实验性,必须为Gemini格式) if hasattr(api_provider, "base_url") and api_provider.base_url: - self.base_url = api_provider.base_url - self.client._api_client._base_url = self.base_url + self._api_client.http_options.base_url = api_provider.base_url @staticmethod def clamp_thinking_budget(tb: int, model_id: str) -> int: From 036be7a99b6fb32e7ee2bc3c2953b96eee7619c0 Mon Sep 17 00:00:00 2001 From: foxplaying <166147707+foxplaying@users.noreply.github.com> Date: Sun, 21 Sep 2025 21:36:02 +0800 Subject: [PATCH 03/26] Update gemini_client.py --- src/llm_models/model_client/gemini_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/llm_models/model_client/gemini_client.py b/src/llm_models/model_client/gemini_client.py index 4ff7d9d6..d80924ba 100644 --- a/src/llm_models/model_client/gemini_client.py +++ b/src/llm_models/model_client/gemini_client.py @@ -351,7 +351,8 @@ class GeminiClient(BaseClient): # 尝试传入自定义base_url(实验性,必须为Gemini格式) if hasattr(api_provider, "base_url") and api_provider.base_url: - self._api_client.http_options.base_url = api_provider.base_url + self.client._api_client._base_url = api_provider.base_url + self._api_client = self.client._api_client @staticmethod def clamp_thinking_budget(tb: int, model_id: str) -> int: From ce8b80a721bd8d416cf773ed8e1d1ea477a44830 Mon Sep 17 00:00:00 2001 From: foxplaying <166147707+foxplaying@users.noreply.github.com> Date: Sun, 21 Sep 2025 21:54:37 +0800 Subject: [PATCH 04/26] Update gemini_client.py --- src/llm_models/model_client/gemini_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/llm_models/model_client/gemini_client.py b/src/llm_models/model_client/gemini_client.py index d80924ba..7f747ad2 100644 --- a/src/llm_models/model_client/gemini_client.py +++ b/src/llm_models/model_client/gemini_client.py @@ -351,7 +351,7 @@ class GeminiClient(BaseClient): # 尝试传入自定义base_url(实验性,必须为Gemini格式) if hasattr(api_provider, "base_url") and api_provider.base_url: - self.client._api_client._base_url = api_provider.base_url + self.client._api_client._http_options.base_url = api_provider.base_url self._api_client = self.client._api_client @staticmethod From 978c627d7d059e0417fa0db6a0e3427e06c9245a Mon Sep 17 00:00:00 2001 From: foxplaying <166147707+foxplaying@users.noreply.github.com> Date: Sun, 21 Sep 2025 22:05:41 +0800 Subject: [PATCH 05/26] =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/llm_models/model_client/gemini_client.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/llm_models/model_client/gemini_client.py b/src/llm_models/model_client/gemini_client.py index 7f747ad2..67c7475e 100644 --- a/src/llm_models/model_client/gemini_client.py +++ b/src/llm_models/model_client/gemini_client.py @@ -351,7 +351,14 @@ class GeminiClient(BaseClient): # 尝试传入自定义base_url(实验性,必须为Gemini格式) if hasattr(api_provider, "base_url") and api_provider.base_url: - self.client._api_client._http_options.base_url = api_provider.base_url + base_url = api_provider.base_url.rstrip("/") # 去掉末尾 / + self.client._api_client._http_options.base_url = base_url + + # 如果 base_url 已经带了 /v1 或 /v1beta,就清掉 SDK 的 api_version + if base_url.endswith("/v1") or base_url.endswith("/v1beta"): + self.client._api_client._http_options.api_version = None + + # 让 GeminiClient 内部也能访问底层 api_client self._api_client = self.client._api_client @staticmethod From 9ad5ebb209eafe59d8d794affd5360e4614aeb22 Mon Sep 17 00:00:00 2001 From: foxplaying <166147707+foxplaying@users.noreply.github.com> Date: Sun, 21 Sep 2025 22:06:40 +0800 Subject: [PATCH 06/26] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=BB=98=E8=AE=A4API?= =?UTF-8?q?=E5=9C=B0=E5=9D=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/model_config_template.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/model_config_template.toml b/template/model_config_template.toml index 6b85cea3..9eae0e3e 100644 --- a/template/model_config_template.toml +++ b/template/model_config_template.toml @@ -23,7 +23,7 @@ retry_interval = 10 [[api_providers]] # 特殊:Google的Gimini使用特殊API,与OpenAI格式不兼容,需要配置client为"gemini" name = "Google" -base_url = "https://api.google.com/v1" +base_url = "https://generativelanguage.googleapis.com/v1beta" api_key = "your-google-api-key-1" client_type = "gemini" max_retry = 2 From 0583fe0aa1b9d6989585b6cce7030b7c1408a850 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Mon, 22 Sep 2025 00:52:11 +0800 Subject: [PATCH 07/26] =?UTF-8?q?update:=E6=9B=B4=E6=96=B0dev?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- changelogs/changelog.md | 5 +++-- src/config/config.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/changelogs/changelog.md b/changelogs/changelog.md index 3d2800c7..ab5f86ad 100644 --- a/changelogs/changelog.md +++ b/changelogs/changelog.md @@ -1,7 +1,8 @@ # Changelog -0.10.4饼 表达方式优化 -无了 +## [0.10.4] - 2025-9-22 +表达方式优化 + ## [0.10.3] - 2025-9-22 ### 🌟 主要功能更改 diff --git a/src/config/config.py b/src/config/config.py index da792fbf..c7d6d31e 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -54,7 +54,7 @@ TEMPLATE_DIR = os.path.join(PROJECT_ROOT, "template") # 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码 # 对该字段的更新,请严格参照语义化版本规范:https://semver.org/lang/zh-CN/ -MMC_VERSION = "0.10.3" +MMC_VERSION = "0.10.4-snapshot.1" def get_key_comment(toml_table, key): From 45115b802f5ce6ca19fec6949e8a7bcdc2b0741b Mon Sep 17 00:00:00 2001 From: foxplaying <166147707+foxplaying@users.noreply.github.com> Date: Mon, 22 Sep 2025 11:59:12 +0800 Subject: [PATCH 08/26] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/llm_models/model_client/gemini_client.py | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/llm_models/model_client/gemini_client.py b/src/llm_models/model_client/gemini_client.py index 67c7475e..1a2e0426 100644 --- a/src/llm_models/model_client/gemini_client.py +++ b/src/llm_models/model_client/gemini_client.py @@ -17,6 +17,7 @@ from google.genai.types import ( EmbedContentResponse, EmbedContentConfig, SafetySetting, + HttpOptions, HarmCategory, HarmBlockThreshold, ) @@ -345,22 +346,21 @@ class GeminiClient(BaseClient): def __init__(self, api_provider: APIProvider): super().__init__(api_provider) + + http_options_kwargs = {"timeout": api_provider.timeout} + # 增加传入参数处理 + if api_provider.base_url: + parts = api_provider.base_url.rsplit("/", 1) + if len(parts) == 2 and parts[1].startswith("v"): + http_options_kwargs["base_url"] = parts[0] + "/" + http_options_kwargs["api_version"] = parts[1] + else: + http_options_kwargs["base_url"] = api_provider.base_url self.client = genai.Client( + http_options=HttpOptions(**http_options_kwargs), api_key=api_provider.api_key, ) # 这里和openai不一样,gemini会自己决定自己是否需要retry - # 尝试传入自定义base_url(实验性,必须为Gemini格式) - if hasattr(api_provider, "base_url") and api_provider.base_url: - base_url = api_provider.base_url.rstrip("/") # 去掉末尾 / - self.client._api_client._http_options.base_url = base_url - - # 如果 base_url 已经带了 /v1 或 /v1beta,就清掉 SDK 的 api_version - if base_url.endswith("/v1") or base_url.endswith("/v1beta"): - self.client._api_client._http_options.api_version = None - - # 让 GeminiClient 内部也能访问底层 api_client - self._api_client = self.client._api_client - @staticmethod def clamp_thinking_budget(tb: int, model_id: str) -> int: """ From be5b1cd1f53c27c18f9420ea5bc430de153492df Mon Sep 17 00:00:00 2001 From: foxplaying <166147707+foxplaying@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:28:06 +0800 Subject: [PATCH 09/26] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/llm_models/model_client/gemini_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/llm_models/model_client/gemini_client.py b/src/llm_models/model_client/gemini_client.py index 1a2e0426..c662a687 100644 --- a/src/llm_models/model_client/gemini_client.py +++ b/src/llm_models/model_client/gemini_client.py @@ -350,9 +350,9 @@ class GeminiClient(BaseClient): http_options_kwargs = {"timeout": api_provider.timeout} # 增加传入参数处理 if api_provider.base_url: - parts = api_provider.base_url.rsplit("/", 1) + parts = api_provider.base_url.rstrip("/").rsplit("/", 1) if len(parts) == 2 and parts[1].startswith("v"): - http_options_kwargs["base_url"] = parts[0] + "/" + http_options_kwargs["base_url"] = f"{parts[0]}/" http_options_kwargs["api_version"] = parts[1] else: http_options_kwargs["base_url"] = api_provider.base_url From 3eeb1a84b3c437d9ee70d205988265141268b69b Mon Sep 17 00:00:00 2001 From: foxplaying <166147707+foxplaying@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:43:34 +0800 Subject: [PATCH 10/26] =?UTF-8?q?=E6=97=B6=E9=97=B4=E5=8D=95=E4=BD=8D?= =?UTF-8?q?=E8=BD=AC=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/llm_models/model_client/gemini_client.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/llm_models/model_client/gemini_client.py b/src/llm_models/model_client/gemini_client.py index c662a687..4c3e4305 100644 --- a/src/llm_models/model_client/gemini_client.py +++ b/src/llm_models/model_client/gemini_client.py @@ -346,9 +346,15 @@ class GeminiClient(BaseClient): def __init__(self, api_provider: APIProvider): super().__init__(api_provider) - - http_options_kwargs = {"timeout": api_provider.timeout} + # 增加传入参数处理 + http_options_kwargs = {"timeout": api_provider.timeout} + + # 秒转换为毫秒传入 + if api_provider.timeout is not None: + http_options_kwargs["timeout"] = int(api_provider.timeout * 1000) + + # 传入并处理地址和版本(必须为Gemini格式) if api_provider.base_url: parts = api_provider.base_url.rstrip("/").rsplit("/", 1) if len(parts) == 2 and parts[1].startswith("v"): From fbc84789cb3c9e5350154402461b93c2b2b9956a Mon Sep 17 00:00:00 2001 From: foxplaying <166147707+foxplaying@users.noreply.github.com> Date: Tue, 23 Sep 2025 17:15:21 +0800 Subject: [PATCH 11/26] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=A2=84=E7=AE=97?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/llm_models/model_client/gemini_client.py | 23 ++++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/llm_models/model_client/gemini_client.py b/src/llm_models/model_client/gemini_client.py index 4c3e4305..9e8844e8 100644 --- a/src/llm_models/model_client/gemini_client.py +++ b/src/llm_models/model_client/gemini_client.py @@ -386,20 +386,29 @@ class GeminiClient(BaseClient): limits = THINKING_BUDGET_LIMITS[key] break - # 特殊值处理 + # 预算值处理 if tb == THINKING_BUDGET_AUTO: return THINKING_BUDGET_AUTO if tb == THINKING_BUDGET_DISABLED: if limits and limits.get("can_disable", False): return THINKING_BUDGET_DISABLED - return limits["min"] if limits else THINKING_BUDGET_AUTO + if limits: + logger.warning(f"模型 {model_id} 不支持禁用思考预算,已回退到最小值 {limits['min']}") + return limits["min"] + return THINKING_BUDGET_AUTO - # 已知模型裁剪到范围 + # 已知模型范围裁剪 + 提示 if limits: - return max(limits["min"], min(tb, limits["max"])) + if tb < limits["min"]: + logger.warning(f"模型 {model_id} 的 thinking_budget={tb} 过小,已调整为最小值 {limits['min']}") + return limits["min"] + if tb > limits["max"]: + logger.warning(f"模型 {model_id} 的 thinking_budget={tb} 过大,已调整为最大值 {limits['max']}") + return limits["max"] + return tb - # 未知模型,返回动态模式 - logger.warning(f"模型 {model_id} 未在 THINKING_BUDGET_LIMITS 中定义,将使用动态模式 tb=-1 兼容。") + # 未知模型 → 默认自动模式 + logger.warning(f"模型 {model_id} 未在 THINKING_BUDGET_LIMITS 中定义,已启用模型自动预算兼容") return THINKING_BUDGET_AUTO async def get_response( @@ -454,7 +463,7 @@ class GeminiClient(BaseClient): try: tb = int(extra_params["thinking_budget"]) except (ValueError, TypeError): - logger.warning(f"无效的 thinking_budget 值 {extra_params['thinking_budget']},将使用默认动态模式 {tb}") + logger.warning(f"无效的 thinking_budget 值 {extra_params['thinking_budget']},将使用模型自动预算模式 {tb}") # 裁剪到模型支持的范围 tb = self.clamp_thinking_budget(tb, model_info.model_identifier) From a2916515066676e13a08e905f830b993f4b98088 Mon Sep 17 00:00:00 2001 From: foxplaying <166147707+foxplaying@users.noreply.github.com> Date: Tue, 23 Sep 2025 17:25:48 +0800 Subject: [PATCH 12/26] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/model_configuration_guide.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/docs/model_configuration_guide.md b/docs/model_configuration_guide.md index fd1cb018..96ab3a23 100644 --- a/docs/model_configuration_guide.md +++ b/docs/model_configuration_guide.md @@ -28,7 +28,7 @@ version = "1.1.1" ```toml [[api_providers]] name = "DeepSeek" # 服务商名称(自定义) -base_url = "https://api.deepseek.cn/v1" # API服务的基础URL +base_url = "https://api.deepseek.com/v1" # API服务的基础URL api_key = "your-api-key-here" # API密钥 client_type = "openai" # 客户端类型 max_retry = 2 # 最大重试次数 @@ -43,19 +43,19 @@ retry_interval = 10 # 重试间隔(秒) | `name` | ✅ | 服务商名称,需要在模型配置中引用 | - | | `base_url` | ✅ | API服务的基础URL | - | | `api_key` | ✅ | API密钥,请替换为实际密钥 | - | -| `client_type` | ❌ | 客户端类型:`openai`(OpenAI格式)或 `gemini`(Gemini格式,现在支持不良好) | `openai` | +| `client_type` | ❌ | 客户端类型:`openai`(OpenAI格式)或 `gemini`(Gemini格式) | `openai` | | `max_retry` | ❌ | API调用失败时的最大重试次数 | 2 | | `timeout` | ❌ | API请求超时时间(秒) | 30 | | `retry_interval` | ❌ | 重试间隔时间(秒) | 10 | -**请注意,对于`client_type`为`gemini`的模型,`base_url`字段无效。** +**请注意,对于`client_type`为`gemini`的模型,`retry`字段由`gemini`自己决定。** ### 2.3 支持的服务商示例 #### DeepSeek ```toml [[api_providers]] name = "DeepSeek" -base_url = "https://api.deepseek.cn/v1" +base_url = "https://api.deepseek.com/v1" api_key = "your-deepseek-api-key" client_type = "openai" ``` @@ -73,7 +73,7 @@ client_type = "openai" ```toml [[api_providers]] name = "Google" -base_url = "https://api.google.com/v1" +base_url = "https://generativelanguage.googleapis.com/v1beta" api_key = "your-google-api-key" client_type = "gemini" # 注意:Gemini需要使用特殊客户端 ``` @@ -133,7 +133,17 @@ thinking = {type = "disabled"} # 禁用思考 ``` 请注意,`extra_params` 的配置应该构成一个合法的TOML字典结构,具体内容取决于API服务商的要求。 -**请注意,对于`client_type`为`gemini`的模型,此字段无效。** +对于`gemini`需要单独进行配置 +```toml +[[models]] +model_identifier = "gemini-2.5-flash" +name = "gemini-2.5-flash" +api_provider = "google" +[models.extra_params] +thinking_budget = 0 # 禁用思考 +# thinking_budget = -1 由模型自己决定 +``` + ### 3.3 配置参数说明 | 参数 | 必填 | 说明 | From 30ef8dd81a892b3c2336d7e6b540b411e3473ebe Mon Sep 17 00:00:00 2001 From: foxplaying <166147707+foxplaying@users.noreply.github.com> Date: Tue, 23 Sep 2025 17:27:35 +0800 Subject: [PATCH 13/26] Update model_configuration_guide.md --- docs/model_configuration_guide.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/model_configuration_guide.md b/docs/model_configuration_guide.md index 96ab3a23..65fae981 100644 --- a/docs/model_configuration_guide.md +++ b/docs/model_configuration_guide.md @@ -131,9 +131,8 @@ enable_thinking = false # 禁用思考 [models.extra_params] thinking = {type = "disabled"} # 禁用思考 ``` -请注意,`extra_params` 的配置应该构成一个合法的TOML字典结构,具体内容取决于API服务商的要求。 -对于`gemini`需要单独进行配置 +而对于`gemini`需要单独进行配置 ```toml [[models]] model_identifier = "gemini-2.5-flash" @@ -144,6 +143,8 @@ thinking_budget = 0 # 禁用思考 # thinking_budget = -1 由模型自己决定 ``` +请注意,`extra_params` 的配置应该构成一个合法的TOML字典结构,具体内容取决于API服务商的要求。 + ### 3.3 配置参数说明 | 参数 | 必填 | 说明 | From f8844fec4f51346a6fc5131d6386bdcdb4dfa128 Mon Sep 17 00:00:00 2001 From: foxplaying <166147707+foxplaying@users.noreply.github.com> Date: Tue, 23 Sep 2025 17:49:06 +0800 Subject: [PATCH 14/26] Update model_configuration_guide.md --- docs/model_configuration_guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/model_configuration_guide.md b/docs/model_configuration_guide.md index 65fae981..f2da8be1 100644 --- a/docs/model_configuration_guide.md +++ b/docs/model_configuration_guide.md @@ -137,7 +137,7 @@ thinking = {type = "disabled"} # 禁用思考 [[models]] model_identifier = "gemini-2.5-flash" name = "gemini-2.5-flash" -api_provider = "google" +api_provider = "Google" [models.extra_params] thinking_budget = 0 # 禁用思考 # thinking_budget = -1 由模型自己决定 From 22ddaa5b72fbd17f8aa3f46b232decd0fc0f1934 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Tue, 23 Sep 2025 19:42:45 +0800 Subject: [PATCH 15/26] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E6=94=B9=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/heart_flow/heartFC_chat.py | 2 +- src/chat/planner_actions/planner.py | 4 ++-- src/plugins/built_in/emoji_plugin/emoji.py | 10 ---------- template/model_config_template.toml | 2 +- 4 files changed, 4 insertions(+), 14 deletions(-) diff --git a/src/chat/heart_flow/heartFC_chat.py b/src/chat/heart_flow/heartFC_chat.py index f528116e..778e0c3d 100644 --- a/src/chat/heart_flow/heartFC_chat.py +++ b/src/chat/heart_flow/heartFC_chat.py @@ -207,7 +207,7 @@ class HeartFChatting: await self._observe(recent_messages_list=recent_messages_list) else: # 没有提到,继续保持沉默,等待5秒防止频繁触发 - await asyncio.sleep(5) + await asyncio.sleep(10) return True else: await asyncio.sleep(0.2) diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index 741aa94b..ed59207a 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -451,8 +451,8 @@ class ActionPlanner: # 调用LLM llm_content, (reasoning_content, _, _) = await self.planner_llm.generate_response_async(prompt=prompt) - logger.info(f"{self.log_prefix}规划器原始提示词: {prompt}") - logger.info(f"{self.log_prefix}规划器原始响应: {llm_content}") + # logger.info(f"{self.log_prefix}规划器原始提示词: {prompt}") + # logger.info(f"{self.log_prefix}规划器原始响应: {llm_content}") if global_config.debug.show_prompt: logger.info(f"{self.log_prefix}规划器原始提示词: {prompt}") diff --git a/src/plugins/built_in/emoji_plugin/emoji.py b/src/plugins/built_in/emoji_plugin/emoji.py index c1f963df..da620d22 100644 --- a/src/plugins/built_in/emoji_plugin/emoji.py +++ b/src/plugins/built_in/emoji_plugin/emoji.py @@ -28,16 +28,6 @@ class EmojiAction(BaseAction): action_name = "emoji" action_description = "发送表情包辅助表达情绪" - # LLM判断提示词 - llm_judge_prompt = """ - 判定是否需要使用表情动作的条件: - 1. 用户明确要求使用表情包 - 2. 这是一个适合表达强烈情绪的场合 - 3. 不要发送太多表情包,如果你已经发送过多个表情包则回答"否" - - 请回答"是"或"否"。 - """ - # 动作参数定义 action_parameters = {} diff --git a/template/model_config_template.toml b/template/model_config_template.toml index fcc7a00a..01786f98 100644 --- a/template/model_config_template.toml +++ b/template/model_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "1.7.0" +version = "1.7.2" # 配置文件版本号迭代规则同bot_config.toml From fad8b82d8bfaa485b63b45ded12816d3753eba4d Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Tue, 23 Sep 2025 20:44:48 +0800 Subject: [PATCH 16/26] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=B4=9F=E8=BD=BD?= =?UTF-8?q?=E5=9D=87=E8=A1=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/llm_models/utils_model.py | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/src/llm_models/utils_model.py b/src/llm_models/utils_model.py index 8bb35ef0..b7a65842 100644 --- a/src/llm_models/utils_model.py +++ b/src/llm_models/utils_model.py @@ -26,18 +26,6 @@ install(extra_lines=3) logger = get_logger("model_utils") -# 常见Error Code Mapping -error_code_mapping = { - 400: "参数不正确", - 401: "API key 错误,认证失败,请检查 config/model_config.toml 中的配置是否正确", - 402: "账号余额不足", - 403: "需要实名,或余额不足", - 404: "Not Found", - 429: "请求过于频繁,请稍后再试", - 500: "服务器内部故障", - 503: "服务器负载过高", -} - class RequestType(Enum): """请求类型枚举""" @@ -267,14 +255,14 @@ class LLMRequest: extra_params=model_info.extra_params, ) elif request_type == RequestType.EMBEDDING: - assert embedding_input is not None + assert embedding_input is not None, "嵌入输入不能为空" return await client.get_embedding( model_info=model_info, embedding_input=embedding_input, extra_params=model_info.extra_params, ) elif request_type == RequestType.AUDIO: - assert audio_base64 is not None + assert audio_base64 is not None, "音频Base64不能为空" return await client.get_audio_transcriptions( model_info=model_info, audio_base64=audio_base64, @@ -365,24 +353,23 @@ class LLMRequest: embedding_input=embedding_input, audio_base64=audio_base64, ) + total_tokens, penalty, usage_penalty = self.model_usage[model_info.name] + if response_usage := response.usage: + total_tokens += response_usage.total_tokens + self.model_usage[model_info.name] = (total_tokens, penalty, usage_penalty - 1) return response, model_info except ModelAttemptFailed as e: last_exception = e.original_exception or e logger.warning(f"模型 '{model_info.name}' 尝试失败,切换到下一个模型。原因: {e}") total_tokens, penalty, usage_penalty = self.model_usage[model_info.name] - self.model_usage[model_info.name] = (total_tokens, penalty + 1, usage_penalty) + self.model_usage[model_info.name] = (total_tokens, penalty + 1, usage_penalty - 1) failed_models_this_request.add(model_info.name) if isinstance(last_exception, RespNotOkException) and last_exception.status_code == 400: logger.error("收到不可恢复的客户端错误 (400),中止所有尝试。") raise last_exception from e - finally: - total_tokens, penalty, usage_penalty = self.model_usage[model_info.name] - if usage_penalty > 0: - self.model_usage[model_info.name] = (total_tokens, penalty, usage_penalty - 1) - logger.error(f"所有 {max_attempts} 个模型均尝试失败。") if last_exception: raise last_exception From 294b451c965a110cfa6a9da3073a9fa3cd5abcc1 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Tue, 23 Sep 2025 20:51:01 +0800 Subject: [PATCH 17/26] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dtyping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/llm_models/model_client/gemini_client.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/llm_models/model_client/gemini_client.py b/src/llm_models/model_client/gemini_client.py index 9e8844e8..c23ba90a 100644 --- a/src/llm_models/model_client/gemini_client.py +++ b/src/llm_models/model_client/gemini_client.py @@ -1,7 +1,7 @@ import asyncio import io import base64 -from typing import Callable, AsyncIterator, Optional, Coroutine, Any, List +from typing import Callable, AsyncIterator, Optional, Coroutine, Any, List, Dict from google import genai from google.genai.types import ( @@ -346,14 +346,14 @@ class GeminiClient(BaseClient): def __init__(self, api_provider: APIProvider): super().__init__(api_provider) - + # 增加传入参数处理 - http_options_kwargs = {"timeout": api_provider.timeout} + http_options_kwargs: Dict[str, Any] = {} # 秒转换为毫秒传入 if api_provider.timeout is not None: http_options_kwargs["timeout"] = int(api_provider.timeout * 1000) - + # 传入并处理地址和版本(必须为Gemini格式) if api_provider.base_url: parts = api_provider.base_url.rstrip("/").rsplit("/", 1) @@ -463,7 +463,9 @@ class GeminiClient(BaseClient): try: tb = int(extra_params["thinking_budget"]) except (ValueError, TypeError): - logger.warning(f"无效的 thinking_budget 值 {extra_params['thinking_budget']},将使用模型自动预算模式 {tb}") + logger.warning( + f"无效的 thinking_budget 值 {extra_params['thinking_budget']},将使用模型自动预算模式 {tb}" + ) # 裁剪到模型支持的范围 tb = self.clamp_thinking_budget(tb, model_info.model_identifier) From 680593d57c50019d53fc1288d3aa00fcbd6b4923 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Tue, 23 Sep 2025 21:02:03 +0800 Subject: [PATCH 18/26] fix #1253 --- src/chat/message_receive/bot.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/chat/message_receive/bot.py b/src/chat/message_receive/bot.py index 0709dcd8..f611bbd2 100644 --- a/src/chat/message_receive/bot.py +++ b/src/chat/message_receive/bot.py @@ -3,7 +3,7 @@ import os import re from typing import Dict, Any, Optional -from maim_message import UserInfo, Seg +from maim_message import UserInfo, Seg, GroupInfo from src.common.logger import get_logger from src.config.config import global_config @@ -27,7 +27,7 @@ PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.. logger = get_logger("chat") -def _check_ban_words(text: str, chat: ChatStream, userinfo: UserInfo) -> bool: +def _check_ban_words(text: str, userinfo: UserInfo, group_info: Optional[GroupInfo] = None) -> bool: """检查消息是否包含过滤词 Args: @@ -40,14 +40,14 @@ def _check_ban_words(text: str, chat: ChatStream, userinfo: UserInfo) -> bool: """ for word in global_config.message_receive.ban_words: if word in text: - chat_name = chat.group_info.group_name if chat.group_info else "私聊" + chat_name = group_info.group_name if group_info else "私聊" logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}") logger.info(f"[过滤词识别]消息中含有{word},filtered") return True return False -def _check_ban_regex(text: str, chat: ChatStream, userinfo: UserInfo) -> bool: +def _check_ban_regex(text: str, userinfo: UserInfo, group_info: Optional[GroupInfo] = None) -> bool: """检查消息是否匹配过滤正则表达式 Args: @@ -61,10 +61,10 @@ def _check_ban_regex(text: str, chat: ChatStream, userinfo: UserInfo) -> bool: # 检查text是否为None或空字符串 if text is None or not text: return False - + for pattern in global_config.message_receive.ban_msgs_regex: if re.search(pattern, text): - chat_name = chat.group_info.group_name if chat.group_info else "私聊" + chat_name = group_info.group_name if group_info else "私聊" logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}") logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered") return True @@ -251,6 +251,18 @@ class ChatBot: # return pass + # 过滤检查 + if _check_ban_words( + message.processed_plain_text, + user_info, # type: ignore + group_info, + ) or _check_ban_regex( + message.raw_message, # type: ignore + user_info, # type: ignore + group_info, + ): + return + get_chat_manager().register_message(message) chat = await get_chat_manager().get_or_create_stream( @@ -268,14 +280,6 @@ class ChatBot: # logger.warning(f"检测到消息中含有违法,色情,暴力,反动,敏感内容,消息内容:{message.processed_plain_text},发送者:{message.message_info.user_info.user_nickname}") # return - # 过滤检查 - if _check_ban_words(message.processed_plain_text, chat, user_info) or _check_ban_regex( # type: ignore - message.raw_message, # type: ignore - chat, - user_info, # type: ignore - ): - return - # 命令处理 - 使用新插件系统检查并处理命令 is_command, cmd_result, continue_process = await self._process_commands_with_new_system(message) From e6c07cb5a6795b512e801e1c972d95158c67151b Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 24 Sep 2025 10:19:23 +0800 Subject: [PATCH 19/26] Update pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 700844c4..5b761fd0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ dependencies = [ "jieba>=0.42.1", "json-repair>=0.47.6", "jsonlines>=4.0.0", - "maim-message>=0.3.8", + "maim-message>=0.5.1", "matplotlib>=3.10.3", "networkx>=3.4.2", "numpy>=2.2.6", From dff75142ecaaa6789b35c8c5a12dbf07b5141b46 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 24 Sep 2025 16:13:43 +0800 Subject: [PATCH 20/26] =?UTF-8?q?feat=EF=BC=9A=E6=B7=BB=E5=8A=A0=E5=86=85?= =?UTF-8?q?=E7=BD=AEemoji=E7=AE=A1=E7=90=86=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + plugins/emoji_manage_plugin/_manifest.json | 53 ++++++ plugins/emoji_manage_plugin/plugin.py | 182 ++++++++++++++++++ src/chat/utils/utils_image.py | 38 ++++ src/plugin_system/apis/emoji_api.py | 210 ++++++++++++++++++++- 5 files changed, 481 insertions(+), 3 deletions(-) create mode 100644 plugins/emoji_manage_plugin/_manifest.json create mode 100644 plugins/emoji_manage_plugin/plugin.py diff --git a/.gitignore b/.gitignore index 104a3012..373171a7 100644 --- a/.gitignore +++ b/.gitignore @@ -321,6 +321,7 @@ run_pet.bat /plugins/* !/plugins !/plugins/hello_world_plugin +!/plugins/emoji_manage_plugin !/plugins/take_picture_plugin config.toml diff --git a/plugins/emoji_manage_plugin/_manifest.json b/plugins/emoji_manage_plugin/_manifest.json new file mode 100644 index 00000000..ee8d8318 --- /dev/null +++ b/plugins/emoji_manage_plugin/_manifest.json @@ -0,0 +1,53 @@ +{ + "manifest_version": 1, + "name": "BetterEmoji", + "version": "1.0.0", + "description": "更好的表情包管理插件", + "author": { + "name": "SengokuCola", + "url": "https://github.com/SengokuCola" + }, + "license": "GPL-v3.0-or-later", + + "host_application": { + "min_version": "0.10.4" + }, + "homepage_url": "https://github.com/SengokuCola/BetterEmoji", + "repository_url": "https://github.com/SengokuCola/BetterEmoji", + "keywords": ["emoji", "manage", "plugin"], + "categories": ["Examples", "Tutorial"], + + "default_locale": "zh-CN", + "locales_path": "_locales", + + "plugin_info": { + "is_built_in": false, + "plugin_type": "emoji_manage", + "components": [ + { + "type": "action", + "name": "hello_greeting", + "description": "向用户发送问候消息" + }, + { + "type": "action", + "name": "bye_greeting", + "description": "向用户发送告别消息", + "activation_modes": ["keyword"], + "keywords": ["再见", "bye", "88", "拜拜"] + }, + { + "type": "command", + "name": "time", + "description": "查询当前时间", + "pattern": "/time" + } + ], + "features": [ + "问候和告别功能", + "时间查询命令", + "配置文件示例", + "新手教程代码" + ] + } +} \ No newline at end of file diff --git a/plugins/emoji_manage_plugin/plugin.py b/plugins/emoji_manage_plugin/plugin.py new file mode 100644 index 00000000..8e0bbb6b --- /dev/null +++ b/plugins/emoji_manage_plugin/plugin.py @@ -0,0 +1,182 @@ +import random +from typing import List, Tuple, Type, Any +from src.plugin_system import ( + BasePlugin, + register_plugin, + BaseAction, + BaseCommand, + BaseTool, + ComponentInfo, + ActionActivationType, + ConfigField, + BaseEventHandler, + EventType, + MaiMessages, + ToolParamType, + ReplyContentType, + emoji_api, +) +from src.config.config import global_config + + +class ListEmojiCommand(BaseCommand): + """列表表情包Command - 响应/emoji list命令""" + + command_name = "emoji_list" + command_description = "列表表情包" + + # === 命令设置(必须填写)=== + command_pattern = r"^/emoji list$" # 精确匹配 "/emoji list" 命令 + + async def execute(self) -> Tuple[bool, str, bool]: + """执行列表表情包""" + import datetime + + # 获取当前时间 + time_format: str = self.get_config("time.format", "%Y-%m-%d %H:%M:%S") # type: ignore + now = datetime.datetime.now() + time_str = now.strftime(time_format) + + # 发送时间信息 + message = f"⏰ 当前时间:{time_str}" + await self.send_text(message) + + return True, f"显示了当前时间: {time_str}", True + + +class PrintMessage(BaseEventHandler): + """打印消息事件处理器 - 处理打印消息事件""" + + event_type = EventType.ON_MESSAGE + handler_name = "print_message_handler" + handler_description = "打印接收到的消息" + + async def execute(self, message: MaiMessages | None) -> Tuple[bool, bool, str | None, None, None]: + """执行打印消息事件处理""" + # 打印接收到的消息 + if self.get_config("print_message.enabled", False): + print(f"接收到消息: {message.raw_message if message else '无效消息'}") + return True, True, "消息已打印", None, None + + +class ForwardMessages(BaseEventHandler): + """ + 把接收到的消息转发到指定聊天ID + + 此组件是HYBRID消息和FORWARD消息的使用示例。 + 每收到10条消息,就会以1%的概率使用HYBRID消息转发,否则使用FORWARD消息转发。 + """ + + event_type = EventType.ON_MESSAGE + handler_name = "forward_messages_handler" + handler_description = "把接收到的消息转发到指定聊天ID" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.counter = 0 # 用于计数转发的消息数量 + self.messages: List[str] = [] + + async def execute(self, message: MaiMessages | None) -> Tuple[bool, bool, None, None, None]: + if not message: + return True, True, None, None, None + stream_id = message.stream_id or "" + + if message.plain_text: + self.messages.append(message.plain_text) + self.counter += 1 + if self.counter % 10 == 0: + if random.random() < 0.01: + success = await self.send_hybrid(stream_id, [(ReplyContentType.TEXT, msg) for msg in self.messages]) + else: + success = await self.send_forward( + stream_id, + [ + ( + str(global_config.bot.qq_account), + str(global_config.bot.nickname), + [(ReplyContentType.TEXT, msg)], + ) + for msg in self.messages + ], + ) + if not success: + raise ValueError("转发消息失败") + self.messages = [] + return True, True, None, None, None + + +class RandomEmojis(BaseCommand): + command_name = "random_emojis" + command_description = "发送多张随机表情包" + command_pattern = r"^/random_emojis$" + + async def execute(self): + emojis = await emoji_api.get_random(5) + if not emojis: + return False, "未找到表情包", False + emoji_base64_list = [] + for emoji in emojis: + emoji_base64_list.append(emoji[0]) + return await self.forward_images(emoji_base64_list) + + async def forward_images(self, images: List[str]): + """ + 把多张图片用合并转发的方式发给用户 + """ + success = await self.send_forward([("0", "神秘用户", [(ReplyContentType.IMAGE, img)]) for img in images]) + return (True, "已发送随机表情包", True) if success else (False, "发送随机表情包失败", False) + + +# ===== 插件注册 ===== + + +@register_plugin +class EmojiManagePlugin(BasePlugin): + """表情包管理插件 - 管理表情包""" + + # 插件基本信息 + plugin_name: str = "emoji_manage_plugin" # 内部标识符 + enable_plugin: bool = False + dependencies: List[str] = [] # 插件依赖列表 + python_dependencies: List[str] = [] # Python包依赖列表 + config_file_name: str = "config.toml" # 配置文件名 + + # 配置节描述 + config_section_descriptions = {"plugin": "插件基本信息", "emoji": "表情包功能配置"} + + # 配置Schema定义 + config_schema: dict = { + "plugin": { + "version": ConfigField(type=str, default="1.0.0", description="插件版本"), + "enabled": ConfigField(type=bool, default=False, description="是否启用插件"), + }, + } + + def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: + return [ + (PrintMessage.get_handler_info(), PrintMessage), + (ForwardMessages.get_handler_info(), ForwardMessages), + (RandomEmojis.get_command_info(), RandomEmojis), + ] + + +# @register_plugin +# class HelloWorldEventPlugin(BaseEPlugin): +# """Hello World事件插件 - 处理问候和告别事件""" + +# plugin_name = "hello_world_event_plugin" +# enable_plugin = False +# dependencies = [] +# python_dependencies = [] +# config_file_name = "event_config.toml" + +# config_schema = { +# "plugin": { +# "name": ConfigField(type=str, default="hello_world_event_plugin", description="插件名称"), +# "version": ConfigField(type=str, default="1.0.0", description="插件版本"), +# "enabled": ConfigField(type=bool, default=True, description="是否启用插件"), +# }, +# } + +# def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: +# return [(PrintMessage.get_handler_info(), PrintMessage)] diff --git a/src/chat/utils/utils_image.py b/src/chat/utils/utils_image.py index 94565b78..deba60a4 100644 --- a/src/chat/utils/utils_image.py +++ b/src/chat/utils/utils_image.py @@ -623,3 +623,41 @@ def image_path_to_base64(image_path: str) -> str: return base64.b64encode(image_data).decode("utf-8") else: raise IOError(f"读取图片文件失败: {image_path}") + + +def base64_to_image(image_base64: str, output_path: str) -> bool: + """将base64编码的图片保存为文件 + + Args: + image_base64: 图片的base64编码 + output_path: 输出文件路径 + + Returns: + bool: 是否成功保存 + + Raises: + ValueError: 当base64编码无效时 + IOError: 当保存文件失败时 + """ + try: + # 确保base64字符串只包含ASCII字符 + if isinstance(image_base64, str): + image_base64 = image_base64.encode("ascii", errors="ignore").decode("ascii") + + # 解码base64 + image_bytes = base64.b64decode(image_base64) + + # 确保输出目录存在 + output_dir = os.path.dirname(output_path) + if output_dir: + os.makedirs(output_dir, exist_ok=True) + + # 保存文件 + with open(output_path, "wb") as f: + f.write(image_bytes) + + return True + + except Exception as e: + logger.error(f"保存base64图片失败: {e}") + return False diff --git a/src/plugin_system/apis/emoji_api.py b/src/plugin_system/apis/emoji_api.py index f8faebfe..d127aa6d 100644 --- a/src/plugin_system/apis/emoji_api.py +++ b/src/plugin_system/apis/emoji_api.py @@ -9,11 +9,15 @@ """ import random +import base64 +import os +import uuid +import time -from typing import Optional, Tuple, List +from typing import Optional, Tuple, List, Dict, Any from src.common.logger import get_logger -from src.chat.emoji_system.emoji_manager import get_emoji_manager -from src.chat.utils.utils_image import image_path_to_base64 +from src.chat.emoji_system.emoji_manager import get_emoji_manager, EMOJI_DIR +from src.chat.utils.utils_image import image_path_to_base64, base64_to_image logger = get_logger("emoji_api") @@ -245,6 +249,42 @@ def get_emotions() -> List[str]: return [] +async def get_all() -> List[Tuple[str, str, str]]: + """获取所有表情包 + + Returns: + List[Tuple[str, str, str]]: 包含(base64编码, 表情包描述, 随机情感标签)的元组列表 + """ + try: + emoji_manager = get_emoji_manager() + all_emojis = emoji_manager.emoji_objects + + if not all_emojis: + logger.warning("[EmojiAPI] 没有可用的表情包") + return [] + + results = [] + for emoji_obj in all_emojis: + if emoji_obj.is_deleted: + continue + + emoji_base64 = image_path_to_base64(emoji_obj.full_path) + + if not emoji_base64: + logger.error(f"[EmojiAPI] 无法转换表情包为base64: {emoji_obj.full_path}") + continue + + matched_emotion = random.choice(emoji_obj.emotion) if emoji_obj.emotion else "随机表情" + results.append((emoji_base64, emoji_obj.description, matched_emotion)) + + logger.debug(f"[EmojiAPI] 成功获取 {len(results)} 个表情包") + return results + + except Exception as e: + logger.error(f"[EmojiAPI] 获取所有表情包失败: {e}") + return [] + + def get_descriptions() -> List[str]: """获取所有表情包描述 @@ -264,3 +304,167 @@ def get_descriptions() -> List[str]: except Exception as e: logger.error(f"[EmojiAPI] 获取表情包描述失败: {e}") return [] + + +# ============================================================================= +# 表情包注册API函数 +# ============================================================================= + + +async def register_emoji(image_base64: str, filename: Optional[str] = None) -> Dict[str, Any]: + """注册新的表情包 + + Args: + image_base64: 图片的base64编码 + filename: 可选的文件名,如果未提供则自动生成 + + Returns: + Dict[str, Any]: 注册结果,包含以下字段: + - success: bool, 是否成功注册 + - message: str, 结果消息 + - description: Optional[str], 表情包描述(成功时) + - emotions: Optional[List[str]], 情感标签列表(成功时) + - replaced: Optional[bool], 是否替换了旧表情包(成功时) + - hash: Optional[str], 表情包哈希值(成功时) + + Raises: + ValueError: 如果base64为空或无效 + TypeError: 如果参数类型不正确 + """ + if not image_base64: + raise ValueError("图片base64编码不能为空") + if not isinstance(image_base64, str): + raise TypeError("image_base64必须是字符串类型") + if filename is not None and not isinstance(filename, str): + raise TypeError("filename必须是字符串类型或None") + + try: + logger.info(f"[EmojiAPI] 开始注册表情包,文件名: {filename or '自动生成'}") + + # 1. 获取emoji管理器并检查容量 + emoji_manager = get_emoji_manager() + count_before = emoji_manager.emoji_num + max_count = emoji_manager.emoji_num_max + + # 2. 检查是否可以注册(未达到上限或启用替换) + can_register = count_before < max_count or ( + count_before >= max_count and emoji_manager.emoji_num_max_reach_deletion + ) + + if not can_register: + return { + "success": False, + "message": f"表情包数量已达上限({count_before}/{max_count})且未启用替换功能", + "description": None, + "emotions": None, + "replaced": None, + "hash": None + } + + # 3. 确保emoji目录存在 + os.makedirs(EMOJI_DIR, exist_ok=True) + + # 4. 生成文件名 + if not filename: + # 基于时间戳和UUID生成唯一文件名 + timestamp = int(time.time()) + unique_id = str(uuid.uuid4())[:8] + filename = f"emoji_{timestamp}_{unique_id}" + + # 确保文件名有扩展名 + if not filename.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')): + filename = f"{filename}.png" # 默认使用png格式 + + # 5. 保存base64图片到emoji目录 + temp_file_path = os.path.join(EMOJI_DIR, filename) + + try: + # 解码base64并保存图片 + if not base64_to_image(image_base64, temp_file_path): + logger.error(f"[EmojiAPI] 无法保存base64图片到文件: {temp_file_path}") + return { + "success": False, + "message": "无法保存图片文件", + "description": None, + "emotions": None, + "replaced": None, + "hash": None + } + + logger.debug(f"[EmojiAPI] 图片已保存到临时文件: {temp_file_path}") + + except Exception as save_error: + logger.error(f"[EmojiAPI] 保存图片文件失败: {save_error}") + return { + "success": False, + "message": f"保存图片文件失败: {str(save_error)}", + "description": None, + "emotions": None, + "replaced": None, + "hash": None + } + + # 6. 调用注册方法 + register_success = await emoji_manager.register_emoji_by_filename(filename) + + # 7. 清理临时文件(如果注册失败但文件还存在) + if not register_success and os.path.exists(temp_file_path): + try: + os.remove(temp_file_path) + logger.debug(f"[EmojiAPI] 已清理临时文件: {temp_file_path}") + except Exception as cleanup_error: + logger.warning(f"[EmojiAPI] 清理临时文件失败: {cleanup_error}") + + # 8. 构建返回结果 + if register_success: + count_after = emoji_manager.emoji_num + replaced = count_after <= count_before # 如果数量没增加,说明是替换 + + # 尝试获取新注册的表情包信息 + new_emoji_info = None + if count_after > count_before or replaced: + # 获取最新的表情包信息 + try: + # 通过文件名查找新注册的表情包(注意:文件名在注册后可能已经改变) + for emoji_obj in reversed(emoji_manager.emoji_objects): + if not emoji_obj.is_deleted and ( + emoji_obj.filename == filename or # 直接匹配 + (hasattr(emoji_obj, 'full_path') and filename in emoji_obj.full_path) # 路径包含匹配 + ): + new_emoji_info = emoji_obj + break + except Exception as find_error: + logger.warning(f"[EmojiAPI] 查找新注册表情包信息失败: {find_error}") + + description = new_emoji_info.description if new_emoji_info else None + emotions = new_emoji_info.emotion if new_emoji_info else None + emoji_hash = new_emoji_info.hash if new_emoji_info else None + + return { + "success": True, + "message": f"表情包注册成功 {'(替换旧表情包)' if replaced else '(新增表情包)'}", + "description": description, + "emotions": emotions, + "replaced": replaced, + "hash": emoji_hash + } + else: + return { + "success": False, + "message": "表情包注册失败,可能因为重复、格式不支持或审核未通过", + "description": None, + "emotions": None, + "replaced": None, + "hash": None + } + + except Exception as e: + logger.error(f"[EmojiAPI] 注册表情包时发生异常: {e}") + return { + "success": False, + "message": f"注册过程中发生错误: {str(e)}", + "description": None, + "emotions": None, + "replaced": None, + "hash": None + } From 174cbbf2a41899b50b8b5ead9dbe3afbf6c13c7a Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Wed, 24 Sep 2025 16:25:02 +0800 Subject: [PATCH 21/26] giving example --- plugins/emoji_manage_plugin/plugin.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/plugins/emoji_manage_plugin/plugin.py b/plugins/emoji_manage_plugin/plugin.py index 8e0bbb6b..56a9aa71 100644 --- a/plugins/emoji_manage_plugin/plugin.py +++ b/plugins/emoji_manage_plugin/plugin.py @@ -16,8 +16,30 @@ from src.plugin_system import ( ReplyContentType, emoji_api, ) +from maim_message import Seg from src.config.config import global_config +class AddEmojiCommand(BaseCommand): + command_name = "add_emoji" + command_description = "添加表情包" + command_pattern = r".*/emoji add.*" + + async def execute(self) -> Tuple[bool, str, bool]: + emoji_base64_list = self.find_and_return_emoji_in_message(self.message.message_segment) + return True, f"找到了{len(emoji_base64_list)}个表情包", True + + def find_and_return_emoji_in_message(self, message_segments: List[Seg]) -> List[str]: + emoji_base64_list = [] + for seg in message_segments: + if seg.type == "emoji": + emoji_base64_list.append(seg.data) + elif seg.type == "image": + # 假设图片数据是base64编码的 + emoji_base64_list.append(seg.data) + elif seg.type == "seglist": + # 递归处理嵌套的Seg列表 + emoji_base64_list.extend(self.find_and_return_emoji_in_message(seg.data)) + return emoji_base64_list class ListEmojiCommand(BaseCommand): """列表表情包Command - 响应/emoji list命令""" From 0964676bfb76cc3758d2764aa6e3906c09147a59 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 24 Sep 2025 17:32:47 +0800 Subject: [PATCH 22/26] =?UTF-8?q?fix=EF=BC=9A=E8=A1=A8=E6=83=85=E5=8C=85?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=8F=92=E4=BB=B6=E5=8F=AF=E5=B7=A5=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/emoji_manage_plugin/plugin.py | 166 +++++++++------------ plugins/hello_world_plugin/plugin.py | 3 +- src/chat/express/expression_learner.py | 69 ++------- src/chat/heart_flow/heartFC_chat.py | 4 + src/chat/planner_actions/planner.py | 4 +- src/plugins/built_in/emoji_plugin/emoji.py | 31 ++-- 6 files changed, 112 insertions(+), 165 deletions(-) diff --git a/plugins/emoji_manage_plugin/plugin.py b/plugins/emoji_manage_plugin/plugin.py index 56a9aa71..b042cabb 100644 --- a/plugins/emoji_manage_plugin/plugin.py +++ b/plugins/emoji_manage_plugin/plugin.py @@ -18,18 +18,84 @@ from src.plugin_system import ( ) from maim_message import Seg from src.config.config import global_config +from src.common.logger import get_logger +logger = get_logger("emoji_manage_plugin") class AddEmojiCommand(BaseCommand): command_name = "add_emoji" command_description = "添加表情包" command_pattern = r".*/emoji add.*" - + async def execute(self) -> Tuple[bool, str, bool]: + # 查找消息中的表情包 + logger.info(f"查找消息中的表情包: {self.message.message_segment}") + emoji_base64_list = self.find_and_return_emoji_in_message(self.message.message_segment) - return True, f"找到了{len(emoji_base64_list)}个表情包", True - - def find_and_return_emoji_in_message(self, message_segments: List[Seg]) -> List[str]: + + if not emoji_base64_list: + return False, "未在消息中找到表情包或图片", False + + # 注册找到的表情包 + success_count = 0 + fail_count = 0 + results = [] + + for i, emoji_base64 in enumerate(emoji_base64_list): + try: + # 使用emoji_api注册表情包 + result = await emoji_api.register_emoji(emoji_base64, filename=f"emoji_{i+1}") + + if result["success"]: + success_count += 1 + description = result.get("description", "未知描述") + emotions = result.get("emotions", []) + replaced = result.get("replaced", False) + + result_msg = f"表情包 {i+1} 注册成功{'(替换旧表情包)' if replaced else '(新增表情包)'}" + if description: + result_msg += f"\n描述: {description}" + if emotions: + result_msg += f"\n情感标签: {', '.join(emotions)}" + + results.append(result_msg) + else: + fail_count += 1 + error_msg = result.get("message", "注册失败") + results.append(f"表情包 {i+1} 注册失败: {error_msg}") + + except Exception as e: + fail_count += 1 + results.append(f"表情包 {i+1} 注册时发生错误: {str(e)}") + + # 构建返回消息 + total_count = success_count + fail_count + summary_msg = f"表情包注册完成: 成功 {success_count} 个,失败 {fail_count} 个,共处理 {total_count} 个" + + # 如果有结果详情,添加到返回消息中 + if results: + details_msg = "\n" + "\n".join(results) + final_msg = summary_msg + details_msg + else: + final_msg = summary_msg + + return success_count > 0, final_msg, success_count > 0 + + def find_and_return_emoji_in_message(self, message_segments) -> List[str]: emoji_base64_list = [] + + # 处理单个Seg对象的情况 + if isinstance(message_segments, Seg): + if message_segments.type == "emoji": + emoji_base64_list.append(message_segments.data) + elif message_segments.type == "image": + # 假设图片数据是base64编码的 + emoji_base64_list.append(message_segments.data) + elif message_segments.type == "seglist": + # 递归处理嵌套的Seg列表 + emoji_base64_list.extend(self.find_and_return_emoji_in_message(message_segments.data)) + return emoji_base64_list + + # 处理Seg列表的情况 for seg in message_segments: if seg.type == "emoji": emoji_base64_list.append(seg.data) @@ -66,67 +132,6 @@ class ListEmojiCommand(BaseCommand): return True, f"显示了当前时间: {time_str}", True -class PrintMessage(BaseEventHandler): - """打印消息事件处理器 - 处理打印消息事件""" - - event_type = EventType.ON_MESSAGE - handler_name = "print_message_handler" - handler_description = "打印接收到的消息" - - async def execute(self, message: MaiMessages | None) -> Tuple[bool, bool, str | None, None, None]: - """执行打印消息事件处理""" - # 打印接收到的消息 - if self.get_config("print_message.enabled", False): - print(f"接收到消息: {message.raw_message if message else '无效消息'}") - return True, True, "消息已打印", None, None - - -class ForwardMessages(BaseEventHandler): - """ - 把接收到的消息转发到指定聊天ID - - 此组件是HYBRID消息和FORWARD消息的使用示例。 - 每收到10条消息,就会以1%的概率使用HYBRID消息转发,否则使用FORWARD消息转发。 - """ - - event_type = EventType.ON_MESSAGE - handler_name = "forward_messages_handler" - handler_description = "把接收到的消息转发到指定聊天ID" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.counter = 0 # 用于计数转发的消息数量 - self.messages: List[str] = [] - - async def execute(self, message: MaiMessages | None) -> Tuple[bool, bool, None, None, None]: - if not message: - return True, True, None, None, None - stream_id = message.stream_id or "" - - if message.plain_text: - self.messages.append(message.plain_text) - self.counter += 1 - if self.counter % 10 == 0: - if random.random() < 0.01: - success = await self.send_hybrid(stream_id, [(ReplyContentType.TEXT, msg) for msg in self.messages]) - else: - success = await self.send_forward( - stream_id, - [ - ( - str(global_config.bot.qq_account), - str(global_config.bot.nickname), - [(ReplyContentType.TEXT, msg)], - ) - for msg in self.messages - ], - ) - if not success: - raise ValueError("转发消息失败") - self.messages = [] - return True, True, None, None, None - - class RandomEmojis(BaseCommand): command_name = "random_emojis" command_description = "发送多张随机表情包" @@ -169,36 +174,13 @@ class EmojiManagePlugin(BasePlugin): # 配置Schema定义 config_schema: dict = { "plugin": { - "version": ConfigField(type=str, default="1.0.0", description="插件版本"), - "enabled": ConfigField(type=bool, default=False, description="是否启用插件"), + "enabled": ConfigField(type=bool, default=True, description="是否启用插件"), + "config_version": ConfigField(type=str, default="1.0.1", description="配置文件版本"), }, } def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: return [ - (PrintMessage.get_handler_info(), PrintMessage), - (ForwardMessages.get_handler_info(), ForwardMessages), (RandomEmojis.get_command_info(), RandomEmojis), - ] - - -# @register_plugin -# class HelloWorldEventPlugin(BaseEPlugin): -# """Hello World事件插件 - 处理问候和告别事件""" - -# plugin_name = "hello_world_event_plugin" -# enable_plugin = False -# dependencies = [] -# python_dependencies = [] -# config_file_name = "event_config.toml" - -# config_schema = { -# "plugin": { -# "name": ConfigField(type=str, default="hello_world_event_plugin", description="插件名称"), -# "version": ConfigField(type=str, default="1.0.0", description="插件版本"), -# "enabled": ConfigField(type=bool, default=True, description="是否启用插件"), -# }, -# } - -# def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: -# return [(PrintMessage.get_handler_info(), PrintMessage)] + (AddEmojiCommand.get_command_info(), AddEmojiCommand), + ] \ No newline at end of file diff --git a/plugins/hello_world_plugin/plugin.py b/plugins/hello_world_plugin/plugin.py index 020748a3..3965ca97 100644 --- a/plugins/hello_world_plugin/plugin.py +++ b/plugins/hello_world_plugin/plugin.py @@ -237,8 +237,7 @@ class HelloWorldPlugin(BasePlugin): # 配置Schema定义 config_schema: dict = { "plugin": { - "name": ConfigField(type=str, default="hello_world_plugin", description="插件名称"), - "version": ConfigField(type=str, default="1.0.0", description="插件版本"), + "config_version": ConfigField(type=str, default="1.0.0", description="配置文件版本"), "enabled": ConfigField(type=bool, default=False, description="是否启用插件"), }, "greeting": { diff --git a/src/chat/express/expression_learner.py b/src/chat/express/expression_learner.py index e36d4d57..968586c2 100644 --- a/src/chat/express/expression_learner.py +++ b/src/chat/express/expression_learner.py @@ -62,6 +62,7 @@ class ExpressionLearner: model_set=model_config.model_task_config.replyer, request_type="expression.learner" ) self.chat_id = chat_id + self.chat_stream = get_chat_manager().get_stream(chat_id) self.chat_name = get_chat_manager().get_stream_name(chat_id) or chat_id # 维护每个chat的上次学习时间 @@ -69,24 +70,8 @@ class ExpressionLearner: # 学习参数 self.min_messages_for_learning = 25 # 触发学习所需的最少消息数 - self.min_learning_interval = 300 # 最短学习时间间隔(秒) - - def can_learn_for_chat(self) -> bool: - """ - 检查指定聊天流是否允许学习表达 - - Args: - chat_id: 聊天流ID - - Returns: - bool: 是否允许学习 - """ - try: - use_expression, enable_learning, _ = global_config.expression.get_expression_config_for_chat(self.chat_id) - return enable_learning - except Exception as e: - logger.error(f"检查学习权限失败: {e}") - return False + _, self.enable_learning, self.learning_intensity = global_config.expression.get_expression_config_for_chat(self.chat_id) + self.min_learning_interval = 300 / self.learning_intensity def should_trigger_learning(self) -> bool: """ @@ -98,27 +83,13 @@ class ExpressionLearner: Returns: bool: 是否应该触发学习 """ - current_time = time.time() - - # 获取该聊天流的学习强度 - try: - _, enable_learning, learning_intensity = global_config.expression.get_expression_config_for_chat( - self.chat_id - ) - except Exception as e: - logger.error(f"获取聊天流 {self.chat_id} 的学习配置失败: {e}") - return False - # 检查是否允许学习 - if not enable_learning: + if not self.enable_learning: return False - # 根据学习强度计算最短学习时间间隔 - min_interval = self.min_learning_interval / learning_intensity - # 检查时间间隔 - time_diff = current_time - self.last_learning_time - if time_diff < min_interval: + time_diff = time.time() - self.last_learning_time + if time_diff < self.min_learning_interval: return False # 检查消息数量(只检查指定聊天流的消息) @@ -228,32 +199,17 @@ class ExpressionLearner: """ 学习并存储表达方式 """ - # 检查是否允许在此聊天流中学习(在函数最前面检查) - if not self.can_learn_for_chat(): - logger.debug(f"聊天流 {self.chat_name} 不允许学习表达,跳过学习") - return [] - res = await self.learn_expression(num) if res is None: + logger.info("没有学习到表达风格") return [] learnt_expressions, chat_id = res - - chat_stream = get_chat_manager().get_stream(chat_id) - if chat_stream is None: - group_name = f"聊天流 {chat_id}" - elif chat_stream.group_info: - group_name = chat_stream.group_info.group_name - else: - group_name = f"{chat_stream.user_info.user_nickname}的私聊" learnt_expressions_str = "" for _chat_id, situation, style in learnt_expressions: learnt_expressions_str += f"{situation}->{style}\n" - logger.info(f"在 {group_name} 学习到表达风格:\n{learnt_expressions_str}") - - if not learnt_expressions: - logger.info("没有学习到表达风格") - return [] + + logger.info(f"在 {self.chat_name} 学习到表达风格:\n{learnt_expressions_str}") # 按chat_id分组 chat_dict: Dict[str, List[Dict[str, Any]]] = {} @@ -316,7 +272,7 @@ class ExpressionLearner: current_time = time.time() - # 获取上次学习时间 + # 获取上次学习之后的消息 random_msg = get_raw_msg_by_timestamp_with_chat_inclusive( chat_id=self.chat_id, timestamp_start=self.last_learning_time, @@ -330,14 +286,15 @@ class ExpressionLearner: chat_id: str = random_msg[0].chat_id # random_msg_str: str = build_readable_messages(random_msg, timestamp_mode="normal") random_msg_str: str = await build_anonymous_messages(random_msg) - # print(f"random_msg_str:{random_msg_str}") + prompt: str = await global_prompt_manager.format_prompt( prompt, chat_str=random_msg_str, ) - logger.debug(f"学习{type_str}的prompt: {prompt}") + print(f"random_msg_str:{random_msg_str}") + logger.info(f"学习{type_str}的prompt: {prompt}") try: response, _ = await self.express_learn_model.generate_response_async(prompt, temperature=0.3) diff --git a/src/chat/heart_flow/heartFC_chat.py b/src/chat/heart_flow/heartFC_chat.py index 778e0c3d..56d63fb4 100644 --- a/src/chat/heart_flow/heartFC_chat.py +++ b/src/chat/heart_flow/heartFC_chat.py @@ -344,6 +344,10 @@ class HeartFChatting: available_actions=available_actions, ) ) + + logger.info( + f"{self.log_prefix}决定执行{len(action_to_use_info)}个动作: {' '.join([a.action_type for a in action_to_use_info])}" + ) # 3. 并行执行所有动作 action_tasks = [ diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index ed59207a..46967075 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -502,8 +502,8 @@ class ActionPlanner: action.action_data = action.action_data or {} action.action_data["loop_start_time"] = loop_start_time - logger.info( - f"{self.log_prefix}规划器决定执行{len(actions)}个动作: {' '.join([a.action_type for a in actions])}" + logger.debug( + f"{self.log_prefix}规划器选择了{len(actions)}个动作: {' '.join([a.action_type for a in actions])}" ) return actions diff --git a/src/plugins/built_in/emoji_plugin/emoji.py b/src/plugins/built_in/emoji_plugin/emoji.py index da620d22..63a9cc51 100644 --- a/src/plugins/built_in/emoji_plugin/emoji.py +++ b/src/plugins/built_in/emoji_plugin/emoji.py @@ -46,8 +46,9 @@ class EmojiAction(BaseAction): """执行表情动作""" try: # 1. 获取发送表情的原因 - reason = self.action_data.get("reason", "表达当前情绪") - + # reason = self.action_data.get("reason", "表达当前情绪") + reason = self.reasoning + # 2. 随机获取20个表情包 sampled_emojis = await emoji_api.get_random(30) if not sampled_emojis: @@ -62,6 +63,9 @@ class EmojiAction(BaseAction): emotion_map[emo].append((b64, desc)) available_emotions = list(emotion_map.keys()) + available_emotions_str = "" + for emotion in available_emotions: + available_emotions_str += f"{emotion}\n" if not available_emotions: logger.warning(f"{self.log_prefix} 获取到的表情包均无情感标签, 将随机发送") @@ -80,14 +84,15 @@ class EmojiAction(BaseAction): ) # 4. 构建prompt让LLM选择情感 - prompt = f""" - 你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个情感标签列表中选择最匹配的一个。 - 这是最近的聊天记录: - {messages_text} - - 这是理由:“{reason}” - 这里是可用的情感标签:{available_emotions} - 请直接返回最匹配的那个情感标签,不要进行任何解释或添加其他多余的文字。 + prompt = f"""你正在进行QQ聊天,你需要根据聊天记录,选出一个合适的情感标签。 +请你根据以下原因和聊天记录进行选择 +原因:{reason} +聊天记录: +{messages_text} + +这里是可用的情感标签: +{available_emotions_str} +请直接返回最匹配的那个情感标签,不要进行任何解释或添加其他多余的文字。 """ if global_config.debug.show_prompt: @@ -97,10 +102,10 @@ class EmojiAction(BaseAction): # 5. 调用LLM models = llm_api.get_available_models() - chat_model_config = models.get("utils_small") # 使用字典访问方式 + chat_model_config = models.get("replyer") # 使用字典访问方式 if not chat_model_config: - logger.error(f"{self.log_prefix} 未找到'utils_small'模型配置,无法调用LLM") - return False, "未找到'utils_small'模型配置" + logger.error(f"{self.log_prefix} 未找到'replyer'模型配置,无法调用LLM") + return False, "未找到'replyer'模型配置" success, chosen_emotion, _, _ = await llm_api.generate_with_model( prompt, model_config=chat_model_config, request_type="emoji" From a2bbbf263c216bb54726f98043cd4b5e4374d9f8 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Wed, 24 Sep 2025 19:52:55 +0800 Subject: [PATCH 23/26] fix filter --- src/chat/message_receive/bot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/chat/message_receive/bot.py b/src/chat/message_receive/bot.py index f611bbd2..d963779b 100644 --- a/src/chat/message_receive/bot.py +++ b/src/chat/message_receive/bot.py @@ -251,6 +251,9 @@ class ChatBot: # return pass + # 处理消息内容,生成纯文本 + await message.process() + # 过滤检查 if _check_ban_words( message.processed_plain_text, @@ -273,9 +276,6 @@ class ChatBot: message.update_chat_stream(chat) - # 处理消息内容,生成纯文本 - await message.process() - # if await self.check_ban_content(message): # logger.warning(f"检测到消息中含有违法,色情,暴力,反动,敏感内容,消息内容:{message.processed_plain_text},发送者:{message.message_info.user_info.user_nickname}") # return From 1c37dd46e86e86914342760767485caed4a773a8 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 24 Sep 2025 18:14:23 +0800 Subject: [PATCH 24/26] =?UTF-8?q?better=EF=BC=9A=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E8=A1=A8=E6=83=85=E5=8C=85=E7=AE=A1=E7=90=86=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复一些问题 Update emoji_api.py --- plugins/emoji_manage_plugin/plugin.py | 236 ++++++++++++++++++++++++- src/plugin_system/apis/emoji_api.py | 241 +++++++++++++++++++++++++- 2 files changed, 464 insertions(+), 13 deletions(-) diff --git a/plugins/emoji_manage_plugin/plugin.py b/plugins/emoji_manage_plugin/plugin.py index b042cabb..893d14b7 100644 --- a/plugins/emoji_manage_plugin/plugin.py +++ b/plugins/emoji_manage_plugin/plugin.py @@ -28,7 +28,7 @@ class AddEmojiCommand(BaseCommand): async def execute(self) -> Tuple[bool, str, bool]: # 查找消息中的表情包 - logger.info(f"查找消息中的表情包: {self.message.message_segment}") + # logger.info(f"查找消息中的表情包: {self.message.message_segment}") emoji_base64_list = self.find_and_return_emoji_in_message(self.message.message_segment) @@ -42,8 +42,8 @@ class AddEmojiCommand(BaseCommand): for i, emoji_base64 in enumerate(emoji_base64_list): try: - # 使用emoji_api注册表情包 - result = await emoji_api.register_emoji(emoji_base64, filename=f"emoji_{i+1}") + # 使用emoji_api注册表情包(让API自动生成唯一文件名) + result = await emoji_api.register_emoji(emoji_base64) if result["success"]: success_count += 1 @@ -72,13 +72,46 @@ class AddEmojiCommand(BaseCommand): summary_msg = f"表情包注册完成: 成功 {success_count} 个,失败 {fail_count} 个,共处理 {total_count} 个" # 如果有结果详情,添加到返回消息中 + details_msg = "" if results: details_msg = "\n" + "\n".join(results) final_msg = summary_msg + details_msg else: final_msg = summary_msg - return success_count > 0, final_msg, success_count > 0 + # 使用表达器重写回复 + try: + from src.plugin_system.apis import generator_api + + # 构建重写数据 + rewrite_data = { + "raw_reply": summary_msg, + "reason": f"注册了表情包:{details_msg}\n", + } + + # 调用表达器重写 + result_status, data = await generator_api.rewrite_reply( + chat_stream=self.message.chat_stream, + reply_data=rewrite_data, + ) + + if result_status: + # 发送重写后的回复 + for reply_seg in data.reply_set.reply_data: + send_data = reply_seg.content + await self.send_text(send_data) + + return success_count > 0, final_msg, success_count > 0 + else: + # 如果重写失败,发送原始消息 + await self.send_text(final_msg) + return success_count > 0, final_msg, success_count > 0 + + except Exception as e: + # 如果表达器调用失败,发送原始消息 + logger.error(f"[add_emoji] 表达器重写失败: {e}") + await self.send_text(final_msg) + return success_count > 0, final_msg, success_count > 0 def find_and_return_emoji_in_message(self, message_segments) -> List[str]: emoji_base64_list = [] @@ -114,22 +147,205 @@ class ListEmojiCommand(BaseCommand): command_description = "列表表情包" # === 命令设置(必须填写)=== - command_pattern = r"^/emoji list$" # 精确匹配 "/emoji list" 命令 + command_pattern = r"^/emoji list(\s+\d+)?$" # 匹配 "/emoji list" 或 "/emoji list 数量" async def execute(self) -> Tuple[bool, str, bool]: """执行列表表情包""" + from src.plugin_system.apis import emoji_api import datetime + # 解析命令参数 + import re + match = re.match(r"^/emoji list(?:\s+(\d+))?$", self.message.raw_message) + max_count = 10 # 默认显示10个 + if match and match.group(1): + max_count = min(int(match.group(1)), 50) # 最多显示50个 + # 获取当前时间 time_format: str = self.get_config("time.format", "%Y-%m-%d %H:%M:%S") # type: ignore now = datetime.datetime.now() time_str = now.strftime(time_format) - # 发送时间信息 - message = f"⏰ 当前时间:{time_str}" - await self.send_text(message) + # 获取表情包信息 + emoji_count = emoji_api.get_count() + emoji_info = emoji_api.get_info() - return True, f"显示了当前时间: {time_str}", True + # 构建返回消息 + message_lines = [ + f"📊 表情包统计信息 ({time_str})", + f"• 总数: {emoji_count} / {emoji_info['max_count']}", + f"• 可用: {emoji_info['available_emojis']}", + ] + + if emoji_count == 0: + message_lines.append("\n❌ 暂无表情包") + final_message = "\n".join(message_lines) + await self.send_text(final_message) + return True, final_message, True + + # 获取所有表情包 + all_emojis = await emoji_api.get_all() + if not all_emojis: + message_lines.append("\n❌ 无法获取表情包列表") + final_message = "\n".join(message_lines) + await self.send_text(final_message) + return False, final_message, True + + # 显示前N个表情包 + display_emojis = all_emojis[:max_count] + message_lines.append(f"\n📋 显示前 {len(display_emojis)} 个表情包:") + + for i, (emoji_base64, description, emotion) in enumerate(display_emojis, 1): + # 截断过长的描述 + short_desc = description[:50] + "..." if len(description) > 50 else description + message_lines.append(f"{i}. {short_desc} [{emotion}]") + + # 如果还有更多表情包,显示总数 + if len(all_emojis) > max_count: + message_lines.append(f"\n💡 还有 {len(all_emojis) - max_count} 个表情包未显示") + + final_message = "\n".join(message_lines) + + # 直接发送文本消息 + await self.send_text(final_message) + + return True, final_message, True + + +class DeleteEmojiCommand(BaseCommand): + command_name = "delete_emoji" + command_description = "删除表情包" + command_pattern = r".*/emoji delete.*" + + async def execute(self) -> Tuple[bool, str, bool]: + # 查找消息中的表情包图片 + logger.info(f"查找消息中的表情包用于删除: {self.message.message_segment}") + + emoji_base64_list = self.find_and_return_emoji_in_message(self.message.message_segment) + + if not emoji_base64_list: + return False, "未在消息中找到表情包或图片", False + + # 删除找到的表情包 + success_count = 0 + fail_count = 0 + results = [] + + for i, emoji_base64 in enumerate(emoji_base64_list): + try: + # 计算图片的哈希值来查找对应的表情包 + import base64 + import hashlib + + # 确保base64字符串只包含ASCII字符 + if isinstance(emoji_base64, str): + emoji_base64_clean = emoji_base64.encode("ascii", errors="ignore").decode("ascii") + else: + emoji_base64_clean = str(emoji_base64) + + # 计算哈希值 + image_bytes = base64.b64decode(emoji_base64_clean) + emoji_hash = hashlib.md5(image_bytes).hexdigest() + + # 使用emoji_api删除表情包 + result = await emoji_api.delete_emoji(emoji_hash) + + if result["success"]: + success_count += 1 + description = result.get("description", "未知描述") + count_before = result.get("count_before", 0) + count_after = result.get("count_after", 0) + emotions = result.get("emotions", []) + + result_msg = f"表情包 {i+1} 删除成功" + if description: + result_msg += f"\n描述: {description}" + if emotions: + result_msg += f"\n情感标签: {', '.join(emotions)}" + result_msg += f"\n表情包数量: {count_before} → {count_after}" + + results.append(result_msg) + else: + fail_count += 1 + error_msg = result.get("message", "删除失败") + results.append(f"表情包 {i+1} 删除失败: {error_msg}") + + except Exception as e: + fail_count += 1 + results.append(f"表情包 {i+1} 删除时发生错误: {str(e)}") + + # 构建返回消息 + total_count = success_count + fail_count + summary_msg = f"表情包删除完成: 成功 {success_count} 个,失败 {fail_count} 个,共处理 {total_count} 个" + + # 如果有结果详情,添加到返回消息中 + details_msg = "" + if results: + details_msg = "\n" + "\n".join(results) + final_msg = summary_msg + details_msg + else: + final_msg = summary_msg + + # 使用表达器重写回复 + try: + from src.plugin_system.apis import generator_api + + # 构建重写数据 + rewrite_data = { + "raw_reply": summary_msg, + "reason": f"删除了表情包:{details_msg}\n", + } + + # 调用表达器重写 + result_status, data = await generator_api.rewrite_reply( + chat_stream=self.message.chat_stream, + reply_data=rewrite_data, + ) + + if result_status: + # 发送重写后的回复 + for reply_seg in data.reply_set.reply_data: + send_data = reply_seg.content + await self.send_text(send_data) + + return success_count > 0, final_msg, success_count > 0 + else: + # 如果重写失败,发送原始消息 + await self.send_text(final_msg) + return success_count > 0, final_msg, success_count > 0 + + except Exception as e: + # 如果表达器调用失败,发送原始消息 + logger.error(f"[delete_emoji] 表达器重写失败: {e}") + await self.send_text(final_msg) + return success_count > 0, final_msg, success_count > 0 + + def find_and_return_emoji_in_message(self, message_segments) -> List[str]: + emoji_base64_list = [] + + # 处理单个Seg对象的情况 + if isinstance(message_segments, Seg): + if message_segments.type == "emoji": + emoji_base64_list.append(message_segments.data) + elif message_segments.type == "image": + # 假设图片数据是base64编码的 + emoji_base64_list.append(message_segments.data) + elif message_segments.type == "seglist": + # 递归处理嵌套的Seg列表 + emoji_base64_list.extend(self.find_and_return_emoji_in_message(message_segments.data)) + return emoji_base64_list + + # 处理Seg列表的情况 + for seg in message_segments: + if seg.type == "emoji": + emoji_base64_list.append(seg.data) + elif seg.type == "image": + # 假设图片数据是base64编码的 + emoji_base64_list.append(seg.data) + elif seg.type == "seglist": + # 递归处理嵌套的Seg列表 + emoji_base64_list.extend(self.find_and_return_emoji_in_message(seg.data)) + return emoji_base64_list class RandomEmojis(BaseCommand): @@ -183,4 +399,6 @@ class EmojiManagePlugin(BasePlugin): return [ (RandomEmojis.get_command_info(), RandomEmojis), (AddEmojiCommand.get_command_info(), AddEmojiCommand), + (ListEmojiCommand.get_command_info(), ListEmojiCommand), + (DeleteEmojiCommand.get_command_info(), DeleteEmojiCommand), ] \ No newline at end of file diff --git a/src/plugin_system/apis/emoji_api.py b/src/plugin_system/apis/emoji_api.py index d127aa6d..6d142a9e 100644 --- a/src/plugin_system/apis/emoji_api.py +++ b/src/plugin_system/apis/emoji_api.py @@ -366,17 +366,72 @@ async def register_emoji(image_base64: str, filename: Optional[str] = None) -> D # 4. 生成文件名 if not filename: - # 基于时间戳和UUID生成唯一文件名 + # 基于时间戳、微秒和短base64生成唯一文件名 + import time timestamp = int(time.time()) - unique_id = str(uuid.uuid4())[:8] - filename = f"emoji_{timestamp}_{unique_id}" + microseconds = int(time.time() * 1000000) % 1000000 # 添加微秒级精度 + + # 生成12位随机标识符,使用base64编码(增加随机性) + import random + random_bytes = random.getrandbits(72).to_bytes(9, 'big') # 72位 = 9字节 = 12位base64 + short_id = base64.b64encode(random_bytes).decode('ascii')[:12].rstrip('=') + # 确保base64编码适合文件名(替换/和-) + short_id = short_id.replace('/', '_').replace('+', '-') + filename = f"emoji_{timestamp}_{microseconds}_{short_id}" # 确保文件名有扩展名 if not filename.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')): filename = f"{filename}.png" # 默认使用png格式 - # 5. 保存base64图片到emoji目录 + # 检查文件名是否已存在,如果存在则重新生成短标识符 temp_file_path = os.path.join(EMOJI_DIR, filename) + attempts = 0 + max_attempts = 10 + while os.path.exists(temp_file_path) and attempts < max_attempts: + # 重新生成短标识符 + import random + random_bytes = random.getrandbits(48).to_bytes(6, 'big') + short_id = base64.b64encode(random_bytes).decode('ascii')[:8].rstrip('=') + short_id = short_id.replace('/', '_').replace('+', '-') + + # 分离文件名和扩展名,重新生成文件名 + name_part, ext = os.path.splitext(filename) + # 去掉原来的标识符,添加新的 + base_name = name_part.rsplit('_', 1)[0] # 移除最后一个_后的部分 + filename = f"{base_name}_{short_id}{ext}" + temp_file_path = os.path.join(EMOJI_DIR, filename) + attempts += 1 + + # 如果还是冲突,使用UUID作为备用方案 + if os.path.exists(temp_file_path): + uuid_short = str(uuid.uuid4())[:8] + name_part, ext = os.path.splitext(filename) + base_name = name_part.rsplit('_', 1)[0] + filename = f"{base_name}_{uuid_short}{ext}" + temp_file_path = os.path.join(EMOJI_DIR, filename) + + # 如果UUID方案也冲突,添加序号 + counter = 1 + original_filename = filename + while os.path.exists(temp_file_path): + name_part, ext = os.path.splitext(original_filename) + filename = f"{name_part}_{counter}{ext}" + temp_file_path = os.path.join(EMOJI_DIR, filename) + counter += 1 + + # 防止无限循环,最多尝试100次 + if counter > 100: + logger.error(f"[EmojiAPI] 无法生成唯一文件名,尝试次数过多: {original_filename}") + return { + "success": False, + "message": "无法生成唯一文件名,请稍后重试", + "description": None, + "emotions": None, + "replaced": None, + "hash": None + } + + # 5. 保存base64图片到emoji目录 try: # 解码base64并保存图片 @@ -468,3 +523,181 @@ async def register_emoji(image_base64: str, filename: Optional[str] = None) -> D "replaced": None, "hash": None } + + +# ============================================================================= +# 表情包删除API函数 +# ============================================================================= + + +async def delete_emoji(emoji_hash: str) -> Dict[str, Any]: + """删除表情包 + + Args: + emoji_hash: 要删除的表情包的哈希值 + + Returns: + Dict[str, Any]: 删除结果,包含以下字段: + - success: bool, 是否成功删除 + - message: str, 结果消息 + - count_before: Optional[int], 删除前的表情包数量 + - count_after: Optional[int], 删除后的表情包数量 + - description: Optional[str], 被删除的表情包描述(成功时) + - emotions: Optional[List[str]], 被删除的表情包情感标签(成功时) + + Raises: + ValueError: 如果哈希值为空 + TypeError: 如果哈希值不是字符串类型 + """ + if not emoji_hash: + raise ValueError("表情包哈希值不能为空") + if not isinstance(emoji_hash, str): + raise TypeError("emoji_hash必须是字符串类型") + + try: + logger.info(f"[EmojiAPI] 开始删除表情包,哈希值: {emoji_hash}") + + # 1. 获取emoji管理器和删除前的数量 + emoji_manager = get_emoji_manager() + count_before = emoji_manager.emoji_num + + # 2. 获取被删除表情包的信息(用于返回结果) + try: + deleted_emoji = await emoji_manager.get_emoji_from_manager(emoji_hash) + description = deleted_emoji.description if deleted_emoji else None + emotions = deleted_emoji.emotion if deleted_emoji else None + except Exception as info_error: + logger.warning(f"[EmojiAPI] 获取被删除表情包信息失败: {info_error}") + description = None + emotions = None + + # 3. 执行删除操作 + delete_success = await emoji_manager.delete_emoji(emoji_hash) + + # 4. 获取删除后的数量 + count_after = emoji_manager.emoji_num + + # 5. 构建返回结果 + if delete_success: + return { + "success": True, + "message": f"表情包删除成功 (哈希: {emoji_hash[:8]}...)", + "count_before": count_before, + "count_after": count_after, + "description": description, + "emotions": emotions + } + else: + return { + "success": False, + "message": f"表情包删除失败,可能因为哈希值不存在或删除过程出错", + "count_before": count_before, + "count_after": count_after, + "description": None, + "emotions": None + } + + except Exception as e: + logger.error(f"[EmojiAPI] 删除表情包时发生异常: {e}") + return { + "success": False, + "message": f"删除过程中发生错误: {str(e)}", + "count_before": None, + "count_after": None, + "description": None, + "emotions": None + } + + +async def delete_emoji_by_description(description: str, exact_match: bool = False) -> Dict[str, Any]: + """根据描述删除表情包 + + Args: + description: 表情包描述文本 + exact_match: 是否精确匹配描述,False则为模糊匹配 + + Returns: + Dict[str, Any]: 删除结果,包含以下字段: + - success: bool, 是否成功删除 + - message: str, 结果消息 + - deleted_count: int, 删除的表情包数量 + - deleted_hashes: List[str], 被删除的表情包哈希列表 + - matched_count: int, 匹配到的表情包数量 + + Raises: + ValueError: 如果描述为空 + TypeError: 如果描述不是字符串类型 + """ + if not description: + raise ValueError("描述不能为空") + if not isinstance(description, str): + raise TypeError("description必须是字符串类型") + + try: + logger.info(f"[EmojiAPI] 根据描述删除表情包: {description} (精确匹配: {exact_match})") + + emoji_manager = get_emoji_manager() + all_emojis = emoji_manager.emoji_objects + + # 筛选匹配的表情包 + matching_emojis = [] + for emoji_obj in all_emojis: + if emoji_obj.is_deleted: + continue + + if exact_match: + if emoji_obj.description == description: + matching_emojis.append(emoji_obj) + else: + if description.lower() in emoji_obj.description.lower(): + matching_emojis.append(emoji_obj) + + matched_count = len(matching_emojis) + if matched_count == 0: + return { + "success": False, + "message": f"未找到匹配描述 '{description}' 的表情包", + "deleted_count": 0, + "deleted_hashes": [], + "matched_count": 0 + } + + # 删除匹配的表情包 + deleted_count = 0 + deleted_hashes = [] + for emoji_obj in matching_emojis: + try: + delete_success = await emoji_manager.delete_emoji(emoji_obj.hash) + if delete_success: + deleted_count += 1 + deleted_hashes.append(emoji_obj.hash) + except Exception as delete_error: + logger.error(f"[EmojiAPI] 删除表情包失败 (哈希: {emoji_obj.hash}): {delete_error}") + + # 构建返回结果 + if deleted_count > 0: + return { + "success": True, + "message": f"成功删除 {deleted_count} 个表情包 (匹配到 {matched_count} 个)", + "deleted_count": deleted_count, + "deleted_hashes": deleted_hashes, + "matched_count": matched_count + } + else: + return { + "success": False, + "message": f"匹配到 {matched_count} 个表情包,但删除全部失败", + "deleted_count": 0, + "deleted_hashes": [], + "matched_count": matched_count + } + + except Exception as e: + logger.error(f"[EmojiAPI] 根据描述删除表情包时发生异常: {e}") + return { + "success": False, + "message": f"删除过程中发生错误: {str(e)}", + "deleted_count": 0, + "deleted_hashes": [], + "matched_count": 0 + } From 8ee3586a0fadcc9e63ae33ad4738550489b8ff1a Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Wed, 24 Sep 2025 20:32:10 +0800 Subject: [PATCH 25/26] =?UTF-8?q?=E7=B2=BE=E7=AE=80requirements.txt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/requirements.txt b/requirements.txt index d4dd2339..81af265e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,7 @@ -APScheduler Pillow aiohttp aiohttp-cors colorama -customtkinter dotenv faiss-cpu fastapi @@ -11,20 +9,14 @@ jieba jsonlines maim_message quick_algo -matplotlib networkx numpy openai -pandas peewee -pyarrow pydantic pypinyin -python-dateutil python-dotenv -python-igraph pymongo -requests ruff scipy setuptools @@ -32,7 +24,6 @@ toml tomli tomli_w tomlkit -tqdm urllib3 uvicorn websockets @@ -40,10 +31,6 @@ strawberry-graphql[fastapi] packaging rich psutil -cryptography json-repair -reportportal-client -scikit-learn -seaborn structlog google.genai From 1ef49380804f0e03fcae808c3f852bfe7274be3a Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 24 Sep 2025 20:50:26 +0800 Subject: [PATCH 26/26] =?UTF-8?q?=E6=9B=B4=E6=96=B0maimessage=E7=89=88?= =?UTF-8?q?=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.lock | 131 ++++++++++++++++------------------------------ requirements.txt | 2 +- 2 files changed, 47 insertions(+), 86 deletions(-) diff --git a/requirements.lock b/requirements.lock index 4eea567b..5e0c5dbe 100644 --- a/requirements.lock +++ b/requirements.lock @@ -1,34 +1,36 @@ # This file was autogenerated by uv via the following command: # uv pip compile requirements.txt -o requirements.lock -aenum==3.1.16 - # via reportportal-client aiohappyeyeballs==2.6.1 # via aiohttp aiohttp==3.12.14 # via # -r requirements.txt + # aiohttp-cors # maim-message - # reportportal-client +aiohttp-cors==0.8.1 + # via -r requirements.txt aiosignal==1.4.0 # via aiohttp annotated-types==0.7.0 # via pydantic anyio==4.9.0 # via + # google-genai # httpx # openai # starlette -apscheduler==3.11.0 - # via -r requirements.txt +async-timeout==5.0.1 + # via aiohttp attrs==25.3.0 # via # aiohttp # jsonlines +cachetools==5.5.2 + # via google-auth certifi==2025.7.9 # via # httpcore # httpx - # reportportal-client # requests cffi==1.17.1 # via cryptography @@ -41,24 +43,16 @@ colorama==0.4.6 # -r requirements.txt # click # tqdm -contourpy==1.3.2 - # via matplotlib cryptography==45.0.5 - # via - # -r requirements.txt - # maim-message -customtkinter==5.2.2 - # via -r requirements.txt -cycler==0.12.1 - # via matplotlib -darkdetect==0.8.0 - # via customtkinter + # via maim-message distro==1.9.0 # via openai dnspython==2.7.0 # via pymongo dotenv==0.9.9 # via -r requirements.txt +exceptiongroup==1.3.0 + # via anyio faiss-cpu==1.11.0 # via -r requirements.txt fastapi==0.116.0 @@ -66,12 +60,14 @@ fastapi==0.116.0 # -r requirements.txt # maim-message # strawberry-graphql -fonttools==4.58.5 - # via matplotlib frozenlist==1.7.0 # via # aiohttp # aiosignal +google-auth==2.40.3 + # via google-genai +google-genai==1.38.0 + # via -r requirements.txt graphql-core==3.2.6 # via strawberry-graphql h11==0.16.0 @@ -81,86 +77,70 @@ h11==0.16.0 httpcore==1.0.9 # via httpx httpx==0.28.1 - # via openai + # via + # google-genai + # openai idna==3.10 # via # anyio # httpx # requests # yarl -igraph==0.11.9 - # via python-igraph jieba==0.42.1 # via -r requirements.txt jiter==0.10.0 # via openai -joblib==1.5.1 - # via scikit-learn json-repair==0.47.6 # via -r requirements.txt jsonlines==4.0.0 # via -r requirements.txt -kiwisolver==1.4.8 - # via matplotlib -maim-message==0.3.8 +maim-message==0.5.1 # via -r requirements.txt markdown-it-py==3.0.0 # via rich -matplotlib==3.10.3 - # via - # -r requirements.txt - # seaborn mdurl==0.1.2 # via markdown-it-py multidict==6.6.3 # via # aiohttp # yarl -networkx==3.5 +networkx==3.4.2 # via -r requirements.txt -numpy==2.3.1 +numpy==2.2.6 # via # -r requirements.txt - # contourpy # faiss-cpu - # matplotlib - # pandas - # scikit-learn # scipy - # seaborn openai==1.95.0 # via -r requirements.txt packaging==25.0 # via # -r requirements.txt - # customtkinter # faiss-cpu - # matplotlib # strawberry-graphql -pandas==2.3.1 - # via - # -r requirements.txt - # seaborn peewee==3.18.2 # via -r requirements.txt pillow==11.3.0 - # via - # -r requirements.txt - # matplotlib + # via -r requirements.txt propcache==0.3.2 # via # aiohttp # yarl psutil==7.0.0 # via -r requirements.txt -pyarrow==20.0.0 - # via -r requirements.txt +pyasn1==0.6.1 + # via + # pyasn1-modules + # rsa +pyasn1-modules==0.4.2 + # via google-auth pycparser==2.22 # via cffi pydantic==2.11.7 # via # -r requirements.txt # fastapi + # google-genai # maim-message # openai pydantic-core==2.33.2 @@ -169,45 +149,27 @@ pygments==2.19.2 # via rich pymongo==4.13.2 # via -r requirements.txt -pyparsing==3.2.3 - # via matplotlib pypinyin==0.54.0 # via -r requirements.txt python-dateutil==2.9.0.post0 - # via - # -r requirements.txt - # matplotlib - # pandas - # strawberry-graphql + # via strawberry-graphql python-dotenv==1.1.1 # via # -r requirements.txt # dotenv -python-igraph==0.11.9 - # via -r requirements.txt python-multipart==0.0.20 # via strawberry-graphql -pytz==2025.2 - # via pandas quick-algo==0.1.3 # via -r requirements.txt -reportportal-client==5.6.5 - # via -r requirements.txt requests==2.32.4 - # via - # -r requirements.txt - # reportportal-client + # via google-genai rich==14.0.0 # via -r requirements.txt +rsa==4.9.1 + # via google-auth ruff==0.12.2 # via -r requirements.txt -scikit-learn==1.7.0 - # via -r requirements.txt -scipy==1.16.0 - # via - # -r requirements.txt - # scikit-learn -seaborn==0.13.2 +scipy==1.15.3 # via -r requirements.txt setuptools==80.9.0 # via -r requirements.txt @@ -223,10 +185,8 @@ strawberry-graphql==0.275.5 # via -r requirements.txt structlog==25.4.0 # via -r requirements.txt -texttable==1.7.0 - # via igraph -threadpoolctl==3.6.0 - # via scikit-learn +tenacity==9.1.2 + # via google-genai toml==0.10.2 # via -r requirements.txt tomli==2.2.1 @@ -236,25 +196,25 @@ tomli-w==1.2.0 tomlkit==0.13.3 # via -r requirements.txt tqdm==4.67.1 - # via - # -r requirements.txt - # openai + # via openai typing-extensions==4.14.1 # via + # aiosignal + # anyio + # exceptiongroup # fastapi + # google-genai + # multidict # openai # pydantic # pydantic-core + # rich # strawberry-graphql + # structlog # typing-inspection + # uvicorn typing-inspection==0.4.1 # via pydantic -tzdata==2025.2 - # via - # pandas - # tzlocal -tzlocal==5.3.1 - # via apscheduler urllib3==2.5.0 # via # -r requirements.txt @@ -266,6 +226,7 @@ uvicorn==0.35.0 websockets==15.0.1 # via # -r requirements.txt + # google-genai # maim-message yarl==1.20.1 # via aiohttp diff --git a/requirements.txt b/requirements.txt index 81af265e..e4645892 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ faiss-cpu fastapi jieba jsonlines -maim_message +maim-message>=0 quick_algo networkx numpy