Conversation
…install flow - add "fromShareCode" tab in extension install dialog - support generating/copying share codes with astrbot-share prefix - add share-code parsing and batch URL installs via performInstallRequest - reuse install dialog entry from InstalledPluginsTab import action - update i18n messages and upload labels for share-code workflows - refine share-mode selection UX and card interaction behavior
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! 此拉取请求旨在通过引入插件分享功能,提供一种轻量、快速且可复制的插件组合分发方案。它允许用户在插件管理页面选择一组插件,生成一个可分享的代码,其他用户可以通过该代码批量导入并安装这些插件。这极大地简化了插件集合的共享过程,提升了用户体验,并为插件分发提供了整合包之外的灵活替代方案。 Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Hey - 我发现了 1 个问题,并给出了一些整体性的反馈:
encodeShareCode、decodeShareCode和parseShareCodeRepos外面包裹的 async 函数本身并没有执行任何异步操作,可以简化为同步函数,从而减少不必要的await,也让调用链更容易理解。- 在分享模式下,全局的
user-select: none规则(例如.share-mode-card * { user-select: none; }以及表格中与分享模式相关的选择器)可能会无意中阻止用户复制插件名称或 URL 等文本;建议更精确地限定这些样式的作用范围,避免在仍然需要正常文本选择的地方进行阻止。 installFromShareCode中顺序执行的for...of循环会在导入大量仓库时变得很慢;可以考虑改用Promise.allSettled模式并行执行安装,同时仍然收集每个仓库失败的信息用于最终汇总。
给 AI Agent 的提示
Please address the comments from this code review:
## Overall Comments
- `encodeShareCode`、`decodeShareCode` 和 `parseShareCodeRepos` 外面包裹的 async 函数本身并没有执行任何异步操作,可以简化为同步函数,从而减少不必要的 `await`,也让调用链更容易理解。
- 在分享模式下,全局的 `user-select: none` 规则(例如 `.share-mode-card * { user-select: none; }` 以及表格中与分享模式相关的选择器)可能会无意中阻止用户复制插件名称或 URL 等文本;建议更精确地限定这些样式的作用范围,避免在仍然需要正常文本选择的地方进行阻止。
- `installFromShareCode` 中顺序执行的 `for...of` 循环会在导入大量仓库时变得很慢;可以考虑改用 `Promise.allSettled` 模式并行执行安装,同时仍然收集每个仓库失败的信息用于最终汇总。
## Individual Comments
### Comment 1
<location path="dashboard/src/views/extension/useExtensionPage.js" line_range="149" />
<code_context>
+ isShareMode.value = true;
+ selectedSharePluginNames.value = new Set();
+ };
+ const encodeShareCode = async (payload) => {
+ const rawJson = JSON.stringify(payload);
+ const compressed = compressToEncodedURIComponent(rawJson);
</code_context>
<issue_to_address>
**issue (complexity):** 可以考虑通过将 share-code 相关的辅助函数改为同步、集中管理 Set 的操作逻辑,并把批量安装循环提取到一个小的辅助函数中,从而简化新分享功能的实现。
在保持现有功能行为不变的前提下,这些调整能够在几个关键位置降低复杂度。
### 1. 去掉纯辅助函数中不必要的 `async`
`encodeShareCode`、`decodeShareCode` 和 `parseShareCodeRepos` 实际上是同步的。将它们改成同步可以减轻理解负担,并简化调用端代码。
```js
// keep at top-level in this file or move to a separate util later
const encodeShareCode = (payload) => {
const rawJson = JSON.stringify(payload);
const compressed = compressToEncodedURIComponent(rawJson);
if (!compressed) return rawJson;
return `${SHARE_CODE_LZ_PREFIX}${compressed}`;
};
const decodeShareCode = (rawCode) => {
if (rawCode.startsWith(SHARE_CODE_LZ_PREFIX)) {
const encodedPart = rawCode.slice(SHARE_CODE_LZ_PREFIX.length);
const decompressed = decompressFromEncodedURIComponent(encodedPart);
if (!decompressed) {
throw new Error("Invalid lz share code");
}
return decompressed;
}
return rawCode;
};
const parseShareCodeRepos = (rawCode) => {
const decodedContent = decodeShareCode(rawCode);
const parsed = JSON.parse(decodedContent);
const repos = Array.isArray(parsed?.repos) ? parsed.repos : [];
return repos
.filter((repo) => typeof repo === "string")
.map((repo) => repo.trim())
.filter((repo) => repo.length > 0);
};
```
然后以同步方式调用:
```js
const confirmShareSelection = async () => {
// ...
const shareCode = encodeShareCode({ repos: uniqueRepos });
// ...
};
const installFromShareCode = async (ignoreVersionCheck = false) => {
// ...
let repos = [];
try {
repos = parseShareCodeRepos(rawCode);
} catch (error) {
toast(tm("messages.invalidShareCode"), "error");
return;
}
// ...
};
```
### 2. 抽取并复用分享选择中 `Set` 的变更 / 守卫逻辑
`selectedSharePluginNames` 的修改和相关的防御性检查逻辑在多个函数中重复出现。通过一个小的辅助函数可以把这些模式集中在一个地方,同时简化各个开关函数。
```js
const selectedSharePluginNames = ref(new Set());
const updateSelectedSharePlugins = (updater) => {
const next = new Set(selectedSharePluginNames.value);
updater(next);
selectedSharePluginNames.value = next;
};
const normalizePluginNames = (pluginNames = []) =>
pluginNames.filter((name) => typeof name === "string" && name.length > 0);
const toggleSharePluginSelection = (pluginName) => {
if (!isShareMode.value || !pluginName) return;
updateSelectedSharePlugins((set) => {
if (set.has(pluginName)) set.delete(pluginName);
else set.add(pluginName);
});
};
const toggleShareSelectAll = (pluginNames = []) => {
if (!isShareMode.value) return;
const names = normalizePluginNames(pluginNames);
if (names.length === 0) return;
const allSelected = names.every((name) =>
selectedSharePluginNames.value.has(name),
);
updateSelectedSharePlugins((set) => {
if (allSelected) {
names.forEach((name) => set.delete(name));
} else {
names.forEach((name) => set.add(name));
}
});
};
const areAllSharePluginsSelected = (pluginNames = []) => {
const names = normalizePluginNames(pluginNames);
if (names.length === 0) return false;
return names.every((name) => selectedSharePluginNames.value.has(name));
};
const isSharePluginSelected = (pluginName) =>
!!pluginName && selectedSharePluginNames.value.has(pluginName);
```
这样可以把所有克隆 / 赋值逻辑集中到一个地方,同时统一处理名称校验。
### 3. 将 `installFromShareCode` 中的核心安装循环提取出来
`installFromShareCode` 目前同时处理解析、状态管理和安装循环。把安装循环抽取到一个小的辅助函数中,可以在不改变行为的前提下,让顶层流程更容易阅读和理解。
```js
const installReposBatch = async (repos, { ignoreVersionCheck }) => {
const failedItems = [];
let successCount = 0;
const previousExtensionUrl = extension_url.value;
try {
for (const repoUrl of repos) {
extension_url.value = repoUrl;
const res = await performInstallRequest({
source: "url",
ignoreVersionCheck,
});
const resData = res.data || {};
if (resData.status === "ok") {
successCount += 1;
} else {
failedItems.push({
url: repoUrl,
message: resData.message || tm("messages.installFailed"),
});
}
}
} catch (err) {
failedItems.push({
url: extension_url.value || tm("status.unknown"),
message: resolveErrorMessage(err, tm("messages.installFailed")),
});
} finally {
extension_url.value = previousExtensionUrl;
}
return { successCount, failedItems };
};
const installFromShareCode = async (ignoreVersionCheck = false) => {
const rawCode = (shareCodeInput.value ?? "").trim();
if (!rawCode) {
toast(tm("messages.fillShareCode"), "error");
return;
}
let repos = [];
try {
repos = parseShareCodeRepos(rawCode);
} catch {
toast(tm("messages.invalidShareCode"), "error");
return;
}
const uniqueRepos = [...new Set(repos)];
if (uniqueRepos.length === 0) {
toast(tm("messages.noRepoInShareCode"), "error");
return;
}
loading_.value = true;
loadingDialog.title = tm("status.loading");
loadingDialog.show = true;
const { successCount, failedItems } = await installReposBatch(uniqueRepos, {
ignoreVersionCheck,
});
loading_.value = false;
// rest of the success/partial/failure UI handling stays the same
};
```
这些改动在保持所有现有功能不变的同时,使 share-code 相关逻辑更加聚焦,也更易于维护。
</issue_to_address>帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进后续的代码审查。
Original comment in English
Hey - I've found 1 issue, and left some high level feedback:
- The async wrappers around
encodeShareCode,decodeShareCode, andparseShareCodeReposdon’t perform any asynchronous work and could be simplified to synchronous functions to reduce unnecessaryawaits and make the call chain easier to follow. - In share mode, the global
user-select: nonerules (e.g..share-mode-card * { user-select: none; }and the table share-mode selectors) may unintentionally prevent copying text such as plugin names or URLs; consider scoping these styles more narrowly to avoid blocking normal text selection where it’s still useful. - The sequential
for...ofloop ininstallFromShareCodecan make importing many repos slow; consider switching to aPromise.allSettledpattern to perform installs in parallel while still collecting per-repo failures for the summary.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The async wrappers around `encodeShareCode`, `decodeShareCode`, and `parseShareCodeRepos` don’t perform any asynchronous work and could be simplified to synchronous functions to reduce unnecessary `await`s and make the call chain easier to follow.
- In share mode, the global `user-select: none` rules (e.g. `.share-mode-card * { user-select: none; }` and the table share-mode selectors) may unintentionally prevent copying text such as plugin names or URLs; consider scoping these styles more narrowly to avoid blocking normal text selection where it’s still useful.
- The sequential `for...of` loop in `installFromShareCode` can make importing many repos slow; consider switching to a `Promise.allSettled` pattern to perform installs in parallel while still collecting per-repo failures for the summary.
## Individual Comments
### Comment 1
<location path="dashboard/src/views/extension/useExtensionPage.js" line_range="149" />
<code_context>
+ isShareMode.value = true;
+ selectedSharePluginNames.value = new Set();
+ };
+ const encodeShareCode = async (payload) => {
+ const rawJson = JSON.stringify(payload);
+ const compressed = compressToEncodedURIComponent(rawJson);
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the new share-code feature by making its helpers synchronous, centralizing Set manipulation, and extracting the batch install loop into a small helper for clearer flow.
You can keep the new feature as-is functionally while reducing complexity in a few tight spots.
### 1. Remove unnecessary `async` from pure helpers
`encodeShareCode`, `decodeShareCode`, and `parseShareCodeRepos` are synchronous. Making them sync reduces mental overhead and simplifies call sites.
```js
// keep at top-level in this file or move to a separate util later
const encodeShareCode = (payload) => {
const rawJson = JSON.stringify(payload);
const compressed = compressToEncodedURIComponent(rawJson);
if (!compressed) return rawJson;
return `${SHARE_CODE_LZ_PREFIX}${compressed}`;
};
const decodeShareCode = (rawCode) => {
if (rawCode.startsWith(SHARE_CODE_LZ_PREFIX)) {
const encodedPart = rawCode.slice(SHARE_CODE_LZ_PREFIX.length);
const decompressed = decompressFromEncodedURIComponent(encodedPart);
if (!decompressed) {
throw new Error("Invalid lz share code");
}
return decompressed;
}
return rawCode;
};
const parseShareCodeRepos = (rawCode) => {
const decodedContent = decodeShareCode(rawCode);
const parsed = JSON.parse(decodedContent);
const repos = Array.isArray(parsed?.repos) ? parsed.repos : [];
return repos
.filter((repo) => typeof repo === "string")
.map((repo) => repo.trim())
.filter((repo) => repo.length > 0);
};
```
Then call them synchronously:
```js
const confirmShareSelection = async () => {
// ...
const shareCode = encodeShareCode({ repos: uniqueRepos });
// ...
};
const installFromShareCode = async (ignoreVersionCheck = false) => {
// ...
let repos = [];
try {
repos = parseShareCodeRepos(rawCode);
} catch (error) {
toast(tm("messages.invalidShareCode"), "error");
return;
}
// ...
};
```
### 2. DRY up `Set` mutation / guard logic for share selection
The `selectedSharePluginNames` manipulation and guard checks are repeated across multiple functions. A tiny helper keeps those patterns in one place and shrinks the toggles.
```js
const selectedSharePluginNames = ref(new Set());
const updateSelectedSharePlugins = (updater) => {
const next = new Set(selectedSharePluginNames.value);
updater(next);
selectedSharePluginNames.value = next;
};
const normalizePluginNames = (pluginNames = []) =>
pluginNames.filter((name) => typeof name === "string" && name.length > 0);
const toggleSharePluginSelection = (pluginName) => {
if (!isShareMode.value || !pluginName) return;
updateSelectedSharePlugins((set) => {
if (set.has(pluginName)) set.delete(pluginName);
else set.add(pluginName);
});
};
const toggleShareSelectAll = (pluginNames = []) => {
if (!isShareMode.value) return;
const names = normalizePluginNames(pluginNames);
if (names.length === 0) return;
const allSelected = names.every((name) =>
selectedSharePluginNames.value.has(name),
);
updateSelectedSharePlugins((set) => {
if (allSelected) {
names.forEach((name) => set.delete(name));
} else {
names.forEach((name) => set.add(name));
}
});
};
const areAllSharePluginsSelected = (pluginNames = []) => {
const names = normalizePluginNames(pluginNames);
if (names.length === 0) return false;
return names.every((name) => selectedSharePluginNames.value.has(name));
};
const isSharePluginSelected = (pluginName) =>
!!pluginName && selectedSharePluginNames.value.has(pluginName);
```
This keeps all the cloning/assignment logic in one place and centralizes name validation.
### 3. Extract the core install loop from `installFromShareCode`
`installFromShareCode` currently mixes parsing, state management, and the install loop. Pulling the loop into a small helper makes the top-level flow easier to scan without changing behavior.
```js
const installReposBatch = async (repos, { ignoreVersionCheck }) => {
const failedItems = [];
let successCount = 0;
const previousExtensionUrl = extension_url.value;
try {
for (const repoUrl of repos) {
extension_url.value = repoUrl;
const res = await performInstallRequest({
source: "url",
ignoreVersionCheck,
});
const resData = res.data || {};
if (resData.status === "ok") {
successCount += 1;
} else {
failedItems.push({
url: repoUrl,
message: resData.message || tm("messages.installFailed"),
});
}
}
} catch (err) {
failedItems.push({
url: extension_url.value || tm("status.unknown"),
message: resolveErrorMessage(err, tm("messages.installFailed")),
});
} finally {
extension_url.value = previousExtensionUrl;
}
return { successCount, failedItems };
};
const installFromShareCode = async (ignoreVersionCheck = false) => {
const rawCode = (shareCodeInput.value ?? "").trim();
if (!rawCode) {
toast(tm("messages.fillShareCode"), "error");
return;
}
let repos = [];
try {
repos = parseShareCodeRepos(rawCode);
} catch {
toast(tm("messages.invalidShareCode"), "error");
return;
}
const uniqueRepos = [...new Set(repos)];
if (uniqueRepos.length === 0) {
toast(tm("messages.noRepoInShareCode"), "error");
return;
}
loading_.value = true;
loadingDialog.title = tm("status.loading");
loadingDialog.show = true;
const { successCount, failedItems } = await installReposBatch(uniqueRepos, {
ignoreVersionCheck,
});
loading_.value = false;
// rest of the success/partial/failure UI handling stays the same
};
```
These changes keep all existing functionality but make the share-code logic more focused and easier to maintain.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
Code Review
This PR introduces a new plugin sharing feature, enabling users to easily share their plugin combinations. The implementation covers the full process from generating share codes using lz-string to bulk installation via imported codes, with well-integrated UI changes. However, a critical security concern has been identified: a potential Cross-Site Scripting (XSS) vulnerability in the ExtensionCard.vue component. This occurs because a repository URL is used in an href attribute without proper sanitization, which could be exploited by malicious plugins or share codes. Additionally, the code review highlighted several areas for improvement in code quality and maintainability, including suggestions to remove unnecessary async keywords, reduce code duplication, minimize side effects by avoiding shared state modification, and address a minor UI color inconsistency.
…selection UX make share-code helpers synchronous and reduce unnecessary await usage centralize share selection Set updates and keep behavior unchanged parallelize share-code imports with Promise.allSettled and keep progress updates align share-mode selected card glow with primary color and adjust user-select behavior
已完成部分优化 |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Hey - 我发现了 1 个问题
供 AI 代理使用的提示词
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="dashboard/src/views/extension/useExtensionPage.js" line_range="135" />
<code_context>
return false;
};
const showReserved = ref(getInitialShowReserved());
+ const isShareMode = ref(false);
+ const selectedSharePluginNames = ref(new Set());
+ const cancelShareMode = () => {
</code_context>
<issue_to_address>
**issue (complexity):** 请考虑将新的分享选择、分享代码以及批量安装逻辑抽取到独立的 composable / helper 中,这样 `useExtensionPage` 只需要协调高层行为,而不必处理底层细节。
新增的分享和批量安装逻辑显著增加了 `useExtensionPage` 的职责。你可以在保留现有行为的前提下,通过抽取几个聚焦明确的 helper/composable 并隐藏底层细节来降低复杂度。
### 1. 将分享选择逻辑抽取为一个小的 helper
所有与 `Set` 相关的管理逻辑都可以被封装起来,让页面只使用一个小而清晰的 API:
```ts
// useSelectionSet.ts
import { ref, computed } from "vue";
export const useSelectionSet = () => {
const selected = ref(new Set<string>());
const clear = () => { selected.value = new Set(); };
const toggle = (name?: string) => {
if (!name) return;
const next = new Set(selected.value);
if (next.has(name)) next.delete(name);
else next.add(name);
selected.value = next;
};
const toggleAll = (items: string[]) => {
const names = items.filter(Boolean);
if (!names.length) return;
const allSelected = names.every((n) => selected.value.has(n));
const next = new Set(selected.value);
if (allSelected) names.forEach((n) => next.delete(n));
else names.forEach((n) => next.add(n));
selected.value = next;
};
const areAllSelected = (items: string[]) =>
items.length > 0 && items.every((n) => selected.value.has(n));
const isSelected = (name?: string) =>
!!name && selected.value.has(name);
const count = computed(() => selected.value.size);
return {
selected,
clear,
toggle,
toggleAll,
areAllSelected,
isSelected,
count,
};
};
```
然后在 `useExtensionPage` 中,你可以收窄与分享相关的接口面:
```ts
const isShareMode = ref(false);
const {
selected: selectedSharePluginNames,
clear: clearShareSelection,
toggle: toggleSharePluginSelection,
toggleAll: toggleShareSelectAll,
areAllSelected: areAllSharePluginsSelected,
isSelected: isSharePluginSelected,
count: selectedSharePluginCount,
} = useSelectionSet();
const cancelShareMode = () => {
isShareMode.value = false;
clearShareSelection();
};
const toggleShareMode = () => {
if (isShareMode.value) {
cancelShareMode();
} else {
isShareMode.value = true;
clearShareSelection();
}
};
```
这样你就可以移除 `normalizePluginNames`、`updateSelectedSharePlugins`,并减少分享选择相关 helper 的数量。
### 2. 收拢分享代码的编码 / 解码 / 解析逻辑
与其让 `encodeShareCode`、`decodeShareCode` 和 `parseShareCodeRepos` 分散在整个 composable 中,不如将它们集中起来:
```ts
// useExtensionShareCode.ts
import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from "lz-string";
const SHARE_CODE_LZ_PREFIX = "astrbot-share:";
export const useExtensionShareCode = () => {
const encode = (repos: string[]) => {
const normalized = [...new Set(
(repos || [])
.filter((r) => typeof r === "string")
.map((r) => r.trim())
.filter(Boolean),
)].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
const rawJson = JSON.stringify({ repos: normalized });
const compressed = compressToEncodedURIComponent(rawJson);
if (!compressed) return rawJson;
return `${SHARE_CODE_LZ_PREFIX}${compressed}`;
};
const decode = (code: string): string[] => {
const raw = code.startsWith(SHARE_CODE_LZ_PREFIX)
? (() => {
const encoded = code.slice(SHARE_CODE_LZ_PREFIX.length);
const decompressed = decompressFromEncodedURIComponent(encoded);
if (!decompressed) throw new Error("Invalid lz share code");
return decompressed;
})()
: code;
const parsed = JSON.parse(raw);
const repos = Array.isArray(parsed?.repos) ? parsed.repos : [];
return repos
.filter((r) => typeof r === "string")
.map((r) => r.trim())
.filter(Boolean);
};
return { encode, decode };
};
```
这样在 `useExtensionPage` 中的使用就会更简单,并且把 JSON / 压缩相关的细节从主要的 composable 中移走:
```ts
const { encode: encodeShareCode, decode: decodeShareCode } = useExtensionShareCode();
const confirmShareSelection = async () => {
const data = Array.isArray(extension_data?.data) ? extension_data.data : [];
const selectedNames = selectedSharePluginNames.value;
const repos = data
.filter((ext) => selectedNames.has(ext.name))
.map((ext) => (ext.repo ?? "").trim());
const shareCode = encodeShareCode(repos);
// clipboard + toast logic unchanged
};
const installFromShareCode = async (ignoreVersionCheck = false) => {
const rawCode = (shareCodeInput.value ?? "").trim();
if (!rawCode) {
toast(tm("messages.fillShareCode"), "error");
return;
}
let repos: string[];
try {
repos = decodeShareCode(rawCode);
} catch {
toast(tm("messages.invalidShareCode"), "error");
return;
}
// continue with batch install using the repos
};
```
这可以完全去掉 `parseShareCodeRepos`,并消除重复的仓库地址归一化逻辑。
### 3. 将批量安装逻辑与 UI / 进度解耦
你可以让 `installReposBatch` 保持纯逻辑实现,并把 `tm`、`resolveErrorMessage` 和进度弹窗相关的内容移动到调用方:
```ts
// batchInstall.ts
export type BatchResult = {
successCount: number;
failedItems: { url: string; message: string }[];
};
export const installReposBatch = async (
repos: string[],
{
ignoreVersionCheck,
performInstall,
onProgress,
}: {
ignoreVersionCheck: boolean;
performInstall: (url: string) => Promise<{ ok: boolean; message?: string }>;
onProgress?: (info: { current: number; total: number; repoUrl: string }) => void;
},
): Promise<BatchResult> => {
let completedCount = 0;
const tasks = repos.map(async (repoUrl) => {
try {
const res = await performInstall(repoUrl);
return { success: res.ok, url: repoUrl, message: res.message };
} finally {
completedCount += 1;
onProgress?.({ current: completedCount, total: repos.length, repoUrl });
}
});
const settled = await Promise.allSettled(tasks);
let successCount = 0;
const failedItems: { url: string; message: string }[] = [];
settled.forEach((r) => {
if (r.status === "fulfilled") {
if (r.value.success) successCount += 1;
else failedItems.push({ url: r.value.url, message: r.value.message ?? "" });
} else {
failedItems.push({ url: "", message: "" }); // caller can decorate errors
}
});
return { successCount, failedItems };
};
```
然后在 `useExtensionPage` 中,你可以通过一个小的封装函数来适配现有的 `performInstallRequest`、`tm` 和弹窗状态,而不是把所有东西都塞进 `installReposBatch`:
```ts
const performInstallUrl = async (repoUrl: string) => {
try {
const res = await performInstallRequest({
source: "url",
ignoreVersionCheck,
url: repoUrl,
});
const resData = res.data || {};
return {
ok: resData.status === "ok",
message: resData.message || tm("messages.installFailed"),
};
} catch (err) {
return {
ok: false,
message: resolveErrorMessage(err, tm("messages.installFailed")),
};
}
};
const { successCount, failedItems } = await installReposBatch(uniqueRepos, {
ignoreVersionCheck,
performInstall: performInstallUrl,
onProgress: ({ current, total, repoUrl }) => {
loadingDialog.progressCurrent = current;
loadingDialog.progressTotal = total;
loadingDialog.progressLabel = repoUrl;
},
});
```
这样可以让 `installReposBatch` 保持可复用、易测试,而 `installFromShareCode` 只需要表达高层流程(准备仓库 → 展示弹窗 → 调用批量 helper → 更新 UI),而不必关心内部的 HTTP / 错误细节。
这些抽取操作在保留全部行为的同时,让 `useExtensionPage` 不再那么臃肿,更易于维护。
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据反馈改进后续评审。
Original comment in English
Hey - I've found 1 issue
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="dashboard/src/views/extension/useExtensionPage.js" line_range="135" />
<code_context>
return false;
};
const showReserved = ref(getInitialShowReserved());
+ const isShareMode = ref(false);
+ const selectedSharePluginNames = ref(new Set());
+ const cancelShareMode = () => {
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting the new share-selection, share-code, and batch-install logic into dedicated composables/helpers so `useExtensionPage` only coordinates high-level behavior instead of managing low-level details.
The added share + batch-install logic significantly increases `useExtensionPage`’s responsibilities. You can keep all behavior while reducing complexity by extracting a few focused helpers/composables and hiding low-level details.
### 1. Extract share-selection into a small helper
All the `Set`-management logic can be encapsulated so the page just uses a small API:
```ts
// useSelectionSet.ts
import { ref, computed } from "vue";
export const useSelectionSet = () => {
const selected = ref(new Set<string>());
const clear = () => { selected.value = new Set(); };
const toggle = (name?: string) => {
if (!name) return;
const next = new Set(selected.value);
if (next.has(name)) next.delete(name);
else next.add(name);
selected.value = next;
};
const toggleAll = (items: string[]) => {
const names = items.filter(Boolean);
if (!names.length) return;
const allSelected = names.every((n) => selected.value.has(n));
const next = new Set(selected.value);
if (allSelected) names.forEach((n) => next.delete(n));
else names.forEach((n) => next.add(n));
selected.value = next;
};
const areAllSelected = (items: string[]) =>
items.length > 0 && items.every((n) => selected.value.has(n));
const isSelected = (name?: string) =>
!!name && selected.value.has(name);
const count = computed(() => selected.value.size);
return {
selected,
clear,
toggle,
toggleAll,
areAllSelected,
isSelected,
count,
};
};
```
Then in `useExtensionPage` you can thin the share-related surface:
```ts
const isShareMode = ref(false);
const {
selected: selectedSharePluginNames,
clear: clearShareSelection,
toggle: toggleSharePluginSelection,
toggleAll: toggleShareSelectAll,
areAllSelected: areAllSharePluginsSelected,
isSelected: isSharePluginSelected,
count: selectedSharePluginCount,
} = useSelectionSet();
const cancelShareMode = () => {
isShareMode.value = false;
clearShareSelection();
};
const toggleShareMode = () => {
if (isShareMode.value) {
cancelShareMode();
} else {
isShareMode.value = true;
clearShareSelection();
}
};
```
You can now drop `normalizePluginNames`, `updateSelectedSharePlugins`, and reduce the number of share-selection helpers.
### 2. Consolidate share-code encode/decode/parse
Instead of `encodeShareCode`, `decodeShareCode`, and `parseShareCodeRepos` spread across the composable, you can centralize them:
```ts
// useExtensionShareCode.ts
import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from "lz-string";
const SHARE_CODE_LZ_PREFIX = "astrbot-share:";
export const useExtensionShareCode = () => {
const encode = (repos: string[]) => {
const normalized = [...new Set(
(repos || [])
.filter((r) => typeof r === "string")
.map((r) => r.trim())
.filter(Boolean),
)].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
const rawJson = JSON.stringify({ repos: normalized });
const compressed = compressToEncodedURIComponent(rawJson);
if (!compressed) return rawJson;
return `${SHARE_CODE_LZ_PREFIX}${compressed}`;
};
const decode = (code: string): string[] => {
const raw = code.startsWith(SHARE_CODE_LZ_PREFIX)
? (() => {
const encoded = code.slice(SHARE_CODE_LZ_PREFIX.length);
const decompressed = decompressFromEncodedURIComponent(encoded);
if (!decompressed) throw new Error("Invalid lz share code");
return decompressed;
})()
: code;
const parsed = JSON.parse(raw);
const repos = Array.isArray(parsed?.repos) ? parsed.repos : [];
return repos
.filter((r) => typeof r === "string")
.map((r) => r.trim())
.filter(Boolean);
};
return { encode, decode };
};
```
Usage in `useExtensionPage` becomes simpler and keeps JSON/compression details out of the main composable:
```ts
const { encode: encodeShareCode, decode: decodeShareCode } = useExtensionShareCode();
const confirmShareSelection = async () => {
const data = Array.isArray(extension_data?.data) ? extension_data.data : [];
const selectedNames = selectedSharePluginNames.value;
const repos = data
.filter((ext) => selectedNames.has(ext.name))
.map((ext) => (ext.repo ?? "").trim());
const shareCode = encodeShareCode(repos);
// clipboard + toast logic unchanged
};
const installFromShareCode = async (ignoreVersionCheck = false) => {
const rawCode = (shareCodeInput.value ?? "").trim();
if (!rawCode) {
toast(tm("messages.fillShareCode"), "error");
return;
}
let repos: string[];
try {
repos = decodeShareCode(rawCode);
} catch {
toast(tm("messages.invalidShareCode"), "error");
return;
}
// continue with batch install using the repos
};
```
This eliminates `parseShareCodeRepos` entirely and removes duplicated repo normalization.
### 3. Decouple batch install logic from UI/progress
You can keep `installReposBatch` pure and move `tm`, `resolveErrorMessage`, and progress dialog concerns into the caller:
```ts
// batchInstall.ts
export type BatchResult = {
successCount: number;
failedItems: { url: string; message: string }[];
};
export const installReposBatch = async (
repos: string[],
{
ignoreVersionCheck,
performInstall,
onProgress,
}: {
ignoreVersionCheck: boolean;
performInstall: (url: string) => Promise<{ ok: boolean; message?: string }>;
onProgress?: (info: { current: number; total: number; repoUrl: string }) => void;
},
): Promise<BatchResult> => {
let completedCount = 0;
const tasks = repos.map(async (repoUrl) => {
try {
const res = await performInstall(repoUrl);
return { success: res.ok, url: repoUrl, message: res.message };
} finally {
completedCount += 1;
onProgress?.({ current: completedCount, total: repos.length, repoUrl });
}
});
const settled = await Promise.allSettled(tasks);
let successCount = 0;
const failedItems: { url: string; message: string }[] = [];
settled.forEach((r) => {
if (r.status === "fulfilled") {
if (r.value.success) successCount += 1;
else failedItems.push({ url: r.value.url, message: r.value.message ?? "" });
} else {
failedItems.push({ url: "", message: "" }); // caller can decorate errors
}
});
return { successCount, failedItems };
};
```
Then, in `useExtensionPage`, you adapt to your existing `performInstallRequest`, `tm`, and dialog state in a small wrapper, instead of embedding everything inside `installReposBatch`:
```ts
const performInstallUrl = async (repoUrl: string) => {
try {
const res = await performInstallRequest({
source: "url",
ignoreVersionCheck,
url: repoUrl,
});
const resData = res.data || {};
return {
ok: resData.status === "ok",
message: resData.message || tm("messages.installFailed"),
};
} catch (err) {
return {
ok: false,
message: resolveErrorMessage(err, tm("messages.installFailed")),
};
}
};
const { successCount, failedItems } = await installReposBatch(uniqueRepos, {
ignoreVersionCheck,
performInstall: performInstallUrl,
onProgress: ({ current, total, repoUrl }) => {
loadingDialog.progressCurrent = current;
loadingDialog.progressTotal = total;
loadingDialog.progressLabel = repoUrl;
},
});
```
This keeps `installReposBatch` reusable and testable, and `installFromShareCode` expresses only the high-level flow (prepare repos → show dialog → call batch helper → update UI), not the internal HTTP/error details.
These extractions preserve all behavior while making `useExtensionPage` less monolithic and easier to maintain.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
@sourcery-ai review |
There was a problem hiding this comment.
Hey - 我发现了 1 个问题,并留下了一些总体反馈:
- share-code 逻辑(SHARE_CODE_LZ_PREFIX、encode/decode、parseShareCodeRepos)在 useExtensionPage 里体量比较大;可以考虑把这部分提取成一个小的工具模块,并在负载中加入一个版本字段,以便未来调整格式时更容易,同时让这个 composable 更专注于 UI/状态。
- 在有高亮文本时跳过选中切换的那层保护(window.getSelection()?.toString())在卡片和表格行点击处理里都重复出现了;你可以把它提取成一个共享的 helper,避免不同视图之间出现细微的行为偏差。
- installReposBatch 目前会并行发送所有安装请求,对于较大的 share code 可能会给后端造成压力;可以考虑增加一个简单的并发上限,或者改成顺序执行,以更好地控制负载和进度反馈。
给 AI Agents 的提示词
请根据这次代码评审中的评论进行修改:
## 整体评论
- share-code 逻辑(SHARE_CODE_LZ_PREFIX、encode/decode、parseShareCodeRepos)在 useExtensionPage 里体量比较大;可以考虑把这部分提取成一个小的工具模块,并在负载中加入一个版本字段,以便未来调整格式时更容易,同时让这个 composable 更专注于 UI/状态。
- 在有高亮文本时跳过选中切换的那层保护(window.getSelection()?.toString())在卡片和表格行点击处理里都重复出现了;你可以把它提取成一个共享的 helper,避免不同视图之间出现细微的行为偏差。
- installReposBatch 目前会并行发送所有安装请求,对于较大的 share code 可能会给后端造成压力;可以考虑增加一个简单的并发上限,或者改成顺序执行,以更好地控制负载和进度反馈。
## 单条评论
### 评论 1
<location path="dashboard/src/views/extension/useExtensionPage.js" line_range="135" />
<code_context>
return false;
};
const showReserved = ref(getInitialShowReserved());
+ const isShareMode = ref(false);
+ const selectedSharePluginNames = ref(new Set());
+ const cancelShareMode = () => {
</code_context>
<issue_to_address>
**issue (complexity):** 可以考虑把新的 share-selection、share-code 和 loading-dialog 进度相关的逻辑重构到更聚焦的 helper/工具中,在保留行为的前提下,让 `useExtensionPage` 更小、更容易快速浏览。
你可以保留所有新增行为,只是通过把强相关的状态和逻辑分组、以及简化几个 helper,来稍微降低一下复杂度。
### 1. 把 share-selection 状态收拢到一个小 helper 中
当前 `isShareMode`、`selectedSharePluginNames`、`toggleShareMode`、`cancelShareMode`、`toggleSharePluginSelection`、`toggleShareSelectAll`、`areAllSharePluginsSelected`、`isSharePluginSelected`、`selectedSharePluginCount` 和 `normalizePluginNames` 都是在 `useExtensionPage` 顶层定义的。
你可以把它们放到一个小的内部 helper/composable 中,在不改变行为的前提下缩小主函数的“表面积”:
```js
// inside useExtensionPage.js
const useShareSelection = (toast, tm, extension_data) => {
const isShareMode = ref(false);
const selectedNames = ref(new Set());
const reset = () => {
isShareMode.value = false;
selectedNames.value = new Set();
};
const toggleMode = () => {
if (isShareMode.value) {
reset();
} else {
isShareMode.value = true;
selectedNames.value = new Set();
}
};
const normalizeNames = (names = []) =>
names.filter((name) => typeof name === "string" && name.length > 0);
const togglePlugin = (pluginName) => {
if (!isShareMode.value || !pluginName) return;
const next = new Set(selectedNames.value);
next.has(pluginName) ? next.delete(pluginName) : next.add(pluginName);
selectedNames.value = next;
};
const toggleAll = (pluginNames = []) => {
if (!isShareMode.value) return;
const names = normalizeNames(pluginNames);
if (names.length === 0) return;
const allSelected = names.every((name) => selectedNames.value.has(name));
const next = new Set(selectedNames.value);
if (allSelected) {
names.forEach((name) => next.delete(name));
} else {
names.forEach((name) => next.add(name));
}
selectedNames.value = next;
};
const allSelected = (pluginNames = []) => {
const names = normalizeNames(pluginNames);
if (names.length === 0) return false;
return names.every((name) => selectedNames.value.has(name));
};
const isSelected = (pluginName) =>
!!pluginName && selectedNames.value.has(pluginName);
const selectedCount = computed(() => selectedNames.value.size);
const confirmShareSelection = async () => {
const data = Array.isArray(extension_data?.data) ? extension_data.data : [];
const repos = data
.filter((ext) => selectedNames.value.has(ext.name))
.map((ext) => (ext.repo ?? "").trim())
.filter((r) => r.length > 0);
const uniqueRepos = [...new Set(repos)].sort((a, b) =>
a.localeCompare(b, undefined, { sensitivity: "base" }),
);
if (uniqueRepos.length === 0) {
toast(tm("messages.noShareableRepos"), "warning");
reset();
return;
}
// reuse your existing encodeShareCode here
const shareCode = encodeShareCode({ repos: uniqueRepos });
try {
if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
throw new Error("Clipboard API unavailable");
}
await navigator.clipboard.writeText(shareCode);
toast(tm("messages.shareCodeCopied"), "success");
} catch (error) {
toast(`${tm("messages.shareCodeCopyFailed")} ${shareCode}`, "warning");
console.error("Failed to copy share code:", error);
} finally {
reset();
}
};
return {
isShareMode,
selectedNames,
selectedCount,
toggleMode,
reset,
togglePlugin,
toggleAll,
allSelected,
isSelected,
confirmShareSelection,
};
};
```
然后在 `useExtensionPage` 中只需要暴露一小块内聚的接口:
```js
const {
isShareMode,
selectedNames: selectedSharePluginNames,
selectedCount: selectedSharePluginCount,
toggleMode: toggleShareMode,
reset: cancelShareMode,
togglePlugin: toggleSharePluginSelection,
toggleAll: toggleShareSelectAll,
allSelected: areAllSharePluginsSelected,
isSelected: isSharePluginSelected,
confirmShareSelection,
} = useShareSelection(toast, tm, extension_data);
```
这样也就不再需要 `updateSelectedSharePlugins` 了,因为 `Set` 的克隆逻辑已经在 `togglePlugin` / `toggleAll` 中被局部化并显式处理了。
### 2. 将 share-code 协议抽取为一个工具
`encodeShareCode`、`decodeShareCode`、`parseShareCodeRepos` 一起定义了你的“share code 格式”。把这部分逻辑放在 view composable 中会让 `useExtensionPage` 更长、更难快速浏览。
你可以把这个“协议”迁移到一个小工具模块中,同时保持现有行为:
```js
// src/utils/shareCode.js
import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from "lz-string";
const SHARE_CODE_LZ_PREFIX = "astrbot-share:";
export const encodeShareCode = (payload) => {
const rawJson = JSON.stringify(payload);
const compressed = compressToEncodedURIComponent(rawJson);
if (!compressed) return rawJson;
return `${SHARE_CODE_LZ_PREFIX}${compressed}`;
};
export const decodeShareCode = (rawCode) => {
if (rawCode.startsWith(SHARE_CODE_LZ_PREFIX)) {
const encodedPart = rawCode.slice(SHARE_CODE_LZ_PREFIX.length);
const decompressed = decompressFromEncodedURIComponent(encodedPart);
if (!decompressed) throw new Error("Invalid lz share code");
return decompressed;
}
return rawCode;
};
export const parseShareCodeRepos = (rawCode) => {
const decodedContent = decodeShareCode(rawCode);
const parsed = JSON.parse(decodedContent);
const repos = Array.isArray(parsed?.repos) ? parsed.repos : [];
return repos
.filter((repo) => typeof repo === "string")
.map((repo) => repo.trim())
.filter((repo) => repo.length > 0);
};
```
然后 `useExtensionPage` 中只需要:
```js
import { encodeShareCode, parseShareCodeRepos } from "@/utils/shareCode";
// ... 在 confirmShareSelection 中使用 encodeShareCode
// ... 在 installFromShareCode 中使用 parseShareCodeRepos
```
这会让 `useExtensionPage` 更精简,同时把协议层面的改动集中到一个地方。
### 3. 将 loading dialog 的进度结构化为嵌套对象
`loadingDialog` 上的四个进度字段以及它们的重置路径增加了比较分散的可变状态。你可以保持 UI 完全不变,只是用一个嵌套的 `progress` 对象和一个工厂函数来管理:
```js
const defaultProgress = () => ({
enabled: false,
current: 0,
total: 0,
label: "",
});
const loadingDialog = reactive({
show: false,
title: "",
statusCode: 0,
result: "",
progress: defaultProgress(),
});
const resetLoadingDialog = () => {
Object.assign(loadingDialog, {
show: false,
title: tm("dialogs.loading.title"),
statusCode: 0,
result: "",
progress: defaultProgress(),
});
};
const onLoadingDialogResult = (statusCode, result, timeToClose = 2000) => {
loadingDialog.statusCode = statusCode;
loadingDialog.result = result;
loadingDialog.progress.enabled = false;
if (timeToClose === -1) return;
setTimeout(resetLoadingDialog, timeToClose);
};
```
并更新批量安装时的使用方式:
```js
loadingDialog.progress.enabled = true;
loadingDialog.progress.current = 0;
loadingDialog.progress.total = uniqueRepos.length;
loadingDialog.progress.label = "";
onProgress: ({ current, total, repoUrl }) => {
loadingDialog.progress.current = current;
loadingDialog.progress.total = total;
loadingDialog.progress.label = repoUrl;
},
```
这样可以保持现有行为,同时让响应式状态的“表面积”更小,并把重置逻辑集中到一个地方。
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据反馈改进后续评审。
Original comment in English
Hey - I've found 1 issue, and left some high level feedback:
- The share-code logic (SHARE_CODE_LZ_PREFIX, encode/decode, parseShareCodeRepos) is fairly substantial inside useExtensionPage; consider extracting it into a small utility module and adding a version field to the payload to make future format changes easier and keep the composable focused on UI/state.
- The guard that skips selection toggling when there is highlighted text (window.getSelection()?.toString()) is duplicated in both card and table row handlers; you could extract this into a shared helper to avoid subtle behavior drift between views.
- installReposBatch currently fires all install requests in parallel, which could stress the backend for large share codes; consider adding a simple concurrency limit or sequential execution to better control load and progress reporting.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The share-code logic (SHARE_CODE_LZ_PREFIX, encode/decode, parseShareCodeRepos) is fairly substantial inside useExtensionPage; consider extracting it into a small utility module and adding a version field to the payload to make future format changes easier and keep the composable focused on UI/state.
- The guard that skips selection toggling when there is highlighted text (window.getSelection()?.toString()) is duplicated in both card and table row handlers; you could extract this into a shared helper to avoid subtle behavior drift between views.
- installReposBatch currently fires all install requests in parallel, which could stress the backend for large share codes; consider adding a simple concurrency limit or sequential execution to better control load and progress reporting.
## Individual Comments
### Comment 1
<location path="dashboard/src/views/extension/useExtensionPage.js" line_range="135" />
<code_context>
return false;
};
const showReserved = ref(getInitialShowReserved());
+ const isShareMode = ref(false);
+ const selectedSharePluginNames = ref(new Set());
+ const cancelShareMode = () => {
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring the new share-selection, share-code, and loading-dialog progress logic into focused helpers/utilities to keep `useExtensionPage` smaller and easier to scan while preserving behavior.
You can keep all the new behavior but reduce complexity a bit by grouping tightly-related state and logic and simplifying a couple of helpers.
### 1. Group share-selection state into a small helper
Right now `isShareMode`, `selectedSharePluginNames`, `toggleShareMode`, `cancelShareMode`, `toggleSharePluginSelection`, `toggleShareSelectAll`, `areAllSharePluginsSelected`, `isSharePluginSelected`, `selectedSharePluginCount` and `normalizePluginNames` are all top-level in `useExtensionPage`.
You can move them into a small internal helper/composable to reduce the surface area of the main function, without changing behavior:
```js
// inside useExtensionPage.js
const useShareSelection = (toast, tm, extension_data) => {
const isShareMode = ref(false);
const selectedNames = ref(new Set());
const reset = () => {
isShareMode.value = false;
selectedNames.value = new Set();
};
const toggleMode = () => {
if (isShareMode.value) {
reset();
} else {
isShareMode.value = true;
selectedNames.value = new Set();
}
};
const normalizeNames = (names = []) =>
names.filter((name) => typeof name === "string" && name.length > 0);
const togglePlugin = (pluginName) => {
if (!isShareMode.value || !pluginName) return;
const next = new Set(selectedNames.value);
next.has(pluginName) ? next.delete(pluginName) : next.add(pluginName);
selectedNames.value = next;
};
const toggleAll = (pluginNames = []) => {
if (!isShareMode.value) return;
const names = normalizeNames(pluginNames);
if (names.length === 0) return;
const allSelected = names.every((name) => selectedNames.value.has(name));
const next = new Set(selectedNames.value);
if (allSelected) {
names.forEach((name) => next.delete(name));
} else {
names.forEach((name) => next.add(name));
}
selectedNames.value = next;
};
const allSelected = (pluginNames = []) => {
const names = normalizeNames(pluginNames);
if (names.length === 0) return false;
return names.every((name) => selectedNames.value.has(name));
};
const isSelected = (pluginName) =>
!!pluginName && selectedNames.value.has(pluginName);
const selectedCount = computed(() => selectedNames.value.size);
const confirmShareSelection = async () => {
const data = Array.isArray(extension_data?.data) ? extension_data.data : [];
const repos = data
.filter((ext) => selectedNames.value.has(ext.name))
.map((ext) => (ext.repo ?? "").trim())
.filter((r) => r.length > 0);
const uniqueRepos = [...new Set(repos)].sort((a, b) =>
a.localeCompare(b, undefined, { sensitivity: "base" }),
);
if (uniqueRepos.length === 0) {
toast(tm("messages.noShareableRepos"), "warning");
reset();
return;
}
// reuse your existing encodeShareCode here
const shareCode = encodeShareCode({ repos: uniqueRepos });
try {
if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
throw new Error("Clipboard API unavailable");
}
await navigator.clipboard.writeText(shareCode);
toast(tm("messages.shareCodeCopied"), "success");
} catch (error) {
toast(`${tm("messages.shareCodeCopyFailed")} ${shareCode}`, "warning");
console.error("Failed to copy share code:", error);
} finally {
reset();
}
};
return {
isShareMode,
selectedNames,
selectedCount,
toggleMode,
reset,
togglePlugin,
toggleAll,
allSelected,
isSelected,
confirmShareSelection,
};
};
```
Then in `useExtensionPage` you only expose a small, coherent block:
```js
const {
isShareMode,
selectedNames: selectedSharePluginNames,
selectedCount: selectedSharePluginCount,
toggleMode: toggleShareMode,
reset: cancelShareMode,
togglePlugin: toggleSharePluginSelection,
toggleAll: toggleShareSelectAll,
allSelected: areAllSharePluginsSelected,
isSelected: isSharePluginSelected,
confirmShareSelection,
} = useShareSelection(toast, tm, extension_data);
```
This also makes `updateSelectedSharePlugins` unnecessary, since the `Set` cloning logic is localised and explicit in `togglePlugin` / `toggleAll`.
### 2. Extract share-code protocol into a utility
`encodeShareCode`, `decodeShareCode`, `parseShareCodeRepos` together define your “share code format”. Keeping that logic in the view composable makes `useExtensionPage` longer and harder to scan.
You can move the protocol into a small utility while preserving all behavior:
```js
// src/utils/shareCode.js
import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from "lz-string";
const SHARE_CODE_LZ_PREFIX = "astrbot-share:";
export const encodeShareCode = (payload) => {
const rawJson = JSON.stringify(payload);
const compressed = compressToEncodedURIComponent(rawJson);
if (!compressed) return rawJson;
return `${SHARE_CODE_LZ_PREFIX}${compressed}`;
};
export const decodeShareCode = (rawCode) => {
if (rawCode.startsWith(SHARE_CODE_LZ_PREFIX)) {
const encodedPart = rawCode.slice(SHARE_CODE_LZ_PREFIX.length);
const decompressed = decompressFromEncodedURIComponent(encodedPart);
if (!decompressed) throw new Error("Invalid lz share code");
return decompressed;
}
return rawCode;
};
export const parseShareCodeRepos = (rawCode) => {
const decodedContent = decodeShareCode(rawCode);
const parsed = JSON.parse(decodedContent);
const repos = Array.isArray(parsed?.repos) ? parsed.repos : [];
return repos
.filter((repo) => typeof repo === "string")
.map((repo) => repo.trim())
.filter((repo) => repo.length > 0);
};
```
Then `useExtensionPage` just does:
```js
import { encodeShareCode, parseShareCodeRepos } from "@/utils/shareCode";
// ... use encodeShareCode in confirmShareSelection
// ... use parseShareCodeRepos in installFromShareCode
```
This shrinks `useExtensionPage` and isolates protocol changes to one place.
### 3. Structure loading dialog progress as a nested object
The four progress fields on `loadingDialog` and their reset paths add more scattered mutable state. You can keep the UI exactly the same with a nested `progress` object and a factory:
```js
const defaultProgress = () => ({
enabled: false,
current: 0,
total: 0,
label: "",
});
const loadingDialog = reactive({
show: false,
title: "",
statusCode: 0,
result: "",
progress: defaultProgress(),
});
const resetLoadingDialog = () => {
Object.assign(loadingDialog, {
show: false,
title: tm("dialogs.loading.title"),
statusCode: 0,
result: "",
progress: defaultProgress(),
});
};
const onLoadingDialogResult = (statusCode, result, timeToClose = 2000) => {
loadingDialog.statusCode = statusCode;
loadingDialog.result = result;
loadingDialog.progress.enabled = false;
if (timeToClose === -1) return;
setTimeout(resetLoadingDialog, timeToClose);
};
```
And update the batch install usage:
```js
loadingDialog.progress.enabled = true;
loadingDialog.progress.current = 0;
loadingDialog.progress.total = uniqueRepos.length;
loadingDialog.progress.label = "";
onProgress: ({ current, total, repoUrl }) => {
loadingDialog.progress.current = current;
loadingDialog.progress.total = total;
loadingDialog.progress.label = repoUrl;
},
```
This keeps the behavior but makes the reactive surface smaller, and the reset logic centralized.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
提供一个“轻量快速分享插件组合”的能力,作为整合包分发的平替方案。
用户可以在插件管理页快速选定一组插件,一键生成分享码;其他人导入该分享码后可自动批量安装对应插件仓库,实现“轻量、快捷、可复制”的分享体验。
Modifications / 改动点
本次主要实现了“分享码一键分享/导入”完整链路:
Screenshots or Test Results / 运行截图或测试结果
Checklist / 检查清单
requirements.txt和pyproject.toml文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations inrequirements.txtandpyproject.toml.Summary by Sourcery
添加基于分享代码的工作流,用于选择已安装插件进行分享,以及导入分享的插件集合,包括批量安装与进度反馈。
New Features:
Enhancements:
Build:
lz-string依赖,用于对插件分享代码进行紧凑的编码与解码。Original summary in English
Summary by Sourcery
Add a share-code based workflow for selecting installed plugins to share and importing shared plugin sets, including batch installation and progress feedback.
New Features:
Enhancements:
Build:
lz-stringdependency to compactly encode and decode plugin share codes.新功能:
增强:
构建:
lz-string依赖,用于对插件分享码进行紧凑的编码与解码。Original summary in English
Summary by Sourcery
添加基于分享代码的工作流,用于选择已安装插件进行分享,以及导入分享的插件集合,包括批量安装与进度反馈。
New Features:
Enhancements:
Build:
lz-string依赖,用于对插件分享代码进行紧凑的编码与解码。Original summary in English
Summary by Sourcery
Add a share-code based workflow for selecting installed plugins to share and importing shared plugin sets, including batch installation and progress feedback.
New Features:
Enhancements:
Build:
lz-stringdependency to compactly encode and decode plugin share codes.Original summary in English
Summary by Sourcery
添加基于分享代码的工作流,用于选择已安装插件进行分享,以及导入分享的插件集合,包括批量安装与进度反馈。
New Features:
Enhancements:
Build:
lz-string依赖,用于对插件分享代码进行紧凑的编码与解码。Original summary in English
Summary by Sourcery
Add a share-code based workflow for selecting installed plugins to share and importing shared plugin sets, including batch installation and progress feedback.
New Features:
Enhancements:
Build:
lz-stringdependency to compactly encode and decode plugin share codes.新功能:
增强:
构建:
lz-string依赖,用于对插件分享码进行紧凑的编码与解码。Original summary in English
Summary by Sourcery
添加基于分享代码的工作流,用于选择已安装插件进行分享,以及导入分享的插件集合,包括批量安装与进度反馈。
New Features:
Enhancements:
Build:
lz-string依赖,用于对插件分享代码进行紧凑的编码与解码。Original summary in English
Summary by Sourcery
Add a share-code based workflow for selecting installed plugins to share and importing shared plugin sets, including batch installation and progress feedback.
New Features:
Enhancements:
Build:
lz-stringdependency to compactly encode and decode plugin share codes.