Skip to content

feat: add AstrBot Pages console, public API, and contextual scheduling#76

Draft
Justice-ocr wants to merge 44 commits into
DBJD-CR:mainfrom
Justice-ocr:codex/ai-pages-contextual-scheduling
Draft

feat: add AstrBot Pages console, public API, and contextual scheduling#76
Justice-ocr wants to merge 44 commits into
DBJD-CR:mainfrom
Justice-ocr:codex/ai-pages-contextual-scheduling

Conversation

@Justice-ocr
Copy link
Copy Markdown

说明

这个 PR 由 AI 辅助生成并在本地做了基础验证。由于改动范围较大、实现仍比较粗糙,各方面都还有待完善:代码结构、UI 细节、兼容性、测试覆盖和功能取舍都需要上游维护者进一步审阅、调整或拆分。建议先按 draft PR 处理,不建议未经复核直接合并。

与上游 main 对比后的主要变化

相对上游 DBJD-CR:main,本分支包含 33 个提交,约 48 个文件变更,主要解决或推进了以下问题:

1. AstrBot Pages 管理页面支持 / #74

  • 新增 pages/proactive-chat/ 下的 AstrBot Pages 管理页面。
  • 后端新增 Pages API 注册与数据接口,见 core/web_admin_server.py
  • Pages 页面现在覆盖运行状态、任务管理、通知中心、文档浏览、全局配置和会话配置等核心入口。
  • 修复了前期 Pages 中遇到的空白页、资源加载、logo 显示、任务页空白、倒计时缺失等问题。
  • 配置保存逻辑改为通过 Pages bridge 调用后端 save_config / get_config,保存后会回读校验,避免“保存后刷新又变回去”。

2. 插件间公开 API / #22

  • 新增 core/public_api.py,并在 main.py 混入。
  • 提供轻量公开方法,方便其他插件查询和控制主动消息能力:
    • get_proactive_chat_status
    • list_proactive_chat_jobs
    • list_proactive_chat_sessions
    • trigger_proactive_chat
    • reschedule_proactive_chat
    • cancel_proactive_chat_job

3. 基于语境的触发时间预测 / #26

  • 在原有随机区间调度基础上新增可开关的语境调度能力。
  • 新增配置项:
    • enable_contextual_timing
    • contextual_timing_history_count
  • 会根据最近消息中的明显语义调整下一次主动触发时间,例如:晚安/睡觉、看电影/追剧、开会/上课、通勤路上、吃饭、稍后再聊等。
  • 没有识别到明确语境时仍回退到原随机区间。
  • 预测结果仍受 min_interval_minutesmax_interval_minutes 限制,不会突破用户配置边界。
  • Pages 任务页和倒计时卡片会显示本次调度是“随机区间”还是“语境预测”,以及命中的规则原因。

4. 上下文与平台历史增强

  • 增强主动消息生成时的上下文注入能力。
  • 支持从 AstrBot conversation history、平台消息历史或混合模式构建上下文。
  • 增加轻量会话态势摘要,帮助主动消息更自然地延续最近话题。

已做的本地验证

  • node --check pages/proactive-chat/page-app.js
  • python -m json.tool _conf_schema.json
  • python -m compileall -q .
  • git diff --check

风险与待完善点

  • 这是一个较大的功能性 PR,建议维护者按模块拆分审阅。
  • Pages UI 中仍有一部分早期 React/bundle 资产保留,后续可以清理或决定是否保留构建链路。
  • 语境调度目前主要是规则型启发式判断,不是完整的 LLM 语义预测;它先覆盖常见明确场景,仍需要更多真实使用数据调优。
  • 缺少正式测试用例,目前只有基础编译、JSON、JS 语法和 diff 检查。
  • UI 文案、交互和 AstrBot 不同版本兼容性仍需要上游进一步验证。

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Sorry @Justice-ocr, your pull request is larger than the review limit of 150000 diff characters

Copy link
Copy Markdown
Contributor

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

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 a comprehensive contextual timing scheduler and a feature-rich Web administration console for the proactive chat plugin, including a lightweight public API, event text extraction, and platform conversation insights. The frontend is built using React and bundled via a custom Babel-based build tool. Key feedback includes addressing missing imports in web_admin_server.py that would cause runtime errors, fixing a parsing bug for composite time expressions and adding exception handling for integer conversions in task_scheduler.py, preventing premature returns on exceptions in llm_adapter.py's history extraction, and safeguarding the frontend against invalid timezone configurations in formatters.js to prevent rendering crashes.

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/web_admin_server.py
Comment on lines 3 to 9
from __future__ import annotations

