From 1215a2edcea7389a13b0184767f4244400d4ee20 Mon Sep 17 00:00:00 2001 From: bridge Date: Mon, 24 Nov 2025 22:30:49 +0800 Subject: [PATCH] update talk event --- src/classes/action/event_helper.py | 2 +- src/classes/avatar.py | 16 +++++++++---- src/classes/event.py | 13 ++++++---- src/classes/event_manager.py | 13 ++++++---- src/classes/mutual_action/conversation.py | 28 +++++++++++----------- src/classes/mutual_action/mutual_action.py | 11 +++++++-- src/classes/mutual_action/talk.py | 5 ++-- tools/img_gen/gen_img.py | 18 +++++++------- 8 files changed, 66 insertions(+), 40 deletions(-) diff --git a/src/classes/action/event_helper.py b/src/classes/action/event_helper.py index ff71368..a5ca927 100644 --- a/src/classes/action/event_helper.py +++ b/src/classes/action/event_helper.py @@ -16,7 +16,7 @@ class EventHelper: def push_pair(event: Event, initiator: "Avatar", target: Optional["Avatar"], *, to_sidebar_once: bool = True) -> None: initiator.add_event(event, to_sidebar=True) if target is not None: - target.add_event(event, to_sidebar=(not to_sidebar_once), to_history=True) + target.add_event(event, to_sidebar=(not to_sidebar_once)) @staticmethod def push_self(event: Event, avatar: "Avatar", *, to_sidebar: bool = True) -> None: diff --git a/src/classes/avatar.py b/src/classes/avatar.py index 305c448..93ccb4f 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -405,9 +405,15 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): action_cls = ActionRegistry.get(action_name) return action_cls(self, self.world) - def load_decide_result_chain(self, action_name_params_pairs: ACTION_NAME_PARAMS_PAIRS, avatar_thinking: str, short_term_objective: str): + def load_decide_result_chain(self, action_name_params_pairs: ACTION_NAME_PARAMS_PAIRS, avatar_thinking: str, short_term_objective: str, prepend: bool = False): """ 加载AI的决策结果(动作链),立即设置第一个为当前动作,其余进入队列。 + + Args: + action_name_params_pairs: 动作名和参数对列表 + avatar_thinking: 思考内容 + short_term_objective: 短期目标 + prepend: 是否插队到最前面(默认False,即追加到末尾) """ if not action_name_params_pairs: return @@ -415,7 +421,10 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): self.short_term_objective = short_term_objective # 转为计划并入队(不立即提交,交由提交阶段统一触发开始事件) plans: List[ActionPlan] = [ActionPlan(name, params) for name, params in action_name_params_pairs] - self.planned_actions.extend(plans) + if prepend: + self.planned_actions[0:0] = plans + else: + self.planned_actions.extend(plans) def clear_plans(self) -> None: self.planned_actions.clear() @@ -607,11 +616,10 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): """ return self.items.get(item, 0) - def add_event(self, event: Event, *, to_sidebar: bool = True, to_history: bool = True) -> None: + def add_event(self, event: Event, *, to_sidebar: bool = True) -> None: """ 添加事件: - to_sidebar: 是否进入全局侧边栏(通过 Avatar._pending_events 暂存) - - to_history: 兼容参数,已废弃(统一改为通过 World.event_manager 查询历史) 注意:事件会先存入_pending_events,统一由Simulator写入event_manager,避免重复 """ diff --git a/src/classes/event.py b/src/classes/event.py index ed4b49e..1765dbd 100644 --- a/src/classes/event.py +++ b/src/classes/event.py @@ -1,8 +1,9 @@ """ event class """ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List, Optional +import uuid from src.classes.calendar import Month, Year, MonthStamp @@ -16,6 +17,8 @@ class Event: is_major: bool = False # 是否为故事事件(不进入记忆索引),默认False is_story: bool = False + # 唯一ID,用于去重 + id: str = field(default_factory=lambda: str(uuid.uuid4())) def __str__(self) -> str: year = self.month_stamp.get_year() @@ -29,7 +32,8 @@ class Event: "content": self.content, "related_avatars": self.related_avatars, "is_major": self.is_major, - "is_story": self.is_story + "is_story": self.is_story, + "id": self.id } @classmethod @@ -40,7 +44,8 @@ class Event: content=data["content"], related_avatars=data.get("related_avatars"), is_major=data.get("is_major", False), - is_story=data.get("is_story", False) + is_story=data.get("is_story", False), + id=data.get("id", str(uuid.uuid4())) ) class NullEvent: @@ -66,4 +71,4 @@ NULL_EVENT = NullEvent() def is_null_event(event) -> bool: """检查事件是否为空事件的便捷函数""" - return event is NULL_EVENT \ No newline at end of file + return event is NULL_EVENT diff --git a/src/classes/event_manager.py b/src/classes/event_manager.py index aba7ec4..8d9f16e 100644 --- a/src/classes/event_manager.py +++ b/src/classes/event_manager.py @@ -33,11 +33,16 @@ class EventManager: dq.popleft() def add_event(self, event: Event) -> None: - # 幂等:若已存在同 event_id,跳过 - if getattr(event, "event_id", None) and event.event_id in self._by_id: + # 过滤掉空事件 + from src.classes.event import is_null_event + if is_null_event(event): return - if getattr(event, "event_id", None): - self._by_id[event.event_id] = event + + # 幂等:若已存在同 id,跳过 + if getattr(event, "id", None) and event.id in self._by_id: + return + if getattr(event, "id", None): + self._by_id[event.id] = event # 全局 self._events.append(event) diff --git a/src/classes/mutual_action/conversation.py b/src/classes/mutual_action/conversation.py index dcfecf7..1f9a044 100644 --- a/src/classes/mutual_action/conversation.py +++ b/src/classes/mutual_action/conversation.py @@ -11,7 +11,7 @@ from src.classes.relations import ( set_relation, cancel_relation, ) -from src.classes.event import Event +from src.classes.event import Event, NULL_EVENT from src.utils.config import CONFIG from src.classes.action_runtime import ActionResult, ActionStatus from src.classes.action.event_helper import EventHelper @@ -72,16 +72,13 @@ class Conversation(MutualAction): # 覆盖 start:自定义事件消息 def start(self, target_avatar: "Avatar|str", **kwargs) -> Event: - target = self._get_target_avatar(target_avatar) - target_name = target.name if target is not None else str(target_avatar) - rel_ids = [self.avatar.id] - if target is not None: - rel_ids.append(target.id) - event = Event(self.world.month_stamp, f"{self.avatar.name} 与 {target_name} 开始交谈", related_avatars=rel_ids) - self.avatar.add_event(event, to_sidebar=False) - if target is not None: - target.add_event(event, to_sidebar=False) - return event + # 记录开始时间 + self._start_month_stamp = self.world.month_stamp + + # Conversation 动作不仅返回 NULL_EVENT 以避免生成“开始交谈”的冗余事件(防止与对话内容事件时序显示混乱), + # 同时也无需手动 add_event,因为我们希望侧边栏和历史记录都只显示最终的对话内容。 + + return NULL_EVENT def _handle_feedback_result(self, target: "Avatar", result: dict) -> ActionResult: """ @@ -92,10 +89,13 @@ class Conversation(MutualAction): new_relation_str = str(result.get("new_relation", "")).strip() cancel_relation_str = str(result.get("cancal_relation", "")).strip() # 保持模板中的拼写 + # 使用开始时间戳 + month_stamp = self._start_month_stamp if self._start_month_stamp is not None else self.world.month_stamp + # 记录对话内容 if conversation_content: content_event = Event( - self.world.month_stamp, + month_stamp, f"{self.avatar.name} 与 {target.name} 的交谈:{conversation_content}", related_avatars=[self.avatar.id, target.id] ) @@ -107,7 +107,7 @@ class Conversation(MutualAction): if rel is not None: set_relation(target, self.avatar, rel) set_event = Event( - self.world.month_stamp, + month_stamp, f"{target.name} 与 {self.avatar.name} 的关系变为:{relation_display_names.get(rel, str(rel))}", related_avatars=[self.avatar.id, target.id], is_major=True @@ -121,7 +121,7 @@ class Conversation(MutualAction): success = cancel_relation(target, self.avatar, rel) if success: cancel_event = Event( - self.world.month_stamp, + month_stamp, f"{target.name} 与 {self.avatar.name} 取消了关系:{relation_display_names.get(rel, str(rel))}", related_avatars=[self.avatar.id, target.id], is_major=True diff --git a/src/classes/mutual_action/mutual_action.py b/src/classes/mutual_action/mutual_action.py index 0c52ad6..7359e74 100644 --- a/src/classes/mutual_action/mutual_action.py +++ b/src/classes/mutual_action/mutual_action.py @@ -53,6 +53,8 @@ class MutualAction(DefineAction, LLMAction, TargetingMixin): # 异步反馈任务句柄与缓存结果 self._feedback_task: asyncio.Task | None = None self._feedback_cached: dict | None = None + # 记录动作开始时间,用于生成事件的时间戳 + self._start_month_stamp: int | None = None def _get_template_path(self) -> Path: return CONFIG.paths.templates / "mutual_action.txt" @@ -170,6 +172,9 @@ class MutualAction(DefineAction, LLMAction, TargetingMixin): """ 启动互动动作,返回开始事件 """ + # 记录开始时间 + self._start_month_stamp = self.world.month_stamp + target = self._get_target_avatar(target_avatar) target_name = target.name if target is not None else str(target_avatar) action_name = self.ACTION_NAME @@ -178,7 +183,7 @@ class MutualAction(DefineAction, LLMAction, TargetingMixin): rel_ids.append(target.id) # 根据IS_MAJOR类变量设置事件类型 is_major = self.__class__.IS_MAJOR if hasattr(self.__class__, 'IS_MAJOR') else False - event = Event(self.world.month_stamp, f"{self.avatar.name} 对 {target_name} 发起 {action_name}", related_avatars=rel_ids, is_major=is_major) + event = Event(self._start_month_stamp, f"{self.avatar.name} 对 {target_name} 发起 {action_name}", related_avatars=rel_ids, is_major=is_major) # 仅写入历史,避免与提交阶段重复推送到侧边栏 self.avatar.add_event(event, to_sidebar=False) if target is not None: @@ -214,7 +219,9 @@ class MutualAction(DefineAction, LLMAction, TargetingMixin): target.thinking = thinking self._settle_feedback(target, feedback) fb_label = self.FEEDBACK_LABELS.get(str(feedback).strip(), str(feedback)) - feedback_event = Event(self.world.month_stamp, f"{target.name} 对 {self.avatar.name} 的反馈:{fb_label}", related_avatars=[self.avatar.id, target.id]) + # 使用开始时间戳 + month_stamp = self._start_month_stamp if self._start_month_stamp is not None else self.world.month_stamp + feedback_event = Event(month_stamp, f"{target.name} 对 {self.avatar.name} 的反馈:{fb_label}", related_avatars=[self.avatar.id, target.id]) EventHelper.push_pair(feedback_event, initiator=self.avatar, target=target, to_sidebar_once=True) self._apply_feedback(target, feedback) return ActionResult(status=ActionStatus.COMPLETED, events=[]) diff --git a/src/classes/mutual_action/talk.py b/src/classes/mutual_action/talk.py index 59ee93c..019fd66 100644 --- a/src/classes/mutual_action/talk.py +++ b/src/classes/mutual_action/talk.py @@ -56,11 +56,12 @@ class Talk(MutualAction): ) EventHelper.push_pair(accept_event, initiator=self.avatar, target=target, to_sidebar_once=True) - # 将 Conversation 加入计划队列,在Talk完成后立即执行 + # 将 Conversation 加入计划队列,在Talk完成后立即执行(插队到最前) self.avatar.load_decide_result_chain( [("Conversation", {"target_avatar": target.name})], self.avatar.thinking, - self.avatar.short_term_objective + self.avatar.short_term_objective, + prepend=True ) else: # 拒绝攀谈 diff --git a/tools/img_gen/gen_img.py b/tools/img_gen/gen_img.py index 13dae67..987ff94 100644 --- a/tools/img_gen/gen_img.py +++ b/tools/img_gen/gen_img.py @@ -165,12 +165,12 @@ if __name__ == "__main__": "幽影之地,暗影重重,光影交错,幽冥之气,黑雾吞噬轮廓。", "船帆如云,炼器炉火。", ] - # for affix in male_affixes: - # prompt_text = male_prompt_base + affix - # save_generated_image(prompt_text, folder="tools/img_gen/tmp/males") - # for affix in female_affixes: - # prompt_text = female_prompt_base + affix - # save_generated_image(prompt_text, folder="tools/img_gen/tmp/females") - for i, affix in enumerate(sect_affixes): - prompt_text = sect_prompt_base + affix - save_generated_image(prompt_text, folder="tools/img_gen/tmp/sects") \ No newline at end of file + for affix in male_affixes: + prompt_text = male_prompt_base + affix + save_generated_image(prompt_text, folder="tools/img_gen/tmp/males") + for affix in female_affixes: + prompt_text = female_prompt_base + affix + save_generated_image(prompt_text, folder="tools/img_gen/tmp/females") + # for i, affix in enumerate(sect_affixes): + # prompt_text = sect_prompt_base + affix + # save_generated_image(prompt_text, folder="tools/img_gen/tmp/sects") \ No newline at end of file