From d93b1f938df0c349a4fe596d6ed2bdd4b39d0ccd Mon Sep 17 00:00:00 2001 From: repo-visualizer Date: Sun, 10 Aug 2025 01:41:11 +0000 Subject: [PATCH 1/7] Repo visualizer: update diagram --- diagram.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diagram.svg b/diagram.svg index 8bbd24a..26a1659 100644 --- a/diagram.svg +++ b/diagram.svg @@ -1 +1 @@ -utilsutilsteststestsstaticsstaticsmodulesmodulescorecoretext2imgtext2imgbf1bf1UIUImd2imgmd2imgtarottarotcrawlcrawlEmoticonsEmoticonsself_containedself_containedrequiredrequiredormormmodelsmodelsstaticstaticdrawdrawdatabasedatabaseblazeblazeIOSIOSstaticstaticreversereversenormalnormalminicraft_infominicraft_infoillillemoji_mixemoji_mixbf1_infobf1_infoai_chatai_chatstatusstatushelperhelperjsjscsscsstemplatetemplatejsjscsscssprovidersproviderspluginspluginscorecore.bat.css.gitignore.html.js.json.md.py.sh.svg.toml.txt.yamleach dot sized by file size \ No newline at end of file +utilsutilsteststestsstaticsstaticsmodulesmodulescorecoretext2imgtext2imgbf1bf1UIUImd2imgmd2imgtarottarotcrawlcrawlEmoticonsEmoticonsself_containedself_containedrequiredrequiredormormmodelsmodelsstaticstaticdrawdrawdatabasedatabaseblazeblazeIOSIOSstaticstaticreversereversenormalnormalminicraft_infominicraft_infoillillemoji_mixemoji_mixbf1_infobf1_infoai_chatai_chatstatusstatushelperhelperjsjscsscsstemplatetemplatejsjscsscssprovidersproviderspluginspluginscorecore.bat.css.gitignore.html.js.json.md.py.sh.svg.toml.txt.yamleach dot sized by file size \ No newline at end of file From 994ce72deaeb58ffb7d60cc95cd6c8682f6eacd6 Mon Sep 17 00:00:00 2001 From: Umaru Date: Sat, 23 Aug 2025 18:25:08 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0CLAUDE.md?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=BB=A5=E6=8F=90=E4=BE=9B=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E6=8C=87=E5=AF=BC=E5=92=8C=E5=BC=80=E5=8F=91=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 +++ CLAUDE.md | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 CLAUDE.md diff --git a/.gitignore b/.gitignore index 22c92c3..5a4a2ee 100644 --- a/.gitignore +++ b/.gitignore @@ -203,3 +203,8 @@ alembic.ini #文件修改记录 .history/ + +# Claude Code 本地配置 +.claude/settings.local.json +.claude/*.local.* +.claude/local/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d0e7031 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,111 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a QQ bot built on the Graia Ariadne framework, named "xiaomai-bot" (小麦机器人). It's a comprehensive chatbot with AI integration, gaming features (especially Battlefield 1), image processing, entertainment functions, and management capabilities. + +## Development Commands + +### Core Commands +- **Run the bot**: `uv run main.py` +- **Install dependencies**: `uv sync` (using uv package manager) +- **Linting**: `ruff check` (configured in pyproject.toml) +- **Formatting**: `ruff format` (configured in pyproject.toml) +- **Tests**: `pytest` (test files in tests/ directory) + +### Convenience Scripts +- **Windows**: `run.bat` - Quick start script for Windows +- **Linux**: `run.sh` - Quick start script for Linux + +### Version Management +- **Bump version**: Uses `bump-my-version` tool (configured in pyproject.toml) +- **Changelog generation**: Uses `git-cliff` (configured in pyproject.toml) + +## Architecture Overview + +### Core Components +- **main.py**: Application entry point with message listeners and bot initialization +- **core/**: Contains the bot's core functionality + - **bot.py**: Main bot class (Umaru) with module loading and initialization + - **config.py**: Global configuration access interface + - **control.py**: Permission, frequency, and feature control components + - **orm/**: Database ORM layer with SQLAlchemy + Alembic migrations + - **models/**: Control component models (frequency, response, saya) + +### Module System +The bot uses a plugin-based architecture organized into: +- **modules/required/**: Essential plugins (auto_upgrade, saya_manager, perm_manager, helper, status, etc.) +- **modules/self_contained/**: Built-in feature plugins (AI chat, BF1 features, image processing, entertainment) +- **modules/third_party/**: External plugins + +Each module has a `metadata.json` file defining its configuration, permissions, and usage. + +### Database Architecture +- Uses SQLAlchemy ORM with async support (AsyncORM) +- SQLite database by default (`data.db`) +- Alembic for database migrations +- Tables defined in `core/orm/tables.py` + +### Key Features +- **AI Chat**: Multi-provider support (OpenAI, DeepSeek) with plugin system for tools +- **Battlefield 1 Integration**: Complete server management and player statistics +- **Image Processing**: Search, generation, meme creation using Playwright +- **Permission System**: Granular user and group permissions +- **Multi-Account Support**: Supports multiple bot accounts with response management + +## Configuration + +### Primary Config +- **config.yaml**: Main configuration file (copy from config_demo.yaml) +- Environment variables supported for Docker deployment +- Critical settings: bot_accounts, mirai_host, verify_key, Master (admin user) + +### Prerequisites +- Python 3.10-3.12 +- Mirai Console with Mirai API HTTP plugin +- UV package manager (recommended) + +## Important Patterns + +### Module Development +- Each module should have proper metadata.json configuration +- Use the control system for permissions and rate limiting +- Follow the existing patterns for message handling and command parsing + +### Database Operations +- Use the AsyncORM from `core.orm` for database operations +- All database models should inherit from Base in `core.orm` +- Use Alembic migrations for schema changes + +### Error Handling +- Comprehensive logging with loguru +- Error logs stored in `log/` directory organized by date +- Exception catcher module handles unhandled exceptions + +## Testing + +- Test files located in `tests/` directory +- Uses pytest with async support +- Includes specialized tests for components like md2img, Minecraft integration + +## Deployment + +Supports multiple deployment methods: +- Direct Python execution +- Docker containers +- Docker Compose +- All methods support environment variable configuration + +## Dependencies + +Major dependencies include: +- graia-ariadne: Core bot framework +- SQLAlchemy + Alembic: Database ORM and migrations +- FastAPI: Web interface +- Playwright: Browser automation for image generation +- httpx: HTTP client +- Various AI providers (openai, etc.) + +The project uses UV for dependency management with lock file support. \ No newline at end of file From 28942a712a8249d22eb2db0a4e07c620c50fe20c Mon Sep 17 00:00:00 2001 From: repo-visualizer Date: Sun, 24 Aug 2025 01:24:50 +0000 Subject: [PATCH 3/7] Repo visualizer: update diagram --- diagram.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diagram.svg b/diagram.svg index 26a1659..0545c26 100644 --- a/diagram.svg +++ b/diagram.svg @@ -1 +1 @@ -utilsutilsteststestsstaticsstaticsmodulesmodulescorecoretext2imgtext2imgbf1bf1UIUImd2imgmd2imgtarottarotcrawlcrawlEmoticonsEmoticonsself_containedself_containedrequiredrequiredormormmodelsmodelsstaticstaticdrawdrawdatabasedatabaseblazeblazeIOSIOSstaticstaticreversereversenormalnormalminicraft_infominicraft_infoillillemoji_mixemoji_mixbf1_infobf1_infoai_chatai_chatstatusstatushelperhelperjsjscsscsstemplatetemplatejsjscsscssprovidersproviderspluginspluginscorecore.bat.css.gitignore.html.js.json.md.py.sh.svg.toml.txt.yamleach dot sized by file size \ No newline at end of file +utilsutilsteststestsstaticsstaticsmodulesmodulescorecoretext2imgtext2imgbf1bf1UIUImd2imgmd2imgtarottarotcrawlcrawlEmoticonsEmoticonsself_containedself_containedrequiredrequiredormormmodelsmodelsstaticstaticdrawdrawdatabasedatabaseblazeblazeIOSIOSstaticstaticreversereversenormalnormalminicraft_infominicraft_infoillillemoji_mixemoji_mixbf1_infobf1_infoai_chatai_chatstatusstatushelperhelperjsjscsscsstemplatetemplatejsjscsscssprovidersproviderspluginspluginscorecore.bat.css.gitignore.html.js.json.md.py.sh.svg.toml.txt.yamleach dot sized by file size \ No newline at end of file From 7d29cca96a118fa78d0171d48910979e75630ffe Mon Sep 17 00:00:00 2001 From: Umaru <84710711+g1331@users.noreply.github.com> Date: Sun, 7 Sep 2025 05:23:41 +0000 Subject: [PATCH 4/7] fix(orm): make insert_or_update upsert-concurrency-safe - Use UPDATE-first strategy; if no rows affected, try INSERT - Catch IntegrityError to handle UNIQUE conflicts and retry UPDATE - Import IntegrityError from sqlalchemy.exc This fixes UNIQUE constraint failures on GroupSetting.group_id when multiple callers race to initialize group settings (e.g., AccountController.init_group). --- core/orm/__init__.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/core/orm/__init__.py b/core/orm/__init__.py index 4a69ebc..70f2b89 100644 --- a/core/orm/__init__.py +++ b/core/orm/__init__.py @@ -6,6 +6,7 @@ from sqlalchemy.engine import Engine from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy.orm import declarative_base, sessionmaker +from sqlalchemy.exc import IntegrityError from core.config import GlobalConfig @@ -207,21 +208,28 @@ async def update_batch(self, table, data_list, conditions_list): async def insert_or_update(self, table, data, condition): """ - 如果满足条件则更新,否则插入 + 如果满足条件则更新,否则插入(并发安全) + - 先尝试 UPDATE,如果受影响行数为 0,则尝试 INSERT + - 若 INSERT 因唯一约束冲突(IntegrityError)失败,则再次执行 UPDATE :param table: 表 :param data: 数据 :param condition: 条件 """ - # 判断是否存在符合条件的数据 - exist = (await self.execute(select(table).where(*condition))).all() - if exist: - # 如果存在,则执行更新操作 - stmt = update(table).where(*condition).values(**data) - stmt = stmt.execution_options(synchronize_session="fetch") - await self.execute(stmt) - else: - # 否则执行插入操作 - await self.execute(insert(table).values(**data)) + # 优先 UPDATE,避免大多数情况下的唯一约束冲突 + stmt = update(table).where(*condition).values(**data) + stmt = stmt.execution_options(synchronize_session="fetch") + result = await self.execute(stmt) + try: + if result is not None and getattr(result, "rowcount", 0) > 0: + return result + # 受影响行数为 0,说明不存在,尝试 INSERT + return await self.execute(insert(table).values(**data)) + except IntegrityError: + # 竞争条件:在我们尝试 INSERT 前,其他协程/进程已插入相同唯一键 + # 回退到 UPDATE,确保数据按预期更新 + stmt2 = update(table).where(*condition).values(**data) + stmt2 = stmt2.execution_options(synchronize_session="fetch") + return await self.execute(stmt2) async def insert_or_update_batch(self, table, data_list, conditions_list): """ From c5c15126f0e15de8ba7422b2c172af17b7773e6b Mon Sep 17 00:00:00 2001 From: Umaru <84710711+g1331@users.noreply.github.com> Date: Tue, 9 Sep 2025 13:42:41 +0800 Subject: [PATCH 5/7] refactor(orm): address code review feedback - Remove redundant `result is not None` check in insert_or_update - Extract _build_update_stmt helper method to eliminate code duplication - Improve code maintainability and readability Addresses review comments from Copilot in PR #189. --- core/orm/__init__.py | 63 +++++++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/core/orm/__init__.py b/core/orm/__init__.py index 70f2b89..4f59fed 100644 --- a/core/orm/__init__.py +++ b/core/orm/__init__.py @@ -3,10 +3,10 @@ from creart import create from loguru import logger from sqlalchemy import MetaData, inspect, delete, update, select, insert, text, event +from sqlalchemy.exc import IntegrityError from sqlalchemy.engine import Engine from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy.orm import declarative_base, sessionmaker -from sqlalchemy.exc import IntegrityError from core.config import GlobalConfig @@ -206,6 +206,18 @@ async def update_batch(self, table, data_list, conditions_list): # 执行批量更新操作 await self.execute_all(update_stmts) + def _build_update_stmt(self, table, data, condition): + """ + 构造 UPDATE 语句的辅助方法 + :param table: 表 + :param data: 数据 + :param condition: 条件 + :return: 配置好的 UPDATE 语句 + """ + stmt = update(table).where(*condition).values(**data) + stmt = stmt.execution_options(synchronize_session="fetch") + return stmt + async def insert_or_update(self, table, data, condition): """ 如果满足条件则更新,否则插入(并发安全) @@ -216,20 +228,18 @@ async def insert_or_update(self, table, data, condition): :param condition: 条件 """ # 优先 UPDATE,避免大多数情况下的唯一约束冲突 - stmt = update(table).where(*condition).values(**data) - stmt = stmt.execution_options(synchronize_session="fetch") - result = await self.execute(stmt) + update_stmt = self._build_update_stmt(table, data, condition) + result = await self.execute(update_stmt) try: - if result is not None and getattr(result, "rowcount", 0) > 0: + if getattr(result, "rowcount", 0) > 0: return result # 受影响行数为 0,说明不存在,尝试 INSERT return await self.execute(insert(table).values(**data)) except IntegrityError: # 竞争条件:在我们尝试 INSERT 前,其他协程/进程已插入相同唯一键 # 回退到 UPDATE,确保数据按预期更新 - stmt2 = update(table).where(*condition).values(**data) - stmt2 = stmt2.execution_options(synchronize_session="fetch") - return await self.execute(stmt2) + retry_update_stmt = self._build_update_stmt(table, data, condition) + return await self.execute(retry_update_stmt) async def insert_or_update_batch(self, table, data_list, conditions_list): """ @@ -238,17 +248,14 @@ async def insert_or_update_batch(self, table, data_list, conditions_list): :param data_list: 数据列表,每个元素是一个dict,表示一条记录的数据 :param conditions_list: 条件列表,每个元素是一个tuple或list,表示该记录的条件,与data_list中的元素一一对应 """ - stmts = [] + # 为了避免批量操作中的约束冲突,我们逐个处理每条记录 for data, condition in zip(data_list, conditions_list): - exist = (await self.execute(select(table).where(*condition))).all() - if exist: - # 如果存在符合条件的数据,则更新 - stmts.append(update(table).where(*condition).values(**data)) - else: - # 否则插入 - stmts.append(insert(table).values(**data)) - # 执行批量插入或更新操作 - await self.execute_all(stmts) + try: + await self.insert_or_update(table, data, condition) + except Exception as e: + logger.error(f"Failed to insert_or_update in batch operation: {e}") + # 继续处理其他记录,不中断整个批量操作 + continue async def insert_or_ignore(self, table, data, condition): """ @@ -257,9 +264,23 @@ async def insert_or_ignore(self, table, data, condition): :param data: 数据 :param condition: 条件 """ - if not (await self.execute(select(table).where(*condition))).all(): - return await self.execute(insert(table).values(**data)) - return None + try: + if not (await self.execute(select(table).where(*condition))).all(): + return await self.execute(insert(table).values(**data)) + return None + except IntegrityError as e: + # 如果是 UNIQUE 约束冲突,说明记录已存在,直接忽略 + if "UNIQUE constraint failed" in str(e): + logger.debug(f"Record already exists, ignoring insert: {e}") + return None + else: + # 其他完整性错误直接抛出 + logger.error(f"IntegrityError in insert_or_ignore: {e}") + raise e + except Exception as e: + # 其他异常直接抛出 + logger.error(f"Unexpected error in insert_or_ignore: {e}") + raise e async def select(self, el, condition=None): """ From dc91e259d12a2e52d142b75f28d9766ae6260ecb Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 03:18:40 +0000 Subject: [PATCH 6/7] chore: add .serena/ to .gitignore from main branch Co-authored-by: Umaru --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 5a4a2ee..64f0671 100644 --- a/.gitignore +++ b/.gitignore @@ -208,3 +208,5 @@ alembic.ini .claude/settings.local.json .claude/*.local.* .claude/local/ + +.serena/ From c676f55e979a0c4ccd974824127d7cfc367957fe Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 03:58:20 +0000 Subject: [PATCH 7/7] fix(orm): address code review issues - fix try-block scope and TOCTOU race - Move UPDATE result check outside try block in insert_or_update to prevent catching UPDATE exceptions incorrectly - Fix TOCTOU race condition in insert_or_ignore by using INSERT-first approach instead of check-then-insert - Remove broad Exception catching, only catch IntegrityError - Use bare 'raise' instead of 'raise e' for better traceback Addresses blocking and high-priority issues from code review. Co-authored-by: Umaru --- core/orm/__init__.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/core/orm/__init__.py b/core/orm/__init__.py index 4f59fed..85a2d5e 100644 --- a/core/orm/__init__.py +++ b/core/orm/__init__.py @@ -230,10 +230,13 @@ async def insert_or_update(self, table, data, condition): # 优先 UPDATE,避免大多数情况下的唯一约束冲突 update_stmt = self._build_update_stmt(table, data, condition) result = await self.execute(update_stmt) + + # 如果 UPDATE 影响了行,则成功返回 + if getattr(result, "rowcount", 0) > 0: + return result + + # 受影响行数为 0,说明不存在,尝试 INSERT try: - if getattr(result, "rowcount", 0) > 0: - return result - # 受影响行数为 0,说明不存在,尝试 INSERT return await self.execute(insert(table).values(**data)) except IntegrityError: # 竞争条件:在我们尝试 INSERT 前,其他协程/进程已插入相同唯一键 @@ -259,15 +262,15 @@ async def insert_or_update_batch(self, table, data_list, conditions_list): async def insert_or_ignore(self, table, data, condition): """ - 不满足条件则插入,否则跳过 + 不满足条件则插入,否则跳过(并发安全) + 直接尝试插入,若 UNIQUE 约束冲突则忽略 :param table: 表 :param data: 数据 - :param condition: 条件 + :param condition: 条件(用于日志记录,实际插入依赖唯一约束) """ try: - if not (await self.execute(select(table).where(*condition))).all(): - return await self.execute(insert(table).values(**data)) - return None + # 直接尝试插入,让数据库的唯一约束来保证不重复 + return await self.execute(insert(table).values(**data)) except IntegrityError as e: # 如果是 UNIQUE 约束冲突,说明记录已存在,直接忽略 if "UNIQUE constraint failed" in str(e): @@ -276,11 +279,7 @@ async def insert_or_ignore(self, table, data, condition): else: # 其他完整性错误直接抛出 logger.error(f"IntegrityError in insert_or_ignore: {e}") - raise e - except Exception as e: - # 其他异常直接抛出 - logger.error(f"Unexpected error in insert_or_ignore: {e}") - raise e + raise async def select(self, el, condition=None): """