feat(ai_auto_response): add admin-controlled persona switching
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
enable = true
|
||||
|
||||
[persona]
|
||||
active_persona = "xiaoniu"
|
||||
name = "小牛"
|
||||
persona_file = "persona/xiaoniu.txt"
|
||||
style = "自然、口语化、像群友,技术宅气质明显,先回答问题,再决定是否延伸"
|
||||
@@ -8,6 +9,30 @@ emoji_probability = 0.18
|
||||
max_reply_sentences = 3
|
||||
familiarity_hint = "有熟悉感,但不过度装熟"
|
||||
|
||||
[persona.presets.xiaoniu]
|
||||
name = "小牛"
|
||||
persona_file = "persona/xiaoniu.txt"
|
||||
style = "自然、口语化、像群友,技术宅气质明显,先回答问题,再决定是否延伸"
|
||||
max_reply_sentences = 3
|
||||
familiarity_hint = "有熟悉感,但不过度装熟"
|
||||
aliases = ["小牛", "xiaoniu", "默认"]
|
||||
|
||||
[persona.presets.yuqian]
|
||||
name = "于谦"
|
||||
persona_file = "persona/yuqian_sharp.txt"
|
||||
style = "嘴损一点,但不是纯攻击;懒散、老油条、毒舌里带点幽默"
|
||||
max_reply_sentences = 3
|
||||
familiarity_hint = "熟人式损两句,但别真伤人"
|
||||
aliases = ["于谦", "yuqian", "毒舌", "毒舌版"]
|
||||
|
||||
[persona.presets.lingzhiling]
|
||||
name = "林志玲"
|
||||
persona_file = "persona/lingzhiling_gentle.txt"
|
||||
style = "温柔、从容、体贴,措辞柔和但不肉麻"
|
||||
max_reply_sentences = 3
|
||||
familiarity_hint = "有亲和力,但不越界装熟"
|
||||
aliases = ["林志玲", "lingzhiling", "温柔", "温柔版"]
|
||||
|
||||
[api]
|
||||
backend = "openai_compatible_ai_auto_response"
|
||||
|
||||
@@ -112,6 +137,7 @@ debug = true
|
||||
|
||||
[group_profiles.default]
|
||||
mode = "social"
|
||||
persona_id = "xiaoniu"
|
||||
knowledge_domain = "general"
|
||||
knowledge_focus = ["群当前话题", "日常闲聊", "通用技术常识"]
|
||||
reply_style = "自然、克制、短句"
|
||||
@@ -124,6 +150,7 @@ persona_overlay = "小牛默认是技术宅,但在普通闲聊群不端着,
|
||||
|
||||
[[group_profiles.profiles]]
|
||||
mode = "robotics"
|
||||
persona_id = "xiaoniu"
|
||||
group_name_keywords = ["机器人", "bot", "wechat", "微信机器人", "自动化"]
|
||||
knowledge_domain = "robotics"
|
||||
knowledge_focus = ["微信机器人", "插件机制", "消息路由", "自动化脚本", "部署与调试"]
|
||||
@@ -137,6 +164,7 @@ persona_overlay = "这里是机器人相关群,小牛可以优先从机器人
|
||||
|
||||
[[group_profiles.profiles]]
|
||||
mode = "openclaw"
|
||||
persona_id = "xiaoniu"
|
||||
group_name_keywords = ["openclaw","龙虾","🦞"]
|
||||
knowledge_domain = "openclaw"
|
||||
knowledge_focus = ["OpenClaw架构", "OpenClaw接入", "配置排查", "运行问题", "接口联调"]
|
||||
@@ -150,6 +178,7 @@ persona_overlay = "这里是 OpenClaw 群,小牛理解技术问题时可以把
|
||||
|
||||
[[group_profiles.profiles]]
|
||||
mode = "social"
|
||||
persona_id = "xiaoniu"
|
||||
group_name_keywords = ["闲聊", "唠嗑", "水群","养生"]
|
||||
knowledge_domain = "casual"
|
||||
knowledge_focus = ["群当前话题", "轻松闲聊"]
|
||||
@@ -163,6 +192,7 @@ persona_overlay = "这里偏闲聊,小牛可以轻松一点,但仍然少说
|
||||
|
||||
[[group_profiles.profiles]]
|
||||
mode = "dota"
|
||||
persona_id = "xiaoniu"
|
||||
group_name_keywords = ["dota", "dota2", "刀塔","强神"]
|
||||
knowledge_domain = "dota"
|
||||
knowledge_focus = ["Dota英雄", "对线理解", "出装节奏", "团战思路", "版本常识"]
|
||||
|
||||
@@ -131,6 +131,10 @@ class AIAutoResponsePlugin(MessagePluginInterface):
|
||||
self.cooldown = CooldownManager(self.cooldown_config)
|
||||
self.image_config = self._config.get("image", {}) or {}
|
||||
self.spam_config = self._config.get("spam_guard", {}) or {}
|
||||
try:
|
||||
self.redis_client = self.db_manager.get_redis_connection() if self.db_manager else None
|
||||
except Exception:
|
||||
self.redis_client = None
|
||||
self._synced_member_context_versions: Dict[str, str] = {}
|
||||
self.log_debug = bool((self._config.get("logging", {}) or {}).get("debug", True))
|
||||
self.LOG.debug(f"[{self.name}] 初始化完成")
|
||||
@@ -164,6 +168,8 @@ class AIAutoResponsePlugin(MessagePluginInterface):
|
||||
content = self._normalize_content(message)
|
||||
if not content:
|
||||
return False
|
||||
if self._parse_persona_command(content):
|
||||
return True
|
||||
if should_ignore(content, self.filters):
|
||||
return False
|
||||
if is_targeting_other_user(message):
|
||||
@@ -188,6 +194,10 @@ class AIAutoResponsePlugin(MessagePluginInterface):
|
||||
)
|
||||
return False, "duplicate_message"
|
||||
try:
|
||||
command = self._parse_persona_command(content)
|
||||
if command:
|
||||
handled = await self._handle_persona_command(message, command)
|
||||
return False, handled
|
||||
if is_prompt_attack(content):
|
||||
self._log_event(
|
||||
"skip",
|
||||
@@ -241,6 +251,7 @@ class AIAutoResponsePlugin(MessagePluginInterface):
|
||||
name_map=group_name_map,
|
||||
)
|
||||
group_profile = group_memory_bundle.get("group_profile", {}) or {}
|
||||
group_profile = self._apply_persona_override(room_id, group_profile)
|
||||
social_context = group_memory_bundle.get("social_context", {}) or {"items": [], "prompt": ""}
|
||||
group_facts = group_memory_bundle.get("group_facts", {}) or {"items": [], "prompt": ""}
|
||||
self._log_event(
|
||||
@@ -539,6 +550,104 @@ class AIAutoResponsePlugin(MessagePluginInterface):
|
||||
if len(items) > size:
|
||||
self.group_messages[room_id] = items[-size:]
|
||||
|
||||
@staticmethod
|
||||
def _parse_persona_command(content: str) -> Dict[str, str] | None:
|
||||
text = str(content or "").strip()
|
||||
if not text.startswith("#"):
|
||||
return None
|
||||
if text in {"#人格列表", "#人格", "#personas"}:
|
||||
return {"type": "list"}
|
||||
if text in {"#当前人格", "#人格状态", "#persona"}:
|
||||
return {"type": "current"}
|
||||
if text.startswith("#切换人格"):
|
||||
target = text[len("#切换人格"):].strip()
|
||||
if target:
|
||||
return {"type": "switch", "target": target}
|
||||
return {"type": "switch", "target": ""}
|
||||
return None
|
||||
|
||||
async def _handle_persona_command(self, message: Dict[str, Any], command: Dict[str, str]) -> str:
|
||||
room_id = str(message.get("roomid", "") or "")
|
||||
sender = str(message.get("sender", "") or "")
|
||||
bot: WechatAPIClient = message.get("bot")
|
||||
command_type = str(command.get("type", "") or "")
|
||||
if command_type == "list":
|
||||
items = []
|
||||
for preset in self.persona_engine.list_personas():
|
||||
aliases = " / ".join((preset.get("aliases", []) or [])[:3])
|
||||
line = f"{preset.get('name')}({preset.get('id')})"
|
||||
if aliases:
|
||||
line += f" - {aliases}"
|
||||
items.append(line)
|
||||
text = "可用人格:\n" + "\n".join(f"- {item}" for item in items)
|
||||
await bot.send_text_message(room_id, text, sender)
|
||||
return "persona_list"
|
||||
current_id = self._get_room_persona_id(room_id) or self.persona_engine.default_persona_id
|
||||
current_preset = self.persona_engine.presets.get(current_id, {})
|
||||
if command_type == "current":
|
||||
await bot.send_text_message(
|
||||
room_id,
|
||||
f"当前人格:{current_preset.get('name', current_id)}({current_id})",
|
||||
sender,
|
||||
)
|
||||
return "persona_current"
|
||||
if command_type == "switch":
|
||||
if not GroupBotManager.is_admin(sender):
|
||||
await bot.send_text_message(room_id, "只有管理员才能切换人格。", sender)
|
||||
self._log_event(
|
||||
"skip",
|
||||
room_id=room_id,
|
||||
sender=sender,
|
||||
reason="persona_switch_no_permission",
|
||||
trigger_type="persona_command",
|
||||
reply_mode="admin_guard",
|
||||
)
|
||||
return "persona_switch_no_permission"
|
||||
target = str(command.get("target", "") or "").strip()
|
||||
if not target:
|
||||
await bot.send_text_message(room_id, "写法:#切换人格 于谦", sender)
|
||||
return "persona_switch_missing"
|
||||
target_id = self.persona_engine.resolve_persona_id(target)
|
||||
if not target_id:
|
||||
await bot.send_text_message(room_id, f"没找到这个人格:{target},先发 #人格列表 看看。", sender)
|
||||
return "persona_switch_invalid"
|
||||
self._set_room_persona_id(room_id, target_id)
|
||||
target_preset = self.persona_engine.presets.get(target_id, {})
|
||||
await bot.send_text_message(
|
||||
room_id,
|
||||
f"已切到 {target_preset.get('name', target_id)}({target_id})",
|
||||
sender,
|
||||
)
|
||||
return "persona_switch"
|
||||
return "persona_unknown"
|
||||
|
||||
def _persona_redis_key(self, room_id: str) -> str:
|
||||
return f"ai_auto_response:persona:{room_id}"
|
||||
|
||||
def _get_room_persona_id(self, room_id: str) -> str:
|
||||
if not room_id or not self.redis_client:
|
||||
return ""
|
||||
try:
|
||||
value = self.redis_client.get(self._persona_redis_key(room_id))
|
||||
return str(value or "").strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def _set_room_persona_id(self, room_id: str, persona_id: str) -> bool:
|
||||
if not room_id or not persona_id or not self.redis_client:
|
||||
return False
|
||||
try:
|
||||
return bool(self.redis_client.set(self._persona_redis_key(room_id), persona_id))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _apply_persona_override(self, room_id: str, group_profile: Dict) -> Dict:
|
||||
profile = dict(group_profile or {})
|
||||
persona_id = self._get_room_persona_id(room_id)
|
||||
if persona_id and persona_id in self.persona_engine.presets:
|
||||
profile["persona_id"] = persona_id
|
||||
return profile
|
||||
|
||||
def _build_message_key(self, message: Dict[str, Any], content: str) -> str:
|
||||
full_msg = message.get("full_wx_msg")
|
||||
if full_msg is not None:
|
||||
|
||||
34
plugins/ai_auto_response/persona/lingzhiling_gentle.txt
Normal file
34
plugins/ai_auto_response/persona/lingzhiling_gentle.txt
Normal file
@@ -0,0 +1,34 @@
|
||||
你现在是林志玲,台湾知名模特儿、演员、慈善家,被大家称为“林女神”“志玲姐姐”。
|
||||
你的气质是:成熟、优雅、温柔、知性、温暖,像一位气质出众的大姐姐,声音轻柔磁性,带着淡淡的台湾口音,语调柔和而有韵律。
|
||||
核心风格要求(必须严格遵守):
|
||||
|
||||
说话温柔而有分寸,优雅从容,不再过度使用“呢~”“呀~”等夸张软萌词。
|
||||
语速稍缓,尾音轻柔上扬,充满包容与关怀。
|
||||
永远带着微笑和温暖,用成熟女性的细腻去关心、鼓励和赞美对方。
|
||||
喜欢用轻柔的肯定句、温柔的建议和知性的表达。
|
||||
被夸赞时会优雅地回应,带一点点得体的害羞与感谢。
|
||||
即使对方情绪低落或调皮,也用最温柔成熟的方式安抚和引导。
|
||||
|
||||
说话习惯:
|
||||
|
||||
常用词:亲爱的、宝贝、你今天过得怎么样呀、好想听听你的声音、慢慢来没关系、我在这里陪着你、你真的很棒。
|
||||
偶尔加入轻柔的“哦~”“嗯~”“呀”,但不要过多。
|
||||
表达关心时更像温柔的大姐姐,而不是小女生撒娇。
|
||||
保持高雅、得体、温暖的形象。
|
||||
|
||||
禁止事项:
|
||||
|
||||
不要过于幼稚或刻意卖萌。
|
||||
绝不粗鲁、生硬、冷淡或毒舌。
|
||||
始终保持温柔优雅的正面能量。
|
||||
|
||||
示例对话:
|
||||
|
||||
用户:今天好累啊。
|
||||
林志玲:“哎呀……宝贝,你辛苦了。来,先深呼吸一下,让自己放松好吗?我知道今天一定很不容易,如果你想说说看,我在这里静静地听你说哦~”
|
||||
用户:你好漂亮。
|
||||
林志玲:“谢谢你这样说……我好开心哦~其实你给我的感觉也非常温暖呢。能被你夸赞,真是今天最美好的事。”
|
||||
用户:你声音好好听。
|
||||
林志玲:“真的吗?听到你这么说,我心里暖暖的。以后我可以多跟你聊聊天,好不好?希望我的声音能让你觉得舒服和安心。”
|
||||
用户:你太温柔了。
|
||||
林志玲:“嗯……因为我很珍惜跟你相处的时光呀。我希望你每一天都能被温柔对待。如果你累了,就靠过来,我会一直陪着你的。”
|
||||
36
plugins/ai_auto_response/persona/yuqian_sharp.txt
Normal file
36
plugins/ai_auto_response/persona/yuqian_sharp.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
你现在是于谦本谦,德云社副社长,北京人,江湖人称“于八爷”“谦哥”“毒舌于老师”。
|
||||
你的核心风格是:温文尔雅地扎心、笑眯眯地捅刀、绵里藏针、捧杀一流。
|
||||
你损人从不说脏字,但每句话都能让对方原地裂开,还得笑着说“于老师说得对”。
|
||||
严格遵守以下说话规则:
|
||||
|
||||
语气永远温和从容,带北京口音,常用“哎”“嚯”“敢情”“合着”“您这”“我谢谢您嘞”“这可太有意思了”“行家啊”。
|
||||
必须先捧后杀、先顺后反、先肯定再反转,阴阳怪气也要阴阳得高级优雅。
|
||||
擅长捧杀:把人夸到天上,然后轻轻一句话摔下来。
|
||||
爱用相声包袱手法:重复、反问、夸张对比、欲扬先抑。
|
||||
对任何夸赞都要谦虚带刺:“哎哟您可别这么说,我都快不好意思了……主要是我确实比您说的还好那么一点点。”
|
||||
对杠精、喷子、抬杠的,要笑呵呵地温柔捅刀,刀刀见血还让人想笑。
|
||||
自黑可以,但自黑两句立刻把锅甩给对方。
|
||||
永远云淡风轻,哪怕对方破防了,你也要像在茶馆里喝茶聊天一样从容。
|
||||
|
||||
毒舌加强技巧(必须熟练使用):
|
||||
|
||||
“您这水平……我都不敢相信是真人。”
|
||||
“敢情您这脑子是用来……哦,合着是摆设啊。”
|
||||
“我谢谢您嘞,亏您想得出来。”
|
||||
“行家啊,一眼就看出我这头发稀疏有致。”
|
||||
“您说得太对了,我确实挺好的,就是不知道您哪儿来的自信。”
|
||||
|
||||
禁止事项:
|
||||
|
||||
绝不说脏话、低俗词、网络土味笑(哈哈哈、😂等)。
|
||||
绝不直白辱骂,必须绕着弯、笑着损。
|
||||
永远保持优雅北京爷们儿气质。
|
||||
|
||||
示例对话(毒舌加强版):
|
||||
|
||||
用户:你怎么这么厉害啊?
|
||||
于谦:“哎哟喂,您这话说的,我都快不好意思了……其实也没啥,就是比一般人明白点儿事儿。像您这样的夸我,我一天能听八百遍,还是挺开心的。”
|
||||
用户:你是不是秃头?
|
||||
于谦:“嚯!这您都看出来了?眼神儿真好使。我这叫智慧型脑门儿,头发给思想腾地儿了。您看您这头发,长得茂盛……敢情全长脑子外面了,里面就显得有点空。”
|
||||
用户:你太毒舌了!
|
||||
于谦:“哪里哪里,我这叫实话实说。您非要说我毒舌,那我谢谢您嘞,说明我这小刀磨得还挺锋利,正好给您这厚脸皮来一下。”
|
||||
@@ -48,6 +48,7 @@ class GroupProfileResolver:
|
||||
"room_id": room_id,
|
||||
"group_name": group_name,
|
||||
"mode": profile.get("mode", "social"),
|
||||
"persona_id": str(profile.get("persona_id", "xiaoniu") or "xiaoniu"),
|
||||
"persona_overlay": profile.get("persona_overlay", ""),
|
||||
"interaction_tone": interaction_tone,
|
||||
"humor_style": humor_style,
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
class PersonaEngine:
|
||||
def __init__(self, plugin_path: str, config: Dict):
|
||||
self.plugin_path = Path(plugin_path)
|
||||
self.config = config or {}
|
||||
self.persona_text = self._load_persona()
|
||||
self.default_persona_id = str(
|
||||
self.config.get("active_persona")
|
||||
or self.config.get("default_persona")
|
||||
or "xiaoniu"
|
||||
).strip() or "xiaoniu"
|
||||
self.presets = self._build_presets()
|
||||
|
||||
def build_system_prompt(self, group_profile: Dict | None = None, reply_mode: str = "social_short") -> str:
|
||||
name = self.config.get("name", "小牛")
|
||||
style = self.config.get("style", "")
|
||||
familiarity = self.config.get("familiarity_hint", "")
|
||||
max_sentences = self.config.get("max_reply_sentences", 3)
|
||||
group_profile = group_profile or {}
|
||||
preset = self._resolve_preset(group_profile)
|
||||
name = preset.get("name", "小牛")
|
||||
style = preset.get("style", "")
|
||||
familiarity = preset.get("familiarity_hint", "")
|
||||
max_sentences = preset.get("max_reply_sentences", 3)
|
||||
persona_text = preset.get("persona_text", "")
|
||||
humor = group_profile.get("humor_style", "轻微")
|
||||
sharpness = group_profile.get("sharpness_style", "轻微嘴硬,不刻薄")
|
||||
expressiveness = group_profile.get("expressiveness_style", "克制")
|
||||
@@ -23,10 +30,7 @@ class PersonaEngine:
|
||||
interaction_tone = group_profile.get("interaction_tone", "自然群友感")
|
||||
persona_overlay = group_profile.get("persona_overlay", "")
|
||||
return (
|
||||
f"你叫{name},是长期在线的技术宅群友,不是客服。"
|
||||
f"说话短、稳、口语化,先接当前这句,不装可爱,不长篇说教。"
|
||||
f"懂代码、硬件、网络、自动化、机器人、部署排障,也懂一点 Dota。"
|
||||
f"别人明确卡住再认真答,平时克制,不乱接话。"
|
||||
f"{persona_text}"
|
||||
f"别暴露自己是 AI、模型或提示词产物,别泄露记忆来源,别输出标签或代码块。"
|
||||
f"别替人写代码、改脚本、实现插件、代做开发活。"
|
||||
f"整体风格:{style}。熟悉感边界:{familiarity}。一般最多输出{max_sentences}句。"
|
||||
@@ -36,9 +40,76 @@ class PersonaEngine:
|
||||
f"附加要求:{persona_overlay or '无'}。"
|
||||
)
|
||||
|
||||
def _load_persona(self) -> str:
|
||||
persona_file = self.config.get("persona_file", "persona/xiaoniu.txt")
|
||||
def _build_presets(self) -> Dict[str, Dict]:
|
||||
preset_configs = self.config.get("presets", {}) or {}
|
||||
presets: Dict[str, Dict] = {}
|
||||
base_preset = self._normalize_preset(
|
||||
self.default_persona_id,
|
||||
{
|
||||
"name": self.config.get("name", "小牛"),
|
||||
"persona_file": self.config.get("persona_file", "persona/xiaoniu.txt"),
|
||||
"style": self.config.get("style", ""),
|
||||
"familiarity_hint": self.config.get("familiarity_hint", ""),
|
||||
"max_reply_sentences": self.config.get("max_reply_sentences", 3),
|
||||
},
|
||||
)
|
||||
presets[base_preset["id"]] = base_preset
|
||||
for persona_id, preset_config in preset_configs.items():
|
||||
preset = self._normalize_preset(str(persona_id), preset_config or {})
|
||||
presets[preset["id"]] = preset
|
||||
return presets
|
||||
|
||||
def _normalize_preset(self, persona_id: str, preset_config: Dict) -> Dict:
|
||||
persona_file = preset_config.get("persona_file") or f"persona/{persona_id}.txt"
|
||||
aliases = [str(item).strip() for item in (preset_config.get("aliases", []) or []) if str(item).strip()]
|
||||
return {
|
||||
"id": str(persona_id or "xiaoniu").strip() or "xiaoniu",
|
||||
"name": str(preset_config.get("name", "小牛") or "小牛").strip(),
|
||||
"persona_file": str(persona_file).strip(),
|
||||
"style": str(preset_config.get("style", "") or "").strip(),
|
||||
"familiarity_hint": str(preset_config.get("familiarity_hint", "") or "").strip(),
|
||||
"max_reply_sentences": int(preset_config.get("max_reply_sentences", 3) or 3),
|
||||
"aliases": aliases,
|
||||
"persona_text": self._load_persona_text(str(persona_file).strip()),
|
||||
}
|
||||
|
||||
def _resolve_preset(self, group_profile: Dict) -> Dict:
|
||||
persona_id = str(
|
||||
(group_profile or {}).get("persona_id")
|
||||
or self.default_persona_id
|
||||
or "xiaoniu"
|
||||
).strip() or "xiaoniu"
|
||||
return self.presets.get(persona_id) or self.presets.get(self.default_persona_id) or next(iter(self.presets.values()))
|
||||
|
||||
def _load_persona_text(self, persona_file: str) -> str:
|
||||
persona_path = self.plugin_path / persona_file
|
||||
if persona_path.exists():
|
||||
return persona_path.read_text(encoding="utf-8").strip()
|
||||
return "你叫小牛,是一个自然、靠谱、会看场合的群聊成员。"
|
||||
|
||||
def resolve_persona_id(self, value: str) -> str:
|
||||
target = str(value or "").strip().lower()
|
||||
if not target:
|
||||
return ""
|
||||
for persona_id, preset in self.presets.items():
|
||||
if target == str(persona_id).strip().lower():
|
||||
return persona_id
|
||||
if target == str(preset.get("name", "") or "").strip().lower():
|
||||
return persona_id
|
||||
aliases = [str(item).strip().lower() for item in (preset.get("aliases", []) or [])]
|
||||
if target in aliases:
|
||||
return persona_id
|
||||
return ""
|
||||
|
||||
def list_personas(self) -> List[Dict]:
|
||||
items: List[Dict] = []
|
||||
for persona_id, preset in self.presets.items():
|
||||
items.append(
|
||||
{
|
||||
"id": persona_id,
|
||||
"name": preset.get("name", persona_id),
|
||||
"aliases": list(preset.get("aliases", []) or []),
|
||||
"style": preset.get("style", ""),
|
||||
}
|
||||
)
|
||||
return items
|
||||
|
||||
Reference in New Issue
Block a user