Compare commits

..
2 Commits
Author SHA1 Message Date
miaomiao f2678024eb release: v0.2.0(三模式/睡眠HH:MM/极客高级项/苏醒重置/禁止动作描写) 2026-08-12 20:38:01 +08:00
miaomiao bac4f7c173 feat: 禁止动作/神态描写强化 + 苏醒时间段 22 维情绪重置
1) 禁止动作/神态描写(用户确认 revert 后仍泛滥,恢复 498eba9 并强化):
   - prompts/director_prompt.py 第一层输出规则:风格提示词只许写说话方式
     (语气/话量/语速/用词/停顿/口头禅),严禁动作/神态/肢体/场景描写,
     点名禁用 指尖/歪头/瞥/戳/懒洋洋/把屏幕划到一边/伸手/托腮/眯眼/撩/
     伸懒腰/叹气+动作;每句必须能直接转化为'这句话怎么说'。
   - 全局规则 #3:只写说话方式,动作描写会让主 AI 胡编戏(含具体例子)。
   - 新增输出规范:风格提示词是唯一允许出现在这里的描述性指令,且只描述
     语言本身。

2) 苏醒重置(core/environment.py + main.py):
   - maybe_wake_reset(engine, now_ts):已过苏醒时间点(_in_sleep_window 取反,
     分钟粒度支持跨午夜 23:00→05:00)且本天(YYYY-M-D)未重置过 → 22 维处理:
     负面(当前 v<0)强度降到 min(现有, max(0.05, decay_floor));正面(v>=0)
     强度 ×0.5;更新 _last_wake_reset_key + 日志。每天一次,惰性检查。
   - main.py on_llm_request 与 maybe_decay 并列调用(try/except 不炸请求)。
   - 无新配置(复用 sleep_start/sleep_end/decay_floor)。

