@coze-arch/cli 0.0.19-beta.1 → 0.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/lib/__templates__/nuxt-vue/app/pages/index.vue +6 -0
  2. package/lib/__templates__/nuxt-vue/nuxt.config.ts +2 -2
  3. package/lib/__templates__/pi-agent/AGENTS.md +7 -2
  4. package/lib/__templates__/pi-agent/README.md +2 -0
  5. package/lib/__templates__/pi-agent/docs/project-overview.md +9 -15
  6. package/lib/__templates__/pi-agent/docs/user/getting-started.md +3 -4
  7. package/lib/__templates__/pi-agent/pi-resources/skills/coze-asr/SKILL.md +4 -10
  8. package/lib/__templates__/pi-agent/pi-resources/skills/coze-image-gen/SKILL.md +6 -18
  9. package/lib/__templates__/pi-agent/pi-resources/skills/coze-tts/SKILL.md +9 -37
  10. package/lib/__templates__/pi-agent/pi-resources/skills/coze-video-gen/SKILL.md +17 -30
  11. package/lib/__templates__/pi-agent/src/config.ts +60 -19
  12. package/lib/__templates__/pi-agent/src/core.ts +1 -0
  13. package/lib/__templates__/pi-agent/src/dashboard/index.ts +39 -4
  14. package/lib/__templates__/pi-agent/src/dashboard/server.ts +0 -12
  15. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/app-layout.tsx +1 -15
  16. package/lib/__templates__/pi-agent/src/dashboard/web/src/main.tsx +0 -6
  17. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/chat-page.tsx +0 -11
  18. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/overview-page.tsx +268 -72
  19. package/lib/__templates__/pi-agent/src/dashboard/web/src/styles.css +0 -91
  20. package/lib/__templates__/pi-agent/tests/config.test.ts +63 -1
  21. package/lib/__templates__/templates.json +24 -24
  22. package/lib/cli.js +1 -1
  23. package/package.json +1 -1
  24. package/lib/__templates__/pi-agent/pi-resources/skills/coze-asr/scripts/asr.mjs +0 -9
  25. package/lib/__templates__/pi-agent/pi-resources/skills/coze-image-gen/scripts/gen.mjs +0 -9
  26. package/lib/__templates__/pi-agent/pi-resources/skills/coze-tts/scripts/tts.mjs +0 -9
  27. package/lib/__templates__/pi-agent/pi-resources/skills/coze-video-gen/scripts/gen.mjs +0 -9
  28. package/lib/__templates__/pi-agent/src/dashboard/api/docs.ts +0 -204
  29. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/channels-page.tsx +0 -188
  30. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/docs-page.tsx +0 -65
  31. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/models-page.tsx +0 -122
  32. package/lib/__templates__/pi-agent/tests/dashboard-docs-api.test.ts +0 -125
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import createJiti from "jiti";
4
-
5
- const jiti = createJiti(import.meta.url);
6
- const { runTtsCli } = await jiti.import("../../../src/skill-cli.ts");
7
-
8
- const code = await runTtsCli(process.argv.slice(2), process.env);
9
- process.exit(code);
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import createJiti from "jiti";
4
-
5
- const jiti = createJiti(import.meta.url);
6
- const { runVideoCli } = await jiti.import("../../../src/skill-cli.ts");
7
-
8
- const code = await runVideoCli(process.argv.slice(2), process.env);
9
- process.exit(code);
@@ -1,204 +0,0 @@
1
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
- import { join, relative, sep } from "node:path";
3
-
4
- export type DashboardDocSummary = {
5
- slug: string;
6
- title: string;
7
- summary: string;
8
- group: string;
9
- order: number;
10
- updatedAt: string;
11
- };
12
-
13
- export type DashboardDocDetail = DashboardDocSummary & {
14
- content: string;
15
- };
16
-
17
- export type DashboardDocsResponse = {
18
- docs: DashboardDocSummary[];
19
- selectedDoc: DashboardDocDetail | null;
20
- requestedSlug?: string;
21
- requestedSlugFound: boolean;
22
- };
23
-
24
- type ParsedFrontmatter = {
25
- title?: string;
26
- summary?: string;
27
- group?: string;
28
- order?: number;
29
- };
30
-
31
- function listMarkdownFiles(rootDir: string): string[] {
32
- if (!existsSync(rootDir)) {
33
- return [];
34
- }
35
-
36
- const entries = readdirSync(rootDir, { withFileTypes: true }).sort((left, right) =>
37
- left.name.localeCompare(right.name)
38
- );
39
- const files: string[] = [];
40
-
41
- for (const entry of entries) {
42
- const fullPath = join(rootDir, entry.name);
43
- if (entry.isDirectory()) {
44
- files.push(...listMarkdownFiles(fullPath));
45
- continue;
46
- }
47
- if (entry.isFile() && entry.name.endsWith(".md")) {
48
- files.push(fullPath);
49
- }
50
- }
51
-
52
- return files;
53
- }
54
-
55
- function stripQuotes(value: string): string {
56
- if (
57
- (value.startsWith('"') && value.endsWith('"')) ||
58
- (value.startsWith("'") && value.endsWith("'"))
59
- ) {
60
- return value.slice(1, -1);
61
- }
62
- return value;
63
- }
64
-
65
- function parseFrontmatter(raw: string): { meta: ParsedFrontmatter; content: string } {
66
- const normalized = raw.replace(/\r\n/g, "\n");
67
- if (!normalized.startsWith("---\n")) {
68
- return { meta: {}, content: raw };
69
- }
70
-
71
- const lines = normalized.split("\n");
72
- let closingIndex = -1;
73
- for (let index = 1; index < lines.length; index += 1) {
74
- if (lines[index]?.trim() === "---") {
75
- closingIndex = index;
76
- break;
77
- }
78
- }
79
-
80
- if (closingIndex === -1) {
81
- return { meta: {}, content: raw };
82
- }
83
-
84
- const meta: ParsedFrontmatter = {};
85
- for (const line of lines.slice(1, closingIndex)) {
86
- const trimmed = line.trim();
87
- if (!trimmed || trimmed.startsWith("#")) continue;
88
-
89
- const match = trimmed.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
90
- if (!match) continue;
91
-
92
- const key = match[1]!;
93
- const value = stripQuotes(match[2]!.trim());
94
- if (!value) continue;
95
-
96
- if (key === "order") {
97
- const parsed = Number(value);
98
- if (Number.isFinite(parsed)) {
99
- meta.order = parsed;
100
- }
101
- continue;
102
- }
103
-
104
- if (key === "title") meta.title = value;
105
- if (key === "summary") meta.summary = value;
106
- if (key === "group") meta.group = value;
107
- }
108
-
109
- return {
110
- meta,
111
- content: lines.slice(closingIndex + 1).join("\n").trimStart(),
112
- };
113
- }
114
-
115
- function extractHeading(content: string): string {
116
- const lines = content.replace(/\r\n/g, "\n").split("\n");
117
- for (const line of lines) {
118
- const match = line.trim().match(/^#\s+(.+)$/);
119
- if (match) {
120
- return match[1]!.trim();
121
- }
122
- }
123
- return "";
124
- }
125
-
126
- function extractSummary(content: string): string {
127
- const lines = content.replace(/\r\n/g, "\n").split("\n");
128
- const paragraph: string[] = [];
129
- let inCodeBlock = false;
130
-
131
- for (const line of lines) {
132
- const trimmed = line.trim();
133
- if (!trimmed) {
134
- if (paragraph.length > 0) break;
135
- continue;
136
- }
137
- if (trimmed.startsWith("```")) {
138
- inCodeBlock = !inCodeBlock;
139
- if (paragraph.length > 0) break;
140
- continue;
141
- }
142
- if (inCodeBlock) continue;
143
- if (trimmed.startsWith("#")) continue;
144
- if (trimmed.startsWith("- ") || trimmed.startsWith("* ") || /^\d+\.\s/.test(trimmed)) {
145
- continue;
146
- }
147
-
148
- paragraph.push(trimmed.replace(/^>\s?/, ""));
149
- }
150
-
151
- return paragraph.join(" ").trim();
152
- }
153
-
154
- function createTitleFromSlug(slug: string): string {
155
- const tail = slug.split("/").at(-1) ?? slug;
156
- return tail
157
- .split(/[-_]/g)
158
- .filter(Boolean)
159
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
160
- .join(" ");
161
- }
162
-
163
- function loadDoc(filePath: string, docsDir: string): DashboardDocDetail {
164
- const relativePath = relative(docsDir, filePath).split(sep).join("/");
165
- const slug = relativePath.replace(/\.md$/i, "");
166
- const raw = readFileSync(filePath, "utf-8");
167
- const parsed = parseFrontmatter(raw);
168
- const title = parsed.meta.title?.trim() || extractHeading(parsed.content) || createTitleFromSlug(slug);
169
- const summary = parsed.meta.summary?.trim() || extractSummary(parsed.content);
170
- const group = parsed.meta.group?.trim() || "其他";
171
- const order = parsed.meta.order ?? Number.MAX_SAFE_INTEGER;
172
- const updatedAt = statSync(filePath).mtime.toISOString();
173
-
174
- return {
175
- slug,
176
- title,
177
- summary,
178
- group,
179
- order,
180
- updatedAt,
181
- content: parsed.content,
182
- };
183
- }
184
-
185
- export function readDocsResponse(args: { docsDir: string; slug?: string }): DashboardDocsResponse {
186
- const docs = listMarkdownFiles(args.docsDir)
187
- .map((filePath) => loadDoc(filePath, args.docsDir))
188
- .sort((left, right) => {
189
- if (left.order !== right.order) return left.order - right.order;
190
- return left.title.localeCompare(right.title);
191
- });
192
-
193
- const requestedSlug = args.slug?.trim();
194
- const selectedDoc = requestedSlug
195
- ? docs.find((doc) => doc.slug === requestedSlug) ?? docs[0] ?? null
196
- : docs[0] ?? null;
197
-
198
- return {
199
- docs: docs.map(({ content: _content, ...summary }) => summary),
200
- selectedDoc,
201
- requestedSlug,
202
- requestedSlugFound: requestedSlug ? selectedDoc?.slug === requestedSlug : true,
203
- };
204
- }
@@ -1,188 +0,0 @@
1
- import React from "react";
2
- import { Alert, AlertDescription, AlertTitle } from "../components/ui/alert";
3
- import { Button } from "../components/ui/button";
4
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card";
5
- import { Input } from "../components/ui/input";
6
- import { Label } from "../components/ui/label";
7
- import { useFetch } from "../hooks/use-fetch";
8
- import { toast } from "sonner";
9
- import { Link } from "react-router-dom";
10
-
11
- type Channels = {
12
- routing: {
13
- feishuGroupRequireMention: boolean;
14
- wechatGroupRequireMention: boolean;
15
- };
16
- feishu: {
17
- enabled: boolean;
18
- requireMention?: boolean;
19
- appId?: string;
20
- domain?: string;
21
- encryptKey?: string;
22
- verificationToken?: string;
23
- appSecret?: string;
24
- thinkingReaction?: {
25
- enabled?: boolean;
26
- emojiType?: string;
27
- };
28
- };
29
- wechat: {
30
- enabled: boolean;
31
- requireMention?: boolean;
32
- implementation?: string;
33
- };
34
- };
35
-
36
- export function ChannelsPage() {
37
- const { data, error, loading } = useFetch<Channels>("/api/channels");
38
- const [saving, setSaving] = React.useState(false);
39
- const [form, setForm] = React.useState<Channels | null>(null);
40
-
41
- React.useEffect(() => {
42
- if (data) setForm(data);
43
- }, [data]);
44
-
45
- const patch = (fn: (draft: Channels) => void) => {
46
- if (!form) return;
47
- const draft = { ...form, feishu: { ...form.feishu }, wechat: { ...form.wechat } };
48
- fn(draft);
49
- setForm(draft);
50
- };
51
-
52
- const onSave = async () => {
53
- if (!form) return;
54
- setSaving(true);
55
- try {
56
- const res = await fetch("/api/channels", {
57
- method: "POST",
58
- headers: { "Content-Type": "application/json" },
59
- body: JSON.stringify({ channels: form }),
60
- });
61
- const json = (await res.json()) as { ok?: boolean; error?: string };
62
- if (!res.ok || json.ok === false) {
63
- throw new Error(json.error || `${res.status} ${res.statusText}`);
64
- }
65
- toast.success("已保存(需要重启进程生效)");
66
- } catch (e) {
67
- toast.error(`保存失败:${String(e)}`, { duration: 4500 });
68
- } finally {
69
- setSaving(false);
70
- }
71
- };
72
-
73
- return (
74
- <section className="space-y-4 px-4 pb-4 pt-0 sm:px-5 sm:pb-5 sm:pt-0 md:px-6 md:pb-6 md:pt-0">
75
- <div className="sticky top-0 z-20 -mx-4 bg-background px-4 pb-3 pt-4 sm:-mx-5 sm:px-5 sm:pb-4 sm:pt-5 md:-mx-6 md:px-6 md:pb-5 md:pt-6">
76
- <div className="flex flex-col gap-3 bg-background md:flex-row md:items-start md:justify-between">
77
- <div className="space-y-1">
78
- <h1 className="text-lg font-semibold tracking-tight">渠道配置</h1>
79
- <p className="text-sm text-muted-foreground">管理各渠道的启用状态与接入参数。</p>
80
- </div>
81
- <Button
82
- onClick={onSave}
83
- disabled={!form || saving || loading || Boolean(error)}
84
- className="shrink-0"
85
- >
86
- {saving ? "保存中…" : "保存"}
87
- </Button>
88
- </div>
89
- </div>
90
-
91
- {loading ? (
92
- <Alert>
93
- <AlertTitle>加载中</AlertTitle>
94
- <AlertDescription>正在读取渠道配置。</AlertDescription>
95
- </Alert>
96
- ) : error ? (
97
- <Alert className="border-destructive/20 bg-destructive/5 text-destructive">
98
- <AlertTitle>读取失败</AlertTitle>
99
- <AlertDescription>错误:{error}</AlertDescription>
100
- </Alert>
101
- ) : form ? (
102
- <>
103
- <div className="space-y-4">
104
- <Card className="border-border/60 bg-background/70 shadow-none">
105
- <CardHeader>
106
- <div className="flex items-center gap-3">
107
- <CardTitle>飞书</CardTitle>
108
- <Link
109
- to="/docs"
110
- className="text-sm font-medium text-muted-foreground underline decoration-border underline-offset-4 transition-colors hover:text-primary"
111
- >
112
- 接入说明
113
- </Link>
114
- </div>
115
- <CardDescription>管理飞书渠道的启用状态与接入凭证。</CardDescription>
116
- </CardHeader>
117
- <CardContent className="grid gap-4">
118
- <CheckboxField
119
- label="启用状态"
120
- checked={!!form.feishu.enabled}
121
- onChange={(checked) => patch((d) => (d.feishu.enabled = checked))}
122
- />
123
- <TextField
124
- label="App ID"
125
- value={form.feishu.appId ?? ""}
126
- onChange={(value) => patch((d) => (d.feishu.appId = value))}
127
- />
128
- <TextField
129
- label="App Secret"
130
- value={form.feishu.appSecret ?? ""}
131
- onChange={(value) => patch((d) => (d.feishu.appSecret = value))}
132
- />
133
- </CardContent>
134
- </Card>
135
- </div>
136
- </>
137
- ) : null}
138
- </section>
139
- );
140
- }
141
-
142
- function FieldShell(props: { label: string; children: React.ReactNode; description?: string }) {
143
- return (
144
- <div className="space-y-2">
145
- <Label className="text-xs uppercase tracking-[0.16em] text-muted-foreground">{props.label}</Label>
146
- {props.children}
147
- {props.description ? <p className="text-xs text-muted-foreground">{props.description}</p> : null}
148
- </div>
149
- );
150
- }
151
-
152
- function TextField(props: {
153
- label: string;
154
- value: string;
155
- placeholder?: string;
156
- onChange: (value: string) => void;
157
- }) {
158
- return (
159
- <FieldShell label={props.label}>
160
- <Input
161
- className="font-mono text-xs sm:text-sm"
162
- value={props.value}
163
- placeholder={props.placeholder}
164
- onChange={(e) => props.onChange(e.target.value)}
165
- />
166
- </FieldShell>
167
- );
168
- }
169
-
170
- function CheckboxField(props: {
171
- label: string;
172
- checked: boolean;
173
- onChange: (checked: boolean) => void;
174
- }) {
175
- return (
176
- <FieldShell label={props.label}>
177
- <label className="flex h-10 items-center gap-3 rounded-md border border-input bg-background/80 px-3 text-sm">
178
- <input
179
- type="checkbox"
180
- className="h-4 w-4 rounded border-border accent-primary"
181
- checked={props.checked}
182
- onChange={(e) => props.onChange(e.target.checked)}
183
- />
184
- <span className="text-sm text-foreground">{props.checked ? "已启用" : "未启用"}</span>
185
- </label>
186
- </FieldShell>
187
- );
188
- }
@@ -1,65 +0,0 @@
1
- import type { ComponentPropsWithoutRef } from "react";
2
- import { Streamdown } from "streamdown";
3
- import { code } from "@streamdown/code";
4
- import { Link } from "react-router-dom";
5
- import { Alert, AlertDescription, AlertTitle } from "../components/ui/alert";
6
- import { useFetch } from "../hooks/use-fetch";
7
-
8
- type DashboardDocDetail = {
9
- slug: string;
10
- title: string;
11
- content: string;
12
- };
13
-
14
- type DashboardDocsResponse = {
15
- selectedDoc: DashboardDocDetail | null;
16
- };
17
-
18
- const DEFAULT_DOC_SLUG = "getting-started";
19
-
20
- function DocsLink(props: ComponentPropsWithoutRef<"a">) {
21
- const href = typeof props.href === "string" ? props.href : "";
22
- const isInternalRoute = href.startsWith("/") && !href.startsWith("//");
23
-
24
- if (isInternalRoute) {
25
- return (
26
- <Link to={href} className={props.className} title={props.title}>
27
- {props.children}
28
- </Link>
29
- );
30
- }
31
-
32
- return <a {...props} />;
33
- }
34
-
35
- export function DocsPage() {
36
- const { data, error, loading } = useFetch<DashboardDocsResponse>(`/api/docs?slug=${DEFAULT_DOC_SLUG}`);
37
- const selectedDoc = data?.selectedDoc ?? null;
38
-
39
- return (
40
- <section className="p-4 sm:p-5 md:p-6">
41
- {loading ? (
42
- <Alert>
43
- <AlertTitle>加载中</AlertTitle>
44
- <AlertDescription>正在读取项目文档。</AlertDescription>
45
- </Alert>
46
- ) : error ? (
47
- <Alert className="border-destructive/20 bg-destructive/5 text-destructive">
48
- <AlertTitle>读取失败</AlertTitle>
49
- <AlertDescription>错误:{error}</AlertDescription>
50
- </Alert>
51
- ) : !selectedDoc ? (
52
- <Alert>
53
- <AlertTitle>暂无文档</AlertTitle>
54
- <AlertDescription>当前项目里还没有可展示的用户文档。</AlertDescription>
55
- </Alert>
56
- ) : (
57
- <div className="docs-content streamdown-content">
58
- <Streamdown plugins={{ code }} parseIncompleteMarkdown={true} components={{ a: DocsLink }}>
59
- {selectedDoc.content}
60
- </Streamdown>
61
- </div>
62
- )}
63
- </section>
64
- );
65
- }
@@ -1,122 +0,0 @@
1
- import React from "react";
2
- import { Alert, AlertDescription, AlertTitle } from "../components/ui/alert";
3
- import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
4
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../components/ui/select";
5
- import { useFetch } from "../hooks/use-fetch";
6
- import { toast } from "sonner";
7
-
8
- type ModelOption = {
9
- value: string;
10
- label: string;
11
- };
12
-
13
- type ModelsConfig = {
14
- defaultModel: string;
15
- options: ModelOption[];
16
- };
17
-
18
- export function ModelsPage() {
19
- const { data, error, loading } = useFetch<ModelsConfig>("/api/models");
20
- const [saving, setSaving] = React.useState(false);
21
- const [form, setForm] = React.useState<ModelsConfig | null>(null);
22
-
23
- React.useEffect(() => {
24
- if (!data) return;
25
- // Be defensive: if the backend returns an older shape or an error envelope,
26
- // we still want a non-crashing UI.
27
- setForm({
28
- defaultModel: typeof data.defaultModel === "string" ? data.defaultModel : "",
29
- options: Array.isArray((data as unknown as { options?: unknown }).options)
30
- ? (data as unknown as { options: ModelOption[] }).options
31
- : [],
32
- });
33
- }, [data]);
34
-
35
- const saveDefaultModel = async (defaultModel: string) => {
36
- setSaving(true);
37
- try {
38
- const res = await fetch("/api/models", {
39
- method: "POST",
40
- headers: { "Content-Type": "application/json" },
41
- body: JSON.stringify({ models: { defaultModel } }),
42
- });
43
- const json = (await res.json()) as { ok?: boolean; error?: string };
44
- if (!res.ok || json.ok === false) {
45
- throw new Error(json.error || `${res.status} ${res.statusText}`);
46
- }
47
- toast.success("已保存(需要重启进程生效)");
48
- } catch (e) {
49
- toast.error(`保存失败:${String(e)}`, { duration: 4500 });
50
- throw e;
51
- } finally {
52
- setSaving(false);
53
- }
54
- };
55
-
56
- const options = form?.options ?? [];
57
- const onDefaultModelChange = async (nextValue: string) => {
58
- if (!form || nextValue === form.defaultModel) return;
59
- const previousValue = form.defaultModel;
60
- setForm({ ...form, defaultModel: nextValue });
61
- try {
62
- await saveDefaultModel(nextValue);
63
- } catch {
64
- setForm((current) => (current ? { ...current, defaultModel: previousValue } : current));
65
- }
66
- };
67
-
68
- return (
69
- <section className="space-y-4 px-4 pb-4 pt-0">
70
- <div className="sticky top-0 z-20 -mx-4 bg-background px-4 pb-3 pt-4">
71
- <div className="space-y-1 bg-background py-0">
72
- <div className="space-y-1">
73
- <h1 className="text-lg font-semibold tracking-tight">模型配置</h1>
74
- </div>
75
- </div>
76
- </div>
77
-
78
- {loading ? (
79
- <Alert>
80
- <AlertTitle>加载中</AlertTitle>
81
- <AlertDescription>正在读取模型配置。</AlertDescription>
82
- </Alert>
83
- ) : error ? (
84
- <Alert className="border-destructive/20 bg-destructive/5 text-destructive">
85
- <AlertTitle>读取失败</AlertTitle>
86
- <AlertDescription>错误:{error}</AlertDescription>
87
- </Alert>
88
- ) : form ? (
89
- options.length > 0 ? (
90
- <Card className="border-border/60 bg-background/70 shadow-none">
91
- <CardHeader className="!pb-3">
92
- <CardTitle className="text-sm">默认模型</CardTitle>
93
- </CardHeader>
94
- <CardContent className="pb-5 pt-0">
95
- <Select
96
- value={form.defaultModel}
97
- onValueChange={onDefaultModelChange}
98
- disabled={saving}
99
- >
100
- <SelectTrigger aria-label="选择默认模型">
101
- <SelectValue placeholder="选择默认模型" />
102
- </SelectTrigger>
103
- <SelectContent>
104
- {options.map((option) => (
105
- <SelectItem key={option.value} value={option.value}>
106
- {option.label}
107
- </SelectItem>
108
- ))}
109
- </SelectContent>
110
- </Select>
111
- </CardContent>
112
- </Card>
113
- ) : (
114
- <Alert>
115
- <AlertTitle>没有可选模型</AlertTitle>
116
- <AlertDescription>当前还没有可用模型,请先补充模型定义。</AlertDescription>
117
- </Alert>
118
- )
119
- ) : null}
120
- </section>
121
- );
122
- }