Skip to content

fix: 添加语境感知主动消息调度#81

Open
Justice-ocr wants to merge 4 commits into
DBJD-CR:mainfrom
Justice-ocr:codex/issue-26
Open

fix: 添加语境感知主动消息调度#81
Justice-ocr wants to merge 4 commits into
DBJD-CR:mainfrom
Justice-ocr:codex/issue-26

Conversation

@Justice-ocr

@Justice-ocr Justice-ocr commented Jun 11, 2026

Copy link
Copy Markdown

📝 描述 / Description

Fixes #26

这个 PR 尝试让主动消息的下一次调度时间参考最近对话语境,而不是每次都只使用固定随机区间。改动由 AI 协助生成,我已经做过静态检查,但实现仍有待上游维护者结合真实运行场景继续审阅和完善。

🛠️ 改动点 / Modifications

  • 不是一个破坏性变更 / This is NOT a breaking change.

  • _conf_schema.json 中新增 enable_contextual_timingcontextual_timing_history_count 配置项。

  • core/task_scheduler.py 中新增语境调度计划构建逻辑,识别显式延迟、明天/明早、晚安/睡觉、开会/上课、通勤、吃饭、洗澡、游戏、稍后等常见语义。

  • 所有语境推断出的时间仍受 min_interval_minutesmax_interval_minutes 约束,避免突破用户配置的调度边界。

  • core/chat_flow.py 中让私聊主动消息发送完成后使用新的调度计划,并将调度策略、原因、规则、来源和区间元信息写入 session_data

  • core/message_events.py 中缓存最近用户文本,作为轻量语境调度输入。

  • core/data_storage.py 中合并并保留新增调度元信息,避免重载后状态丢失。

📸 运行截图或测试结果 / Screenshots or Test Results

# codex/issue-26
python -m compileall -q E:\CodexWorkspaces\issue-26-check
# passed

git diff --check upstream/main..HEAD
# passed

Diff against upstream/main:
5 files changed, 407 insertions(+), 43 deletions(-)

✅ 检查清单 / Checklist

  • 😊 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。/ If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
  • 👀 我的更改经过了良好的测试,并已在上方提供了“运行截图”或“测试日志”。/ My changes have been well-tested, and "Screenshots" or "Test Logs" have been provided above.
  • 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 requirements.txt 文件中。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to requirements.txt.
  • 😮 我的更改没有引入恶意代码。/ My changes do not introduce malicious code.

❤️ CONTRIBUTING

  • 🥳 我已阅读并同意遵守该项目的 贡献指南 / I have read and agree to abide by the CONTRIBUTING of this project.

@sourcery-ai

sourcery-ai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

