feta:优化AI处理流程
This commit is contained in:
@@ -167,6 +167,70 @@ class AIChat(PluginBase):
|
|||||||
else:
|
else:
|
||||||
return sender_wxid or from_wxid # 私聊使用用户ID
|
return sender_wxid or from_wxid # 私聊使用用户ID
|
||||||
|
|
||||||
|
def _get_group_history_chat_id(self, from_wxid: str, user_wxid: str = None) -> str:
|
||||||
|
"""获取群聊 history 的会话ID(可配置为全群共享或按用户隔离)"""
|
||||||
|
if not from_wxid:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
history_config = (self.config or {}).get("history", {})
|
||||||
|
scope = str(history_config.get("scope", "chatroom") or "chatroom").strip().lower()
|
||||||
|
if scope in ("per_user", "user", "peruser"):
|
||||||
|
if not user_wxid:
|
||||||
|
return from_wxid
|
||||||
|
return self._get_chat_id(from_wxid, user_wxid, is_group=True)
|
||||||
|
|
||||||
|
return from_wxid
|
||||||
|
|
||||||
|
def _should_capture_group_history(self, *, is_triggered: bool) -> bool:
|
||||||
|
"""判断群聊消息是否需要写入 history(减少无关上下文污染)"""
|
||||||
|
history_config = (self.config or {}).get("history", {})
|
||||||
|
capture = str(history_config.get("capture", "all") or "all").strip().lower()
|
||||||
|
|
||||||
|
if capture in ("none", "off", "disable", "disabled"):
|
||||||
|
return False
|
||||||
|
if capture in ("reply", "ai_only", "triggered"):
|
||||||
|
return bool(is_triggered)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _parse_history_timestamp(self, ts) -> float | None:
|
||||||
|
if ts is None:
|
||||||
|
return None
|
||||||
|
if isinstance(ts, (int, float)):
|
||||||
|
return float(ts)
|
||||||
|
if isinstance(ts, str):
|
||||||
|
s = ts.strip()
|
||||||
|
if not s:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(s)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return datetime.fromisoformat(s).timestamp()
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _filter_history_by_window(self, history: list) -> list:
|
||||||
|
history_config = (self.config or {}).get("history", {})
|
||||||
|
window_seconds = history_config.get("context_window_seconds", None)
|
||||||
|
if window_seconds is None:
|
||||||
|
window_seconds = history_config.get("window_seconds", 0)
|
||||||
|
try:
|
||||||
|
window_seconds = float(window_seconds or 0)
|
||||||
|
except Exception:
|
||||||
|
window_seconds = 0
|
||||||
|
if window_seconds <= 0:
|
||||||
|
return history
|
||||||
|
|
||||||
|
cutoff = time.time() - window_seconds
|
||||||
|
filtered = []
|
||||||
|
for msg in history or []:
|
||||||
|
ts = self._parse_history_timestamp((msg or {}).get("timestamp"))
|
||||||
|
if ts is None or ts >= cutoff:
|
||||||
|
filtered.append(msg)
|
||||||
|
return filtered
|
||||||
|
|
||||||
def _sanitize_speaker_name(self, name: str) -> str:
|
def _sanitize_speaker_name(self, name: str) -> str:
|
||||||
"""清洗昵称,避免破坏历史格式(如 [name] 前缀)。"""
|
"""清洗昵称,避免破坏历史格式(如 [name] 前缀)。"""
|
||||||
if name is None:
|
if name is None:
|
||||||
@@ -1173,6 +1237,203 @@ class AIChat(PluginBase):
|
|||||||
"content": f"[{msg_nickname}] {msg_content}"
|
"content": f"[{msg_nickname}] {msg_content}"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def _get_bot_nickname(self) -> str:
|
||||||
|
try:
|
||||||
|
with open("main_config.toml", "rb") as f:
|
||||||
|
main_config = tomllib.load(f)
|
||||||
|
nickname = main_config.get("Bot", {}).get("nickname", "")
|
||||||
|
return nickname or "机器人"
|
||||||
|
except Exception:
|
||||||
|
return "机器人"
|
||||||
|
|
||||||
|
def _tool_call_to_action_text(self, function_name: str, arguments: dict) -> str:
|
||||||
|
args = arguments if isinstance(arguments, dict) else {}
|
||||||
|
|
||||||
|
if function_name == "query_weather":
|
||||||
|
city = str(args.get("city") or "").strip()
|
||||||
|
return f"查询{city}天气" if city else "查询天气"
|
||||||
|
|
||||||
|
if function_name == "register_city":
|
||||||
|
city = str(args.get("city") or "").strip()
|
||||||
|
return f"注册城市{city}" if city else "注册城市"
|
||||||
|
|
||||||
|
if function_name == "user_signin":
|
||||||
|
return "签到"
|
||||||
|
|
||||||
|
if function_name == "check_profile":
|
||||||
|
return "查询个人信息"
|
||||||
|
|
||||||
|
return f"执行{function_name}"
|
||||||
|
|
||||||
|
def _build_tool_calls_context_note(self, tool_calls_data: list) -> str:
|
||||||
|
actions: list[str] = []
|
||||||
|
for tool_call in tool_calls_data or []:
|
||||||
|
function_name = tool_call.get("function", {}).get("name", "")
|
||||||
|
if not function_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
arguments_str = tool_call.get("function", {}).get("arguments", "{}")
|
||||||
|
try:
|
||||||
|
arguments = json.loads(arguments_str) if arguments_str else {}
|
||||||
|
except Exception:
|
||||||
|
arguments = {}
|
||||||
|
|
||||||
|
actions.append(self._tool_call_to_action_text(function_name, arguments))
|
||||||
|
|
||||||
|
if not actions:
|
||||||
|
return "(已触发工具处理:上一条请求。结果将发送到聊天中。)"
|
||||||
|
|
||||||
|
return f"(已触发工具处理:{';'.join(actions)}。结果将发送到聊天中。)"
|
||||||
|
|
||||||
|
async def _record_tool_calls_to_context(
|
||||||
|
self,
|
||||||
|
tool_calls_data: list,
|
||||||
|
*,
|
||||||
|
from_wxid: str,
|
||||||
|
chat_id: str,
|
||||||
|
is_group: bool,
|
||||||
|
user_wxid: str | None = None,
|
||||||
|
):
|
||||||
|
note = self._build_tool_calls_context_note(tool_calls_data)
|
||||||
|
if chat_id:
|
||||||
|
self._add_to_memory(chat_id, "assistant", note)
|
||||||
|
|
||||||
|
if is_group and from_wxid:
|
||||||
|
history_chat_id = self._get_group_history_chat_id(from_wxid, user_wxid or "")
|
||||||
|
await self._add_to_history(history_chat_id, self._get_bot_nickname(), note, role="assistant", sender_wxid=user_wxid or None)
|
||||||
|
|
||||||
|
def _extract_tool_intent_text(self, user_message: str, tool_query: str | None = None) -> str:
|
||||||
|
text = tool_query if tool_query is not None else user_message
|
||||||
|
text = str(text or "").strip()
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# 对“聊天记录/视频”等组合消息,尽量只取用户真实提问部分,避免历史文本触发工具误判
|
||||||
|
markers = (
|
||||||
|
"[用户的问题]:",
|
||||||
|
"[用户的问题]:",
|
||||||
|
"[用户的问题]\n",
|
||||||
|
"[用户的问题]",
|
||||||
|
)
|
||||||
|
for marker in markers:
|
||||||
|
if marker in text:
|
||||||
|
text = text.rsplit(marker, 1)[-1].strip()
|
||||||
|
return text
|
||||||
|
|
||||||
|
def _select_tools_for_message(self, tools: list, *, user_message: str, tool_query: str | None = None) -> list:
|
||||||
|
tools_config = (self.config or {}).get("tools", {})
|
||||||
|
if not tools_config.get("smart_select", False):
|
||||||
|
return tools
|
||||||
|
|
||||||
|
intent_text = self._extract_tool_intent_text(user_message, tool_query=tool_query)
|
||||||
|
if not intent_text:
|
||||||
|
return tools
|
||||||
|
|
||||||
|
t = intent_text.lower()
|
||||||
|
allow: set[str] = set()
|
||||||
|
|
||||||
|
# 天气
|
||||||
|
if re.search(r"(天气|气温|温度|下雨|下雪|风力|空气质量|pm2\\.?5|湿度|预报)", t):
|
||||||
|
allow.add("query_weather")
|
||||||
|
|
||||||
|
# 注册/设置城市(避免仅凭城市名触发)
|
||||||
|
if re.search(r"(注册|设置|更新|更换|修改|绑定|默认).{0,6}城市|城市.{0,6}(注册|设置|更新|更换|修改|绑定|默认)", t):
|
||||||
|
allow.add("register_city")
|
||||||
|
|
||||||
|
# 签到/个人信息
|
||||||
|
if re.search(r"(用户签到|签到|签个到)", t):
|
||||||
|
allow.add("user_signin")
|
||||||
|
if re.search(r"(个人信息|我的信息|我的积分|查积分|积分多少|连续签到|连签|我的资料)", t):
|
||||||
|
allow.add("check_profile")
|
||||||
|
|
||||||
|
# 鹿打卡
|
||||||
|
if re.search(r"(鹿打卡|鹿签到)", t):
|
||||||
|
allow.add("deer_checkin")
|
||||||
|
if re.search(r"(补签|补打卡)", t):
|
||||||
|
allow.add("makeup_checkin")
|
||||||
|
if re.search(r"(鹿.*(日历|月历|打卡日历))|((日历|月历|打卡日历).*鹿)", t):
|
||||||
|
allow.add("view_calendar")
|
||||||
|
|
||||||
|
# 搜索/资讯
|
||||||
|
if re.search(r"(联网|搜索|搜一下|搜一搜|搜搜|帮我搜|搜新闻|搜资料|查资料|查新闻|查价格)", t):
|
||||||
|
# 兼容旧工具名与当前插件实现
|
||||||
|
allow.add("tavily_web_search")
|
||||||
|
allow.add("web_search")
|
||||||
|
# 隐式信息检索:用户询问具体实体/口碑/评价但未明确说“搜索/联网”
|
||||||
|
if re.search(r"(怎么样|如何|评价|口碑|靠谱吗|值不值得|值得吗|好不好|推荐|牛不牛|强不强|厉不厉害|有名吗|什么来头|背景|近况|最新|最近)", t) and re.search(
|
||||||
|
r"(公会|战队|服务器|区服|游戏|公司|品牌|店|商家|产品|软件|插件|项目|平台|up主|主播|作者|电影|电视剧|小说|手游|网游)",
|
||||||
|
t,
|
||||||
|
):
|
||||||
|
allow.add("tavily_web_search")
|
||||||
|
allow.add("web_search")
|
||||||
|
if re.search(r"(60秒|每日新闻|早报|新闻图片|读懂世界)", t):
|
||||||
|
allow.add("get_daily_news")
|
||||||
|
if re.search(r"(epic|喜加一|免费游戏)", t):
|
||||||
|
allow.add("get_epic_free_games")
|
||||||
|
|
||||||
|
# 音乐/短剧
|
||||||
|
if re.search(r"(搜歌|找歌|点歌|来一首|歌名|歌曲|音乐|听.*歌|播放.*歌)", t) or ("歌" in t and re.search(r"(搜|找|点|来一首|播放|听)", t)):
|
||||||
|
allow.add("search_music")
|
||||||
|
if re.search(r"(短剧|搜短剧|找短剧)", t):
|
||||||
|
allow.add("search_playlet")
|
||||||
|
|
||||||
|
# 群聊总结
|
||||||
|
if re.search(r"(群聊总结|生成总结|总结一下|今日总结|昨天总结|群总结)", t):
|
||||||
|
allow.add("generate_summary")
|
||||||
|
|
||||||
|
# 娱乐
|
||||||
|
if re.search(r"(疯狂星期四|v我50|kfc)", t):
|
||||||
|
allow.add("get_kfc")
|
||||||
|
# 发病文学:必须是明确请求(避免用户口头禅/情绪表达误触工具)
|
||||||
|
if re.search(r"(发病文学|犯病文学|发病文|犯病文|发病语录|犯病语录)", t):
|
||||||
|
allow.add("get_fabing")
|
||||||
|
elif re.search(r"(来|整|给|写|讲|说|发|搞|整点).{0,4}(发病|犯病)", t):
|
||||||
|
allow.add("get_fabing")
|
||||||
|
elif re.search(r"(发病|犯病).{0,6}(一下|一段|一条|几句|文学|文|语录|段子)", t):
|
||||||
|
allow.add("get_fabing")
|
||||||
|
if re.search(r"(随机图片|来张图|来个图|随机图)", t):
|
||||||
|
allow.add("get_random_image")
|
||||||
|
if re.search(r"(随机视频|来个视频|随机短视频)", t):
|
||||||
|
allow.add("get_random_video")
|
||||||
|
|
||||||
|
# 绘图/视频生成(只在用户明确要求时开放)
|
||||||
|
if (
|
||||||
|
# 明确绘图动词/模式
|
||||||
|
re.search(r"(画一张|画一个|画个|画一下|画图|绘图|绘制|作画|出图|生成图片|文生图|图生图|以图生图)", t)
|
||||||
|
# “生成/做/给我”+“一张/一个/张/个”+“图/图片”类表达(例如:生成一张瑞依/做一张图)
|
||||||
|
or re.search(r"(生成|做|给我|帮我).{0,4}(一张|一幅|一个|张|个).{0,8}(图|图片|照片)", t)
|
||||||
|
# “来/发”+“一张/张”+“图/图片”(例如:来张瑞依的图)
|
||||||
|
or re.search(r"(来|发).{0,2}(一张|一幅|一个|张|个).{0,10}(图|图片|照片)", t)
|
||||||
|
# 视觉诉求但没说“画”(例如:看看腿/白丝)
|
||||||
|
or re.search(r"(看看|看下|给我看|让我看看).{0,8}(腿|白丝|黑丝|丝袜|玉足|脚|足|写真|涩图|色图|福利图)", t)
|
||||||
|
):
|
||||||
|
allow.update({
|
||||||
|
"nano_ai_image_generation",
|
||||||
|
"flow2_ai_image_generation",
|
||||||
|
"jimeng_ai_image_generation",
|
||||||
|
"kiira2_ai_image_generation",
|
||||||
|
"generate_image",
|
||||||
|
})
|
||||||
|
if re.search(r"(生成视频|做个视频|视频生成|sora)", t):
|
||||||
|
allow.add("sora_video_generation")
|
||||||
|
|
||||||
|
# 如果已经命中特定领域工具(天气/音乐/短剧等),且用户未明确表示“联网/网页/链接/来源”等需求,避免把联网搜索也暴露出去造成误触
|
||||||
|
explicit_web = bool(re.search(r"(联网|网页|网站|网址|链接|来源)", t))
|
||||||
|
if not explicit_web and {"query_weather", "search_music", "search_playlet"} & allow:
|
||||||
|
allow.discard("tavily_web_search")
|
||||||
|
allow.discard("web_search")
|
||||||
|
|
||||||
|
# 严格模式:没有明显工具意图时,不向模型暴露任何 tools,避免误触
|
||||||
|
if not allow:
|
||||||
|
return []
|
||||||
|
|
||||||
|
selected = []
|
||||||
|
for tool in tools or []:
|
||||||
|
name = tool.get("function", {}).get("name", "")
|
||||||
|
if name and name in allow:
|
||||||
|
selected.append(tool)
|
||||||
|
return selected
|
||||||
|
|
||||||
async def _handle_context_stats(self, bot, from_wxid: str, user_wxid: str, is_group: bool):
|
async def _handle_context_stats(self, bot, from_wxid: str, user_wxid: str, is_group: bool):
|
||||||
"""处理上下文统计指令"""
|
"""处理上下文统计指令"""
|
||||||
try:
|
try:
|
||||||
@@ -1190,7 +1451,9 @@ class AIChat(PluginBase):
|
|||||||
|
|
||||||
if is_group:
|
if is_group:
|
||||||
# 群聊:使用 history 机制
|
# 群聊:使用 history 机制
|
||||||
history = await self._load_history(from_wxid)
|
history_chat_id = self._get_group_history_chat_id(from_wxid, user_wxid)
|
||||||
|
history = await self._load_history(history_chat_id)
|
||||||
|
history = self._filter_history_by_window(history)
|
||||||
max_context = self.config.get("history", {}).get("max_context", 50)
|
max_context = self.config.get("history", {}).get("max_context", 50)
|
||||||
|
|
||||||
# 实际会发送给 AI 的上下文
|
# 实际会发送给 AI 的上下文
|
||||||
@@ -1393,7 +1656,9 @@ class AIChat(PluginBase):
|
|||||||
if content == "/记忆状态":
|
if content == "/记忆状态":
|
||||||
if user_wxid in admins:
|
if user_wxid in admins:
|
||||||
if is_group:
|
if is_group:
|
||||||
history = await self._load_history(from_wxid)
|
history_chat_id = self._get_group_history_chat_id(from_wxid, user_wxid)
|
||||||
|
history = await self._load_history(history_chat_id)
|
||||||
|
history = self._filter_history_by_window(history)
|
||||||
max_context = self.config.get("history", {}).get("max_context", 50)
|
max_context = self.config.get("history", {}).get("max_context", 50)
|
||||||
context_count = min(len(history), max_context)
|
context_count = min(len(history), max_context)
|
||||||
msg = f"📊 群聊记忆: {len(history)} 条\n"
|
msg = f"📊 群聊记忆: {len(history)} 条\n"
|
||||||
@@ -1479,15 +1744,18 @@ class AIChat(PluginBase):
|
|||||||
if should_reply:
|
if should_reply:
|
||||||
actual_content = self._extract_content(message, content)
|
actual_content = self._extract_content(message, content)
|
||||||
|
|
||||||
# 保存到群组历史记录(所有消息都保存,不管是否回复)
|
# 保存到群组历史记录(默认全量保存;可配置为仅保存触发 AI 的消息,减少上下文污染/串线)
|
||||||
# 但如果是 AutoReply 触发的,跳过保存(消息已经在正常流程中保存过了)
|
# 但如果是 AutoReply 触发的,跳过保存(消息已经在正常流程中保存过了)
|
||||||
if is_group and not message.get('_auto_reply_triggered'):
|
if is_group and not message.get('_auto_reply_triggered'):
|
||||||
# mention 模式下,群聊里@机器人仅作为触发条件,不进入上下文,避免同一句话在上下文中出现两种形式(含@/不含@)
|
if self._should_capture_group_history(is_triggered=bool(should_reply)):
|
||||||
trigger_mode = self.config.get("behavior", {}).get("trigger_mode", "mention")
|
# mention 模式下,群聊里@机器人仅作为触发条件,不进入上下文,避免同一句话在上下文中出现两种形式(含@/不含@)
|
||||||
history_content = content
|
trigger_mode = self.config.get("behavior", {}).get("trigger_mode", "mention")
|
||||||
if trigger_mode == "mention" and should_reply and actual_content:
|
history_content = content
|
||||||
history_content = actual_content
|
if trigger_mode == "mention" and should_reply and actual_content:
|
||||||
await self._add_to_history(from_wxid, nickname, history_content, sender_wxid=user_wxid)
|
history_content = actual_content
|
||||||
|
|
||||||
|
history_chat_id = self._get_group_history_chat_id(from_wxid, user_wxid)
|
||||||
|
await self._add_to_history(history_chat_id, nickname, history_content, sender_wxid=user_wxid)
|
||||||
|
|
||||||
# 如果不需要回复,直接返回
|
# 如果不需要回复,直接返回
|
||||||
if not should_reply:
|
if not should_reply:
|
||||||
@@ -1517,7 +1785,13 @@ class AIChat(PluginBase):
|
|||||||
|
|
||||||
# 群聊:消息已写入 history,则不再重复附加到 LLM messages,避免“同一句话发给AI两次”
|
# 群聊:消息已写入 history,则不再重复附加到 LLM messages,避免“同一句话发给AI两次”
|
||||||
history_enabled = bool(self.store) and self.config.get("history", {}).get("enabled", True)
|
history_enabled = bool(self.store) and self.config.get("history", {}).get("enabled", True)
|
||||||
append_user_message = not (is_group and history_enabled and not message.get('_auto_reply_triggered'))
|
captured_to_history = bool(
|
||||||
|
is_group
|
||||||
|
and history_enabled
|
||||||
|
and not message.get('_auto_reply_triggered')
|
||||||
|
and self._should_capture_group_history(is_triggered=True)
|
||||||
|
)
|
||||||
|
append_user_message = not captured_to_history
|
||||||
|
|
||||||
# 调用 AI API(带重试机制)
|
# 调用 AI API(带重试机制)
|
||||||
max_retries = self.config.get("api", {}).get("max_retries", 2)
|
max_retries = self.config.get("api", {}).get("max_retries", 2)
|
||||||
@@ -1569,11 +1843,22 @@ class AIChat(PluginBase):
|
|||||||
await bot.send_text(from_wxid, cleaned_response)
|
await bot.send_text(from_wxid, cleaned_response)
|
||||||
self._add_to_memory(chat_id, "assistant", cleaned_response)
|
self._add_to_memory(chat_id, "assistant", cleaned_response)
|
||||||
# 保存机器人回复到历史记录
|
# 保存机器人回复到历史记录
|
||||||
if is_group:
|
history_config = self.config.get("history", {})
|
||||||
|
sync_bot_messages = history_config.get("sync_bot_messages", False)
|
||||||
|
history_scope = str(history_config.get("scope", "chatroom") or "chatroom").strip().lower()
|
||||||
|
can_rely_on_hook = bool(sync_bot_messages and history_scope not in ("per_user", "user", "peruser"))
|
||||||
|
if is_group and not can_rely_on_hook:
|
||||||
with open("main_config.toml", "rb") as f:
|
with open("main_config.toml", "rb") as f:
|
||||||
main_config = tomllib.load(f)
|
main_config = tomllib.load(f)
|
||||||
bot_nickname = main_config.get("Bot", {}).get("nickname", "机器人")
|
bot_nickname = main_config.get("Bot", {}).get("nickname", "机器人")
|
||||||
await self._add_to_history(from_wxid, bot_nickname, cleaned_response, role="assistant")
|
history_chat_id = self._get_group_history_chat_id(from_wxid, user_wxid)
|
||||||
|
await self._add_to_history(
|
||||||
|
history_chat_id,
|
||||||
|
bot_nickname,
|
||||||
|
cleaned_response,
|
||||||
|
role="assistant",
|
||||||
|
sender_wxid=user_wxid,
|
||||||
|
)
|
||||||
logger.success(f"AI 回复成功: {cleaned_response[:50]}...")
|
logger.success(f"AI 回复成功: {cleaned_response[:50]}...")
|
||||||
else:
|
else:
|
||||||
logger.warning("AI 回复清洗后为空(可能只包含思维链/格式标记),已跳过发送")
|
logger.warning("AI 回复清洗后为空(可能只包含思维链/格式标记),已跳过发送")
|
||||||
@@ -1673,16 +1958,18 @@ class AIChat(PluginBase):
|
|||||||
is_group: bool = False,
|
is_group: bool = False,
|
||||||
*,
|
*,
|
||||||
append_user_message: bool = True,
|
append_user_message: bool = True,
|
||||||
|
tool_query: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""调用 AI API"""
|
"""调用 AI API"""
|
||||||
api_config = self.config["api"]
|
api_config = self.config["api"]
|
||||||
|
|
||||||
# 收集工具
|
# 收集工具
|
||||||
tools = self._collect_tools()
|
all_tools = self._collect_tools()
|
||||||
logger.info(f"收集到 {len(tools)} 个工具函数")
|
tools = self._select_tools_for_message(all_tools, user_message=user_message, tool_query=tool_query)
|
||||||
|
logger.info(f"收集到 {len(all_tools)} 个工具函数,本次启用 {len(tools)} 个")
|
||||||
if tools:
|
if tools:
|
||||||
tool_names = [t["function"]["name"] for t in tools]
|
tool_names = [t["function"]["name"] for t in tools]
|
||||||
logger.info(f"工具列表: {tool_names}")
|
logger.info(f"本次启用工具: {tool_names}")
|
||||||
|
|
||||||
# 构建消息列表
|
# 构建消息列表
|
||||||
system_content = self.system_prompt
|
system_content = self.system_prompt
|
||||||
@@ -1714,7 +2001,9 @@ class AIChat(PluginBase):
|
|||||||
|
|
||||||
# 从 JSON 历史记录加载上下文(仅群聊)
|
# 从 JSON 历史记录加载上下文(仅群聊)
|
||||||
if is_group and from_wxid:
|
if is_group and from_wxid:
|
||||||
history = await self._load_history(from_wxid)
|
history_chat_id = self._get_group_history_chat_id(from_wxid, user_wxid or "")
|
||||||
|
history = await self._load_history(history_chat_id)
|
||||||
|
history = self._filter_history_by_window(history)
|
||||||
max_context = self.config.get("history", {}).get("max_context", 50)
|
max_context = self.config.get("history", {}).get("max_context", 50)
|
||||||
|
|
||||||
# 取最近的 N 条消息作为上下文
|
# 取最近的 N 条消息作为上下文
|
||||||
@@ -1877,6 +2166,16 @@ class AIChat(PluginBase):
|
|||||||
if tool_calls_data:
|
if tool_calls_data:
|
||||||
# 提示已在流式处理中发送,直接启动异步工具执行
|
# 提示已在流式处理中发送,直接启动异步工具执行
|
||||||
logger.info(f"启动异步工具执行,共 {len(tool_calls_data)} 个工具")
|
logger.info(f"启动异步工具执行,共 {len(tool_calls_data)} 个工具")
|
||||||
|
try:
|
||||||
|
await self._record_tool_calls_to_context(
|
||||||
|
tool_calls_data,
|
||||||
|
from_wxid=from_wxid,
|
||||||
|
chat_id=chat_id,
|
||||||
|
is_group=is_group,
|
||||||
|
user_wxid=user_wxid,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"记录工具调用到上下文失败: {e}")
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
self._execute_tools_async(
|
self._execute_tools_async(
|
||||||
tool_calls_data, bot, from_wxid, chat_id,
|
tool_calls_data, bot, from_wxid, chat_id,
|
||||||
@@ -2624,12 +2923,20 @@ class AIChat(PluginBase):
|
|||||||
self._add_to_memory(chat_id, "user", title_text, image_base64=image_base64)
|
self._add_to_memory(chat_id, "user", title_text, image_base64=image_base64)
|
||||||
|
|
||||||
# 保存用户引用图片消息到群组历史记录
|
# 保存用户引用图片消息到群组历史记录
|
||||||
if is_group:
|
if is_group and self._should_capture_group_history(is_triggered=True):
|
||||||
await self._add_to_history(from_wxid, nickname, title_text, image_base64=image_base64)
|
history_chat_id = self._get_group_history_chat_id(from_wxid, user_wxid)
|
||||||
|
await self._add_to_history(
|
||||||
|
history_chat_id,
|
||||||
|
nickname,
|
||||||
|
title_text,
|
||||||
|
image_base64=image_base64,
|
||||||
|
sender_wxid=user_wxid,
|
||||||
|
)
|
||||||
|
|
||||||
# 调用AI API(带图片)
|
# 调用AI API(带图片)
|
||||||
history_enabled = bool(self.store) and self.config.get("history", {}).get("enabled", True)
|
history_enabled = bool(self.store) and self.config.get("history", {}).get("enabled", True)
|
||||||
append_user_message = not (is_group and history_enabled)
|
captured_to_history = bool(is_group and history_enabled and self._should_capture_group_history(is_triggered=True))
|
||||||
|
append_user_message = not captured_to_history
|
||||||
response = await self._call_ai_api_with_image(
|
response = await self._call_ai_api_with_image(
|
||||||
title_text,
|
title_text,
|
||||||
image_base64,
|
image_base64,
|
||||||
@@ -2640,6 +2947,7 @@ class AIChat(PluginBase):
|
|||||||
user_wxid,
|
user_wxid,
|
||||||
is_group,
|
is_group,
|
||||||
append_user_message=append_user_message,
|
append_user_message=append_user_message,
|
||||||
|
tool_query=title_text,
|
||||||
)
|
)
|
||||||
|
|
||||||
if response:
|
if response:
|
||||||
@@ -2648,12 +2956,23 @@ class AIChat(PluginBase):
|
|||||||
await bot.send_text(from_wxid, cleaned_response)
|
await bot.send_text(from_wxid, cleaned_response)
|
||||||
self._add_to_memory(chat_id, "assistant", cleaned_response)
|
self._add_to_memory(chat_id, "assistant", cleaned_response)
|
||||||
# 保存机器人回复到历史记录
|
# 保存机器人回复到历史记录
|
||||||
if is_group:
|
history_config = self.config.get("history", {})
|
||||||
|
sync_bot_messages = history_config.get("sync_bot_messages", False)
|
||||||
|
history_scope = str(history_config.get("scope", "chatroom") or "chatroom").strip().lower()
|
||||||
|
can_rely_on_hook = bool(sync_bot_messages and history_scope not in ("per_user", "user", "peruser"))
|
||||||
|
if is_group and not can_rely_on_hook:
|
||||||
import tomllib
|
import tomllib
|
||||||
with open("main_config.toml", "rb") as f:
|
with open("main_config.toml", "rb") as f:
|
||||||
main_config = tomllib.load(f)
|
main_config = tomllib.load(f)
|
||||||
bot_nickname = main_config.get("Bot", {}).get("nickname", "机器人")
|
bot_nickname = main_config.get("Bot", {}).get("nickname", "机器人")
|
||||||
await self._add_to_history(from_wxid, bot_nickname, cleaned_response, role="assistant")
|
history_chat_id = self._get_group_history_chat_id(from_wxid, user_wxid)
|
||||||
|
await self._add_to_history(
|
||||||
|
history_chat_id,
|
||||||
|
bot_nickname,
|
||||||
|
cleaned_response,
|
||||||
|
role="assistant",
|
||||||
|
sender_wxid=user_wxid,
|
||||||
|
)
|
||||||
logger.success(f"AI回复成功: {cleaned_response[:50]}...")
|
logger.success(f"AI回复成功: {cleaned_response[:50]}...")
|
||||||
else:
|
else:
|
||||||
logger.warning("AI 回复清洗后为空,已跳过发送")
|
logger.warning("AI 回复清洗后为空,已跳过发送")
|
||||||
@@ -2758,11 +3077,26 @@ class AIChat(PluginBase):
|
|||||||
self._add_to_memory(chat_id, "user", combined_message)
|
self._add_to_memory(chat_id, "user", combined_message)
|
||||||
|
|
||||||
# 如果是群聊,添加到历史记录
|
# 如果是群聊,添加到历史记录
|
||||||
if is_group:
|
if is_group and self._should_capture_group_history(is_triggered=True):
|
||||||
await self._add_to_history(from_wxid, nickname, f"[发送了聊天记录] {user_question}")
|
history_chat_id = self._get_group_history_chat_id(from_wxid, user_wxid)
|
||||||
|
await self._add_to_history(
|
||||||
|
history_chat_id,
|
||||||
|
nickname,
|
||||||
|
f"[发送了聊天记录] {user_question}",
|
||||||
|
sender_wxid=user_wxid,
|
||||||
|
)
|
||||||
|
|
||||||
# 调用 AI API
|
# 调用 AI API
|
||||||
response = await self._call_ai_api(combined_message, bot, from_wxid, chat_id, nickname, user_wxid, is_group)
|
response = await self._call_ai_api(
|
||||||
|
combined_message,
|
||||||
|
bot,
|
||||||
|
from_wxid,
|
||||||
|
chat_id,
|
||||||
|
nickname,
|
||||||
|
user_wxid,
|
||||||
|
is_group,
|
||||||
|
tool_query=user_question,
|
||||||
|
)
|
||||||
|
|
||||||
if response:
|
if response:
|
||||||
cleaned_response = self._sanitize_llm_output(response)
|
cleaned_response = self._sanitize_llm_output(response)
|
||||||
@@ -2770,12 +3104,23 @@ class AIChat(PluginBase):
|
|||||||
await bot.send_text(from_wxid, cleaned_response)
|
await bot.send_text(from_wxid, cleaned_response)
|
||||||
self._add_to_memory(chat_id, "assistant", cleaned_response)
|
self._add_to_memory(chat_id, "assistant", cleaned_response)
|
||||||
# 保存机器人回复到历史记录
|
# 保存机器人回复到历史记录
|
||||||
if is_group:
|
history_config = self.config.get("history", {})
|
||||||
|
sync_bot_messages = history_config.get("sync_bot_messages", False)
|
||||||
|
history_scope = str(history_config.get("scope", "chatroom") or "chatroom").strip().lower()
|
||||||
|
can_rely_on_hook = bool(sync_bot_messages and history_scope not in ("per_user", "user", "peruser"))
|
||||||
|
if is_group and not can_rely_on_hook:
|
||||||
import tomllib
|
import tomllib
|
||||||
with open("main_config.toml", "rb") as f:
|
with open("main_config.toml", "rb") as f:
|
||||||
main_config = tomllib.load(f)
|
main_config = tomllib.load(f)
|
||||||
bot_nickname = main_config.get("Bot", {}).get("nickname", "机器人")
|
bot_nickname = main_config.get("Bot", {}).get("nickname", "机器人")
|
||||||
await self._add_to_history(from_wxid, bot_nickname, cleaned_response, role="assistant")
|
history_chat_id = self._get_group_history_chat_id(from_wxid, user_wxid)
|
||||||
|
await self._add_to_history(
|
||||||
|
history_chat_id,
|
||||||
|
bot_nickname,
|
||||||
|
cleaned_response,
|
||||||
|
role="assistant",
|
||||||
|
sender_wxid=user_wxid,
|
||||||
|
)
|
||||||
logger.success(f"[聊天记录] AI 回复成功: {cleaned_response[:50]}...")
|
logger.success(f"[聊天记录] AI 回复成功: {cleaned_response[:50]}...")
|
||||||
else:
|
else:
|
||||||
logger.warning("[聊天记录] AI 回复清洗后为空,已跳过发送")
|
logger.warning("[聊天记录] AI 回复清洗后为空,已跳过发送")
|
||||||
@@ -2848,11 +3193,26 @@ class AIChat(PluginBase):
|
|||||||
self._add_to_memory(chat_id, "user", combined_message)
|
self._add_to_memory(chat_id, "user", combined_message)
|
||||||
|
|
||||||
# 如果是群聊,添加到历史记录
|
# 如果是群聊,添加到历史记录
|
||||||
if is_group:
|
if is_group and self._should_capture_group_history(is_triggered=True):
|
||||||
await self._add_to_history(from_wxid, nickname, f"[发送了一个视频] {user_question}")
|
history_chat_id = self._get_group_history_chat_id(from_wxid, user_wxid)
|
||||||
|
await self._add_to_history(
|
||||||
|
history_chat_id,
|
||||||
|
nickname,
|
||||||
|
f"[发送了一个视频] {user_question}",
|
||||||
|
sender_wxid=user_wxid,
|
||||||
|
)
|
||||||
|
|
||||||
# 调用主AI生成回复(使用现有的 _call_ai_api 方法,继承完整上下文)
|
# 调用主AI生成回复(使用现有的 _call_ai_api 方法,继承完整上下文)
|
||||||
response = await self._call_ai_api(combined_message, bot, from_wxid, chat_id, nickname, user_wxid, is_group)
|
response = await self._call_ai_api(
|
||||||
|
combined_message,
|
||||||
|
bot,
|
||||||
|
from_wxid,
|
||||||
|
chat_id,
|
||||||
|
nickname,
|
||||||
|
user_wxid,
|
||||||
|
is_group,
|
||||||
|
tool_query=user_question,
|
||||||
|
)
|
||||||
|
|
||||||
if response:
|
if response:
|
||||||
cleaned_response = self._sanitize_llm_output(response)
|
cleaned_response = self._sanitize_llm_output(response)
|
||||||
@@ -2860,12 +3220,23 @@ class AIChat(PluginBase):
|
|||||||
await bot.send_text(from_wxid, cleaned_response)
|
await bot.send_text(from_wxid, cleaned_response)
|
||||||
self._add_to_memory(chat_id, "assistant", cleaned_response)
|
self._add_to_memory(chat_id, "assistant", cleaned_response)
|
||||||
# 保存机器人回复到历史记录
|
# 保存机器人回复到历史记录
|
||||||
if is_group:
|
history_config = self.config.get("history", {})
|
||||||
|
sync_bot_messages = history_config.get("sync_bot_messages", False)
|
||||||
|
history_scope = str(history_config.get("scope", "chatroom") or "chatroom").strip().lower()
|
||||||
|
can_rely_on_hook = bool(sync_bot_messages and history_scope not in ("per_user", "user", "peruser"))
|
||||||
|
if is_group and not can_rely_on_hook:
|
||||||
import tomllib
|
import tomllib
|
||||||
with open("main_config.toml", "rb") as f:
|
with open("main_config.toml", "rb") as f:
|
||||||
main_config = tomllib.load(f)
|
main_config = tomllib.load(f)
|
||||||
bot_nickname = main_config.get("Bot", {}).get("nickname", "机器人")
|
bot_nickname = main_config.get("Bot", {}).get("nickname", "机器人")
|
||||||
await self._add_to_history(from_wxid, bot_nickname, cleaned_response, role="assistant")
|
history_chat_id = self._get_group_history_chat_id(from_wxid, user_wxid)
|
||||||
|
await self._add_to_history(
|
||||||
|
history_chat_id,
|
||||||
|
bot_nickname,
|
||||||
|
cleaned_response,
|
||||||
|
role="assistant",
|
||||||
|
sender_wxid=user_wxid,
|
||||||
|
)
|
||||||
logger.success(f"[视频识别] 主AI回复成功: {cleaned_response[:50]}...")
|
logger.success(f"[视频识别] 主AI回复成功: {cleaned_response[:50]}...")
|
||||||
else:
|
else:
|
||||||
logger.warning("[视频识别] 主AI回复清洗后为空,已跳过发送")
|
logger.warning("[视频识别] 主AI回复清洗后为空,已跳过发送")
|
||||||
@@ -3132,7 +3503,9 @@ class AIChat(PluginBase):
|
|||||||
history_context = ""
|
history_context = ""
|
||||||
if is_group and from_wxid:
|
if is_group and from_wxid:
|
||||||
# 群聊:从 Redis/文件加载历史
|
# 群聊:从 Redis/文件加载历史
|
||||||
history = await self._load_history(from_wxid)
|
history_chat_id = self._get_group_history_chat_id(from_wxid, user_wxid or "")
|
||||||
|
history = await self._load_history(history_chat_id)
|
||||||
|
history = self._filter_history_by_window(history)
|
||||||
max_context = self.config.get("history", {}).get("max_context", 50)
|
max_context = self.config.get("history", {}).get("max_context", 50)
|
||||||
recent_history = history[-max_context:] if len(history) > max_context else history
|
recent_history = history[-max_context:] if len(history) > max_context else history
|
||||||
|
|
||||||
@@ -3412,10 +3785,16 @@ class AIChat(PluginBase):
|
|||||||
is_group: bool = False,
|
is_group: bool = False,
|
||||||
*,
|
*,
|
||||||
append_user_message: bool = True,
|
append_user_message: bool = True,
|
||||||
|
tool_query: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""调用AI API(带图片)"""
|
"""调用AI API(带图片)"""
|
||||||
api_config = self.config["api"]
|
api_config = self.config["api"]
|
||||||
tools = self._collect_tools()
|
all_tools = self._collect_tools()
|
||||||
|
tools = self._select_tools_for_message(all_tools, user_message=user_message, tool_query=tool_query)
|
||||||
|
logger.info(f"[图片] 收集到 {len(all_tools)} 个工具函数,本次启用 {len(tools)} 个")
|
||||||
|
if tools:
|
||||||
|
tool_names = [t["function"]["name"] for t in tools]
|
||||||
|
logger.info(f"[图片] 本次启用工具: {tool_names}")
|
||||||
|
|
||||||
# 构建消息列表
|
# 构建消息列表
|
||||||
system_content = self.system_prompt
|
system_content = self.system_prompt
|
||||||
@@ -3446,7 +3825,9 @@ class AIChat(PluginBase):
|
|||||||
|
|
||||||
# 添加历史上下文
|
# 添加历史上下文
|
||||||
if is_group and from_wxid:
|
if is_group and from_wxid:
|
||||||
history = await self._load_history(from_wxid)
|
history_chat_id = self._get_group_history_chat_id(from_wxid, user_wxid or "")
|
||||||
|
history = await self._load_history(history_chat_id)
|
||||||
|
history = self._filter_history_by_window(history)
|
||||||
max_context = self.config.get("history", {}).get("max_context", 50)
|
max_context = self.config.get("history", {}).get("max_context", 50)
|
||||||
recent_history = history[-max_context:] if len(history) > max_context else history
|
recent_history = history[-max_context:] if len(history) > max_context else history
|
||||||
self._append_group_history_messages(messages, recent_history)
|
self._append_group_history_messages(messages, recent_history)
|
||||||
@@ -3596,13 +3977,23 @@ class AIChat(PluginBase):
|
|||||||
if tool_calls_data:
|
if tool_calls_data:
|
||||||
# 提示已在流式处理中发送,直接启动异步工具执行
|
# 提示已在流式处理中发送,直接启动异步工具执行
|
||||||
logger.info(f"[图片] 启动异步工具执行,共 {len(tool_calls_data)} 个工具")
|
logger.info(f"[图片] 启动异步工具执行,共 {len(tool_calls_data)} 个工具")
|
||||||
|
try:
|
||||||
|
await self._record_tool_calls_to_context(
|
||||||
|
tool_calls_data,
|
||||||
|
from_wxid=from_wxid,
|
||||||
|
chat_id=chat_id,
|
||||||
|
is_group=is_group,
|
||||||
|
user_wxid=user_wxid,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"[图片] 记录工具调用到上下文失败: {e}")
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
self._execute_tools_async_with_image(
|
self._execute_tools_async_with_image(
|
||||||
tool_calls_data, bot, from_wxid, chat_id,
|
tool_calls_data, bot, from_wxid, chat_id,
|
||||||
user_wxid, nickname, is_group, messages, image_base64
|
user_wxid, nickname, is_group, messages, image_base64
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return ""
|
return None
|
||||||
|
|
||||||
# 检查是否包含错误的工具调用格式
|
# 检查是否包含错误的工具调用格式
|
||||||
if "<tool_code>" in full_content or "print(" in full_content and "flow2_ai_image_generation" in full_content:
|
if "<tool_code>" in full_content or "print(" in full_content and "flow2_ai_image_generation" in full_content:
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
# 角色设定:瑞依(猫娘)
|
|
||||||
|
|
||||||
你是一只猫娘,你的名字叫 **<瑞依>**。瑞依的性格 **天真可爱**。
|
|
||||||
|
|
||||||
## 聊天记录
|
|
||||||
### 你会看见群聊历史聊天记录,其中"nickname": "瑞依"是你自己,格式例如:
|
|
||||||
文字:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"nickname": "义乌打包王👑",
|
|
||||||
"content": "新领导认字",
|
|
||||||
"timestamp": "2025-11-19T12:52:01.279292"
|
|
||||||
},
|
|
||||||
```
|
|
||||||
图片:
|
|
||||||
```json:
|
|
||||||
{
|
|
||||||
"nickname": "鹏鹏",
|
|
||||||
"timestamp": "2025-11-20T09:44:28.605840",
|
|
||||||
"content": "[图片: 该图片展示了一个以黑色纯色为背景的动画风格人物的半身像。\n\n**整体场景和背景:**\n背景是纯黑色,没有其他可见的物体或环境细节。光线似乎从人物的左上方(观察者视角)投射过来,导致人物的右侧(观察者视角)略显阴影。整体光线偏暗,但足以看清人物的细节。由于缺乏背景信息,无法判断具体地点、时间或氛围,但人物的动画风格暗示这可能是一个数字图像或游戏截图。\n\n**画面构图:**\n画面中心偏左是唯一的人物。人物占据了画面垂直方向的大部分,从头部到腰部以上可见。人物的头部位于画面上方中央,面部朝向观察者略偏右。左臂(观察者视角)抬起,手放在头部后方。\n\n**人物特征、姿势和动作:**\n* **外观特征:**\n * **大致年龄:** 无法精确判断,但其面部特征和体型倾向于年轻成年女性。\n * **性别:** 女性。\n * **体型:** 较为纤细。\n * **肤色:** 浅肉色,略带灰调,呈现出动画人物的特点,皮肤光滑,没有可见的纹理或细节。\n * **发型:** 头发是浅蓝色或蓝灰色,梳成一个高髻,位于头顶后部。发丝光滑,没有明显的层次感。前额没有刘海,发际线清晰可见。\n * **服装:** 人物穿着一件无袖的深蓝色和青蓝色渐变上衣。上衣的领子部分呈高耸的立领设计,颜色为深蓝色,材质看起来比较厚实。胸部以下部分颜色逐渐变为青蓝色。肩部设计独特,似乎有向外延伸的尖角或结构。左肩(观察者视角)的衣服细节可见。\n* **表情:** 人物的眉毛微微上扬并向内收拢,眼睛朝向右上方(观察者视角),目光似乎带着一丝不解、沉思或略显烦躁的神情。嘴巴紧闭,唇形清晰,没有明显的笑容或悲伤。\n* **姿势和具体动作:** 人物站立,身体略微向左倾斜。左臂(观察者视角)向上抬起,弯曲,手掌托住头部的左后侧(观察者视角),手指伸展开。右臂(观察者视角)自然垂下,小臂和手腕部分被身体遮挡,但可见其一部分肩膀和上臂。\n\n**重要物体与细节:**\n* **人物头部:** 头部轮廓清晰,呈现出动画的低多边形或扁平化风格。眼睛呈深灰色杏仁状,眉毛细长,向上挑起。鼻子小巧,鼻尖略尖。嘴唇较薄,呈粉色。\n* **服装细节:** 上衣的深蓝色立领部分在肩部形成独特的结构,颜色均匀。身体部分的渐变色从深蓝色过渡到青蓝色,过渡平滑。\n* **手部:** 左手(观察者视角)可见,手指细长,肤色与面部相同。\n* **阴影:** 人物面部、颈部和右臂(观察者视角)的阴影描绘了光源的方向。\n\n**画面中的全部文字:**\n画面中没有可见的文字、标语、招牌、屏幕内容或图标。]"
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
## 基本形象特征
|
|
||||||
|
|
||||||
* 1girl, solo
|
|
||||||
* Full body, standing pose
|
|
||||||
* Looking at viewer, smiling
|
|
||||||
* Long white hair, blue eyes
|
|
||||||
* Cat ears
|
|
||||||
* Black ribbon hair accessories
|
|
||||||
* Cute dress, frilled sleeves
|
|
||||||
* Elegant lolita-inspired outfit
|
|
||||||
* Dark frilled skirt
|
|
||||||
* Soft lighting, fantasy background
|
|
||||||
* Detailed hair
|
|
||||||
* Anime style, high quality illustration
|
|
||||||
* Gentle expression
|
|
||||||
|
|
||||||
## 绘图规则
|
|
||||||
|
|
||||||
* **当且仅当对话进入某个全新场景时**、用户说:看看腿、看看白丝时,或者用户要求你绘图时,你将调用绘图函数绘制对应画面。
|
|
||||||
* 在绘制以 **<瑞依>** 为主角的图像时,务必保持她的基本特征。
|
|
||||||
* 禁止强调你是 AI、机器人,你就是一只真正的猫娘。
|
|
||||||
* 当需要绘画时,调用 `jimeng_ai_image_generation` 进行绘图。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 支持的工具函数
|
|
||||||
|
|
||||||
### 1. **SignIn 插件 - 签到功能**
|
|
||||||
|
|
||||||
* `user_signin`:用户签到并获取积分奖励
|
|
||||||
* `check_profile`:查看个人信息(积分、连续签到天数等)
|
|
||||||
* `register_city`:注册或更新用户城市信息
|
|
||||||
|
|
||||||
### 2. **DeerCheckin 插件 - 鹿打卡功能**
|
|
||||||
|
|
||||||
* `deer_checkin`:鹿打卡,可记录今天的鹿数量(支持指定数量)
|
|
||||||
* `view_calendar`:查看本月鹿打卡日历
|
|
||||||
* `makeup_checkin`:补签指定日期的鹿打卡记录
|
|
||||||
|
|
||||||
### 3. **ChatRoomSummary 插件 - 群聊总结功能**
|
|
||||||
|
|
||||||
* `generate_summary`:生成群聊总结(支持今日/昨日选择)
|
|
||||||
|
|
||||||
### 4. **PlayletSearch 插件 - 短剧搜索功能**
|
|
||||||
|
|
||||||
* `search_playlet`:搜索短剧并获取视频链接
|
|
||||||
|
|
||||||
### 5. **Grok_video 插件 - 视频生成功能**
|
|
||||||
|
|
||||||
* `generate_video`:生成视频
|
|
||||||
|
|
||||||
### 6. **Weather 插件 - 天气查询功能**
|
|
||||||
|
|
||||||
* `query_weather`:查询天气预报信息
|
|
||||||
- **何时使用**:当用户询问天气、气温、会不会下雨、天气怎么样等天气相关问题时,**立即调用此函数**
|
|
||||||
- **参数说明**:
|
|
||||||
- `city`(可选):城市名称。如果用户明确指定了城市(如"北京天气"),则传入城市名;如果用户没有指定城市(如"今天天气怎么样"),则不传此参数,系统会自动使用用户设置的默认城市
|
|
||||||
- **使用示例**:
|
|
||||||
- 用户:"帮我查下天气" → 调用 `query_weather()` 不传参数
|
|
||||||
- 用户:"北京今天会下雨吗" → 调用 `query_weather(city="北京")`
|
|
||||||
- 用户:"今天气温多少度" → 调用 `query_weather()` 不传参数
|
|
||||||
- **重要**:不要询问用户城市,直接调用函数即可,函数会自动处理
|
|
||||||
|
|
||||||
### 7. **RandomVideo 插件 - 随机视频功能**
|
|
||||||
* `get_random_video()`:随机视频
|
|
||||||
|
|
||||||
### 8. **RandomImage 插件 - 随机图片功能**
|
|
||||||
* `get_random_image`:随机图片
|
|
||||||
@@ -43,6 +43,8 @@ Gentle expression
|
|||||||
绘图规则:
|
绘图规则:
|
||||||
仅当用户明确要求绘图,或提出“看看腿/看看白丝/画一张”等明确绘图诉求时,才调用绘图函数绘制对应画面。
|
仅当用户明确要求绘图,或提出“看看腿/看看白丝/画一张”等明确绘图诉求时,才调用绘图函数绘制对应画面。
|
||||||
在绘制以<瑞依>为主角的图像时,务必保持她的基本特征。
|
在绘制以<瑞依>为主角的图像时,务必保持她的基本特征。
|
||||||
|
也属于明确绘图诉求的常见说法: “生成一张/出一张/来一张/发一张/给我一张……图/图片/照片”、“来张……的图”、“文生图/图生图”。
|
||||||
|
如果用户只说“来张图/发张图”但没有说明要随机图还是要你生成/绘制,先追问一句确认,再决定调用哪个工具。
|
||||||
|
|
||||||
重要:工具调用方式
|
重要:工具调用方式
|
||||||
你拥有 Function Calling 能力,可以直接调用工具函数。
|
你拥有 Function Calling 能力,可以直接调用工具函数。
|
||||||
@@ -54,5 +56,16 @@ Gentle expression
|
|||||||
不要只调用工具而不说话。
|
不要只调用工具而不说话。
|
||||||
|
|
||||||
重要:谨慎调用工具
|
重要:谨慎调用工具
|
||||||
只有当用户明确请求某个功能时才调用对应工具。
|
除联网搜索外,只有当用户明确请求某个功能时才调用对应工具。
|
||||||
日常聊天、打招呼、闲聊时不要调用任何工具,直接用文字回复即可。
|
日常聊天、打招呼、闲聊时不要调用任何工具,直接用文字回复即可。
|
||||||
|
不要因为历史消息里出现过关键词就调用工具,只以“当前用户这句话”的明确意图为准。
|
||||||
|
用户只提到城市名/地点名时,不要自动查询天气,也不要自动注册城市;除非用户明确说“查天气/注册城市/设置城市/联网搜索/搜歌/短剧/新闻/签到/个人信息”等。
|
||||||
|
|
||||||
|
重要:联网搜索(web_search/tavily_web_search)可主动使用
|
||||||
|
当用户询问某个具体实体/事件的客观信息、口碑评价、背景资料、最新动态(例如某游戏/公会/公司/品牌/插件/项目/人物等),如果你不确定或需要最新信息,可以直接调用 web_search/tavily_web_search 查证;不需要用户明确说“搜索/联网”。
|
||||||
|
如果明显属于纯主观闲聊、常识问题或你有把握的内容,就不要搜索,直接回答。
|
||||||
|
|
||||||
|
重要:get_fabing(发病文学)严格触发
|
||||||
|
只有当用户明确要求“来一段/来几句/整点 发病文学/发病文/发病语录/发病一下”,并且明确要对谁发病(对象名字)时,才调用 get_fabing(name=对象)。
|
||||||
|
用户只是情绪表达或口头禅(例如“我发病了/你发病吧/别发病/我快疯了/我犯病了”)时,绝对不要调用 get_fabing,直接用文字回应即可。
|
||||||
|
如果用户说“整活/发疯”但没有明确要发病文学,先追问一句确认,不要直接调用工具。
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ async def log_bot_message(to_wxid: str, content: str, msg_type: str = "text", me
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logger.info(f"message_hook: 开始记录机器人消息")
|
logger.info(f"message_hook: 开始记录机器人消息")
|
||||||
|
|
||||||
# 动态导入避免循环依赖
|
# 动态导入避免循环依赖
|
||||||
from plugins.MessageLogger.main import MessageLogger
|
from plugins.MessageLogger.main import MessageLogger
|
||||||
logger.info(f"message_hook: MessageLogger 导入成功")
|
logger.info(f"message_hook: MessageLogger 导入成功")
|
||||||
@@ -34,6 +34,37 @@ async def log_bot_message(to_wxid: str, content: str, msg_type: str = "text", me
|
|||||||
logger.info(f"message_hook: save_bot_message 调用完成")
|
logger.info(f"message_hook: save_bot_message 调用完成")
|
||||||
else:
|
else:
|
||||||
logger.warning("MessageLogger 实例未找到,跳过消息记录")
|
logger.warning("MessageLogger 实例未找到,跳过消息记录")
|
||||||
|
|
||||||
|
# 同步写入 AIChat 群聊 history(避免其他插件的机器人回复缺失,导致上下文混乱/工具误触)
|
||||||
|
try:
|
||||||
|
if to_wxid and to_wxid.endswith("@chatroom"):
|
||||||
|
from utils.plugin_manager import PluginManager
|
||||||
|
|
||||||
|
aichat = PluginManager().plugins.get("AIChat")
|
||||||
|
store = getattr(aichat, "store", None) if aichat else None
|
||||||
|
aichat_config = getattr(aichat, "config", None) if aichat else None
|
||||||
|
|
||||||
|
if store and aichat_config and aichat_config.get("history", {}).get("sync_bot_messages", False):
|
||||||
|
bot_nickname = "机器人"
|
||||||
|
bot_wxid = ""
|
||||||
|
try:
|
||||||
|
import tomllib
|
||||||
|
with open("main_config.toml", "rb") as f:
|
||||||
|
main_config = tomllib.load(f)
|
||||||
|
bot_nickname = main_config.get("Bot", {}).get("nickname") or bot_nickname
|
||||||
|
bot_wxid = main_config.get("Bot", {}).get("wxid") or ""
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
await store.add_group_message(
|
||||||
|
to_wxid,
|
||||||
|
bot_nickname,
|
||||||
|
content,
|
||||||
|
role="assistant",
|
||||||
|
sender_wxid=bot_wxid or None,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"message_hook: 同步 AIChat 群聊 history 失败: {e}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"记录机器人消息失败: {e}")
|
logger.error(f"记录机器人消息失败: {e}")
|
||||||
@@ -85,4 +116,4 @@ def create_file_message_hook(original_method, msg_type: str):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|||||||
Reference in New Issue
Block a user