Skip to content

Conversation

@LIghtJUNction
Copy link
Member

@LIghtJUNction LIghtJUNction commented Nov 2, 2025

.env文件保存统一的路径
静态文件一起打包

fixes #XYZ


Motivation / 动机

Modifications / 改动点

Verification Steps / 验证步骤

Screenshots or Test Results / 运行截图或测试结果

Compatibility & Breaking Changes / 兼容性与破坏性变更

  • 这是一个破坏性变更 (Breaking Change)。/ This is a breaking change.
  • 这不是一个破坏性变更。/ This is NOT a breaking change.

Checklist / 检查清单

  • 😊 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。/ If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
  • 👀 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”。/ My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
  • 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 requirements.txtpyproject.toml 文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
  • 😮 我的更改没有引入恶意代码。/ My changes do not introduce malicious code.

Sourcery 总结

引入统一的 AstrbotPaths 类进行路径管理,弃用旧的路径工具函数,采用 importlib.resources 加载静态资源,并通过 .env 文件集中化配置。

新功能:

  • 添加 AstrbotPaths 类,带有 IAstrbotPaths 抽象基类,用于集中化和结构化的路径处理

改进:

  • 弃用旧的 get_astrbot_* 函数和 CLI 路径检查,转而使用 AstrbotPaths 并附带弃用警告
  • 将模板和示例资源的加载切换到 importlib.resources,以实现更安全的包资源访问
  • 更新 AstrBotUpdator,在解压更新时使用新的 AstrbotPaths 根路径

构建:

  • pyproject.toml 中添加 dotenv 依赖,用于基于环境的配置

文档:

  • 提供 .env.env.example 文件,用于管理 ASTRBOT_ROOT
Original summary in English

Summary by Sourcery

Introduce a unified AstrbotPaths class for path management and deprecate legacy path utility functions, adopt importlib.resources for static asset loading, and centralize configuration via a .env file.

New Features:

  • Add AstrbotPaths class with IAstrbotPaths abstract base for centralized and structured path handling

Enhancements:

  • Deprecate old get_astrbot_* functions and CLI path checks in favor of AstrbotPaths with deprecation warnings
  • Switch loading of templates and sample assets to importlib.resources for safer package resource access
  • Update AstrBotUpdator to use the new AstrbotPaths root when unpacking updates

Build:

  • Add dotenv dependency in pyproject.toml for environment-based configuration

Documentation:

  • Provide .env and .env.example files for managing ASTRBOT_ROOT

Sourcery 总结

通过引入 AstrbotPaths 类来统一和集中路径管理,废弃旧的实用函数,更新代码库以使用新的路径 API,通过 .env 配置环境,并为新的路径管理器添加全面的测试。

新功能:

  • 引入 AstrbotPaths 类及 IAstrbotPaths 接口,用于统一和结构化的路径管理
  • AstrbotPaths 添加全面的测试,涵盖初始化、属性、上下文管理器和单例行为

改进:

  • 废弃旧的 get_astrbot_* 实用函数和 CLI 路径检查,转而使用 AstrbotPaths 并附带废弃警告
  • 将核心、CLI 和平台适配器中手动路径连接和硬编码目录替换为 AstrbotPaths
  • 采用 importlib.resources 加载静态包资产,并更新仪表板 CLI 以使用集中式的、通过环境变量配置的根目录
  • 将主应用程序启动重构到 astrbot/__main__.py 中,并简化 CLI 和 Python 模块的入口点

构建:

  • pyproject.toml 中添加 dotenv 依赖,并提供 .env/.env.example 文件用于集中化 ASTRBOT_ROOT 配置
  • pyproject.toml 中引入严格的 mypy 配置

测试:

  • 添加 tests/test_paths.py 用于测试 AstrbotPaths 的行为和模块隔离
Original summary in English

Summary by Sourcery

Unify and centralize path management by introducing AstrbotPaths class, deprecate old utility functions, update codebase to use the new path API, configure environment via .env, and add comprehensive tests for the new path manager.

New Features:

  • Introduce AstrbotPaths class with IAstrbotPaths interface for unified and structured path management
  • Add comprehensive tests for AstrbotPaths covering initialization, properties, context managers, and singleton behavior

Enhancements:

  • Deprecate legacy get_astrbot_* utility functions and CLI path checks in favor of AstrbotPaths with deprecation warnings
  • Replace manual path joins and hardcoded directories across core, CLI, and platform adapters with AstrbotPaths usage
  • Adopt importlib.resources for loading static package assets and update dashboard CLI to use centralized env-configured root
  • Refactor main application startup into astrbot/main.py and simplify entrypoints for CLI and Python module

Build:

  • Add dotenv dependency to pyproject.toml and provide .env/.env.example for centralizing ASTRBOT_ROOT configuration
  • Introduce strict mypy configuration in pyproject.toml

Tests:

  • Add tests/test_paths.py for AstrbotPaths behavior and module isolation

.env文件保存统一的路径
静态文件一起打包而不是从上上上级目录找...

Co-Authored-By: 赵天乐(tyler zhao) <[email protected]>
@auto-assign auto-assign bot requested review from Fridemn and Larch-C November 2, 2025 14:48
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `astrbot/dashboard/routes/config.py:464-466` </location>
<code_context>
-                    get_astrbot_path(),
-                    "samples",
-                    "stt_health_check.wav",
+                sample_audio_path = str(
+                    importlib.resources.files("astrbot")
+                    / "samples"
+                    / "stt_health_check.wav"
                 )
</code_context>

<issue_to_address>
**issue (bug_risk):** Switching to importlib.resources for file access may break if the resource is not packaged.

Confirm that 'samples/stt_health_check.wav' is packaged and accessible via importlib.resources, or add error handling for missing resources.
</issue_to_address>

### Comment 2
<location> `astrbot/base/paths.py:34-40` </location>
<code_context>
+    @classmethod
+    def getPaths(cls, name: str) -> AstrbotPaths:
+        """返回Paths实例,用于访问模块的各类目录."""
+        normalized_name: NormalizedName = canonicalize_name(name)
+        instance: AstrbotPaths = cls(normalized_name)
+        instance.name = normalized_name
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Using canonicalize_name for directory names may introduce unexpected normalization.

Verify that canonicalize_name does not cause naming conflicts or break compatibility with existing directory naming conventions used by plugins or modules.

