Skip to content

V1.4.1 wss连接qq服务器 (break fix) - #10

Open
dudu7615 wants to merge 9 commits into
mainfrom
feat/wss-connect
Open

dudu7615 wants to merge 9 commits into
mainfrom
feat/wss-connect

Conversation

@dudu7615

@dudu7615 dudu7615 commented Jul 19, 2026 •

Copy link
Copy Markdown
Owner
  1. wss连接qq(提取自hermes)
  2. 增加群聊支持
  3. ai使用本地newapi的deepseek-v4-flash

Summary by CodeRabbit

  • New Features

    • Added QQ Bot WebSocket connectivity with automatic reconnection, heartbeats, session recovery, and message deduplication.
    • Added support for private and group messaging, long-text splitting, media uploads, typing indicators, and interactive keyboards.
    • Added QR-based bot onboarding and token management.
    • Added AI chat support with conversation history controls and configurable context limits.
    • Added group-message support to weather and holiday responses.
  • Bug Fixes

    • Improved reply routing so responses return to the correct private chat or group.
  • Documentation

    • Added setup and integration guidance for the WebSocket client.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @dudu7615, your pull request is larger than the review limit of 150000 diff characters

@coderabbitai

coderabbitai Bot commented Jul 19, 2026 •

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8caceef4-9ff3-4524-b0c1-63cb946d1d72

📥 Commits

Reviewing files that changed from the base of the PR and between 1ec11c1 and ef05ca0.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (72)
  • .github/workflows/black-format.yml
  • .github/workflows/codeql.yml
  • .gitignore
  • .python-version
  • .sourcery.yml
  • .vscode/launch.json
  • .vscode/settings.json
  • CLAUDE.md
  • LICENSE
  • QBot.code-workspace
  • README.md
  • config.yaml
  • main.py
  • modules/AutoAuth.py
  • modules/Config.py
  • modules/HermesWss/README.md
  • modules/HermesWss/__init__.py
  • modules/HermesWss/chunked_upload.py
  • modules/HermesWss/constants.py
  • modules/HermesWss/crypto.py
  • modules/HermesWss/exceptions.py
  • modules/HermesWss/keyboards.py
  • modules/HermesWss/models.py
  • modules/HermesWss/onboard.py
  • modules/HermesWss/sender.py
  • modules/HermesWss/token_manager.py
  • modules/HermesWss/utils.py
  • modules/HermesWss/wss_client.py
  • modules/HttpSender.py
  • modules/Types.py
  • modules/Urls.py
  • plugin/AiChat.py
  • plugin/AiChat_d/.gitignore
  • plugin/AiChat_d/Config.py
  • plugin/AiChat_d/History.py
  • plugin/AiChat_d/Paths.py
  • plugin/AiChat_d/RequApi.py
  • plugin/AiChat_d/Sql.py
  • plugin/AiChat_d/Types.py
  • plugin/AiChat_d/config.yaml
  • plugin/AiChat_d/data/personality/xy.md
  • plugin/AiChat_d/sql/init.sql
  • plugin/AiChat_d/sql/message/add.sql
  • plugin/AiChat_d/sql/message/clear.sql
  • plugin/AiChat_d/sql/message/get.sql
  • plugin/AiChat_d/sql/user/add.sql
  • plugin/AiChat_d/sql/user/check.sql
  • plugin/AiChat_d/sql/user/del.sql
  • plugin/GlmAi.py
  • plugin/GlmAi_d/Config.py
  • plugin/GlmAi_d/config.yaml
  • plugin/Holiday.py
  • plugin/Holiday_d/.gitignore
  • plugin/Holiday_d/RequApi.py
  • plugin/Holiday_d/SqlCache.py
  • plugin/Holiday_d/test.http
  • plugin/PluginBase.py
  • plugin/TestAutoReply.py
  • plugin/Weather.py
  • plugin/Weather_d/.gitignore
  • plugin/Weather_d/GetWeather.py
  • plugin/Weather_d/SqlCache.py
  • plugin/Weather_d/config.yaml
  • plugin/Weather_d/test.http
  • plugin/__init__.py
  • pyproject.toml
  • run.sh
  • test/ai.json
  • test/aiChat.py
  • test/passCallback.py
  • test/recieveHttp.py
  • test/testSqlmodel.py
💤 Files with no reviewable changes (3)
  • plugin/GlmAi_d/config.yaml
  • plugin/GlmAi_d/Config.py
  • plugin/GlmAi.py

📝 Walkthrough

Walkthrough

The application migrates from FastAPI webhooks to QQ WebSocket processing, adds HermesWss protocol and messaging capabilities, introduces QR onboarding and uploads, and replaces the previous AI plugin with configurable AiChat support for person and group conversations.

Changes

WebSocket and HermesWss integration

Layer / File(s) Summary
Protocol contracts and utilities
modules/HermesWss/{constants,models,exceptions,crypto,utils}.py, modules/HermesWss/__init__.py
Adds typed gateway, message, keyboard, upload, onboarding, exception, encryption, and request-header contracts with an explicit package API.
Gateway lifecycle and onboarding
modules/HermesWss/wss_client.py, modules/HermesWss/token_manager.py, modules/HermesWss/onboard.py
Adds token caching, gateway connection, heartbeat, identify/resume, event dispatch, reconnection, QR binding, polling, and secret decryption.
Messaging and uploads
modules/HermesWss/sender.py, modules/HermesWss/chunked_upload.py, modules/HermesWss/keyboards.py
Adds retryable text, long-text, media, typing, approval-keyboard, interaction, and multipart upload flows.

Application and plugin flow

