Aspire CLI 的 API 文档命令组:aspire docs api 的设计规格与源码实现解析
【免费下载链接】aspireAspire is the tool for code-first, extensible, observable dev and deploy.项目地址: https://gitcode.com/GitHub_Trending/as/aspire
本文基于 Aspire 仓库中的规格文档 api-docs-commands.md,系统讲解aspire docs api命令组的完整设计:它如何从aspire.dev的 sitemap 构建 API 参考索引,如何按作用域浏览 C# 与 TypeScript 的 API 目录,如何用加权词法搜索定位 API 条目,以及如何按稳定标识符取回 Markdown 内容。读完本文,你将掌握该命令组的命令面、标识符模型、层次模型、缓存机制,以及 源码实现 中的评分权重与索引管线细节。
设计背景:为什么需要独立的 API 文档管线
Aspire CLI 的aspire docs命令族负责把aspire.dev站点内容引入命令行。根据规格文档,aspire docs api虽然也挂在aspire docs下,但走的是与现有 prose 文档命令不同的摄取(ingestion)管线:
aspire docs索引的是llms-full.txt(面向自然语言文档的单篇式文本);aspire docs api索引的是sitemap-0.xml,并把直接的 API 页面路由作为可寻址条目(addressable items)。
这一区分决定了整个命令组的核心设计取向:API 参考页是层级化的(包 → 类型 → 成员组),因此 CLI 需要一套稳定的路径式标识符和分作用域的浏览模型,而不是一股脑地把所有 API 倒出来。
目标与非目标
规格文档明确了 5 个设计目标:
- 通过 Aspire CLI 暴露 API 参考内容;
- 尽可能复用现有的抓取(fetch)、ETag 与磁盘缓存模式;
- 保留 API 层级结构,让 CLI 可以浏览目录而不必一次性输出全部 API;
- 同时支持 C# 与 TypeScript 的 API 参考页;
get操作返回 Markdown 格式内容。
同时列出 3 个非目标(Non-goals),界定了功能边界:
- 不做无作用域的全量目录列表(full unscoped catalog listing);
- 不做面向
aspire.dev全部内容的通用网页爬虫; - 不做超出 Aspire API 路由结构的跨语言标识符归一化。
这组“非目标”在实现中体现得很彻底:源码里 ApiReferenceLanguages 明确将受支持语言集合固定为csharp和typescript两种,list也始终以 scope 为参数,从不返回整个目录。
命令面:list / search / get 三件套
命令组的完整命令面如下:
aspire docs api list <scope> [--format json] aspire docs api search <query> [--language <language>] [--limit|-n <count>] [--format json] aspire docs api get <id> [--format json]这三个子命令在源码中分别由 ApiListCommand.cs、ApiSearchCommand.cs、ApiGetCommand.cs 实现,并由父命令 ApiCommand.cs 统一注册到docs命令树下的api节点。
list:按作用域浏览
list接受一个必选的scope位置参数和--format选项。默认以表格渲染,表头为 Name、Id、Kind、Group 四列(Group 列显示成员组,无则为-);--format json则直接输出 JSON 数组。源码中的实现逻辑值得注意:
- 空结果时不会报错退出,而是提示“在该 scope 下未找到条目”并返回成功;
- 列表按条目
Kind的固定顺序排序(包/模块 → 类型/符号 → 成员组/成员),再按名称排序,保证浏览体验稳定。
search:语言过滤与结果数量控制
search的参数组合为--language(可选语言过滤)、--limit/-n(结果数量上限)与--format。这里有一个规格文档没有写明、但源码中明确存在的约束:
var limit = Math.Clamp(parseResult.GetValue(s_limitOption) ?? 5, 1, 10);见 ApiSearchCommand.cs:结果默认返回 5 条,且--limit的值被钳制在 1 到 10 之间。也就是说--limit 50实际等价于 10。此外--language传入了不受支持的语言值时,服务层会直接返回空结果集而不是报错。
get:按标识符取回 Markdown
get接受唯一的必选参数id(精确的 API 标识符)与--format。非 JSON 模式下,源码调用InteractionService.DisplayMarkdown(item.Content)直接在终端渲染 Markdown 正文;找不到条目时返回失败码并提示该 id 不存在。
浏览模型:作用域语义
list是一个有作用域限制的浏览命令,它永远不返回整个 API 目录。支持的 scope 形如:
aspire docs api list csharp aspire docs api list csharp/<package> aspire docs api list csharp/<package>/<type> aspire docs api list typescript aspire docs api list typescript/<module> aspire docs api list typescript/<module>/<symbol>各 scope 的返回语义如下表(继承自规格文档):
| Scope | 返回内容 |
|---|---|
csharp | 顶层 C# 包(packages) |
csharp/<package> | 包内的类型(types) |
csharp/<package>/<type> | 该类型的成员组页面(如methods、properties、constructors) |
typescript | 顶层 TypeScript 模块(modules) |
typescript/<module> | 模块内的直接符号(symbols) |
typescript/<module>/<symbol> | 符号下的成员(members) |
关键约束:如果一个 scope 没有子项,list返回空结果,而不会横向扩展或返回兄弟作用域。从源码看,ApiDocsIndexService.ListAsync 的过滤逻辑正是严格的父级匹配——只挑出ParentId与归一化后 scope 相等的条目,不做任何“找不到就向上/向旁找”的兜底,这与规格中的语义一一对应。
标识符模型:从路由派生的稳定 ID
CLI 使用从 API 路由结构派生的路径式稳定标识符。基本规则是:对于 sitemap 支撑的页面,标识符等于路由路径中/reference/api/之后的部分,去掉尾部斜杠。规格文档给出的示例:
csharp/aspire.azure.ai.inference csharp/aspire.azure.ai.inference/aspireazureaiinferenceextensions typescript/aspire.hosting.azure.appconfiguration typescript/aspire.hosting.azure.appconfiguration/azureappconfigurationresource typescript/aspire.hosting.azure.appconfiguration/azureappconfigurationresource/runasemulator在实现中,这条规则由 sitemap 解析后逐段拼接而来。BuildCSharpItems 根据 URL 段数生成不同层级的条目:1 段是包(csharp/<package>),2 段是类型(csharp/<package>/<type>),3 段是成员组(csharp/<package>/<type>/<member-group>);TypeScript 侧的 BuildTypeScriptItems 同理,1 段为模块、2 段为符号或成员、3 段为成员。查找时对 id 做Trim().Trim('/')归一化(见 NormalizeId),所以用户粘贴标识符时多带一个首尾斜杠也不影响匹配。
层次模型与 kind 分类
规格文档定义了两条对外暴露的层级链:
- C#:
language -> package -> type -> member-group page - TypeScript:
language -> module -> symbol -> member(与 sitemap 结构直接对齐)
源码中每个条目都带一个kind字段,取值由 ApiReferenceKinds 固定为:package、module、type、symbol、member、member-group。这里有一个实现层面的细节可以印证规格中“保留 API 层级”的目标:TypeScript 的 2 段路径条目,其 kind 并非拍脑袋决定,而是动态判定的——如果typescript/<module>/<symbol>这个 id 同时也是一个包含 3 段子页面的容器(即 sitemap 中存在它的子页面),则标记为symbol,否则标记为member。这保证了list在typescript/<module>下能正确区分“可继续下钻的符号”与“叶子成员”。
搜索行为:加权词法匹配与评分权重
规格文档规定search对索引执行加权词法匹配(weighted lexical matching),优先级依次为:精确标识符匹配 > 精确 API 名称匹配 > 包/模块匹配 > 类型/符号匹配 > 成员组匹配 > (若索引元数据可用的话)摘要与描述片段匹配;并且要求搜索结果包含足够元数据,让用户能直接把返回的标识符复制进aspire docs api get。
源码中的 ScoreItem 方法完整实现了这套优先级,具体权重常量(定义于 ApiDocsIndexService.cs)如下:
| 常量 | 权重 | 含义 |
|---|---|---|
ExactIdMatchBonus | 60.0 | 标识符完全相等加分 |
ExactNameMatchBonus | 45.0 | 名称完全相等加分 |
NamePrefixMatchBonus | 32.0 | 名称前缀匹配加分 |
PathSegmentMatchBonus | 20.0 | 查询词命中 ID 的任一路径段 |
PathPrefixMatchBonus | 18.0 | 路径段前缀匹配加分 |
PrefixTightnessMaxBonus | 16 | 前缀“紧致度”上限:匹配段与查询词长度差越小,加分越高(最长额外 16 分) |
NameWeight | 10.0 | 名称字段词法得分的乘数 |
IdWeight | 8.0 | 标识符字段词法得分的乘数 |
SummaryWeight | 4.0 | 摘要字段词法得分的乘数 |
MemberGroupWeight | 3.0 | 成员组字段词法得分的乘数 |
ParentWeight | 2.5 | 父级标识符字段词法得分的乘数 |
在此基础上还有两类“宽泛查询”(单个长度 ≥5 且不含/ # . ( )等字符的裸词查询)的额外处理:
- 前缀匹配加分:名称或路径段以查询词开头时,在 32 / 18 基础分之外再按紧致度加分;
- kind 倾向加分(GetBroadQueryKindBonus):
type/symbol+28、package/module+16、member-group+8,使宽泛搜索优先浮出类型和符号。
分词方面,查询文本经LexicalScoring.Tokenize切分,最小词元长度为 2(MinTokenLength = 2),词法得分按“标识符+连字符”模式(IdentifierWithHyphen)计算。结果按分数降序、同分按 id 字典序排列后取前 topK 条。
另外,规格中“搜索结果应含足够元数据”这一点在输出模型上得到落实:每条搜索结果都携带id、name、language、kind、parentId以及(实现中额外包含的)memberGroup、summary字段,可以直接把id复制给get使用。
Get 行为:直接路由项与 Markdown 解析
get按精确标识符解析条目并返回 Markdown 内容。对于标识符直接映射到页面路由的条目(规格所称 direct-route items),流程是:
- 在内存索引中按归一化后的 id 查表命中条目;
- 通过抓取器取回该 API 页面的 Markdown;
- 将页面中站点相对链接改写为绝对 URL 后返回。
第 3 步由 ApiDocsSourceConfiguration.RewriteMarkdownLinks 完成:以正则在 Markdown 中匹配](/...)与](#...)形式的本地链接,分别补全为站点根和当前页 URL,保证终端输出的内容里链接是可点击的。
实现中还有一层规格文档只字未提、但对体验很关键的机制——成员索引的懒加载。C# 类型的成员(方法、属性等)并不全部预先进入 sitemap 级索引,而是当get的 id 带有#fragment(定位到某个成员)或list的 scope 是一个member-group页时,才按需抓取对应类型页、用 ApiMemberMarkdownParser 从 Markdown 中解析出成员条目并合并进索引(见 GetMemberContainerIdForId 与 EnsureMemberContainersIndexedAsync),解析出的成员索引同样落盘缓存。搜索路径同样受益:当基础路由搜索无果或未命中成员级条目时,服务会按批次(MemberSearchBatchSize = 8)并行展开候选容器页继续搜索。
数据来源与缓存策略
规格文档规定实现使用三个要素:
- 以
https://aspire.dev/sitemap-0.xml作为 API 目录来源; - 采用固定的 Markdown 解析规则——在规范 API 页面 URL 后追加
.md得到 Markdown 地址; - 对 sitemap 与页面内容都做 ETag 缓存与磁盘持久化。
并且要求:不要把 sitemap 和 Markdown 端点硬编码进命令处理器。实现遵循了这一点,端点全部收敛在 ApiDocsSourceConfiguration.cs 中:
- 默认 sitemap URL 为常量
DefaultSitemapUrl = "https://aspire.dev/sitemap-0.xml"; - 支持通过配置路径
docs:api:sitemapUrl覆盖来源(GetSitemapUrl),便于测试或对接环境; - BuildMarkdownUrl 实现“追加
.md”规则:先剥离 URL fragment,再把页面 URL 的 scheme/host/port 重定基(rebase)到配置的 sitemap 来源主机,最后若尚未以.md结尾则追加; - RebasePageUrl 保证 sitemap 覆盖后,条目中的页面 URL 与 sitemap 指向同一主机,避免跨源抓取。
缓存链路(由 ApiDocsFetcher.cs 与 ApiDocsCache.cs 承担)在 EnsureIndexedAsync 中体现为四级策略:
- 内存已索引则直接返回;
- 抓取 sitemap 失败但本地有缓存索引 →降级使用缓存索引并记录告警(离线或网络抖动时命令仍然可用);
- sitemap 抓取成功,用
SourceContentFingerprint.Compute(sitemapContent, IndexSchemaVersion)计算源内容指纹,与缓存指纹比对——一致则直接从磁盘缓存加载索引,跳过重新解析; - 指纹变化(或无缓存)→ 解析 sitemap、重建索引并写回磁盘缓存与指纹。
IndexSchemaVersion(当前为 1)是缓存 schema 版本号,源码注释说明:只要代码变更导致同一 sitemap 会生成不同的索引数组,就应提升该版本以使旧缓存自动失效。成员级索引另有独立指纹v2:{基础指纹},与基础索引指纹联动失效。
解析管线:从 sitemap 到三个命令
规格文档将 API 管线描述为五个步骤,源码实现与之逐条对应:
- 抓取 sitemap(
_fetcher.FetchSitemapAsync); - 解析 C# 与 TypeScript API 路由(ApiSitemapParser.Parse 产出
ApiSitemapEntry,含语言与路径段); - 构建层次化 API map(
BuildBaseItems按语言分派到BuildCSharpItems/BuildTypeScriptItems,再经Deduplicate按 id 去重); - 把构建好的索引持久化到磁盘(
_cache.SetIndexAsync+ 指纹写入); - 用索引服务
list/search/get。
索引构建完成后条目按 id 排序(OrderBy(item.Id, OrdinalIgnoreCase)),既保证输出稳定,也便于list的排序逻辑。
输出模型:三种 JSON 形状
规格定义了三种命令的输出模型,实现中的 POCO 类与之对齐(并各自带有一条注释,提醒改动时与 cli-output-formats.md 保持同步):
List 输出(ApiListItem):
id、name、language、kind、parentId(实现另含memberGroup,供表格 Group 列展示)
Search 输出(ApiSearchResult):
id、name、language、kind、parentId、score(实现另含memberGroup、summary)
Get 输出(ApiContent):
id、name、language、kind、parentId、url、content(实现另含memberGroup)
--format json均通过System.Text.Json的源生成序列化上下文输出(JsonSourceGenerationContext.RelaxedEscaping),避免运行时反射序列化开销。
测试覆盖
规格文档要求的功能测试点,在仓库中均有对应测试文件(位于 tests/Aspire.Cli.Tests/Mcp/ApiDocs/ 目录):
| 规格要求的测试点 | 对应测试文件 |
|---|---|
| sitemap 解析与过滤 | ApiSitemapParserTests.cs |
| C# 成员组页面索引 / TypeScript 层次解析 / 作用域 list / 语言过滤 search / 标识符 get | ApiDocsIndexServiceTests.cs |
| ETag 感知的抓取行为 | ApiDocsFetcherTests.cs |
| 磁盘持久化的 API 索引缓存 | ApiDocsCacheTests.cs |
| 来源配置(URL rebase、Markdown URL 规则、链接改写) | ApiDocsSourceConfigurationTests.cs |
| 成员 Markdown 解析 | ApiMemberMarkdownParserTests.cs |
命令层行为(表格渲染、JSON 输出、错误分支)另有 ApiCommandTests.cs 覆盖。测试中使用 TestApiDocsFetcher 注入伪造的抓取器,使整条管线可以在不访问真实站点的情况下验证。
小结:一个可复用的文档索引模式
aspire docs api的设计可以用三句话概括:用 sitemap 代替爬虫获得稳定的可寻址条目集合,用路由路径派生的标识符让浏览、搜索、取回三个动作共享同一套寻址方案,用 ETag + 内容指纹 + 磁盘缓存让索引构建昂贵但可增量、可离线降级。对于想在自己的 CLI 中接入站点参考文档的场景,这套“sitemap → 层次索引 → 稳定 ID → 懒加载详情”的管线(源码集中在 src/Aspire.Cli/Documentation/ApiDocs/)以及docs:api:sitemapUrl这类“来源可配置、端点不硬编码”的做法,都是可以直接借鉴的工程实践。
【免费下载链接】aspireAspire is the tool for code-first, extensible, observable dev and deploy.项目地址: https://gitcode.com/GitHub_Trending/as/aspire
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考