```suggestion
    @classmethod
    def getPaths(cls, name: str) -> AstrbotPaths:
        """返回Paths实例,用于访问模块的各类目录."""
        instance: AstrbotPaths = cls(name)
        instance.name = name
        return instance
```
</issue_to_address>

### Comment 3
<location> `astrbot/base/paths.py:45-46` </location>
<code_context>
+    @property
+    def root(self) -> Path:
+        """返回根目录."""
+        return (
+            self.astrbot_root if self.astrbot_root.exists() else Path.cwd() / ".astrbot"
+        )
+
</code_context>

<issue_to_address>
**issue (bug_risk):** Fallback to Path.cwd()/.astrbot may not be expected.

Switching to the working directory if astrbot_root is missing may cause unexpected results. Instead, raise an error or notify the user to ensure clarity.
</issue_to_address>

### Comment 4
<location> `astrbot/base/paths.py:21` </location>
<code_context>
+    from collections.abc import AsyncGenerator, Generator
+
+
+class AstrbotPaths(IAstrbotPaths):
+    """Class to manage and provide paths used by Astrbot Canary."""
+
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the class to use helper functions and unified logic for path properties, directory changes, and environment reloads to reduce boilerplate and nested abstractions.

```markdown
You can significantly cut down boilerplate and nested abstractions by

  1. removing `getPaths` and normalizing the name in `__init__`  
  2. generating all “mkdir + return Path” properties from one helper  
  3. unifying sync/async `chdir` logic  
  4. moving the env-reload logic into a small helper

For example:

```python
from contextlib import contextmanager, asynccontextmanager
from pathlib import Path
from os import chdir, getenv
from dotenv import load_dotenv
from packaging.utils import canonicalize_name

def _path_property(subdir: str):
    @property
    def prop(self) -> Path:
        p = self.astrbot_root / subdir / self.name
        p.mkdir(parents=True, exist_ok=True)
        return p
    return prop

class AstrbotPaths:
    home   = _path_property("home")
    config = _path_property("config")
    data   = _path_property("data")
    log    = _path_property("logs")

    def __init__(self, name: str):
        # normalize once
        self.name = canonicalize_name(name)
        load_dotenv()  # ensure env is loaded
        self.astrbot_root = Path(
            getenv("ASTRBOT_ROOT", Path.home() / ".astrbot")
        ).absolute()
        self.astrbot_root.mkdir(parents=True, exist_ok=True)

    @contextmanager
    def chdir(self, key: str = "home"):
        original = Path.cwd()
        target = getattr(self, key)
        chdir(target)
        try:
            yield target
        finally:
            chdir(original)

    @asynccontextmanager
    async def achdir(self, key: str = "home"):
        # reuse the sync chdir under the hood
        with self.chdir(key) as target:
            yield target

    def reload_env(self) -> None:
        load_dotenv()
        self.astrbot_root = Path(
            getenv("ASTRBOT_ROOT", Path.home() / ".astrbot")
        ).absolute()
```

Key benefits:

- **No more** duplicated `mkdir` blocks: one helper covers all subdirs.  
- **Drop** `getPaths` and duplicate `.name` sets; `__init__` does normalization.  
- **Single** sync `chdir`, async version just wraps it.  
- **Env-reload** is now a focused `reload_env()` method.  
- **All** original behavior remains intact.
```
</issue_to_address>

### Comment 5
<location> `astrbot/base/paths.py:89-91` </location>
<code_context>
    @classmethod
    def is_root(cls, path: Path) -> bool:
        """检查路径是否为 Astrbot 根目录."""
        if not path.exists() or not path.is_dir():
            return False
        # 检查此目录内是是否包含.astrbot标记文件
        if not (path / ".astrbot").exists():
            return False
        return True

</code_context>

<issue_to_address>
**suggestion (code-quality):** We've found these issues:

- Lift code into else after jump in control flow ([`reintroduce-else`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/reintroduce-else/))
- Replace if statement with if expression ([`assign-if-exp`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/assign-if-exp/))
- Simplify boolean if expression ([`boolean-if-exp-identity`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/boolean-if-exp-identity/))

```suggestion
        return bool((path / ".astrbot").exists())
```
</issue_to_address>

### Comment 6
<location> `astrbot/core/pipeline/respond/stage.py:152` </location>
<code_context>
    async def process(
        self,
        event: AstrMessageEvent,
    ) -> None:
        result = event.get_result()
        if result is None:
            return
        if result.result_content_type == ResultContentType.STREAMING_FINISH:
            return

        logger.info(
            f"Prepare to send - {event.get_sender_name()}/{event.get_sender_id()}: {event._outline_chain(result.chain)}",
        )

        if result.result_content_type == ResultContentType.STREAMING_RESULT:
            if result.async_stream is None:
                logger.warning("async_stream 为空,跳过发送。")
                return
            # 流式结果直接交付平台适配器处理
            use_fallback = self.config.get("provider_settings", {}).get(
                "streaming_segmented",
                False,
            )
            logger.info(f"应用流式输出({event.get_platform_id()})")
            await event.send_streaming(result.async_stream, use_fallback)
            return
        if len(result.chain) > 0:
            # 检查路径映射
            if mappings := self.platform_settings.get("path_mapping", []):
                for idx, component in enumerate(result.chain):
                    if isinstance(component, Comp.File) and component.file:
                        # 支持 File 消息段的路径映射。
                        component.file = path_Mapping(mappings, component.file)
                        event.get_result().chain[idx] = component

            # 检查消息链是否为空
            try:
                if await self._is_empty_message_chain(result.chain):
                    logger.info("消息为空,跳过发送阶段")
                    return
            except Exception as e:
                logger.warning(f"空内容检查异常: {e}")

            # 将 Plain 为空的消息段移除
            result.chain = [
                comp
                for comp in result.chain
                if not (
                    isinstance(comp, Comp.Plain)
                    and (not comp.text or not comp.text.strip())
                )
            ]

            # 发送消息链
            # Record 需要强制单独发送
            need_separately = {ComponentType.Record}
            if self.is_seg_reply_required(event):
                header_comps = self._extract_comp(
                    result.chain,
                    {ComponentType.Reply, ComponentType.At},
                    modify_raw_chain=True,
                )
                if not result.chain or len(result.chain) == 0:
                    # may fix #2670
                    logger.warning(
                        f"实际消息链为空, 跳过发送阶段。header_chain: {header_comps}, actual_chain: {result.chain}",
                    )
                    return
                async with session_lock_manager.acquire_lock(event.unified_msg_origin):
                    for comp in result.chain:
                        i = await self._calc_comp_interval(comp)
                        await asyncio.sleep(i)
                        try:
                            if comp.type in need_separately:
                                await event.send(MessageChain([comp]))
                            else:
                                await event.send(MessageChain([*header_comps, comp]))
                                header_comps.clear()
                        except Exception as e:
                            logger.error(
                                f"发送消息链失败: chain = {MessageChain([comp])}, error = {e}",
                                exc_info=True,
                            )
            else:
                if all(
                    comp.type in {ComponentType.Reply, ComponentType.At}
                    for comp in result.chain
                ):
                    # may fix #2670
                    logger.warning(
                        f"消息链全为 Reply 和 At 消息段, 跳过发送阶段。chain: {result.chain}",
                    )
                    return
                sep_comps = self._extract_comp(
                    result.chain,
                    need_separately,
                    modify_raw_chain=True,
                )
                for comp in sep_comps:
                    chain = MessageChain([comp])
                    try:
                        await event.send(chain)
                    except Exception as e:
                        logger.error(
                            f"发送消息链失败: chain = {chain}, error = {e}",
                            exc_info=True,
                        )
                chain = MessageChain(result.chain)
                if result.chain and len(result.chain) > 0:
                    try:
                        await event.send(chain)
                    except Exception as e:
                        logger.error(
                            f"发送消息链失败: chain = {chain}, error = {e}",
                            exc_info=True,
                        )

        if await call_event_hook(event, EventType.OnAfterMessageSentEvent):
            return

        event.clear_result()

</code_context>

<issue_to_address>
**issue (code-quality):** We've found these issues:

- Remove redundant conditional [×2] ([`remove-redundant-if`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-redundant-if/))
- Simplify sequence length comparison [×2] ([`simplify-len-comparison`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/simplify-len-comparison/))
- Low code quality found in RespondStage.process - 12% ([`low-code-quality`](https://docs.sourcery.ai/Reference/Default-Rules/comments/low-code-quality/))

<br/><details><summary>Explanation</summary>



The quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.

How can you solve this?

It might be worth refactoring this function to make it shorter and more readable.

- Reduce the function length by extracting pieces of functionality out into
  their own functions. This is the most important thing you can do - ideally a
  function should be less than 10 lines.
- Reduce nesting, perhaps by introducing guard clauses to return early.
- Ensure that variables are tightly scoped, so that code using related concepts
  sits together within the function rather than being scattered.</details>
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@LIghtJUNction
Copy link
Member Author

我蓄谋已久的路径统一
旧函数弃用,调用处没改
本次pr完全不破坏兼容性
考虑到utils包日渐庞大,而其中的路径函数零零散散(与cli包不一致)
现新增base包(注意本包仅可单向导出)
base包为临时过渡方案,后续将迁移至更规范的位置

LIghtJUNction and others added 3 commits November 2, 2025 22:54
Co-Authored-By: 赵天乐(tyler zhao) <[email protected]>
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
@LIghtJUNction LIghtJUNction requested a review from Copilot November 2, 2025 16:07
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR refactors the AstrBot project's path management system by introducing a centralized AstrbotPaths class to replace scattered path utility functions. The refactoring moves core startup logic from main.py to astrbot/__main__.py and deprecates old path functions while maintaining backward compatibility.

Key changes:

  • Introduces a new astrbot.base package with AstrbotPaths class for unified path management using pathlib.Path
  • Replaces get_astrbot_data_path() and related functions throughout the codebase with AstrbotPaths.astrbot_root
  • Migrates VERSION string from hardcoded value to dynamic retrieval via importlib.metadata.version()

Reviewed Changes

Copilot reviewed 36 out of 37 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
pyproject.toml Added dotenv>=0.9.9 dependency for environment variable loading
astrbot/base/paths.py New centralized path management class with singleton pattern
astrbot/main.py Moved application entry point logic from main.py
main.py Simplified to delegate to astrbot.main.main()
astrbot/core/utils/astrbot_path.py Deprecated functions with warnings, delegating to AstrbotPaths
Multiple plugin/core files Updated to use AstrbotPaths instead of old path functions
astrbot/core/config/default.py VERSION now uses importlib.metadata.version()
astrbot/core/message/components.py Improved File component's .file property behavior

@LIghtJUNction LIghtJUNction requested a review from Copilot November 2, 2025 16:12
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Copilot reviewed 36 out of 37 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

astrbot/base/paths.py:1

  • The documentation message is confusing and contains an awkward example AstrbotPaths.getPaths('any!'). The proper usage should just be AstrbotPaths.astrbot_root as shown in the implementation. Update the message to: \"不要再使用本函数!实际上就是获取data目录!等效于: AstrbotPaths.astrbot_root\"
from __future__ import annotations

astrbot/base/paths.py:1

  • The deprecation warning message is unclear and contains incorrect syntax. AstrbotPaths.getPaths('any!').root.env is not valid - .env is not a property of Path. The message should simply state: \"等效于: AstrbotPaths.astrbot_root (环境变量 ASTRBOT_ROOT 默认值: ./data)\"
from __future__ import annotations

@LIghtJUNction
Copy link
Member Author

@copilot 你可以使用ruff format --check帮我格式化吗?创建一个pr帮我格式化一下。

Copy link
Contributor

Copilot AI commented Nov 2, 2025

@LIghtJUNction I've opened a new pull request, #3283, to work on those changes. Once the pull request is ready, I'll request review from you.

* Initial plan

* chore: 使用 ruff format 格式化代码

Co-authored-by: LIghtJUNction <[email protected]>

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: LIghtJUNction <[email protected]>
@LIghtJUNction
Copy link
Member Author

@copilot 创建一个pr,执行ruff check --fix,然后提交

Copy link
Contributor

Copilot AI commented Nov 2, 2025

@LIghtJUNction I've opened a new pull request, #3285, to work on those changes. Once the pull request is ready, I'll request review from you.

@LIghtJUNction
Copy link
Member Author

@copilot 运行ruff check --fix,并执行uv run main.py检查

Copy link
Contributor

Copilot AI commented Nov 2, 2025

@LIghtJUNction I've opened a new pull request, #3286, to work on those changes. Once the pull request is ready, I'll request review from you.

* Initial plan

* fix: 移除未使用的临时目录变量 (ruff check --fix)

Co-authored-by: LIghtJUNction <[email protected]>

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: LIghtJUNction <[email protected]>
@LIghtJUNction
Copy link
Member Author

@copilot 对路径类进行覆盖率测试,测试各种情况。覆盖率需达到100%

Copy link
Contributor

Copilot AI commented Nov 2, 2025

@LIghtJUNction I've opened a new pull request, #3287, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI and others added 2 commits November 3, 2025 03:06
* Initial plan

* feat: 为 AstrbotPaths 添加全面测试,覆盖率达到 100%

Co-authored-by: LIghtJUNction <[email protected]>

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: LIghtJUNction <[email protected]>
@LIghtJUNction
Copy link
Member Author

@copilot 使用ruff修复格式等错误,使用mypy检查类型错误

Copy link
Contributor

Copilot AI commented Nov 2, 2025

@LIghtJUNction I've opened a new pull request, #3289, to work on those changes. Once the pull request is ready, I'll request review from you.

@LIghtJUNction
Copy link
Member Author

@Dt8333 check

LIghtJUNction and others added 9 commits November 4, 2025 18:49
* Chore: Dockerfile (#3266)

* fix: Dockerfile

python main.py 改为uv run main.py

* fix(dockerfile): 减少重复安装

* fix: 修复一些细节问题

* fix(.dockerignore): 需要git文件夹以获取astrbot版本(带git commit hash后缀)

* fix(.dockerignore): uv run之前会uv sync

* Replace insecure random with secrets module in cryptographic contexts (#3248)

* Initial plan

* Security fixes: Replace insecure random with secrets module and improve SSL context

Co-authored-by: LIghtJUNction <[email protected]>

* Address code review feedback: fix POST method and add named constants

Co-authored-by: LIghtJUNction <[email protected]>

* Improve documentation for random number generation constants

Co-authored-by: LIghtJUNction <[email protected]>

* Update astrbot/core/utils/io.py

Co-authored-by: Copilot <[email protected]>

* Update astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py

Co-authored-by: Copilot <[email protected]>

* Update tests/test_security_fixes.py

Co-authored-by: Copilot <[email protected]>

* Update astrbot/core/utils/io.py

Co-authored-by: Copilot <[email protected]>

* Update astrbot/core/utils/io.py

Co-authored-by: Copilot <[email protected]>

* Fix: Handle path parameter in SSL fallback for download_image_by_url

Co-authored-by: LIghtJUNction <[email protected]>

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: LIghtJUNction <[email protected]>
Co-authored-by: LIghtJUNction <[email protected]>
Co-authored-by: Copilot <[email protected]>

* chore: nodejs in Dockerfile

* fix: typing error (#3267)

* fix: 修复一些小错误。

修复aiocqhttp和slack中部分逻辑缺失的await。修复discord中错误的异常捕获类型。

* fix(core.platform): 修复discord适配器中错误的message_chain赋值

* fix(aiocqhttp): 更新convert_message方法的返回类型为AstrBotMessage | None

---------

Co-authored-by: Soulter <[email protected]>

* feat: support options to delete plugins config and data (#3280)

* - 为插件管理页面中,删除插件提供一致的二次确认(原本只有卡片视图有二次确认)
- 二次确认时可选删除插件配置和持久化数据
- 添加对应的i18n支持

* ruff

* 移除未使用的
const $confirm = inject('$confirm');

---------

Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Soulter <[email protected]>
Co-authored-by: Dt8333 <[email protected]>
Co-authored-by: Soulter <[email protected]>
Co-authored-by: Misaka Mikoto <[email protected]>
@LIghtJUNction
Copy link
Member Author

我需要知道目前代码中,如何正确获得项目自身的根路径。(即pyproject.toml所在的路径)
先前版本中get_astrbot_path通过读取自己文件位置实现了这个功能。

这个保留吧,我发现目前最大的用途就是手动部署时更新源代码时候用得到,其余时候都不应该使用。

@LIghtJUNction LIghtJUNction marked this pull request as draft November 5, 2025 04:54
Copy link
Member

@Dt8333 Dt8333 left a comment

Choose a reason for hiding this comment

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

目前行为基本与先前一致。

astrbot plug new计划弃用(By LIghtJUNction) ,ignore。

@LIghtJUNction LIghtJUNction marked this pull request as ready for review November 5, 2025 15:50
@auto-assign auto-assign bot requested a review from anka-afk November 5, 2025 15:50
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • The dynamic VERSION calculation (subtracting one for dev builds) is brittle—consider centralizing version logic or simplifying it to avoid off-by-one mistakes.
  • Deprecation warnings on every call to legacy path functions will spam logs; consider throttling them or issuing a one-time migration notice instead.
  • The AstrbotPaths singleton pattern isn’t guarded against concurrent access—if you expect multi-threaded usage, add synchronization or document its thread-unsafe nature.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The dynamic VERSION calculation (subtracting one for dev builds) is brittle—consider centralizing version logic or simplifying it to avoid off-by-one mistakes.
- Deprecation warnings on every call to legacy path functions will spam logs; consider throttling them or issuing a one-time migration notice instead.
- The AstrbotPaths singleton pattern isn’t guarded against concurrent access—if you expect multi-threaded usage, add synchronization or document its thread-unsafe nature.

## Individual Comments

### Comment 1
<location> `astrbot/core/utils/pip_installer.py:24-29` </location>
<code_context>
         mirror: str | None = None,
-    ):
+    ) -> None:
+        if requirements_path:
+            cwd = Path(requirements_path).parent.resolve()
+        elif project_path:
+            cwd = Path(project_path).resolve()
+        else:
+            cwd = Path().cwd()  # 安装pip包时避免cwd变量未初始化
+
         args = ["install"]
</code_context>

<issue_to_address>
**suggestion:** cwd is set based on requirements_path or project_path, but fallback is Path().cwd().

Path().cwd() may not reflect the intended working directory for pip installs. Please consider making cwd selection more explicit or allowing it to be configured.

Suggested implementation:

```python
    async def install(
        self,
        package_name: str | None = None,
        requirements_path: str | None = None,
        project_path: str | None = None,
        mirror: str | None = None,
        cwd: str | Path | None = None,
    ) -> None:
        if cwd is not None:
            cwd_path = Path(cwd).resolve()
        elif requirements_path:
            cwd_path = Path(requirements_path).parent.resolve()
        elif project_path:
            cwd_path = Path(project_path).resolve()
        else:
            cwd_path = Path.cwd()  # fallback to current working directory if nothing is provided

        args = ["install"]

```

You will need to update any calls to `install()` elsewhere in your codebase to optionally pass the new `cwd` parameter if you want to explicitly control the working directory for pip installs.
Also, ensure that wherever `cwd` is used later in the function (e.g., in subprocess calls), you replace it with `cwd_path`.
</issue_to_address>

### Comment 2
<location> `tests/test_paths.py:190-196` </location>
<code_context>
+        assert paths.root == non_existent
+        assert non_existent.exists()
+
+    def test_root_property_fallback_to_cwd(
+        self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
+    ) -> None:
+        """测试 root 属性在根目录被删除后回退到 cwd/.astrbot."""
+        import shutil
+
+        # 创建并设置一个根目录
+        temp_root = tmp_path / "test_root"
+        temp_root.mkdir(parents=True, exist_ok=True)
+
+        # 清除实例缓存
+        AstrbotPaths._instances.clear()
+        AstrbotPaths.astrbot_root = temp_root
+
+        # 创建实例
+        paths = AstrbotPaths.getPaths("test-fallback")
+
+        # 删除根目录(模拟被外部删除的情况)
+        shutil.rmtree(temp_root)
+
+        # 现在访问 root 应该回退到 cwd/.astrbot
+        expected = Path.cwd() / ".astrbot"
+        assert paths.root == expected
+
+    def test_home_property(self, paths_instance: AstrbotPaths, temp_root: Path) -> None:
</code_context>

<issue_to_address>
**suggestion (testing):** Consider adding tests for permission errors and invalid path types.

Adding tests for permission errors and invalid path types will improve coverage of error handling and ensure the code behaves correctly in these scenarios.

```suggestion
    def test_home_property(self, paths_instance: AstrbotPaths, temp_root: Path) -> None:
        """测试 home 属性."""
        home_path = paths_instance.home
        expected = temp_root / "home" / paths_instance.name
        assert home_path == expected
        assert home_path.exists()
        assert home_path.is_dir()

    def test_root_permission_error(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """测试 root 属性在根目录权限不足时的行为."""
        import os
        import stat

        temp_root = tmp_path / "perm_root"
        temp_root.mkdir(parents=True, exist_ok=True)
        AstrbotPaths._instances.clear()
        AstrbotPaths.astrbot_root = temp_root

        # 移除所有权限
        temp_root.chmod(0)
        paths = AstrbotPaths.getPaths("test-perm")
        try:
            # 访问 root 属性应引发 PermissionError
            with pytest.raises(PermissionError):
                _ = paths.root.exists()
        finally:
            # 恢复权限,避免影响后续测试
            temp_root.chmod(stat.S_IRWXU)

    def test_root_invalid_path_type(self, tmp_path: Path) -> None:
        """测试 root 属性为文件而非目录时的行为."""
        temp_file = tmp_path / "not_a_dir"
        temp_file.write_text("not a directory")
        AstrbotPaths._instances.clear()
        AstrbotPaths.astrbot_root = temp_file

        paths = AstrbotPaths.getPaths("test-invalid-type")
        # 访问 root 属性应检测到不是目录
        assert paths.root == temp_file
        assert temp_file.is_file()
        # 可以根据实际 AstrbotPaths 行为补充断言,比如是否回退或报错
```
</issue_to_address>

### Comment 3
<location> `tests/test_paths.py:296-302` </location>
<code_context>
+
+        assert AstrbotPaths.is_root(temp_root) is False
+
+    def test_is_root_with_non_existent_path(self) -> None:
+        """测试不存在的路径."""
+        non_existent = Path("/definitely/not/exist/path")
+        assert AstrbotPaths.is_root(non_existent) is False
+
+    def test_is_root_with_file_not_directory(self, temp_root: Path) -> None:
</code_context>

<issue_to_address>
**suggestion (testing):** Missing test for is_root with symlinked directories.

Please add a test for AstrbotPaths.is_root with a symlink to a root directory to ensure correct handling of symlinks.

```suggestion
    def test_is_root_with_file_not_directory(self, temp_root: Path) -> None:
        """测试路径是文件而非目录."""
        test_file = temp_root / "test.txt"
        test_file.touch()

        assert AstrbotPaths.is_root(test_file) is False

    def test_is_root_with_symlink_to_root(self, tmp_path: Path) -> None:
        """测试符号链接指向根目录."""
        root_dir = tmp_path / "root"
        root_dir.mkdir()
        marker_file = root_dir / ".astrbot"
        marker_file.touch()
        symlink_dir = tmp_path / "symlink_to_root"
        symlink_dir.symlink_to(root_dir, target_is_directory=True)

        assert AstrbotPaths.is_root(symlink_dir) is True

    def test_is_root_with_symlink_to_non_root(self, tmp_path: Path) -> None:
        """测试符号链接指向非根目录."""
        non_root_dir = tmp_path / "non_root"
        non_root_dir.mkdir()
        symlink_dir = tmp_path / "symlink_to_non_root"
        symlink_dir.symlink_to(non_root_dir, target_is_directory=True)

        assert AstrbotPaths.is_root(symlink_dir) is False
```
</issue_to_address>

### Comment 4
<location> `astrbot/base/paths.py:21` </location>
<code_context>
+    from collections.abc import AsyncGenerator, Generator
+
+
+class AstrbotPaths(IAstrbotPaths):
+    """统一化路径获取."""
+
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring to use lru_cache, a helper for directory creation, and unified context manager logic to reduce boilerplate and indirection.

Here are a few ways to cut down on indirection and repeated boilerplate while keeping all of your functionality:

1. Replace manual instance‐cache with an `lru_cache` on your factory:  
```python
from functools import lru_cache

class AstrbotPaths(IAstrbotPaths):
    # … keep __init__, astrbot_root, load_dotenv, etc.

    @classmethod
    @lru_cache(maxsize=None)
    def get_paths(cls, name: str) -> AstrbotPaths:
        normalized = canonicalize_name(name)
        return cls(normalized)
```

2. Factor out all of the identical “mk-dir + return” logic into one helper:  
```python
class AstrbotPaths(IAstrbotPaths):
    #
    def _ensure(self, section: str) -> Path:
        p = self.astrbot_root / section / self.name
        p.mkdir(parents=True, exist_ok=True)
        return p

    @property
    def home(self) -> Path:
        return self._ensure("home")

    @property
    def config(self) -> Path:
        return self._ensure("config")

    # same for data, logs, temp, plugins…
```

3. Unify your sync/async context managers to share the same body:  
```python
from contextlib import contextmanager, asynccontextmanager

class AstrbotPaths(IAstrbotPaths):
    #

    @contextmanager
    def chdir(self, cwd: Path) -> Generator[Path, None, None]:
        original = Path.cwd()
        target   = self.root / cwd
        os.chdir(target)
        try:
            yield target
        finally:
            os.chdir(original)

    @asynccontextmanager
    async def achdir(self, cwd: Path) -> AsyncGenerator[Path, None]:
        with self.chdir(cwd) as target:
            yield target
```

These small refactors remove the manual cache dict, collapse six nearly‐identical properties into one helper, and let your async version simply wrap the sync one—all while preserving exactly the same behavior.
</issue_to_address>

### Comment 5
<location> `astrbot/core/utils/astrbot_path.py:16` </location>
<code_context>
+from astrbot.base import AstrbotPaths


 def get_astrbot_path() -> str:
-    """获取Astrbot项目路径"""
+    """获取Astrbot项目路径 --仅供手动部署时/更新源代码时使用.
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the repeated deprecation wrappers into a single factory function to centralize logic and avoid duplication.

Here’s how you can collapse all of these identical deprecation‐wrappers into a tiny factory (or `__getattr__`) so you don’t have to keep copy/pasting doc‐strings and `warnings.warn()` calls:

```python
import warnings
from astrbot.base import AstrbotPaths

_deprecated = {
    "get_astrbot_path": ("astrbot_path",
        "Only for manual source‐deploy/update.  Use AstrbotPaths.astrbot_root for most cases."),
    "get_astrbot_root": ("astrbot_root",
        "Use AstrbotPaths.astrbot_root."),
    "get_astrbot_data_path": ("astrbot_root",
        "Data dir is the same as AstrbotPaths.astrbot_root."),
    "get_astrbot_config_path": ("astrbot_root",
        "Use AstrbotPaths.astrbot_root / 'config'."),
    "get_astrbot_plugin_path": ("astrbot_root",
        "Use AstrbotPaths.astrbot_root / 'plugins'."),
}

def _make_shim(old_name, attr_name, hint):
    def shim() -> str:
        warnings.warn(
            f"{old_name} is deprecated, use AstrbotPaths.{attr_name}. {hint}",
            DeprecationWarning,
            stacklevel=2,
        )
        return str(getattr(AstrbotPaths, attr_name))
    shim.__name__ = old_name
    shim.__doc__ = f"DEPRECATED: {hint}"
    return shim

# populate module‐globals
for old, (attr, hint) in _deprecated.items():
    globals()[old] = _make_shim(old, attr, hint)
```

This keeps each stub 5 lines, centralizes the warning logic, and you never repeat yourself.
</issue_to_address>

### Comment 6
<location> `astrbot/core/message/components.py:658` </location>
<code_context>

     @property
     def file(self) -> str:
-        """获取文件路径,如果文件不存在但有URL,则同步下载文件
+        """
</code_context>

<issue_to_address>
**issue (complexity):** Consider removing synchronous download logic from the deprecated .file property and centralizing temp-file handling in a helper function to simplify the code.

Here are two concrete ways to cut down on the complexity without removing any existing public API:

1) **Drop all of the synchronous-download branches from `.file`**  
   Since you’re already deprecating it, just emit one warning and forward everybody to `get_file()`. That will collapse nearly the entire `if self.url: …` block into a single line:

```python
    @property
    def file(self) -> str:
        warnings.warn(
            "File.file is deprecated; use `await get_file()` instead",
            DeprecationWarning,
            stacklevel=2,
        )
        return self.file_ if self.file_ and os.path.exists(self.file_) else ""
```

2) **Extract the “download / temp‐file” logic into a single helper**  
   Almost every `async def …` (“convert_to_base64”, “convert_to_path”, “register_to_file_service” in all components) does:
   - make a temp dir
   - pick a UUID filename
   - download via URL, base64, or copy an existing file
   - return an absolute path

Move that to one private function, then each method becomes a trivial dispatcher:

```python
# utils.py
async def _fetch_to_temp(source: str, ext: str="") -> str:
    """
    If source is a file:// or existing local path, return abs path.
    If http:// URL, download via download_file().
    If base64://… decode and write to .jpg (or ext).
    """
    if source.startswith("file:///"):
        return source[len("file:///"):]
    tmp_root = AstrbotPaths.astrbot_root / "temp"
    os.makedirs(tmp_root, exist_ok=True)
    target = tmp_root / f"{uuid.uuid4().hex}{ext}"
    if source.startswith("http"):
        await download_file(source, target)
    elif source.startswith("base64://"):
        data = base64.b64decode(source.removeprefix("base64://"))
        target.write_bytes(data)
    elif os.path.exists(source):
        return os.path.abspath(source)
    else:
        raise ValueError(f"not a valid source: {source}")
    return str(target.resolve())
```

```python
# in your component
async def convert_to_path(self) -> str:
    return await _fetch_to_temp(self.file, ext="")
```

```python
async def convert_to_base64(self) -> str:
    # if you need base64 out, first fetch to path then:
    path = await _fetch_to_temp(self.file, ext=".jpg")
    return file_to_base64(path)
```

This removes almost all of the repeated branching and temp-dir boilerplate from the component methods, while keeping every existing public method signature exactly the same.
</issue_to_address>

### Comment 7
<location> `astrbot/core/updator.py:120-122` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Raise a specific error instead of the general `Exception` or `BaseException` ([`raise-specific-error`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/raise-specific-error))

<details><summary>Explanation</summary>If a piece of code raises a specific exception type
rather than the generic
[`BaseException`](https://docs.python.org/3/library/exceptions.html#BaseException)
or [`Exception`](https://docs.python.org/3/library/exceptions.html#Exception),
the calling code can:

- get more information about what type of error it is
- define specific exception handling for it

This way, callers of the code can handle the error appropriately.

How can you solve this?

- Use one of the [built-in exceptions](https://docs.python.org/3/library/exceptions.html) of the standard library.
- [Define your own error class](https://docs.python.org/3/tutorial/errors.html#tut-userexceptions) that subclasses `Exception`.

So instead of having code raising `Exception` or `BaseException` like

```python
if incorrect_input(value):
    raise Exception("The input is incorrect")
```

you can have code raising a specific error like

```python
if incorrect_input(value):
    raise ValueError("The input is incorrect")
```

or

```python
class IncorrectInputError(Exception):
    pass


if incorrect_input(value):
    raise IncorrectInputError("The input is incorrect")
```
</details>
</issue_to_address>

### Comment 8
<location> `astrbot/__main__.py:15` </location>
<code_context>
def check_env():
    if not (sys.version_info.major == 3 and sys.version_info.minor >= 10):
        logger.error("请使用 Python3.10+ 运行本项目。")
        exit()

    # os.makedirs("data/config", exist_ok=True)
    # os.makedirs("data/plugins", exist_ok=True)
    # os.makedirs("data/temp", exist_ok=True)

    # 针对问题 #181 的临时解决方案
    mimetypes.add_type("text/javascript", ".js")
    mimetypes.add_type("text/javascript", ".mjs")
    mimetypes.add_type("application/json", ".json")

</code_context>

<issue_to_address>
**suggestion (code-quality):** Simplify logical expression using De Morgan identities ([`de-morgan`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/de-morgan/))

```suggestion
    if sys.version_info.major != 3 or sys.version_info.minor < 10:
```
</issue_to_address>

### Comment 9
<location> `astrbot/cli/commands/cmd_init.py:33` </location>
<code_context>
async def initialize_astrbot(astrbot_root: Path) -> None:
    """执行 AstrBot 初始化逻辑"""
    dot_astrbot = astrbot_root / ".astrbot"

    if not dot_astrbot.exists():
        click.echo(f"Current Directory: {astrbot_root}")
        click.echo(
            "如果你确认这是 Astrbot root directory, 你需要在当前目录下创建一个 .astrbot 文件标记该目录为 AstrBot 的数据目录。",
        )
        if click.confirm(
            f"请检查当前目录是否正确,确认正确请回车: {astrbot_root}",
            default=True,
            abort=True,
        ):
            dot_astrbot.touch()
            click.echo(f"Created {dot_astrbot}")

    paths = {
        "config": astrbot_root / "config",
        "plugins": astrbot_root / "plugins",
        "temp": astrbot_root / "temp",
    }

    for _, path in paths.items():
        path.mkdir(parents=True, exist_ok=True)
        click.echo(f"{'Created' if not path.exists() else 'Directory exists'}: {path}")

    await check_dashboard(astrbot_root)

</code_context>

<issue_to_address>
**suggestion (code-quality):** Replace calls to `dict.items` with `dict.values` when the keys are not used ([`replace-dict-items-with-values`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/replace-dict-items-with-values/))

```suggestion
    for path in paths.values():
```
</issue_to_address>

### Comment 10
<location> `astrbot/core/pipeline/respond/stage.py:152` </location>
<code_context>
    async def process(
        self,
        event: AstrMessageEvent,
    ) -> None:
        result = event.get_result()
        if result is None:
            return
        if result.result_content_type == ResultContentType.STREAMING_FINISH:
            return

        logger.info(
            f"Prepare to send - {event.get_sender_name()}/{event.get_sender_id()}: {event._outline_chain(result.chain)}",
        )

        if result.result_content_type == ResultContentType.STREAMING_RESULT:
            if result.async_stream is None:
                logger.warning("async_stream 为空,跳过发送。")
                return
            # 流式结果直接交付平台适配器处理
            use_fallback = self.config.get("provider_settings", {}).get(
                "streaming_segmented",
                False,
            )
            logger.info(f"应用流式输出({event.get_platform_id()})")
            await event.send_streaming(result.async_stream, use_fallback)
            return
        if len(result.chain) > 0:
            # 检查路径映射
            if mappings := self.platform_settings.get("path_mapping", []):
                for idx, component in enumerate(result.chain):
                    if isinstance(component, Comp.File) and component.file:
                        # 支持 File 消息段的路径映射。
                        component.file = path_Mapping(mappings, component.file)
                        event.get_result().chain[idx] = component

            # 检查消息链是否为空
            try:
                if await self._is_empty_message_chain(result.chain):
                    logger.info("消息为空,跳过发送阶段")
                    return
            except Exception as e:
                logger.warning(f"空内容检查异常: {e}")

            # 将 Plain 为空的消息段移除
            result.chain = [
                comp
                for comp in result.chain
                if not (
                    isinstance(comp, Comp.Plain)
                    and (not comp.text or not comp.text.strip())
                )
            ]

            # 发送消息链
            # Record 需要强制单独发送
            need_separately = {ComponentType.Record}
            if self.is_seg_reply_required(event):
                header_comps = self._extract_comp(
                    result.chain,
                    {ComponentType.Reply, ComponentType.At},
                    modify_raw_chain=True,
                )
                if not result.chain or len(result.chain) == 0:
                    # may fix #2670
                    logger.warning(
                        f"实际消息链为空, 跳过发送阶段。header_chain: {header_comps}, actual_chain: {result.chain}",
                    )
                    return
                async with session_lock_manager.acquire_lock(event.unified_msg_origin):
                    for comp in result.chain:
                        i = await self._calc_comp_interval(comp)
                        await asyncio.sleep(i)
                        try:
                            if comp.type in need_separately:
                                await event.send(MessageChain([comp]))
                            else:
                                await event.send(MessageChain([*header_comps, comp]))
                                header_comps.clear()
                        except Exception as e:
                            logger.error(
                                f"发送消息链失败: chain = {MessageChain([comp])}, error = {e}",
                                exc_info=True,
                            )
            else:
                if all(
                    comp.type in {ComponentType.Reply, ComponentType.At}
                    for comp in result.chain
                ):
                    # may fix #2670
                    logger.warning(
                        f"消息链全为 Reply 和 At 消息段, 跳过发送阶段。chain: {result.chain}",
                    )
                    return
                sep_comps = self._extract_comp(
                    result.chain,
                    need_separately,
                    modify_raw_chain=True,
                )
                for comp in sep_comps:
                    chain = MessageChain([comp])
                    try:
                        await event.send(chain)
                    except Exception as e:
                        logger.error(
                            f"发送消息链失败: chain = {chain}, error = {e}",
                            exc_info=True,
                        )
                chain = MessageChain(result.chain)
                if result.chain and len(result.chain) > 0:
                    try:
                        await event.send(chain)
                    except Exception as e:
                        logger.error(
                            f"发送消息链失败: chain = {chain}, error = {e}",
                            exc_info=True,
                        )

        if await call_event_hook(event, EventType.OnAfterMessageSentEvent):
            return

        event.clear_result()

</code_context>

<issue_to_address>
**issue (code-quality):** We've found these issues:

- Remove redundant conditional [×2] ([`remove-redundant-if`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-redundant-if/))
- Simplify sequence length comparison [×2] ([`simplify-len-comparison`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/simplify-len-comparison/))
- Low code quality found in RespondStage.process - 12% ([`low-code-quality`](https://docs.sourcery.ai/Reference/Default-Rules/comments/low-code-quality/))

<br/><details><summary>Explanation</summary>



The quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.

How can you solve this?

It might be worth refactoring this function to make it shorter and more readable.

- Reduce the function length by extracting pieces of functionality out into
  their own functions. This is the most important thing you can do - ideally a
  function should be less than 10 lines.
- Reduce nesting, perhaps by introducing guard clauses to return early.
- Ensure that variables are tightly scoped, so that code using related concepts
  sits together within the function rather than being scattered.</details>
</issue_to_address>

### Comment 11
<location> `astrbot/core/updator.py:100-108` </location>
<code_context>
    def _generate_update_instruction(
        self, latest: bool = True, version: str | None = None
    ) -> str:
        """私有辅助函数

        Args:
            latest: 是否更新到最新版本
            version: 目标版本号,如果 latest=True 则忽略

        Returns:
            str: 更新指令字符串
        """
        if latest:
            pip_cmd = "pip install git+https://github.com/AstrBotDevs/AstrBot.git"
            uv_cmd = "uv tool upgrade astrbot"
        else:
            if version:
                pip_cmd = f"pip install git+https://github.com/AstrBotDevs/AstrBot.git@{version}"
                uv_cmd = f"uv tool install --force git+https://github.com/AstrBotDevs/AstrBot.git@{version} astrbot"
            else:
                raise ValueError("当 latest=False 时,必须提供 version")

        return (
            "命令行启动时,请直接使用uv tool upgrade astrbot更新\n"
            f"或者使用此命令更新: {pip_cmd}"
            f"使用uv: {uv_cmd}"
        )

</code_context>

<issue_to_address>
**issue (code-quality):** We've found these issues:

- Merge else clause's nested if statement into elif ([`merge-else-if-into-elif`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/merge-else-if-into-elif/))
- Swap if/else branches [×2] ([`swap-if-else-branches`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/swap-if-else-branches/))
</issue_to_address>

### Comment 12
<location> `astrbot/core/utils/pip_installer.py:47` </location>
<code_context>
    async def install(
        self,
        package_name: str | None = None,
        requirements_path: str | None = None,
        project_path: str | None = None,
        mirror: str | None = None,
    ) -> None:
        if requirements_path:
            cwd = Path(requirements_path).parent.resolve()
        elif project_path:
            cwd = Path(project_path).resolve()
        else:
            cwd = Path().cwd()  # 安装pip包时避免cwd变量未初始化

        args = ["install"]
        if package_name:
            args.append(package_name)
        elif requirements_path:
            args.extend(["-r", requirements_path])
        elif project_path:
            args.extend(".")

        index_url = mirror or self.pypi_index_url or "https://pypi.org/simple"

        args.extend(["--trusted-host", "mirrors.aliyun.com", "-i", index_url])

        if self.pip_install_arg:
            args.extend(self.pip_install_arg.split())

        logger.info(f"Pip 包管理器: pip {' '.join(args)}")
        try:
            async with self.paths.achdir(cwd):
                process = await asyncio.create_subprocess_exec(
                    sys.executable,
                    "-m",
                    "pip",
                    *args,
                    stdout=asyncio.subprocess.PIPE,
                    stderr=asyncio.subprocess.STDOUT,
                )

            assert process.stdout is not None
            async for line in process.stdout:
                logger.info(line.decode().strip())

            await process.wait()

            if process.returncode != 0:
                raise Exception(f"安装失败,错误码:{process.returncode}")
        except FileNotFoundError:
            # 没有 pip
            from pip import main as pip_main

            result_code = await asyncio.to_thread(pip_main, args)

            # 清除 pip.main 导致的多余的 logging handlers
            for handler in logging.root.handlers[:]:
                logging.root.removeHandler(handler)

            if result_code != 0:
                raise Exception(f"安装失败,错误码:{result_code}")

</code_context>

<issue_to_address>
**issue (code-quality):** Explicitly raise from a previous error ([`raise-from-previous-error`](https://docs.sourcery.ai/Reference/Default-Rules/suggestions/raise-from-previous-error/))
</issue_to_address>

### Comment 13
<location> `tests/test_paths.py:507-510` </location>
<code_context>
    def test_singleton_pattern_thread_safe(self, temp_root: Path) -> None:
        """测试单例模式的基本行为(注意:不是真正的线程安全测试)."""
        instances = []
        for _ in range(10):
            instances.append(AstrbotPaths.getPaths("singleton-test"))

        # 所有实例应该是同一个对象
        first = instances[0]
        for instance in instances[1:]:
            assert instance is first

</code_context>

<issue_to_address>
**suggestion (code-quality):** Convert for loop into list comprehension ([`list-comprehension`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/list-comprehension/))

```suggestion
        instances = [AstrbotPaths.getPaths("singleton-test") for _ in range(10)]
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

LIghtJUNction and others added 2 commits November 5, 2025 23:56
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
@LIghtJUNction
Copy link
Member Author

LIghtJUNction commented Nov 5, 2025

@Soulter 最后再请求下你的意见
引入了一个路径管理类
sample文件移动到源代码文件夹内一起打包
版本号获取方式改为使用标准库动态获取项目版本号
并对.dev后缀的开发版本号进行特殊处理(获取稳定版版本号,用于构造下载链接)
将main.py的主体移动到源代码内一起打包,可以这样运行:
python -m astrbot
保留main.py,不破坏兼容性
pip安装器逻辑新增一个可选参数,project_path
之后安装使用pip install .命令,优先从pyprojec.toml解析依赖。

安装插件依赖时用上下文管理器,将cwd临时切换到插件目录

@LIghtJUNction LIghtJUNction marked this pull request as draft November 6, 2025 17:32
@LIghtJUNction LIghtJUNction deleted the chore/uni-paths branch November 7, 2025 07:16
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.

4 participants