Skip to content

Add pagination and search to AutoPluginManager's /apm l command - #1179

Merged
ACaiCat merged 2 commits into
masterfrom
copilot/add-pagination-and-search-command
Jul 18, 2026
Merged

Add pagination and search to AutoPluginManager's /apm l command#1179
ACaiCat merged 2 commits into
masterfrom
copilot/add-pagination-and-search-command

Conversation

Copilot AI commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

/apm l dumped the entire cloud plugin list (100+ entries) in one go, flooding chat/console with no way to page through it.

Pagination

  • /apm l [page] now paginates via PaginationTools, 30 plugins per page.
  • Original plugin index numbers are preserved across pages (so /apm -i <序号> still works with the number shown, regardless of page).

Search

  • New /apm s [关键词] command fuzzy-searches the cloud plugin list using Levenshtein distance on plugin names, returning the 10 closest matches.
  • Matches also retain their original index for use with /apm -i.
/apm l          # page 1, 30 plugins
/apm l 2        # page 2
/apm s essenti  # top 10 closest matches to "essenti"

Misc

  • Extracted a shared FormatPluginLine helper (used by both /apm l and /apm s) to keep index/formatting logic in one place.
  • Bumped plugin version to 2.1.0.3 and updated README changelog/command tables (zh, en-US, es-ES) and i18n templates for the new/changed strings.

Summary by Sourcery

在保持基于索引的安装流程不变的前提下,为 AutoPluginManager 云插件列表添加分页展示和模糊搜索功能,并将插件版本提升至 2.1.0.3。

新功能:

  • /apm l 云插件列表命令添加分页支持,每页显示 30 个插件
  • 引入 /apm s [keyword] 命令,对云插件进行模糊搜索,并按名称显示最接近的 10 个结果

改进:

  • 通过共享的辅助工具统一云插件列表的格式,同时保留原有的插件索引
  • 添加基于 Levenshtein 距离的工具,用于可复用的字符串模糊匹配
  • 更新游戏内 /apm l 帮助说明,并新增 /apm s 使用提示

文档:

  • 更新中文、英文和西班牙文的 README,加入分页与搜索命令的使用说明,以及 2.1.0.3 版本的更新日志条目
Original summary in English

Summary by Sourcery

Add paginated listing and fuzzy-search capabilities to the AutoPluginManager cloud plugin list while preserving index-based installation workflows and bumping the plugin version to 2.1.0.3.

New Features:

  • Add pagination support to the /apm l cloud plugin list command with 30 plugins per page
  • Introduce a /apm s [keyword] command to fuzzy-search cloud plugins and show the 10 closest matches by name

Enhancements:

  • Unify cloud plugin list formatting via a shared helper that preserves original plugin indices
  • Add a Levenshtein distance utility for reusable fuzzy string matching
  • Update in-game help text for /apm l and new /apm s usage hints

Documentation:

  • Update Chinese, English, and Spanish READMEs with pagination and search command usage and changelog entry for version 2.1.0.3

Copilot AI linked an issue Jul 18, 2026 that may be closed by this pull request
Copilot AI changed the title [WIP] Add pagination to apm l and new command apm s Add pagination and search to AutoPluginManager's /apm l command Jul 18, 2026
Copilot AI requested a review from ACaiCat July 18, 2026 07:12
@ACaiCat
ACaiCat marked this pull request as ready for review July 18, 2026 07:17

@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 - 我发现了 3 个问题,并留下了一些整体性的反馈:

  • SearchPlugins 的标题行使用了 GetString($"[{keyword}] 的搜索结果:"),这会生成一个动态的 i18n key,很可能无法被正确解析;建议改用带占位符的静态 key(例如 string.Format(GetString("[{0}] 的搜索结果:"), keyword))。
  • FormatPluginLine 中,如果当前区域文化和 zh-CN 都不在 plugin.Description 中存在,plugin.Description[cultureName] 会抛异常;建议在 key 缺失时回退到任意可用的描述,或者一个硬编码的默认字符串。
AI Agents 提示词
Please address the comments from this code review:

## Overall Comments
- `SearchPlugins` 的标题行使用了 `GetString($"[{keyword}] 的搜索结果:")`,这会生成一个动态的 i18n key,很可能无法被正确解析;建议改用带占位符的静态 key(例如 `string.Format(GetString("[{0}] 的搜索结果:"), keyword)`)。
-`FormatPluginLine` 中,如果当前区域文化和 `zh-CN` 都不在 `plugin.Description` 中存在,`plugin.Description[cultureName]` 会抛异常;建议在 key 缺失时回退到任意可用的描述,或者一个硬编码的默认字符串。

## Individual Comments

### Comment 1
<location path="src/AutoPluginManager/Internal/CommandAdapter.cs" line_range="410" />
<code_context>
+            return;
+        }
+
+        args.Player.SendInfoMessage(GetString($"[{keyword}] 的搜索结果:"));
+        foreach (var match in matches)
+        {
</code_context>
<issue_to_address>
**issue (bug_risk):** 避免使用插值后的运行时字符串作为 GetString 的 i18n 查找 key。

在这里使用插值字符串,会让查找 key 依赖用户输入,因此不会匹配任何已定义的 i18n 条目,从而破坏本地化或触发不期望的回退。请使用常量资源 key,并在翻译之后再注入 `keyword`,例如 `GetString("SearchResultHeader", keyword)``string.Format(GetString("SearchResultHeader"), keyword)`。
</issue_to_address>

### Comment 2
<location path="src/AutoPluginManager/Internal/CommandAdapter.cs" line_range="396-399" />
<code_context>
+            return;
+        }
+
+        var keyword = string.Join(" ", args.Parameters.Skip(1)).ToLower();
+        var manifest = PluginManagementContext.Instance.ClouldPluginManifests.Values.ToArray();
+        var matches = manifest
+            .Select((plugin, index) => (Plugin: plugin, Index: index + 1, Distance: Utils.LevenshteinDistance(keyword, plugin.Name.ToLower())))
+            .OrderBy(x => x.Distance)
+            .Take(10)
</code_context>
<issue_to_address>
**suggestion (bug_risk):** 在模糊搜索中使用与区域文化无关的大小写转换(并尽量避免重复调用 `ToLower`)。

由于这种归一化仅用于匹配,`ToLowerInvariant()``ToLower()` 更安全,因为它避免了区域文化相关的大小写差异。此外,`plugin.Name.ToLower()` 在每个 LINQ 项目中都会被重新计算;建议缓存一个归一化后的名称,或至少将其改为 `ToLowerInvariant()````suggestion
        var keyword = string.Join(" ", args.Parameters.Skip(1)).ToLowerInvariant();
        var manifest = PluginManagementContext.Instance.ClouldPluginManifests.Values
            .Select(plugin => new
            {
                Plugin = plugin,
                NormalizedName = plugin.Name?.ToLowerInvariant() ?? string.Empty
            })
            .ToArray();
        var matches = manifest
            .Select((entry, index) => (Plugin: entry.Plugin, Index: index + 1, Distance: Utils.LevenshteinDistance(keyword, entry.NormalizedName)))
```
</issue_to_address>

### Comment 3
<location path="src/AutoPluginManager/Utils.cs" line_range="215-237" />
<code_context>
+    /// <param name="a"></param>
+    /// <param name="b"></param>
+    /// <returns></returns>
+    public static int LevenshteinDistance(string a, string b)
+    {
+        var lenA = a.Length;
+        var lenB = b.Length;
+        var dp = new int[lenA + 1, lenB + 1];
+        for (var i = 0; i <= lenA; i++)
+        {
+            dp[i, 0] = i;
+        }
+        for (var j = 0; j <= lenB; j++)
+        {
+            dp[0, j] = j;
+        }
+        for (var i = 1; i <= lenA; i++)
+        {
+            for (var j = 1; j <= lenB; j++)
+            {
+                var cost = a[i - 1] == b[j - 1] ? 0 : 1;
+                dp[i, j] = Math.Min(Math.Min(dp[i - 1, j] + 1, dp[i, j - 1] + 1), dp[i - 1, j - 1] + cost);
+            }
+        }
+        return dp[lenA, lenB];
+    }
+
</code_context>
<issue_to_address>
**suggestion:** 建议在 `LevenshteinDistance` 中添加一些基础防护和快捷路径,以提升健壮性和效率。

