From 6b11b6ef29dcc0089a86bfea17baf8918964383d Mon Sep 17 00:00:00 2001 From: tcmofashi Date: Tue, 11 Mar 2025 20:41:03 +0800 Subject: [PATCH 1/8] =?UTF-8?q?fix:=20=E4=B8=80=E4=BA=9Bfrom=20disct?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/message_base.py | 8 +++++--- src/plugins/chat/message_cq.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/plugins/chat/message_base.py b/src/plugins/chat/message_base.py index d17c2c357..334859e5b 100644 --- a/src/plugins/chat/message_base.py +++ b/src/plugins/chat/message_base.py @@ -65,6 +65,8 @@ class GroupInfo: Returns: GroupInfo: 新的实例 """ + if data.get('group_id') is None: + return None return cls( platform=data.get('platform'), group_id=data.get('group_id'), @@ -129,8 +131,8 @@ class BaseMessageInfo: Returns: BaseMessageInfo: 新的实例 """ - group_info = GroupInfo(**data.get('group_info', {})) - user_info = UserInfo(**data.get('user_info', {})) + group_info = GroupInfo.from_dict(data.get('group_info', {})) + user_info = UserInfo.from_dict(data.get('user_info', {})) return cls( platform=data.get('platform'), message_id=data.get('message_id'), @@ -173,7 +175,7 @@ class MessageBase: Returns: MessageBase: 新的实例 """ - message_info = BaseMessageInfo(**data.get('message_info', {})) + message_info = BaseMessageInfo.from_dict(data.get('message_info', {})) message_segment = Seg(**data.get('message_segment', {})) raw_message = data.get('raw_message',None) return cls( diff --git a/src/plugins/chat/message_cq.py b/src/plugins/chat/message_cq.py index 6bfa47c3f..aa82d8822 100644 --- a/src/plugins/chat/message_cq.py +++ b/src/plugins/chat/message_cq.py @@ -62,7 +62,7 @@ class MessageRecvCQ(MessageCQ): # 调用父类初始化 super().__init__(message_id, user_info, group_info, platform) - if group_info.group_name is None: + if group_info and group_info.group_name is None: group_info.group_name = get_groupname(group_info.group_id) # 解析消息段 From 588aecd0f39ea14254a6a5a1e436675318719b4b Mon Sep 17 00:00:00 2001 From: tcmofashi Date: Wed, 12 Mar 2025 21:30:38 +0800 Subject: [PATCH 2/8] =?UTF-8?q?fix:=20=E4=B8=BA=E6=B2=A1=E6=9C=89hash?= =?UTF-8?q?=E7=9A=84=E8=A1=A8=E6=83=85=E5=8C=85=E6=B7=BB=E5=8A=A0hash?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8Dset=20reply?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/emoji_manager.py | 172 ++++++++++++------------- src/plugins/chat/message.py | 83 +++++------- src/plugins/chat/utils_image.py | 206 ++++++++++++++---------------- 3 files changed, 208 insertions(+), 253 deletions(-) diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py index 1c8a07699..a30940ec7 100644 --- a/src/plugins/chat/emoji_manager.py +++ b/src/plugins/chat/emoji_manager.py @@ -38,9 +38,9 @@ class EmojiManager: self.db = Database.get_instance() self._scan_task = None self.vlm = LLM_request(model=global_config.vlm, temperature=0.3, max_tokens=1000) - self.llm_emotion_judge = LLM_request(model=global_config.llm_emotion_judge, max_tokens=60, - temperature=0.8) # 更高的温度,更少的token(后续可以根据情绪来调整温度) - + self.llm_emotion_judge = LLM_request( + model=global_config.llm_emotion_judge, max_tokens=60, temperature=0.8 + ) # 更高的温度,更少的token(后续可以根据情绪来调整温度) def _ensure_emoji_dir(self): """确保表情存储目录存在""" @@ -68,42 +68,39 @@ class EmojiManager: def _ensure_emoji_collection(self): """确保emoji集合存在并创建索引 - + 这个函数用于确保MongoDB数据库中存在emoji集合,并创建必要的索引。 - + 索引的作用是加快数据库查询速度: - embedding字段的2dsphere索引: 用于加速向量相似度搜索,帮助快速找到相似的表情包 - tags字段的普通索引: 加快按标签搜索表情包的速度 - filename字段的唯一索引: 确保文件名不重复,同时加快按文件名查找的速度 - + 没有索引的话,数据库每次查询都需要扫描全部数据,建立索引后可以大大提高查询效率。 """ - if 'emoji' not in self.db.list_collection_names(): - self.db.create_collection('emoji') - self.db.emoji.create_index([('embedding', '2dsphere')]) - self.db.emoji.create_index([('filename', 1)], unique=True) + if "emoji" not in self.db.list_collection_names(): + self.db.create_collection("emoji") + self.db.emoji.create_index([("embedding", "2dsphere")]) + self.db.emoji.create_index([("filename", 1)], unique=True) def record_usage(self, emoji_id: str): """记录表情使用次数""" try: self._ensure_db() - self.db.emoji.update_one( - {'_id': emoji_id}, - {'$inc': {'usage_count': 1}} - ) + self.db.emoji.update_one({"_id": emoji_id}, {"$inc": {"usage_count": 1}}) except Exception as e: logger.error(f"记录表情使用失败: {str(e)}") - - async def get_emoji_for_text(self, text: str) -> Optional[Tuple[str,str]]: + + async def get_emoji_for_text(self, text: str) -> Optional[Tuple[str, str]]: """根据文本内容获取相关表情包 Args: text: 输入文本 Returns: Optional[str]: 表情包文件路径,如果没有找到则返回None - - + + 可不可以通过 配置文件中的指令 来自定义使用表情包的逻辑? - 我觉得可行 + 我觉得可行 """ try: @@ -121,7 +118,7 @@ class EmojiManager: try: # 获取所有表情包 - all_emojis = list(self.db.emoji.find({}, {'_id': 1, 'path': 1, 'embedding': 1, 'description': 1})) + all_emojis = list(self.db.emoji.find({}, {"_id": 1, "path": 1, "embedding": 1, "description": 1})) if not all_emojis: logger.warning("数据库中没有任何表情包") @@ -140,34 +137,31 @@ class EmojiManager: # 计算所有表情包与输入文本的相似度 emoji_similarities = [ - (emoji, cosine_similarity(text_embedding, emoji.get('embedding', []))) - for emoji in all_emojis + (emoji, cosine_similarity(text_embedding, emoji.get("embedding", []))) for emoji in all_emojis ] # 按相似度降序排序 emoji_similarities.sort(key=lambda x: x[1], reverse=True) # 获取前3个最相似的表情包 - top_10_emojis = emoji_similarities[:10 if len(emoji_similarities) > 10 else len(emoji_similarities)] - + top_10_emojis = emoji_similarities[: 10 if len(emoji_similarities) > 10 else len(emoji_similarities)] + if not top_10_emojis: logger.warning("未找到匹配的表情包") return None # 从前3个中随机选择一个 selected_emoji, similarity = random.choice(top_10_emojis) - - if selected_emoji and 'path' in selected_emoji: + + if selected_emoji and "path" in selected_emoji: # 更新使用次数 - self.db.emoji.update_one( - {'_id': selected_emoji['_id']}, - {'$inc': {'usage_count': 1}} - ) + self.db.emoji.update_one({"_id": selected_emoji["_id"]}, {"$inc": {"usage_count": 1}}) logger.success( - f"找到匹配的表情包: {selected_emoji.get('description', '无描述')} (相似度: {similarity:.4f})") + f"找到匹配的表情包: {selected_emoji.get('description', '无描述')} (相似度: {similarity:.4f})" + ) # 稍微改一下文本描述,不然容易产生幻觉,描述已经包含 表情包 了 - return selected_emoji['path'], "[ %s ]" % selected_emoji.get('description', '无描述') + return selected_emoji["path"], "[ %s ]" % selected_emoji.get("description", "无描述") except Exception as search_error: logger.error(f"搜索表情包失败: {str(search_error)}") @@ -179,7 +173,6 @@ class EmojiManager: logger.error(f"获取表情包失败: {str(e)}") return None - async def _get_emoji_discription(self, image_base64: str) -> str: """获取表情包的标签,使用image_manager的描述生成功能""" @@ -187,16 +180,16 @@ class EmojiManager: # 使用image_manager获取描述,去掉前后的方括号和"表情包:"前缀 description = await image_manager.get_emoji_description(image_base64) # 去掉[表情包:xxx]的格式,只保留描述内容 - description = description.strip('[]').replace('表情包:', '') + description = description.strip("[]").replace("表情包:", "") return description - + except Exception as e: logger.error(f"获取标签失败: {str(e)}") return None async def _check_emoji(self, image_base64: str, image_format: str) -> str: try: - prompt = f'这是一个表情包,请回答这个表情包是否满足\"{global_config.EMOJI_CHECK_PROMPT}\"的要求,是则回答是,否则回答否,不要出现任何其他内容' + prompt = f'这是一个表情包,请回答这个表情包是否满足"{global_config.EMOJI_CHECK_PROMPT}"的要求,是则回答是,否则回答否,不要出现任何其他内容' content, _ = await self.vlm.generate_response_for_image(prompt, image_base64, image_format) logger.debug(f"输出描述: {content}") @@ -208,9 +201,9 @@ class EmojiManager: async def _get_kimoji_for_text(self, text: str): try: - prompt = f'这是{global_config.BOT_NICKNAME}将要发送的消息内容:\n{text}\n若要为其配上表情包,请你输出这个表情包应该表达怎样的情感,应该给人什么样的感觉,不要太简洁也不要太长,注意不要输出任何对消息内容的分析内容,只输出\"一种什么样的感觉\"中间的形容词部分。' + prompt = f'这是{global_config.BOT_NICKNAME}将要发送的消息内容:\n{text}\n若要为其配上表情包,请你输出这个表情包应该表达怎样的情感,应该给人什么样的感觉,不要太简洁也不要太长,注意不要输出任何对消息内容的分析内容,只输出"一种什么样的感觉"中间的形容词部分。' - content, _ = await self.llm_emotion_judge.generate_response_async(prompt,temperature=1.5) + content, _ = await self.llm_emotion_judge.generate_response_async(prompt, temperature=1.5) logger.info(f"输出描述: {content}") return content @@ -225,63 +218,58 @@ class EmojiManager: os.makedirs(emoji_dir, exist_ok=True) # 获取所有支持的图片文件 - files_to_process = [f for f in os.listdir(emoji_dir) if - f.lower().endswith(('.jpg', '.jpeg', '.png', '.gif'))] + files_to_process = [ + f for f in os.listdir(emoji_dir) if f.lower().endswith((".jpg", ".jpeg", ".png", ".gif")) + ] for filename in files_to_process: image_path = os.path.join(emoji_dir, filename) - + # 获取图片的base64编码和哈希值 image_base64 = image_path_to_base64(image_path) if image_base64 is None: os.remove(image_path) continue - + image_bytes = base64.b64decode(image_base64) image_hash = hashlib.md5(image_bytes).hexdigest() image_format = Image.open(io.BytesIO(image_bytes)).format.lower() # 检查是否已经注册过 - existing_emoji = self.db['emoji'].find_one({'filename': filename}) + existing_emoji = self.db["emoji"].find_one({"filename": filename}) description = None - + if existing_emoji: # 即使表情包已存在,也检查是否需要同步到images集合 - description = existing_emoji.get('discription') + description = existing_emoji.get("discription") # 检查是否在images集合中存在 - existing_image = image_manager.db.images.find_one({'hash': image_hash}) + existing_image = image_manager.db.images.find_one({"hash": image_hash}) if not existing_image: # 同步到images集合 image_doc = { - 'hash': image_hash, - 'path': image_path, - 'type': 'emoji', - 'description': description, - 'timestamp': int(time.time()) + "hash": image_hash, + "path": image_path, + "type": "emoji", + "description": description, + "timestamp": int(time.time()), } - image_manager.db.images.update_one( - {'hash': image_hash}, - {'$set': image_doc}, - upsert=True - ) + image_manager.db.images.update_one({"hash": image_hash}, {"$set": image_doc}, upsert=True) # 保存描述到image_descriptions集合 - image_manager._save_description_to_db(image_hash, description, 'emoji') + image_manager._save_description_to_db(image_hash, description, "emoji") logger.success(f"同步已存在的表情包到images集合: {filename}") continue - + # 检查是否在images集合中已有描述 - existing_description = image_manager._get_description_from_db(image_hash, 'emoji') - + existing_description = image_manager._get_description_from_db(image_hash, "emoji") + if existing_description: description = existing_description else: # 获取表情包的描述 description = await self._get_emoji_discription(image_base64) - - if global_config.EMOJI_CHECK: check = await self._check_emoji(image_base64, image_format) - if '是' not in check: + if "是" not in check: os.remove(image_path) logger.info(f"描述: {description}") @@ -289,44 +277,39 @@ class EmojiManager: logger.info(f"其不满足过滤规则,被剔除 {check}") continue logger.info(f"check通过 {check}") - + if description is not None: embedding = await get_embedding(description) - + if description is not None: embedding = await get_embedding(description) # 准备数据库记录 emoji_record = { - 'filename': filename, - 'path': image_path, - 'embedding': embedding, - 'discription': description, - 'hash': image_hash, - 'timestamp': int(time.time()) + "filename": filename, + "path": image_path, + "embedding": embedding, + "discription": description, + "hash": image_hash, + "timestamp": int(time.time()), } - + # 保存到emoji数据库 - self.db['emoji'].insert_one(emoji_record) + self.db["emoji"].insert_one(emoji_record) logger.success(f"注册新表情包: {filename}") logger.info(f"描述: {description}") - # 保存到images数据库 image_doc = { - 'hash': image_hash, - 'path': image_path, - 'type': 'emoji', - 'description': description, - 'timestamp': int(time.time()) + "hash": image_hash, + "path": image_path, + "type": "emoji", + "description": description, + "timestamp": int(time.time()), } - image_manager.db.images.update_one( - {'hash': image_hash}, - {'$set': image_doc}, - upsert=True - ) + image_manager.db.images.update_one({"hash": image_hash}, {"$set": image_doc}, upsert=True) # 保存描述到image_descriptions集合 - image_manager._save_description_to_db(image_hash, description, 'emoji') + image_manager._save_description_to_db(image_hash, description, "emoji") logger.success(f"同步保存到images集合: {filename}") else: logger.warning(f"跳过表情包: {filename}") @@ -354,23 +337,28 @@ class EmojiManager: for emoji in all_emojis: try: - if 'path' not in emoji: + if "path" not in emoji: logger.warning(f"发现无效记录(缺少path字段),ID: {emoji.get('_id', 'unknown')}") - self.db.emoji.delete_one({'_id': emoji['_id']}) + self.db.emoji.delete_one({"_id": emoji["_id"]}) removed_count += 1 continue - if 'embedding' not in emoji: + if "embedding" not in emoji: logger.warning(f"发现过时记录(缺少embedding字段),ID: {emoji.get('_id', 'unknown')}") - self.db.emoji.delete_one({'_id': emoji['_id']}) + self.db.emoji.delete_one({"_id": emoji["_id"]}) removed_count += 1 continue + if "hash" not in emoji: + logger.warning(f"发现缺失记录(缺少hash字段),ID: {emoji.get('_id', 'unknown')}") + hash = hashlib.md5(open(emoji["path"], "rb").read()).hexdigest() + self.db.emoji.update_one({"_id": emoji["_id"]}, {"$set": {"hash": hash}}) + # 检查文件是否存在 - if not os.path.exists(emoji['path']): + if not os.path.exists(emoji["path"]): logger.warning(f"表情包文件已被删除: {emoji['path']}") # 从数据库中删除记录 - result = self.db.emoji.delete_one({'_id': emoji['_id']}) + result = self.db.emoji.delete_one({"_id": emoji["_id"]}) if result.deleted_count > 0: logger.debug(f"成功删除数据库记录: {emoji['_id']}") removed_count += 1 @@ -401,5 +389,3 @@ class EmojiManager: # 创建全局单例 emoji_manager = EmojiManager() - - diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index 626e7cf4e..96308c50b 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -23,8 +23,8 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @dataclass class Message(MessageBase): - chat_stream: ChatStream=None - reply: Optional['Message'] = None + chat_stream: ChatStream = None + reply: Optional["Message"] = None detailed_plain_text: str = "" processed_plain_text: str = "" @@ -35,7 +35,7 @@ class Message(MessageBase): chat_stream: ChatStream, user_info: UserInfo, message_segment: Optional[Seg] = None, - reply: Optional['MessageRecv'] = None, + reply: Optional["MessageRecv"] = None, detailed_plain_text: str = "", processed_plain_text: str = "", ): @@ -45,21 +45,17 @@ class Message(MessageBase): message_id=message_id, time=time, group_info=chat_stream.group_info, - user_info=user_info + user_info=user_info, ) # 调用父类初始化 - super().__init__( - message_info=message_info, - message_segment=message_segment, - raw_message=None - ) + super().__init__(message_info=message_info, message_segment=message_segment, raw_message=None) self.chat_stream = chat_stream # 文本处理相关属性 self.processed_plain_text = processed_plain_text self.detailed_plain_text = detailed_plain_text - + # 回复消息 self.reply = reply @@ -74,41 +70,38 @@ class MessageRecv(Message): Args: message_dict: MessageCQ序列化后的字典 """ - self.message_info = BaseMessageInfo.from_dict(message_dict.get('message_info', {})) + self.message_info = BaseMessageInfo.from_dict(message_dict.get("message_info", {})) - message_segment = message_dict.get('message_segment', {}) + message_segment = message_dict.get("message_segment", {}) - if message_segment.get('data','') == '[json]': + if message_segment.get("data", "") == "[json]": # 提取json消息中的展示信息 - pattern = r'\[CQ:json,data=(?P.+?)\]' - match = re.search(pattern, message_dict.get('raw_message','')) - raw_json = html.unescape(match.group('json_data')) + pattern = r"\[CQ:json,data=(?P.+?)\]" + match = re.search(pattern, message_dict.get("raw_message", "")) + raw_json = html.unescape(match.group("json_data")) try: json_message = json.loads(raw_json) except json.JSONDecodeError: json_message = {} - message_segment['data'] = json_message.get('prompt','') + message_segment["data"] = json_message.get("prompt", "") + + self.message_segment = Seg.from_dict(message_dict.get("message_segment", {})) + self.raw_message = message_dict.get("raw_message") - self.message_segment = Seg.from_dict(message_dict.get('message_segment', {})) - self.raw_message = message_dict.get('raw_message') - # 处理消息内容 self.processed_plain_text = "" # 初始化为空字符串 - self.detailed_plain_text = "" # 初始化为空字符串 - self.is_emoji=False - - - def update_chat_stream(self,chat_stream:ChatStream): - self.chat_stream=chat_stream - + self.detailed_plain_text = "" # 初始化为空字符串 + self.is_emoji = False + + def update_chat_stream(self, chat_stream: ChatStream): + self.chat_stream = chat_stream + async def process(self) -> None: """处理消息内容,生成纯文本和详细文本 这个方法必须在创建实例后显式调用,因为它包含异步操作。 """ - self.processed_plain_text = await self._process_message_segments( - self.message_segment - ) + self.processed_plain_text = await self._process_message_segments(self.message_segment) self.detailed_plain_text = self._generate_detailed_text() async def _process_message_segments(self, segment: Seg) -> str: @@ -157,16 +150,12 @@ class MessageRecv(Message): else: return f"[{seg.type}:{str(seg.data)}]" except Exception as e: - logger.error( - f"处理消息段失败: {str(e)}, 类型: {seg.type}, 数据: {seg.data}" - ) + logger.error(f"处理消息段失败: {str(e)}, 类型: {seg.type}, 数据: {seg.data}") return f"[处理失败的{seg.type}消息]" def _generate_detailed_text(self) -> str: """生成详细文本,包含时间和用户信息""" - time_str = time.strftime( - "%m-%d %H:%M:%S", time.localtime(self.message_info.time) - ) + time_str = time.strftime("%m-%d %H:%M:%S", time.localtime(self.message_info.time)) user_info = self.message_info.user_info name = ( f"{user_info.user_nickname}(ta的昵称:{user_info.user_cardname},ta的id:{user_info.user_id})" @@ -174,7 +163,7 @@ class MessageRecv(Message): else f"{user_info.user_nickname}(ta的id:{user_info.user_id})" ) return f"[{time_str}] {name}: {self.processed_plain_text}\n" - + @dataclass class MessageProcessBase(Message): @@ -257,16 +246,12 @@ class MessageProcessBase(Message): else: return f"[{seg.type}:{str(seg.data)}]" except Exception as e: - logger.error( - f"处理消息段失败: {str(e)}, 类型: {seg.type}, 数据: {seg.data}" - ) + logger.error(f"处理消息段失败: {str(e)}, 类型: {seg.type}, 数据: {seg.data}") return f"[处理失败的{seg.type}消息]" def _generate_detailed_text(self) -> str: """生成详细文本,包含时间和用户信息""" - time_str = time.strftime( - "%m-%d %H:%M:%S", time.localtime(self.message_info.time) - ) + time_str = time.strftime("%m-%d %H:%M:%S", time.localtime(self.message_info.time)) user_info = self.message_info.user_info name = ( f"{user_info.user_nickname}(ta的昵称:{user_info.user_cardname},ta的id:{user_info.user_id})" @@ -330,10 +315,11 @@ class MessageSending(MessageProcessBase): self.is_head = is_head self.is_emoji = is_emoji - def set_reply(self, reply: Optional["MessageRecv"]) -> None: + def set_reply(self, reply: Optional["MessageRecv"] = None) -> None: """设置回复消息""" if reply: self.reply = reply + if self.reply: self.reply_to_message_id = self.reply.message_info.message_id self.message_segment = Seg( type="seglist", @@ -346,9 +332,7 @@ class MessageSending(MessageProcessBase): async def process(self) -> None: """处理消息内容,生成纯文本和详细文本""" if self.message_segment: - self.processed_plain_text = await self._process_message_segments( - self.message_segment - ) + self.processed_plain_text = await self._process_message_segments(self.message_segment) self.detailed_plain_text = self._generate_detailed_text() @classmethod @@ -377,10 +361,7 @@ class MessageSending(MessageProcessBase): def is_private_message(self) -> bool: """判断是否为私聊消息""" - return ( - self.message_info.group_info is None - or self.message_info.group_info.group_id is None - ) + return self.message_info.group_info is None or self.message_info.group_info.group_id is None @dataclass diff --git a/src/plugins/chat/utils_image.py b/src/plugins/chat/utils_image.py index 94014b5b4..14658f4f1 100644 --- a/src/plugins/chat/utils_image.py +++ b/src/plugins/chat/utils_image.py @@ -13,20 +13,22 @@ from nonebot import get_driver from ...common.database import Database from ..chat.config import global_config from ..models.utils_model import LLM_request + driver = get_driver() config = driver.config + class ImageManager: _instance = None IMAGE_DIR = "data" # 图像存储根目录 - + def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) cls._instance.db = None cls._instance._initialized = False return cls._instance - + def __init__(self): if not self._initialized: self.db = Database.get_instance() @@ -35,68 +37,62 @@ class ImageManager: self._ensure_image_dir() self._initialized = True self._llm = LLM_request(model=global_config.vlm, temperature=0.4, max_tokens=300) - + def _ensure_image_dir(self): """确保图像存储目录存在""" os.makedirs(self.IMAGE_DIR, exist_ok=True) - + def _ensure_image_collection(self): """确保images集合存在并创建索引""" - if 'images' not in self.db.list_collection_names(): - self.db.create_collection('images') + if "images" not in self.db.list_collection_names(): + self.db.create_collection("images") # 创建索引 - self.db.images.create_index([('hash', 1)], unique=True) - self.db.images.create_index([('url', 1)]) - self.db.images.create_index([('path', 1)]) + self.db.images.create_index([("hash", 1)], unique=True) + self.db.images.create_index([("url", 1)]) + self.db.images.create_index([("path", 1)]) def _ensure_description_collection(self): """确保image_descriptions集合存在并创建索引""" - if 'image_descriptions' not in self.db.list_collection_names(): - self.db.create_collection('image_descriptions') + if "image_descriptions" not in self.db.list_collection_names(): + self.db.create_collection("image_descriptions") # 创建索引 - self.db.image_descriptions.create_index([('hash', 1)], unique=True) - self.db.image_descriptions.create_index([('type', 1)]) + self.db.image_descriptions.create_index([("hash", 1)], unique=True) + self.db.image_descriptions.create_index([("type", 1)]) def _get_description_from_db(self, image_hash: str, description_type: str) -> Optional[str]: """从数据库获取图片描述 - + Args: image_hash: 图片哈希值 description_type: 描述类型 ('emoji' 或 'image') - + Returns: Optional[str]: 描述文本,如果不存在则返回None """ - result= self.db.image_descriptions.find_one({ - 'hash': image_hash, - 'type': description_type - }) - return result['description'] if result else None + if image_hash is None: + return + result = self.db.image_descriptions.find_one({"hash": image_hash, "type": description_type}) + return result["description"] if result else None def _save_description_to_db(self, image_hash: str, description: str, description_type: str) -> None: """保存图片描述到数据库 - + Args: image_hash: 图片哈希值 description: 描述文本 description_type: 描述类型 ('emoji' 或 'image') """ + if image_hash is None: + return self.db.image_descriptions.update_one( - {'hash': image_hash, 'type': description_type}, - { - '$set': { - 'description': description, - 'timestamp': int(time.time()) - } - }, - upsert=True + {"hash": image_hash, "type": description_type}, + {"$set": {"description": description, "timestamp": int(time.time())}}, + upsert=True, ) - async def save_image(self, - image_data: Union[str, bytes], - url: str = None, - description: str = None, - is_base64: bool = False) -> Optional[str]: + async def save_image( + self, image_data: Union[str, bytes], url: str = None, description: str = None, is_base64: bool = False + ) -> Optional[str]: """保存图像 Args: image_data: 图像数据(base64字符串或字节) @@ -118,41 +114,41 @@ class ImageManager: image_bytes = image_data else: return None - + # 计算哈希值 image_hash = hashlib.md5(image_bytes).hexdigest() image_format = Image.open(io.BytesIO(image_bytes)).format.lower() - + # 查重 - existing = self.db.images.find_one({'hash': image_hash}) + existing = self.db.images.find_one({"hash": image_hash}) if existing: - return existing['path'] - + return existing["path"] + # 生成文件名和路径 timestamp = int(time.time()) filename = f"{timestamp}_{image_hash[:8]}.{image_format}" file_path = os.path.join(self.IMAGE_DIR, filename) - + # 保存文件 with open(file_path, "wb") as f: f.write(image_bytes) - + # 保存到数据库 image_doc = { - 'hash': image_hash, - 'path': file_path, - 'url': url, - 'description': description, - 'timestamp': timestamp + "hash": image_hash, + "path": file_path, + "url": url, + "description": description, + "timestamp": timestamp, } self.db.images.insert_one(image_doc) - + return file_path - + except Exception as e: logger.error(f"保存图像失败: {str(e)}") return None - + async def get_image_by_url(self, url: str) -> Optional[str]: """根据URL获取图像路径(带查重) Args: @@ -162,10 +158,10 @@ class ImageManager: """ try: # 先查找是否已存在 - existing = self.db.images.find_one({'url': url}) + existing = self.db.images.find_one({"url": url}) if existing: - return existing['path'] - + return existing["path"] + # 下载图像 async with aiohttp.ClientSession() as session: async with session.get(url) as resp: @@ -173,11 +169,11 @@ class ImageManager: image_bytes = await resp.read() return await self.save_image(image_bytes, url=url) return None - + except Exception as e: logger.error(f"获取图像失败: {str(e)}") return None - + async def get_base64_by_url(self, url: str) -> Optional[str]: """根据URL获取base64(带查重) Args: @@ -189,16 +185,15 @@ class ImageManager: image_path = await self.get_image_by_url(url) if not image_path: return None - - with open(image_path, 'rb') as f: + + with open(image_path, "rb") as f: image_bytes = f.read() - return base64.b64encode(image_bytes).decode('utf-8') - + return base64.b64encode(image_bytes).decode("utf-8") + except Exception as e: logger.error(f"获取base64失败: {str(e)}") return None - - + def check_url_exists(self, url: str) -> bool: """检查URL是否已存在 Args: @@ -206,8 +201,8 @@ class ImageManager: Returns: bool: 是否存在 """ - return self.db.images.find_one({'url': url}) is not None - + return self.db.images.find_one({"url": url}) is not None + def check_hash_exists(self, image_data: Union[str, bytes], is_base64: bool = False) -> bool: """检查图像是否已存在 Args: @@ -227,14 +222,14 @@ class ImageManager: image_bytes = image_data else: return False - + image_hash = hashlib.md5(image_bytes).hexdigest() - return self.db.images.find_one({'hash': image_hash}) is not None - + return self.db.images.find_one({"hash": image_hash}) is not None + except Exception as e: logger.error(f"检查哈希失败: {str(e)}") return False - + async def get_emoji_description(self, image_base64: str) -> str: """获取表情包描述,带查重和保存功能""" try: @@ -244,7 +239,7 @@ class ImageManager: image_format = Image.open(io.BytesIO(image_bytes)).format.lower() # 查询缓存的描述 - cached_description = self._get_description_from_db(image_hash, 'emoji') + cached_description = self._get_description_from_db(image_hash, "emoji") if cached_description: logger.info(f"缓存表情包描述: {cached_description}") return f"[表情包:{cached_description}]" @@ -252,39 +247,35 @@ class ImageManager: # 调用AI获取描述 prompt = "这是一个表情包,使用中文简洁的描述一下表情包的内容和表情包所表达的情感" description, _ = await self._llm.generate_response_for_image(prompt, image_base64, image_format) - + # 根据配置决定是否保存图片 if global_config.EMOJI_SAVE: # 生成文件名和路径 timestamp = int(time.time()) filename = f"{timestamp}_{image_hash[:8]}.{image_format}" - file_path = os.path.join(self.IMAGE_DIR, 'emoji',filename) - + file_path = os.path.join(self.IMAGE_DIR, "emoji", filename) + try: # 保存文件 with open(file_path, "wb") as f: f.write(image_bytes) - + # 保存到数据库 image_doc = { - 'hash': image_hash, - 'path': file_path, - 'type': 'emoji', - 'description': description, - 'timestamp': timestamp + "hash": image_hash, + "path": file_path, + "type": "emoji", + "description": description, + "timestamp": timestamp, } - self.db.images.update_one( - {'hash': image_hash}, - {'$set': image_doc}, - upsert=True - ) + self.db.images.update_one({"hash": image_hash}, {"$set": image_doc}, upsert=True) logger.success(f"保存表情包: {file_path}") except Exception as e: logger.error(f"保存表情包文件失败: {str(e)}") - + # 保存描述到数据库 - self._save_description_to_db(image_hash, description, 'emoji') - + self._save_description_to_db(image_hash, description, "emoji") + return f"[表情包:{description}]" except Exception as e: logger.error(f"获取表情包描述失败: {str(e)}") @@ -300,60 +291,57 @@ class ImageManager: image_format = Image.open(io.BytesIO(image_bytes)).format.lower() # 查询缓存的描述 - cached_description = self._get_description_from_db(image_hash, 'image') + cached_description = self._get_description_from_db(image_hash, "image") if cached_description: print("图片描述缓存中") return f"[图片:{cached_description}]" # 调用AI获取描述 - prompt = "请用中文描述这张图片的内容。如果有文字,请把文字都描述出来。并尝试猜测这个图片的含义。最多200个字。" + prompt = ( + "请用中文描述这张图片的内容。如果有文字,请把文字都描述出来。并尝试猜测这个图片的含义。最多200个字。" + ) description, _ = await self._llm.generate_response_for_image(prompt, image_base64, image_format) - + print(f"描述是{description}") - + if description is None: logger.warning("AI未能生成图片描述") return "[图片]" - + # 根据配置决定是否保存图片 if global_config.EMOJI_SAVE: # 生成文件名和路径 timestamp = int(time.time()) filename = f"{timestamp}_{image_hash[:8]}.{image_format}" - file_path = os.path.join(self.IMAGE_DIR,'image', filename) - + file_path = os.path.join(self.IMAGE_DIR, "image", filename) + try: # 保存文件 with open(file_path, "wb") as f: f.write(image_bytes) - + # 保存到数据库 image_doc = { - 'hash': image_hash, - 'path': file_path, - 'type': 'image', - 'description': description, - 'timestamp': timestamp + "hash": image_hash, + "path": file_path, + "type": "image", + "description": description, + "timestamp": timestamp, } - self.db.images.update_one( - {'hash': image_hash}, - {'$set': image_doc}, - upsert=True - ) + self.db.images.update_one({"hash": image_hash}, {"$set": image_doc}, upsert=True) logger.success(f"保存图片: {file_path}") except Exception as e: logger.error(f"保存图片文件失败: {str(e)}") - + # 保存描述到数据库 - self._save_description_to_db(image_hash, description, 'image') - + self._save_description_to_db(image_hash, description, "image") + return f"[图片:{description}]" except Exception as e: logger.error(f"获取图片描述失败: {str(e)}") return "[图片]" - # 创建全局单例 image_manager = ImageManager() @@ -366,9 +354,9 @@ def image_path_to_base64(image_path: str) -> str: str: base64编码的图片数据 """ try: - with open(image_path, 'rb') as f: + with open(image_path, "rb") as f: image_data = f.read() - return base64.b64encode(image_data).decode('utf-8') + return base64.b64encode(image_data).decode("utf-8") except Exception as e: logger.error(f"读取图片失败: {image_path}, 错误: {str(e)}") - return None \ No newline at end of file + return None From 3b1fc70e26f73d7ef584aa14957ab28ee4111c02 Mon Sep 17 00:00:00 2001 From: tcmofashi Date: Wed, 12 Mar 2025 21:51:16 +0800 Subject: [PATCH 3/8] =?UTF-8?q?fix:=20=E8=B7=AF=E5=BE=84=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=E6=80=A7=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/utils_image.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/plugins/chat/utils_image.py b/src/plugins/chat/utils_image.py index 0dfc17111..f4969d3e9 100644 --- a/src/plugins/chat/utils_image.py +++ b/src/plugins/chat/utils_image.py @@ -247,6 +247,8 @@ class ImageManager: # 生成文件名和路径 timestamp = int(time.time()) filename = f"{timestamp}_{image_hash[:8]}.{image_format}" + if not os.path.exists(os.path.join(self.IMAGE_DIR, "emoji")): + os.makedirs(os.path.join(self.IMAGE_DIR, "emoji")) file_path = os.path.join(self.IMAGE_DIR, "emoji", filename) try: @@ -307,6 +309,8 @@ class ImageManager: # 生成文件名和路径 timestamp = int(time.time()) filename = f"{timestamp}_{image_hash[:8]}.{image_format}" + if not os.path.exists(os.path.join(self.IMAGE_DIR, "image")): + os.makedirs(os.path.join(self.IMAGE_DIR, "image")) file_path = os.path.join(self.IMAGE_DIR, "image", filename) try: From acb59e5d8db52817e654a788ccf4960ee7e60ff4 Mon Sep 17 00:00:00 2001 From: KawaiiYusora Date: Wed, 12 Mar 2025 23:07:28 +0800 Subject: [PATCH 4/8] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=BC=BA=E5=A4=B1?= =?UTF-8?q?hash=E5=AD=97=E6=AE=B5=E7=9A=84=E8=AE=B0=E5=BD=95=20=E6=8A=9B?= =?UTF-8?q?=E5=87=BAFile=20not=20found?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/emoji_manager.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py index 0933644ec..2fa2a268d 100644 --- a/src/plugins/chat/emoji_manager.py +++ b/src/plugins/chat/emoji_manager.py @@ -346,11 +346,6 @@ class EmojiManager: removed_count += 1 continue - if "hash" not in emoji: - logger.warning(f"发现缺失记录(缺少hash字段),ID: {emoji.get('_id', 'unknown')}") - hash = hashlib.md5(open(emoji["path"], "rb").read()).hexdigest() - db.emoji.update_one({"_id": emoji["_id"]}, {"$set": {"hash": hash}}) - # 检查文件是否存在 if not os.path.exists(emoji["path"]): logger.warning(f"表情包文件已被删除: {emoji['path']}") @@ -361,6 +356,12 @@ class EmojiManager: removed_count += 1 else: logger.error(f"删除数据库记录失败: {emoji['_id']}") + continue + + if "hash" not in emoji: + logger.warning(f"发现缺失记录(缺少hash字段),ID: {emoji.get('_id', 'unknown')}") + hash = hashlib.md5(open(emoji["path"], "rb").read()).hexdigest() + db.emoji.update_one({"_id": emoji["_id"]}, {"$set": {"hash": hash}}) except Exception as item_error: logger.error(f"处理表情包记录时出错: {str(item_error)}") continue From 24a3cf3b663bc1d0a2f3dbbb9db3bd6115ed96b6 Mon Sep 17 00:00:00 2001 From: HYY Date: Wed, 12 Mar 2025 23:09:53 +0800 Subject: [PATCH 5/8] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E4=B8=AAhash=20unset=E5=8F=AF=E8=83=BD=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=E7=9A=84=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/emoji_manager.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py index 0933644ec..aae24ac53 100644 --- a/src/plugins/chat/emoji_manager.py +++ b/src/plugins/chat/emoji_manager.py @@ -346,11 +346,6 @@ class EmojiManager: removed_count += 1 continue - if "hash" not in emoji: - logger.warning(f"发现缺失记录(缺少hash字段),ID: {emoji.get('_id', 'unknown')}") - hash = hashlib.md5(open(emoji["path"], "rb").read()).hexdigest() - db.emoji.update_one({"_id": emoji["_id"]}, {"$set": {"hash": hash}}) - # 检查文件是否存在 if not os.path.exists(emoji["path"]): logger.warning(f"表情包文件已被删除: {emoji['path']}") @@ -361,6 +356,12 @@ class EmojiManager: removed_count += 1 else: logger.error(f"删除数据库记录失败: {emoji['_id']}") + + if "hash" not in emoji: + logger.warning(f"发现缺失记录(缺少hash字段),ID: {emoji.get('_id', 'unknown')}") + hash = hashlib.md5(open(emoji["path"], "rb").read()).hexdigest() + db.emoji.update_one({"_id": emoji["_id"]}, {"$set": {"hash": hash}}) + except Exception as item_error: logger.error(f"处理表情包记录时出错: {str(item_error)}") continue From eafc3dea7188b05500e5fc7eb735285f90fbe812 Mon Sep 17 00:00:00 2001 From: Pliosauroidea Date: Wed, 12 Mar 2025 23:16:29 +0800 Subject: [PATCH 6/8] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=BA=86message=E5=8F=98?= =?UTF-8?q?=E5=8A=A8=E5=AF=BC=E8=87=B4=E7=9A=84=E7=A7=81=E8=81=8Aerror?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/bot.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 1db38477c..ec2f3a0b4 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -126,7 +126,7 @@ class ChatBot: for word in global_config.ban_words: if word in message.processed_plain_text: logger.info( - f"[{chat.group_info.group_name if chat.group_info.group_id else '私聊'}]{userinfo.user_nickname}:{message.processed_plain_text}" + f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{message.processed_plain_text}" ) logger.info(f"[过滤词识别]消息中含有{word},filtered") return @@ -135,7 +135,7 @@ class ChatBot: for pattern in global_config.ban_msgs_regex: if re.search(pattern, message.raw_message): logger.info( - f"[{chat.group_info.group_name if chat.group_info.group_id else '私聊'}]{userinfo.user_nickname}:{message.raw_message}" + f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{message.raw_message}" ) logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered") return @@ -143,7 +143,7 @@ class ChatBot: current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(messageinfo.time)) # topic=await topic_identifier.identify_topic_llm(message.processed_plain_text) - + topic = "" interested_rate = await hippocampus.memory_activate_value(message.processed_plain_text) / 100 logger.debug(f"对{message.processed_plain_text}的激活度:{interested_rate}") @@ -163,7 +163,7 @@ class ChatBot: current_willing = willing_manager.get_willing(chat_stream=chat) logger.info( - f"[{current_time}][{chat.group_info.group_name if chat.group_info.group_id else '私聊'}]{chat.user_info.user_nickname}:" + f"[{current_time}][{chat.group_info.group_name if chat.group_info else '私聊'}]{chat.user_info.user_nickname}:" f"{message.processed_plain_text}[回复意愿:{current_willing:.2f}][概率:{reply_probability * 100:.1f}%]" ) From 2d7da8cd70aa6a6f7622482bd9cf791b91ff962f Mon Sep 17 00:00:00 2001 From: tcmofashi Date: Thu, 13 Mar 2025 00:46:50 +0800 Subject: [PATCH 7/8] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=A1=A8=E6=83=85?= =?UTF-8?q?=E5=8C=85=E9=87=8D=E5=A4=8D=E4=BF=9D=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/emoji_manager.py | 2 +- src/plugins/chat/utils_image.py | 124 +++--------------------------- 2 files changed, 10 insertions(+), 116 deletions(-) diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py index 657b17c67..76437f8f2 100644 --- a/src/plugins/chat/emoji_manager.py +++ b/src/plugins/chat/emoji_manager.py @@ -232,7 +232,7 @@ class EmojiManager: image_hash = hashlib.md5(image_bytes).hexdigest() image_format = Image.open(io.BytesIO(image_bytes)).format.lower() # 检查是否已经注册过 - existing_emoji = db["emoji"].find_one({"filename": filename}) + existing_emoji = db["emoji"].find_one({"hash": image_hash}) description = None if existing_emoji: diff --git a/src/plugins/chat/utils_image.py b/src/plugins/chat/utils_image.py index f4969d3e9..cc3a6ca3d 100644 --- a/src/plugins/chat/utils_image.py +++ b/src/plugins/chat/utils_image.py @@ -84,65 +84,6 @@ class ImageManager: upsert=True, ) - async def save_image( - self, image_data: Union[str, bytes], url: str = None, description: str = None, is_base64: bool = False - ) -> Optional[str]: - """保存图像 - Args: - image_data: 图像数据(base64字符串或字节) - url: 图像URL - description: 图像描述 - is_base64: image_data是否为base64格式 - Returns: - str: 保存后的文件路径,失败返回None - """ - try: - # 转换为字节格式 - if is_base64: - if isinstance(image_data, str): - image_bytes = base64.b64decode(image_data) - else: - return None - else: - if isinstance(image_data, bytes): - image_bytes = image_data - else: - return None - - # 计算哈希值 - image_hash = hashlib.md5(image_bytes).hexdigest() - image_format = Image.open(io.BytesIO(image_bytes)).format.lower() - - # 查重 - existing = db.images.find_one({"hash": image_hash}) - if existing: - return existing["path"] - - # 生成文件名和路径 - timestamp = int(time.time()) - filename = f"{timestamp}_{image_hash[:8]}.{image_format}" - file_path = os.path.join(self.IMAGE_DIR, filename) - - # 保存文件 - with open(file_path, "wb") as f: - f.write(image_bytes) - - # 保存到数据库 - image_doc = { - "hash": image_hash, - "path": file_path, - "url": url, - "description": description, - "timestamp": timestamp, - } - db.images.insert_one(image_doc) - - return file_path - - except Exception as e: - logger.error(f"保存图像失败: {str(e)}") - return None - async def get_image_by_url(self, url: str) -> Optional[str]: """根据URL获取图像路径(带查重) Args: @@ -168,62 +109,6 @@ class ImageManager: logger.error(f"获取图像失败: {str(e)}") return None - async def get_base64_by_url(self, url: str) -> Optional[str]: - """根据URL获取base64(带查重) - Args: - url: 图像URL - Returns: - str: base64字符串,失败返回None - """ - try: - image_path = await self.get_image_by_url(url) - if not image_path: - return None - - with open(image_path, "rb") as f: - image_bytes = f.read() - return base64.b64encode(image_bytes).decode("utf-8") - - except Exception as e: - logger.error(f"获取base64失败: {str(e)}") - return None - - def check_url_exists(self, url: str) -> bool: - """检查URL是否已存在 - Args: - url: 图像URL - Returns: - bool: 是否存在 - """ - return db.images.find_one({"url": url}) is not None - - def check_hash_exists(self, image_data: Union[str, bytes], is_base64: bool = False) -> bool: - """检查图像是否已存在 - Args: - image_data: 图像数据(base64或字节) - is_base64: 是否为base64格式 - Returns: - bool: 是否存在 - """ - try: - if is_base64: - if isinstance(image_data, str): - image_bytes = base64.b64decode(image_data) - else: - return False - else: - if isinstance(image_data, bytes): - image_bytes = image_data - else: - return False - - image_hash = hashlib.md5(image_bytes).hexdigest() - return db.images.find_one({"hash": image_hash}) is not None - - except Exception as e: - logger.error(f"检查哈希失败: {str(e)}") - return False - async def get_emoji_description(self, image_base64: str) -> str: """获取表情包描述,带查重和保存功能""" try: @@ -242,6 +127,11 @@ class ImageManager: prompt = "这是一个表情包,使用中文简洁的描述一下表情包的内容和表情包所表达的情感" description, _ = await self._llm.generate_response_for_image(prompt, image_base64, image_format) + cached_description = self._get_description_from_db(image_hash, "emoji") + if cached_description: + logger.warning(f"虽然生成了描述,但找到缓存表情包描述: {cached_description}") + return f"[表情包:{cached_description}]" + # 根据配置决定是否保存图片 if global_config.EMOJI_SAVE: # 生成文件名和路径 @@ -297,6 +187,10 @@ class ImageManager: "请用中文描述这张图片的内容。如果有文字,请把文字都描述出来。并尝试猜测这个图片的含义。最多200个字。" ) description, _ = await self._llm.generate_response_for_image(prompt, image_base64, image_format) + cached_description = self._get_description_from_db(image_hash, "emoji") + if cached_description: + logger.info(f"缓存图片描述: {cached_description}") + return f"[图片:{cached_description}]" print(f"描述是{description}") From e7b9e9d20b7e6f44a81a36f532bff222b13b6fcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E7=8C=AB?= Date: Thu, 13 Mar 2025 02:22:50 +0900 Subject: [PATCH 8/8] add ruff action to debug branch --- .github/workflows/ruff.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/workflows/ruff.yml diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 000000000..4adeffd74 --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,8 @@ +name: Ruff +on: [ push, pull_request ] +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/ruff-action@v3