Repository navigation
feat(LLMsDocsCli): support Extensions components #1271
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
74ae533
2a6b629
8e149f9
7d44f8d
9d1685e
9d62bf4
13cc82c
57b00fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| # BootstrapBlazor LLMs Docs CLI(`bb-llms`) | ||
|
|
||
| 一个轻量命令行工具,用于从 [www.blazor.zone/llms](https://www.blazor.zone/llms) | ||
| 拉取 BootstrapBlazor 组件的**官方 API 文档**(参数、事件回调、公开方法、源码链接), | ||
| 拉取 BootstrapBlazor 及 BootstrapBlazor.Extensions 组件的**官方 API 文档**(参数、事件回调、公开方法、源码链接), | ||
| 供 AI agent(Claude Code / Cursor / Trae / Codex 等经 Bash 调用)和开发者查询。 | ||
|
|
||
| 组件文档由官网的 `BootstrapBlazor.LLMsDocsGenerator`(生产者,CI 侧用 Roslyn 生成并发布)产出, | ||
|
|
@@ -49,6 +49,7 @@ dotnet tool uninstall -g BootstrapBlazor.LLMsDocs.Cli | |
| ```bash | ||
| bb-llms search table # 按关键词搜索组件,定位准确名称 | ||
| bb-llms get Table # 拉取 Table 组件的完整 API 文档(核心命令) | ||
| bb-llms get SelectCity # 拉取 Region 扩展包中 SelectCity 的 API 文档 | ||
| bb-llms list # 列出全部组件 | ||
| bb-llms index # 打印原始 llms.txt 索引 | ||
| ``` | ||
|
|
@@ -68,10 +69,12 @@ bb-llms index # 打印原始 llms.txt 索引 | |
|
|
||
| 要点: | ||
|
|
||
| - `get` / `index` 为**原样透传**,不解析格式,输出即远程 `.txt` 原文。 | ||
| - `list` / `search` 默认输出 **JSON**;加 `--format text` 输出纯文本(`Name - 描述` 每行一条)。 | ||
| - `get` / `index` 的输出保留远程 `.txt` 原文;`get` 会先解析 `llms.txt`,按名称找到文档路径,再直接读取核心或扩展文档,不通过请求失败来尝试其他目录。本地源也必须包含该索引。 | ||
| - `list` / `search` 包括组件和服务,默认输出 **JSON**;`isExtension` 表示文档位于 `extensions/`,不表示接口源码一定声明在扩展仓库。加 `--format text` 输出纯文本(`Name - 描述` 每行一条)。 | ||
| - `search --limit` 默认 `20`,取值范围会被限制在 `1~100`。 | ||
| - `search` 按文档路径去重后应用结果数量限制,避免多个包共用的服务占满结果;`list` 保留各包的索引项。 | ||
| - 找不到组件时 `get` 退出码非 0,并提示用 `bb-llms search <关键词>` 重新定位。 | ||
| - 索引缺失、索引中没有对应名称、目标文档缺失均返回非零退出码,并保留具体失败原因;不会把缺少 `llms.txt` 误报为组件不存在。 | ||
|
|
||
| 示例: | ||
|
|
||
|
|
@@ -80,8 +83,12 @@ bb-llms search button --limit 5 # 最多 5 条,Button 类优先 | |
| bb-llms list --query table # 仅名称/描述含 "table" 的组件 | ||
| bb-llms list --format text # 纯文本清单 | ||
| bb-llms get Table # Table 的参数表 / 事件 / 方法 / GitHub 源码链接 | ||
| bb-llms get SelectCity # BootstrapBlazor.Region 扩展包的 SelectCity 文档 | ||
| bb-llms get IRecognizerProvider # 共享服务接口,按包列出 Azure/Baidu 注册方式 | ||
| ``` | ||
|
|
||
| 多个扩展包共用一个接口时,各包的索引项指向同一份服务文档;按实际使用的包选择其中一种注册方式,不要全部复制执行。 | ||
|
|
||
| ### 源与缓存选项(数据命令通用) | ||
|
|
||
| ``` | ||
|
|
@@ -120,6 +127,8 @@ bb-llms get Button --base-url ./wwwroot/llms | |
|
|
||
| > 含义:官网发版后,最长 24 小时本地才会感知到文档更新(用 `--refresh` 可立即刷新)。 | ||
|
|
||
| 索引同样缓存。新增组件或扩展服务已发布但提示索引中不存在时,使用 `bb-llms get SelectCity --refresh` 同时刷新索引及目标文档。 | ||
|
|
||
| --- | ||
|
|
||
| ## 给 AI agent 使用(发现层) | ||
|
|
@@ -181,12 +190,18 @@ bb-llms install --client claude # 2. 装发现层(写入 | |
|
|
||
| ## 开发 / 本地运行 | ||
|
|
||
| 在 Extentions 仓库根目录执行 CLI 回归检查: | ||
|
|
||
| ```powershell | ||
| .\tools\BootstrapBlazor.LLMsDocs.Cli\tests\Verify-Client.ps1 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ⚪ Low · Documented verification script is missing When a maintainer follows the documented local regression-check command, the README directs maintainers to run Add the referenced verification script or change the README to point to an existing test command. Prompt for AI agentsIn `tools/BootstrapBlazor.LLMsDocs.Cli/README.md` at line 196:
**Documented verification script is missing**
When a maintainer follows the documented local regression-check command, the README directs maintainers to run `tests/Verify-Client.ps1`, but the CLI directory contains no such PowerShell script, so the documented regression check fails before exercising the client.
Add the referenced verification script or change the README to point to an existing test command. |
||
| ``` | ||
|
|
||
| ```bash | ||
| cd tools/BootstrapBlazor.LLMsDocs.Cli | ||
| dotnet build | ||
| dotnet run -- get Button | ||
| dotnet run -- search table --limit 5 | ||
| dotnet run -- get Button --base-url ../BootstrapBlazor.LLMsDocsGenerator/<生成目录>/llms | ||
| dotnet run -- get Button --base-url "<生成目录>/wwwroot/llms" | ||
| ``` | ||
|
|
||
| 打包冒烟: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -63,14 +63,21 @@ public LlmsDocsClient(string? baseOverride, bool refresh = false, bool noCache = | |
|
|
||
| public Task<string> GetIndexAsync(CancellationToken ct = default) => FetchAsync("llms.txt", ct); | ||
|
|
||
| public Task<string> GetComponentAsync(string name, CancellationToken ct = default) | ||
| public async Task<string> GetComponentAsync(string name, CancellationToken ct = default) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(name)) | ||
| { | ||
| throw new ArgumentException("Component name is required.", nameof(name)); | ||
| } | ||
|
|
||
| return FetchAsync($"components/{name}.txt", ct); | ||
| var entry = ParseIndex(await GetIndexAsync(ct)) | ||
| .FirstOrDefault(e => string.Equals(e.Name, name, StringComparison.OrdinalIgnoreCase)); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium · Extension documents become inaccessible When the index contains core and extension entries with the same name, Provide a way to disambiguate matching names so callers can select the extension document. Prompt for AI agentsIn `tools/BootstrapBlazor.LLMsDocs.Cli/Services/LlmsDocsClient.cs` at line 74:
**Extension documents become inaccessible**
When the index contains core and extension entries with the same name, `GetComponentAsync` uses `FirstOrDefault` to select the first matching name, and core entries precede extension entries, so `get` always fetches the core document and provides no way to retrieve the extension document.
Provide a way to disambiguate matching names so callers can select the extension document. |
||
| if (entry is null) | ||
| { | ||
| throw new FileNotFoundException($"Component not found in index: {name}"); | ||
| } | ||
|
|
||
| return await FetchAsync(entry.DocumentPath, ct); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium · Concurrent reads return partial documents When one CLI invocation refreshes a remote document while another invocation reads the same cached path, Write cache content to a temporary file and atomically replace the cache file, coordinating metadata updates so readers cannot observe an in-progress refresh. Prompt for AI agentsIn `tools/BootstrapBlazor.LLMsDocs.Cli/Services/LlmsDocsClient.cs` at line 80:
**Concurrent reads return partial documents**
When one CLI invocation refreshes a remote document while another invocation reads the same cached path, `FetchAsync` writes refreshed cache content directly to the shared file before updating its metadata; another CLI invocation can read that file while it is truncated or partially written and return incomplete documentation.
Write cache content to a temporary file and atomically replace the cache file, coordinating metadata updates so readers cannot observe an in-progress refresh. |
||
| } | ||
|
|
||
| public async Task<IReadOnlyList<ComponentEntry>> ListAsync(string? query, CancellationToken ct = default) | ||
|
|
@@ -96,6 +103,7 @@ public async Task<IReadOnlyList<ComponentEntry>> SearchAsync(string query, int l | |
|
|
||
| var matches = await ListAsync(query, ct); | ||
| return matches | ||
| .DistinctBy(e => e.DocumentPath, StringComparer.Ordinal) | ||
| .OrderBy(e => e.Name.StartsWith(query, StringComparison.OrdinalIgnoreCase) ? 0 : 1) | ||
| .ThenBy(e => e.Name, StringComparer.OrdinalIgnoreCase) | ||
| .Take(Math.Clamp(limit, 1, 100)) | ||
|
|
@@ -157,8 +165,8 @@ private async Task<string> FetchAsync(string relativePath, CancellationToken ct) | |
|
|
||
| // ---- index parsing ---------------------------------------------------- | ||
|
|
||
| // Matches: "- [Button](components/Button.txt) - optional description" | ||
| [GeneratedRegex(@"^- \[(?<name>[^\]]+)\]\(components/[^)]+\.txt\)(?:\s*-\s*(?<desc>.*))?$")] | ||
| // Matches component and extension links in llms.txt. | ||
| [GeneratedRegex(@"^- \[(?<name>[^\]]+)\]\((?<path>(?:components|extensions)/[^)]+\.txt)\)(?:\s*-\s*(?<desc>.*))?$")] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 High · Untrusted paths escape documentation roots When an untrusted index contains a traversal path and a caller requests that entry, Reject traversal paths and verify resolved read and cache paths stay inside their configured roots before accessing them. Also at Prompt for AI agentsIn `tools/BootstrapBlazor.LLMsDocs.Cli/Services/LlmsDocsClient.cs` at line 169:
**Untrusted paths escape documentation roots**
When an untrusted index contains a traversal path and a caller requests that entry, `ParseIndex` accepts the path and `GetComponentAsync` passes it to `FetchAsync` without containment checks, so local reads escape `_baseDir` and remote caching writes outside `_cacheRoot`, exposing or overwriting local files.
Reject traversal paths and verify resolved read and cache paths stay inside their configured roots before accessing them.
Also at `tools/BootstrapBlazor.LLMsDocs.Cli/Services/LlmsDocsClient.cs:203`. |
||
| private static partial Regex IndexLineRegex(); | ||
|
|
||
| // Matches a category heading: "### Buttons" | ||
|
|
@@ -189,7 +197,11 @@ private static List<ComponentEntry> ParseIndex(string indexText) | |
|
|
||
| var name = lineMatch.Groups["name"].Value.Trim(); | ||
| var desc = lineMatch.Groups["desc"].Success ? lineMatch.Groups["desc"].Value.Trim() : null; | ||
| entries.Add(new ComponentEntry(name, string.IsNullOrEmpty(desc) ? null : desc, category)); | ||
| entries.Add(new ComponentEntry(name, string.IsNullOrEmpty(desc) ? null : desc, category) | ||
| { | ||
| IsExtension = lineMatch.Groups["path"].Value.StartsWith("extensions/", StringComparison.Ordinal), | ||
| DocumentPath = lineMatch.Groups["path"].Value | ||
| }); | ||
| } | ||
|
|
||
| return entries; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Medium · Lookup errors expose source details
When a requested document is missing and the configured source path or URL contains sensitive information,
BuildGetCommandprints the exception message verbatim;FetchAsyncincludes the full local path or request URL in missing-document exceptions, so stderr and captured agent or CI logs expose private paths or secret-bearing source URLs.Print a generic missing-document message in the normal error path and expose detailed diagnostics only through an explicitly enabled, redacted debug mode.
Prompt for AI agents