当前方法假定 `a``b` 为非 null,如果不是则会抛异常。添加显式的 null 检查(例如抛出 `ArgumentNullException`)可以让 API 更安全。当任一字符串为空(`lenA == 0``lenB == 0`)时,也可以提前返回,而无需分配和填充完整的 DP 矩阵,从而避免在简单场景下做不必要的工作。

```suggestion
    public static int LevenshteinDistance(string a, string b)
    {
        if (a is null)
            throw new ArgumentNullException(nameof(a));
        if (b is null)
            throw new ArgumentNullException(nameof(b));

        var lenA = a.Length;
        var lenB = b.Length;

        if (lenA == 0)
            return lenB;
        if (lenB == 0)
            return lenA;

        var dp = new int[lenA + 1, lenB + 1];

        for (var i = 0; i <= lenA; i++)
        {
            dp[i, 0] = i;
        }

        for (var j = 0; j <= lenB; j++)
        {
            dp[0, j] = j;
        }

        for (var i = 1; i <= lenA; i++)
        {
            for (var j = 1; j <= lenB; j++)
            {
                var cost = a[i - 1] == b[j - 1] ? 0 : 1;
                dp[i, j] = Math.Min(
                    Math.Min(dp[i - 1, j] + 1, dp[i, j - 1] + 1),
                    dp[i - 1, j - 1] + cost);
            }
        }

        return dp[lenA, lenB];
    }
```
</issue_to_address>

Sourcery 对开源项目是免费的 —— 如果你觉得我们的评审有帮助,欢迎分享 ✨
帮我变得更有用!请对每条评论点击 👍 或 👎,我会根据这些反馈改进后续评审。
Original comment in English

Hey - I've found 3 issues, and left some high level feedback:

  • The SearchPlugins header line uses GetString($"[{keyword}] 的搜索结果:"), which will generate a dynamic i18n key and likely fail to resolve; consider using a static key with a placeholder (e.g. string.Format(GetString("[{0}] 的搜索结果:"), keyword)) instead.
  • In FormatPluginLine, if neither the current culture nor zh-CN exist in plugin.Description, plugin.Description[cultureName] will throw; consider falling back to any available description or a hardcoded default string when the key is missing.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `SearchPlugins` header line uses `GetString($"[{keyword}] 的搜索结果:")`, which will generate a dynamic i18n key and likely fail to resolve; consider using a static key with a placeholder (e.g. `string.Format(GetString("[{0}] 的搜索结果:"), keyword)`) instead.
- In `FormatPluginLine`, if neither the current culture nor `zh-CN` exist in `plugin.Description`, `plugin.Description[cultureName]` will throw; consider falling back to any available description or a hardcoded default string when the key is missing.

## Individual Comments

### Comment 1
<location path="src/AutoPluginManager/Internal/CommandAdapter.cs" line_range="410" />
<code_context>
+            return;
+        }
+
+        args.Player.SendInfoMessage(GetString($"[{keyword}] 的搜索结果:"));
+        foreach (var match in matches)
+        {
</code_context>
<issue_to_address>
**issue (bug_risk):** Avoid using an interpolated runtime string as the i18n lookup key for GetString.

Using an interpolated string here makes the lookup key depend on user input, so it won’t match any defined i18n entries and will break localization or trigger unwanted fallbacks. Use a constant resource key and inject `keyword` after translation, e.g. `GetString("SearchResultHeader", keyword)` or `string.Format(GetString("SearchResultHeader"), keyword)`.
</issue_to_address>

### Comment 2
<location path="src/AutoPluginManager/Internal/CommandAdapter.cs" line_range="396-399" />
<code_context>
+            return;
+        }
+
+        var keyword = string.Join(" ", args.Parameters.Skip(1)).ToLower();
+        var manifest = PluginManagementContext.Instance.ClouldPluginManifests.Values.ToArray();
+        var matches = manifest
+            .Select((plugin, index) => (Plugin: plugin, Index: index + 1, Distance: Utils.LevenshteinDistance(keyword, plugin.Name.ToLower())))
+            .OrderBy(x => x.Distance)
+            .Take(10)
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Use culture-invariant casing (and possibly avoid repeated `ToLower`) for the fuzzy search.

