XiHan Framework 处于预览阶段

查看获取方式
跳转到内容

命令行宿主 ​

本页讲命令行宿主怎样把命令行参数翻译成内核调用:内置命令、操作调用的载荷与上下文选项、输出与退出码、自定义命令,以及经 stdio 提供 MCP,适用于运维工具、批处理脚本和要给 MCP 客户端当子进程的应用。完整 API 见 Hosting.CommandLine 包。

宿主做什么 ​

Hosting.CommandLine 是 Ring 4 的宿主,跑在 .NET 通用宿主(Host.CreateApplicationBuilder)里,一次进程执行一条命令。同一份操作契约在命令行上是一条命令:invoke greetings.create --name Ada 或简写 greetings create --name Ada,调用与 HTTP 一样经过完整的内核管道,Authenticate 阶段解释凭据,Authorize 阶段按契约判定。stdout 只承载命令输出,日志与失败信息只写 stderr,脚本可以直接把 stdout 交给 jq 一类的工具。

启用 ​

  1. 用控制台项目,引用 Microsoft.Extensions.Hosting 与 XiHan.Framework.Hosting.CommandLine;要暴露操作时再引用 Kernel.Generators(以分析器方式)生成操作登记代码。
  2. AddXiHan(...) 装配特性,builder.Services.AddXiHanCommandLine() 登记内置命令。
  3. 以 RunXiHanCommandLineAsync(args) 运行,返回值就是进程退出码。

samples/SimpleCli 的 Program.cs:

csharp
using Microsoft.Extensions.Hosting;
using SimpleCli;
using XiHan.Framework.Hosting.CommandLine;
using XiHan.Framework.Kernel.Hosting;
using XiHan.Framework.Localization.Abstractions;
using XiHan.Framework.Localization.Abstractions.Messages;
using XiHan.Framework.Profiles.Minimal;
using XiHan.Framework.Protocols.Mcp;

var builder = Host.CreateApplicationBuilder(args);

// 同一份操作契约既是命令(greetings create --name Ada),也是 MCP tool(mcp 命令的 stdio 传输)
builder.AddXiHan(xihan => xihan
    .UseMinimalProfile()
    .UseFeature<McpProtocolFeature>()
    .UseFeature<GreetingsFeature>());

builder.Services.AddMessageCatalog(MessageCatalog.FromEmbeddedResources("samples", typeof(CliMessages)));
builder.Services.AddXiHanCommandLine();

return await builder.Build().RunXiHanCommandLineAsync(args);

GreetingsFeature 登记了一个操作 greetings.create,它的 [Operation] 写了 Expose = Expose.CommandLine | Expose.Mcp。HostApplicationBuilder 上的 AddXiHan 与 ASP.NET Core 宿主一样,先装载 XIHAN_CONFIG_PATH 目录里的 JSON 文件(可重载,紧接 appsettings.{环境}.json 插入),见宿主与 Web 应用。AddXiHanCommandLine() 清掉通用宿主的默认日志提供器,只留写 stderr 的控制台日志。RunXiHanCommandLineAsync 先启动宿主再执行命令,命令结束后按顺序停止宿主;Ctrl+C 取消当前命令(退出码 8),宿主仍正常停止。

内置命令 ​

命令输出
help用法与命令列表
graph能力图谱(App.DescribeGraph())
pipeline管道阶段与中间件(App.DescribePipeline())
operations操作登记表,每行 ID<TAB>稳定性<TAB>暴露的协议<TAB>permission=…、policy=… 或 -
invoke <操作 ID>调用操作,见下一节
mcpProtocols.Mcp 注册:MCP 的 stdio 传输,接受与 invoke 相同的上下文选项

没有命令时(包括只写了 --help)把用法写到 stderr 并返回 64;写了命令再带 --help 时把用法写到 stdout 并返回 0。

bash
dotnet run --project framework/samples/SimpleCli -- operations
text
greetings.create	Experimental	Mcp, CommandLine	-

operations 列出登记表里的全部操作,包括没有暴露给命令行的,用来核对各协议上的暴露情况。

