Skip to content

feat(plugin): let plugins declare their marketplace category in metadata - #11094

Open
XTsat wants to merge 8 commits into
AstrBotDevs:masterfrom
XTsat:feat/plugin-metadata-category
Open

XTsat wants to merge 8 commits into
AstrBotDevs:masterfrom
XTsat:feat/plugin-metadata-category

Conversation

@XTsat

@XTsat XTsat commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

resolve #10638

插件市场的分类此前是"前端写死"的:WebUI 里维护了一份 6 个分类的 fallbackMap 和 i18n 文案,而后端 GET /api/v1/plugins/market/categories 直接返回空数组 {"categories": []}。结果是:

  • 插件无法声明自己属于哪一类,市场只能靠索引里的零散取值猜分类;
  • 分类词表分散在"市场索引 / WebUI / 运行时"三处,新增或调整一个分类要改多个地方,容易不一致;
  • 索引里出现拼错或临时取的分类值时,筛选条会多出一个一次性的分类条目,只能筛到 1 个插件。

本次改动让分类成为插件自己声明的、机器可读的稳定标识(slug),市场索引、WebUI 和运行时共用同一份词表,并把市场分类筛选从下拉框改成更适合多分类的 chip 行。

Modifications / 改动点

后端

  • 新增 astrbot/core/star/plugin_categories.py:定义 PLUGIN_CATEGORY_SLUGS(19 个规范 slug,含显示顺序)与 normalize_plugin_category()。归一化只做轻度处理(首尾空格、大小写,/-/_分隔符等价),未知取值降级为other` 并在日志中告警,不会阻断插件加载、安装或更新。
  • StarMetadata 新增 category 字段;star_manager 在加载 metadata.yaml 时归一化写入(校验阶段已告警过,故 warn=False 避免重复打日志),插件热重载时同步该字段。
  • updater.validate_plugin_metadata 对未知 category 只告警不抛错——metadata.yaml 写错分类不应让已有插件变得无法安装/更新。
  • GET /api/v1/plugins/market/categories 改为返回规范 slug 列表(仅 slug,展示文案留在前端 i18n);插件列表与详情响应新增 category 字段。

WebUI

  • 市场分类筛选由 v-select 改为可横向滚动的 chip 行:默认单行,放不下时出现展开/收起按钮,展开后换行显示全部分类;wheel 事件被映射为横向滚动,滚到头不会带着整页跳动;窄屏下分类独占一行、排序控件折行保留自身宽度。
  • chip 顺序随后端词表(旧入口「分类」下拉框 → 新位置为市场列表上方的分类 chip 行);不在词表内的取值统一折叠进 other,避免出现一次性分类;缺失翻译时把 slug 渲染成可读单词而不是原始标识。
  • 补齐 13 个新分类在 zh-CN / en-US / ja-JP / ru-RU 的文案;刷新按钮移到标题栏并改为 tooltip 按钮;新增 pluginApi.marketCategories() 客户端封装。

文档

  • docs/zh|en/dev/star/plugin-new.md:新增「声明插件分类(Optional)」章节,含 19 个标识、中文释义、适用场景,以及归一化与未知值降级规则。
  • docs/zh|en/dev/star/plugin-publish.md:metadata.yaml 模板中补充 category 可选字段示例。
  • docs/zh|en/use/plugin.md:更新市场分类筛选的操作说明。
  • docs/zh|en/dev/plugin-market/2026-06-27.md:说明市场索引中 category 的来源与 other 兜底规则。

测试

  • 新增 tests/unit/test_plugin_categories.py:全量 slug 通过、拼写归一化、空值不降级、非法值降级并告警、warn=False 抑制告警。
  • tests/test_plugin_manager.py:加载保留分类、Group Games 归一化为 group_games、未知值降级 other、未声明为 None、更新校验不阻断、加载只告警一次、热重载保持分类。
  • tests/test_dashboard.py / tests/test_fastapi_v1_dashboard.py:插件列表/详情返回 category,market categories 接口返回规范 slug 列表。

兼容性

  • category 是可选字段。未声明的插件 category=None,市场侧按 other 展示,现有插件无需任何迁移。

  • 后端接口只把 categories 从空数组改为 slug 列表,字段名与结构不变;前端仍是先读插件列表再统计计数,接口不可用时自动回退到"按发现顺序"。

  • 未引入新依赖,pyproject.toml / requirements.txt 未改动。

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

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

SnowShot_Video_2026-11-11_20-59-33.mp4
image image

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 checked the affected WebUI instructions and screenshots in docs/zh and docs/en against the changed navigation, page structure, and labels, and updated them in this PR (or explained why no documentation update is needed). For renamed, moved, or merged entry points, I included an old entry → new entry mapping in the documentation and changelog.
    / 我已对照变化后的 WebUI 入口、页面结构和术语,核对并在本 PR 中更新 docs/zh 和 docs/en 的相关操作说明与截图(或说明无需更新文档的原因)。入口改名、移动或合并时,已在文档和 changelog 中补充 旧入口 → 新入口 对照。

  • 🤓 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.
    / 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到 requirements.txt 和 pyproject.toml 文件相应位置。

  • 😮 My changes do not introduce malicious code.
    / 我的更改没有引入恶意代码。

Summary by Sourcery

Enable plugins to declare stable marketplace categories and use the shared category vocabulary throughout the runtime, APIs, WebUI, and documentation.

New Features:

  • Allow plugins to declare a normalized marketplace category in metadata and expose it through plugin APIs.
  • Provide canonical marketplace category slugs and localized labels across the backend and WebUI.
  • Replace the marketplace category selector with responsive, ordered, expandable filter chips.

Bug Fixes:

  • Prevent unsupported or ad hoc plugin categories from creating one-off marketplace filters while keeping installation and updates non-blocking.

Enhancements:

  • Synchronize plugin category metadata across loading, hot reload, marketplace listings, and detail responses.
  • Add localized category guidance and usage documentation for plugin authors and marketplace users.

Documentation:

  • Document the optional plugin category field, supported identifiers, normalization behavior, fallback handling, and marketplace filtering workflow in English and Chinese documentation.

Tests:

  • Add coverage for category normalization, fallback warnings, metadata loading, plugin updates, hot reload, API responses, and canonical category listing.

Plugins could not declare their own marketplace category: metadata.yaml was
never read for a category, so the marketplace fell back to review-assigned
categories and most plugins ended up in "other".

Add a canonical category list (astrbot/core/star/plugin_categories.py), keep
the declared value on StarMetadata, and validate it on every metadata entry
point (loading, repository inspection, archive inspection, install, update).
Unsupported or malformed declarations only log a warning and degrade to
"other" so that a typo cannot make a plugin uninstallable.
/plugins/market/categories used to return an empty list, so the WebUI had to
invent its filter entries from whatever slugs a marketplace happened to
contain. Return the canonical slug list instead so the marketplace, the WebUI
and the runtime share one vocabulary.

The response is the existing `Ok` envelope, so the OpenAPI document and the
generated client stay unchanged.
The market category select read whatever `category` values the current registry
happened to contain, so unknown or mistyped values created one-off filter
entries, competed with search and sort for toolbar width, and showed the raw
slug when a translation was missing.

Read the vocabulary from /plugins/market/categories instead, keep its order, and
collapse unknown values into "other". Replace the select with a single
scrollable chip row that hides its scrollbar: pointing at it and using the wheel
scrolls the row, and when the chips overflow a toggle on the right expands every
category into wrapped rows. Below the small-screen breakpoint the chips take the
first line and the sort control wraps onto a second one. The "全部插件" heading is
gone, refresh now sits beside the source button, and every supported slug has a
label in all four locales.
Add the `category` section to both plugin development guides, the
metadata.yaml template in the publishing guide and the marketplace index
docs, so plugin authors can find the supported slugs.

The Chinese section lives in the current guide (plugin-new.md) to match the
English page; the legacy guide is left untouched and the marketplace index
link points at the current guide.
"entertainment" covered 398 plugins in the current registry, mixing in-chat
games, connectors for other games and generic entertainment, so the category
could not be used to find anything.

Keep "entertainment" for generic entertainment that is not a game, keep
"group_games" for games whose rules the plugin implements itself, and add
"game_integration" for plugins built around another game: connectors, wikis and
data lookups, server status and player actions. Add "ai_context" for plugins
that shape conversation context across turns (persona, context compression and
injection, reply style), which had no home outside "long_term_memory".

Order the vocabulary by cluster and keep related slugs adjacent: the AI cluster
leads, "long_term_memory" and "knowledge_base" sit directly under it,
"platform_adapter" follows "integrations", the entertainment cluster closes the
list and "other" stays last. Every locale gets a label for both new slugs so
the market filter never falls back to the raw identifier.
Drop the claim that categories are no longer assigned during review, write the
identifiers in the space-separated spelling (spaces, underscores, hyphens and
mixed case are equivalent) and document every category with its localized label
and what it is meant for, so plugin authors pick the narrower one. Labels
follow the dashboard bundles verbatim, including 外部集成 and AI Conversation,
and the table repeats the runtime display order.

Adjacent categories state their boundaries explicitly: one-shot text work is
"ai_tools", stored memories are "long_term_memory" and external documents are
"knowledge_base". Keep the identifier and label columns on one line with a
page-scoped style so the spaced identifiers do not wrap mid-phrase.

The user guide still described the market filter as six fixed categories;
it now follows the plugin's declared category row in both locales.
getMarketCategoryLabel carried a hardcoded English map for the seven original
categories, so a missing translation produced a different label style for those
seven than for the twelve newer ones (and disagreed with the "never fall back to
the raw identifier" promise of the category filter).

Drop the map and the raw-category pass-through: an unknown category now always
renders as words derived from the slug, which also reads better than the raw
value for ad-hoc marketplace categories.
The warning spelled out all nineteen supported slugs on every emit, so a single
typo produced a 336-character log line that pushed the useful part out of view.
Keep the message to the offending value and the fallback; the supported list is
documented in the plugin development guide.
@sourcery-ai

sourcery-ai Bot commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

Linked issues

Checked at commit 28b5b1c

#10638 [Feature] 插件元数据新增分类声明 · 3 of 4 met · 1 to check by hand

# Requirement Status Notes
1 Allow plugin authors to declare an optional category in metadata. ✅ Met Adds the optional category field to plugin metadata handling and documents how authors declare it.
2 Use metadata as the sole source of plugin marketplace categories. 👀 Check by hand (external system) Verify that the live marketplace index takes each plugin's category from its metadata.yaml; this repository fetches the index as external data and does not enforce that sourcing.
3 Provide quick category filtering in the plugin installation market, consistent with the web market. ✅ Met Replaces the market category select with category chips and retains category-based filtering.
4 Provide a category for adapters that add platform support. ✅ Met The canonical vocabulary includes platform_adapter and documents its intended use.

@sourcery-ai sourcery-ai Bot left a comment

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.

Hey - I've found 8 issues

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="244-246" />
<code_context>
+    const slug = normalized.replace(/[\s-]+/g, "_");
+    // Unknown slugs collapse into "other" so that a typo or an ad-hoc value in
+    // a marketplace index cannot create a one-off filter entry.
+    if (
+      marketCategorySlugs.value.length > 0 &&
+      !marketCategorySlugs.value.includes(slug)
+    ) {
+      return "other";
</code_context>
<issue_to_address>
**Unsupported categories become separate chips**

When the categories request fails or returns an empty list while the market index contains an unsupported category, `normalizeMarketCategory` skips its allowlist check when `marketCategorySlugs` is empty, so `marketCategoryItems` exposes the unsupported value as a one-off filter chip instead of folding it into `other`.

Ensure unsupported marketplace categories normalize to `other` even when the categories vocabulary is empty.
</issue_to_address>

### Comment 2
<location path="astrbot/core/star/plugin_categories.py" line_range="61" />
<code_context>
+        return None
+
+    if isinstance(value, str):
+        slug = value.strip().lower().replace("-", "_").replace(" ", "_")
+        if slug in PLUGIN_CATEGORY_SLUGS:
+            return slug
</code_context>
<issue_to_address>
**Category spellings normalize differently**

When a category declaration contains repeated or mixed whitespace and hyphen separators, `normalize_plugin_category` replaces separators individually, so repeated separators produce a different slug or fall back to `other`, while the WebUI collapses separator runs and treats the same value as `ai_tools`; runtime and market then disagree on the category.

Normalize separator runs in `normalize_plugin_category` the same way the WebUI does.

Also at `dashboard/src/views/extension/useExtensionPage.js:241`.
</issue_to_address>

### Comment 3
<location path="dashboard/src/views/extension/useExtensionPage.js" line_range="2383" />
<code_context>
     }
     loading_.value = true;
     try {
+      await loadMarketCategories();
       await getExtensions({ withLoading: false });

</code_context>
<issue_to_address>
**Market initialization can hang**

When the categories endpoint connection stalls without returning or rejecting, `loadMarketCategories` awaits an Axios request with no configured timeout, and `onMounted` awaits it before calling `getExtensions` or loading market data. If the request stalls without settling, initialization remains pending and the plugin page stays in its loading state.

Give the request a bounded timeout or avoid making page initialization wait for it.
</issue_to_address>

### Comment 4
<location path="astrbot/core/star/star.py" line_range="72-74" />
<code_context>
     astrbot_version: str | None = None
     """插件要求的 AstrBot 版本范围(PEP 440 specifier,如 >=4.13.0,<4.17.0)"""

+    category: str | None = None
+    """Plugin market category slug declared in metadata (None when undeclared)."""
+
     i18n: dict[str, dict] = field(default_factory=dict)
     """插件自带的国际化文案,按 locale 分组。"""
</code_context>
<issue_to_address>
**Positional metadata callers misbind fields**

When an external consumer passes `i18n` or later fields positionally, a caller constructing the exported `StarMetadata` dataclass with positional arguments through `i18n` now binds its old `i18n` argument to `category` (and shifts later arguments), so existing callers receive incorrect metadata or a type error.

Make the new field keyword-only or append it after the existing positional fields.
</issue_to_address>

### Comment 5
<location path="dashboard/src/views/extension/useExtensionPage.js" line_range="224" />
<code_context>
+  const loadMarketCategories = async () => {
+    try {
+      const res = await pluginApi.marketCategories();
+      const slugs = res.data?.data?.categories;
+      marketCategorySlugs.value = Array.isArray(slugs)
+        ? slugs.map((slug) => String(slug || "").trim()).filter(Boolean)
</code_context>
<issue_to_address>
**Declared categories do not reach market records**

When a plugin author changes only `metadata.yaml` and the corresponding marketplace registry record is not separately updated, the category request supplies only the allowed slug list; marketplace entries still get their category from the registry record, so changing a plugin’s `metadata.yaml` alone does not change its market filter category. Authors or index maintainers must separately update the registry, and the declared category is not reflected in the market until that happens.

Add or document an explicit registry-update path that copies the normalized metadata category into the marketplace record.
</issue_to_address>

### Comment 6
<location path="docs/en/dev/star/plugin-new.md" line_range="156" />
<code_context>
+category: media processing
+```
+
+`category` takes one of the following identifiers. The English label is only the display text, so writing it back is not accepted.
+
+| Identifier | English label | Use it for |
</code_context>
<issue_to_address>
**Valid category spelling is documented as invalid**

The guide says writing an English label back is not accepted, but `normalize_plugin_category` accepts `AI Tools` as `ai_tools`; plugin authors following the instruction can be misled about which metadata values are valid.

Clarify that localized labels are not accepted, while spellings that normalize to a supported slug (including `AI Tools`) are accepted.
</issue_to_address>

### Comment 7
<location path="dashboard/src/views/extension/MarketPluginsTab.vue" line_range="367-407" />
<code_context>
-          <h2>
-            {{ tm("market.allPlugins") }}
-          </h2>
+      <div class="market-category-row">
+        <div
+          v-if="hasCategories"
+          class="market-category-chips"
+          :class="{ 'is-expanded': categoryChipsExpanded }"
+        >
+          <div
+            ref="chipsRowEl"
+            class="market-category-chips-scroll"
+            @wheel="onCategoryChipsWheel"
+          >
+            <button
+              v-for="item in marketCategoryItems"
+              :key="item.value"
+              type="button"
+              class="market-category-chip"
+              :class="{ 'is-active': marketCategoryFilter === item.value }"
+              @click="marketCategoryFilter = item.value"
+            >
+              {{ item.label }}
+              <span class="market-category-chip-count">{{ item.count }}</span>
+            </button>
+          </div>
           <v-btn
+            v-if="chipsToggleVisible"
</code_context>
<issue_to_address>
**Filter move lacks mapping**

When the category filter is moved from the toolbar dropdown to the chip row, the category filter moves from a toolbar dropdown to a chip row above the market list, but the documentation and changelog do not record the old dropdown → new chip row mapping required for this UI move.

Add the old dropdown → new chip row mapping to the relevant Chinese and English docs and changelogs.
</issue_to_address>

### Comment 8
<location path="tests/unit/test_plugin_categories.py" line_range="1-79" />
<code_context>
+"""Tests for astrbot.core.star.plugin_categories module."""
</code_context>
<issue_to_address>
**New test file added**

When this PR adds `tests/unit/test_plugin_categories.py`, adding `tests/unit/test_plugin_categories.py` creates a test file, contrary to the repository instruction.

Remove the new test file to follow the repository guidance.
</issue_to_address>

Sourcery assessment

Approval pending. 5 findings to address first.

Blocking findings: dashboard/src/views/extension/useExtensionPage.js:246, astrbot/core/star/plugin_categories.py:61, dashboard/src/views/extension/useExtensionPage.js:2383, astrbot/core/star/star.py:74, dashboard/src/views/extension/useExtensionPage.js:224


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment on lines +244 to +246
if (
marketCategorySlugs.value.length > 0 &&
!marketCategorySlugs.value.includes(slug)

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.

🟡 Medium · Unsupported categories become separate chips

When the categories request fails or returns an empty list while the market index contains an unsupported category, normalizeMarketCategory skips its allowlist check when marketCategorySlugs is empty, so marketCategoryItems exposes the unsupported value as a one-off filter chip instead of folding it into other.

Ensure unsupported marketplace categories normalize to other even when the categories vocabulary is empty.

Prompt for AI agents
In `dashboard/src/views/extension/useExtensionPage.js` at lines 244-246:

**Unsupported categories become separate chips**

When the categories request fails or returns an empty list while the market index contains an unsupported category, `normalizeMarketCategory` skips its allowlist check when `marketCategorySlugs` is empty, so `marketCategoryItems` exposes the unsupported value as a one-off filter chip instead of folding it into `other`.

Ensure unsupported marketplace categories normalize to `other` even when the categories vocabulary is empty.

return None

if isinstance(value, str):
slug = value.strip().lower().replace("-", "_").replace(" ", "_")

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.

🟡 Medium · Category spellings normalize differently

When a category declaration contains repeated or mixed whitespace and hyphen separators, normalize_plugin_category replaces separators individually, so repeated separators produce a different slug or fall back to other, while the WebUI collapses separator runs and treats the same value as ai_tools; runtime and market then disagree on the category.

Normalize separator runs in normalize_plugin_category the same way the WebUI does.

Also at dashboard/src/views/extension/useExtensionPage.js:241.

Prompt for AI agents
In `astrbot/core/star/plugin_categories.py` at line 61:

**Category spellings normalize differently**

When a category declaration contains repeated or mixed whitespace and hyphen separators, `normalize_plugin_category` replaces separators individually, so repeated separators produce a different slug or fall back to `other`, while the WebUI collapses separator runs and treats the same value as `ai_tools`; runtime and market then disagree on the category.

Normalize separator runs in `normalize_plugin_category` the same way the WebUI does.

Also at `dashboard/src/views/extension/useExtensionPage.js:241`.

}
loading_.value = true;
try {
await loadMarketCategories();

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.

🟡 Medium · Market initialization can hang

When the categories endpoint connection stalls without returning or rejecting, loadMarketCategories awaits an Axios request with no configured timeout, and onMounted awaits it before calling getExtensions or loading market data. If the request stalls without settling, initialization remains pending and the plugin page stays in its loading state.

Give the request a bounded timeout or avoid making page initialization wait for it.

Prompt for AI agents
In `dashboard/src/views/extension/useExtensionPage.js` at line 2383:

**Market initialization can hang**

When the categories endpoint connection stalls without returning or rejecting, `loadMarketCategories` awaits an Axios request with no configured timeout, and `onMounted` awaits it before calling `getExtensions` or loading market data. If the request stalls without settling, initialization remains pending and the plugin page stays in its loading state.

Give the request a bounded timeout or avoid making page initialization wait for it.

Comment thread astrbot/core/star/star.py
Comment on lines +72 to +74
category: str | None = None
"""Plugin market category slug declared in metadata (None when undeclared)."""

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.

🟡 Medium · Positional metadata callers misbind fields

When an external consumer passes i18n or later fields positionally, a caller constructing the exported StarMetadata dataclass with positional arguments through i18n now binds its old i18n argument to category (and shifts later arguments), so existing callers receive incorrect metadata or a type error.

Make the new field keyword-only or append it after the existing positional fields.

Prompt for AI agents
In `astrbot/core/star/star.py` at lines 72-74:

**Positional metadata callers misbind fields**

When an external consumer passes `i18n` or later fields positionally, a caller constructing the exported `StarMetadata` dataclass with positional arguments through `i18n` now binds its old `i18n` argument to `category` (and shifts later arguments), so existing callers receive incorrect metadata or a type error.

Make the new field keyword-only or append it after the existing positional fields.

const loadMarketCategories = async () => {
try {
const res = await pluginApi.marketCategories();
const slugs = res.data?.data?.categories;

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.

🟡 Medium · Declared categories do not reach market records

When a plugin author changes only metadata.yaml and the corresponding marketplace registry record is not separately updated, the category request supplies only the allowed slug list; marketplace entries still get their category from the registry record, so changing a plugin’s metadata.yaml alone does not change its market filter category. Authors or index maintainers must separately update the registry, and the declared category is not reflected in the market until that happens.

Add or document an explicit registry-update path that copies the normalized metadata category into the marketplace record.

Prompt for AI agents
In `dashboard/src/views/extension/useExtensionPage.js` at line 224:

**Declared categories do not reach market records**

When a plugin author changes only `metadata.yaml` and the corresponding marketplace registry record is not separately updated, the category request supplies only the allowed slug list; marketplace entries still get their category from the registry record, so changing a plugin’s `metadata.yaml` alone does not change its market filter category. Authors or index maintainers must separately update the registry, and the declared category is not reflected in the market until that happens.

Add or document an explicit registry-update path that copies the normalized metadata category into the marketplace record.

category: media processing
```

`category` takes one of the following identifiers. The English label is only the display text, so writing it back is not accepted.

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.

⚪ Low · Valid category spelling is documented as invalid

The guide says writing an English label back is not accepted, but normalize_plugin_category accepts AI Tools as ai_tools; plugin authors following the instruction can be misled about which metadata values are valid.

Clarify that localized labels are not accepted, while spellings that normalize to a supported slug (including AI Tools) are accepted.

Prompt for AI agents
In `docs/en/dev/star/plugin-new.md` at line 156:

**Valid category spelling is documented as invalid**

The guide says writing an English label back is not accepted, but `normalize_plugin_category` accepts `AI Tools` as `ai_tools`; plugin authors following the instruction can be misled about which metadata values are valid.

Clarify that localized labels are not accepted, while spellings that normalize to a supported slug (including `AI Tools`) are accepted.

Comment on lines +367 to 407
<div class="market-category-row">
<div
v-if="hasCategories"
class="market-category-chips"
:class="{ 'is-expanded': categoryChipsExpanded }"
>
<div
ref="chipsRowEl"
class="market-category-chips-scroll"
@wheel="onCategoryChipsWheel"
>
<button
v-for="item in marketCategoryItems"
:key="item.value"
type="button"
class="market-category-chip"
:class="{ 'is-active': marketCategoryFilter === item.value }"
@click="marketCategoryFilter = item.value"
>
{{ item.label }}
<span class="market-category-chip-count">{{ item.count }}</span>
</button>
</div>
<v-btn
v-if="chipsToggleVisible"
icon
size="x-small"
variant="text"
@click="refreshPluginMarket"
:loading="loading_ || refreshingMarket"
:disabled="loading_ || refreshingMarket"
class="market-category-chips-toggle"
:aria-label="
categoryChipsExpanded
? tm('market.collapseCategories')
: tm('market.expandCategories')
"
@click="toggleCategoryChips"
>
<v-icon>mdi-refresh</v-icon>
<v-icon size="18">
{{ categoryChipsExpanded ? "mdi-chevron-up" : "mdi-chevron-down" }}
</v-icon>
</v-btn>
</div>

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.

⚪ Low · Filter move lacks mapping

When the category filter is moved from the toolbar dropdown to the chip row, the category filter moves from a toolbar dropdown to a chip row above the market list, but the documentation and changelog do not record the old dropdown → new chip row mapping required for this UI move.

Add the old dropdown → new chip row mapping to the relevant Chinese and English docs and changelogs.

Prompt for AI agents
In `dashboard/src/views/extension/MarketPluginsTab.vue` at lines 367-407:

**Filter move lacks mapping**

When the category filter is moved from the toolbar dropdown to the chip row, the category filter moves from a toolbar dropdown to a chip row above the market list, but the documentation and changelog do not record the old dropdown → new chip row mapping required for this UI move.

Add the old dropdown → new chip row mapping to the relevant Chinese and English docs and changelogs.

From CONTRIBUTING.md L42, CONTRIBUTING.md L121:

- 入口改名、移动或合并时,在对应文档和 `changelogs/` 中写明「旧入口 → 新入口」,并同步维护 [WebUI 入口对照](docs/zh/use/webui.md#菜单与旧入口对照)。截图若仍展示旧结构,应更新或移除,并用可独立完成操作的文字步骤替代。

Comment on lines +1 to +79
"""Tests for astrbot.core.star.plugin_categories module."""

import pytest

from astrbot.core.star import plugin_categories
from astrbot.core.star.plugin_categories import (
DEFAULT_PLUGIN_CATEGORY,
PLUGIN_CATEGORY_SLUGS,
normalize_plugin_category,
)


class TestNormalizePluginCategory:
"""Test cases for normalize_plugin_category."""

@pytest.mark.parametrize("slug", PLUGIN_CATEGORY_SLUGS)
def test_accepts_supported_slug(self, slug: str):
"""Every supported slug is returned unchanged."""
assert normalize_plugin_category(slug) == slug

@pytest.mark.parametrize(
("raw", "expected"),
[
(" ai_tools ", "ai_tools"),
("AI_TOOLS", "ai_tools"),
("AI Tools", "ai_tools"),
("ai-tools", "ai_tools"),
("Long-term Memory", "long_term_memory"),
],
)
def test_normalizes_slug_spelling(self, raw: str, expected: str):
"""Case, whitespace and separators do not change the resulting slug."""
assert normalize_plugin_category(raw) == expected

@pytest.mark.parametrize("raw", [None, "", " "])
def test_missing_category_stays_undeclared(self, raw):
"""An absent category is reported as None instead of the fallback."""
assert normalize_plugin_category(raw) is None

@pytest.mark.parametrize(
"raw",
["not_a_category", "工具", 1, ["ai_tools"], {"name": "ai_tools"}],
)
def test_unsupported_category_falls_back(self, raw):
"""Unsupported values degrade to the fallback category."""
assert normalize_plugin_category(raw) == DEFAULT_PLUGIN_CATEGORY

def test_unsupported_category_warns(self, monkeypatch: pytest.MonkeyPatch):
"""Degrading an unsupported value is reported in the log."""
warnings: list[str] = []
monkeypatch.setattr(
plugin_categories.logger,
"warning",
lambda message: warnings.append(message),
)

assert normalize_plugin_category("Not A Category") == DEFAULT_PLUGIN_CATEGORY
assert len(warnings) == 1
assert "Not A Category" in warnings[0]

def test_unsupported_category_can_skip_the_warning(
self, monkeypatch: pytest.MonkeyPatch
):
"""Callers that already validated the metadata can suppress the report."""
warnings: list[str] = []
monkeypatch.setattr(
plugin_categories.logger,
"warning",
lambda message: warnings.append(message),
)

assert (
normalize_plugin_category("Not A Category", warn=False)
== DEFAULT_PLUGIN_CATEGORY
)
assert warnings == []

def test_supported_category_does_not_warn(self, monkeypatch: pytest.MonkeyPatch):
"""A supported category is accepted silently."""

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.

⚪ Low · New test file added

When this PR adds tests/unit/test_plugin_categories.py, adding tests/unit/test_plugin_categories.py creates a test file, contrary to the repository instruction.

Remove the new test file to follow the repository guidance.

Prompt for AI agents
In `tests/unit/test_plugin_categories.py` at lines 1-79:

**New test file added**

When this PR adds `tests/unit/test_plugin_categories.py`, adding `tests/unit/test_plugin_categories.py` creates a test file, contrary to the repository instruction.

Remove the new test file to follow the repository guidance.

From .github/copilot-instructions.md L27:

Do not generate test files for now.

@XTsat

XTsat commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

新分类在过渡阶段可能遇到问题,有这几种过渡方案

  1. 全部分类都暂时归为其他(忽略市场分类)
  2. 建立映射表把上游的中文暂时转换(市场现有分类转换新分类)
  3. 完全不动,然后新的分类和老的分类目前同时存在(过渡的阵痛)

目前采取的方案 3,最好是把插件市场的分类清空

或者至少将分类数量少于 10 的旧插件清空,有很多旧插件分类只有一个插件的情况

501738a063560cb48ef0f759fd00d935

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

This branch has not been deployed

No deployments
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.

[Feature] 插件元数据新增分类声明

2 participants