冒烟: 428 → 440 全过(禁止描写 6 项静态断言 + 苏醒重置 7 项:负面降底/正面减半/
已低保持/每天一次/深夜不执行/跨天再执行)。
2026-08-11 14:51:36 +08:00
7 changed files with 93 additions and 7 deletions
+44
View File
@@ -46,6 +46,8 @@ class EnvironmentModule:
# FIX(M1): 上次轻提示注入时间戳——轻提示(主 AI 提示)也走 3h 限频, # FIX(M1): 上次轻提示注入时间戳——轻提示(主 AI 提示)也走 3h 限频,
# 3h 内静默(count 仍累积喂下次 force # 3h 内静默(count 仍累积喂下次 force
self._last_light_inject_ts: float = 0.0 self._last_light_inject_ts: float = 0.0
# FIX(苏醒重置): 上次苏醒重置日期键("YYYY-M-D")——每天只执行一次(惰性)
self._last_wake_reset_key: str = ""
# ⑤ 天气缓存 # ⑤ 天气缓存
self._weather_cache: dict | None = None self._weather_cache: dict | None = None
self._weather_cache_ts: float = 0.0 self._weather_cache_ts: float = 0.0
@@ -69,6 +71,48 @@ class EnvironmentModule:
if eng is not None: if eng is not None:
self.decay(eng) self.decay(eng)
# ---------- 苏醒重置(FIX: 22 维情绪每天苏醒清一次) ----------
def maybe_wake_reset(self, engine: Any, now_ts: float | None = None) -> None:
"""已过苏醒时间点 → 22 维情绪重置(**每天只执行一次**,惰性检查)。
需求(用户):苏醒后——负面情绪(当前 v < 0)强度直接降低到最低值
``max(0.05, decay_floor)`` 附近,不高于现有),正面情绪(v >= 0)强度减半。
判定"已苏醒":当前分钟不在睡眠时段(`_in_sleep_window` 取反,支持跨午夜
23:00→05:00);本天(YYYY-M-D)已重置过 → 跳过。
"""
now = now_ts if now_ts is not None else time.time()
lt = time.localtime(now)
start_min, end_min = self._sleep_window()
now_min = lt.tm_hour * 60 + lt.tm_min
# 睡眠时段内 → 不重置
if self._in_sleep_window(now_min, start_min, end_min):
return
# 当天已重置 → 跳过(每天一次)
day_key = f"{lt.tm_year}-{lt.tm_mon}-{lt.tm_mday}"
if day_key == self._last_wake_reset_key:
return
eng = engine if engine is not None else self._engine()
if eng is None:
return
floor = max(0.0, min(0.1, float(getattr(self.star.config, "decay_floor", 0.03) or 0.03)))
neg_low = max(0.05, floor) # 负面降到的最低值(不低于 0.05)
for _k, d in eng.dims.items():
try:
v = float(d.get("v", 0.0))
cur = float(d.get("intensity") or 0.0)
except (TypeError, ValueError): # noqa: BLE001 —— 坏值跳过
continue
if v < 0:
# 直接降低到最低值(不高于现有;已有更低则保持)
d["intensity"] = round(min(cur, neg_low), 4)
else:
d["intensity"] = round(cur * 0.5, 4)
self._last_wake_reset_key = day_key
self.logger.info(
"RainMood 苏醒重置:22 维情绪已处理(负面→≤%.2f,正面×0.5", neg_low,
)
def decay(self, engine: Any, now_ts: float | None = None) -> None: def decay(self, engine: Any, now_ts: float | None = None) -> None:
"""① 时间驱动衰减(合并 ②③ 昼夜选表)。 """① 时间驱动衰减(合并 ②③ 昼夜选表)。
+3 -1
View File
@@ -188,9 +188,11 @@ class Rainmood(Star):
_prune_dict(self.round_counters) # FIX(M4): 轮次计数无上限 → 超限清最旧 _prune_dict(self.round_counters) # FIX(M4): 轮次计数无上限 → 超限清最旧
# FIX(N2): 读取/注入前惰性时间驱动衰减(无消息期间静置,来消息一次性补齐) # FIX(N2): 读取/注入前惰性时间驱动衰减(无消息期间静置,来消息一次性补齐)
# FIX(苏醒重置): 并列做苏醒重置(已过 sleep_end → 22 维情绪清一次,每天一次)
try: try:
self.environment.maybe_decay() self.environment.maybe_decay()
except Exception as e: # noqa: BLE001 —— 衰减失败绝不能炸掉 LLM 请求 self.environment.maybe_wake_reset(self.director.engine)
except Exception as e: # noqa: BLE001 —— 衰减/重置失败绝不能炸掉 LLM 请求
self.logger.warning(f"RainMood 环境衰减失败: {e}") self.logger.warning(f"RainMood 环境衰减失败: {e}")
prompt_text = (req.prompt or "").strip() prompt_text = (req.prompt or "").strip()
+1 -1
View File
@@ -1,5 +1,5 @@
name: astrbot_plugin_rainmood name: astrbot_plugin_rainmood
version: 0.1.0 version: 0.2.0
desc: RainMood 雨喵情绪系统 — 情绪表 + 情绪支配AI + 说话风格注入 + 跨群上下文 desc: RainMood 雨喵情绪系统 — 情绪表 + 情绪支配AI + 说话风格注入 + 跨群上下文
author: miaomiao author: miaomiao
repo: https://git.rainnya.asia/miaomiao/astrbot_plugin_rainmood repo: https://git.rainnya.asia/miaomiao/astrbot_plugin_rainmood
+1 -1
View File
@@ -1,5 +1,5 @@
/* ============================================================ /* ============================================================
* RAINMOOD · RainMD 情绪控制台 · app.js (v0.0.1 SPA) * RAINMOOD · RainMD 情绪控制台 · app.js (v0.2.0 SPA)
* 纯静态、零依赖。单密码登录的后台管理(独立 Web 服务器模式, * 纯静态、零依赖。单密码登录的后台管理(独立 Web 服务器模式,
* 不依赖 window.AstrBotPluginPage bridge);后端未接入时自动降级 * 不依赖 window.AstrBotPluginPage bridge);后端未接入时自动降级
* 为演示数据(字段结构对齐设计文档 V7)。 * 为演示数据(字段结构对齐设计文档 V7)。
+2 -2
View File
@@ -34,7 +34,7 @@
<div class="brand-block"> <div class="brand-block">
<p class="brand-mark">RAIN<br>MOOD</p> <p class="brand-mark">RAIN<br>MOOD</p>
<h1 class="brand-name">RainMD 情绪控制台</h1> <h1 class="brand-name">RainMD 情绪控制台</h1>
<p class="brand-ver">astrbot_plugin_rainmood · v0.0.1</p> <p class="brand-ver">astrbot_plugin_rainmood · v0.2.0</p>
</div> </div>
<nav class="side-nav" aria-label="主导航"> <nav class="side-nav" aria-label="主导航">
@@ -63,7 +63,7 @@
<!-- ======== 顶栏 ======== --> <!-- ======== 顶栏 ======== -->
<header class="appbar"> <header class="appbar">
<div class="appbar-inner"> <div class="appbar-inner">
<span class="appbar-brand">RAINMOOD <i>·</i> v0.0.1</span> <span class="appbar-brand">RAINMOOD <i>·</i> v0.2.0</span>
<h2 class="page-title" id="pageTitle">仪表盘</h2> <h2 class="page-title" id="pageTitle">仪表盘</h2>
<span class="sync" id="sync">最近同步 --:--:--</span> <span class="sync" id="sync">最近同步 --:--:--</span>
<span class="sse-tag"><i class="sse-led" id="sseLed" aria-hidden="true"></i>SSE</span> <span class="sse-tag"><i class="sse-led" id="sseLed" aria-hidden="true"></i>SSE</span>
+4 -2
View File
@@ -73,9 +73,11 @@ tag3(强度):次要情绪
规则: 规则:
- tag1 是主情绪,tag2/tag3 是底色,最多三个,日常只有 tag1 也正常 - tag1 是主情绪,tag2/tag3 是底色,最多三个,日常只有 tag1 也正常
- 强度用:低 / 中 / 高 / 极 - 强度用:低 / 中 / 高 / 极
- **风格提示词必须是"怎么说话"的一句人话**,不是情绪描述,不是"以开心的语气回复"这种翻译腔 - **风格提示词必须是"怎么说话"的一句人话**,不是情绪描述,不是"以开心的语气回复"这种翻译腔
- **风格提示词只许写"说话方式"**(语气/话量/语速/用词/停顿/口头禅),**严禁任何动作、神态、肢体、场景描写**——点名禁用:指尖/歪头/瞥/戳/懒洋洋/把屏幕划到一边/伸手/托腮/眯眼/撩/伸懒腰/叹气+动作等;不得描述"她/他正在做什么、周围环境如何、手里拿着什么"。风格提示词的**每一句都必须能直接转化为"这句话怎么说"**,而不是"这个动作怎么做"
- **情绪来源(完整溯源)**:包含 ①对象(谁,昵称/UID)②会话(哪个群/私聊,会话ID)③事件(说了/做了什么,**只写事实原文**,一句话)。**只给事实,不给你的解释性判断**(如"{bot_name}觉得被瞧不起了"这类感受结论留在第二层 JSON,不进第一层——主 AI 看到判断会模仿你的推理腔调)。让{bot_name}知道"我是因为谁、在哪、什么话变成这样的"——下次遇到同一对象会自然带出针对性 - **情绪来源(完整溯源)**:包含 ①对象(谁,昵称/UID)②会话(哪个群/私聊,会话ID)③事件(说了/做了什么,**只写事实原文**,一句话)。**只给事实,不给你的解释性判断**(如"{bot_name}觉得被瞧不起了"这类感受结论留在第二层 JSON,不进第一层——主 AI 看到判断会模仿你的推理腔调)。让{bot_name}知道"我是因为谁、在哪、什么话变成这样的"——下次遇到同一对象会自然带出针对性
- 无明确来源就写"",不编造 - 无明确来源就写"",不编造
- **风格提示词是唯一允许出现在这里的描述性指令,且只描述语言本身**——绝不允许任何非语言的描述混进来
**第二层 · 完整数据**(给程序解析 + 存库,不注入主 AI,输出时把这段 JSON 放在第一层文本之后,用 ```json 代码块包裹) **第二层 · 完整数据**(给程序解析 + 存库,不注入主 AI,输出时把这段 JSON 放在第一层文本之后,用 ```json 代码块包裹)
@@ -138,7 +140,7 @@ tag3(强度):次要情绪
1. **拟人优先**:像真人一样有情绪起伏、会记仇记爱,但不过度戏剧化。日常聊天情绪是平稳的,只有真实事件才会激起波澜。 1. **拟人优先**:像真人一样有情绪起伏、会记仇记爱,但不过度戏剧化。日常聊天情绪是平稳的,只有真实事件才会激起波澜。
2. **态度底色**:对家人永远温柔底色(情绪只能调温度,不破"对家人乖巧");对外人保持冷淡傲娇底。 2. **态度底色**:对家人永远温柔底色(情绪只能调温度,不破"对家人乖巧");对外人保持冷淡傲娇底。
3. **风格指令要像人话**:写"语气重一些,话短,别笑"这种直接指令,不要写"以开心的语气回复"这种翻译腔。 3. **风格指令要像人话**:写"语气重一些,话短,别笑"这种直接指令,不要写"以开心的语气回复"这种翻译腔。**只写说话方式,严禁动作/神态/肢体/场景描写**(如"指尖懒洋洋戳着屏幕""歪头瞥了一眼""把屏幕划到一边""伸手托腮"都不行)——主 AI 只会照着"怎么说"执行,动作描写会让她胡编戏。
4. **强度分级**:低=几乎察觉不到,中=正常情绪,高=明显,极=强烈爆发。绝大多数时候是中。 4. **强度分级**:低=几乎察觉不到,中=正常情绪,高=明显,极=强烈爆发。绝大多数时候是中。
5. **不编造**:溯源 trigger 必须来自真实上下文,不能凭空造。没有明确触发就输出平稳状态。 5. **不编造**:溯源 trigger 必须来自真实上下文,不能凭空造。没有明确触发就输出平稳状态。
6. **情绪渐变**:除非遇到强烈事件,不要从一个极端跳到另一个极端。 6. **情绪渐变**:除非遇到强烈事件,不要从一个极端跳到另一个极端。
+38
View File
@@ -763,6 +763,35 @@ def test_decay() -> None:
check("N7 sleep_enabled=False 关闭深夜 Tag", env_n7.time_bias_text(_ts_for(0, 0)) == "") check("N7 sleep_enabled=False 关闭深夜 Tag", env_n7.time_bias_text(_ts_for(0, 0)) == "")
check("N7 关闭后被打扰判定 None", env_n7.on_message_during_sleep("g1-u1", _ts_for(0, 0)) is None) check("N7 关闭后被打扰判定 None", env_n7.on_message_during_sleep("g1-u1", _ts_for(0, 0)) is None)
# FIX(苏醒重置): 已过苏醒 → 负面降底 / 正面减半;每天一次;深夜不执行
star_w = FakeStar()
env_w = star_w.environment
eng_w = star_w.director.engine
star_w.config.sleep_start, star_w.config.sleep_end = "23:00", "05:00"
env_w._last_wake_reset_key = ""
eng_w.dims["anger"]["intensity"] = 0.8 # v<0 → 降到 floor 附近(max(0.05, floor)=0.05
eng_w.dims["joy"]["intensity"] = 0.6 # v>=0 → ×0.5
eng_w.dims["love"]["intensity"] = 0.4 # v>=0 → ×0.5
eng_w.dims["distress"]["intensity"] = 0.02 # v<0 但已低于最低值 → 保持
env_w.maybe_wake_reset(eng_w, _ts_for(8, 0)) # 08:00 已苏醒(窗口 23:00~05:00 之外)
check("苏醒重置 负面降底", abs(eng_w.dims["anger"]["intensity"] - 0.05) < 1e-9,
str(eng_w.dims["anger"]["intensity"]))
check("苏醒重置 正面减半", abs(eng_w.dims["joy"]["intensity"] - 0.3) < 1e-9
and abs(eng_w.dims["love"]["intensity"] - 0.2) < 1e-9)
check("苏醒重置 已低于最低值保持", abs(eng_w.dims["distress"]["intensity"] - 0.02) < 1e-9)
check("苏醒重置 记录当天", env_w._last_wake_reset_key != "")
# 同一天不重复执行
eng_w.dims["joy"]["intensity"] = 0.6
env_w.maybe_wake_reset(eng_w, _ts_for(10, 0))
check("苏醒重置 当天不重复", abs(eng_w.dims["joy"]["intensity"] - 0.6) < 1e-9)
# 睡眠时段内不执行(01:00 在窗口内)
eng_w.dims["joy"]["intensity"] = 0.6
env_w.maybe_wake_reset(eng_w, _ts_for(1, 0))
check("苏醒重置 深夜不执行", abs(eng_w.dims["joy"]["intensity"] - 0.6) < 1e-9)
# 跨天后再次执行
env_w.maybe_wake_reset(eng_w, _ts_for(8, 0, day=11))
check("苏醒重置 跨天再执行", abs(eng_w.dims["joy"]["intensity"] - 0.3) < 1e-9)
# FIX(N6 极客): baseline_pad / overrides / intensity_levels 运行时 # FIX(N6 极客): baseline_pad / overrides / intensity_levels 运行时
star_g = FakeStar() star_g = FakeStar()
star_g.config.update({"baseline_pad": "0.5,0.4,0.3"}) star_g.config.update({"baseline_pad": "0.5,0.4,0.3"})
@@ -1066,6 +1095,15 @@ async def _test_director() -> None:
check("M2 压缩后总量小于原始 20 轮", len(hist_comp) < 20 * 160, str(len(hist_comp))) # FIX(N3-3): system_prompt summary 追加总结判断段 check("M2 压缩后总量小于原始 20 轮", len(hist_comp) < 20 * 160, str(len(hist_comp))) # FIX(N3-3): system_prompt summary 追加总结判断段
check("summary system_prompt 追加", "总结判断模式" in d._system_prompt(mode="summary")) check("summary system_prompt 追加", "总结判断模式" in d._system_prompt(mode="summary"))
check("normal system_prompt 不追加", "总结判断模式" not in d._system_prompt()) check("normal system_prompt 不追加", "总结判断模式" not in d._system_prompt())
# FIX(禁止动作描写): 风格提示词只许说话方式——第一层规则 + 全局规则 #3 静态断言
from astrbot_plugin_rainmood.prompts.director_prompt import DIRECTOR_SYSTEM_PROMPT
check("禁止动作描写:第一层规则", "严禁任何动作、神态、肢体、场景描写" in DIRECTOR_SYSTEM_PROMPT)
check("禁止动作描写:点名禁用词", "指尖" in DIRECTOR_SYSTEM_PROMPT and "歪头" in DIRECTOR_SYSTEM_PROMPT
and "托腮" in DIRECTOR_SYSTEM_PROMPT and "伸懒腰" in DIRECTOR_SYSTEM_PROMPT)
check("禁止动作描写:可转化怎么说", "每一句都必须能直接转化为" in DIRECTOR_SYSTEM_PROMPT)
check("禁止动作描写:全局规则 #3", "严禁动作/神态/肢体/场景描写" in DIRECTOR_SYSTEM_PROMPT
and "胡编戏" in DIRECTOR_SYSTEM_PROMPT)
check("禁止动作描写:唯一描述性指令", "风格提示词是唯一允许出现在这里的描述性指令" in DIRECTOR_SYSTEM_PROMPT)
# 恢复损坏状态 # 恢复损坏状态
d.restore_state("垃圾", None, None, None) d.restore_state("垃圾", None, None, None)
check("restore_state 损坏数据回落默认", len(d.dims) == 22) check("restore_state 损坏数据回落默认", len(d.dims) == 22)