审阅者指南(Reviewer's Guide)

通过从最近的用户消息内容中推导下一次触发时间,实现上下文感知的主动聊天(proactive-chat)调度,同时持久化更丰富的调度元数据,并在会话之间安全地合并这些数据。

上下文主动调度的时序图(Sequence Diagram)

sequenceDiagram
    actor User
    participant EventsMixin
    participant TaskScheduler

    User->>EventsMixin: on_friend_message(event)
    EventsMixin->>EventsMixin: _extract_event_text(event)
    EventsMixin->>EventsMixin: session_temp_state[session_id].last_user_text = text

    User->>TaskScheduler: _schedule_next_chat_and_save(session_id)
    TaskScheduler->>TaskScheduler: _build_next_schedule_plan(session_id, session_config)
    TaskScheduler->>TaskScheduler: _get_schedule_bounds(schedule_conf)
    TaskScheduler->>TaskScheduler: _get_contextual_schedule_settings(schedule_conf)
    TaskScheduler->>TaskScheduler: _collect_contextual_schedule_texts(session_id, history_count)
    loop recent_texts
        TaskScheduler->>TaskScheduler: _predict_contextual_interval_from_text(text, min_interval, max_interval)
        alt [context match]
            TaskScheduler->>TaskScheduler: plan.strategy = contextual
        else [no match]
            TaskScheduler->>TaskScheduler: keep random fallback
        end
    end
    TaskScheduler->>TaskScheduler: _write_schedule_plan_to_session(session_payload, plan)
    TaskScheduler-->>User: next_trigger_time set from contextual or random plan
Loading

文件级变更(File-Level Changes)

Change Details Files
引入上下文调度规划辅助函数,用可复用的规划流水线替换仅随机调度。
  • 新增辅助方法用于计算调度边界、限制间隔范围,并解析上下文计时配置(包括历史条数限制和启用开关)。
  • 实现用于上下文规则的文本归一化、带抖动的间隔选择,以及下一次本地时间计算工具方法。
  • 新增上下文间隔预测器,从最近消息中识别显式延迟、与“明天”相关的短语,以及基于规则的提示(睡觉、会议、通勤、用餐、洗澡、游戏、短时间稍后、免打扰 DND)。
  • 从中英双语的“小时/分钟”表达中提取显式延迟时长,并对允许区间做安全防护。
  • 实现异步规划构建器,预先计算调度计划(随机兜底 + 上下文覆盖),以及将所有计划字段写入会话数据的写入器,包括 strategy/reason/rule/source,并在主调度路径中使用这些能力。
core/task_scheduler.py
在会话临时状态中缓存最新用户文本,为上下文调度提供输入。
  • 新增事件文本提取器,从不同的 event/message 属性中提取文本,并将其归一化为长度受限的字符串。
  • 对私聊消息捕获最后一条用户文本,并与最后消息时间戳一起存入 session_temp_state,供调度器复用。
core/message_events.py
在发送主动消息后重新调度聊天时使用上下文调度规划。
  • 在聊天流程的收尾逻辑中,在获取数据锁之前,为私聊会话预先构建调度计划。
  • 在锁内复用已计算的计划来更新会话调度字段,并将 run_date 传入调度任务负载中,避免在锁内重复计算。
core/chat_flow.py
在会话数据合并时保留扩展的调度元数据。
  • 扩展在合并中考虑的调度相关键列表,加入 strategy、reason、rule 和 source。
  • 确保当 base 和 incoming 的 last_scheduled_at 相等时,所有扩展调度键能与现有行为一致地被合并。
core/data_storage.py
扩展配置模式以支持上下文计时选项。
  • 在配置模式的调度设置部分增加 enable_contextual_timing 和 contextual_timing_history_count 字段。
_conf_schema.json

针对关联问题的评估(Assessment against linked issues)

Issue Objective Addressed Explanation
#26 实现一种主动消息调度机制,使用最近的会话上下文(而非纯随机的最小/最大静默间隔)来决定下一次触发时间。
#26 让上下文计时能够反映常见的用户状态(如睡觉、明天、会议/课堂、通勤、用餐、洗澡、玩游戏、短时间稍后),从而让后续消息更加自然、减少打扰感。
#26 增加配置与状态处理,用于控制和持久化上下文调度行为(例如启用/禁用上下文计时、历史窗口以及调度元数据)。

可能关联的问题(Possibly linked issues)


提示与命令(Tips and commands)

与 Sourcery 交互(Interacting with Sourcery)

  • 触发新评审: 在 pull request 中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的评审评论。
  • 从评审评论生成 GitHub issue: 在评审评论下回复,请求 Sourcery 从该评论创建一个 issue。你也可以直接回复 @sourcery-ai issue 来从该评论创建 issue。
  • 生成 pull request 标题: 在 pull request 标题中任意位置写上 @sourcery-ai,即可在任意时间生成标题。你也可以在 pull request 中评论 @sourcery-ai title 以(重新)生成标题。
  • 生成 pull request 总结: 在 pull request 正文中任意位置写上 @sourcery-ai summary,即可在你想要的位置生成 PR 总结。你也可以在 pull request 中评论 @sourcery-ai summary 以(重新)生成总结。
  • 生成审阅者指南: 在 pull request 中评论 @sourcery-ai guide,可在任意时间(重新)生成审阅者指南。
  • 解决所有 Sourcery 评论: 在 pull request 中评论 @sourcery-ai resolve,以一次性解决所有 Sourcery 评论。适用于你已经处理完所有评论且不希望再看到它们的情况。
  • 关闭所有 Sourcery 评审: 在 pull request 中评论 @sourcery-ai dismiss,以关闭所有已有的 Sourcery 评审。特别适用于你希望从零开始进行新一轮评审——别忘了再评论 @sourcery-ai review 来触发新的评审!

自定义你的体验(Customizing Your Experience)

访问你的 dashboard 以:

  • 启用或禁用评审功能,例如 Sourcery 生成的 pull request 总结、审阅者指南等。
  • 更改评审语言。
  • 添加、删除或编辑自定义评审指令。
  • 调整其他评审设置。

获取帮助(Getting Help)

Original review guide in English

Reviewer's Guide

Implements contextual proactive-chat scheduling by deriving next trigger times from recent user message content, while persisting richer schedule metadata and safely merging it across sessions.

Sequence diagram for contextual proactive scheduling

sequenceDiagram
    actor User
    participant EventsMixin
    participant TaskScheduler

    User->>EventsMixin: on_friend_message(event)
    EventsMixin->>EventsMixin: _extract_event_text(event)
    EventsMixin->>EventsMixin: session_temp_state[session_id].last_user_text = text

    User->>TaskScheduler: _schedule_next_chat_and_save(session_id)
    TaskScheduler->>TaskScheduler: _build_next_schedule_plan(session_id, session_config)
    TaskScheduler->>TaskScheduler: _get_schedule_bounds(schedule_conf)
    TaskScheduler->>TaskScheduler: _get_contextual_schedule_settings(schedule_conf)
    TaskScheduler->>TaskScheduler: _collect_contextual_schedule_texts(session_id, history_count)
    loop recent_texts
        TaskScheduler->>TaskScheduler: _predict_contextual_interval_from_text(text, min_interval, max_interval)
        alt [context match]
            TaskScheduler->>TaskScheduler: plan.strategy = contextual
        else [no match]
            TaskScheduler->>TaskScheduler: keep random fallback
        end
    end
    TaskScheduler->>TaskScheduler: _write_schedule_plan_to_session(session_payload, plan)
    TaskScheduler-->>User: next_trigger_time set from contextual or random plan
Loading

File-Level Changes

Change Details Files
Introduce contextual schedule planning helpers and replace random-only scheduling with a reusable planning pipeline.
  • Add helper methods to compute schedule bounds, clamp intervals, and parse contextual timing configuration including history limits and enable flag.
  • Implement text normalization, jittered interval selection, and next-local-time calculation utilities for contextual rules.
  • Add a contextual interval predictor that looks for explicit delays, tomorrow-related phrases, and rule-based hints (sleep, meeting, commute, meal, shower, game, short-later, DND) in recent messages.
  • Extract explicit delay durations from Chinese/English hour/minute expressions with guardrails on allowed ranges.
  • Implement an async plan builder that precomputes a schedule plan (random fallback plus contextual override) and a writer that persists all plan fields into session data, including strategy/reason/rule/source, and use these from the main scheduling path.
core/task_scheduler.py
Cache latest user text in session temp state to feed contextual scheduling.
  • Add an event-text extractor that pulls text from various event/message attributes and normalizes it into a bounded string.
  • Capture last user text for private messages and store it alongside last-message timestamps in session_temp_state for the scheduler to reuse.
core/message_events.py
Use contextual schedule planning when rescheduling chats after sending proactive messages.
  • In the chat flow finalize logic, pre-build a schedule plan for private sessions before acquiring the data lock.
  • Within the lock, reuse the computed plan to update session schedule fields and to pass the run_date into the scheduler job payload, avoiding recomputation under lock.
core/chat_flow.py
Preserve extended schedule metadata during session-data merges.
  • Extend the list of schedule-related keys considered in the merge to include strategy, reason, rule, and source.
  • Ensure that when last_scheduled_at is equal between base and incoming, all extended scheduling keys are merged consistently with existing behavior.
core/data_storage.py
Extend configuration schema to support contextual timing options.
  • Add enable_contextual_timing and contextual_timing_history_count fields to the schedule settings section of the configuration schema.
_conf_schema.json

Assessment against linked issues

Issue Objective Addressed Explanation
#26 Implement a proactive message scheduling mechanism that uses recent conversation context (rather than purely random min/max silence intervals) to decide the next trigger time.
#26 Make the contextual timing reflect common user states (e.g., sleep, tomorrow, meetings/classes, commuting, meals, shower, gaming, short-later) so that follow-up messages feel more natural and less interruptive.
#26 Add configuration and state handling to control and persist contextual scheduling behavior (e.g., enable/disable contextual timing, history window, and schedule metadata).

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@dosubot dosubot Bot added the size:L 修改了 100-499 行代码 (忽略生成文件) label Jun 11, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces contextual timing features that dynamically adjust the scheduling of active messages based on the semantic context of recent user messages (such as detecting keywords like '晚安' or explicit delays like '30分钟后'). The review feedback highlights three key issues: first, a missing last_user_time timestamp in on_friend_message causes the temporary session state to expire prematurely; second, simple substring matching for short English keywords (such as 'gn' or 'bed') can lead to false positives on words like 'design' or 'embed'; and third, the regex for parsing explicit delays incorrectly matches trailing digits in decimal values (e.g., parsing '1.5小时' as '5小时'). Suggestions with code blocks are provided to resolve each of these issues.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread core/message_events.py Outdated
Comment on lines +86 to +89
last_user_text = self._extract_event_text(event)
self.session_temp_state.setdefault(normalized_session_id, {})[
"last_user_text"
] = last_user_text

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.

high

In on_friend_message, the last_user_text is cached in self.session_temp_state but last_user_time is not set. During the periodic cleanup in _cleanup_expired_session_states, state.get("last_user_time", 0) will return 0 for friend sessions, causing current_time - last_user_time to always exceed the 300-second threshold. This leads to immediate deletion of the friend session's temporary state during the next cleanup cycle, breaking contextual scheduling for friend messages. Ensure last_user_time is also set when caching the text.

Suggested change
last_user_text = self._extract_event_text(event)
self.session_temp_state.setdefault(normalized_session_id, {})[
"last_user_text"
] = last_user_text
last_user_text = self._extract_event_text(event)
temp_state = self.session_temp_state.setdefault(normalized_session_id, {})
temp_state["last_user_text"] = last_user_text
temp_state["last_user_time"] = current_time

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.

已在 ea670ba 中修复:私聊缓存 last_user_text 时现在会同时写入 last_user_time,并放到 data_lock 内更新,避免下一轮 _cleanup_expired_session_states 把刚写入的临时语境立即清掉。

Comment thread core/task_scheduler.py Outdated
Comment on lines +321 to +330
for rule, markers, minute_range in rules:
if any(marker in normalized for marker in markers):
seconds = self._pick_schedule_jitter(*minute_range)
seconds = self._clamp_schedule_interval(seconds, min_interval, max_interval)
return {
"interval_seconds": seconds,
"strategy": "contextual",
"rule": rule,
"reason": f"context:{rule}",
}

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.

high

Using simple substring matching (marker in normalized) for short English keywords can lead to severe false positives. For example, "gn" will match words like "design" or "signal", "bed" will match "embed" or "obedient", and "exam" will match "example" or "examine". This can cause the scheduler to incorrectly apply long delays (e.g., 7-10 hours for "sleep_night" or 1.5-3 hours for "meeting_or_class") when a user simply uses common words. To prevent this, English alphabetic markers should be matched using word boundaries (\b), while Chinese markers can continue to use substring matching.

Suggested change
for rule, markers, minute_range in rules:
if any(marker in normalized for marker in markers):
seconds = self._pick_schedule_jitter(*minute_range)
seconds = self._clamp_schedule_interval(seconds, min_interval, max_interval)
return {
"interval_seconds": seconds,
"strategy": "contextual",
"rule": rule,
"reason": f"context:{rule}",
}
for rule, markers, minute_range in rules:
matched = False
for marker in markers:
if re.match(r"^[a-zA-Z\s]+$", marker):
if re.search(rf"\b{re.escape(marker)}\b", normalized):
matched = True
break
elif marker in normalized:
matched = True
break
if matched:
seconds = self._pick_schedule_jitter(*minute_range)
seconds = self._clamp_schedule_interval(seconds, min_interval, max_interval)
return {
"interval_seconds": seconds,
"strategy": "contextual",
"rule": rule,
"reason": f"context:{rule}",
}

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.

已在 ea670ba 中修复:新增 _contains_schedule_marker,英文关键词现在使用 (?<![a-z0-9])...(?![a-z0-9]) 边界匹配,避免 gn 命中 design/signalbed 命中 embedexam 命中 example 这类误判;中文关键词仍保留子串匹配。

Comment thread core/task_scheduler.py Outdated
Comment on lines +338 to +348
minute_match = re.search(r"(\d{1,3})\s*(分钟|分|mins?|minutes?)\s*(后|later)?", text)
if minute_match:
value = int(minute_match.group(1))
if 1 <= value <= 1440:
return value

hour_match = re.search(r"(\d{1,2})\s*(个)?\s*(小时|钟头|hours?|hrs?|h)\s*(后|later)?", text)
if hour_match:
value = int(hour_match.group(1))
if 1 <= value <= 48:
return value * 60

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.

high

The regular expressions for extracting explicit delay minutes and hours are prone to incorrect matches when users specify decimal/fractional values (e.g., "1.5小时" or "2.5小时"). Because re.search scans the string and matches any substring satisfying the pattern, "1.5小时" will fail to match at "1" (since it's followed by ".5"), but will successfully match "5小时" at index 2. This causes "1.5小时" (90 minutes) to be incorrectly parsed as "5小时" (300 minutes). Additionally, without boundary constraints, a longer sequence of digits (e.g., "1500分钟") can match its suffix (e.g., "500分钟"). To fix both issues, use a negative lookbehind (?<![\d.]) to ensure the matched digits are not preceded by another digit or a decimal point.

Suggested change
minute_match = re.search(r"(\d{1,3})\s*(分钟|分|mins?|minutes?)\s*(后|later)?", text)
if minute_match:
value = int(minute_match.group(1))
if 1 <= value <= 1440:
return value
hour_match = re.search(r"(\d{1,2})\s*(个)?\s*(小时|钟头|hours?|hrs?|h)\s*(后|later)?", text)
if hour_match:
value = int(hour_match.group(1))
if 1 <= value <= 48:
return value * 60
minute_match = re.search(r"(?<![\d.])(\d{1,3})\s*(分钟|分|mins?|minutes?)\s*(后|later)?", text)
if minute_match:
value = int(minute_match.group(1))
if 1 <= value <= 1440:
return value
hour_match = re.search(r"(?<![\d.])(\d{1,2})\s*(个)?\s*(小时|钟头|hours?|hrs?|h)\s*(后|later)?", text)
if hour_match:
value = int(hour_match.group(1))
if 1 <= value <= 48:
return value * 60

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.

已在 ea670ba 中修复:显式分钟/小时正则增加了数字和小数点边界,1.5小时后2.5 hours later 不会再被截取成 5小时/5 hours;整数表达如 30分钟后2小时后 仍可正常解析。

@Justice-ocr Justice-ocr changed the title fix: add contextual proactive scheduling fix: 添加语境感知主动消息调度 Jun 11, 2026

@sourcery-ai sourcery-ai Bot left a comment

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.

Hey - 我发现了 2 个问题,并留下了一些高层次反馈:

  • _predict_contextual_interval_from_text 中的上下文调度规则(关键词列表和分钟范围)目前都是硬编码的,这会让后续调优/本地化变得更困难;建议把这些规则移入某种配置结构中(例如放到 schedule_settings 或单独的映射里),这样可以在不改代码的情况下进行调整。
  • _collect_contextual_schedule_text 和事件处理器中对 session_temp_state 的访问目前没有同步;如果这个 dict 在任务之间共享,为了避免隐蔽的竞争条件,最好使用同一个 data_lock 或单独的锁来保护读写操作。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The contextual scheduling rules (keyword lists and minute ranges in `_predict_contextual_interval_from_text`) are all hard-coded, which will make future tuning/localization harder; consider moving these into a config structure (e.g., in `schedule_settings` or a separate mapping) so they can be adjusted without code changes.
- Access to `session_temp_state` in `_collect_contextual_schedule_text` and in the event handlers is currently unsynchronized; if this dict is shared across tasks, it would be safer to guard mutations/reads with the same `data_lock` or a dedicated lock to avoid subtle race conditions.

## Individual Comments

### Comment 1
<location path="core/chat_flow.py" line_range="92-95" />
<code_context>
         scheduled_job_payload = None
+        scheduled_plan = None
+
+        if is_private_session:
+            session_config = self._get_session_config(session_id)
+            if not session_config:
+                return
+            scheduled_plan = await self._build_next_schedule_plan(
+                session_id,
</code_context>
<issue_to_address>
**issue (bug_risk):** Early return for missing session_config now skips unanswered-count updates for private sessions.

This refactor changes behavior: for private sessions without a config, we now return before entering `data_lock`, so `unanswered_count` / `new_unanswered_count` are no longer updated. If that’s not intended, consider still doing the counter updates (e.g., by only moving the lookup outside the lock and delaying the early return, or handling the missing-config case inside the lock) so metrics stay consistent.
</issue_to_address>

### Comment 2
<location path="core/message_events.py" line_range="37-46" />
<code_context>
+                if value:
+                    candidates.append(value)
+
+        try:
+            messages = event.get_messages()
+        except Exception:
+            messages = []
+        if messages:
+            extractor = getattr(self, "_extract_platform_message_text", None)
+            if callable(extractor):
+                try:
+                    extracted = extractor(messages)
+                    if extracted:
+                        candidates.append(extracted)
+                except Exception:
+                    pass
</code_context>
<issue_to_address>
**issue (bug_risk):** _extract_platform_message_text may receive a messages list where it expects a single content object.

Here `extractor` is called with `messages` from `event.get_messages()` (likely a list/sequence), while `_collect_contextual_schedule_texts` calls the same extractor with a single `content` object. If `_extract_platform_message_text` is intended for a single message payload, this mixed usage can introduce subtle bugs or force it to handle multiple input shapes. Consider either iterating over `messages` and calling the extractor per message, or standardizing the extractor’s contract (single vs list) and updating all call sites accordingly.
</issue_to_address>

Sourcery 对开源项目免费——如果你喜欢我们的评审,请考虑分享它们 ✨
帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进后续的评审。
Original comment in English

Hey - I've found 2 issues, and left some high level feedback:

  • The contextual scheduling rules (keyword lists and minute ranges in _predict_contextual_interval_from_text) are all hard-coded, which will make future tuning/localization harder; consider moving these into a config structure (e.g., in schedule_settings or a separate mapping) so they can be adjusted without code changes.
  • Access to session_temp_state in _collect_contextual_schedule_text and in the event handlers is currently unsynchronized; if this dict is shared across tasks, it would be safer to guard mutations/reads with the same data_lock or a dedicated lock to avoid subtle race conditions.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The contextual scheduling rules (keyword lists and minute ranges in `_predict_contextual_interval_from_text`) are all hard-coded, which will make future tuning/localization harder; consider moving these into a config structure (e.g., in `schedule_settings` or a separate mapping) so they can be adjusted without code changes.
- Access to `session_temp_state` in `_collect_contextual_schedule_text` and in the event handlers is currently unsynchronized; if this dict is shared across tasks, it would be safer to guard mutations/reads with the same `data_lock` or a dedicated lock to avoid subtle race conditions.

## Individual Comments

### Comment 1
<location path="core/chat_flow.py" line_range="92-95" />
<code_context>
         scheduled_job_payload = None
+        scheduled_plan = None
+
+        if is_private_session:
+            session_config = self._get_session_config(session_id)
+            if not session_config:
+                return
+            scheduled_plan = await self._build_next_schedule_plan(
+                session_id,
</code_context>
<issue_to_address>
**issue (bug_risk):** Early return for missing session_config now skips unanswered-count updates for private sessions.

This refactor changes behavior: for private sessions without a config, we now return before entering `data_lock`, so `unanswered_count` / `new_unanswered_count` are no longer updated. If that’s not intended, consider still doing the counter updates (e.g., by only moving the lookup outside the lock and delaying the early return, or handling the missing-config case inside the lock) so metrics stay consistent.
</issue_to_address>

### Comment 2
<location path="core/message_events.py" line_range="37-46" />
<code_context>
+                if value:
+                    candidates.append(value)
+
+        try:
+            messages = event.get_messages()
+        except Exception:
+            messages = []
+        if messages:
+            extractor = getattr(self, "_extract_platform_message_text", None)
+            if callable(extractor):
+                try:
+                    extracted = extractor(messages)
+                    if extracted:
+                        candidates.append(extracted)
+                except Exception:
+                    pass
</code_context>
<issue_to_address>
**issue (bug_risk):** _extract_platform_message_text may receive a messages list where it expects a single content object.

Here `extractor` is called with `messages` from `event.get_messages()` (likely a list/sequence), while `_collect_contextual_schedule_texts` calls the same extractor with a single `content` object. If `_extract_platform_message_text` is intended for a single message payload, this mixed usage can introduce subtle bugs or force it to handle multiple input shapes. Consider either iterating over `messages` and calling the extractor per message, or standardizing the extractor’s contract (single vs list) and updating all call sites accordingly.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread core/chat_flow.py Outdated
Comment thread core/message_events.py Outdated
@Justice-ocr

Copy link
Copy Markdown
Author

已根据 review 推送 ea670ba,主要处理这些问题:

  • 私聊语境缓存现在同时写入 last_user_time,避免被临时状态清理器立即删除。
  • _extract_event_text 改为逐条 message 调用 _extract_platform_message_text,不再把列表整体传入。
  • 英文语境关键词改为边界匹配,避免 gn/designbed/embedexam/example 这类误命中。
  • 显式时间解析增加数字/小数点边界,避免 1.5小时后 被错误解析成 5小时后
  • 私聊缺少 session_config 时不再提前跳过未回复计数,只跳过后续调度计划。
  • _collect_contextual_schedule_texts 读取 session_temp_state 时会在锁内复制快照,减少共享 dict 的竞态风险。

验证:

python -m compileall -q E:\CodexWorkspaces\New project\astrbot_plugin_proactive_chat
# passed

git diff --check upstream/main..HEAD
# passed

额外针对性检查:design/embed/example/signal 不触发英文短词规则;1.5小时/2.5 hours 不再被截断解析;30分钟/2小时仍可解析。

关于 Sourcery 提到“规则硬编码”的建议:这个方向我认同,但为了让本 PR 继续聚焦 #26 的最小可审阅修复,暂时没有把规则表完全配置化;如果维护者希望,我可以后续单独拆一个 PR 做规则配置化/本地化。

@DBJD-CR

DBJD-CR commented Jun 11, 2026

Copy link
Copy Markdown
Owner

这个我个人建议还是用 LLM 来判断下一次进行主动消息的时间,纯靠词表的实现太拉跨了。类似的实现可以参考其他主动回复型的插件,套在主动消息上也可以。

@dosubot dosubot Bot added size:XL 修改了 500-999 行代码 (忽略生成文件) and removed size:L 修改了 100-499 行代码 (忽略生成文件) labels Jun 12, 2026
@Justice-ocr

Copy link
Copy Markdown
Author

补充说明一下最新一次调整的逻辑:

  • 保留了基于 LLM 的语境判断路径:下一次主动消息时间仍会结合最近用户消息、当前时间段和配置范围来决定,而不是只靠固定词表命中。
  • 为了降低维护成本,把语境调度相关逻辑从 core/task_scheduler.py 拆到了 core/contextual_scheduler.pyContextualSchedulePlanner 中;task_scheduler.py 现在只负责调度流程编排,具体的语境分析、JSON 解析、回退规则都收敛在独立模块里。
  • 如果语境感知未启用、LLM 调用失败、返回无法解析或给出的时间不合法,会回退到原有范围内的规则调度,避免因为新逻辑导致任务无法继续安排。
  • 已在本地重新跑过 Python 编译检查和 diff 空白检查,拆分本身不改变外部配置项和调用方式。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 修改了 500-999 行代码 (忽略生成文件)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants