Feat: export and import plugins#8165
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces plugin export and import functionality to the dashboard, allowing users to share plugin configurations via compressed strings. Key changes include the addition of the lz-string library, new Material Design Icons, and the implementation of a PluginImportDialog component. Feedback identifies a logic error in the import progress calculation and missing props/event handlers in the dialog's integration that are necessary for proxy support and UI refreshing.
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- In
PluginImportDialog.vue,installStatusmixes numeric keys for statuses and${idx}_msgkeys for error messages on the same object, which makes it harder to reason about; consider using a structured map like{ [idx]: { status, message } }to keep per-plugin state cohesive and reduce key collisions.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `PluginImportDialog.vue`, `installStatus` mixes numeric keys for statuses and `${idx}_msg` keys for error messages on the same object, which makes it harder to reason about; consider using a structured map like `{ [idx]: { status, message } }` to keep per-plugin state cohesive and reduce key collisions.
## Individual Comments
### Comment 1
<location path="dashboard/src/views/extension/InstalledPluginsTab.vue" line_range="284-205" />
<code_context>
+ exportPlugin(picked);
+}
+
+const copyExportCode = async () => {
+ try {
+ await navigator.clipboard.writeText(exportCode.value);
+ toast(tm("exportImport.errors.copySuccess"), "success");
+ } catch (err) {
+ console.error("Copy failed", err);
+ toast(tm("exportImport.errors.copyFailed"), "error");
+ }
+}
+
</code_context>
<issue_to_address>
**suggestion:** Handle environments where `navigator.clipboard` is unavailable with a graceful fallback.
This relies on `navigator.clipboard.writeText` always being present and allowed. In environments where it’s missing or disallowed (older browsers, some WebViews, non-HTTPS), the copy flow will just fail. Since you already handle errors, consider first checking for `navigator.clipboard?.writeText` and, if unavailable, falling back to selecting the textarea via a ref (or at least surfacing a clearer “please copy manually” message).
Suggested implementation:
```
const copyExportCode = async () => {
if (!exportCode.value) {
toast(tm("exportImport.errors.nothingToCopy"), "warning");
return;
}
const clipboard = typeof navigator !== "undefined" ? navigator.clipboard : undefined;
const canUseClipboardApi =
clipboard && typeof clipboard.writeText === "function";
if (!canUseClipboardApi) {
// Clipboard API not available or not allowed in this environment
toast(tm("exportImport.errors.copyUnavailable"), "warning");
return;
}
try {
await clipboard.writeText(exportCode.value);
toast(tm("exportImport.errors.copySuccess"), "success");
} catch (err) {
console.error("Copy failed", err);
toast(tm("exportImport.errors.copyFailed"), "error");
}
};
const showExportCode = ref(false);
const exportCode = ref("");
```
To provide a richer fallback (e.g. auto-selecting the export code for manual copy), you may also want to:
1. Add a `const exportCodeTextarea = ref(null);` in the script and bind it to the export textarea in the template via `ref="exportCodeTextarea"`.
2. In the `!canUseClipboardApi` branch, before the toast, call something like:
```js
if (exportCodeTextarea.value) {
exportCodeTextarea.value.focus();
exportCodeTextarea.value.select();
}
toast(tm("exportImport.errors.copyUnavailable"), "warning");
```
3. Ensure the new i18n keys (`exportImport.errors.nothingToCopy`, `exportImport.errors.copyUnavailable`) are added to your locale files.
</issue_to_address>
### Comment 2
<location path="dashboard/src/components/extension/PluginImportDialog.vue" line_range="71-62" />
<code_context>
+ emit("update:modelValue", false);
+};
+
+const parseImportCode = () => {
+ importError.value = "";
+ importPlugins.value = [];
+ selected.value = [];
+ installStatus.value = {};
+ const code = (importCode.value || "").trim();
+ if (!code) {
+ importError.value = tm("exportImport.errors.needCode");
+ return;
+ }
+ try {
+ const jsonStr = LZString.decompressFromEncodedURIComponent(code);
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Avoid clearing existing parsed plugins before validating new import code.
`parseImportCode` clears `importPlugins`, `selected`, and `installStatus` before decompression/parsing. If parsing fails (e.g., invalid snippet), the previous valid state is lost. Consider parsing into local variables first, validating, and only then updating `importPlugins`/`selected` so failures leave the existing state intact and only surface an error.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| @@ -202,6 +205,98 @@ const togglePinnedExtension = (extension) => { | |||
| } | |||
There was a problem hiding this comment.
suggestion: Handle environments where navigator.clipboard is unavailable with a graceful fallback.
This relies on navigator.clipboard.writeText always being present and allowed. In environments where it’s missing or disallowed (older browsers, some WebViews, non-HTTPS), the copy flow will just fail. Since you already handle errors, consider first checking for navigator.clipboard?.writeText and, if unavailable, falling back to selecting the textarea via a ref (or at least surfacing a clearer “please copy manually” message).
Suggested implementation:
const copyExportCode = async () => {
if (!exportCode.value) {
toast(tm("exportImport.errors.nothingToCopy"), "warning");
return;
}
const clipboard = typeof navigator !== "undefined" ? navigator.clipboard : undefined;
const canUseClipboardApi =
clipboard && typeof clipboard.writeText === "function";
if (!canUseClipboardApi) {
// Clipboard API not available or not allowed in this environment
toast(tm("exportImport.errors.copyUnavailable"), "warning");
return;
}
try {
await clipboard.writeText(exportCode.value);
toast(tm("exportImport.errors.copySuccess"), "success");
} catch (err) {
console.error("Copy failed", err);
toast(tm("exportImport.errors.copyFailed"), "error");
}
};
const showExportCode = ref(false);
const exportCode = ref("");
To provide a richer fallback (e.g. auto-selecting the export code for manual copy), you may also want to:
- Add a
const exportCodeTextarea = ref(null);in the script and bind it to the export textarea in the template viaref="exportCodeTextarea". - In the
!canUseClipboardApibranch, before the toast, call something like:if (exportCodeTextarea.value) { exportCodeTextarea.value.focus(); exportCodeTextarea.value.select(); } toast(tm("exportImport.errors.copyUnavailable"), "warning");
- Ensure the new i18n keys (
exportImport.errors.nothingToCopy,exportImport.errors.copyUnavailable) are added to your locale files.
| selected.value = []; | ||
| installStatus.value = {}; | ||
| installing.value = false; | ||
| } |
There was a problem hiding this comment.
suggestion (bug_risk): Avoid clearing existing parsed plugins before validating new import code.
parseImportCode clears importPlugins, selected, and installStatus before decompression/parsing. If parsing fails (e.g., invalid snippet), the previous valid state is lost. Consider parsing into local variables first, validating, and only then updating importPlugins/selected so failures leave the existing state intact and only surface an error.
Modifications / 改动点
核心功能:插件导入/导出功能
本次 PR 为 AstrBot Dashboard 新增了插件导入/导出功能,方便用户备份和分享已安装的插件配置。
修改的文件:
src/views/extension/InstalledPluginsTab.vueastrbot和builtin_commands两个内置插件src/components/extension/PluginImportDialog.vuesrc/i18n/locales/zh-CN/features/extension.jsonexportImport分类,包含导出/导入相关的所有 i18n 文本src/i18n/locales/en-US/features/extension.jsondashboard/package.jsonlz-string依赖(用于压缩/解压插件码)功能特点:
导出:支持一键导出当前筛选结果、置顶插件列表或手动挑选
导入:粘贴插件码 → 解析预览 → 选择安装 → 顺序下载
压缩:使用 LZString 进行压缩,方便复制分享
安全提示:导入时显示安全警告,提醒用户验证插件来源
This is NOT a breaking change. / 这不是一个破坏性变更。
Screenshots or Test Results / 运行截图或测试结果
导出插件码:



导出自选插件:
解析插件并导入下载:
Checklist / 检查清单
😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
/ 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。
👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
/ 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”。
🤓 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.txtandpyproject.toml./ 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到
requirements.txt和pyproject.toml文件相应位置。😮 My changes do not introduce malicious code.
/ 我的更改没有引入恶意代码。
Summary by Sourcery
Add plugin export/import capabilities to the AstrBot Dashboard, enabling users to share and restore plugin configurations via compressed plugin codes.
New Features:
Enhancements:
Build: