From 80f74046a89a37b5e532add2042561def1bae60c Mon Sep 17 00:00:00 2001 From: 23q3 <2335125256@qq.com> Date: Fri, 2 Jan 2026 01:24:38 +0800 Subject: [PATCH 1/6] =?UTF-8?q?ci:=20=E6=B7=BB=E5=8A=A0=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=8C=96=E5=8F=91=E5=B8=83=E5=B7=A5=E4=BD=9C=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - prepare-release: 检测 changelog 变更,自动更新版本号并创建 PR - auto-tag: PR 合并后自动创建 Tag 和 GitHub Release - 迁移历史版本日志到 changelogs/ 目录 --- .github/workflows/auto-tag.yml | 110 ++++++++++++ .github/workflows/prepare-release.yml | 235 ++++++++++++++++++++++++++ changelogs/v1.0.0.md | 2 + changelogs/v1.0.1.md | 4 + changelogs/v1.0.2.md | 5 + changelogs/v1.0.3.md | 2 + changelogs/v1.0.4.md | 1 + changelogs/v2.0.0.md | 3 + changelogs/v2.0.1.md | 1 + changelogs/v2.1.0.md | 3 + changelogs/v2.1.1.md | 2 + changelogs/v2.1.2.md | 1 + changelogs/v2.1.3.md | 2 + changelogs/v2.1.4.md | 2 + changelogs/v2.1.5.md | 2 + changelogs/v2.1.6.md | 2 + changelogs/v2.1.7.md | 5 + 17 files changed, 382 insertions(+) create mode 100644 .github/workflows/auto-tag.yml create mode 100644 .github/workflows/prepare-release.yml create mode 100644 changelogs/v1.0.0.md create mode 100644 changelogs/v1.0.1.md create mode 100644 changelogs/v1.0.2.md create mode 100644 changelogs/v1.0.3.md create mode 100644 changelogs/v1.0.4.md create mode 100644 changelogs/v2.0.0.md create mode 100644 changelogs/v2.0.1.md create mode 100644 changelogs/v2.1.0.md create mode 100644 changelogs/v2.1.1.md create mode 100644 changelogs/v2.1.2.md create mode 100644 changelogs/v2.1.3.md create mode 100644 changelogs/v2.1.4.md create mode 100644 changelogs/v2.1.5.md create mode 100644 changelogs/v2.1.6.md create mode 100644 changelogs/v2.1.7.md diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml new file mode 100644 index 0000000..cba2f1b --- /dev/null +++ b/.github/workflows/auto-tag.yml @@ -0,0 +1,110 @@ +name: Auto Tag and Release on Merge + +on: + pull_request: + types: + - closed + branches: + - main + +jobs: + create-tag-and-release: + # 只在 PR 被合并且带有 release 标签时执行 + if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release') + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.merge_commit_sha }} + + - name: Extract version from metadata.yaml + id: version + run: | + # 从 metadata.yaml 读取版本号,移除注释和空白 + VERSION=$(grep '^version:' metadata.yaml | sed 's/version: *//' | sed 's/ *#.*//' | tr -d ' ') + + if [ -z "$VERSION" ]; then + echo "Could not extract version from metadata.yaml" + exit 1 + fi + + # 确保版本号以 v 开头 + if [[ "$VERSION" != v* ]]; then + VERSION="v$VERSION" + fi + + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Extracted version: $VERSION" + + - name: Check if tag already exists + id: check_tag + run: | + VERSION="${{ steps.version.outputs.version }}" + git fetch --tags + + if git tag -l "$VERSION" | grep -q .; then + echo "Tag $VERSION already exists, skipping" + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "Tag $VERSION does not exist, will create" + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + - name: Create and push tag + if: steps.check_tag.outputs.exists == 'false' + run: | + VERSION="${{ steps.version.outputs.version }}" + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git tag -a "$VERSION" -m "Release $VERSION" + git push origin "$VERSION" + + echo "Created and pushed tag $VERSION" + + - name: Get changelog content + if: steps.check_tag.outputs.exists == 'false' + id: changelog + run: | + VERSION="${{ steps.version.outputs.version }}" + CHANGELOG_FILE="changelogs/${VERSION}.md" + + if [ -f "$CHANGELOG_FILE" ]; then + echo "Found changelog file: $CHANGELOG_FILE" + echo "changelog_file=$CHANGELOG_FILE" >> "$GITHUB_OUTPUT" + else + echo "Changelog file not found: $CHANGELOG_FILE" + echo "Auto-generated release for $VERSION" > /tmp/release_body.md + echo "changelog_file=/tmp/release_body.md" >> "$GITHUB_OUTPUT" + fi + + - name: Determine if prerelease + if: steps.check_tag.outputs.exists == 'false' + id: prerelease + run: | + VERSION="${{ steps.version.outputs.version }}" + + if [[ "$VERSION" == *-* ]]; then + echo "is_prerelease=true" >> "$GITHUB_OUTPUT" + echo "Version $VERSION is a prerelease" + else + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + echo "Version $VERSION is a stable release" + fi + + - name: Create GitHub Release + if: steps.check_tag.outputs.exists == 'false' + uses: ncipollo/release-action@v1 + with: + tag: ${{ steps.version.outputs.version }} + name: ${{ steps.version.outputs.version }} + bodyFile: ${{ steps.changelog.outputs.changelog_file }} + generateReleaseNotes: true + prerelease: ${{ steps.prerelease.outputs.is_prerelease }} + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 0000000..83a309b --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,235 @@ +name: Prepare Release + +on: + push: + branches: + - dev + paths: + - 'changelogs/v*.md' + +concurrency: + group: prepare-release + cancel-in-progress: false + +jobs: + prepare-release: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.sha }} + + - name: Find new changelog file + id: find_changelog + run: | + # 获取本次推送中变更的 changelog 文件 + CHANGELOG_FILE=$(git diff --name-only ${{ github.event.before }}..${{ github.sha }} -- 'changelogs/v*.md' | head -1 || true) + + if [ -z "$CHANGELOG_FILE" ]; then + echo "No changelog file found in push range, checking added files..." + CHANGELOG_FILE=$(git diff --name-only --diff-filter=A ${{ github.event.before }}..${{ github.sha }} | grep '^changelogs/v.*\.md$' | head -1 || true) + fi + + if [ -z "$CHANGELOG_FILE" ]; then + echo "No new changelog file found in commit" + exit 1 + fi + + if [ ! -f "$CHANGELOG_FILE" ]; then + echo "Changelog file does not exist: $CHANGELOG_FILE" + exit 1 + fi + + echo "changelog_file=$CHANGELOG_FILE" >> "$GITHUB_OUTPUT" + echo "Found changelog file: $CHANGELOG_FILE" + + - name: Extract version from filename + id: version + run: | + CHANGELOG_FILE="${{ steps.find_changelog.outputs.changelog_file }}" + + # 从文件名提取版本号 (changelogs/v2.1.8.md -> v2.1.8) + VERSION=$(basename "$CHANGELOG_FILE" .md) + VERSION_NO_V="${VERSION#v}" + + # 验证版本格式 + if ! [[ "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then + echo "Invalid version format: $VERSION" + echo "Expected format: vX.X.X or vX.X.X-suffix" + exit 1 + fi + + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "version_no_v=$VERSION_NO_V" >> "$GITHUB_OUTPUT" + echo "Detected version: $VERSION" + + - name: Check if tag already exists + run: | + VERSION="${{ steps.version.outputs.version }}" + git fetch --tags + if git tag -l "$VERSION" | grep -q .; then + echo "Error: Tag $VERSION already exists!" + exit 1 + fi + echo "Tag $VERSION does not exist, proceeding..." + + - name: Get current date + id: date + run: echo "date=$(date +'%Y-%m-%d')" >> "$GITHUB_OUTPUT" + + - name: Read changelog content + id: changelog + run: | + CHANGELOG_FILE="${{ steps.find_changelog.outputs.changelog_file }}" + + # 读取 changelog 内容并保存到临时文件 + cat "$CHANGELOG_FILE" > /tmp/changelog_content.txt + + # 存储到 output (使用 delimiter 处理多行) + { + echo 'content<> "$GITHUB_OUTPUT" + + - name: Update metadata.yaml + run: | + VERSION="${{ steps.version.outputs.version }}" + # 替换版本号,保留可能的注释 + sed -i "s/^version: *v[^ #]*/version: $VERSION/" metadata.yaml + + # 验证更新成功 + if ! grep -q "^version: $VERSION" metadata.yaml; then + echo "Failed to update metadata.yaml" + exit 1 + fi + echo "Updated metadata.yaml to version $VERSION" + + - name: Update main.py + run: | + VERSION_NO_V="${{ steps.version.outputs.version_no_v }}" + # 更新 @register 装饰器中的版本号(支持预发布版本) + sed -i "s/\"[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\(-[a-zA-Z0-9.]*\)\{0,1\}\",/\"$VERSION_NO_V\",/" main.py + + # 验证更新成功 + if ! grep -q "\"$VERSION_NO_V\"," main.py; then + echo "Failed to update main.py" + exit 1 + fi + echo "Updated main.py to version $VERSION_NO_V" + + - name: Update README.md badge + run: | + VERSION="${{ steps.version.outputs.version }}" + # 匹配 version-vX.X.X[-suffix]-blue,保留 -blue 颜色后缀 + sed -i "s/version-v[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\(-[a-zA-Z0-9.]*\)\{0,1\}-blue/version-$VERSION-blue/" README.md + + # 验证更新成功 + if ! grep -q "version-$VERSION" README.md; then + echo "Failed to update README.md badge" + exit 1 + fi + echo "Updated README.md badge to $VERSION" + + - name: Update README.md latest version section + run: | + VERSION="${{ steps.version.outputs.version }}" + DATE="${{ steps.date.outputs.date }}" + + # 创建新的"最新版本"section内容(不带前导空格) + { + echo "## 📋 最新版本" + echo "" + echo "### $VERSION ($DATE)" + echo "" + cat /tmp/changelog_content.txt + echo "" + } > /tmp/new_section.txt + + # 使用 awk 替换 README 中的"最新版本"section + awk ' + BEGIN { skip = 0 } + /^## 📋 最新版本/ { + skip = 1 + while ((getline line < "/tmp/new_section.txt") > 0) print line + close("/tmp/new_section.txt") + next + } + /^## / && skip { skip = 0 } + !skip { print } + ' README.md > README.md.tmp && mv README.md.tmp README.md + + echo "Updated README.md latest version section" + + - name: Commit changes + run: | + VERSION="${{ steps.version.outputs.version }}" + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git add metadata.yaml main.py README.md + + # 检查是否有变更 + if git diff --staged --quiet; then + echo "No changes to commit" + exit 0 + fi + + git commit -m "chore: bump version to $VERSION" + git push origin HEAD:dev + + echo "Committed and pushed changes" + + - name: Create Pull Request + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${{ steps.version.outputs.version }}" + + # 创建 PR body + { + echo "## Release $VERSION" + echo "" + echo "This PR was automatically created by the release workflow." + echo "" + echo "### Changes" + cat /tmp/changelog_content.txt + echo "" + echo "### Files Updated" + echo "- \`metadata.yaml\` - version updated" + echo "- \`main.py\` - version in @register decorator updated" + echo "- \`README.md\` - badge and latest version section updated" + echo "" + echo "---" + echo "*After merging, a tag will be automatically created and a GitHub Release will be published.*" + } > /tmp/pr_body.md + + # 检查是否已存在 dev -> main 的 PR + EXISTING_PR=$(gh pr list --base main --head dev --state open --json number --jq '.[0].number // empty') + + if [ -n "$EXISTING_PR" ]; then + echo "PR #$EXISTING_PR already exists for dev -> main, updating..." + gh pr edit "$EXISTING_PR" \ + --title "Release $VERSION" \ + --body-file /tmp/pr_body.md \ + --add-label "release" + echo "Updated PR #$EXISTING_PR" + exit 0 + fi + + # 创建 PR + gh pr create \ + --base main \ + --head dev \ + --title "Release $VERSION" \ + --body-file /tmp/pr_body.md \ + --label "release" + + echo "Created PR for Release $VERSION" diff --git a/changelogs/v1.0.0.md b/changelogs/v1.0.0.md new file mode 100644 index 0000000..74659ff --- /dev/null +++ b/changelogs/v1.0.0.md @@ -0,0 +1,2 @@ +- 🎉 首次发布 +- ✨ 实现基本的群聊互动功能 diff --git a/changelogs/v1.0.1.md b/changelogs/v1.0.1.md new file mode 100644 index 0000000..9fdcbf0 --- /dev/null +++ b/changelogs/v1.0.1.md @@ -0,0 +1,4 @@ +- 🔍 增加了读空气功能 +- 🔍 增加了函数工具开关配置 +- 🔄 更换了request_llm方法调用大模型,提高兼容性 +- 🛠️ 优化部分代码 diff --git a/changelogs/v1.0.2.md b/changelogs/v1.0.2.md new file mode 100644 index 0000000..6d912a4 --- /dev/null +++ b/changelogs/v1.0.2.md @@ -0,0 +1,5 @@ +- 🔒 添加了群组锁机制,防止并发调用大模型 +- 🛠️ 优化了消息处理存储流程,极大提高了性能 +- 🔍 添加了清除聊天记录的指令 +- 🔍 添加了检测指令关键词不回复功能 +- 📝 改进了代码结构 diff --git a/changelogs/v1.0.3.md b/changelogs/v1.0.3.md new file mode 100644 index 0000000..6ecb51f --- /dev/null +++ b/changelogs/v1.0.3.md @@ -0,0 +1,2 @@ +- 🐛 在处理大模型回复时增加了对角色的判断,避免调用函数工具时出错 [#15](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/15) +- 🐛 在提示词增加了bot的昵称和qq号,避免大模型不知道聊天记录中哪个是自己 [#14](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/14) diff --git a/changelogs/v1.0.4.md b/changelogs/v1.0.4.md new file mode 100644 index 0000000..dde624d --- /dev/null +++ b/changelogs/v1.0.4.md @@ -0,0 +1 @@ +- 🐛 修正处理大模型回复时的条件判断逻辑 [#15](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/15) diff --git a/changelogs/v2.0.0.md b/changelogs/v2.0.0.md new file mode 100644 index 0000000..9f3b0e3 --- /dev/null +++ b/changelogs/v2.0.0.md @@ -0,0 +1,3 @@ +- 🏗️ 完全重构 抛弃使用协议端API获取聊天记录的方式,改为基于Astrbot本身,支持了更多消息平台 [#21](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/21) [#4](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/4) +- 🔄 架构改进 采用高度模块化设计,每个功能封装在独立工具类中 +- 📸 图片转述 支持图片转述功能 [#16](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/16) diff --git a/changelogs/v2.0.1.md b/changelogs/v2.0.1.md new file mode 100644 index 0000000..54ac812 --- /dev/null +++ b/changelogs/v2.0.1.md @@ -0,0 +1 @@ +- 🐛 **修复Docker部署问题** - 改进路径处理方式,修复在Docker环境下无法保存/读取消息历史的问题 diff --git a/changelogs/v2.1.0.md b/changelogs/v2.1.0.md new file mode 100644 index 0000000..98dd47d --- /dev/null +++ b/changelogs/v2.1.0.md @@ -0,0 +1,3 @@ +- 🔄 **数据存储格式优化** - 使用jsonpickle库替换pickle,提高数据可读性和跨平台兼容性 +- 🐛 **修复Docker环境兼容性** - 采用JSON序列化格式,彻底解决Docker环境下消息历史存取问题 [#31](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/31) +- 🐛 **修复私聊时无法正确保存bot消息的问题** - 修复了在私聊场景下bot发送的消息无法被正确保存到历史记录的问题,确保私聊对话的完整性 diff --git a/changelogs/v2.1.1.md b/changelogs/v2.1.1.md new file mode 100644 index 0000000..5735358 --- /dev/null +++ b/changelogs/v2.1.1.md @@ -0,0 +1,2 @@ +- ✨ **新增黑名单关键词功能** - 添加黑名单关键词配置,可以设置不触发回复的关键词 +- 🐛 **修复重置历史记录问题** - 修复重置聊天记录后提示消息被错误保存到历史记录的问题 [#41](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/41) diff --git a/changelogs/v2.1.2.md b/changelogs/v2.1.2.md new file mode 100644 index 0000000..5b77b61 --- /dev/null +++ b/changelogs/v2.1.2.md @@ -0,0 +1 @@ +- 🐛 **修复Reply消息处理错误** - 修复在处理包含回复消息的历史记录时出现'Reply' object has no attribute 'sender_str'错误的问题 [#46](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/46) diff --git a/changelogs/v2.1.3.md b/changelogs/v2.1.3.md new file mode 100644 index 0000000..a231e76 --- /dev/null +++ b/changelogs/v2.1.3.md @@ -0,0 +1,2 @@ +- ✨ **新增图片持久化存储功能** - 添加图片本地存储和自动清理机制,解决聊天记录中图片链接过期问题。新增 `enable_image_persistence` 和 `image_retention_days` 配置选项 [#52](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/52) +- 🐛 **修复读空气功能干扰命令执行的问题** - 将读空气处理逻辑从on_llm_response移至on_decorating_result阶段,避免在大模型回复后立即停止事件传播导致命令逻辑被中断 [#33](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/33) diff --git a/changelogs/v2.1.4.md b/changelogs/v2.1.4.md new file mode 100644 index 0000000..fc11984 --- /dev/null +++ b/changelogs/v2.1.4.md @@ -0,0 +1,2 @@ +- ✨ **为LLM回复添加限制条件** - 在结尾提示词中添加对于回复的限制,避免LLM使用[At:id(昵称)]这样的格式 [#54](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/54)@Fossssss +- 🐛 **修复与其他插件的兼容性问题** - 移除AiocqhttpMessageEvent类型断言,使用安全检查和异常处理机制,解决其他插件构造输入时的报错问题 [#57](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/57) diff --git a/changelogs/v2.1.5.md b/changelogs/v2.1.5.md new file mode 100644 index 0000000..a12d3fd --- /dev/null +++ b/changelogs/v2.1.5.md @@ -0,0 +1,2 @@ +- ✨ **新增临时禁言功能** - 添加闭嘴/说话指令,支持临时禁用自动回复功能,默认5分钟,可自定义时长 [#63](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/63) +- 🔒 **添加权限控制** - 对管理指令添加了管理员权限限制 diff --git a/changelogs/v2.1.6.md b/changelogs/v2.1.6.md new file mode 100644 index 0000000..674dd00 --- /dev/null +++ b/changelogs/v2.1.6.md @@ -0,0 +1,2 @@ +- 🐛 **修复空消息异常** - 修复napcat发送私聊"正在输入"状态时,导致插件异常的问题 [#70](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/70) +- 🐛 **修复代码问题并改进错误处理** - 修复装饰器参数位置错误、历史记录加载失败崩溃问题,改进路径处理和日志记录 diff --git a/changelogs/v2.1.7.md b/changelogs/v2.1.7.md new file mode 100644 index 0000000..f991b85 --- /dev/null +++ b/changelogs/v2.1.7.md @@ -0,0 +1,5 @@ +- 🐛 **修复AstrBot兼容性问题** - 移除对不存在的 `Anonymous` 等消息组件类的依赖,使用类型字符串检查替代类实例检查,兼容 AstrBot 新版本 +- ⚡ **优化私聊回复机制** - 将私聊回复概率固定为1,确保历史消息格式统一 +- ⚡ **优化引用消息显示** - 改进 Reply 组件处理,提供更完整的发送者信息和内容 +- ⚡ **优化@消息处理** - 重构 'at' 和 'atall' 组件处理逻辑,提高代码清晰度和功能性 +- [#72](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/72) [#73](https://github.com/23q3/astrbot_plugin_SpectreCore/pull/73) @Hola-Gracias From 9fe59b9418659d8e077877aa868f60091da5a00c Mon Sep 17 00:00:00 2001 From: 23q3 <2335125256@qq.com> Date: Fri, 2 Jan 2026 02:39:24 +0800 Subject: [PATCH 2/6] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=20LLM=20?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E7=9A=84=20prompt=20=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将环境信息从 prompt 移至 system_prompt,保持 prompt 干净供 KB 检索 - 基于 message_id 精确排除当前消息,避免历史记录重复 Thanks to @NineSober for the feedback (#76) --- utils/llm_utils.py | 161 ++++++++++++++++++++++----------------------- 1 file changed, 78 insertions(+), 83 deletions(-) diff --git a/utils/llm_utils.py b/utils/llm_utils.py index 69aae53..baac07a 100644 --- a/utils/llm_utils.py +++ b/utils/llm_utils.py @@ -100,85 +100,30 @@ def get_last_call_time(platform_name: str, is_private_chat: bool, chat_id: str) async def call_llm(event: AstrMessageEvent, config: AstrBotConfig, context: Context) -> ProviderRequest: """ 构建调用大模型的请求对象 - + Args: event: 消息对象 config: 配置对象 context: Context 对象,用于获取LLM工具管理器 - + Returns: ProviderRequest 对象 """ platform_name = event.get_platform_name() is_private = event.is_private_chat() chat_id = event.get_group_id() if not is_private else event.get_sender_id() - - # 构建基础Prompt - # 对于aiocqhttp平台 通过调用协议端api获取bot用户名 - if platform_name == "aiocqhttp" and hasattr(event, "bot"): - # 我们不再需要断言,因为 hasattr 已经做了安全的检查 - client = event.bot - try: - bot_name = (await client.api.get_login_info())["nickname"] - prompt = f"你正在浏览聊天软件,你在聊天软件上的id是{event.get_self_id()},用户名是{bot_name},你正在" - except Exception as e: - logger.warning(f"通过 event.bot 获取机器人昵称失败: {e},将使用通用提示词。") - prompt = f"你正在浏览聊天软件,你在聊天软件上的id是{event.get_self_id()},你正在" - else: - prompt = f"你正在浏览聊天软件,你在聊天软件上的id是{event.get_self_id()},你正在" - - if is_private: - sender_display_name = event.get_sender_name() if event.get_sender_name() else f"ID为 {event.get_sender_id()} 的人" - prompt += f"和 {sender_display_name} 私聊页面中。" - else: - group_display_name = chat_id - # 尝试获取更详细的群名 - if platform_name in ["aiocqhttp", "gewechat"]: - try: - group = await event.get_group() - if group and group.group_name: - group_display_name = f"{group.group_name}({chat_id})" - except Exception as e: - logger.warning(f"为 {platform_name} 获取群组信息失败: {e}") - - prompt += f"群聊 {group_display_name} 中。" - - # 添加历史记录 - try: - history_limit = config.get("group_msg_history", 10) # 从配置读取历史记录数量 - history_messages = HistoryStorage.get_history(platform_name, is_private, chat_id) - if history_messages: - formatted_history = (await MessageUtils.format_history_for_llm(history_messages, max_messages=history_limit)) - prompt += "\n\n以下是最近的聊天记录:\n" + formatted_history - else: - prompt += "\n\n你没看见任何聊天记录,看来最近没有消息。" - except Exception as e: - logger.error(f"获取或格式化历史记录失败: {e}") - # 发生错误时使用空历史记录继续 - prompt += "\n\n你没看见任何聊天记录,看来最近没有消息。" - # 结尾提示词 - prompt += "\n(在聊天记录中,你的用户名以AstrBot被代替了)" - prompt += "\n(如果你想回复某人,不要使用类似 [At:id(昵称)]这样的格式)" - # 判断是否开启读空气 - if config.get("read_air", False): - prompt += "\n\n你的反应是:\n(如果你想发送一条消息,直接输出发送的内容,如果你选择忽略,直接输出)" - else: - prompt += "\n\n你决定发送一条消息(你输出的内容将作为消息发送)" - # 准备并调用大模型 func_tools_mgr = context.get_llm_tool_manager() if config.get("use_func_tool", False) else None - + # 获取配置中指定的人格 system_prompt = "" contexts = [] persona_name = config.get("persona", "") - + 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'): @@ -188,58 +133,108 @@ async def call_llm(event: AstrMessageEvent, config: AstrBotConfig, context: Cont begin_dialogs = persona.get('_begin_dialogs_processed', []) if begin_dialogs: contexts.extend(begin_dialogs) - + logger.debug(f"找到人格 '{persona_name}' ") else: logger.warning(f"未找到名为 '{persona_name}' 的人格") except Exception as e: logger.error(f"获取人格信息失败: {e}") + # 构建环境描述(注入到 system_prompt,不污染 prompt) + env_description = f"\n\n你正在浏览聊天软件,你在聊天软件上的id是{event.get_self_id()}" + + # 对于aiocqhttp平台,尝试获取bot用户名 + if platform_name == "aiocqhttp" and hasattr(event, "bot"): + try: + bot_name = (await event.bot.api.get_login_info())["nickname"] + env_description += f",用户名是{bot_name}" + except Exception as e: + logger.warning(f"通过 event.bot 获取机器人昵称失败: {e}") + + if is_private: + sender_display_name = event.get_sender_name() if event.get_sender_name() else f"ID为 {event.get_sender_id()} 的人" + env_description += f",你正在和 {sender_display_name} 私聊页面中。" + else: + group_display_name = chat_id + if platform_name in ["aiocqhttp", "gewechat"]: + try: + group = await event.get_group() + if group and group.group_name: + group_display_name = f"{group.group_name}({chat_id})" + except Exception as e: + logger.warning(f"为 {platform_name} 获取群组信息失败: {e}") + env_description += f",你正在群聊 {group_display_name} 中。" + + # 添加历史记录(文本格式,注入到 system_prompt) + # 注意:基于 message_id 精确排除当前消息,避免重复 + history_limit = config.get("group_msg_history", 10) + history_messages = HistoryStorage.get_history(platform_name, is_private, chat_id) + + try: + if history_messages: + # 获取当前消息的 message_id 用于精确排除 + current_msg_id = getattr(event.message_obj, 'message_id', None) if hasattr(event, 'message_obj') else None + if current_msg_id: + history_for_context = [m for m in history_messages if getattr(m, 'message_id', None) != current_msg_id] + else: + # 回退到排除最后一条 + 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) + env_description += "\n\n以下是最近的聊天记录:\n" + formatted_history + else: + env_description += "\n\n你没看见任何聊天记录,看来最近没有消息。" + else: + env_description += "\n\n你没看见任何聊天记录,看来最近没有消息。" + except Exception as e: + logger.error(f"获取或格式化历史记录失败: {e}") + env_description += "\n\n你没看见任何聊天记录,看来最近没有消息。" + + # 行为指引 + env_description += "\n(在聊天记录中,你的用户名以AstrBot被代替了)" + env_description += "\n(如果你想回复某人,不要使用类似 [At:id(昵称)]这样的格式)" + + if config.get("read_air", False): + env_description += "\n\n现在你收到了一条新消息,你的反应是:\n(如果你想发送一条消息,直接输出发送的内容,如果你选择忽略,直接输出)" + else: + env_description += "\n\n现在你收到了一条新消息,你决定发送一条消息回复(你输出的内容将作为消息发送)" + + # 将环境描述追加到 system_prompt + system_prompt += env_description + # 图片相关处理 image_urls = [] if image_count := config.get("image_processing", {}).get("image_count", 0): - # 只从会发送给大模型的历史消息中提取图片(受history_limit限制) if history_messages: - # 先计算将给大模型的消息范围 messages_to_show = history_messages[-history_limit:] if len(history_messages) > history_limit else history_messages - - # 按时间从新到旧遍历消息 + for message in reversed(messages_to_show): - # 检查消息是否包含图片 if hasattr(message, "message") and message.message: for component in message.message: - # 判断是否为图片组件 if isinstance(component, Image): try: - # 只使用本地路径格式 if component.file: - image_url = component.file - else: - continue # 跳过其他格式 - - image_urls.append(image_url) - - # 如果已收集足够数量的图片,停止收集 - if len(image_urls) >= image_count: - break + image_urls.append(component.file) + if len(image_urls) >= image_count: + break except Exception as e: logger.warning(f"处理图片URL时出错: {e}") continue - - # 如果已收集足够数量的图片,停止遍历消息 if len(image_urls) >= image_count: break - - # 如果收集到了图片,添加提示词 + if image_urls: - prompt += f"\n\n已经按照从晚到早的顺序为你提供了聊天记录中的{len(image_urls)}张图片,你可以直接查看并理解它们。这些图片出现在聊天记录中。" + system_prompt += f"\n\n已经按照从晚到早的顺序为你提供了聊天记录中的{len(image_urls)}张图片,你可以直接查看并理解它们。这些图片出现在聊天记录中。" + + # prompt 只保留用户当前消息,保持干净供 KB 检索 + prompt = event.get_message_outline() return event.request_llm( prompt=prompt, func_tool_manager=func_tools_mgr, contexts=contexts, - system_prompt=system_prompt, - image_urls=image_urls, + system_prompt=system_prompt, + image_urls=image_urls, ) @staticmethod From 2dd219f876313f5a14c60278eaa968ecff392acd Mon Sep 17 00:00:00 2001 From: 23q3 <2335125256@qq.com> Date: Fri, 2 Jan 2026 21:19:21 +0800 Subject: [PATCH 3/6] =?UTF-8?q?fix:=20QQ=E5=AE=98=E6=96=B9=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0help=E5=91=BD=E4=BB=A4=E5=8E=BB=E9=99=A4URL=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E8=A2=AB=E6=8B=A6=E6=88=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 检测平台为qq_official或qq_official_webhook时不输出GitHub链接 - 保留文档阅读指引文字 Closes #78 --- main.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index 16e92c2..64b0f1a 100644 --- a/main.py +++ b/main.py @@ -120,17 +120,21 @@ def spectrecore(self): @spectrecore.command("help", alias=['帮助', 'helpme']) async def help(self, event: AstrMessageEvent): """查看插件的帮助喵""" - yield event.plain_result( + help_text = ( "SpectreCore插件帮助文档\n" "使用spectrecore或sc作为指令前缀 如/sc help\n" "使用reset指令重置当前聊天记录 如/sc reset\n" " 你也可以重置指定群聊天记录 如/sc reset 群号\n" "使用history指令可以查看最近聊天记录 如/sc history\n" "使用mute/闭嘴指令临时禁用自动回复 如/sc mute 5 或 /sc 闭嘴 10\n" - "使用unmute/说话指令解除禁用 如/sc unmute 或 /sc 说话\n" - "↓强烈建议您阅读Github中的README文档\n↓" - "https://github.com/23q3/astrbot_plugin_SpectreCore" + "使用unmute/说话指令解除禁用 如/sc unmute 或 /sc 说话" ) + platform_name = event.get_platform_name() + if platform_name in ("qq_official", "qq_official_webhook"): + help_text += "\n强烈建议前往Github阅读README文档" + else: + help_text += "\n↓强烈建议您阅读Github中的README文档↓\nhttps://github.com/23q3/astrbot_plugin_SpectreCore" + yield event.plain_result(help_text) @filter.permission_type(filter.PermissionType.ADMIN) @spectrecore.command("history") async def history(self, event: AstrMessageEvent, count: int = 10): From afbe440303f529963079a130feeadf451063cbff Mon Sep 17 00:00:00 2001 From: 23q3 <2335125256@qq.com> Date: Sat, 3 Jan 2026 01:27:34 +0800 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20=E9=80=82=E9=85=8DAstrBot=E6=96=B0?= =?UTF-8?q?=E7=89=88=E6=9C=ACAPI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除对已删除属性的依赖(platform_name, private_id) - 显式传递platform_name参数 - 添加None/属性检查增强健壮性 - 使用getattr处理适配器特定属性 - 迁移至persona_manager.selected_default_persona_v3 - 图片URL提取兼容url和file属性 --- main.py | 2 +- utils/history_storage.py | 30 +++++++++++------------------- utils/image_caption.py | 30 ++++++++++++++++++------------ utils/llm_utils.py | 8 +++++--- utils/persona_utils.py | 3 ++- 5 files changed, 37 insertions(+), 36 deletions(-) diff --git a/main.py b/main.py index 64b0f1a..9f4b7e5 100644 --- a/main.py +++ b/main.py @@ -189,7 +189,7 @@ async def history(self, event: AstrMessageEvent, count: int = 10): yield event.plain_result(f"获取历史记录失败喵:{str(e)}") @filter.permission_type(filter.PermissionType.ADMIN) @spectrecore.command("reset") - async def reset(self, event: AstrMessageEvent, group_id: str = None): + async def reset(self, event: AstrMessageEvent, group_id: str | None = None): """重置历史记录喵,不带参数重置当前聊天记录,带群号则重置指定群聊记录 如/sc reset 123456""" try: # 获取平台名称 diff --git a/utils/history_storage.py b/utils/history_storage.py index 001e984..3b3f544 100644 --- a/utils/history_storage.py +++ b/utils/history_storage.py @@ -75,26 +75,23 @@ def _sanitize_message(message: AstrBotMessage) -> AstrBotMessage: return sanitized_message @staticmethod - async def save_message(message: AstrBotMessage) -> bool: + async def save_message(message: AstrBotMessage, platform_name: str) -> bool: """ 保存消息到历史记录 - + Args: message: AstrBot消息对象 - + platform_name: 平台名称 + Returns: 是否保存成功 """ try: # 判断是群聊还是私聊 is_private_chat = not bool(message.group_id) - platform_name = message.platform_name if hasattr(message, "platform_name") else "unknown" if is_private_chat: - if hasattr(message, "private_id") and message.private_id: - chat_id = message.private_id - else: - chat_id = message.sender.user_id + chat_id = message.sender.user_id else: chat_id = message.group_id @@ -180,10 +177,9 @@ async def process_and_save_user_message(event: AstrMessageEvent) -> None: # 创建消息对象 message_obj = event.message_obj - message_obj.platform_name = event.get_platform_name() - + # 保存消息 - success = await HistoryStorage.save_message(message_obj) + success = await HistoryStorage.save_message(message_obj, event.get_platform_name()) chat_type = "私聊" if event.is_private_chat() else "群聊" if success: @@ -205,10 +201,9 @@ def create_bot_message(chain: List[BaseMessageComponent], event: AstrMessageEven """ # 创建消息对象 msg = AstrBotMessage() - + # 设置基本属性 msg.message = chain - msg.platform_name = event.get_platform_name() msg.timestamp = int(time.time()) # 设置消息类型和会话信息 @@ -217,12 +212,9 @@ def create_bot_message(chain: List[BaseMessageComponent], event: AstrMessageEven if not is_private: msg.group_id = event.get_group_id() - # 设置发送者信息 + # 设置发送者信息 msg.sender = MessageMember(user_id=event.get_self_id(), nickname="AstrBot") - # 设置对方的id - msg.private_id = event.get_sender_id() - # 生成纯文本消息 msg.message_str = "" for comp in chain: @@ -256,9 +248,9 @@ async def save_bot_message_from_chain(chain: List[BaseMessageComponent], event: # 创建机器人消息对象 bot_msg = HistoryStorage.create_bot_message(chain, event) - + # 保存消息 - return await HistoryStorage.save_message(bot_msg) + return await HistoryStorage.save_message(bot_msg, event.get_platform_name()) except Exception as e: logger.error(f"保存机器人消息失败: {e}") return False diff --git a/utils/image_caption.py b/utils/image_caption.py index 4095a2a..e2dd816 100644 --- a/utils/image_caption.py +++ b/utils/image_caption.py @@ -5,15 +5,15 @@ class ImageCaptionUtils: """ 图片转述工具类 - + 用于调用大语言模型将图片转述为文本描述 """ - + # 保存context和config对象的静态变量 - context = None - config = None + context: Optional[Context] = None + config: Optional[AstrBotConfig] = None # 图片描述缓存 - caption_cache = {} + caption_cache: dict[str, str] = {} @staticmethod def init(context: Context, config: AstrBotConfig): @@ -44,6 +44,11 @@ async def generate_image_caption( # 获取配置 config = ImageCaptionUtils.config context = ImageCaptionUtils.context + + if not config or not context: + logger.warning("ImageCaptionUtils 未初始化") + return None + # 检查是否已启用图片转述 image_processing_config = config.get("image_processing", {}) if not image_processing_config.get("use_image_caption", False): @@ -55,20 +60,21 @@ async def generate_image_caption( provider = context.get_using_provider() else: provider = context.get_provider_by_id(provider_id) - - if not provider: + + if not provider or not hasattr(provider, "text_chat"): logger.warning(f"无法找到提供商: {provider_id if provider_id else '默认'}") return None + text_chat = getattr(provider, "text_chat") try: # 带超时控制的调用大模型进行图片转述 async def call_llm(): - return await provider.text_chat( + return await text_chat( prompt=image_processing_config.get("image_caption_prompt", "请直接简短描述这张图片"), - contexts=[], - image_urls=[image], # 图片链接,支持路径和网络链接 - func_tool=None, # 当前用户启用的函数调用工具。如果不需要,可以不传 - system_prompt="" # 系统提示,可以不传 + contexts=[], + image_urls=[image], + func_tool=None, + system_prompt="" ) # 使用asyncio.wait_for添加超时控制 diff --git a/utils/llm_utils.py b/utils/llm_utils.py index baac07a..c10e119 100644 --- a/utils/llm_utils.py +++ b/utils/llm_utils.py @@ -146,7 +146,8 @@ async def call_llm(event: AstrMessageEvent, config: AstrBotConfig, context: Cont # 对于aiocqhttp平台,尝试获取bot用户名 if platform_name == "aiocqhttp" and hasattr(event, "bot"): try: - bot_name = (await event.bot.api.get_login_info())["nickname"] + bot = getattr(event, "bot") + bot_name = (await bot.api.get_login_info())["nickname"] env_description += f",用户名是{bot_name}" except Exception as e: logger.warning(f"通过 event.bot 获取机器人昵称失败: {e}") @@ -213,8 +214,9 @@ async def call_llm(event: AstrMessageEvent, config: AstrBotConfig, context: Cont for component in message.message: if isinstance(component, Image): try: - if component.file: - image_urls.append(component.file) + url = component.url or component.file + if url: + image_urls.append(url) if len(image_urls) >= image_count: break except Exception as e: diff --git a/utils/persona_utils.py b/utils/persona_utils.py index a4fb778..bbc548d 100644 --- a/utils/persona_utils.py +++ b/utils/persona_utils.py @@ -37,7 +37,8 @@ def get_default_persona(context: Context) -> Optional[str]: 默认人格的ID,如果获取失败则返回None """ try: - return context.provider_manager.selected_default_persona["name"] + persona = context.persona_manager.selected_default_persona_v3 + return persona["name"] if persona else None except Exception as e: logger.error(f"获取默认人格失败: {e}") return None From d979272321cf61d330a8ae13bc04667e35f6982e Mon Sep 17 00:00:00 2001 From: 23q3 <2335125256@qq.com> Date: Sat, 3 Jan 2026 02:28:01 +0800 Subject: [PATCH 5/6] chore: add v2.1.8 changelog --- changelogs/v2.1.8.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelogs/v2.1.8.md diff --git a/changelogs/v2.1.8.md b/changelogs/v2.1.8.md new file mode 100644 index 0000000..707709e --- /dev/null +++ b/changelogs/v2.1.8.md @@ -0,0 +1,3 @@ +- ⚡ **优化LLM调用的prompt结构** - 将环境信息移至system_prompt,保持prompt干净以支持知识库检索;基于message_id精确排除当前消息避免历史记录重复 [#76](https://github.com/23q3/astrbot_plugin_SpectreCore/pull/76) +- 🐛 **修复QQ官方平台help命令** - 检测到QQ官方平台时不输出GitHub链接,避免消息被拦截 [#78](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/78) +- 🐛 **适配AstrBot新版本API** - 移除对已删除属性的依赖,迁移至新版人格管理接口,增强图片URL提取兼容性 From 6c4a2b98d1728919423a9c8cf24ff7dd62c1a2ce Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 2 Jan 2026 18:28:12 +0000 Subject: [PATCH 6/6] chore: bump version to v2.1.8 --- README.md | 14 +++++--------- main.py | 2 +- metadata.yaml | 2 +- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 892f12f..82c0cd7 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ![SpectreCore](https://avatars.githubusercontent.com/u/129108081?s=48&v=4) -[![version](https://img.shields.io/badge/version-v2.1.7-blue.svg?style=flat-square)](https://github.com/23q3/astrbot_plugin_SpectreCore) +[![version](https://img.shields.io/badge/version-v2.1.8-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) @@ -70,15 +70,11 @@ SpectreCore (影芯) 是一个为 AstrBot 设计的高级群聊互动插件, ## 📋 最新版本 -### v2.1.7 (2025-11-17) +### v2.1.8 (2026-01-02) -- 🐛 **修复AstrBot兼容性问题** - 移除对不存在的 `Anonymous` 等消息组件类的依赖,使用类型字符串检查替代类实例检查,兼容 AstrBot 新版本 -- ⚡ **优化私聊回复机制** - 将私聊回复概率固定为1,确保历史消息格式统一 -- ⚡ **优化引用消息显示** - 改进 Reply 组件处理,提供更完整的发送者信息和内容 -- ⚡ **优化@消息处理** - 重构 'at' 和 'atall' 组件处理逻辑,提高代码清晰度和功能性 -- [#72](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/72) [#73](https://github.com/23q3/astrbot_plugin_SpectreCore/pull/73) @Hola-Gracias - -查看完整的[更新日志](./CHANGELOG.md),了解项目的版本历史和功能变化。 +- ⚡ **优化LLM调用的prompt结构** - 将环境信息移至system_prompt,保持prompt干净以支持知识库检索;基于message_id精确排除当前消息避免历史记录重复 [#76](https://github.com/23q3/astrbot_plugin_SpectreCore/pull/76) +- 🐛 **修复QQ官方平台help命令** - 检测到QQ官方平台时不输出GitHub链接,避免消息被拦截 [#78](https://github.com/23q3/astrbot_plugin_SpectreCore/issues/78) +- 🐛 **适配AstrBot新版本API** - 移除对已删除属性的依赖,迁移至新版人格管理接口,增强图片URL提取兼容性 ## ⚠️ 注意事项 diff --git a/main.py b/main.py index 9f4b7e5..2b07871 100644 --- a/main.py +++ b/main.py @@ -7,7 +7,7 @@ "spectrecore", "23q3", "使大模型更好的主动回复群聊中的消息,带来生动和沉浸的群聊对话体验", - "2.1.7", + "2.1.8", "https://github.com/23q3/astrbot_plugin_SpectreCore" ) class SpectreCore(Star): diff --git a/metadata.yaml b/metadata.yaml index 1e6fd94..ef80905 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -1,7 +1,7 @@ name: spectrecore # 这是你的插件的唯一识别名。 desc: 使大模型更好的主动回复群聊中的消息,带来生动和沉浸的群聊对话体验 # 插件简短描述 help: 自动检测群聊消息并让AI模型进行回复,让群聊更加生动有趣。 # 插件的帮助信息 -version: v2.1.7 # 插件版本号。格式:v1.1.1 或者 v1.1 +version: v2.1.8 # 插件版本号。格式:v1.1.1 或者 v1.1 author: 23q3 # 作者 repo: https://github.com/23q3/astrbot_plugin_SpectreCore # 插件的仓库地址 display_name: 🌟 SpectreCore