Since this normalization is only for matching, `ToLowerInvariant()` is safer than `ToLower()` because it avoids culture-specific casing differences. Also, `plugin.Name.ToLower()` is recomputed for every item in the LINQ; consider caching a normalized name or at least switching that to `ToLowerInvariant()` as well.

```suggestion
        var keyword = string.Join(" ", args.Parameters.Skip(1)).ToLowerInvariant();
        var manifest = PluginManagementContext.Instance.ClouldPluginManifests.Values
            .Select(plugin => new
            {
                Plugin = plugin,
                NormalizedName = plugin.Name?.ToLowerInvariant() ?? string.Empty
            })
            .ToArray();
        var matches = manifest
            .Select((entry, index) => (Plugin: entry.Plugin, Index: index + 1, Distance: Utils.LevenshteinDistance(keyword, entry.NormalizedName)))
```
</issue_to_address>

### Comment 3
<location path="src/AutoPluginManager/Utils.cs" line_range="215-237" />
<code_context>
+    /// <param name="a"></param>
+    /// <param name="b"></param>
+    /// <returns></returns>
+    public static int LevenshteinDistance(string a, string b)
+    {
+        var lenA = a.Length;
+        var lenB = b.Length;
+        var dp = new int[lenA + 1, lenB + 1];
+        for (var i = 0; i <= lenA; i++)
+        {
+            dp[i, 0] = i;
+        }
+        for (var j = 0; j <= lenB; j++)
+        {
+            dp[0, j] = j;
+        }
+        for (var i = 1; i <= lenA; i++)
+        {
+            for (var j = 1; j <= lenB; j++)
+            {
+                var cost = a[i - 1] == b[j - 1] ? 0 : 1;
+                dp[i, j] = Math.Min(Math.Min(dp[i - 1, j] + 1, dp[i, j - 1] + 1), dp[i - 1, j - 1] + cost);
+            }
+        }
+        return dp[lenA, lenB];
+    }
+
</code_context>
<issue_to_address>
**suggestion:** Consider adding basic guards/shortcuts in `LevenshteinDistance` for robustness and efficiency.

The method currently assumes `a` and `b` are non-null and will throw if they aren’t. Adding explicit null checks (e.g., throwing `ArgumentNullException`) would make the API safer. You can also early-return when either string is empty (`lenA == 0` or `lenB == 0`) instead of allocating and filling the full DP matrix, which avoids unnecessary work for trivial cases.

```suggestion
    public static int LevenshteinDistance(string a, string b)
    {
        if (a is null)
            throw new ArgumentNullException(nameof(a));
        if (b is null)
            throw new ArgumentNullException(nameof(b));

        var lenA = a.Length;
        var lenB = b.Length;

        if (lenA == 0)
            return lenB;
        if (lenB == 0)
            return lenA;

        var dp = new int[lenA + 1, lenB + 1];

        for (var i = 0; i <= lenA; i++)
        {
            dp[i, 0] = i;
        }

        for (var j = 0; j <= lenB; j++)
        {
            dp[0, j] = j;
        }

        for (var i = 1; i <= lenA; i++)
        {
            for (var j = 1; j <= lenB; j++)
            {
                var cost = a[i - 1] == b[j - 1] ? 0 : 1;
                dp[i, j] = Math.Min(
                    Math.Min(dp[i - 1, j] + 1, dp[i, j - 1] + 1),
                    dp[i - 1, j - 1] + cost);
            }
        }

        return dp[lenA, lenB];
    }
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/AutoPluginManager/Internal/CommandAdapter.cs
Comment on lines +396 to +399
var keyword = string.Join(" ", args.Parameters.Skip(1)).ToLower();
var manifest = PluginManagementContext.Instance.ClouldPluginManifests.Values.ToArray();
var matches = manifest
.Select((plugin, index) => (Plugin: plugin, Index: index + 1, Distance: Utils.LevenshteinDistance(keyword, plugin.Name.ToLower())))

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.

suggestion (bug_risk): 在模糊搜索中使用与区域文化无关的大小写转换(并尽量避免重复调用 ToLower)。

由于这种归一化仅用于匹配,ToLowerInvariant()ToLower() 更安全,因为它避免了区域文化相关的大小写差异。此外,plugin.Name.ToLower() 在每个 LINQ 项目中都会被重新计算;建议缓存一个归一化后的名称,或至少将其改为 ToLowerInvariant()

Suggested change
var keyword = string.Join(" ", args.Parameters.Skip(1)).ToLower();
var manifest = PluginManagementContext.Instance.ClouldPluginManifests.Values.ToArray();
var matches = manifest
.Select((plugin, index) => (Plugin: plugin, Index: index + 1, Distance: Utils.LevenshteinDistance(keyword, plugin.Name.ToLower())))
var keyword = string.Join(" ", args.Parameters.Skip(1)).ToLowerInvariant();
var manifest = PluginManagementContext.Instance.ClouldPluginManifests.Values
.Select(plugin => new
{
Plugin = plugin,
NormalizedName = plugin.Name?.ToLowerInvariant() ?? string.Empty
})
.ToArray();
var matches = manifest
.Select((entry, index) => (Plugin: entry.Plugin, Index: index + 1, Distance: Utils.LevenshteinDistance(keyword, entry.NormalizedName)))
Original comment in English

suggestion (bug_risk): Use culture-invariant casing (and possibly avoid repeated ToLower) for the fuzzy search.

Since this normalization is only for matching, ToLowerInvariant() is safer than ToLower() because it avoids culture-specific casing differences. Also, plugin.Name.ToLower() is recomputed for every item in the LINQ; consider caching a normalized name or at least switching that to ToLowerInvariant() as well.

Suggested change
var keyword = string.Join(" ", args.Parameters.Skip(1)).ToLower();
var manifest = PluginManagementContext.Instance.ClouldPluginManifests.Values.ToArray();
var matches = manifest
.Select((plugin, index) => (Plugin: plugin, Index: index + 1, Distance: Utils.LevenshteinDistance(keyword, plugin.Name.ToLower())))
var keyword = string.Join(" ", args.Parameters.Skip(1)).ToLowerInvariant();
var manifest = PluginManagementContext.Instance.ClouldPluginManifests.Values
.Select(plugin => new
{
Plugin = plugin,
NormalizedName = plugin.Name?.ToLowerInvariant() ?? string.Empty
})
.ToArray();
var matches = manifest
.Select((entry, index) => (Plugin: entry.Plugin, Index: index + 1, Distance: Utils.LevenshteinDistance(keyword, entry.NormalizedName)))

Comment on lines +215 to +237
public static int LevenshteinDistance(string a, string b)
{
var lenA = a.Length;
var lenB = b.Length;
var dp = new int[lenA + 1, lenB + 1];
for (var i = 0; i <= lenA; i++)
{
dp[i, 0] = i;
}
for (var j = 0; j <= lenB; j++)
{
dp[0, j] = j;
}
for (var i = 1; i <= lenA; i++)
{
for (var j = 1; j <= lenB; j++)
{
var cost = a[i - 1] == b[j - 1] ? 0 : 1;
dp[i, j] = Math.Min(Math.Min(dp[i - 1, j] + 1, dp[i, j - 1] + 1), dp[i - 1, j - 1] + cost);
}
}
return dp[lenA, lenB];
}

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.

suggestion: 建议在 LevenshteinDistance 中添加一些基础防护和快捷路径,以提升健壮性和效率。

当前方法假定 ab 为非 null,如果不是则会抛异常。添加显式的 null 检查(例如抛出 ArgumentNullException)可以让 API 更安全。当任一字符串为空(lenA == 0lenB == 0)时,也可以提前返回,而无需分配和填充完整的 DP 矩阵,从而避免在简单场景下做不必要的工作。

Suggested change
public static int LevenshteinDistance(string a, string b)
{
var lenA = a.Length;
var lenB = b.Length;
var dp = new int[lenA + 1, lenB + 1];
for (var i = 0; i <= lenA; i++)
{
dp[i, 0] = i;
}
for (var j = 0; j <= lenB; j++)
{
dp[0, j] = j;
}
for (var i = 1; i <= lenA; i++)
{
for (var j = 1; j <= lenB; j++)
{
var cost = a[i - 1] == b[j - 1] ? 0 : 1;
dp[i, j] = Math.Min(Math.Min(dp[i - 1, j] + 1, dp[i, j - 1] + 1), dp[i - 1, j - 1] + cost);
}
}
return dp[lenA, lenB];
}
public static int LevenshteinDistance(string a, string b)
{
if (a is null)
throw new ArgumentNullException(nameof(a));
if (b is null)
throw new ArgumentNullException(nameof(b));
var lenA = a.Length;
var lenB = b.Length;
if (lenA == 0)
return lenB;
if (lenB == 0)
return lenA;
var dp = new int[lenA + 1, lenB + 1];
for (var i = 0; i <= lenA; i++)
{
dp[i, 0] = i;
}
for (var j = 0; j <= lenB; j++)
{
dp[0, j] = j;
}
for (var i = 1; i <= lenA; i++)
{
for (var j = 1; j <= lenB; j++)
{
var cost = a[i - 1] == b[j - 1] ? 0 : 1;
dp[i, j] = Math.Min(
Math.Min(dp[i - 1, j] + 1, dp[i, j - 1] + 1),
dp[i - 1, j - 1] + cost);
}
}
return dp[lenA, lenB];
}
Original comment in English

suggestion: Consider adding basic guards/shortcuts in LevenshteinDistance for robustness and efficiency.

The method currently assumes a and b are non-null and will throw if they aren’t. Adding explicit null checks (e.g., throwing ArgumentNullException) would make the API safer. You can also early-return when either string is empty (lenA == 0 or lenB == 0) instead of allocating and filling the full DP matrix, which avoids unnecessary work for trivial cases.

Suggested change
public static int LevenshteinDistance(string a, string b)
{
var lenA = a.Length;
var lenB = b.Length;
var dp = new int[lenA + 1, lenB + 1];
for (var i = 0; i <= lenA; i++)
{
dp[i, 0] = i;
}
for (var j = 0; j <= lenB; j++)
{
dp[0, j] = j;
}
for (var i = 1; i <= lenA; i++)
{
for (var j = 1; j <= lenB; j++)
{
var cost = a[i - 1] == b[j - 1] ? 0 : 1;
dp[i, j] = Math.Min(Math.Min(dp[i - 1, j] + 1, dp[i, j - 1] + 1), dp[i - 1, j - 1] + cost);
}
}
return dp[lenA, lenB];
}
public static int LevenshteinDistance(string a, string b)
{
if (a is null)
throw new ArgumentNullException(nameof(a));
if (b is null)
throw new ArgumentNullException(nameof(b));
var lenA = a.Length;
var lenB = b.Length;
if (lenA == 0)
return lenB;
if (lenB == 0)
return lenA;
var dp = new int[lenA + 1, lenB + 1];
for (var i = 0; i <= lenA; i++)
{
dp[i, 0] = i;
}
for (var j = 0; j <= lenB; j++)
{
dp[0, j] = j;
}
for (var i = 1; i <= lenA; i++)
{
for (var j = 1; j <= lenB; j++)
{
var cost = a[i - 1] == b[j - 1] ? 0 : 1;
dp[i, j] = Math.Min(
Math.Min(dp[i - 1, j] + 1, dp[i, j - 1] + 1),
dp[i - 1, j - 1] + cost);
}
}
return dp[lenA, lenB];
}

@ACaiCat
ACaiCat merged commit 885c01e into master Jul 18, 2026
3 checks passed
@ACaiCat
ACaiCat deleted the copilot/add-pagination-and-search-command branch July 18, 2026 07:23
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.

apm l 能不能不要一股脑全打印了。

2 participants