Skip to content
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ L1 临近上限时把细节下放到 L2,只留指针。

整理职责:去重合并、重要性校准与标签统一、画像精炼、知识沉淀、临近上限时压缩 L1。

调度可靠性:待执行任务的触发时刻随状态一起落盘,重启后会补排(停机期间错过的窗口
一分钟内补跑);任务的 `misfire_grace_time` 置空,事件循环被卡住也不会把这次运行
静默丢弃;单轮失败按 30 分钟间隔最多重试 3 次,用户再发消息会重置重试计数。

## 嵌入模型与重映射

向量只在同一模型下可比,换模型会让旧向量静默劣化且不报错。插件在集合 metadata 里记
Expand Down Expand Up @@ -223,6 +227,7 @@ L1 临近上限时把细节下放到 L2,只留指针。
/memory group delete <ID> 删除群共享记忆(仅管理员 / 群主)
/memory memo 以合并转发查看当前作用域备忘录
/memory dmn 立即触发一轮巩固循环
/memory heatmap 渲染作息热度图(含下次运行时间)
```

`memo` 只读:查看谁都有权限,写入走工具。群里 `/memory memo` 看的是群备忘录,
Expand All @@ -232,6 +237,10 @@ L1 临近上限时把细节下放到 L2,只留指针。
按未知选项处理。鉴权顺序为超级用户白名单,其次 `lp.admin` 权限节点。
还需要 `subconscious.enabled = true` 且已配好 `target_user_id`。

`heatmap`(别名 `heat`、`热图`、`热度图`)同样隐藏且鉴权一致:发一张 7×12 的作息
热度图,格内数字是原始观测条数,颜色按半衰期衰减后的权重取色,图上标出当前时槽
与下次 DMN 运行时间(含窗口长度、分层与 penalty)。

兼容旧格式 `/memory list` 等,默认以个人范围执行。群聊里 `user` 与 `group`
两个范围都可用,私聊只有 `user`。

Expand Down Expand Up @@ -457,6 +466,7 @@ amrita_plugin_memory/
├── runner.py # SubconsciousRunner — 核心编排器
├── hooks.py # on_precompletion hook / 生命周期管理
├── schedule.py # 空闲窗口规划(纯数据 + 纯函数,唯一时区解析入口)
├── heatmap.py # 热度图渲染(Pillow,纯渲染不碰运行时)
├── tools.py # 21 个巩固循环工具 handler
├── schemas.py # ToolFunctionSchema 定义
└── knowledge.py # KnowledgeBaseManager — 三方同步知识库
Expand Down
70 changes: 69 additions & 1 deletion amrita_plugin_memory/matchers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""NoneBot2 命令:/memory"""

from typing import cast
from datetime import datetime, tzinfo
from typing import Any, cast

from amrita.plugins.chat.utils.functions import split_message_into_chats
from amrita.plugins.menu.models import MatcherData
Expand Down Expand Up @@ -66,6 +67,12 @@ async def _(
await matcher.finish(f"未知选项:{sub}")
await matcher.finish(await _trigger_dmn())

if sub in ("heatmap", "heat", "热图", "热度图"):
if not await _is_admin(event):
await matcher.finish(f"未知选项:{sub}")
await _send_heatmap(matcher)
await matcher.finish()

ope = AsyncUserMemory(get_db_conn())
await ope.init()

Expand Down Expand Up @@ -148,6 +155,67 @@ async def _trigger_dmn() -> str:
return "已触发,潜意识将在后台跑一轮"


_WD_SHORT = ("周一", "周二", "周三", "周四", "周五", "周六", "周日")


async def _send_heatmap(matcher: Matcher) -> None:
"""渲染 7×12 热度图并发送,附带下次运行时间。"""
from .rethinking import _state
from .rethinking.heatmap import render_heatmap_png

cfg = (await DataManager().safe_get_config()).subconscious
if not cfg.enabled:
await matcher.finish("潜意识调度未启用:subconscious.enabled 为 false")
runner = _state.get_runner()
if runner is None:
await matcher.finish("潜意识调度未启动:钩子挂载失败或启动异常,检查日志")

snap = runner.scheduler_snapshot()
planner = runner.planner
caption = _heatmap_caption(snap, planner.tz)
try:
png = render_heatmap_png(
planner,
next_run=snap["next_run"],
plan=snap["plan"],
penalty=int(snap["penalty"]),
)
except Exception as e:
# 出图失败不能连文字状态一起吞掉,退回纯文本
logger.opt(exception=e, colors=True, raw=True).error(
f"[Subconscious] Heatmap render failed: {e}"
)
await matcher.finish(caption)

await matcher.send(MessageSegment.image(png))
await matcher.finish(caption)


def _heatmap_caption(snap: dict[str, Any], tz: tzinfo) -> str:
"""图下那行状态:下次触发时刻、窗口长度与当前是否在跑。"""
next_run: datetime | None = snap["next_run"]
if next_run is None:
head = "下次运行:尚未排期,等用户下一条消息触发规划"
else:
local = next_run.astimezone(tz)
head = (
f"下次运行:{local.strftime('%m-%d')} "
f"{_WD_SHORT[local.weekday()]} {local.strftime('%H:%M')}"
)

plan = snap["plan"]
if plan is None:
return f"{head}\n暂无规划记录,热度图只反映历史观测。"
end = plan.end.astimezone(tz)
state = "运行中" if snap["is_running"] else "待命"
return (
f"{head}\n"
f"窗口 {plan.start.strftime('%H:%M')}-{end.strftime('%H:%M')} · "
f"{plan.length_minutes} 分钟 · tier={plan.tier} · "
f"penalty #{snap['penalty']} · {state}"
)


MEMO_SEGMENT_CHARS = 600


Expand Down
Loading
Loading