Add pagination and search to AutoPluginManager's /apm l command - #1179
Conversation
/apm l command
There was a problem hiding this comment.
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>帮我变得更有用!请对每条评论点击 👍 或 👎,我会根据这些反馈改进后续评审。
Original comment in English
Hey - I've found 3 issues, and left some high level feedback:
- The
SearchPluginsheader line usesGetString($"[{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 norzh-CNexist inplugin.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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| 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()))) |
There was a problem hiding this comment.
suggestion (bug_risk): 在模糊搜索中使用与区域文化无关的大小写转换(并尽量避免重复调用 ToLower)。
由于这种归一化仅用于匹配,ToLowerInvariant() 比 ToLower() 更安全,因为它避免了区域文化相关的大小写差异。此外,plugin.Name.ToLower() 在每个 LINQ 项目中都会被重新计算;建议缓存一个归一化后的名称,或至少将其改为 ToLowerInvariant()。
| 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.
| 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))) |
| 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]; | ||
| } |
There was a problem hiding this comment.
suggestion: 建议在 LevenshteinDistance 中添加一些基础防护和快捷路径,以提升健壮性和效率。
当前方法假定 a 和 b 为非 null,如果不是则会抛异常。添加显式的 null 检查(例如抛出 ArgumentNullException)可以让 API 更安全。当任一字符串为空(lenA == 0 或 lenB == 0)时,也可以提前返回,而无需分配和填充完整的 DP 矩阵,从而避免在简单场景下做不必要的工作。
| 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.
| 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]; | |
| } |
/apm ldumped 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 viaPaginationTools, 30 plugins per page./apm -i <序号>still works with the number shown, regardless of page).Search
/apm s [关键词]command fuzzy-searches the cloud plugin list using Levenshtein distance on plugin names, returning the 10 closest matches./apm -i.Misc
FormatPluginLinehelper (used by both/apm land/apm s) to keep index/formatting logic in one place.2.1.0.3and 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 个结果改进:
/apm l帮助说明,并新增/apm s使用提示文档:
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:
/apm lcloud plugin list command with 30 plugins per page/apm s [keyword]command to fuzzy-search cloud plugins and show the 10 closest matches by nameEnhancements:
/apm land new/apm susage hintsDocumentation: