feat: 插件管理#2097
Conversation
总体概览此PR引入了完整的插件导入管理系统,包括后端服务层和前端UI界面。新增PluginImportService类处理ZIP及目录插件的导入、预览、删除操作,同时添加了SettingPluginInterface UI模块供用户管理第三方插件,并将其集成至应用设置界面。 变更清单
序列图sequenceDiagram
actor User as 用户
participant UI as SettingPluginInterface
participant Dialog as 文件对话框
participant Service as PluginImportService
participant FS as 文件系统
participant Context as OneDragonContext
User->>UI: 点击导入ZIP
activate UI
UI->>Dialog: 打开文件选择
Dialog-->>UI: 返回ZIP路径
UI->>Service: preview_plugin(zip_path)
activate Service
Service->>FS: 读取ZIP内容
Service->>Service: 解析*_const.py
Service-->>UI: PluginPreviewInfo
deactivate Service
UI->>User: 显示预览&确认对话框
User->>UI: 确认导入
UI->>Service: import_plugin(zip_path)
activate Service
Service->>FS: 验证ZIP结构
Service->>FS: 检查覆盖冲突
Service->>FS: 解压插件到目标目录
Service-->>UI: ImportResult
deactivate Service
alt 导入成功
UI->>Context: 更新third_party_plugins
UI->>UI: 刷新插件卡片列表
UI->>User: 显示成功提示
else 版本冲突
UI->>User: 显示覆盖/降级警告
end
deactivate UI
代码审查工作量评估🎯 4 (复杂) | ⏱️ ~50 分钟 兔子的庆祝诗
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/one_dragon/base/operation/application/plugin_import_service.py`:
- Around line 143-168: plugin_dir_name and extraction currently trust ZIP
entries and can escape plugins_dir; update _get_plugin_dir_name to normalize and
sanitize the candidate name (strip leading slashes, reject or collapse any '..'
segments) and ensure it returns a safe single directory basename, then before
using it compute target_dir = (self.plugins_dir / plugin_dir_name).resolve() and
verify target_dir.is_relative_to(self.plugins_dir.resolve()) (or compare parent
prefixes) to prevent path traversal; similarly, in _extract_plugin validate each
member path by joining to target_dir, resolving, and ensuring the resolved path
remains under plugins_dir before writing, and refuse/skip any absolute or '..'
entries to avoid deleting or writing outside the intended directory (also guard
the overwrite rmtree by verifying target_dir is inside plugins_dir).
- Around line 154-169: The code currently deletes the existing plugin directory
(target_dir) before extracting new contents, which can cause data loss if
extraction fails; change the logic in the import flow (the block around
target_dir, plugin_dir_name, overwrite and calls to self._extract_plugin) to
extract into a temporary directory first (e.g., tmp_target_dir), perform all
validation/checks there, and only after successful extraction/verification
atomically replace the old plugin directory (move/rename tmp_target_dir over
target_dir or remove old and rename tmp_target_dir) to avoid losing the
currently installed version; apply the same pattern to the other similar block
referenced (the second rmtree usage around lines 379-396) so both code paths use
temp extraction + atomic swap instead of rmtree before extraction.
- Around line 205-214: The import validation currently only checks for
*_factory.py (has_factory) and returns ImportResult; add a parallel hard-check
for the plugin const module (e.g., compute has_const =
any(name.endswith('_const.py') for name in file_list)) and if missing return
ImportResult(success=False, plugin_name="", message="无效的插件结构:缺少 *_const.py 文件");
apply the same change to the other validation/preview branches that mirror this
logic (the other ImportResult return sites in this module) so ZIP/dir import and
preview all enforce *_const.py presence, since
application_factory_manager._read_plugin_metadata requires the const module and
will raise ImportError otherwise.
- Around line 457-481: The current parent-directory check on plugin_dir is
bypassable and allows deleting the plugins root; fix by normalizing paths before
checks: call plugin_dir_resolved = Path(plugin_dir).resolve(strict=False) and
plugins_dir_resolved = self.plugins_dir.resolve(strict=False), use
plugin_dir_resolved.name for plugin_name, then reject if not
plugin_dir_resolved.is_relative_to(plugins_dir_resolved) or if
plugin_dir_resolved == plugins_dir_resolved (i.e., disallow deleting the root);
only after these checks call shutil.rmtree(plugin_dir_resolved) to remove the
directory.
In `@src/zzz_od/gui/view/setting/setting_plugin_interface.py`:
- Around line 323-336: The installed_plugins map uses p.app_id as key but later
code (ImportResult.plugin_name / result.plugin_name from
plugin_import_service.preview_plugin) supplies plugin directory names, so
lookups miss matches causing downgrade/overwrite checks to be skipped; change
the map key to the plugin directory/name used by the import service (use
whatever property on p represents the directory/name — e.g., p.plugin_name or
p.dir_name) so installed_plugins = { <directory-key>: p for p in
self.ctx.factory_manager.third_party_plugins }, then keep the existing logic
that uses preview = self.plugin_import_service.preview_plugin(fp), new_ver =
preview.version, old_ver = installed_plugins.get(r.plugin_name, None),
is_downgrade = self._is_version_lower(...) so comparisons use the same
identifier for installed_plugins, overwrite_info, and the
ImportResult.plugin_name/result.plugin_name lookups.
- Around line 53-56: The PluginCard currently creates homepage_btn only if
plugin_info.homepage exists, causing recycled cards to miss the button; always
create self.homepage_btn (connected to _on_homepage_clicked) in the PluginCard
initialization and remove the conditional creation, then in the card
update/refresh method (the routine that reapplies plugin_info when cards are
reused) set self.homepage_btn.setVisible(bool(plugin_info.homepage)) and update
any related state (e.g., tooltip or URL) so the button appears or hides
correctly when a card is reused for a plugin that does or does not have a
homepage.
- Around line 38-44: The constructors PluginCard.__init__ and
SettingPluginInterface.__init__ are missing precise type annotations: replace
the built-in callable with collections.abc.Callable, annotate parent as
Optional[Any] (or Optional[QWidget] if using PyQt/PySide), and add explicit
return type -> None; import Optional and Any from typing and Callable from
collections.abc and update the callback parameters to use Callable[..., Any] (or
more specific signatures like Callable[[], None] if known); apply the same
changes to the other __init__ overload at lines ~121-133 so all constructors
have complete modern Python 3.11+ annotations.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 257533ae-0984-42b9-8f32-50fde7bc6cd3
📒 Files selected for processing (3)
src/one_dragon/base/operation/application/plugin_import_service.pysrc/zzz_od/gui/view/setting/app_setting_interface.pysrc/zzz_od/gui/view/setting/setting_plugin_interface.py
| plugin_dir_name = self._get_plugin_dir_name(zf) | ||
| if not plugin_dir_name: | ||
| return ImportResult( | ||
| success=False, | ||
| plugin_name=zip_path.stem, | ||
| message="无法确定插件目录名" | ||
| ) | ||
|
|
||
| target_dir = self.plugins_dir / plugin_dir_name | ||
|
|
||
| # 检查目录是否已存在 | ||
| if target_dir.exists(): | ||
| if overwrite: | ||
| # 覆盖安装:先删除旧目录 | ||
| shutil.rmtree(target_dir) | ||
| log.info(f"覆盖安装: 已删除旧插件目录 {plugin_dir_name}") | ||
| else: | ||
| return ImportResult( | ||
| success=False, | ||
| plugin_name=plugin_dir_name, | ||
| message=f"插件目录已存在: {plugin_dir_name}", | ||
| plugin_dir=target_dir | ||
| ) | ||
|
|
||
| # 解压文件 | ||
| self._extract_plugin(zf, target_dir, plugin_dir_name) |
There was a problem hiding this comment.
ZIP 条目路径未净化,当前实现可以写出 plugins/ 之外。
这里把归档里的首段路径直接当成 plugin_dir_name,后面又直接拿它参与 target_dir 计算、覆盖删除和解压写盘。只要 ZIP 内包含绝对路径或 ../ 片段,就能把文件写到插件目录外;覆盖分支下甚至可能删错目录。
Also applies to: 283-336
🧰 Tools
🪛 Ruff (0.15.5)
[warning] 156-156: Comment contains ambiguous : (FULLWIDTH COLON). Did you mean : (COLON)?
(RUF003)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/one_dragon/base/operation/application/plugin_import_service.py` around
lines 143 - 168, plugin_dir_name and extraction currently trust ZIP entries and
can escape plugins_dir; update _get_plugin_dir_name to normalize and sanitize
the candidate name (strip leading slashes, reject or collapse any '..' segments)
and ensure it returns a safe single directory basename, then before using it
compute target_dir = (self.plugins_dir / plugin_dir_name).resolve() and verify
target_dir.is_relative_to(self.plugins_dir.resolve()) (or compare parent
prefixes) to prevent path traversal; similarly, in _extract_plugin validate each
member path by joining to target_dir, resolving, and ensuring the resolved path
remains under plugins_dir before writing, and refuse/skip any absolute or '..'
entries to avoid deleting or writing outside the intended directory (also guard
the overwrite rmtree by verifying target_dir is inside plugins_dir).
| if target_dir.exists(): | ||
| if overwrite: | ||
| # 覆盖安装:先删除旧目录 | ||
| shutil.rmtree(target_dir) | ||
| log.info(f"覆盖安装: 已删除旧插件目录 {plugin_dir_name}") | ||
| else: | ||
| return ImportResult( | ||
| success=False, | ||
| plugin_name=plugin_dir_name, | ||
| message=f"插件目录已存在: {plugin_dir_name}", | ||
| plugin_dir=target_dir | ||
| ) | ||
|
|
||
| # 解压文件 | ||
| self._extract_plugin(zf, target_dir, plugin_dir_name) | ||
|
|
There was a problem hiding this comment.
覆盖安装先删旧目录会把失败场景变成数据丢失。
Line 157 和 Line 381 都是在新内容完成解压/复制之前先 rmtree() 旧插件。后续只要出现一次 I/O 或校验异常,用户当前能用的版本就已经没了。这里应该先导入到临时目录,成功后再替换目标目录。
Also applies to: 379-396
🧰 Tools
🪛 Ruff (0.15.5)
[warning] 156-156: Comment contains ambiguous : (FULLWIDTH COLON). Did you mean : (COLON)?
(RUF003)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/one_dragon/base/operation/application/plugin_import_service.py` around
lines 154 - 169, The code currently deletes the existing plugin directory
(target_dir) before extracting new contents, which can cause data loss if
extraction fails; change the logic in the import flow (the block around
target_dir, plugin_dir_name, overwrite and calls to self._extract_plugin) to
extract into a temporary directory first (e.g., tmp_target_dir), perform all
validation/checks there, and only after successful extraction/verification
atomically replace the old plugin directory (move/rename tmp_target_dir over
target_dir or remove old and rename tmp_target_dir) to avoid losing the
currently installed version; apply the same pattern to the other similar block
referenced (the second rmtree usage around lines 379-396) so both code paths use
temp extraction + atomic swap instead of rmtree before extraction.
| # 检查是否有 factory 文件 | ||
| has_factory = any(name.endswith('_factory.py') for name in file_list) | ||
| if not has_factory: | ||
| return ImportResult( | ||
| success=False, | ||
| plugin_name="", | ||
| message="无效的插件结构:缺少 *_factory.py 文件" | ||
| ) | ||
|
|
||
| return ImportResult(success=True, plugin_name="", message="") |
There was a problem hiding this comment.
导入校验还没有把 *_const.py 当成硬约束。
现在 ZIP/目录导入和预览都只检查 *_factory.py,缺少 *_const.py 的插件仍会被当成可导入对象。按当前加载器的实现,这类插件在后续注册时会直接加载失败,最后变成“导入成功但插件不可用”。
🧩 最小修正方向
- has_factory = any(name.endswith('_factory.py') for name in file_list)
- if not has_factory:
+ has_factory = any(name.endswith('_factory.py') for name in file_list)
+ has_const = any(name.endswith('_const.py') for name in file_list)
+ if not (has_factory and has_const):
return ImportResult(
success=False,
plugin_name="",
- message="无效的插件结构:缺少 *_factory.py 文件"
+ message="无效的插件结构:缺少 *_factory.py 或 *_const.py 文件"
)目录导入和两个 preview 分支也需要做同样的硬校验。
Based on learnings: 在 src/one_dragon/base/operation/application/application_factory_manager.py 中,插件的 const 模块(*_const.py)是必需的。如果插件缺少对应的 const 模块,_read_plugin_metadata 方法将抛出 ImportError,导致该插件加载失败,这是预期的设计行为。
Also applies to: 235-252, 366-373, 425-446
🧰 Tools
🪛 Ruff (0.15.5)
[warning] 211-211: String contains ambiguous : (FULLWIDTH COLON). Did you mean : (COLON)?
(RUF001)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/one_dragon/base/operation/application/plugin_import_service.py` around
lines 205 - 214, The import validation currently only checks for *_factory.py
(has_factory) and returns ImportResult; add a parallel hard-check for the plugin
const module (e.g., compute has_const = any(name.endswith('_const.py') for name
in file_list)) and if missing return ImportResult(success=False, plugin_name="",
message="无效的插件结构:缺少 *_const.py 文件"); apply the same change to the other
validation/preview branches that mirror this logic (the other ImportResult
return sites in this module) so ZIP/dir import and preview all enforce
*_const.py presence, since application_factory_manager._read_plugin_metadata
requires the const module and will raise ImportError otherwise.
| if isinstance(plugin_dir, str): | ||
| # 如果只是目录名,构建完整路径 | ||
| if not Path(plugin_dir).is_absolute(): | ||
| plugin_dir = self.plugins_dir / plugin_dir | ||
|
|
||
| plugin_dir = Path(plugin_dir) | ||
| plugin_name = plugin_dir.name | ||
|
|
||
| if not plugin_dir.exists(): | ||
| return ImportResult( | ||
| success=False, | ||
| plugin_name=plugin_name, | ||
| message=f"插件目录不存在: {plugin_name}" | ||
| ) | ||
|
|
||
| # 安全检查:确保删除的是 plugins 目录下的内容 | ||
| if self.plugins_dir not in plugin_dir.parents and plugin_dir != self.plugins_dir: | ||
| return ImportResult( | ||
| success=False, | ||
| plugin_name=plugin_name, | ||
| message="只能删除 plugins 目录下的插件" | ||
| ) | ||
|
|
||
| try: | ||
| shutil.rmtree(plugin_dir) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
python - <<'PY'
from pathlib import Path
plugins_dir = Path("/tmp/project/plugins")
candidate = plugins_dir / "../foo"
print("candidate =", candidate)
print("parents =", list(candidate.parents))
print("passes_current_check =", plugins_dir in candidate.parents or candidate == plugins_dir)
print("resolved =", candidate.resolve(strict=False))
PYRepository: OneDragon-Anything/ZenlessZoneZero-OneDragon
Length of output: 329
🏁 Script executed:
find . -type f -name "plugin_import_service.py" | head -5Repository: OneDragon-Anything/ZenlessZoneZero-OneDragon
Length of output: 156
🏁 Script executed:
sed -n '457,481p' ./src/one_dragon/base/operation/application/plugin_import_service.py | cat -nRepository: OneDragon-Anything/ZenlessZoneZero-OneDragon
Length of output: 1069
安全检查对路径遍历无效,允许删除 plugins 根目录。
父目录检查在未 resolve() 的路径上进行。如调用 delete_plugin("../foo"),路径变为 plugins/../foo,但 plugins 仍在其 parents 中,通过安全检查后解析为目录外的路径。另外 plugin_dir == self.plugins_dir 条件会被放行,允许用 shutil.rmtree() 删除整个 plugins 根目录。
应先用 plugin_dir.resolve(strict=False) 将路径标准化,再进行父目录检查,并移除 plugin_dir == self.plugins_dir 条件的放行逻辑。
🧰 Tools
🪛 Ruff (0.15.5)
[warning] 458-458: Comment contains ambiguous , (FULLWIDTH COMMA). Did you mean , (COMMA)?
(RUF003)
[warning] 472-472: Comment contains ambiguous : (FULLWIDTH COLON). Did you mean : (COLON)?
(RUF003)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/one_dragon/base/operation/application/plugin_import_service.py` around
lines 457 - 481, The current parent-directory check on plugin_dir is bypassable
and allows deleting the plugins root; fix by normalizing paths before checks:
call plugin_dir_resolved = Path(plugin_dir).resolve(strict=False) and
plugins_dir_resolved = self.plugins_dir.resolve(strict=False), use
plugin_dir_resolved.name for plugin_name, then reject if not
plugin_dir_resolved.is_relative_to(plugins_dir_resolved) or if
plugin_dir_resolved == plugins_dir_resolved (i.e., disallow deleting the root);
only after these checks call shutil.rmtree(plugin_dir_resolved) to remove the
directory.
| def __init__( | ||
| self, | ||
| plugin_info: PluginInfo, | ||
| on_delete: callable, | ||
| on_open_homepage: callable, | ||
| parent=None | ||
| ): |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
python - <<'PY'
import ast
from pathlib import Path
path = Path("src/zzz_od/gui/view/setting/setting_plugin_interface.py")
tree = ast.parse(path.read_text(encoding="utf-8"))
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
missing = []
if node.returns is None:
missing.append("return")
for arg in [*node.args.posonlyargs, *node.args.args, *node.args.kwonlyargs]:
if arg.arg not in {"self", "cls"} and arg.annotation is None:
missing.append(arg.arg)
uses_builtin_callable = any(
isinstance(sub, ast.Name) and sub.id == "callable"
for sub in ast.walk(node)
)
if missing or uses_builtin_callable:
print(
f"line={node.lineno} name={node.name} "
f"missing={missing} uses_builtin_callable={uses_builtin_callable}"
)
PYRepository: OneDragon-Anything/ZenlessZoneZero-OneDragon
Length of output: 245
补齐构造函数和回调的类型标注
PluginCard.__init__() 和 SettingPluginInterface.__init__() 缺少返回类型注解,parent 参数和回调参数的类型注解不完整。第 38-44 行使用了内置 callable 而非 collections.abc.Callable,不符合 Python 3.11+ 的现代语法要求。
♻️ 建议修改
+from collections.abc import Callable
...
def __init__(
self,
plugin_info: PluginInfo,
- on_delete: callable,
- on_open_homepage: callable,
- parent=None
- ):
+ on_delete: Callable[[PluginInfo], None],
+ on_open_homepage: Callable[[PluginInfo], None],
+ parent: QWidget | None = None,
+ ) -> None:
...
- def __init__(self, ctx: ZContext, parent=None):
+ def __init__(self, ctx: ZContext, parent: QWidget | None = None) -> None:亦应用于第 121-133 行。根据编码指南要求,所有函数和方法必须包含完整的类型标注,并使用 Python 3.11+ 现代语法。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/zzz_od/gui/view/setting/setting_plugin_interface.py` around lines 38 -
44, The constructors PluginCard.__init__ and SettingPluginInterface.__init__ are
missing precise type annotations: replace the built-in callable with
collections.abc.Callable, annotate parent as Optional[Any] (or Optional[QWidget]
if using PyQt/PySide), and add explicit return type -> None; import Optional and
Any from typing and Callable from collections.abc and update the callback
parameters to use Callable[..., Any] (or more specific signatures like
Callable[[], None] if known); apply the same changes to the other __init__
overload at lines ~121-133 so all constructors have complete modern Python 3.11+
annotations.
| if plugin_info.homepage: | ||
| self.homepage_btn = PushButton(text=gt("主页")) | ||
| self.homepage_btn.clicked.connect(self._on_homepage_clicked) | ||
| buttons.append(self.homepage_btn) |
There was a problem hiding this comment.
不要把“主页”按钮的存在性绑定到卡片首次创建时的插件状态。
Line 238-259 会按索引复用 PluginCard,但 Line 53-56 只在首次有 homepage 时才创建按钮,Line 114-115 也只会切换已有按钮的可见性。这样一来,最初绑定到“无主页”插件的卡片在删除/重排后被复用给“有主页”插件时,主页按钮永远不会出现。
💡 一个直接的修正方向
- if plugin_info.homepage:
- self.homepage_btn = PushButton(text=gt("主页"))
- self.homepage_btn.clicked.connect(self._on_homepage_clicked)
- buttons.append(self.homepage_btn)
+ self.homepage_btn = PushButton(text=gt("主页"))
+ self.homepage_btn.clicked.connect(self._on_homepage_clicked)
+ self.homepage_btn.setVisible(bool(plugin_info.homepage))
+ buttons.append(self.homepage_btn)
...
- if hasattr(self, 'homepage_btn'):
- self.homepage_btn.setVisible(bool(plugin_info.homepage))
+ self.homepage_btn.setVisible(bool(plugin_info.homepage))Also applies to: 113-115, 238-259
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/zzz_od/gui/view/setting/setting_plugin_interface.py` around lines 53 -
56, The PluginCard currently creates homepage_btn only if plugin_info.homepage
exists, causing recycled cards to miss the button; always create
self.homepage_btn (connected to _on_homepage_clicked) in the PluginCard
initialization and remove the conditional creation, then in the card
update/refresh method (the routine that reapplies plugin_info when cards are
reused) set self.homepage_btn.setVisible(bool(plugin_info.homepage)) and update
any related state (e.g., tooltip or URL) so the button appears or hides
correctly when a card is reused for a plugin that does or does not have a
homepage.
| installed_plugins = { | ||
| p.app_id: p for p in self.ctx.factory_manager.third_party_plugins | ||
| } | ||
|
|
||
| # 检查版本信息 | ||
| overwrite_info = [] # (file_path, plugin_name, new_ver, old_ver, is_downgrade) | ||
| for fp, r in existing_plugins: | ||
| preview = self.plugin_import_service.preview_plugin(fp) | ||
| new_ver = preview.version if preview else None | ||
| old_ver = installed_plugins.get(r.plugin_name, None) | ||
| old_ver_str = old_ver.version if old_ver else None | ||
|
|
||
| is_downgrade = self._is_version_lower(new_ver, old_ver_str) | ||
| overwrite_info.append((fp, r.plugin_name, new_ver, old_ver_str, is_downgrade)) |
There was a problem hiding this comment.
覆盖/降级判断把目录名当成了 app_id。
Line 324 的字典键是 p.app_id,但 Line 332 和 Line 479 查的是 ImportResult.plugin_name / result.plugin_name。这两个值在导入服务里实际表示插件目录名,不是 app_id。只要目录名和 app_id 不一致,旧版本就会查不到,降级提示被绕过,覆盖确认也会落到错误对象上。
🔧 建议按目录名对齐当前 service 契约
- installed_plugins = {
- p.app_id: p for p in self.ctx.factory_manager.third_party_plugins
- }
+ installed_plugins = {
+ p.plugin_dir.name: p
+ for p in self.ctx.factory_manager.third_party_plugins
+ if p.plugin_dir is not None
+ }
...
- old_ver = installed_plugins.get(r.plugin_name, None)
+ old_ver = installed_plugins.get(r.plugin_name)
old_ver_str = old_ver.version if old_ver else None
...
- (p for p in self.ctx.factory_manager.third_party_plugins if p.app_id == result.plugin_name),
+ (
+ p
+ for p in self.ctx.factory_manager.third_party_plugins
+ if p.plugin_dir is not None and p.plugin_dir.name == result.plugin_name
+ ),Also applies to: 478-485
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/zzz_od/gui/view/setting/setting_plugin_interface.py` around lines 323 -
336, The installed_plugins map uses p.app_id as key but later code
(ImportResult.plugin_name / result.plugin_name from
plugin_import_service.preview_plugin) supplies plugin directory names, so
lookups miss matches causing downgrade/overwrite checks to be skipped; change
the map key to the plugin directory/name used by the import service (use
whatever property on p represents the directory/name — e.g., p.plugin_name or
p.dir_name) so installed_plugins = { <directory-key>: p for p in
self.ctx.factory_manager.third_party_plugins }, then keep the existing logic
that uses preview = self.plugin_import_service.preview_plugin(fp), new_ver =
preview.version, old_ver = installed_plugins.get(r.plugin_name, None),
is_downgrade = self._is_version_lower(...) so comparisons use the same
identifier for installed_plugins, overwrite_info, and the
ImportResult.plugin_name/result.plugin_name lookups.
Summary by CodeRabbit
新功能