Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .github/workflows/prepare-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ jobs:

echo "Updated README.md latest version section"

- name: Commit changes
- name: Commit changes (amend to changelog commit)
run: |
VERSION="${{ steps.version.outputs.version }}"

Expand All @@ -182,10 +182,11 @@ jobs:
exit 0
fi

git commit -m "chore: bump version to $VERSION"
git push origin HEAD:dev
# 将 bump version 变更合并到 changelog 提交中
git commit --amend -m "release: $VERSION"
git push origin HEAD:dev --force-with-lease

echo "Committed and pushed changes"
echo "Amended changelog commit with version bump"

- name: Create Pull Request
env:
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

![SpectreCore](https://avatars.githubusercontent.com/u/129108081?s=48&v=4)

[![version](https://img.shields.io/badge/version-v2.1.9-blue.svg?style=flat-square)](https://github.com/23q3/astrbot_plugin_SpectreCore)
[![version](https://img.shields.io/badge/version-v2.1.10-blue.svg?style=flat-square)](https://github.com/23q3/astrbot_plugin_SpectreCore)
[![license](https://img.shields.io/badge/license-AGPL--3.0-green.svg?style=flat-square)](LICENSE)
[![author](https://img.shields.io/badge/author-23q3-orange.svg?style=flat-square)](https://github.com/23q3)

Expand Down Expand Up @@ -70,9 +70,11 @@ SpectreCore (影芯) 是一个为 AstrBot 设计的高级群聊互动插件,

## 📋 最新版本

### v2.1.9 (2026-01-05)
### v2.1.10 (2026-01-07)

- 🐛 **修复GIF图片格式识别错误** - 修复图片URL优先级问题,优先使用file字段以保留准确格式信息,避免HTTP URL下载后MIME类型检测错误导致Gemini API返回400 [#81](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/81)
- ✨ **新增群聊黑名单和全局回复开关** - 添加群聊黑名单功能,可禁用指定群的自动回复;新增全局回复开关,一键启用/禁用所有群的自动回复功能 [#61](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/61)[#83](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/83)
- 🔄 **使用AstrBot原生UMO人格机制** - 移除自定义人格注入逻辑,改用AstrBot原生的Unified Model Output人格机制,提高兼容性和稳定性 [#77](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/77)
- 🐛 **修复当前消息图片未被转述** - 修复当前消息中的图片未被包含在历史记录转述中的问题 [#84](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/84)[#85](https://github.com/23q3/astrbot_plugin_SpectreCore/pull/85) @lymangos

## ⚠️ 注意事项

Expand Down
20 changes: 14 additions & 6 deletions _conf_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,26 @@
"hint": "决定了会输入给大模型多少条q群历史消息(最多200条)",
"default": 20
},
"enable_all_groups": {
"description": "回复所有群聊",
"type": "bool",
"hint": "开启后将回复所有群聊(黑名单中的群除外)。关闭则只回复白名单中的群。",
"default": false
},
"enabled_groups": {
"description": "启用回复功能的群聊列表",
"type": "list",
"items": {"type": "string"},
"hint": "只有在这个列表中的群聊才会启用回复功能,如果为空则不启用回复功能",
"default": []
},
"blocked_groups": {
"description": "黑名单群组列表",
"type": "list",
"items": {"type": "string"},
"hint": "列表中的群聊将不会回复,优先级高于白名单和全局开关。",
"default": []
},
"enabled_private": {
"description": "启用私聊回复功能",
"type": "bool",
Expand All @@ -23,12 +37,6 @@
"hint": "是否过滤大模型回复中被<think></think>标签包裹的思考内容",
"default": true
},
"persona":{
"description":"使用的人格",
"type":"string",
"hint":"填写人格名称,如果为空则不使用人格",
"default":""
},
"read_air":{
"description":"是否开启读空气",
"type":"bool",
Expand Down
3 changes: 3 additions & 0 deletions changelogs/v2.1.10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- ✨ **新增群聊黑名单和全局回复开关** - 添加群聊黑名单功能,可禁用指定群的自动回复;新增全局回复开关,一键启用/禁用所有群的自动回复功能 [#61](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/61)[#83](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/83)
- 🔄 **使用AstrBot原生UMO人格机制** - 移除自定义人格注入逻辑,改用AstrBot原生的Unified Model Output人格机制,提高兼容性和稳定性 [#77](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/77)
- 🐛 **修复当前消息图片未被转述** - 修复当前消息中的图片未被包含在历史记录转述中的问题 [#84](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/84)[#85](https://github.com/23q3/astrbot_plugin_SpectreCore/pull/85) @lymangos
6 changes: 3 additions & 3 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"spectrecore",
"23q3",
"使大模型更好的主动回复群聊中的消息,带来生动和沉浸的群聊对话体验",
"2.1.9",
"2.1.10",
"https://github.com/23q3/astrbot_plugin_SpectreCore"
)
class SpectreCore(Star):
Expand Down Expand Up @@ -167,8 +167,8 @@ async def history(self, event: AstrMessageEvent, count: int = 10):
# 只取最近的记录
recent_history = history[-count:] if len(history) > count else history

# 格式化历史记录
formatted_history = await MessageUtils.format_history_for_llm(recent_history)
# 格式化历史记录
formatted_history = await MessageUtils.format_history_for_llm(recent_history, umo=event.unified_msg_origin)

# 添加标题
chat_type = "私聊" if is_private else f"群聊({chat_id})"
Expand Down
2 changes: 1 addition & 1 deletion metadata.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: spectrecore # 这是你的插件的唯一识别名。
desc: 使大模型更好的主动回复群聊中的消息,带来生动和沉浸的群聊对话体验 # 插件简短描述
help: 自动检测群聊消息并让AI模型进行回复,让群聊更加生动有趣。 # 插件的帮助信息
version: v2.1.9 # 插件版本号。格式:v1.1.1 或者 v1.1
version: v2.1.10 # 插件版本号。格式:v1.1.1 或者 v1.1
author: 23q3 # 作者
repo: https://github.com/23q3/astrbot_plugin_SpectreCore # 插件的仓库地址
display_name: 🌟 SpectreCore
15 changes: 14 additions & 1 deletion utils/history_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,20 @@ def is_chat_enabled(event: AstrMessageEvent) -> bool:
return HistoryStorage.config.get("enabled_private", False)
else:
group_id = event.get_group_id()
return group_id in HistoryStorage.config.get("enabled_groups", [])
if not group_id:
return False
group_id = str(group_id).strip()

# 获取配置集合并规范化 (O(1) 查找)
blocked_groups = {str(g).strip() for g in HistoryStorage.config.get("blocked_groups", []) if str(g).strip()}
enabled_groups = {str(g).strip() for g in HistoryStorage.config.get("enabled_groups", []) if str(g).strip()}

# 优先级: 黑名单 > 全局开关 > 白名单
if group_id in blocked_groups:
return False
if HistoryStorage.config.get("enable_all_groups", False):
return True
return group_id in enabled_groups

@staticmethod
async def process_and_save_user_message(event: AstrMessageEvent) -> None:
Expand Down
10 changes: 6 additions & 4 deletions utils/image_caption.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,17 @@ def init(context: Context, config: AstrBotConfig):
@staticmethod
async def generate_image_caption(
image: str, # 图片的base64编码或URL
umo: Optional[str] = None, # unified_msg_origin,用于 UMO 路由
timeout: int = 30
) -> Optional[str]:
"""
为单张图片生成文字描述

Args:
image: 图片的base64编码或URL
umo: unified_msg_origin,用于获取对应 UMO 的 provider
timeout: 超时时间(秒)

Returns:
生成的图片描述文本,如果失败则返回None
"""
Expand All @@ -55,9 +57,9 @@ async def generate_image_caption(
return None

provider_id = image_processing_config.get("image_caption_provider_id", "")
# 获取提供商
# 获取提供商,支持 UMO 路由
if provider_id == "":
provider = context.get_using_provider()
provider = context.get_using_provider(umo=umo)
else:
provider = context.get_provider_by_id(provider_id)

Expand Down
81 changes: 60 additions & 21 deletions utils/llm_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from .history_storage import HistoryStorage
from .message_utils import MessageUtils
from astrbot.core.provider.entites import ProviderRequest
from .persona_utils import PersonaUtils

class LLMUtils:
"""
Expand Down Expand Up @@ -116,29 +115,66 @@ async def call_llm(event: AstrMessageEvent, config: AstrBotConfig, context: Cont
# 准备并调用大模型
func_tools_mgr = context.get_llm_tool_manager() if config.get("use_func_tool", False) else None

# 获取配置中指定的人格
# 使用 AstrBot 原生 UMO 人格机制获取人格
system_prompt = ""
contexts = []
persona_name = config.get("persona", "")
umo = event.unified_msg_origin

if persona_name:
try:
persona = PersonaUtils.get_persona_by_name(context, persona_name)
if persona:
system_prompt = persona.get('prompt', '')
if persona.get('_mood_imitation_dialogs_processed'):
mood_dialogs = persona.get('_mood_imitation_dialogs_processed', [])
system_prompt += "\n请模仿以下示例的对话风格来反应(示例中,a代表用户,b代表你)\n" + mood_dialogs
try:
# 遵循 AstrBot 原生人格获取优先级:
# 1. session_service_config.persona_id (会话级别配置)
# 2. 配置文件的 default_personality
# 3. 全局默认人格

begin_dialogs = persona.get('_begin_dialogs_processed', [])
if begin_dialogs:
contexts.extend(begin_dialogs)
persona_id = None
persona = None

logger.debug(f"找到人格 '{persona_name}' ")
else:
logger.warning(f"未找到名为 '{persona_name}' 的人格")
# 优先级1: 查询会话级别的人格配置 (通过全局 SharedPreferences)
try:
from astrbot.api import sp
session_config = await sp.get_async(
scope="umo", scope_id=umo, key="session_service_config", default={}
)
persona_id = session_config.get("persona_id")
if persona_id:
logger.debug(f"从 session_service_config 获取人格: '{persona_id}'")
except Exception as e:
logger.error(f"获取人格信息失败: {e}")
logger.debug(f"获取 session_service_config 失败: {e}")

# 优先级2/3: 使用 get_default_persona_v3 获取配置文件或全局默认人格
if not persona_id:
if hasattr(context, 'persona_manager') and hasattr(context.persona_manager, 'get_default_persona_v3'):
persona = await context.persona_manager.get_default_persona_v3(umo=umo)
persona_id = persona.get('name') if persona else None
else:
# Fallback: 旧版 AstrBot 兼容
persona = context.persona_manager.selected_default_persona_v3 if hasattr(context, 'persona_manager') else None
persona_id = persona.get('name') if persona else None

# 根据 persona_id 获取完整的人格数据
if persona_id and not persona:
try:
persona = next(
(p for p in context.persona_manager.personas_v3 if p["name"] == persona_id),
None
)
except Exception:
pass

if persona:
system_prompt = persona.get('prompt', '')
if persona.get('_mood_imitation_dialogs_processed'):
mood_dialogs = persona.get('_mood_imitation_dialogs_processed', '')
if mood_dialogs:
system_prompt += "\n请模仿以下示例的对话风格来反应(示例中,a代表用户,b代表你)\n" + str(mood_dialogs)

begin_dialogs = persona.get('_begin_dialogs_processed', [])
if begin_dialogs:
contexts.extend(begin_dialogs)

logger.debug(f"使用 UMO '{umo}' 对应的人格: '{persona.get('name', 'default')}'")
except Exception as e:
logger.error(f"获取人格信息失败: {e}")

# 构建环境描述(注入到 system_prompt,不污染 prompt)
env_description = f"\n\n你正在浏览聊天软件,你在聊天软件上的id是{event.get_self_id()}"
Expand Down Expand Up @@ -181,7 +217,7 @@ async def call_llm(event: AstrMessageEvent, config: AstrBotConfig, context: Cont
# 回退到排除最后一条
history_for_context = history_messages[:-1] if len(history_messages) > 1 else []
if history_for_context:
formatted_history = await MessageUtils.format_history_for_llm(history_for_context, max_messages=history_limit)
formatted_history = await MessageUtils.format_history_for_llm(history_for_context, max_messages=history_limit, umo=umo)
env_description += "\n\n以下是最近的聊天记录:\n" + formatted_history
else:
env_description += "\n\n你没看见任何聊天记录,看来最近没有消息。"
Expand Down Expand Up @@ -228,8 +264,11 @@ async def call_llm(event: AstrMessageEvent, config: AstrBotConfig, context: Cont
if image_urls:
system_prompt += f"\n\n已经按照从晚到早的顺序为你提供了聊天记录中的{len(image_urls)}张图片,你可以直接查看并理解它们。这些图片出现在聊天记录中。"

# prompt 只保留用户当前消息,保持干净供 KB 检索
prompt = event.get_message_outline()
# prompt 只保留用户当前消息,使用 MessageUtils 确保图片被转述
if hasattr(event, "message_obj") and hasattr(event.message_obj, "message"):
prompt = await MessageUtils.outline_message_list(event.message_obj.message, umo=umo)
else:
prompt = event.get_message_outline()

return event.request_llm(
prompt=prompt,
Expand Down
29 changes: 19 additions & 10 deletions utils/message_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from astrbot.api.all import *
from typing import List, Dict, Any
from typing import List, Dict, Any, Optional
import os
import time
from datetime import datetime
Expand All @@ -15,14 +15,15 @@ class MessageUtils:
"""

@staticmethod
async def format_history_for_llm(history_messages: List[AstrBotMessage], max_messages: int = 20) -> str:
async def format_history_for_llm(history_messages: List[AstrBotMessage], max_messages: int = 20, umo: Optional[str] = None) -> str:
"""
将历史消息列表格式化为适合输入给大模型的文本格式

Args:
history_messages: 历史消息列表
max_messages: 最大消息数量,默认20条

umo: unified_msg_origin,用于 UMO 路由

Returns:
格式化后的历史消息文本
"""
Expand Down Expand Up @@ -54,7 +55,7 @@ async def format_history_for_llm(history_messages: List[AstrBotMessage], max_mes
pass

# 获取消息内容 (异步调用)
message_content = await MessageUtils.outline_message_list(msg.message) if hasattr(msg, "message") and msg.message else ""
message_content = await MessageUtils.outline_message_list(msg.message, umo=umo) if hasattr(msg, "message") and msg.message else ""

# 格式化该条消息
message_text = f"发送者: {sender_name} (ID: {sender_id})\n"
Expand All @@ -71,10 +72,14 @@ async def format_history_for_llm(history_messages: List[AstrBotMessage], max_mes
return formatted_text

@staticmethod
async def outline_message_list(message_list: List[BaseMessageComponent]) -> str:
async def outline_message_list(message_list: List[BaseMessageComponent], umo: Optional[str] = None) -> str:
"""
获取消息概要。
使用类型检查而不是类实例检查,避免依赖不存在的类。

Args:
message_list: 消息组件列表
umo: unified_msg_origin,用于 UMO 路由
"""
outline = ""
for i in message_list:
Expand All @@ -86,7 +91,7 @@ async def outline_message_list(message_list: List[BaseMessageComponent]) -> str:

# 特别优化 Reply 组件的处理
if component_type == "reply" or isinstance(i, Reply):
outline += await MessageUtils._format_reply_component(i)
outline += await MessageUtils._format_reply_component(i, umo=umo)
continue

# 根据类型处理不同的消息组件
Expand All @@ -105,7 +110,7 @@ async def outline_message_list(message_list: List[BaseMessageComponent]) -> str:
continue
image = image_path

caption = await ImageCaptionUtils.generate_image_caption(image)
caption = await ImageCaptionUtils.generate_image_caption(image, umo=umo)
if caption:
outline += f"[图片: {caption}]"
else:
Expand Down Expand Up @@ -194,9 +199,13 @@ async def outline_message_list(message_list: List[BaseMessageComponent]) -> str:
return outline

@staticmethod
async def _format_reply_component(reply_component: Reply) -> str:
async def _format_reply_component(reply_component: Reply, umo: Optional[str] = None) -> str:
"""
优化格式化引用回复组件

Args:
reply_component: 回复组件
umo: unified_msg_origin,用于 UMO 路由
"""
try:
# 构建发送者信息
Expand All @@ -216,7 +225,7 @@ async def _format_reply_component(reply_component: Reply) -> str:

# 优先使用 chain(原始消息组件)
if hasattr(reply_component, 'chain') and reply_component.chain:
reply_content = await MessageUtils.outline_message_list(reply_component.chain)
reply_content = await MessageUtils.outline_message_list(reply_component.chain, umo=umo)
# 其次使用 message_str(纯文本消息)
elif hasattr(reply_component, 'message_str') and reply_component.message_str:
reply_content = reply_component.message_str
Expand Down
Loading