@zhin.js/console 1.0.21 → 1.0.23

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 (56) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +4 -4
  3. package/client/components.json +17 -0
  4. package/client/index.html +1 -1
  5. package/client/src/components/PluginConfigForm/BasicFieldRenderers.tsx +89 -180
  6. package/client/src/components/PluginConfigForm/CollectionFieldRenderers.tsx +97 -200
  7. package/client/src/components/PluginConfigForm/CompositeFieldRenderers.tsx +31 -70
  8. package/client/src/components/PluginConfigForm/FieldRenderer.tsx +27 -77
  9. package/client/src/components/PluginConfigForm/NestedFieldRenderer.tsx +33 -53
  10. package/client/src/components/PluginConfigForm/index.tsx +71 -173
  11. package/client/src/components/ui/accordion.tsx +54 -0
  12. package/client/src/components/ui/alert.tsx +62 -0
  13. package/client/src/components/ui/avatar.tsx +41 -0
  14. package/client/src/components/ui/badge.tsx +32 -0
  15. package/client/src/components/ui/button.tsx +50 -0
  16. package/client/src/components/ui/card.tsx +50 -0
  17. package/client/src/components/ui/checkbox.tsx +25 -0
  18. package/client/src/components/ui/dialog.tsx +87 -0
  19. package/client/src/components/ui/dropdown-menu.tsx +97 -0
  20. package/client/src/components/ui/input.tsx +21 -0
  21. package/client/src/components/ui/scroll-area.tsx +43 -0
  22. package/client/src/components/ui/select.tsx +127 -0
  23. package/client/src/components/ui/separator.tsx +23 -0
  24. package/client/src/components/ui/skeleton.tsx +12 -0
  25. package/client/src/components/ui/switch.tsx +26 -0
  26. package/client/src/components/ui/tabs.tsx +52 -0
  27. package/client/src/components/ui/textarea.tsx +20 -0
  28. package/client/src/components/ui/tooltip.tsx +27 -0
  29. package/client/src/layouts/dashboard.tsx +91 -221
  30. package/client/src/main.tsx +38 -42
  31. package/client/src/pages/dashboard-bots.tsx +91 -137
  32. package/client/src/pages/dashboard-home.tsx +133 -204
  33. package/client/src/pages/dashboard-logs.tsx +125 -196
  34. package/client/src/pages/dashboard-plugin-detail.tsx +261 -329
  35. package/client/src/pages/dashboard-plugins.tsx +108 -105
  36. package/client/src/style.css +156 -865
  37. package/client/src/theme/index.ts +60 -35
  38. package/client/tailwind.config.js +78 -69
  39. package/dist/client.js +1 -1
  40. package/dist/cva.js +47 -0
  41. package/dist/index.html +1 -1
  42. package/dist/index.js +6 -6
  43. package/dist/react-router.js +7121 -5585
  44. package/dist/react.js +192 -149
  45. package/dist/style.css +2 -2
  46. package/lib/bin.js +2 -2
  47. package/lib/build.js +2 -2
  48. package/lib/index.d.ts +0 -3
  49. package/lib/index.js +160 -205
  50. package/lib/transform.d.ts +26 -0
  51. package/lib/transform.js +78 -0
  52. package/lib/websocket.d.ts +0 -1
  53. package/package.json +9 -8
  54. package/dist/radix-ui-themes.js +0 -9305
  55. package/lib/dev.d.ts +0 -18
  56. package/lib/dev.js +0 -87
@@ -1,53 +1,50 @@
1
1
  import { useEffect, useState } from 'react'
2
2
  import { useParams, useNavigate } from 'react-router'
