Skip to content

Commit 3bf8eb2

Browse files
committed
fix(ui): simplify onboarding manifest generation
1 parent 9058434 commit 3bf8eb2

3 files changed

Lines changed: 253 additions & 326 deletions

File tree

catalog/ui-onboarding.json

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,36 @@
5050
"id": "top_level_commands",
5151
"title": "顶层命令",
5252
"description": "由服务端动态填充的顶层命令列表。",
53-
"items": []
53+
"items": [
54+
{ "id": "agent", "description": "管理 AI Agent 子系统。需要子命令,例如 /cli agent status。" },
55+
{ "id": "app", "description": "管理应用依赖声明。需要子命令,例如 /cli app list;注册应用可用 /cli app register --name demo --needs '[]'。" },
56+
{ "id": "ask", "description": "向 AI Agent 提问。需要补问题内容,例如 /cli ask 这台机器适合跑什么模型?" },
57+
{ "id": "askforhelp", "description": "连接支持服务。可直接写求助内容,例如 /cli askforhelp 模型部署失败。" },
58+
{ "id": "benchmark", "description": "记录并查询基准测试结果。需要子命令,例如 /cli benchmark list。" },
59+
{ "id": "catalog", "description": "管理 YAML 知识目录。需要子命令,例如 /cli catalog status 或 /cli catalog validate。" },
60+
{ "id": "completion", "description": "为指定 shell 生成自动补全脚本。需要补 shell 名称,例如 /cli completion zsh。" },
61+
{ "id": "config", "description": "获取或设置持久化配置。需要子命令,例如 /cli config get agent.endpoint 或 /cli config set agent.model {sample_model}。" },
62+
{ "id": "deploy", "description": "部署推理服务。通常需要模型名,例如 /cli deploy {sample_model} --dry-run;查看部署列表可用 /cli deploy list。" },
63+
{ "id": "discover", "description": "发现局域网上的 LLM 推理服务。可直接执行,例如 /cli discover。" },
64+
{ "id": "engine", "description": "管理推理引擎。需要子命令,例如 /cli engine list 或 /cli engine pull。" },
65+
{ "id": "explore", "description": "持久化探索任务。需要子命令,例如 /cli explore start --model {sample_model} 或 /cli explore status --id <run-id>。" },
66+
{ "id": "fleet", "description": "管理局域网中的 AIMA 设备集群。需要子命令,例如 /cli fleet devices 或 /cli fleet info <device-id>。" },
67+
{ "id": "hal", "description": "硬件抽象层:检测能力并采集指标。需要子命令,例如 /cli hal detect 或 /cli hal metrics。" },
68+
{ "id": "help", "description": "查看任意命令帮助。可直接执行,例如 /cli help;查看 model 的帮助可用 /cli help model。" },
69+
{ "id": "init", "description": "安装基础设施栈。可直接执行,例如 /cli init;完整安装可用 /cli init --k3s。" },
70+
{ "id": "knowledge", "description": "管理知识库。需要子命令,例如 /cli knowledge list 或 /cli knowledge resolve {sample_model}。" },
71+
{ "id": "mcp", "description": "通过 stdio 提供 MCP 服务。可直接执行,例如 /cli mcp。" },
72+
{ "id": "model", "description": "管理模型。需要子命令,例如 /cli model list 或 /cli model pull {sample_model}。" },
73+
{ "id": "openclaw", "description": "OpenClaw 集成:将 AIMA 模型同步为 providers。需要子命令,例如 /cli openclaw status 或 /cli openclaw sync。" },
74+
{ "id": "run", "description": "下载、部署并提供模型服务。需要模型名,例如 /cli run {sample_model}。" },
75+
{ "id": "scenario", "description": "管理部署场景。需要子命令,例如 /cli scenario list 或 /cli scenario apply <scenario-name>。" },
76+
{ "id": "serve", "description": "启动 AIMA 服务。可直接执行,例如 /cli serve。" },
77+
{ "id": "status", "description": "显示系统状态。可直接执行,例如 /cli status。" },
78+
{ "id": "tui", "description": "交互式终端仪表盘。可直接执行,例如 /cli tui。" },
79+
{ "id": "tuning", "description": "自动调优:参数搜索 + 基准测试 + 应用最佳结果。需要子命令,例如 /cli tuning start --model {sample_model} 或 /cli tuning results。" },
80+
{ "id": "undeploy", "description": "移除已部署的推理服务。需要部署名,例如 /cli undeploy <deployment-name>。" },
81+
{ "id": "version", "description": "显示 AIMA 版本和构建信息。可直接执行,例如 /cli version。" }
82+
]
5483
},
5584
{
5685
"id": "common_examples",
@@ -121,7 +150,36 @@
121150
"id": "top_level_commands",
122151
"title": "Top-level commands",
123152
"description": "Top-level commands filled dynamically by the server.",
124-
"items": []
153+
"items": [
154+
{ "id": "agent", "description": "Manage the AI agent subsystem. It needs a subcommand, for example /cli agent status." },
155+
{ "id": "app", "description": "Manage application dependency declarations. It needs a subcommand, for example /cli app list; to register an app use /cli app register --name demo --needs '[]'." },
156+
{ "id": "ask", "description": "Ask the AI agent a question. You need to add the question text, for example /cli ask What model fits this machine?" },
157+
{ "id": "askforhelp", "description": "Connect to the support service. You can write the help request directly, for example /cli askforhelp Model deployment failed." },
158+
{ "id": "benchmark", "description": "Record and query benchmark results. It needs a subcommand, for example /cli benchmark list." },
159+
{ "id": "catalog", "description": "Manage the YAML knowledge catalog. It needs a subcommand, for example /cli catalog status or /cli catalog validate." },
160+
{ "id": "completion", "description": "Generate shell autocompletion. You need to add the shell name, for example /cli completion zsh." },
161+
{ "id": "config", "description": "Get or set persistent configuration. It needs a subcommand, for example /cli config get agent.endpoint or /cli config set agent.model {sample_model}." },
162+
{ "id": "deploy", "description": "Deploy an inference service. It usually needs a model name, for example /cli deploy {sample_model} --dry-run; to inspect deployments use /cli deploy list." },
163+
{ "id": "discover", "description": "Discover LLM inference services on the local network. You can run it directly, for example /cli discover." },
164+
{ "id": "engine", "description": "Manage inference engines. It needs a subcommand, for example /cli engine list or /cli engine pull." },
165+
{ "id": "explore", "description": "Run persistent exploration jobs. It needs a subcommand, for example /cli explore start --model {sample_model} or /cli explore status --id <run-id>." },
166+
{ "id": "fleet", "description": "Manage AIMA devices on the LAN. It needs a subcommand, for example /cli fleet devices or /cli fleet info <device-id>." },
167+
{ "id": "hal", "description": "Inspect hardware capabilities and metrics. It needs a subcommand, for example /cli hal detect or /cli hal metrics." },
168+
{ "id": "help", "description": "View help for any command. You can run it directly, for example /cli help; for model help use /cli help model." },
169+
{ "id": "init", "description": "Install the infrastructure stack. You can run it directly, for example /cli init; for the full install use /cli init --k3s." },
170+
{ "id": "knowledge", "description": "Manage the knowledge base. It needs a subcommand, for example /cli knowledge list or /cli knowledge resolve {sample_model}." },
171+
{ "id": "mcp", "description": "Serve MCP over stdio. You can run it directly, for example /cli mcp." },
172+
{ "id": "model", "description": "Manage models. It needs a subcommand, for example /cli model list or /cli model pull {sample_model}." },
173+
{ "id": "openclaw", "description": "Manage the OpenClaw integration. It needs a subcommand, for example /cli openclaw status or /cli openclaw sync." },
174+
{ "id": "run", "description": "Download, deploy, and serve a model. You need to add a model name, for example /cli run {sample_model}." },
175+
{ "id": "scenario", "description": "Manage deployment scenarios. It needs a subcommand, for example /cli scenario list or /cli scenario apply <scenario-name>." },
176+
{ "id": "serve", "description": "Start the AIMA server. You can run it directly, for example /cli serve." },
177+
{ "id": "status", "description": "Show system status. You can run it directly, for example /cli status." },
178+
{ "id": "tui", "description": "Open the terminal dashboard. You can run it directly, for example /cli tui." },
179+
{ "id": "tuning", "description": "Auto-tune model parameters. It needs a subcommand, for example /cli tuning start --model {sample_model} or /cli tuning results." },
180+
{ "id": "undeploy", "description": "Remove a deployed inference service. You need to add the deployment name, for example /cli undeploy <deployment-name>." },
181+
{ "id": "version", "description": "Show version and build information. You can run it directly, for example /cli version." }
182+
]
125183
},
126184
{
127185
"id": "common_examples",

cmd/aima/main_test.go

Lines changed: 125 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,29 @@ type mockCommandRunner struct {
5252
pipe func(context.Context, []string, []string) error
5353
}
5454

55+
type onboardingManifestDoc struct {
56+
Locales map[string]*onboardingLocaleDoc `json:"locales"`
57+
}
58+
59+
type onboardingLocaleDoc struct {
60+
FullCommands onboardingFullCommandsDoc `json:"full_commands"`
61+
}
62+
63+
type onboardingFullCommandsDoc struct {
64+
Groups []onboardingGroupDoc `json:"groups"`
65+
}
66+
67+
type onboardingGroupDoc struct {
68+
ID string `json:"id"`
69+
Items []onboardingCommandDoc `json:"items"`
70+
}
71+
72+
type onboardingCommandDoc struct {
73+
ID string `json:"id"`
74+
Command string `json:"command"`
75+
Description string `json:"description"`
76+
}
77+
5578
func (m *mockCommandRunner) Run(ctx context.Context, name string, args ...string) ([]byte, error) {
5679
if m.run != nil {
5780
return m.run(ctx, name, args...)
@@ -116,28 +139,13 @@ func TestBuildOnboardingManifestJSON_IncludesAllTopLevelCommands(t *testing.T) {
116139
t.Fatalf("build onboarding manifest: %v", err)
117140
}
118141

119-
var manifest onboardingManifest
120-
if err := json.Unmarshal(raw, &manifest); err != nil {
121-
t.Fatalf("unmarshal onboarding manifest: %v", err)
122-
}
123-
142+
manifest := decodeOnboardingManifest(t, raw)
124143
zh, ok := manifest.Locales["zh"]
125144
if !ok || zh == nil {
126145
t.Fatalf("zh locale missing: %#v", manifest.Locales)
127146
}
128147

129-
var topLevel onboardingGroup
130-
found := false
131-
for _, group := range zh.FullCommands.Groups {
132-
if group.ID == "top_level_commands" {
133-
topLevel = group
134-
found = true
135-
break
136-
}
137-
}
138-
if !found {
139-
t.Fatalf("top_level_commands group missing: %#v", zh.FullCommands.Groups)
140-
}
148+
topLevel := findOnboardingGroup(t, zh.FullCommands.Groups, "top_level_commands")
141149

142150
root := cli.NewRootCmd(&cli.App{})
143151
root.InitDefaultHelpCmd()
@@ -209,82 +217,146 @@ func TestBuildOnboardingManifestJSON_MarksSampleModelExamplesAsReplaceable(t *te
209217
}
210218
}
211219

212-
func TestBuildOnboardingManifestJSON_LocalizesTopLevelCommandDescriptions(t *testing.T) {
220+
func TestBuildOnboardingManifestJSON_UsesLocalizedTemplates(t *testing.T) {
213221
t.Parallel()
214222

215-
raw, err := buildOnboardingManifestJSON(&knowledge.Catalog{})
223+
sourceRaw, err := catalog.FS.ReadFile("ui-onboarding.json")
216224
if err != nil {
217-
t.Fatalf("build onboarding manifest: %v", err)
225+
t.Fatalf("read embedded onboarding manifest: %v", err)
218226
}
219227

220-
var manifest onboardingManifest
221-
if err := json.Unmarshal(raw, &manifest); err != nil {
222-
t.Fatalf("unmarshal onboarding manifest: %v", err)
228+
source := decodeOnboardingManifest(t, sourceRaw)
229+
zhSource, ok := source.Locales["zh"]
230+
if !ok || zhSource == nil {
231+
t.Fatalf("source zh locale missing: %#v", source.Locales)
232+
}
233+
enSource, ok := source.Locales["en"]
234+
if !ok || enSource == nil {
235+
t.Fatalf("source en locale missing: %#v", source.Locales)
236+
}
237+
238+
zhTemplates := findOnboardingGroup(t, zhSource.FullCommands.Groups, "top_level_commands")
239+
enTemplates := findOnboardingGroup(t, enSource.FullCommands.Groups, "top_level_commands")
240+
zhHelpTemplate := findOnboardingCommandByID(t, zhTemplates.Items, "help")
241+
zhModelTemplate := findOnboardingCommandByID(t, zhTemplates.Items, "model")
242+
zhDeployTemplate := findOnboardingCommandByID(t, zhTemplates.Items, "deploy")
243+
enHelpTemplate := findOnboardingCommandByID(t, enTemplates.Items, "help")
244+
245+
for _, template := range []onboardingCommandDoc{zhHelpTemplate, zhModelTemplate, zhDeployTemplate, enHelpTemplate} {
246+
if strings.TrimSpace(template.Description) == "" {
247+
t.Fatalf("top_level_commands template missing description: %#v", template)
248+
}
249+
}
250+
251+
raw, err := buildOnboardingManifestJSON(&knowledge.Catalog{
252+
ModelAssets: []knowledge.ModelAsset{
253+
{
254+
Metadata: knowledge.ModelMetadata{
255+
Name: "demo-llm",
256+
Type: "llm",
257+
Family: "demo",
258+
ParameterCount: "1B",
259+
},
260+
},
261+
},
262+
})
263+
if err != nil {
264+
t.Fatalf("build onboarding manifest: %v", err)
223265
}
224266

267+
manifest := decodeOnboardingManifest(t, raw)
225268
zh := manifest.Locales["zh"]
226269
en := manifest.Locales["en"]
227270
if zh == nil || en == nil {
228271
t.Fatalf("missing locales: %#v", manifest.Locales)
229272
}
230273

231274
zhHelp := findOnboardingCommandDescription(t, zh.FullCommands.Groups, "top_level_commands", "/cli help")
232-
enHelp := findOnboardingCommandDescription(t, en.FullCommands.Groups, "top_level_commands", "/cli help")
233-
if zhHelp != "查看任意命令帮助。可直接执行,例如 /cli help;查看 model 的帮助可用 /cli help model。" {
234-
t.Fatalf("zh /cli help description = %q, want %q", zhHelp, "查看任意命令帮助。可直接执行,例如 /cli help;查看 model 的帮助可用 /cli help model。")
275+
if want := replaceSampleModelPlaceholder(zhHelpTemplate.Description, "demo-llm"); zhHelp != want {
276+
t.Fatalf("zh /cli help description = %q, want %q", zhHelp, want)
235277
}
236-
if enHelp != "View help for any command. You can run it directly, for example /cli help; for model help use /cli help model." {
237-
t.Fatalf("en /cli help description = %q, want %q", enHelp, "View help for any command. You can run it directly, for example /cli help; for model help use /cli help model.")
278+
279+
enHelp := findOnboardingCommandDescription(t, en.FullCommands.Groups, "top_level_commands", "/cli help")
280+
if want := replaceSampleModelPlaceholder(enHelpTemplate.Description, "demo-llm"); enHelp != want {
281+
t.Fatalf("en /cli help description = %q, want %q", enHelp, want)
238282
}
239283

240284
zhModel := findOnboardingCommandDescription(t, zh.FullCommands.Groups, "top_level_commands", "/cli model")
241-
if zhModel != "管理模型。需要子命令,例如 /cli model list 或 /cli model pull qwen3-8b。" {
242-
t.Fatalf("zh /cli model description = %q, want %q", zhModel, "管理模型。需要子命令,例如 /cli model list 或 /cli model pull qwen3-8b。")
285+
if want := replaceSampleModelPlaceholder(zhModelTemplate.Description, "demo-llm"); zhModel != want {
286+
t.Fatalf("zh /cli model description = %q, want %q", zhModel, want)
243287
}
244288

245289
zhDeploy := findOnboardingCommandDescription(t, zh.FullCommands.Groups, "top_level_commands", "/cli deploy")
246-
if zhDeploy != "部署推理服务。通常需要模型名,例如 /cli deploy qwen3-8b --dry-run;查看部署列表可用 /cli deploy list。" {
247-
t.Fatalf("zh /cli deploy description = %q, want %q", zhDeploy, "部署推理服务。通常需要模型名,例如 /cli deploy qwen3-8b --dry-run;查看部署列表可用 /cli deploy list。")
290+
if want := replaceSampleModelPlaceholder(zhDeployTemplate.Description, "demo-llm"); zhDeploy != want {
291+
t.Fatalf("zh /cli deploy description = %q, want %q", zhDeploy, want)
248292
}
249293
}
250294

251-
func TestRewriteOnboardingCommands_DropsKnownCommandWhenCLICommandIsMissing(t *testing.T) {
295+
func TestBuildTopLevelOnboardingItems_FallsBackToCommandShort(t *testing.T) {
252296
t.Parallel()
253297

254298
root := &cobra.Command{Use: "aima"}
255-
items := []onboardingCommand{
256-
{ID: "help", Command: "/cli help", Description: "show help"},
257-
{ID: "custom", Command: "/cli custom", Description: "custom command"},
258-
}
299+
root.AddCommand(&cobra.Command{
300+
Use: "version",
301+
Short: "Show version information",
302+
})
259303

260-
got := rewriteOnboardingCommands(items, root, "demo-llm")
261-
if len(got) != 1 {
262-
t.Fatalf("rewriteOnboardingCommands() item count = %d, want 1 (%#v)", len(got), got)
304+
items := buildTopLevelOnboardingItems(root, map[string]string{})
305+
if len(items) != 1 {
306+
t.Fatalf("buildTopLevelOnboardingItems() item count = %d, want 1", len(items))
263307
}
264-
if got[0].ID != "custom" {
265-
t.Fatalf("rewriteOnboardingCommands() preserved wrong item: %#v", got)
308+
if got := items[0]["description"]; got != "Show version information" {
309+
t.Fatalf("buildTopLevelOnboardingItems() description = %#v, want %q", got, "Show version information")
266310
}
267-
if got[0].Command != "/cli custom" {
268-
t.Fatalf("rewriteOnboardingCommands() command = %q, want /cli custom", got[0].Command)
311+
}
312+
313+
func decodeOnboardingManifest(t *testing.T, raw []byte) onboardingManifestDoc {
314+
t.Helper()
315+
316+
var manifest onboardingManifestDoc
317+
if err := json.Unmarshal(raw, &manifest); err != nil {
318+
t.Fatalf("unmarshal onboarding manifest: %v", err)
269319
}
320+
return manifest
270321
}
271322

272-
func findOnboardingCommandDescription(t *testing.T, groups []onboardingGroup, groupID, command string) string {
323+
func findOnboardingGroup(t *testing.T, groups []onboardingGroupDoc, groupID string) onboardingGroupDoc {
273324
t.Helper()
274325

275326
for _, group := range groups {
276-
if group.ID != groupID {
277-
continue
278-
}
279-
for _, item := range group.Items {
280-
if item.Command == command {
281-
return item.Description
282-
}
327+
if group.ID == groupID {
328+
return group
283329
}
284-
t.Fatalf("command %q not found in group %q", command, groupID)
285330
}
286331

287332
t.Fatalf("group %q not found", groupID)
333+
return onboardingGroupDoc{}
334+
}
335+
336+
func findOnboardingCommandByID(t *testing.T, items []onboardingCommandDoc, id string) onboardingCommandDoc {
337+
t.Helper()
338+
339+
for _, item := range items {
340+
if item.ID == id {
341+
return item
342+
}
343+
}
344+
345+
t.Fatalf("command id %q not found", id)
346+
return onboardingCommandDoc{}
347+
}
348+
349+
func findOnboardingCommandDescription(t *testing.T, groups []onboardingGroupDoc, groupID, command string) string {
350+
t.Helper()
351+
352+
group := findOnboardingGroup(t, groups, groupID)
353+
for _, item := range group.Items {
354+
if item.Command == command {
355+
return item.Description
356+
}
357+
}
358+
359+
t.Fatalf("command %q not found in group %q", command, groupID)
288360
return ""
289361
}
290362

0 commit comments

Comments
 (0)