调用操作 ​

invoke <操作 ID> 只接受 Expose 含 CommandLine、稳定性不是 Stability.Internal 的操作,其他操作得到 HOSTING_NOTFOUND_001、退出码 3。命令名不是已登记的命令、且与下一个参数能拼成合法操作 ID 时,<resource> <verb> 等价于 invoke <resource>.<verb>。

载荷是一个 JSON 对象,按下面的优先顺序取第一个:

来源写法说明
--json--json '{"name":"Ada","times":2}'必须是 JSON 对象
--data--data request.json 或 --data @request.json文件内容必须是 JSON 对象
字段选项--name Ada --times 2其余非保留选项拼成对象:值按操作的请求 Schema 写成对应类型,重复的选项成数组,没有字段时为 {}

保留选项不进载荷:json、data、authorization、tenant、correlation、locale、timeout、help。

字段选项的值按操作的请求 JSON Schema 转换,所以 --times 2 进载荷是 "times":2:

Schema 声明写成
string,或没有约束类型字符串,--name 42 仍是 "name":"42"
booleantrue 或 false,不跟值的 --flag 是 true
integer、numberJSON 数字
array数组,元素按 items 转换,单个值也是一元数组

对象字段不从文本解析,要传嵌套对象就用 --json 或 --data,这两种载荷都原样进入、不做转换。转换不了的值(如 --times abc)不是用法错误:它留成字符串进管道,由 Validate 阶段报 KERNEL_VALIDATION_003,退出码 2。

操作没有 Schema(手写 AddOperation 时没传 OperationSchema)时值都写字符串。这时请求类型要从字符串读数字,序列化上下文需要 NumberHandling = JsonNumberHandling.AllowReadingFromString。

参数的解析规则:第一个不是选项的记号是命令名,其余是位置参数;--name value 与 --name=value 等价;后面不跟值的 --flag 记为 "true";-- 之后全是位置参数。标志后面要接位置参数时写 --flag=true,或把标志放到最后。

bash
dotnet run --project framework/samples/SimpleCli -- greetings create --name Ada --times 2
json
{"message":"Hello, Ada! Hello, Ada!","traceId":"4bf92f3577b34da6a3ce929d0e0e4736"}

调用上下文 ​

选项调用上下文说明
--tenant <id>Baggage xihan.tenant
--correlation <id>Baggage xihan.correlation
--locale <区域>Baggage xihan.locale规范化后写入,失败文案据此翻译;格式不合法时忽略
--timeout <秒>截止时间正数秒,可带小数,按宿主时钟换算成绝对时间;不是正数时为用法错误
--authorization '<方案> <值>'凭据没有选项时取环境变量 XIHAN_AUTHORIZATION;原样放入,由 Authenticate 阶段解释;格式不是「方案 空格 值」时为用法错误

每次调用生成新的 trace-id,调用来源记协议 cli 与操作 ID,没有远端地址。凭据放进环境变量可以不出现在进程列表与 shell 历史里:

bash
export XIHAN_AUTHORIZATION="ApiKey dev-alice-key"
app orders create --name widget --amount 3 --tenant acme --locale zh-CN --timeout 30

这里的 app 指应用发布出的可执行文件。

输出与退出码 ​

成功时把响应 JSON 写一行到 stdout,退出码 0。失败时把一行 JSON 写到 stderr:{"code":"<错误码>","detail":"<文案>","traceId":"<trace-id>"},detail 按 --locale 或默认区域翻译;还没有建出调用上下文的失败(用法错误、操作未暴露)没有 traceId。

bash
dotnet run --project framework/samples/SimpleCli -- invoke greetings.create --json '{"name":"Ada","times":99}'; echo $?
text
{"code":"SAMPLES_VALIDATION_001","detail":"The number of greetings must be between 1 and 10, but 99 was given.","traceId":"0af7651916cd43dd8448eb211c80319c"}
2

退出码按错误码的 KIND 段决定(CommandLineExitCodes.For):

KIND / 情形退出码常量
成功0Success
VALIDATION2Validation
NOTFOUND3NotFound
DENIED4Denied
CONFLICT5Conflict
UNAVAILABLE6Unavailable
TIMEOUT7Timeout
CANCELLED、Ctrl+C8Cancelled
其他(含 UNEXPECTED)1Unexpected
用法错误64Usage

用法错误包括:没有命令、未知命令(HOSTING_NOTFOUND_002)、invoke 缺操作 ID、--json 不是 JSON 对象、--data 文件不存在、--timeout 或 --authorization 格式不对(都是 HOSTING_VALIDATION_001)。认证失败与缺少权限都是 DENIED,退出码 4,不区分 HTTP 上的 401 与 403。

自定义命令 ​

实现 ICommand(命令名、说明的消息键、执行),用 AddXiHanCommand<TCommand>() 登记;help 按名字排序列出全部命令,说明用 DescriptionKey 按当前区域取文案。

csharp
using XiHan.Framework.Hosting.CommandLine;
using XiHan.Framework.Hosting.CommandLine.Commands;
using XiHan.Framework.Primitives;

public sealed class VersionCommand : ICommand
{
    public string Name => "version";

    public string DescriptionKey => ToolMessages.VersionDescription;

    public async ValueTask<int> ExecuteAsync(CommandContext context)
    {
        // 命令输出只写 context.Output(stdout),诊断信息写 context.Error(stderr)
        await context.Output.WriteLineAsync(FrameworkInfo.Version);
        return CommandLineExitCodes.Success;
    }
}

public static class ToolMessages
{
    // 文案放在应用消息目录的 Localization/<区域>.json 里
    public const string VersionDescription = "tool.version.description";
}
csharp
builder.Services.AddXiHanCommand<VersionCommand>();

CommandContext 提供解析结果(CommandLine、Arguments、Option(name)、HasOption(name))、作用域服务(Services)、输出流与取消令牌。命令要调用操作时,用 CommandLineInvocation.Create(context, operationId) 按同一套选项建调用上下文(一条命令里发起多次独立调用时用 Create(context, services, operationId),每次在自己的服务作用域里建,mcp 命令就是这样为每个 tools/call 建上下文),失败用 CommandLineFailure.WriteAsync 写 stderr 并得到退出码,与 invoke 一致。命令名重复时 CommandLineRunner 构造失败(ArgumentException)。

MCP stdio ​

装配了 McpProtocolFeature 时,mcp 命令以 stdio 提供 MCP 服务端:stdin 与 stdout 每行一条 JSON-RPC 消息,日志只写 stderr,stdin 关闭即结束会话。stdin 按 UTF-8 解码,与控制台代码页无关;答复只含 ASCII(非 ASCII 字符按 JSON 转义)。MCP 客户端以子进程方式启动应用:

bash
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | dotnet run --project framework/samples/SimpleCli -- mcp

答复里的 tool greetings_create 带消息目录里的本地化描述。mcp 命令接受与 invoke 相同的上下文选项,作用于整个会话里的每个 tools/call:凭据 --authorization '<scheme> <value>' 或环境变量 XIHAN_AUTHORIZATION(选项优先,MCP 规范要求 stdio 服务端从运行环境取凭据),--tenant、--correlation、--locale(tools/list 的标题与描述、失败文案按它翻译),--timeout <秒> 是每个 tools/call 的时限。每个 tools/call 经完整管道,凭据在这次调用的 Authenticate 阶段认证;stdio 没有外层认证,凭据无效时需要认证的 tool 得到 isError(SECURITY_DENIED_003),会话继续。选项不合法(凭据不是 <scheme> <value>、时限不是正数)时不进入会话,stderr 写一行用法错误,退出码 64。

bash
XIHAN_AUTHORIZATION="ApiKey dev-alice-key" dotnet run --project framework/samples/SimpleCli -- mcp --tenant acme --locale zh-CN

tool 的规则见 gRPC、MCP 与 CloudEvents。

相关 ​

Released under The MIT License