3
- import * as Themes from '@radix-ui/themes'
4
- import { ArrowLeft, AlertCircle, Package, Terminal, Box as IconBox, Layers, Database, Clock, FileText } from 'lucide-react'
5
- import {PluginConfigForm} from '../components/PluginConfigForm'
3
+ import { ArrowLeft, AlertCircle, Package, Terminal, Box as IconBox, Layers, Clock, Database, Brain, Wrench, Shield, Settings, Plug, Server, type LucideIcon } from 'lucide-react'
4
+ import { PluginConfigForm } from '../components/PluginConfigForm'
5
+ import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card'
6
+ import { Badge } from '../components/ui/badge'
7
+ import { Button } from '../components/ui/button'
8
+ import { Alert, AlertDescription } from '../components/ui/alert'
9
+ import { Skeleton } from '../components/ui/skeleton'
10
+ import { Separator } from '../components/ui/separator'
6
11
 
7
- const { Flex, Box, Spinner, Text, Callout, Heading, Badge, Grid, Card, Button, Code, Separator, ScrollArea, Dialog } = Themes
12
+
13
+ /** Feature 序列化格式(与后端 FeatureJSON 一致) */
14
+ interface FeatureJSON {
15
+ name: string
16
+ icon: string
17
+ desc: string
18
+ count: number
19
+ items: any[]
20
+ }
8
21
 
9
22
  interface PluginDetail {
10
23
  name: string
11
24
  filename: string
12
25
  status: 'active' | 'inactive'
13
26
  description: string
14
- commands: Array<{
15
- name: string
16
- desc?: string[]
17
- usage?: string[]
18
- examples?: string[]
19
- help?: string
20
- }>
21
- components: Array<{
22
- name: string
23
- props: Record<string, any>
24
- type: string
25
- }>
26
- middlewares: Array<{
27
- id: string
28
- type: string
29
- }>
30
- contexts: Array<{
31
- name: string
32
- description: string
33
- }>
34
- crons: Array<{
35
- id: string
36
- pattern: string
37
- running: boolean
38
- }>
39
- definitions: Array<{
40
- name: string
41
- fields: string[]
42
- }>
43
- statistics: {
44
- commandCount: number
45
- componentCount: number
46
- middlewareCount: number
47
- contextCount: number
48
- cronCount: number
49
- definitionCount: number
50
- }
27
+ features: FeatureJSON[]
28
+ contexts: Array<{ name: string }>
29
+ }
30
+
31
+ /** 根据后端返回的 icon 名称映射到 lucide-react 图标组件 */
32
+ const iconMap: Record<string, LucideIcon> = {
33
+ Terminal,
34
+ Box: IconBox,
35
+ Layers,
36
+ Clock,
37
+ Brain,
38
+ Wrench,
39
+ Database,
40
+ Shield,
41
+ Settings,
42
+ Plug,
43
+ Server,
44
+ }
45
+
46
+ function getIcon(iconName: string): LucideIcon {
47
+ return iconMap[iconName] || Package
51
48
  }
52
49
 
53
50
  export default function DashboardPluginDetail() {
@@ -58,23 +55,16 @@ export default function DashboardPluginDetail() {
58
55
  const [error, setError] = useState<string | null>(null)
59
56
 
60
57
  useEffect(() => {
61
- if (name) {
62
- fetchPluginDetail(name)
63
- }
58
+ if (name) fetchPluginDetail(name)
64
59
  }, [name])
65
60
 
66
61
  const fetchPluginDetail = async (pluginName: string) => {
67
62
  try {
68
63
  const res = await fetch(`/api/plugins/${encodeURIComponent(pluginName)}`, { credentials: 'include' })
69
64
  if (!res.ok) throw new Error('API 请求失败')
70
-
71
65
  const data = await res.json()
72
- if (data.success) {
73
- setPlugin(data.data)
74
- setError(null)
75
- } else {
76
- throw new Error('数据格式错误')
77
- }
66
+ if (data.success) { setPlugin(data.data); setError(null) }
67
+ else throw new Error('数据格式错误')
78
68
  } catch (err) {
79
69
  setError((err as Error).message)
80
70
  } finally {
@@ -82,307 +72,249 @@ export default function DashboardPluginDetail() {
82
72
  }
83
73
  }
84
74
 
85
-
86
-
87
75
  if (loading) {
88
76
  return (
89
- <Flex align="center" justify="center" className="h-full">
90
- <Flex direction="column" align="center" gap="3">
91
- <Spinner size="3" />
92
- <Text size="2" color="gray">加载中...</Text>
93
- </Flex>
94
- </Flex>
77
+ <div className="space-y-4">
78
+ <Skeleton className="h-8 w-24" />
79
+ <div className="flex items-center gap-3"><Skeleton className="h-12 w-12 rounded-xl" /><div><Skeleton className="h-6 w-48" /><Skeleton className="h-4 w-64 mt-1" /></div></div>
80
+ <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-2">{[...Array(6)].map((_, i) => <Skeleton key={i} className="h-20" />)}</div>
81
+ </div>
95
82
  )
96
83
  }
97
84
 
98
85
  if (error || !plugin) {
99
86
  return (
100
- <Box>
101
- <Button variant="ghost" onClick={() => navigate('/plugins')} mb="4" size="2">
102
- <ArrowLeft className="w-4 h-4" />
103
- 返回
87
+ <div>
88
+ <Button variant="ghost" onClick={() => navigate('/plugins')} className="mb-4">
89
+ <ArrowLeft className="w-4 h-4 mr-1" /> 返回
104
90
  </Button>
105
- <Callout.Root color="red">
106
- <Callout.Icon>
107
- <AlertCircle />
108
- </Callout.Icon>
109
- <Callout.Text>
110
- 加载失败: {error || '插件不存在'}
111
- </Callout.Text>
112
- </Callout.Root>
113
- </Box>
91
+ <Alert variant="destructive">
92
+ <AlertCircle className="h-4 w-4" />
93
+ <AlertDescription>加载失败: {error || '插件不存在'}</AlertDescription>
94
+ </Alert>
95
+ </div>
114
96
  )
115
97
  }
116
98
 
117
99
  return (
118
- <Box>
119
- {/* 头部 */}
120
- <Flex direction="column" gap="3" mb="4">
121
- <Button variant="ghost" onClick={() => navigate('/plugins')} size="2">
122
- <ArrowLeft className="w-4 h-4" />
123
- 返回
124
- </Button>
125
-
126
- <Flex align="center" gap="3">
127
- <Flex align="center" justify="center" className="w-12 h-12 rounded-xl bg-blue-500/10 dark:bg-blue-400/10">
128
- <Package className="w-6 h-6 text-blue-600 dark:text-blue-400" />
129
- </Flex>
130
- <Flex direction="column" gap="1">
131
- <Flex align="center" gap="2">
132
- <Heading size="5">{plugin.name}</Heading>
133
- <Badge color={plugin.status === 'active' ? 'green' : 'gray'} size="1">
134
- {plugin.status === 'active' ? '运行中' : '已停止'}
135
- </Badge>
136
- </Flex>
137
- <Text size="2" color="gray">{plugin.description || '暂无描述'}</Text>
138
- </Flex>
139
- </Flex>
140
- </Flex>
141
-
142
- {/* 插件配置折叠面板 */}
143
- <PluginConfigForm
144
- pluginName={plugin.name}
145
- onSuccess={() => {
146
- // 配置更新会通过 WebSocket 自动同步
147
- }}
148
- />
100
+ <div className="space-y-4">
101
+ {/* Header */}
102
+ <Button variant="ghost" size="sm" onClick={() => navigate('/plugins')}>
103
+ <ArrowLeft className="w-4 h-4 mr-1" /> 返回
104
+ </Button>
149
105
 
150
- <Separator size="4" my="4" />
106
+ <div className="flex items-center gap-3">
107
+ <div className="flex items-center justify-center w-12 h-12 rounded-xl bg-secondary">
108
+ <Package className="w-6 h-6" />
109
+ </div>
110
+ <div>
111
+ <div className="flex items-center gap-2">
112
+ <h1 className="text-xl font-bold">{plugin.name}</h1>
113
+ <Badge variant={plugin.status === 'active' ? 'success' : 'secondary'}>
114
+ {plugin.status === 'active' ? '运行中' : '已停止'}
115
+ </Badge>
116
+ </div>
117
+ <p className="text-sm text-muted-foreground">{plugin.description || '暂无描述'}</p>
118
+ </div>
119
+ </div>
151
120
 
152
- {/* 统计概览 - 紧凑卡片 */}
153
- <Grid columns={{ initial: '2', sm: '3', md: '6' }} gap="2" mb="4">
154
- <Card size="1">
155
- <Flex direction="column" align="center" gap="1" p="2">
156
- <Terminal className="w-4 h-4 text-blue-600 dark:text-blue-400" />
157
- <Text size="4" weight="bold">{plugin.statistics.commandCount}</Text>
158
- <Text size="1" color="gray">命令</Text>
159
- </Flex>
160
- </Card>
121
+ {/* Config form */}
122
+ <PluginConfigForm pluginName={plugin.name} onSuccess={() => {}} />
161
123
 
162
- <Card size="1">
163
- <Flex direction="column" align="center" gap="1" p="2">
164
- <IconBox className="w-4 h-4 text-green-600 dark:text-green-400" />
165
- <Text size="4" weight="bold">{plugin.statistics.componentCount}</Text>
166
- <Text size="1" color="gray">组件</Text>
167
- </Flex>
168
- </Card>
124
+ <Separator />
169
125
 
170
- <Card size="1">
171
- <Flex direction="column" align="center" gap="1" p="2">
172
- <Layers className="w-4 h-4 text-purple-600 dark:text-purple-400" />
173
- <Text size="4" weight="bold">{plugin.statistics.middlewareCount}</Text>
174
- <Text size="1" color="gray">中间件</Text>
175
- </Flex>
176
- </Card>
126
+ {/* Stats grid - 动态渲染 features 摘要 */}
127
+ {plugin.features.length > 0 && (
128
+ <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-2">
129
+ {plugin.features.map((feature) => {
130
+ const Icon = getIcon(feature.icon)
131
+ return (
132
+ <Card key={feature.name}>
133
+ <CardContent className="flex flex-col items-center gap-1 p-3">
134
+ <Icon className="w-4 h-4 text-muted-foreground" />
135
+ <span className="text-2xl font-bold">{feature.count}</span>
136
+ <span className="text-xs text-muted-foreground">{feature.desc}</span>
137
+ </CardContent>
138
+ </Card>
139
+ )
140
+ })}
141
+ {/* Contexts card */}
142
+ {plugin.contexts.length > 0 && (
143
+ <Card>
144
+ <CardContent className="flex flex-col items-center gap-1 p-3">
145
+ <Database className="w-4 h-4 text-muted-foreground" />
146
+ <span className="text-2xl font-bold">{plugin.contexts.length}</span>
147
+ <span className="text-xs text-muted-foreground">上下文</span>
148
+ </CardContent>
149
+ </Card>
150
+ )}
151
+ </div>
152
+ )}
177
153
 
178
- <Card size="1">
179
- <Flex direction="column" align="center" gap="1" p="2">
180
- <Database className="w-4 h-4 text-orange-600 dark:text-orange-400" />
181
- <Text size="4" weight="bold">{plugin.statistics.contextCount}</Text>
182
- <Text size="1" color="gray">上下文</Text>
183
- </Flex>
184
- </Card>
154
+ {/* Detail sections - 动态渲染每个 Feature 的 items */}
155
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
156
+ {plugin.features.map((feature) => {
157
+ if (feature.items.length === 0) return null
158
+ const Icon = getIcon(feature.icon)
185
159
 
186
- <Card size="1">
187
- <Flex direction="column" align="center" gap="1" p="2">
188
- <Clock className="w-4 h-4 text-amber-600 dark:text-amber-400" />
189
- <Text size="4" weight="bold">{plugin.statistics.cronCount}</Text>
190
- <Text size="1" color="gray">定时任务</Text>
191
- </Flex>
192
- </Card>
193
-
194
- <Card size="1">
195
- <Flex direction="column" align="center" gap="1" p="2">
196
- <FileText className="w-4 h-4 text-cyan-600 dark:text-cyan-400" />
197
- <Text size="4" weight="bold">{plugin.statistics.definitionCount}</Text>
198
- <Text size="1" color="gray">数据模型</Text>
199
- </Flex>
200
- </Card>
201
- </Grid>
202
-
203
- {/* 详细信息 - 紧凑布局 */}
204
- <Grid columns={{ initial: '1', md: '2' }} gap="3">
205
- {/* 命令列表 */}
206
- {plugin.commands.length > 0 && (
207
- <Card size="2">
208
- <Flex direction="column" gap="2" p="3">
209
- <Flex align="center" gap="2">
210
- <Terminal className="w-4 h-4 text-blue-600 dark:text-blue-400" />
211
- <Heading size="3">命令</Heading>
212
- <Badge size="1">{plugin.commands.length}</Badge>
213
- </Flex>
214
- <Separator size="4" />
215
- <Flex direction="column" gap="2" className="max-h-60 overflow-y-auto">
216
- {plugin.commands.map((cmd, index) => (
217
- <Box key={index} className="rounded-lg bg-gray-50 dark:bg-gray-900 p-3">
218
- <Flex direction="column" gap="2">
219
- <Code size="2" weight="bold">{cmd.name}</Code>
220
-
221
- {cmd.desc && cmd.desc.length > 0 && (
222
- <Flex direction="column" gap="1">
223
- {cmd.desc.map((desc, i) => (
224
- <Text key={i} size="1" color="gray">{desc}</Text>
225
- ))}
226
- </Flex>
227
- )}
228
-
229
- {cmd.usage && cmd.usage.length > 0 && (
230
- <Flex direction="column" gap="1">
231
- <Text size="1" weight="bold" color="blue">用法:</Text>
232
- {cmd.usage.map((usage, i) => (
233
- <Code key={i} size="1" variant="soft">{usage}</Code>
234
- ))}
235
- </Flex>
236
- )}
237
-
238
- {cmd.examples && cmd.examples.length > 0 && (
239
- <Flex direction="column" gap="1">
240
- <Text size="1" weight="bold" color="green">示例:</Text>
241
- {cmd.examples.map((example, i) => (
242
- <Code key={i} size="1" variant="soft" color="green">{example}</Code>
243
- ))}
244
- </Flex>
245
- )}
246
- </Flex>
247
- </Box>
248
- ))}
249
- </Flex>
250
- </Flex>
251
- </Card>
252
- )}
160
+ return (
161
+ <Card key={feature.name}>
162
+ <CardHeader className="pb-2">
163
+ <div className="flex items-center gap-2">
164
+ <Icon className="w-4 h-4 text-muted-foreground" />
165
+ <CardTitle className="text-base">{feature.desc}</CardTitle>
166
+ <Badge variant="secondary">{feature.count}</Badge>
167
+ </div>
168
+ </CardHeader>
169
+ <CardContent>
170
+ <div className="max-h-60 overflow-y-auto pr-1 space-y-2">
171
+ {feature.items.map((item, index) => (
172
+ <FeatureItemCard key={index} featureName={feature.name} item={item} />
173
+ ))}
174
+ </div>
175
+ </CardContent>
176
+ </Card>
177
+ )
178
+ })}
253
179
 
254
- {/* 组件列表 */}
255
- {plugin.components.length > 0 && (
256
- <Card size="2">
257
- <Flex direction="column" gap="2" p="3">
258
- <Flex align="center" gap="2">
259
- <Box className="w-4 h-4 text-green-600 dark:text-green-400" />
260
- <Heading size="3">组件</Heading>
261
- <Badge size="1">{plugin.components.length}</Badge>
262
- </Flex>
263
- <Separator size="4" />
264
- <Flex direction="column" gap="2" className="max-h-60 overflow-y-auto">
265
- {plugin.components.map((comp, index) => (
266
- <Box key={index} className="rounded-lg bg-gray-50 dark:bg-gray-900 p-2">
267
- <Flex direction="column" gap="1">
268
- <Flex align="center" gap="2">
269
- <Code size="2">{comp.name}</Code>
270
- <Badge size="1" variant="soft">{comp.type}</Badge>
271
- </Flex>
272
- <Text size="1" color="gray">
273
- 属性数: {Object.keys(comp.props).length}
274
- </Text>
275
- </Flex>
276
- </Box>
277
- ))}
278
- </Flex>
279
- </Flex>
280
- </Card>
281
- )}
282
-
283
- {/* 中间件列表 */}
284
- {plugin.middlewares.length > 0 && (
285
- <Card size="2">
286
- <Flex direction="column" gap="2" p="3">
287
- <Flex align="center" gap="2">
288
- <Layers className="w-4 h-4 text-purple-600 dark:text-purple-400" />
289
- <Heading size="3">中间件</Heading>
290
- <Badge size="1">{plugin.middlewares.length}</Badge>
291
- </Flex>
292
- <Separator size="4" />
293
- <Flex direction="column" gap="2" className="max-h-60 overflow-y-auto">
294
- {plugin.middlewares.map((mw, index) => (
295
- <Box key={index} className="rounded-lg bg-gray-50 dark:bg-gray-900 p-2">
296
- <Flex align="center" gap="2">
297
- <Code size="2">{mw.id}</Code>
298
- <Badge size="1" variant="soft">{mw.type}</Badge>
299
- </Flex>
300
- </Box>
301
- ))}
302
- </Flex>
303
- </Flex>
304
- </Card>
305
- )}
306
-
307
- {/* 上下文列表 */}
180
+ {/* Contexts section */}
308
181
  {plugin.contexts.length > 0 && (
309
- <Card size="2">
310
- <Flex direction="column" gap="2" p="3">
311
- <Flex align="center" gap="2">
312
- <Database className="w-4 h-4 text-orange-600 dark:text-orange-400" />
313
- <Heading size="3">上下文</Heading>
314
- <Badge size="1">{plugin.contexts.length}</Badge>
315
- </Flex>
316
- <Separator size="4" />
317
- <Flex direction="column" gap="2" className="max-h-60 overflow-y-auto">
182
+ <Card>
183
+ <CardHeader className="pb-2">
184
+ <div className="flex items-center gap-2">
185
+ <Database className="w-4 h-4 text-muted-foreground" />
186
+ <CardTitle className="text-base">上下文</CardTitle>
187
+ <Badge variant="secondary">{plugin.contexts.length}</Badge>
188
+ </div>
189
+ </CardHeader>
190
+ <CardContent>
191
+ <div className="max-h-60 overflow-y-auto pr-1 space-y-2">
318
192
  {plugin.contexts.map((ctx, index) => (
319
- <Box key={index} className="rounded-lg bg-gray-50 dark:bg-gray-900 p-2">
320
- <Flex direction="column" gap="1">
321
- <Code size="2">{ctx.name}</Code>
322
- <Text size="1" color="gray">{ctx.description}</Text>
323
- </Flex>
324
- </Box>
193
+ <div key={index} className="rounded-md bg-muted/50 p-2">
194
+ <code className="text-sm">{ctx.name}</code>
195
+ </div>
325
196
  ))}
326
- </Flex>
327
- </Flex>
197
+ </div>
198
+ </CardContent>
328
199
  </Card>
329
200
  )}
330
-
331
- {/* 定时任务列表 */}
332
- {plugin.crons.length > 0 && (
333
- <Card size="2">
334
- <Flex direction="column" gap="2" p="3">
335
- <Flex align="center" gap="2">
336
- <Clock className="w-4 h-4 text-amber-600 dark:text-amber-400" />
337
- <Heading size="3">定时任务</Heading>
338
- <Badge size="1">{plugin.crons.length}</Badge>
339
- </Flex>
340
- <Separator size="4" />
341
- <Flex direction="column" gap="2" className="max-h-60 overflow-y-auto">
342
- {plugin.crons.map((cron, index) => (
343
- <Box key={index} className="rounded-lg bg-gray-50 dark:bg-gray-900 p-2">
344
- <Flex justify="between" align="center">
345
- <Flex direction="column" gap="1">
346
- <Code size="2">{cron.id}</Code>
347
- <Text size="1" color="gray">{cron.pattern}</Text>
348
- </Flex>
349
- <Badge color={cron.running ? 'green' : 'gray'} size="1">
350
- {cron.running ? '运行中' : '已停止'}
351
- </Badge>
352
- </Flex>
353
- </Box>
354
- ))}
355
- </Flex>
356
- </Flex>
357
- </Card>
358
- )}
359
-
360
- {/* 数据模型列表 */}
361
- {plugin.definitions.length > 0 && (
362
- <Card size="2">
363
- <Flex direction="column" gap="2" p="3">
364
- <Flex align="center" gap="2">
365
- <FileText className="w-4 h-4 text-cyan-600 dark:text-cyan-400" />
366
- <Heading size="3">数据模型</Heading>
367
- <Badge size="1">{plugin.definitions.length}</Badge>
368
- </Flex>
369
- <Separator size="4" />
370
- <Flex direction="column" gap="2" className="max-h-60 overflow-y-auto">
371
- {plugin.definitions.map((definition, index) => (
372
- <Box key={index} className="rounded-lg bg-gray-50 dark:bg-gray-900 p-2">
373
- <Flex direction="column" gap="1">
374
- <Code size="2">{definition.name}</Code>
375
- <Text size="1" color="gray">
376
- 字段: {definition.fields.join(', ')}
377
- </Text>
378
- </Flex>
379
- </Box>
380
- ))}
381
- </Flex>
382
- </Flex>
383
- </Card>
384
- )}
385
- </Grid>
386
- </Box>
201
+ </div>
202
+ </div>
387
203
  )
388
204
  }
205
+
206
+ /** 根据 Feature 类型渲染不同样式的 item 卡片 */
207
+ function FeatureItemCard({ featureName, item }: { featureName: string; item: any }) {
208
+ switch (featureName) {
209
+ case 'command':
210
+ return (
211
+ <div className="rounded-md bg-muted/50 p-3 space-y-1">
212
+ <code className="text-sm font-semibold">{item.name}</code>
213
+ {item.desc?.map((desc: string, i: number) => (
214
+ <p key={i} className="text-xs text-muted-foreground">{desc}</p>
215
+ ))}
216
+ {item.usage && item.usage.length > 0 && (
217
+ <div>
218
+ <span className="text-xs font-semibold text-blue-600 dark:text-blue-400">用法:</span>
219
+ {item.usage.map((u: string, i: number) => (
220
+ <code key={i} className="block text-xs bg-muted rounded px-1 py-0.5 mt-0.5">{u}</code>
221
+ ))}
222
+ </div>
223
+ )}
224
+ {item.examples && item.examples.length > 0 && (
225
+ <div>
226
+ <span className="text-xs font-semibold text-emerald-600 dark:text-emerald-400">示例:</span>
227
+ {item.examples.map((e: string, i: number) => (
228
+ <code key={i} className="block text-xs bg-muted rounded px-1 py-0.5 mt-0.5">{e}</code>
229
+ ))}
230
+ </div>
231
+ )}
232
+ </div>
233
+ )
234
+ case 'cron':
235
+ return (
236
+ <div className="flex justify-between items-center rounded-md bg-muted/50 p-2">
237
+ <code className="text-sm">{item.expression}</code>
238
+ <Badge variant={item.running ? 'success' : 'secondary'}>
239
+ {item.running ? '运行中' : '已停止'}
240
+ </Badge>
241
+ </div>
242
+ )
243
+ case 'tool':
244
+ return (
245
+ <div className="rounded-md bg-muted/50 p-3 space-y-1">
246
+ <code className="text-sm font-semibold">{item.name}</code>
247
+ {item.desc && <p className="text-xs text-muted-foreground">{item.desc}</p>}
248
+ {item.platforms && item.platforms.length > 0 && (
249
+ <div className="flex gap-1 flex-wrap">
250
+ {item.platforms.map((p: string, i: number) => (
251
+ <Badge key={i} variant="outline" className="text-[10px]">{p}</Badge>
252
+ ))}
253
+ </div>
254
+ )}
255
+ </div>
256
+ )
257
+ case 'skill':
258
+ return (
259
+ <div className="rounded-md bg-muted/50 p-3 space-y-1">
260
+ <code className="text-sm font-semibold">{item.name}</code>
261
+ {item.desc && <p className="text-xs text-muted-foreground">{item.desc}</p>}
262
+ {item.toolCount != null && (
263
+ <span className="text-xs text-muted-foreground">工具: {item.toolCount}</span>
264
+ )}
265
+ </div>
266
+ )
267
+ case 'config':
268
+ return (
269
+ <div className="flex justify-between items-center rounded-md bg-muted/50 p-2">
270
+ <code className="text-sm">{item.name}</code>
271
+ {item.defaultValue !== undefined && (
272
+ <span className="text-xs text-muted-foreground">默认: {JSON.stringify(item.defaultValue)}</span>
273
+ )}
274
+ </div>
275
+ )
276
+ case 'permission':
277
+ return (
278
+ <div className="rounded-md bg-muted/50 p-2">
279
+ <code className="text-sm">{item.name}</code>
280
+ </div>
281
+ )
282
+ case 'database':
283
+ return (
284
+ <div className="rounded-md bg-muted/50 p-2">
285
+ <code className="text-sm">{item.name}</code>
286
+ </div>
287
+ )
288
+ case 'adapter':
289
+ return (
290
+ <div className="rounded-md bg-muted/50 p-3 space-y-1">
291
+ <div className="flex items-center gap-2">
292
+ <Plug className="w-3.5 h-3.5 text-muted-foreground" />
293
+ <code className="text-sm font-semibold">{item.name}</code>
294
+ </div>
295
+ <div className="flex gap-3 text-xs text-muted-foreground">
296
+ <span>Bot: {item.bots ?? 0}</span>
297
+ <span>在线: {item.online ?? 0}</span>
298
+ {item.tools > 0 && <span>工具: {item.tools}</span>}
299
+ </div>
300
+ </div>
301
+ )
302
+ case 'service':
303
+ return (
304
+ <div className="rounded-md bg-muted/50 p-2 flex items-center gap-2">
305
+ <Server className="w-3.5 h-3.5 text-muted-foreground" />
306
+ <code className="text-sm">{item.name}</code>
307
+ {item.desc && item.desc !== item.name && (
308
+ <span className="text-xs text-muted-foreground">- {item.desc}</span>
309
+ )}
310
+ </div>
311
+ )
312
+ default:
313
+ // 通用渲染:显示 item.name 或 JSON
314
+ return (
315
+ <div className="rounded-md bg-muted/50 p-2">
316
+ <code className="text-sm">{item.name || JSON.stringify(item)}</code>
317
+ </div>
318
+ )
319
+ }
320
+ }