Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 46 additions & 24 deletions core/chat_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ async def _finalize_and_reschedule(
)
session_config = None
scheduled_job_payload = None
limit_reached_after_send = False

async with self.data_lock:
# 更新未回复计数器
Expand All @@ -106,33 +107,54 @@ async def _finalize_and_reschedule(
if not session_config:
return

schedule_conf = session_config.get("schedule_settings", {})
min_interval = int(schedule_conf.get("min_interval_minutes", 30)) * 60
max_interval = max(
min_interval,
int(schedule_conf.get("max_interval_minutes", 900)) * 60,
)
# 私聊采用配置区间内随机间隔,减少触发规律性
random_interval = random.randint(min_interval, max_interval)
scheduled_at = time.time()
next_trigger_time = scheduled_at + random_interval
run_date = datetime.fromtimestamp(next_trigger_time, tz=self.timezone)

session_payload = self.session_data.setdefault(session_id, {})
session_payload["next_trigger_time"] = next_trigger_time
session_payload["last_scheduled_at"] = scheduled_at
session_payload["last_schedule_min_interval_seconds"] = min_interval
session_payload["last_schedule_max_interval_seconds"] = max_interval
session_payload["last_schedule_random_interval_seconds"] = (
random_interval
)
scheduled_job_payload = {
"run_date": run_date,
"session_config": session_config,
}
if self._is_unanswered_limit_reached(
session_id, session_config, new_unanswered_count
):
limit_reached_after_send = True
self._clear_session_schedule_state(session_id)
logger.info(
f"[主动消息] {self._get_session_log_str(session_id, session_config)} 的未回复次数已达到上限,停止安排下一次主动消息喵。"
)
else:
schedule_conf = session_config.get("schedule_settings", {})
min_interval = (
int(schedule_conf.get("min_interval_minutes", 30)) * 60
)
max_interval = max(
min_interval,
int(schedule_conf.get("max_interval_minutes", 900)) * 60,
)
# 私聊采用配置区间内随机间隔,减少触发规律性
random_interval = random.randint(min_interval, max_interval)
scheduled_at = time.time()
next_trigger_time = scheduled_at + random_interval
run_date = datetime.fromtimestamp(
next_trigger_time, tz=self.timezone
)

session_payload = self.session_data.setdefault(session_id, {})
session_payload["next_trigger_time"] = next_trigger_time
session_payload["last_scheduled_at"] = scheduled_at
session_payload["last_schedule_min_interval_seconds"] = (
min_interval
)
session_payload["last_schedule_max_interval_seconds"] = (
max_interval
)
session_payload["last_schedule_random_interval_seconds"] = (
random_interval
)
scheduled_job_payload = {
"run_date": run_date,
"session_config": session_config,
}

await self._save_data_internal()

if limit_reached_after_send:
self._purge_related_jobs(session_id)
return

if scheduled_job_payload is not None:
self.scheduler.add_job(
self.check_and_chat,
Expand Down
1 change: 1 addition & 0 deletions core/plugin_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ async def initialize(self) -> None:
try:
if self.web_admin_server:
await self.web_admin_server.start()
self.web_admin_server.register_astrbot_page_api()
except Exception as e:
logger.error(f"[主动消息] Web 管理端启动失败喵: {e}")
if self.telemetry and self.telemetry.enabled:
Expand Down
71 changes: 71 additions & 0 deletions core/task_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,43 @@ def _is_persisted_task_still_valid(
# 与 APScheduler misfire_grace_time 保持一致,允许 60 秒轻微抖动
return check_time < (next_trigger + 60)

def _get_max_unanswered_count(
self, session_config: dict | None, default: int = 3
) -> int:
"""Return the configured unanswered limit, treating non-positive values as disabled."""
if not isinstance(session_config, dict):
return default
schedule_conf = session_config.get("schedule_settings", {})
if not isinstance(schedule_conf, dict):
return default
try:
return int(schedule_conf.get("max_unanswered_times", default) or 0)
except (TypeError, ValueError):
return default

def _is_unanswered_limit_reached(
self,
session_id: str,
session_config: dict | None,
unanswered_count: int | None = None,
) -> bool:
"""Shared guard used by runtime scheduling and Pages task snapshots."""
max_unanswered = self._get_max_unanswered_count(session_config)
if max_unanswered <= 0:
return False
if unanswered_count is None:
session_info = self.session_data.get(session_id)
if isinstance(session_info, dict):
try:
unanswered_count = int(
session_info.get("unanswered_count", 0) or 0
)
except (TypeError, ValueError):
unanswered_count = 0
else:
unanswered_count = 0
Comment on lines +158 to +168

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

如果 self.session_datasession_id 对应的值为 None(例如数据损坏或初始化异常),self.session_data.get(session_id, {}) 将返回 None。随后调用 .get("unanswered_count", 0) 会抛出 AttributeError,而该异常并未被 try-except (TypeError, ValueError) 捕获,从而导致程序崩溃。建议使用更安全的防御性代码。

Suggested change
if unanswered_count is None:
try:
unanswered_count = int(
self.session_data.get(session_id, {}).get("unanswered_count", 0)
or 0
)
except (TypeError, ValueError):
unanswered_count = 0
if unanswered_count is None:
session_info = self.session_data.get(session_id)
if isinstance(session_info, dict):
try:
unanswered_count = int(session_info.get("unanswered_count", 0) or 0)
except (TypeError, ValueError):
unanswered_count = 0
else:
unanswered_count = 0

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已在 e707217 中修复:_is_unanswered_limit_reached 现在会先确认 session_info 是 dict,再读取 unanswered_count;如果数据损坏为 None 或其他类型,会安全回退为 0,避免 AttributeError

return unanswered_count >= max_unanswered

def _clear_session_schedule_state(
self,
session_id: str,
Expand Down Expand Up @@ -158,6 +195,10 @@ def _clear_session_schedule_state(
"last_schedule_min_interval_seconds",
"last_schedule_max_interval_seconds",
"last_schedule_random_interval_seconds",
"last_schedule_strategy",
"last_schedule_reason",
"last_schedule_rule",
"last_schedule_source",
}

changed = False
Expand Down Expand Up @@ -191,6 +232,18 @@ def _purge_related_jobs(self, session_id: str) -> None:
except Exception:
pass

def _remove_schedule_for_limit_reached(
self, session_id: str, session_config: dict | None = None
) -> bool:
"""达到未回复上限时清理调度器任务和持久化调度字段。"""
normalized_session_id = self._normalize_session_id(session_id)
self._purge_related_jobs(normalized_session_id)
changed = self._clear_session_schedule_state(normalized_session_id)
logger.info(
f"[主动消息] {self._get_session_log_str(normalized_session_id, session_config)} 已达到未回复次数上限,已清理待触发任务并暂停调度喵。"
)
return changed

def _has_related_persisted_task(self, session_id: str) -> bool:
"""判断同一目标是否存在仍可恢复的持久化任务(避免重复触发)。"""
parsed = self._parse_session_id(session_id)
Expand Down Expand Up @@ -420,6 +473,14 @@ async def _init_jobs_from_data(self) -> None:
)
continue

if self._is_unanswered_limit_reached(session_id, session_config):
if self._clear_session_schedule_state(session_id):
cleaned_runtime_state += 1
logger.info(
f"[主动消息] {self._get_session_log_str(session_id, session_config)} 的未回复次数已达到上限,跳过恢复定时任务喵。"
)
continue

try:
run_date = datetime.fromtimestamp(next_trigger, tz=self.timezone)
existing_job = self.scheduler.get_job(session_id)
Expand Down Expand Up @@ -499,6 +560,16 @@ async def _schedule_next_chat_and_save(
"unanswered_count"
] = 0

if not reset_counter and self._is_unanswered_limit_reached(
normalized_session_id, session_config
):
changed = self._remove_schedule_for_limit_reached(
normalized_session_id, session_config
)
if changed:
await self._save_data_internal()
return

# 计算随机触发时间
min_interval = int(schedule_conf.get("min_interval_minutes", 30)) * 60
max_interval = max(
Expand Down
Loading