Layer / File(s) Summary
Runtime migration and routing
main.py, modules/HttpSender.py, modules/Types.py, plugin/{PluginBase,Holiday,Weather}.py
Replaces FastAPI startup with WebSocket callbacks, translates QQMessage events into legacy data, and routes replies to C2C or group endpoints.
AiChat plugin and persistence
plugin/AiChat.py, plugin/AiChat_d/*, plugin/__init__.py, plugin/GlmAi.py
Adds configurable AiChat requests, typed API payloads, persisted history truncation, SQL schema/scripts, personality data, and replaces the GlmAi export.
Project and development configuration
pyproject.toml, run.sh, .vscode/*, QBot.code-workspace, CLAUDE.md, config.yaml
Updates dependencies, strict mypy settings, startup and debugging commands, workspace metadata, repository guidance, and application credentials.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • dudu7615/QBot#9: Migrates runtime handling to HermesWss and builds on the WebSocket module introduced here.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: switching QQ connectivity to WSS in V1.4.1.
Docstring Coverage ✅ Passed Docstring coverage is 80.81% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/wss-connect

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
plugin/Holiday_d/RequApi.py (1)

7-33: 🚀 Performance & Scalability | 🔴 Critical | ⚡ Quick win

Avoid blocking the async event loop with synchronous HTTP calls.

updataCache performs a synchronous httpx.get request. Since it is called from the asynchronous Holiday.run method, this blocking network I/O will halt the entire asyncio event loop. If the request takes a few seconds or times out, it can prevent the bot from responding to other messages or sending heartbeats, which may cause the WebSocket to disconnect.

Convert these functions to use async/await with httpx.AsyncClient to avoid blocking the event loop.

⚡ Proposed fix

Update plugin/Holiday_d/RequApi.py:

-def updataCache():
+async def updataCache():
     url = "https://timor.tech/api/holiday/year"
     for _ in range(3):
         try:
-            resp = httpx.get(url, timeout=10)
-            if resp.status_code == 200:
-                reply = resp.json()
-                if reply["code"] == 0:
-                    for _, holiday in reply["holiday"].items():
-                        Cache.addCache(
-                            holiday["name"],
-                            datetime.strptime(holiday["date"], "%Y-%m-%d"),
-                            holiday["holiday"],
-                        )
-                    logger.info("获取节假日信息成功")
-                    return
+            async with httpx.AsyncClient() as client:
+                resp = await client.get(url, timeout=10)
+                if resp.status_code == 200:
+                    reply = resp.json()
+                    if reply["code"] == 0:
+                        for _, holiday in reply["holiday"].items():
+                            Cache.addCache(
+                                holiday["name"],
+                                datetime.strptime(holiday["date"], "%Y-%m-%d"),
+                                holiday["holiday"],
+                            )
+                        logger.info("获取节假日信息成功")
+                        return
         except Exception as e:
             logger.warning(f"尝试获取节假日信息失败,错误信息:{e}")
             continue
     logger.error("获取节假日信息失败,请检查网络连接")
     return None
 
-def checkAndUpdata():
+async def checkAndUpdata():
     cachedYear = Cache.getCachedYear()
     if cachedYear is None or cachedYear.year != datetime.now().year:
-        updataCache()
+        await updataCache()

Then, update the caller in plugin/Holiday.py around line 32:

        if not self._checked:
-           RequApi.checkAndUpdata()
+           await RequApi.checkAndUpdata()
            self._checked = True
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugin/Holiday_d/RequApi.py` around lines 7 - 33, Convert updataCache and
checkAndUpdata in RequApi.py to async functions, perform requests through
httpx.AsyncClient with awaited calls, and preserve the existing retry, caching,
and logging behavior. Update the Holiday.run caller to await
RequApi.checkAndUpdata() before setting _checked.
🧹 Nitpick comments (4)
CLAUDE.md (1)

30-30: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Specify a language for fenced code blocks.

These code blocks omit a language identifier, which degrades syntax highlighting in viewers and triggers markdownlint warnings (MD040).

  • CLAUDE.md#L30-L30: add text to the fenced code block opening to specify the language.
  • modules/HermesWss/README.md#L28-L28: add text to the fenced code block opening to specify the language.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` at line 30, Specify the text language on the fenced code block
openings in CLAUDE.md (line 30) and modules/HermesWss/README.md (line 28) by
adding the text identifier; no other content changes are needed.

Source: Linters/SAST tools

modules/HermesWss/models.py (1)

404-407: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use Any/Awaitable for these callback aliases. Coroutine is Coroutine[YieldType, SendType, ReturnType], so asyncio.AbstractEventLoop is the wrong first parameter here. Update PostMessageFn, OnMessageCallback, and OnInteractionCallback to Coroutine[Any, Any, ...] (or Awaitable[...] if only awaitability is needed).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/HermesWss/models.py` around lines 404 - 407, Update the callback type
aliases PostMessageFn, OnMessageCallback, and OnInteractionCallback to use
Coroutine[Any, Any, ReturnType] or Awaitable[ReturnType] instead of treating
asyncio.AbstractEventLoop as the coroutine yield type. Add or reuse the
appropriate Any import and preserve each alias’s existing parameter and return
types.

Source: Coding guidelines

plugin/Weather.py (1)

75-103: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consistently use target from _reply_target.

In the /天气预报 and /常住地 branches, author["id"] is used instead of the target variable returned by _reply_target. While functionally equivalent for person messages, using target consistently is cleaner and fully utilizes the helper function (as done in the /今日天气 branch).

♻️ Proposed refactor
             logger.info(f"发送天气信息: {weather}")
             send_type, target = self._reply_target(message)
             if send_type == "group":
                 await HttpSender.sendText2Group(weather, message, target)
             else:
-                await HttpSender.sendText2Person(weather, message, author["id"])
+                await HttpSender.sendText2Person(weather, message, target)
 
         elif content.startswith("/常住地"):
             if len(content.split(" ")) != 2:
                 send_type, target = self._reply_target(message)
                 if send_type == "group":
                     await HttpSender.sendText2Group(
                         "输入格式错误. 请输入 /常住地 <城市名>", message, target
                     )
                 else:
                     await HttpSender.sendText2Person(
-                        "输入格式错误. 请输入 /常住地 <城市名>", message, author["id"]
+                        "输入格式错误. 请输入 /常住地 <城市名>", message, target
                     )
             else:
                 city = content.split(" ")[1]
                 SqlCache.User.add(author["id"], city)
                 send_type, target = self._reply_target(message)
                 if send_type == "group":
                     await HttpSender.sendText2Group(
                         f"设置常住地: {city}", message, target
                     )
                 else:
                     await HttpSender.sendText2Person(
-                        f"设置常住地: {city}", message, author["id"]
+                        f"设置常住地: {city}", message, target
                     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugin/Weather.py` around lines 75 - 103, Update the `/天气预报` and `/常住地`
branches in the message handler to pass the `target` returned by
`_reply_target(message)` to `HttpSender.sendText2Person` instead of
`author["id"]`. Preserve the existing group-message behavior and response
content.
plugin/Weather_d/GetWeather.py (1)

34-38: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Prevent rate-limit race conditions under concurrent load.

The current rate-limiting logic uses a global _lastCheck variable without synchronization. If multiple users request weather data simultaneously, they might all bypass the while loop at the same time and hit the API concurrently, exceeding the rate limit.

Consider using an asyncio.Lock() to ensure that concurrent requests wait their turn and accurately update _lastCheck.

🔒️ Proposed refactor to add a lock

Add a global lock at the top of the file:

import asyncio

_rate_limit_lock = asyncio.Lock()

Then, wrap the rate-limit check and the update of _lastCheck inside the lock:

-            # 防止请求过快
-            while datetime.now() - _lastCheck < timedelta(seconds=1.05):
-                await asyncio.sleep(1)
-            _lastCheck = datetime.now()
+            # 防止请求过快
+            async with _rate_limit_lock:
+                while datetime.now() - _lastCheck < timedelta(seconds=1.05):
+                    await asyncio.sleep(0.1)
+                _lastCheck = datetime.now()

(Apply this change to both getTodayWeather and getFutureWeather.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugin/Weather_d/GetWeather.py` around lines 34 - 38, Protect the shared
_lastCheck rate-limit state in both getTodayWeather and getFutureWeather with a
module-level asyncio.Lock. Acquire the lock around each method’s wait loop and
_lastCheck update so concurrent requests serialize their timing checks while
preserving the existing delay behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@config.yaml`:
- Around line 1-2: Remove the exposed clientSecret and any other real
credentials from config.yaml, revoke and rotate the leaked QQ Gateway secret,
and replace repository values with placeholders or environment-variable
references. Update .gitignore to exclude config.yaml so local credentials cannot
be committed again.

In `@modules/HermesWss/chunked_upload.py`:
- Around line 385-387: Update the retry-delay condition in the complete-upload
retry loop around the visible backoff calculation so it also sleeps before the
final retry attempt. Match the boundary behavior used by _upload_single_part and
_PART_UPLOAD_MAX_RETRIES, while preserving the existing exponential delay
calculation.

In `@modules/HermesWss/wss_client.py`:
- Line 56: Replace stdlib logging throughout HermesWss with loguru: in
modules/HermesWss/wss_client.py:56-56, sender.py:32-32, onboard.py:38-38,
keyboards.py:25-25, and chunked_upload.py:30-30, remove each logging import and
getLogger setup and import the module-level logger from loguru; in
onboard.py:38-38 also replace the print block with appropriate logger calls
while preserving its messages.
- Around line 311-317: Update the WebSocket connection setup in the surrounding
ws_connect flow to apply CONNECT_TIMEOUT_SECONDS to the connection-establishment
timeout rather than only the ws_close timeout. Configure the aiohttp timeout
parameter that bounds connect setup, while preserving the existing gateway_url,
headers, and close-timeout behavior.

In `@plugin/AiChat_d/Config.py`:
- Around line 6-18: Replace the unsafe yaml.load call in the Config
configuration-loading flow with yaml.safe_load, and replace the TypedDict Config
definition with a Pydantic model that validates the configuration fields and
types. Instantiate that model from the safely loaded YAML data while preserving
the existing Config field names and Paths.CONFIG_FILE source.

In `@plugin/AiChat_d/config.yaml`:
- Line 1: Remove the hardcoded value from the apiKey configuration in
config.yaml and replace it with the project’s supported environment-variable
reference or a non-secret placeholder. Revoke the exposed API key through the
provider, without adding another secret to the repository.

In `@plugin/AiChat_d/History.py`:
- Line 3: Replace the wildcard import from plugin.AiChat_d.Sql in History.py
with explicit imports for only the Sql symbols used by the module, preserving
existing behavior and avoiding namespace collisions.

In `@plugin/AiChat_d/Sql.py`:
- Around line 147-156: Update the truncation logic in the message cleanup block
to preserve the oldest system prompt at all times: when deleting messages beyond
keep_count from the newest-first all_msgs list, exclude the final element (index
-1) from the deletion slice while still removing excess intermediate messages.

In `@plugin/AiChat_d/sql/init.sql`:
- Around line 1-17: The SQL schema and queries must match the ORM models in
Sql.py. In plugin/AiChat_d/sql/init.sql lines 1-17, rename users/messages to
user/message, rename open_id/updated_at/time to openId/latestChat/createdAt, and
add messageCount to user; update plugin/AiChat_d/sql/message/add.sql lines 1-8
to use message/user with openId/latestChat, plugin/AiChat_d/sql/message/get.sql
lines 1-3 to use message with openId/createdAt, and
plugin/AiChat_d/sql/user/add.sql lines 1 and plugin/AiChat_d/sql/user/check.sql
lines 1-2 to use user and openId.

In `@plugin/AiChat_d/sql/message/clear.sql`:
- Line 1: Update the DELETE statement in the clear SQL script to use valid
DELETE FROM syntax and target the messages table name defined by init.sql, while
preserving the open_id filter.

In `@plugin/AiChat_d/sql/user/del.sql`:
- Line 1: Update the DELETE statement in the user deletion SQL to use valid
DELETE FROM syntax, and replace the positional placeholder with the named
open_id parameter format used by the surrounding SQL files.

In `@plugin/AiChat_d/Types.py`:
- Around line 1-39: Replace the TypedDict definitions in Types.py with Pydantic
BaseModel classes, preserving the existing fields, annotations, nesting, and
defaults or required-field behavior. Convert ApiJson_Messages, ApiJson,
ReplyJson_Choices_Message, ReplyJson_Choices, and ReplyJson consistently, using
BaseModel-compatible nested model types.

In `@plugin/AiChat.py`:
- Line 40: Update the union_id assignment to use safe dictionary lookups for
both author fields, preserving the fallback from union_openid to id without
raising KeyError when either key is absent.

---

Outside diff comments:
In `@plugin/Holiday_d/RequApi.py`:
- Around line 7-33: Convert updataCache and checkAndUpdata in RequApi.py to
async functions, perform requests through httpx.AsyncClient with awaited calls,
and preserve the existing retry, caching, and logging behavior. Update the
Holiday.run caller to await RequApi.checkAndUpdata() before setting _checked.

---

Nitpick comments:
In `@CLAUDE.md`:
- Line 30: Specify the text language on the fenced code block openings in
CLAUDE.md (line 30) and modules/HermesWss/README.md (line 28) by adding the text
identifier; no other content changes are needed.

In `@modules/HermesWss/models.py`:
- Around line 404-407: Update the callback type aliases PostMessageFn,
OnMessageCallback, and OnInteractionCallback to use Coroutine[Any, Any,
ReturnType] or Awaitable[ReturnType] instead of treating
asyncio.AbstractEventLoop as the coroutine yield type. Add or reuse the
appropriate Any import and preserve each alias’s existing parameter and return
types.

In `@plugin/Weather_d/GetWeather.py`:
- Around line 34-38: Protect the shared _lastCheck rate-limit state in both
getTodayWeather and getFutureWeather with a module-level asyncio.Lock. Acquire
the lock around each method’s wait loop and _lastCheck update so concurrent
requests serialize their timing checks while preserving the existing delay
behavior.

In `@plugin/Weather.py`:
- Around line 75-103: Update the `/天气预报` and `/常住地` branches in the message
handler to pass the `target` returned by `_reply_target(message)` to
`HttpSender.sendText2Person` instead of `author["id"]`. Preserve the existing
group-message behavior and response content.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8caceef4-9ff3-4524-b0c1-63cb946d1d72

📥 Commits

Reviewing files that changed from the base of the PR and between 1ec11c1 and ef05ca0.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (72)
  • .github/workflows/black-format.yml
  • .github/workflows/codeql.yml
  • .gitignore
  • .python-version
  • .sourcery.yml
  • .vscode/launch.json
  • .vscode/settings.json
  • CLAUDE.md
  • LICENSE
  • QBot.code-workspace
  • README.md
  • config.yaml
  • main.py
  • modules/AutoAuth.py
  • modules/Config.py
  • modules/HermesWss/README.md
  • modules/HermesWss/__init__.py
  • modules/HermesWss/chunked_upload.py
  • modules/HermesWss/constants.py
  • modules/HermesWss/crypto.py
  • modules/HermesWss/exceptions.py
  • modules/HermesWss/keyboards.py
  • modules/HermesWss/models.py
  • modules/HermesWss/onboard.py
  • modules/HermesWss/sender.py
  • modules/HermesWss/token_manager.py
  • modules/HermesWss/utils.py
  • modules/HermesWss/wss_client.py
  • modules/HttpSender.py
  • modules/Types.py
  • modules/Urls.py
  • plugin/AiChat.py
  • plugin/AiChat_d/.gitignore
  • plugin/AiChat_d/Config.py
  • plugin/AiChat_d/History.py
  • plugin/AiChat_d/Paths.py
  • plugin/AiChat_d/RequApi.py
  • plugin/AiChat_d/Sql.py
  • plugin/AiChat_d/Types.py
  • plugin/AiChat_d/config.yaml
  • plugin/AiChat_d/data/personality/xy.md
  • plugin/AiChat_d/sql/init.sql
  • plugin/AiChat_d/sql/message/add.sql
  • plugin/AiChat_d/sql/message/clear.sql
  • plugin/AiChat_d/sql/message/get.sql
  • plugin/AiChat_d/sql/user/add.sql
  • plugin/AiChat_d/sql/user/check.sql
  • plugin/AiChat_d/sql/user/del.sql
  • plugin/GlmAi.py
  • plugin/GlmAi_d/Config.py
  • plugin/GlmAi_d/config.yaml
  • plugin/Holiday.py
  • plugin/Holiday_d/.gitignore
  • plugin/Holiday_d/RequApi.py
  • plugin/Holiday_d/SqlCache.py
  • plugin/Holiday_d/test.http
  • plugin/PluginBase.py
  • plugin/TestAutoReply.py
  • plugin/Weather.py
  • plugin/Weather_d/.gitignore
  • plugin/Weather_d/GetWeather.py
  • plugin/Weather_d/SqlCache.py
  • plugin/Weather_d/config.yaml
  • plugin/Weather_d/test.http
  • plugin/__init__.py
  • pyproject.toml
  • run.sh
  • test/ai.json
  • test/aiChat.py
  • test/passCallback.py
  • test/recieveHttp.py
  • test/testSqlmodel.py
💤 Files with no reviewable changes (3)
  • plugin/GlmAi_d/config.yaml
  • plugin/GlmAi_d/Config.py
  • plugin/GlmAi.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
plugin/Holiday_d/RequApi.py (1)

7-33: 🚀 Performance & Scalability | 🔴 Critical | ⚡ Quick win

Avoid blocking the async event loop with synchronous HTTP calls.

updataCache performs a synchronous httpx.get request. Since it is called from the asynchronous Holiday.run method, this blocking network I/O will halt the entire asyncio event loop. If the request takes a few seconds or times out, it can prevent the bot from responding to other messages or sending heartbeats, which may cause the WebSocket to disconnect.

Convert these functions to use async/await with httpx.AsyncClient to avoid blocking the event loop.

⚡ Proposed fix

Update plugin/Holiday_d/RequApi.py:

-def updataCache():
+async def updataCache():
     url = "https://timor.tech/api/holiday/year"
     for _ in range(3):
         try:
-            resp = httpx.get(url, timeout=10)
-            if resp.status_code == 200:
-                reply = resp.json()
-                if reply["code"] == 0:
-                    for _, holiday in reply["holiday"].items():
-                        Cache.addCache(
-                            holiday["name"],
-                            datetime.strptime(holiday["date"], "%Y-%m-%d"),
-                            holiday["holiday"],
-                        )
-                    logger.info("获取节假日信息成功")
-                    return
+            async with httpx.AsyncClient() as client:
+                resp = await client.get(url, timeout=10)
+                if resp.status_code == 200:
+                    reply = resp.json()
+                    if reply["code"] == 0:
+                        for _, holiday in reply["holiday"].items():
+                            Cache.addCache(
+                                holiday["name"],
+                                datetime.strptime(holiday["date"], "%Y-%m-%d"),
+                                holiday["holiday"],
+                            )
+                        logger.info("获取节假日信息成功")
+                        return
         except Exception as e:
             logger.warning(f"尝试获取节假日信息失败,错误信息:{e}")
             continue
     logger.error("获取节假日信息失败,请检查网络连接")
     return None
 
-def checkAndUpdata():
+async def checkAndUpdata():
     cachedYear = Cache.getCachedYear()
     if cachedYear is None or cachedYear.year != datetime.now().year:
-        updataCache()
+        await updataCache()

Then, update the caller in plugin/Holiday.py around line 32:

        if not self._checked:
-           RequApi.checkAndUpdata()
+           await RequApi.checkAndUpdata()
            self._checked = True
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugin/Holiday_d/RequApi.py` around lines 7 - 33, Convert updataCache and
checkAndUpdata in RequApi.py to async functions, perform requests through
httpx.AsyncClient with awaited calls, and preserve the existing retry, caching,
and logging behavior. Update the Holiday.run caller to await
RequApi.checkAndUpdata() before setting _checked.
🧹 Nitpick comments (4)
CLAUDE.md (1)

30-30: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Specify a language for fenced code blocks.

These code blocks omit a language identifier, which degrades syntax highlighting in viewers and triggers markdownlint warnings (MD040).

  • CLAUDE.md#L30-L30: add text to the fenced code block opening to specify the language.
  • modules/HermesWss/README.md#L28-L28: add text to the fenced code block opening to specify the language.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` at line 30, Specify the text language on the fenced code block
openings in CLAUDE.md (line 30) and modules/HermesWss/README.md (line 28) by
adding the text identifier; no other content changes are needed.

Source: Linters/SAST tools

modules/HermesWss/models.py (1)

404-407: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use Any/Awaitable for these callback aliases. Coroutine is Coroutine[YieldType, SendType, ReturnType], so asyncio.AbstractEventLoop is the wrong first parameter here. Update PostMessageFn, OnMessageCallback, and OnInteractionCallback to Coroutine[Any, Any, ...] (or Awaitable[...] if only awaitability is needed).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/HermesWss/models.py` around lines 404 - 407, Update the callback type
aliases PostMessageFn, OnMessageCallback, and OnInteractionCallback to use
Coroutine[Any, Any, ReturnType] or Awaitable[ReturnType] instead of treating
asyncio.AbstractEventLoop as the coroutine yield type. Add or reuse the
appropriate Any import and preserve each alias’s existing parameter and return
types.

Source: Coding guidelines

plugin/Weather.py (1)

75-103: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consistently use target from _reply_target.

In the /天气预报 and /常住地 branches, author["id"] is used instead of the target variable returned by _reply_target. While functionally equivalent for person messages, using target consistently is cleaner and fully utilizes the helper function (as done in the /今日天气 branch).

♻️ Proposed refactor
             logger.info(f"发送天气信息: {weather}")
             send_type, target = self._reply_target(message)
             if send_type == "group":
                 await HttpSender.sendText2Group(weather, message, target)
             else:
-                await HttpSender.sendText2Person(weather, message, author["id"])
+                await HttpSender.sendText2Person(weather, message, target)
 
         elif content.startswith("/常住地"):
             if len(content.split(" ")) != 2:
                 send_type, target = self._reply_target(message)
                 if send_type == "group":
                     await HttpSender.sendText2Group(
                         "输入格式错误. 请输入 /常住地 <城市名>", message, target
                     )
                 else:
                     await HttpSender.sendText2Person(
-                        "输入格式错误. 请输入 /常住地 <城市名>", message, author["id"]
+                        "输入格式错误. 请输入 /常住地 <城市名>", message, target
                     )
             else:
                 city = content.split(" ")[1]
                 SqlCache.User.add(author["id"], city)
                 send_type, target = self._reply_target(message)
                 if send_type == "group":
                     await HttpSender.sendText2Group(
                         f"设置常住地: {city}", message, target
                     )
                 else:
                     await HttpSender.sendText2Person(
-                        f"设置常住地: {city}", message, author["id"]
+                        f"设置常住地: {city}", message, target
                     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugin/Weather.py` around lines 75 - 103, Update the `/天气预报` and `/常住地`
branches in the message handler to pass the `target` returned by
`_reply_target(message)` to `HttpSender.sendText2Person` instead of
`author["id"]`. Preserve the existing group-message behavior and response
content.
plugin/Weather_d/GetWeather.py (1)

34-38: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Prevent rate-limit race conditions under concurrent load.

The current rate-limiting logic uses a global _lastCheck variable without synchronization. If multiple users request weather data simultaneously, they might all bypass the while loop at the same time and hit the API concurrently, exceeding the rate limit.

Consider using an asyncio.Lock() to ensure that concurrent requests wait their turn and accurately update _lastCheck.

🔒️ Proposed refactor to add a lock

Add a global lock at the top of the file:

import asyncio

_rate_limit_lock = asyncio.Lock()

Then, wrap the rate-limit check and the update of _lastCheck inside the lock:

-            # 防止请求过快
-            while datetime.now() - _lastCheck < timedelta(seconds=1.05):
-                await asyncio.sleep(1)
-            _lastCheck = datetime.now()
+            # 防止请求过快
+            async with _rate_limit_lock:
+                while datetime.now() - _lastCheck < timedelta(seconds=1.05):
+                    await asyncio.sleep(0.1)
+                _lastCheck = datetime.now()

(Apply this change to both getTodayWeather and getFutureWeather.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugin/Weather_d/GetWeather.py` around lines 34 - 38, Protect the shared
_lastCheck rate-limit state in both getTodayWeather and getFutureWeather with a
module-level asyncio.Lock. Acquire the lock around each method’s wait loop and
_lastCheck update so concurrent requests serialize their timing checks while
preserving the existing delay behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@config.yaml`:
- Around line 1-2: Remove the exposed clientSecret and any other real
credentials from config.yaml, revoke and rotate the leaked QQ Gateway secret,
and replace repository values with placeholders or environment-variable
references. Update .gitignore to exclude config.yaml so local credentials cannot
be committed again.

In `@modules/HermesWss/chunked_upload.py`:
- Around line 385-387: Update the retry-delay condition in the complete-upload
retry loop around the visible backoff calculation so it also sleeps before the
final retry attempt. Match the boundary behavior used by _upload_single_part and
_PART_UPLOAD_MAX_RETRIES, while preserving the existing exponential delay
calculation.

In `@modules/HermesWss/wss_client.py`:
- Line 56: Replace stdlib logging throughout HermesWss with loguru: in
modules/HermesWss/wss_client.py:56-56, sender.py:32-32, onboard.py:38-38,
keyboards.py:25-25, and chunked_upload.py:30-30, remove each logging import and
getLogger setup and import the module-level logger from loguru; in
onboard.py:38-38 also replace the print block with appropriate logger calls
while preserving its messages.
- Around line 311-317: Update the WebSocket connection setup in the surrounding
ws_connect flow to apply CONNECT_TIMEOUT_SECONDS to the connection-establishment
timeout rather than only the ws_close timeout. Configure the aiohttp timeout
parameter that bounds connect setup, while preserving the existing gateway_url,
headers, and close-timeout behavior.

In `@plugin/AiChat_d/Config.py`:
- Around line 6-18: Replace the unsafe yaml.load call in the Config
configuration-loading flow with yaml.safe_load, and replace the TypedDict Config
definition with a Pydantic model that validates the configuration fields and
types. Instantiate that model from the safely loaded YAML data while preserving
the existing Config field names and Paths.CONFIG_FILE source.

In `@plugin/AiChat_d/config.yaml`:
- Line 1: Remove the hardcoded value from the apiKey configuration in
config.yaml and replace it with the project’s supported environment-variable
reference or a non-secret placeholder. Revoke the exposed API key through the
provider, without adding another secret to the repository.

In `@plugin/AiChat_d/History.py`:
- Line 3: Replace the wildcard import from plugin.AiChat_d.Sql in History.py
with explicit imports for only the Sql symbols used by the module, preserving
existing behavior and avoiding namespace collisions.

In `@plugin/AiChat_d/Sql.py`:
- Around line 147-156: Update the truncation logic in the message cleanup block
to preserve the oldest system prompt at all times: when deleting messages beyond
keep_count from the newest-first all_msgs list, exclude the final element (index
-1) from the deletion slice while still removing excess intermediate messages.

In `@plugin/AiChat_d/sql/init.sql`:
- Around line 1-17: The SQL schema and queries must match the ORM models in
Sql.py. In plugin/AiChat_d/sql/init.sql lines 1-17, rename users/messages to
user/message, rename open_id/updated_at/time to openId/latestChat/createdAt, and
add messageCount to user; update plugin/AiChat_d/sql/message/add.sql lines 1-8
to use message/user with openId/latestChat, plugin/AiChat_d/sql/message/get.sql
lines 1-3 to use message with openId/createdAt, and
plugin/AiChat_d/sql/user/add.sql lines 1 and plugin/AiChat_d/sql/user/check.sql
lines 1-2 to use user and openId.

In `@plugin/AiChat_d/sql/message/clear.sql`:
- Line 1: Update the DELETE statement in the clear SQL script to use valid
DELETE FROM syntax and target the messages table name defined by init.sql, while
preserving the open_id filter.

In `@plugin/AiChat_d/sql/user/del.sql`:
- Line 1: Update the DELETE statement in the user deletion SQL to use valid
DELETE FROM syntax, and replace the positional placeholder with the named
open_id parameter format used by the surrounding SQL files.

In `@plugin/AiChat_d/Types.py`:
- Around line 1-39: Replace the TypedDict definitions in Types.py with Pydantic
BaseModel classes, preserving the existing fields, annotations, nesting, and
defaults or required-field behavior. Convert ApiJson_Messages, ApiJson,
ReplyJson_Choices_Message, ReplyJson_Choices, and ReplyJson consistently, using
BaseModel-compatible nested model types.

In `@plugin/AiChat.py`:
- Line 40: Update the union_id assignment to use safe dictionary lookups for
both author fields, preserving the fallback from union_openid to id without
raising KeyError when either key is absent.

---

Outside diff comments:
In `@plugin/Holiday_d/RequApi.py`:
- Around line 7-33: Convert updataCache and checkAndUpdata in RequApi.py to
async functions, perform requests through httpx.AsyncClient with awaited calls,
and preserve the existing retry, caching, and logging behavior. Update the
Holiday.run caller to await RequApi.checkAndUpdata() before setting _checked.

---

Nitpick comments:
In `@CLAUDE.md`:
- Line 30: Specify the text language on the fenced code block openings in
CLAUDE.md (line 30) and modules/HermesWss/README.md (line 28) by adding the text
identifier; no other content changes are needed.

In `@modules/HermesWss/models.py`:
- Around line 404-407: Update the callback type aliases PostMessageFn,
OnMessageCallback, and OnInteractionCallback to use Coroutine[Any, Any,
ReturnType] or Awaitable[ReturnType] instead of treating
asyncio.AbstractEventLoop as the coroutine yield type. Add or reuse the
appropriate Any import and preserve each alias’s existing parameter and return
types.

In `@plugin/Weather_d/GetWeather.py`:
- Around line 34-38: Protect the shared _lastCheck rate-limit state in both
getTodayWeather and getFutureWeather with a module-level asyncio.Lock. Acquire
the lock around each method’s wait loop and _lastCheck update so concurrent
requests serialize their timing checks while preserving the existing delay
behavior.

In `@plugin/Weather.py`:
- Around line 75-103: Update the `/天气预报` and `/常住地` branches in the message
handler to pass the `target` returned by `_reply_target(message)` to
`HttpSender.sendText2Person` instead of `author["id"]`. Preserve the existing
group-message behavior and response content.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8caceef4-9ff3-4524-b0c1-63cb946d1d72

📥 Commits

Reviewing files that changed from the base of the PR and between 1ec11c1 and ef05ca0.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (72)
  • .github/workflows/black-format.yml
  • .github/workflows/codeql.yml
  • .gitignore
  • .python-version
  • .sourcery.yml
  • .vscode/launch.json
  • .vscode/settings.json
  • CLAUDE.md
  • LICENSE
  • QBot.code-workspace
  • README.md
  • config.yaml
  • main.py
  • modules/AutoAuth.py
  • modules/Config.py
  • modules/HermesWss/README.md
  • modules/HermesWss/__init__.py
  • modules/HermesWss/chunked_upload.py
  • modules/HermesWss/constants.py
  • modules/HermesWss/crypto.py
  • modules/HermesWss/exceptions.py
  • modules/HermesWss/keyboards.py
  • modules/HermesWss/models.py
  • modules/HermesWss/onboard.py
  • modules/HermesWss/sender.py
  • modules/HermesWss/token_manager.py
  • modules/HermesWss/utils.py
  • modules/HermesWss/wss_client.py
  • modules/HttpSender.py
  • modules/Types.py
  • modules/Urls.py
  • plugin/AiChat.py
  • plugin/AiChat_d/.gitignore
  • plugin/AiChat_d/Config.py
  • plugin/AiChat_d/History.py
  • plugin/AiChat_d/Paths.py
  • plugin/AiChat_d/RequApi.py
  • plugin/AiChat_d/Sql.py
  • plugin/AiChat_d/Types.py
  • plugin/AiChat_d/config.yaml
  • plugin/AiChat_d/data/personality/xy.md
  • plugin/AiChat_d/sql/init.sql
  • plugin/AiChat_d/sql/message/add.sql
  • plugin/AiChat_d/sql/message/clear.sql
  • plugin/AiChat_d/sql/message/get.sql
  • plugin/AiChat_d/sql/user/add.sql
  • plugin/AiChat_d/sql/user/check.sql
  • plugin/AiChat_d/sql/user/del.sql
  • plugin/GlmAi.py
  • plugin/GlmAi_d/Config.py
  • plugin/GlmAi_d/config.yaml
  • plugin/Holiday.py
  • plugin/Holiday_d/.gitignore
  • plugin/Holiday_d/RequApi.py
  • plugin/Holiday_d/SqlCache.py
  • plugin/Holiday_d/test.http
  • plugin/PluginBase.py
  • plugin/TestAutoReply.py
  • plugin/Weather.py
  • plugin/Weather_d/.gitignore
  • plugin/Weather_d/GetWeather.py
  • plugin/Weather_d/SqlCache.py
  • plugin/Weather_d/config.yaml
  • plugin/Weather_d/test.http
  • plugin/__init__.py
  • pyproject.toml
  • run.sh
  • test/ai.json
  • test/aiChat.py
  • test/passCallback.py
  • test/recieveHttp.py
  • test/testSqlmodel.py
💤 Files with no reviewable changes (3)
  • plugin/GlmAi_d/config.yaml
  • plugin/GlmAi_d/Config.py
  • plugin/GlmAi.py
🛑 Comments failed to post (13)
config.yaml (1)

1-2: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Do not commit real credentials to the repository.

The clientSecret appears to be a real, sensitive API key for your QQ Gateway application. Hardcoding secrets in the repository poses a critical security risk. Please revoke and rotate this secret immediately in the QQ Open Platform. Replace the values in config.yaml with placeholder strings, and ensure config.yaml is added to .gitignore to prevent future leaks (or load credentials from environment variables).

🛡️ Proposed fix to remove secrets
-appId: "1905194016"
-clientSecret: OgyHbvGcyLj8XxOpHkEiDjGnLuT3eGsV
+appId: "YOUR_APP_ID"
+clientSecret: "YOUR_CLIENT_SECRET"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

appId: "YOUR_APP_ID"
clientSecret: "YOUR_CLIENT_SECRET"
🧰 Tools
🪛 Betterleaks (1.6.1)

[high] 2-2: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@config.yaml` around lines 1 - 2, Remove the exposed clientSecret and any
other real credentials from config.yaml, revoke and rotate the leaked QQ Gateway
secret, and replace repository values with placeholders or environment-variable
references. Update .gitignore to exclude config.yaml so local credentials cannot
be committed again.

Source: Linters/SAST tools

modules/HermesWss/chunked_upload.py (1)

385-387: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Backoff skips the delay before the final retry.

With _COMPLETE_UPLOAD_MAX_RETRIES = 2 the loop runs attempts 0,1,2, but attempt < _COMPLETE_UPLOAD_MAX_RETRIES - 1 (i.e. < 1) sleeps only after attempt 0. The retry from attempt 1 → 2 fires immediately with no backoff. This diverges from _upload_single_part (Line 332), which correctly uses attempt < _PART_UPLOAD_MAX_RETRIES.

🐛 Proposed fix
-            if attempt < _COMPLETE_UPLOAD_MAX_RETRIES - 1:
+            if attempt < _COMPLETE_UPLOAD_MAX_RETRIES:
                 delay: float = _COMPLETE_UPLOAD_BASE_DELAY * (2.0 ** attempt)
                 await asyncio.sleep(delay)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

            if attempt < _COMPLETE_UPLOAD_MAX_RETRIES:
                delay: float = _COMPLETE_UPLOAD_BASE_DELAY * (2.0 ** attempt)
                await asyncio.sleep(delay)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/HermesWss/chunked_upload.py` around lines 385 - 387, Update the
retry-delay condition in the complete-upload retry loop around the visible
backoff calculation so it also sleeps before the final retry attempt. Match the
boundary behavior used by _upload_single_part and _PART_UPLOAD_MAX_RETRIES,
while preserving the existing exponential delay calculation.
modules/HermesWss/wss_client.py (2)

56-56: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Replace standard-library logging (and print) with loguru across HermesWss. Repository guidelines require loguru for logging output and disallow both print and the stdlib logging module. Each module currently creates a logging.getLogger(__name__) logger; replace the import/logger setup with from loguru import logger and drop the getLogger call (loguru exposes a module-level logger).

  • modules/HermesWss/wss_client.py#L56-L56: remove import logging (Line 22) and logger = logging.getLogger(__name__); use from loguru import logger.
  • modules/HermesWss/sender.py#L32-L32: remove import logging (Line 13) and the getLogger call; use from loguru import logger.
  • modules/HermesWss/onboard.py#L38-L38: remove import logging (Line 19) and the getLogger call; also convert the print(...) block (Lines 278-283) to logger calls.
  • modules/HermesWss/keyboards.py#L25-L25: remove import logging (Line 9) and the getLogger call; use from loguru import logger.
  • modules/HermesWss/chunked_upload.py#L30-L30: remove import logging (Line 17) and the getLogger call; use from loguru import logger.
📍 Affects 5 files
  • modules/HermesWss/wss_client.py#L56-L56 (this comment)
  • modules/HermesWss/sender.py#L32-L32
  • modules/HermesWss/onboard.py#L38-L38
  • modules/HermesWss/keyboards.py#L25-L25
  • modules/HermesWss/chunked_upload.py#L30-L30
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/HermesWss/wss_client.py` at line 56, Replace stdlib logging
throughout HermesWss with loguru: in modules/HermesWss/wss_client.py:56-56,
sender.py:32-32, onboard.py:38-38, keyboards.py:25-25, and
chunked_upload.py:30-30, remove each logging import and getLogger setup and
import the module-level logger from loguru; in onboard.py:38-38 also replace the
print block with appropriate logger calls while preserving its messages.

Source: Coding guidelines


311-317: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

aiohttp ClientWSTimeout ws_close ws_receive fields meaning ws_connect timeout

💡 Result:

In aiohttp, ClientWSTimeout is a dataclass used to configure timeout settings specifically for WebSocket connections [1][2]. It is passed as the timeout argument to the ws_connect coroutine [1][3]. The fields of ClientWSTimeout are defined as follows: ws_receive: A float (or None) representing the maximum time allowed to receive a complete message [1][2]. It defaults to None, meaning no timeout [1]. ws_close: A float (or None) representing the maximum time allowed for the WebSocket to perform a closing handshake [1][2]. It defaults to 10.0 seconds [1][4]. When using ws_connect, these timeouts manage the behavior of the returned ClientWebSocketResponse object [1][2]. The default configuration is a ClientWSTimeout object with ws_receive=None and ws_close=10.0 [1][4]. Note that the timeout parameter in ws_connect is distinct from the general ClientTimeout structure used for standard HTTP requests [2]. While earlier versions of aiohttp accepted a float for the timeout parameter in ws_connect (which would be used for ws_close), this is now deprecated in favor of explicitly using the ClientWSTimeout class [3][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## wss_client.py around the reported lines\n'
sed -n '250,360p' modules/HermesWss/wss_client.py

printf '\n## timeout-related symbols in modules/HermesWss/wss_client.py\n'
rg -n "ClientTimeout|ClientWSTimeout|ws_connect|CONNECT_TIMEOUT_SECONDS|timeout=" modules/HermesWss/wss_client.py

printf '\n## session creation in modules/HermesWss/wss_client.py\n'
rg -n "_ws_session|ClientSession|Timeout" modules/HermesWss/wss_client.py

Repository: dudu7615/QBot

Length of output: 5422


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## aiohttp docs in repository references\n'
rg -n "ClientWSTimeout|ws_close|ws_receive|ws_connect" -g '!**/.git/**' .

Repository: dudu7615/QBot

Length of output: 468


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "### file context"
wc -l modules/HermesWss/wss_client.py
sed -n '290,340p' modules/HermesWss/wss_client.py

echo "### session / timeout references"
rg -n "_ws_session|ClientSession|ClientTimeout|ClientWSTimeout|ws_connect|CONNECT_TIMEOUT_SECONDS" modules/HermesWss/wss_client.py

echo "### nearby websocket code"
sed -n '1,220p' modules/HermesWss/wss_client.py

Repository: dudu7615/QBot

Length of output: 9448


CONNECT_TIMEOUT_SECONDS doesn't cap WebSocket establishment. ClientWSTimeout(ws_close=...) only covers the close handshake, so this ws_connect call is still governed by the session's timeout defaults. If the goal is to bound connect setup, apply a connect/session timeout here instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/HermesWss/wss_client.py` around lines 311 - 317, Update the WebSocket
connection setup in the surrounding ws_connect flow to apply
CONNECT_TIMEOUT_SECONDS to the connection-establishment timeout rather than only
the ws_close timeout. Configure the aiohttp timeout parameter that bounds
connect setup, while preserving the existing gateway_url, headers, and
close-timeout behavior.
plugin/AiChat_d/Config.py (1)

6-18: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Use yaml.safe_load and define data models with Pydantic.

yaml.load with FullLoader is susceptible to arbitrary code execution vulnerabilities. Additionally, the coding guidelines mandate the use of Pydantic for data models instead of TypedDict.

🛡️ Proposed fix with Pydantic validation
-from typing import TypedDict
+from pydantic import BaseModel
 import yaml
 from plugin.AiChat_d import Paths
 
 
-class Config(TypedDict):
+class ConfigModel(BaseModel):
     apiKey: str
     url: str
     model: str
     personality: str
     max_context_length: int  # 超过此消息数时触发上下文截断
     keep_recent: int  # 截断后保留的最新消息数(含 system prompt)
 
 
-config: Config = yaml.load(
-    open(Paths.CONFIG_FILE, "r", encoding="utf8"),
-    Loader=yaml.FullLoader,
-)
+with open(Paths.CONFIG_FILE, "r", encoding="utf8") as f:
+    config = ConfigModel(**yaml.safe_load(f)).model_dump()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

class ConfigModel(BaseModel):
    apiKey: str
    url: str
    model: str
    personality: str
    max_context_length: int  # 超过此消息数时触发上下文截断
    keep_recent: int  # 截断后保留的最新消息数(含 system prompt)


with open(Paths.CONFIG_FILE, "r", encoding="utf8") as f:
    config = ConfigModel(**yaml.safe_load(f)).model_dump()
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 15-15: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(Paths.CONFIG_FILE, "r", encoding="utf8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🪛 OpenGrep (1.25.0)

[ERROR] 15-18: yaml.load() without SafeLoader can execute arbitrary Python code. Use yaml.safe_load() or yaml.load(..., Loader=SafeLoader) instead.

(coderabbit.deserialization.python-yaml-unsafe-load)

🪛 Ruff (0.15.21)

[warning] 12-12: Comment contains ambiguous ( (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF003)


[warning] 12-12: Comment contains ambiguous ) (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF003)


[error] 17-17: Probable use of unsafe loader FullLoader with yaml.load. Allows instantiation of arbitrary objects. Consider yaml.safe_load.

(S506)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugin/AiChat_d/Config.py` around lines 6 - 18, Replace the unsafe yaml.load
call in the Config configuration-loading flow with yaml.safe_load, and replace
the TypedDict Config definition with a Pydantic model that validates the
configuration fields and types. Instantiate that model from the safely loaded
YAML data while preserving the existing Config field names and Paths.CONFIG_FILE
source.

Sources: Coding guidelines, Linters/SAST tools

plugin/AiChat_d/config.yaml (1)

1-1: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Remove hardcoded API key.

The file contains a hardcoded API key (sk-3PNdKhLiSokJybbnW4O9aER1IZjiQ8lNYJS9c9SBAWjYRxUW). Hardcoding secrets in the codebase poses a severe security risk, even for development configuration files. Please remove this key and use an environment variable or a placeholder instead, and ensure the exposed key is revoked.

🛡️ Proposed fix
-apiKey: sk-3PNdKhLiSokJybbnW4O9aER1IZjiQ8lNYJS9c9SBAWjYRxUW
+apiKey: "YOUR_API_KEY_HERE"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

apiKey: "YOUR_API_KEY_HERE"
🧰 Tools
🪛 Betterleaks (1.6.1)

[high] 1-1: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugin/AiChat_d/config.yaml` at line 1, Remove the hardcoded value from the
apiKey configuration in config.yaml and replace it with the project’s supported
environment-variable reference or a non-secret placeholder. Revoke the exposed
API key through the provider, without adding another secret to the repository.

Source: Linters/SAST tools

plugin/AiChat_d/History.py (1)

3-3: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace wildcard import with explicit imports.

Wildcard imports make it difficult to trace dependencies and can cause namespace collisions.

♻️ Proposed fix
-from plugin.AiChat_d.Sql import *
+from plugin.AiChat_d.Sql import Message, MessageRole, User
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

from plugin.AiChat_d.Sql import Message, MessageRole, User
🧰 Tools
🪛 Ruff (0.15.21)

[error] 3-3: from plugin.AiChat_d.Sql import * used; unable to detect undefined names

(F403)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugin/AiChat_d/History.py` at line 3, Replace the wildcard import from
plugin.AiChat_d.Sql in History.py with explicit imports for only the Sql symbols
used by the module, preserving existing behavior and avoiding namespace
collisions.
plugin/AiChat_d/Sql.py (1)

147-156: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Prevent the system prompt from being deleted during truncation.

The messages are sorted by createdAt DESC (newest first), which means the system prompt (the oldest message) is located at the end of the all_msgs list. Deleting all_msgs[keep_count:] removes the oldest messages, inherently deleting the vital system prompt. Since messageCount will not be zero during the next call, the prompt will never be reloaded, permanently breaking the bot's persona for the user.

Adjust the slice bounds to preserve both the most recent messages and the system prompt (the absolute oldest message at index -1).

🐛 Proposed fix to preserve the system prompt
             all_msgs: list[Message] = list(
                 session.exec(
                     select(Message)
                     .where(Message.openId == openId)
                     .order_by(text("createdAt DESC"))
                 )
             )
             if len(all_msgs) > keep_count:
-                for msg in all_msgs[keep_count:]:
+                # Delete everything except the first keep_count-1 (newest) and the last 1 (system prompt)
+                to_delete = all_msgs[keep_count - 1 : -1] if keep_count > 1 else all_msgs[:-1]
+                for msg in to_delete:
                     session.delete(msg)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugin/AiChat_d/Sql.py` around lines 147 - 156, Update the truncation logic
in the message cleanup block to preserve the oldest system prompt at all times:
when deleting messages beyond keep_count from the newest-first all_msgs list,
exclude the final element (index -1) from the deletion slice while still
removing excess intermediate messages.
plugin/AiChat_d/sql/init.sql (1)

1-17: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Schema mismatch between SQL definitions and ORM models.
The raw SQL files use table and column names that do not match the ORM definitions in Sql.py (user, message, openId, latestChat, createdAt). Running these raw SQL queries alongside the ORM will result in missing table or column errors.

  • plugin/AiChat_d/sql/init.sql#L1-L17: Align the table names (users -> user, messages -> message) and column names (open_id -> openId, updated_at -> latestChat, time -> createdAt) to match the Sql.py models. Also, ensure the messageCount column is added to the user table.
  • plugin/AiChat_d/sql/message/add.sql#L1-L8: Update to use message and user tables, and openId, latestChat columns.
  • plugin/AiChat_d/sql/message/get.sql#L1-L3: Update to use the message table, and openId, createdAt columns.
  • plugin/AiChat_d/sql/user/add.sql#L1-L1: Update to use the user table and openId column.
  • plugin/AiChat_d/sql/user/check.sql#L1-L2: Update to use the user table and openId column.
📍 Affects 5 files
  • plugin/AiChat_d/sql/init.sql#L1-L17 (this comment)
  • plugin/AiChat_d/sql/message/add.sql#L1-L8
  • plugin/AiChat_d/sql/message/get.sql#L1-L3
  • plugin/AiChat_d/sql/user/add.sql#L1-L1
  • plugin/AiChat_d/sql/user/check.sql#L1-L2
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugin/AiChat_d/sql/init.sql` around lines 1 - 17, The SQL schema and queries
must match the ORM models in Sql.py. In plugin/AiChat_d/sql/init.sql lines 1-17,
rename users/messages to user/message, rename open_id/updated_at/time to
openId/latestChat/createdAt, and add messageCount to user; update
plugin/AiChat_d/sql/message/add.sql lines 1-8 to use message/user with
openId/latestChat, plugin/AiChat_d/sql/message/get.sql lines 1-3 to use message
with openId/createdAt, and plugin/AiChat_d/sql/user/add.sql lines 1 and
plugin/AiChat_d/sql/user/check.sql lines 1-2 to use user and openId.
plugin/AiChat_d/sql/message/clear.sql (1)

1-1: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Fix syntax error in DELETE statement.

The SQL syntax DELETE * FROM is invalid. The correct syntax is DELETE FROM. Also, note that init.sql names the table messages, not message.

🐛 Proposed fix
-DELETE * FROM message WHERE open_id = :open_id;
+DELETE FROM messages WHERE open_id = :open_id;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

DELETE FROM messages WHERE open_id = :open_id;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugin/AiChat_d/sql/message/clear.sql` at line 1, Update the DELETE statement
in the clear SQL script to use valid DELETE FROM syntax and target the messages
table name defined by init.sql, while preserving the open_id filter.
plugin/AiChat_d/sql/user/del.sql (1)

1-1: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Fix syntax error in DELETE statement.

The SQL syntax DELETE * FROM is invalid. The correct syntax is DELETE FROM. Also, consider using named parameters (e.g., :open_id) to stay consistent with the other SQL files.

🐛 Proposed fix
-DELETE * FROM users WHERE open_id = ?;
+DELETE FROM users WHERE open_id = :open_id;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

DELETE FROM users WHERE open_id = :open_id;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugin/AiChat_d/sql/user/del.sql` at line 1, Update the DELETE statement in
the user deletion SQL to use valid DELETE FROM syntax, and replace the
positional placeholder with the named open_id parameter format used by the
surrounding SQL files.
plugin/AiChat_d/Types.py (1)

1-39: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use Pydantic for data models instead of TypedDict.

As per coding guidelines, define data models and data types with Pydantic. Please convert these TypedDict definitions to Pydantic BaseModel classes.

♻️ Proposed refactor to use Pydantic
-from typing import TypedDict
+from pydantic import BaseModel
 
 
-class ApiJson_Messages(TypedDict):
+class ApiJson_Messages(BaseModel):
     role: str  # system, user, assistant, tool
     content: str
 
 
-class ApiJson(TypedDict):
+class ApiJson(BaseModel):
     model: str  # glm-4.5-flash
     messages: list[ApiJson_Messages]
     temperature: float  # 1
     max_tokens: int  # 65536
     stream: bool  # False
     thinking: dict[str, str]  # {"type": "enabled"|"disabled"}
     do_sample: bool  # True
     top_p: float  # 0.95
 
 
-class ReplyJson_Choices_Message(ApiJson_Messages): ...
+class ReplyJson_Choices_Message(ApiJson_Messages):
+    pass
 
 
-class ReplyJson_Choices(TypedDict):
+class ReplyJson_Choices(BaseModel):
     index: int
     message: ReplyJson_Choices_Message
     finish_reason: str
 
 
-class ReplyJson(TypedDict):
+class ReplyJson(BaseModel):
     id: str
     object: str
     created: int
     model: str
     choices: list[ReplyJson_Choices]
     usage: dict[str, int]
 
     time: str
     role: str
     content: str
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

from pydantic import BaseModel


class ApiJson_Messages(BaseModel):
    role: str  # system, user, assistant, tool
    content: str


class ApiJson(BaseModel):
    model: str  # glm-4.5-flash
    messages: list[ApiJson_Messages]
    temperature: float  # 1
    max_tokens: int  # 65536
    stream: bool  # False
    thinking: dict[str, str]  # {"type": "enabled"|"disabled"}
    do_sample: bool  # True
    top_p: float  # 0.95


class ReplyJson_Choices_Message(ApiJson_Messages):
    pass


class ReplyJson_Choices(BaseModel):
    index: int
    message: ReplyJson_Choices_Message
    finish_reason: str


class ReplyJson(BaseModel):
    id: str
    object: str
    created: int
    model: str
    choices: list[ReplyJson_Choices]
    usage: dict[str, int]

    time: str
    role: str
    content: str
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 1-1: The file is executable but no shebang is present

(EXE002)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugin/AiChat_d/Types.py` around lines 1 - 39, Replace the TypedDict
definitions in Types.py with Pydantic BaseModel classes, preserving the existing
fields, annotations, nesting, and defaults or required-field behavior. Convert
ApiJson_Messages, ApiJson, ReplyJson_Choices_Message, ReplyJson_Choices, and
ReplyJson consistently, using BaseModel-compatible nested model types.

Source: Coding guidelines

plugin/AiChat.py (1)

40-40: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use .get() for safe dictionary access.

Directly accessing author["union_openid"] will raise a KeyError if the key is missing from the event payload. Use .get() to ensure a safe fallback to author["id"].

🛠️ Proposed fix
-        union_id = author["union_openid"] or author["id"]
+        union_id = author.get("union_openid") or author.get("id")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        union_id = author.get("union_openid") or author.get("id")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugin/AiChat.py` at line 40, Update the union_id assignment to use safe
dictionary lookups for both author fields, preserving the fallback from
union_openid to id without raising KeyError when either key is absent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant