Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion dashboard/src/views/ExtensionPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ const {
filteredExtensions,
filteredPlugins,
filteredMarketPlugins,
getMarketPluginKey,
sortedPlugins,
RANDOM_PLUGINS_COUNT,
randomPlugins,
Expand Down Expand Up @@ -217,11 +218,18 @@ const selectedMarketPlugin = computed(() => {
? pluginMarketData.value
: [];
const installedPlugin = selectedInstalledPlugin.value;
// Resolve by the unique market plugin key first; the `name` match is a
// fallback for legacy deep links, since multiple market entries can share
// the same metadata name.
const marketKeyMatch =
market.find((item) => getMarketPluginKey(item) === selectedPluginId.value) ||
null;
const marketNameMatch =
market.find((item) => item.name === selectedPluginId.value) || null;
const marketMatch = marketKeyMatch || marketNameMatch;

if (selectedDetailTab.value === "market" || !installedPlugin) {
return marketNameMatch;
return marketMatch;
}

const repo = normalizeRepoUrl(installedPlugin.repo);
Expand Down
12 changes: 8 additions & 4 deletions dashboard/src/views/extension/MarketPluginsTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ const {
randomPluginNames,
marketCategoryFilter,
marketCategoryItems,
getMarketPluginKey,
normalizeStr,
toPinyinText,
toInitials,
Expand Down Expand Up @@ -174,11 +175,14 @@ const marketCategorySelectItems = computed(() =>
})),
);

// Navigate with the unique market plugin key instead of the metadata name —
// two market entries can share the same `name`.
const openMarketPluginDetail = (plugin) => {
if (!plugin?.name) return;
const pluginKey = getMarketPluginKey(plugin);
if (!pluginKey) return;
router.push({
name: "ExtensionMarketDetails",
params: { pluginId: plugin.name },
params: { pluginId: pluginKey },
});
};
</script>
Expand Down Expand Up @@ -339,7 +343,7 @@ const openMarketPluginDetail = (plugin) => {
<v-row style="min-height: 26rem" dense>
<v-col
v-for="plugin in paginatedPlugins"
:key="plugin.name"
:key="getMarketPluginKey(plugin)"
cols="12"
md="6"
lg="4"
Expand Down Expand Up @@ -387,7 +391,7 @@ const openMarketPluginDetail = (plugin) => {
<v-row class="mb-6" dense>
<v-col
v-for="plugin in randomPlugins"
:key="`random-${plugin.name}`"
:key="getMarketPluginKey(plugin)"
cols="12"
md="6"
lg="4"
Expand Down
23 changes: 19 additions & 4 deletions dashboard/src/views/extension/useExtensionPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -432,11 +432,13 @@ export const useExtensionPage = (initialTab = "installed") => {
const allPlugins = pluginMarketData.value;
if (allPlugins.length === 0) return [];

const pluginsByName = new Map(
allPlugins.map((plugin) => [plugin.name, plugin]),
// Key by the unique market plugin key: metadata `name` is not unique and
// would collapse same-named plugins into one entry.
const pluginsByKey = new Map(
allPlugins.map((plugin) => [getMarketPluginKey(plugin), plugin]),
);
const selected = randomPluginNames.value
.map((name) => pluginsByName.get(name))
.map((key) => pluginsByKey.get(key))
.filter(Boolean);

if (selected.length > 0) {
Expand All @@ -462,7 +464,7 @@ export const useExtensionPage = (initialTab = "installed") => {
const shuffled = shufflePlugins(pluginMarketData.value);
randomPluginNames.value = shuffled
.slice(0, Math.min(RANDOM_PLUGINS_COUNT, shuffled.length))
.map((plugin) => plugin.name);
.map((plugin) => getMarketPluginKey(plugin));
};

// 分页计算属性
Expand Down Expand Up @@ -686,6 +688,17 @@ export const useExtensionPage = (initialTab = "installed") => {
return String(plugin?.market_plugin_id || "").trim();
};

// Unique identity for a market entry. Metadata `name` alone is not unique —
// different authors can publish plugins with the same name — so fall back to
// repo and finally name for legacy registry entries without an explicit id.
const getMarketPluginKey = (plugin) => {
return (
getMarketPluginId(plugin) ||
String(plugin?.repo || "").trim() ||
String(plugin?.name || "").trim()
);
};
Comment on lines +691 to +700

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): market_plugin_id || repo || name is not a globally unique key because values from different fallback levels share the same namespace; an entry whose repo equals another entry's market_plugin_id or name produces the same key. Duplicate or empty fallback values also collapse entries in the random-plugin Map, create duplicate Vue keys, and cause detail lookup to select the first entry.

Triggers: When a registry contains entries with colliding fallback values, duplicate repository values, or entries missing all three identity fields.

Suggested fix: Prefix the selected identity with its source field (for example, id:, repo:, or name:), and define a deterministic handling strategy for entries whose resulting identity is still not unique.


const getMarketInstallSourcePayload = () => {
const plugin = selectedMarketInstallPlugin.value;
if (
Expand Down Expand Up @@ -2574,6 +2587,8 @@ export const useExtensionPage = (initialTab = "installed") => {
randomPluginNames,
normalizeStr,
toPinyinText,
getMarketPluginId,
getMarketPluginKey,
toInitials,
filteredExtensions,
filteredPlugins,
Expand Down