import asyncio
import inspect
import json
import math
import os
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

_open_directory_payloadupdate_config 等方法中使用了 syssubprocessPath (来自 pathlib) 以及 JSONResponse (来自 fastapi.responses),但这些模块和类并未在文件顶部导入。这会在运行时触发 NameError。建议在文件顶部补齐这些导入。

Suggested change
from __future__ import annotations
import asyncio
import inspect
import json
import math
import os
from __future__ import annotations
import asyncio
import inspect
import json
import math
import os
import sys
import subprocess
from pathlib import Path
from fastapi.responses import JSONResponse

Comment thread core/task_scheduler.py
Comment on lines +334 to +350
def _extract_explicit_delay_minutes(self, text: str) -> int | None:
if "半小时" in text or "半个小时" in text:
return 30

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

return None
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

_extract_explicit_delay_minutes 在解析复合时间表达式(例如 "1小时10分钟")时存在逻辑缺陷。由于它会优先匹配 "10分钟" 并直接返回,导致 "1小时" 的部分被完全忽略,从而返回 10 分钟而不是 70 分钟。建议将其改为累加解析,以同时支持单一和复合时间表达式。

    def _extract_explicit_delay_minutes(self, text: str) -> int | None:
        if "半小时" in text or "半个小时" in text:
            return 30

        total_minutes = 0
        has_match = False

        hour_match = re.search(r"(\\d{1,2})\\s*(个)?\\s*(小时|钟头|hours?|hrs?|h)", text)
        if hour_match:
            total_minutes += int(hour_match.group(1)) * 60
            has_match = True

        minute_match = re.search(r"(\\d{1,3})\\s*(分钟|分|mins?|minutes?)", text)
        if minute_match:
            total_minutes += int(minute_match.group(1))
            has_match = True

        if has_match and 1 <= total_minutes <= 2880:
            return total_minutes

         return None

Comment thread core/task_scheduler.py
Comment on lines +178 to +183
def _get_schedule_bounds(self, schedule_conf: dict) -> tuple[int, int]:
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
)
return min_interval, max_interval
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

_get_schedule_bounds 在从配置中读取 min_interval_minutesmax_interval_minutes 并转换为整数时,缺少异常处理。如果用户在配置中填写了非数字字符串或空值,会导致 ValueErrorTypeError 从而使调度器崩溃。建议添加异常处理以提高健壮性。

    def _get_schedule_bounds(self, schedule_conf: dict) -> tuple[int, int]:
        try:
            min_val = int(schedule_conf.get("min_interval_minutes", 30))
        except (ValueError, TypeError):
            min_val = 30
        try:
            max_val = int(schedule_conf.get("max_interval_minutes", 900))
        except (ValueError, TypeError):
            max_val = 900
        min_interval = min_val * 60
        max_interval = max(min_interval, max_val * 60)
        return min_interval, max_interval

Comment thread core/llm_adapter.py Outdated
Comment on lines +468 to +473
def _extract_history_message_text(self, message: Any) -> str:
if hasattr(message, "to_dict"):
try:
message = message.to_dict()
except Exception:
return ""
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

_extract_history_message_text 中,如果 message.to_dict() 抛出异常,当前代码会直接返回 ""。然而,该对象可能仍然拥有一个有效的 content 属性,可以通过后续的 getattr(message, "content", "") 兜底逻辑正常获取。建议在 except 块中使用 pass 而不是直接返回,以允许兜底逻辑执行,提高防御性编程的容错能力。

Suggested change
def _extract_history_message_text(self, message: Any) -> str:
if hasattr(message, "to_dict"):
try:
message = message.to_dict()
except Exception:
return ""
def _extract_history_message_text(self, message: Any) -> str:
if hasattr(message, "to_dict"):
try:
message = message.to_dict()
except Exception:
pass

Comment on lines +70 to +79

// IANA 模式交给 Intl 处理,可正确覆盖夏令时等复杂时区规则。
const formatter = new Intl.DateTimeFormat('zh-CN', {
year: includeYear ? 'numeric' : undefined,
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: includeSeconds ? '2-digit' : undefined,
hour12: false,
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

如果用户在配置中填写了无效的时区字符串(例如拼写错误),Intl.DateTimeFormat 会抛出 RangeError 并导致整个前端页面渲染崩溃。建议使用 try-catch 块包裹该实例化过程,并在捕获到错误时优雅地降级到默认时区(如 'Asia/Shanghai'),以防止页面白屏。

        let formatter;
        try {
            formatter = new Intl.DateTimeFormat('zh-CN', { 
                year: includeYear ? 'numeric' : undefined,
                month: '2-digit',
                day: '2-digit',
                hour: '2-digit',
                minute: '2-digit',
                second: includeSeconds ? '2-digit' : undefined,
                hour12: false,
                timeZone: tz.value,
            });
        } catch (e) {
            formatter = new Intl.DateTimeFormat('zh-CN', { 
                year: includeYear ? 'numeric' : undefined,
                month: '2-digit',
                day: '2-digit',
                hour: '2-digit',
                minute: '2-digit',
                second: includeSeconds ? '2-digit' : undefined,
                hour12: false,
                timeZone: 'Asia/Shanghai',
            });
        }

@DBJD-CR DBJD-CR added size:XXL 修改了 1000+ 行代码 (忽略生成文件) type/feat ✨ 新功能 / New Feature status/wip 🚧 施工中 / Do Not Merge type/design 🎨 交互设计、UI/UX 方案讨论 labels Jun 6, 2026
@Justice-ocr Justice-ocr force-pushed the codex/ai-pages-contextual-scheduling branch from d0885ca to 073d9db Compare June 7, 2026 04:30
@Justice-ocr
Copy link
Copy Markdown
Author

补充说明一下本 PR 后续根据 review 和实际运行反馈追加的修正。本 PR 仍然是 AI 辅助生成/迭代的改动,覆盖面比较大,仍希望上游维护者继续谨慎 review。

针对 review 中指出的问题,当前分支已经做了这些处理:

  • core/web_admin_server.py:补齐 syssubprocessPathJSONResponse 等缺失导入,避免 Web 管理端相关路径在运行时触发 NameError
  • core/task_scheduler.py:增强调度配置解析健壮性,min_interval_minutes / max_interval_minutes 遇到空值或非数字字符串时回退默认值;同时改进显式时间解析,支持类似“1小时10分钟”的复合表达式累加。
  • core/llm_adapter.py:历史消息文本提取中,to_dict() 失败后不再直接返回空文本,而是继续走 content 等兜底逻辑。
  • pages/proactive-chat/js/utils/formatters.js:无效时区会降级到默认时区,避免 Intl.DateTimeFormatRangeError 导致 Pages 白屏。

后续实际测试里还补了几类运行时问题:

  • 增加主动消息目标去重和执行中 guard,减少同一私聊目标因不同 UMO/重复任务导致并发发送。
  • 未回复次数达到上限后会清理/隐藏对应任务,避免前端仍显示任务倒计时、后端仍反复尝试。
  • 私聊中的 Bot 自身消息不会再被当作用户回复,因此其它插件发出的 Bot 消息不会重置本插件的未回复次数。
  • 修复 URL 编码会话 ID、异常处理器初始化、Pages 任务计数等运行中发现的问题。

最新提交 a02544f (fix: delay proactive sends after external bot messages) 的作用是处理“其它插件刚给同一用户/会话发过消息”时的插话问题:

  • 通过 after_message_sent 记录同一会话的外部 Bot 发送时间。
  • 如果本插件已有主动消息任务,会把触发时间顺延到“外部 Bot 消息之后 + 原本调度间隔”,但不重置 unanswered_count
  • 如果仍处在自动触发倒计时或群聊沉默倒计时,也会对应后推计时器。
  • 如果外部 Bot 消息发生在本插件 LLM 生成期间,会丢弃本次生成结果并重新排期,避免生成完后紧接着插话。
  • 同时用短暂发送标记区分本插件自己的主动消息,避免本插件自己的发送被误识别为“其它插件刚发过消息”。

行为边界:这个逻辑只响应 AstrBot 的 after_message_sent 事件。其它插件如果只是做后端处理、调用 LLM、准备内容但最终没有触发消息发送事件,不会导致本插件延后。若 AstrBot 在平台实际投递失败前也触发了 after_message_sent,当前实现会保守地认为 Bot 刚发过消息并顺延。

本地已跑过:

python -m compileall -q .
git diff --check

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

Labels

size:XXL 修改了 1000+ 行代码 (忽略生成文件) status/wip 🚧 施工中 / Do Not Merge type/design 🎨 交互设计、UI/UX 方案讨论 type/feat ✨ 新功能 / New Feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants