work-agent 0.1.0

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 (245) hide show
  1. package/README.md +234 -0
  2. package/app/(admin)/approvals/page.tsx +16 -0
  3. package/app/(admin)/audit/page.tsx +18 -0
  4. package/app/(admin)/layout.tsx +47 -0
  5. package/app/(admin)/scheduled-tasks/page.tsx +17 -0
  6. package/app/(admin)/settings/page.tsx +46 -0
  7. package/app/(admin)/skills/[name]/page.tsx +378 -0
  8. package/app/(admin)/skills/page.tsx +406 -0
  9. package/app/(admin)/statistics/page.tsx +416 -0
  10. package/app/(admin)/tickets/[id]/page.tsx +348 -0
  11. package/app/(admin)/tickets/new/page.tsx +309 -0
  12. package/app/(admin)/tickets/page.tsx +27 -0
  13. package/app/api/audit/route.ts +30 -0
  14. package/app/api/auth/feishu/callback/route.ts +72 -0
  15. package/app/api/auth/feishu/login/route.ts +17 -0
  16. package/app/api/auth/feishu/sso/route.ts +78 -0
  17. package/app/api/auth/login/route.ts +85 -0
  18. package/app/api/auth/oauth/route.ts +168 -0
  19. package/app/api/config/providers/route.ts +105 -0
  20. package/app/api/config/route.ts +115 -0
  21. package/app/api/config/status/route.ts +56 -0
  22. package/app/api/config/test/route.ts +212 -0
  23. package/app/api/documents/[id]/route.ts +88 -0
  24. package/app/api/documents/route.ts +53 -0
  25. package/app/api/health/route.ts +32 -0
  26. package/app/api/knowledge/[id]/route.ts +152 -0
  27. package/app/api/knowledge/from-session/route.ts +27 -0
  28. package/app/api/knowledge/route.ts +100 -0
  29. package/app/api/market/knowledge/[id]/route.ts +92 -0
  30. package/app/api/market/knowledge/route.ts +130 -0
  31. package/app/api/marketplace/skills/[id]/approve/route.ts +68 -0
  32. package/app/api/marketplace/skills/[id]/certify/route.ts +54 -0
  33. package/app/api/marketplace/skills/[id]/install/route.ts +180 -0
  34. package/app/api/marketplace/skills/[id]/promote-to-system/route.ts +219 -0
  35. package/app/api/marketplace/skills/[id]/rate/route.ts +90 -0
  36. package/app/api/marketplace/skills/[id]/ratings/route.ts +55 -0
  37. package/app/api/marketplace/skills/[id]/reject/route.ts +68 -0
  38. package/app/api/marketplace/skills/[id]/route.ts +177 -0
  39. package/app/api/marketplace/skills/route.ts +235 -0
  40. package/app/api/memory/route.ts +40 -0
  41. package/app/api/my/files/[id]/route.ts +52 -0
  42. package/app/api/my/files/route.ts +230 -0
  43. package/app/api/my/knowledge/route.ts +36 -0
  44. package/app/api/pi-chat/route.ts +443 -0
  45. package/app/api/recommend/route.ts +38 -0
  46. package/app/api/scheduled-tasks/[id]/execute/route.ts +132 -0
  47. package/app/api/scheduled-tasks/[id]/route.ts +165 -0
  48. package/app/api/scheduled-tasks/[id]/toggle/route.ts +53 -0
  49. package/app/api/scheduled-tasks/route.ts +101 -0
  50. package/app/api/sessions/[id]/messages/route.ts +212 -0
  51. package/app/api/sessions/route.ts +101 -0
  52. package/app/api/share/file/[id]/route.ts +37 -0
  53. package/app/api/skills/[name]/execute/route.ts +121 -0
  54. package/app/api/skills/[name]/route.ts +167 -0
  55. package/app/api/skills/create/route.ts +65 -0
  56. package/app/api/skills/generate/route.ts +405 -0
  57. package/app/api/skills/installed/route.ts +151 -0
  58. package/app/api/skills/route.ts +174 -0
  59. package/app/api/skills/translate/route.ts +40 -0
  60. package/app/api/skills/user/[name]/route.ts +159 -0
  61. package/app/api/skills/user/route.ts +90 -0
  62. package/app/api/statistics/route.ts +94 -0
  63. package/app/api/task-executions/[id]/route.ts +34 -0
  64. package/app/api/task-executions/route.ts +29 -0
  65. package/app/api/tickets/[id]/approve/route.ts +129 -0
  66. package/app/api/tickets/[id]/execute/route.ts +201 -0
  67. package/app/api/tickets/[id]/route.ts +127 -0
  68. package/app/api/tickets/route.ts +103 -0
  69. package/app/api/user/skills/route.ts +175 -0
  70. package/app/api/users/route.ts +80 -0
  71. package/app/chat/page.tsx +5 -0
  72. package/app/globals.css +84 -0
  73. package/app/h5/layout.tsx +5 -0
  74. package/app/h5/mobile-approvals-page.tsx +167 -0
  75. package/app/h5/mobile-chat-page.tsx +951 -0
  76. package/app/h5/mobile-profile-page.tsx +147 -0
  77. package/app/h5/mobile-tickets-page.tsx +121 -0
  78. package/app/h5/page.tsx +23 -0
  79. package/app/h5/ticket-action-buttons.tsx +80 -0
  80. package/app/layout.tsx +26 -0
  81. package/app/login/page.tsx +318 -0
  82. package/app/market/knowledge/[id]/page.tsx +77 -0
  83. package/app/market/knowledge/page.tsx +358 -0
  84. package/app/market/layout.tsx +29 -0
  85. package/app/market/page.tsx +18 -0
  86. package/app/market/skills/page.tsx +397 -0
  87. package/app/my/files/page.tsx +511 -0
  88. package/app/my/knowledge/[id]/page.tsx +271 -0
  89. package/app/my/knowledge/new/page.tsx +234 -0
  90. package/app/my/knowledge/page.tsx +248 -0
  91. package/app/my/layout.tsx +32 -0
  92. package/app/my/memory/page.tsx +164 -0
  93. package/app/my/page.tsx +18 -0
  94. package/app/my/scheduled-tasks/[id]/edit/page.tsx +290 -0
  95. package/app/my/scheduled-tasks/[id]/executions/page.tsx +275 -0
  96. package/app/my/scheduled-tasks/[id]/page.tsx +284 -0
  97. package/app/my/scheduled-tasks/new/page.tsx +230 -0
  98. package/app/my/scheduled-tasks/page.tsx +27 -0
  99. package/app/my/skills/[name]/page.tsx +320 -0
  100. package/app/my/skills/new/page.tsx +394 -0
  101. package/app/my/skills/page.tsx +303 -0
  102. package/app/page.tsx +2288 -0
  103. package/app/share/[sessionId]/page.tsx +226 -0
  104. package/app/share/file/[id]/page.tsx +140 -0
  105. package/bin/README.md +63 -0
  106. package/bin/generate-api-system +300 -0
  107. package/bin/postinstall.js +95 -0
  108. package/bin/work-agent.js +173 -0
  109. package/components/ai-elements/agent.tsx +142 -0
  110. package/components/ai-elements/artifact.tsx +149 -0
  111. package/components/ai-elements/attachments.tsx +427 -0
  112. package/components/ai-elements/audio-player.tsx +232 -0
  113. package/components/ai-elements/canvas.tsx +26 -0
  114. package/components/ai-elements/chain-of-thought.tsx +223 -0
  115. package/components/ai-elements/checkpoint.tsx +72 -0
  116. package/components/ai-elements/code-block.tsx +555 -0
  117. package/components/ai-elements/commit.tsx +449 -0
  118. package/components/ai-elements/confirmation.tsx +173 -0
  119. package/components/ai-elements/connection.tsx +28 -0
  120. package/components/ai-elements/context.tsx +410 -0
  121. package/components/ai-elements/controls.tsx +19 -0
  122. package/components/ai-elements/conversation.tsx +167 -0
  123. package/components/ai-elements/edge.tsx +144 -0
  124. package/components/ai-elements/environment-variables.tsx +325 -0
  125. package/components/ai-elements/file-tree.tsx +298 -0
  126. package/components/ai-elements/image.tsx +25 -0
  127. package/components/ai-elements/inline-citation.tsx +294 -0
  128. package/components/ai-elements/jsx-preview.tsx +250 -0
  129. package/components/ai-elements/message.tsx +367 -0
  130. package/components/ai-elements/mic-selector.tsx +372 -0
  131. package/components/ai-elements/model-selector.tsx +214 -0
  132. package/components/ai-elements/node.tsx +72 -0
  133. package/components/ai-elements/open-in-chat.tsx +367 -0
  134. package/components/ai-elements/package-info.tsx +235 -0
  135. package/components/ai-elements/panel.tsx +16 -0
  136. package/components/ai-elements/persona.tsx +280 -0
  137. package/components/ai-elements/plan.tsx +144 -0
  138. package/components/ai-elements/prompt-input.tsx +1341 -0
  139. package/components/ai-elements/queue.tsx +275 -0
  140. package/components/ai-elements/reasoning.tsx +355 -0
  141. package/components/ai-elements/sandbox.tsx +133 -0
  142. package/components/ai-elements/schema-display.tsx +473 -0
  143. package/components/ai-elements/shimmer.tsx +78 -0
  144. package/components/ai-elements/snippet.tsx +141 -0
  145. package/components/ai-elements/sources.tsx +78 -0
  146. package/components/ai-elements/speech-input.tsx +324 -0
  147. package/components/ai-elements/stack-trace.tsx +531 -0
  148. package/components/ai-elements/suggestion.tsx +58 -0
  149. package/components/ai-elements/task.tsx +88 -0
  150. package/components/ai-elements/terminal.tsx +277 -0
  151. package/components/ai-elements/test-results.tsx +497 -0
  152. package/components/ai-elements/tool.tsx +174 -0
  153. package/components/ai-elements/toolbar.tsx +17 -0
  154. package/components/ai-elements/transcription.tsx +126 -0
  155. package/components/ai-elements/voice-selector.tsx +525 -0
  156. package/components/ai-elements/web-preview.tsx +282 -0
  157. package/components/audit-log-list.tsx +114 -0
  158. package/components/chat/EmptyPreviewState.tsx +12 -0
  159. package/components/chat/KnowledgePickerDialog.tsx +464 -0
  160. package/components/chat/KnowledgePreview.tsx +70 -0
  161. package/components/chat/KnowledgePreviewPanel.tsx +86 -0
  162. package/components/chat/MentionInput.tsx +309 -0
  163. package/components/chat/OrganizeDialog.tsx +258 -0
  164. package/components/chat/RecommendationBanner.tsx +94 -0
  165. package/components/chat/SaveToKnowledgeDialog.tsx +193 -0
  166. package/components/chat/SkillSelector.tsx +305 -0
  167. package/components/chat/SkillSwitcher.tsx +163 -0
  168. package/components/client-layout.tsx +15 -0
  169. package/components/knowledge/KnowledgeMetadataPanel.tsx +293 -0
  170. package/components/layout-wrapper.tsx +18 -0
  171. package/components/mobile-layout.tsx +62 -0
  172. package/components/scheduled-task-list.tsx +356 -0
  173. package/components/setup-guide.tsx +484 -0
  174. package/components/sub-nav.tsx +54 -0
  175. package/components/ticket-detail-content.tsx +383 -0
  176. package/components/ticket-list.tsx +366 -0
  177. package/components/top-nav.tsx +132 -0
  178. package/components/ui/accordion.tsx +58 -0
  179. package/components/ui/alert.tsx +59 -0
  180. package/components/ui/avatar.tsx +50 -0
  181. package/components/ui/badge.tsx +36 -0
  182. package/components/ui/button-group.tsx +83 -0
  183. package/components/ui/button.tsx +57 -0
  184. package/components/ui/card.tsx +91 -0
  185. package/components/ui/carousel.tsx +262 -0
  186. package/components/ui/collapsible.tsx +11 -0
  187. package/components/ui/command.tsx +153 -0
  188. package/components/ui/dialog.tsx +122 -0
  189. package/components/ui/dropdown-menu.tsx +200 -0
  190. package/components/ui/hover-card.tsx +29 -0
  191. package/components/ui/input-group.tsx +170 -0
  192. package/components/ui/input.tsx +22 -0
  193. package/components/ui/label.tsx +26 -0
  194. package/components/ui/popover.tsx +31 -0
  195. package/components/ui/progress.tsx +28 -0
  196. package/components/ui/scroll-area.tsx +48 -0
  197. package/components/ui/select.tsx +174 -0
  198. package/components/ui/separator.tsx +31 -0
  199. package/components/ui/spinner.tsx +16 -0
  200. package/components/ui/switch.tsx +29 -0
  201. package/components/ui/table.tsx +120 -0
  202. package/components/ui/tabs.tsx +55 -0
  203. package/components/ui/textarea.tsx +22 -0
  204. package/components/ui/tooltip.tsx +30 -0
  205. package/components/welcome-guide.tsx +182 -0
  206. package/components.json +24 -0
  207. package/lib/command-parser.ts +331 -0
  208. package/lib/dangerous-commands.ts +672 -0
  209. package/lib/db.ts +2250 -0
  210. package/lib/feishu-auth.ts +135 -0
  211. package/lib/file-storage.ts +306 -0
  212. package/lib/file-tool.ts +583 -0
  213. package/lib/knowledge-tool.ts +152 -0
  214. package/lib/knowledge-types.ts +66 -0
  215. package/lib/market-client.ts +313 -0
  216. package/lib/market-db.ts +736 -0
  217. package/lib/market-types.ts +51 -0
  218. package/lib/memory-tool.ts +211 -0
  219. package/lib/memory.ts +197 -0
  220. package/lib/pi-config.ts +436 -0
  221. package/lib/pi-session.ts +799 -0
  222. package/lib/pinyin.ts +13 -0
  223. package/lib/recommendation.ts +227 -0
  224. package/lib/risk-estimator.ts +350 -0
  225. package/lib/scheduled-task-tool.ts +184 -0
  226. package/lib/scheduler-init.ts +43 -0
  227. package/lib/scheduler.ts +416 -0
  228. package/lib/secure-bash-tool.ts +413 -0
  229. package/lib/skill-engine.ts +396 -0
  230. package/lib/skill-generator.ts +269 -0
  231. package/lib/skill-loader.ts +234 -0
  232. package/lib/skill-tool.ts +188 -0
  233. package/lib/skill-types.ts +82 -0
  234. package/lib/skills-init.ts +58 -0
  235. package/lib/ticket-tool.ts +246 -0
  236. package/lib/user-skill-types.ts +30 -0
  237. package/lib/user-skills.ts +362 -0
  238. package/lib/utils.ts +6 -0
  239. package/lib/workflow.ts +154 -0
  240. package/lib/zip-tool.ts +191 -0
  241. package/next.config.js +8 -0
  242. package/package.json +106 -0
  243. package/public/.gitkeep +1 -0
  244. package/public/icon.svg +1 -0
  245. package/tsconfig.json +42 -0
@@ -0,0 +1,366 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import Link from 'next/link';
5
+ import { Terminal, Wrench, CheckCircle, XCircle, Clock, Zap, Trash2 } from 'lucide-react';
6
+ import type { Ticket } from '@/lib/db';
7
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
8
+ import { Button } from '@/components/ui/button';
9
+ import { Input } from '@/components/ui/input';
10
+ import { Badge } from '@/components/ui/badge';
11
+ import {
12
+ Select,
13
+ SelectContent,
14
+ SelectItem,
15
+ SelectTrigger,
16
+ SelectValue,
17
+ } from '@/components/ui/select';
18
+
19
+ export function TicketList({ status }: { status?: string }) {
20
+ const [tickets, setTickets] = useState<Ticket[]>([]);
21
+ const [filteredTickets, setFilteredTickets] = useState<Ticket[]>([]);
22
+ const [loading, setLoading] = useState(true);
23
+ const [error, setError] = useState<string | null>(null);
24
+
25
+ // 搜索和筛选状态
26
+ const [searchQuery, setSearchQuery] = useState('');
27
+ const [statusFilter, setStatusFilter] = useState(status || 'all');
28
+ const [typeFilter, setTypeFilter] = useState('all');
29
+
30
+ useEffect(() => {
31
+ async function fetchTickets() {
32
+ try {
33
+ const response = await fetch('/api/tickets');
34
+ if (!response.ok) throw new Error('获取工单失败');
35
+
36
+ const data = await response.json();
37
+ setTickets(data.tickets);
38
+ setFilteredTickets(data.tickets);
39
+ } catch (err) {
40
+ setError(err instanceof Error ? err.message : '未知错误');
41
+ } finally {
42
+ setLoading(false);
43
+ }
44
+ }
45
+
46
+ fetchTickets();
47
+ }, []);
48
+
49
+ // 筛选和搜索逻辑
50
+ useEffect(() => {
51
+ let filtered = tickets;
52
+
53
+ // 状态筛选
54
+ if (statusFilter !== 'all') {
55
+ filtered = filtered.filter(t => t.status === statusFilter);
56
+ }
57
+
58
+ // 类型筛选
59
+ if (typeFilter !== 'all') {
60
+ filtered = filtered.filter(t => t.type === typeFilter);
61
+ }
62
+
63
+ // 搜索
64
+ if (searchQuery) {
65
+ const query = searchQuery.toLowerCase();
66
+ filtered = filtered.filter(t =>
67
+ t.title.toLowerCase().includes(query) ||
68
+ t.description?.toLowerCase().includes(query) ||
69
+ t.id.toLowerCase().includes(query) ||
70
+ t.command?.toLowerCase().includes(query) ||
71
+ t.skillName?.toLowerCase().includes(query) ||
72
+ t.scriptContent?.toLowerCase().includes(query)
73
+ );
74
+ }
75
+
76
+ setFilteredTickets(filtered);
77
+ }, [tickets, statusFilter, typeFilter, searchQuery]);
78
+
79
+ if (loading) {
80
+ return (
81
+ <div className="flex items-center justify-center py-16">
82
+ <div className="flex flex-col items-center gap-4">
83
+ <div className="w-10 h-10 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
84
+ <p className="text-muted-foreground">加载中...</p>
85
+ </div>
86
+ </div>
87
+ );
88
+ }
89
+
90
+ if (error) {
91
+ return (
92
+ <div className="bg-destructive/10 border border-destructive/20 rounded-xl p-6">
93
+ <p className="text-destructive">❌ {error}</p>
94
+ </div>
95
+ );
96
+ }
97
+
98
+ return (
99
+ <div className="space-y-4">
100
+ {/* 搜索和筛选栏 */}
101
+ <div className="bg-card rounded-xl border shadow-sm p-4">
102
+ <div className="flex flex-col md:flex-row gap-4">
103
+ <div className="flex-1">
104
+ <Input
105
+ type="text"
106
+ value={searchQuery}
107
+ onChange={(e) => setSearchQuery(e.target.value)}
108
+ placeholder="搜索工单标题、描述、ID、集群或资源名称..."
109
+ className="w-full"
110
+ />
111
+ </div>
112
+ <div className="flex gap-2">
113
+ <Select value={statusFilter} onValueChange={setStatusFilter}>
114
+ <SelectTrigger className="w-[140px]">
115
+ <SelectValue placeholder="所有状态" />
116
+ </SelectTrigger>
117
+ <SelectContent>
118
+ <SelectItem value="all">所有状态</SelectItem>
119
+ <SelectItem value="draft">草稿</SelectItem>
120
+ <SelectItem value="pending">待审核</SelectItem>
121
+ <SelectItem value="approved">已批准</SelectItem>
122
+ <SelectItem value="executing">执行中</SelectItem>
123
+ <SelectItem value="completed">已完成</SelectItem>
124
+ <SelectItem value="failed">失败</SelectItem>
125
+ <SelectItem value="rejected">已拒绝</SelectItem>
126
+ </SelectContent>
127
+ </Select>
128
+ <Select value={typeFilter} onValueChange={setTypeFilter}>
129
+ <SelectTrigger className="w-[140px]">
130
+ <SelectValue placeholder="所有类型" />
131
+ </SelectTrigger>
132
+ <SelectContent>
133
+ <SelectItem value="all">所有类型</SelectItem>
134
+ <SelectItem value="bash-execute">💻 Bash 命令</SelectItem>
135
+ <SelectItem value="skill-script">🛠️ Skill 脚本</SelectItem>
136
+ </SelectContent>
137
+ </Select>
138
+ </div>
139
+ </div>
140
+ {filteredTickets.length !== tickets.length && (
141
+ <p className="text-sm text-muted-foreground mt-2">
142
+ 找到 {filteredTickets.length} 个工单(共 {tickets.length} 个)
143
+ </p>
144
+ )}
145
+ </div>
146
+
147
+ {/* 工单表格 */}
148
+ {filteredTickets.length === 0 ? (
149
+ <div className="text-center py-16 bg-card rounded-xl border shadow-sm">
150
+ <div className="text-6xl mb-4">📭</div>
151
+ <p className="text-muted-foreground text-lg">暂无工单</p>
152
+ <p className="text-muted-foreground/70 text-sm mt-2">
153
+ {searchQuery || statusFilter !== 'all' || typeFilter !== 'all'
154
+ ? '没有符合条件的工单,请调整筛选条件'
155
+ : '点击上方"新建工单"创建第一个工单'}
156
+ </p>
157
+ </div>
158
+ ) : (
159
+ <div className="bg-card rounded-xl border shadow-sm overflow-hidden">
160
+ <Table>
161
+ <TableHeader>
162
+ <TableRow>
163
+ <TableHead>工单</TableHead>
164
+ <TableHead>状态</TableHead>
165
+ <TableHead>类型</TableHead>
166
+ <TableHead>优先级</TableHead>
167
+ <TableHead>风险等级</TableHead>
168
+ <TableHead>创建时间</TableHead>
169
+ <TableHead className="text-right">操作</TableHead>
170
+ </TableRow>
171
+ </TableHeader>
172
+ <TableBody>
173
+ {filteredTickets.map((ticket) => (
174
+ <TableRow key={ticket.id}>
175
+ <TableCell>
176
+ <div className="flex items-start gap-3">
177
+ <TypeIcon type={ticket.type} />
178
+ <div className="flex-1 min-w-0">
179
+ <Link
180
+ href={`/tickets/${ticket.id}`}
181
+ className="font-medium hover:text-primary transition-colors block truncate"
182
+ >
183
+ {ticket.title}
184
+ </Link>
185
+ <p className="text-xs text-muted-foreground truncate">{ticket.id}</p>
186
+ </div>
187
+ </div>
188
+ </TableCell>
189
+ <TableCell>
190
+ <StatusBadge status={ticket.status} />
191
+ </TableCell>
192
+ <TableCell>
193
+ <TypeBadge type={ticket.type} />
194
+ </TableCell>
195
+ <TableCell>
196
+ <PriorityBadge priority={ticket.priority} />
197
+ </TableCell>
198
+ <TableCell>
199
+ <RiskBadge risk={ticket.riskLevel} />
200
+ </TableCell>
201
+ <TableCell>
202
+ <span className="text-sm text-muted-foreground">
203
+ {new Date(ticket.createdAt).toLocaleDateString('zh-CN')}
204
+ </span>
205
+ </TableCell>
206
+ <TableCell className="text-right">
207
+ <div className="flex items-center justify-end gap-1">
208
+ {ticket.status === 'approved' && (
209
+ <Button
210
+ variant="ghost"
211
+ size="icon"
212
+ onClick={async () => {
213
+ if (!confirm(`确定执行工单 "${ticket.title}" 吗?`)) return;
214
+ try {
215
+ const response = await fetch(`/api/tickets/${ticket.id}/execute`, {
216
+ method: 'POST',
217
+ headers: { 'Content-Type': 'application/json' },
218
+ body: JSON.stringify({ ticketId: ticket.id }),
219
+ });
220
+ if (response.ok) {
221
+ const data = await response.json();
222
+ setTickets(tickets.map(t => t.id === ticket.id ? data.ticket : t));
223
+ }
224
+ } catch (err) {
225
+ console.error(err);
226
+ }
227
+ }}
228
+ className="h-8 w-8 text-green-600 hover:text-green-700 hover:bg-green-50"
229
+ title="执行工单"
230
+ >
231
+ <Zap className="h-4 w-4" />
232
+ </Button>
233
+ )}
234
+ {ticket.status === 'pending' && (
235
+ <>
236
+ <Button
237
+ variant="ghost"
238
+ size="icon"
239
+ onClick={async () => {
240
+ if (!confirm(`批准工单 "${ticket.title}"?`)) return;
241
+ try {
242
+ const response = await fetch(`/api/tickets/${ticket.id}/approve`, {
243
+ method: 'POST',
244
+ headers: { 'Content-Type': 'application/json' },
245
+ body: JSON.stringify({ action: 'approve', approver: 'user', comment: '' }),
246
+ });
247
+ if (response.ok) {
248
+ const data = await response.json();
249
+ setTickets(tickets.map(t => t.id === ticket.id ? data.ticket : t));
250
+ }
251
+ } catch (err) {
252
+ console.error(err);
253
+ }
254
+ }}
255
+ className="h-8 w-8 text-green-600 hover:text-green-700 hover:bg-green-50"
256
+ title="批准"
257
+ >
258
+
259
+ </Button>
260
+ <Button
261
+ variant="ghost"
262
+ size="icon"
263
+ onClick={async () => {
264
+ if (!confirm(`拒绝工单 "${ticket.title}"?`)) return;
265
+ try {
266
+ const response = await fetch(`/api/tickets/${ticket.id}/approve`, {
267
+ method: 'POST',
268
+ headers: { 'Content-Type': 'application/json' },
269
+ body: JSON.stringify({ action: 'reject', approver: 'user', comment: '' }),
270
+ });
271
+ if (response.ok) {
272
+ const data = await response.json();
273
+ setTickets(tickets.map(t => t.id === ticket.id ? data.ticket : t));
274
+ }
275
+ } catch (err) {
276
+ console.error(err);
277
+ }
278
+ }}
279
+ className="h-8 w-8 text-destructive hover:text-destructive hover:bg-destructive/10"
280
+ title="拒绝"
281
+ >
282
+
283
+ </Button>
284
+ </>
285
+ )}
286
+ <Button
287
+ variant="ghost"
288
+ size="icon"
289
+ asChild
290
+ className="h-8 w-8"
291
+ >
292
+ <Link href={`/tickets/${ticket.id}`} title="查看详情">
293
+
294
+ </Link>
295
+ </Button>
296
+ </div>
297
+ </TableCell>
298
+ </TableRow>
299
+ ))}
300
+ </TableBody>
301
+ </Table>
302
+ </div>
303
+ )}
304
+ </div>
305
+ );
306
+ }
307
+
308
+ // 工单类型图标组件
309
+ function TypeIcon({ type }: { type: Ticket['type'] }) {
310
+ const iconMap: Record<string, typeof Terminal> = {
311
+ 'bash-execute': Terminal,
312
+ 'skill-script': Wrench,
313
+ };
314
+ const Icon = iconMap[type] || Terminal;
315
+ return <Icon className="h-5 w-5 text-muted-foreground flex-shrink-0" />;
316
+ }
317
+
318
+ // 类型徽章组件
319
+ function TypeBadge({ type }: { type: Ticket['type'] }) {
320
+ const config: Record<string, { label: string; variant: 'secondary' | 'outline' | 'default' | 'destructive' }> = {
321
+ 'bash-execute': { label: 'Bash', variant: 'secondary' },
322
+ 'skill-script': { label: 'Skill', variant: 'outline' },
323
+ };
324
+ const { label, variant } = config[type] || { label: type, variant: 'secondary' };
325
+ return <Badge variant={variant}>{label}</Badge>;
326
+ }
327
+
328
+ // 状态徽章组件
329
+ function StatusBadge({ status }: { status: Ticket['status'] }) {
330
+ const config = {
331
+ draft: { label: '草稿', variant: 'secondary' as const },
332
+ pending: { label: '待审核', variant: 'outline' as const },
333
+ approved: { label: '已批准', variant: 'default' as const },
334
+ rejected: { label: '已拒绝', variant: 'destructive' as const },
335
+ executing: { label: '执行中', variant: 'outline' as const },
336
+ completed: { label: '已完成', variant: 'default' as const },
337
+ failed: { label: '失败', variant: 'destructive' as const },
338
+ deleted: { label: '已删除', variant: 'secondary' as const },
339
+ };
340
+ const { label, variant } = config[status] || config.draft;
341
+ return <Badge variant={variant}>{label}</Badge>;
342
+ }
343
+
344
+ // 优先级徽章组件
345
+ function PriorityBadge({ priority }: { priority: Ticket['priority'] }) {
346
+ const config: Record<string, { label: string; variant: 'secondary' | 'outline' | 'default' | 'destructive' }> = {
347
+ low: { label: '低', variant: 'secondary' },
348
+ medium: { label: '中', variant: 'outline' },
349
+ high: { label: '高', variant: 'destructive' },
350
+ critical: { label: '极高', variant: 'destructive' },
351
+ };
352
+ const { label, variant } = config[priority] || config.medium;
353
+ return <Badge variant={variant}>{label}</Badge>;
354
+ }
355
+
356
+ // 风险等级徽章组件
357
+ function RiskBadge({ risk }: { risk: Ticket['riskLevel'] }) {
358
+ const config: Record<string, { label: string; variant: 'secondary' | 'outline' | 'default' | 'destructive' }> = {
359
+ low: { label: '低风险', variant: 'secondary' },
360
+ medium: { label: '中风险', variant: 'outline' },
361
+ high: { label: '高风险', variant: 'destructive' },
362
+ critical: { label: '极高风险', variant: 'destructive' },
363
+ };
364
+ const { label, variant } = config[risk] || config.medium;
365
+ return <Badge variant={variant}>{label}</Badge>;
366
+ }
@@ -0,0 +1,132 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { usePathname } from 'next/navigation';
5
+ import { useState, useEffect } from 'react';
6
+ import {
7
+ Bot,
8
+ ClipboardList,
9
+ CheckCircle,
10
+ FileText,
11
+ Settings,
12
+ BookOpen,
13
+ Clock,
14
+ Package,
15
+ User,
16
+ LogOut,
17
+ Shield,
18
+ Sparkles,
19
+ } from 'lucide-react';
20
+
21
+ export function TopNav() {
22
+ const pathname = usePathname() || '';
23
+ const [role, setRole] = useState<'admin' | 'guest'>('guest');
24
+ const [username, setUsername] = useState('');
25
+ const [avatar, setAvatar] = useState('');
26
+
27
+ useEffect(() => {
28
+ const savedRole = localStorage.getItem('role');
29
+ if (savedRole === 'admin') {
30
+ setRole('admin');
31
+ }
32
+
33
+ const savedUsername = localStorage.getItem('username');
34
+ if (savedUsername) {
35
+ setUsername(savedUsername);
36
+ }
37
+
38
+ const savedAvatar = localStorage.getItem('feishuAvatar');
39
+ if (savedAvatar) {
40
+ setAvatar(savedAvatar);
41
+ }
42
+ }, []);
43
+
44
+ const handleLogout = () => {
45
+ localStorage.removeItem('userId');
46
+ localStorage.removeItem('username');
47
+ localStorage.removeItem('role');
48
+ window.location.href = '/login';
49
+ };
50
+
51
+ const isActive = (path: string) => {
52
+ if (path === '/') return pathname === '/';
53
+ return pathname.startsWith(path);
54
+ };
55
+
56
+ const navItems = [
57
+ { href: '/', label: 'AI助手', icon: Sparkles },
58
+ { href: '/market', label: '市场', icon: Package },
59
+ { href: '/my/knowledge', label: '我的', icon: User },
60
+ {
61
+ href: role === 'admin' ? '/tickets' : '/',
62
+ label: '系统',
63
+ icon: Settings,
64
+ adminOnly: true
65
+ },
66
+ ];
67
+
68
+ return (
69
+ <header className="bg-white border-b h-12 flex items-center px-4 gap-1 justify-between">
70
+ <div className="flex items-center gap-1">
71
+ <Link href="/" className="flex items-center gap-2 mr-6 no-underline">
72
+ <div className="w-7 h-7 rounded-md bg-blue-600 flex items-center justify-center">
73
+ <Bot className="h-4 w-4 text-white" />
74
+ </div>
75
+ <span className="font-semibold text-sm text-gray-900">智能工作助手</span>
76
+ </Link>
77
+
78
+ {navItems.map((item) => {
79
+ const Icon = item.icon;
80
+ // 管理员显示所有,非管理员隐藏系统
81
+ if (item.adminOnly && role !== 'admin') return null;
82
+
83
+ return (
84
+ <Link
85
+ key={item.href}
86
+ href={item.href}
87
+ className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm no-underline ${
88
+ isActive(item.href)
89
+ ? 'bg-blue-600 text-white'
90
+ : 'text-gray-600 hover:bg-gray-100'
91
+ }`}
92
+ >
93
+ <Icon className="h-4 w-4" />
94
+ {item.label}
95
+ </Link>
96
+ );
97
+ })}
98
+ </div>
99
+
100
+ <div className="flex items-center gap-3">
101
+ <div className="flex items-center gap-2 px-2 py-1 bg-gray-50 rounded text-xs">
102
+ {avatar ? (
103
+ <img
104
+ src={avatar}
105
+ alt={username}
106
+ className="w-5 h-5 rounded-full object-cover"
107
+ />
108
+ ) : role === 'admin' ? (
109
+ <Shield className="h-3 w-3 text-blue-600" />
110
+ ) : (
111
+ <User className="h-3 w-3 text-gray-500" />
112
+ )}
113
+ <span className="text-gray-700">{username}</span>
114
+ {role === 'admin' && (
115
+ <>
116
+ <span className="text-gray-400">|</span>
117
+ <span className="text-blue-600 font-medium">管理员</span>
118
+ </>
119
+ )}
120
+ </div>
121
+ <button
122
+ onClick={handleLogout}
123
+ className="flex items-center gap-1 px-2 py-1 text-xs text-gray-500 hover:text-red-600 hover:bg-red-50 rounded transition-colors"
124
+ title="退出登录"
125
+ >
126
+ <LogOut className="h-3 w-3" />
127
+ 退出
128
+ </button>
129
+ </div>
130
+ </header>
131
+ );
132
+ }
@@ -0,0 +1,58 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AccordionPrimitive from "@radix-ui/react-accordion"
5
+ import { ChevronDown } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ const Accordion = AccordionPrimitive.Root
10
+
11
+ const AccordionItem = React.forwardRef<
12
+ React.ElementRef<typeof AccordionPrimitive.Item>,
13
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
14
+ >(({ className, ...props }, ref) => (
15
+ <AccordionPrimitive.Item
16
+ ref={ref}
17
+ className={cn("border-b", className)}
18
+ {...props}
19
+ />
20
+ ))
21
+ AccordionItem.displayName = "AccordionItem"
22
+
23
+ const AccordionTrigger = React.forwardRef<
24
+ React.ElementRef<typeof AccordionPrimitive.Trigger>,
25
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
26
+ >(({ className, children, ...props }, ref) => (
27
+ <AccordionPrimitive.Header className="flex">
28
+ <AccordionPrimitive.Trigger
29
+ ref={ref}
30
+ className={cn(
31
+ "flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
32
+ className
33
+ )}
34
+ {...props}
35
+ >
36
+ {children}
37
+ <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
38
+ </AccordionPrimitive.Trigger>
39
+ </AccordionPrimitive.Header>
40
+ ))
41
+ AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
42
+
43
+ const AccordionContent = React.forwardRef<
44
+ React.ElementRef<typeof AccordionPrimitive.Content>,
45
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
46
+ >(({ className, children, ...props }, ref) => (
47
+ <AccordionPrimitive.Content
48
+ ref={ref}
49
+ className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
50
+ {...props}
51
+ >
52
+ <div className={cn("pb-4 pt-0", className)}>{children}</div>
53
+ </AccordionPrimitive.Content>
54
+ ))
55
+
56
+ AccordionContent.displayName = AccordionPrimitive.Content.displayName
57
+
58
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
@@ -0,0 +1,59 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-background text-foreground",
12
+ destructive:
13
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ }
20
+ )
21
+
22
+ const Alert = React.forwardRef<
23
+ HTMLDivElement,
24
+ React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
25
+ >(({ className, variant, ...props }, ref) => (
26
+ <div
27
+ ref={ref}
28
+ role="alert"
29
+ className={cn(alertVariants({ variant }), className)}
30
+ {...props}
31
+ />
32
+ ))
33
+ Alert.displayName = "Alert"
34
+
35
+ const AlertTitle = React.forwardRef<
36
+ HTMLParagraphElement,
37
+ React.HTMLAttributes<HTMLHeadingElement>
38
+ >(({ className, ...props }, ref) => (
39
+ <h5
40
+ ref={ref}
41
+ className={cn("mb-1 font-medium leading-none tracking-tight", className)}
42
+ {...props}
43
+ />
44
+ ))
45
+ AlertTitle.displayName = "AlertTitle"
46
+
47
+ const AlertDescription = React.forwardRef<
48
+ HTMLParagraphElement,
49
+ React.HTMLAttributes<HTMLParagraphElement>
50
+ >(({ className, ...props }, ref) => (
51
+ <div
52
+ ref={ref}
53
+ className={cn("text-sm [&_p]:leading-relaxed", className)}
54
+ {...props}
55
+ />
56
+ ))
57
+ AlertDescription.displayName = "AlertDescription"
58
+
59
+ export { Alert, AlertTitle, AlertDescription }
@@ -0,0 +1,50 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AvatarPrimitive from "@radix-ui/react-avatar"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const Avatar = React.forwardRef<
9
+ React.ElementRef<typeof AvatarPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
11
+ >(({ className, ...props }, ref) => (
12
+ <AvatarPrimitive.Root
13
+ ref={ref}
14
+ className={cn(
15
+ "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ ))
21
+ Avatar.displayName = AvatarPrimitive.Root.displayName
22
+
23
+ const AvatarImage = React.forwardRef<
24
+ React.ElementRef<typeof AvatarPrimitive.Image>,
25
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
26
+ >(({ className, ...props }, ref) => (
27
+ <AvatarPrimitive.Image
28
+ ref={ref}
29
+ className={cn("aspect-square h-full w-full", className)}
30
+ {...props}
31
+ />
32
+ ))
33
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName
34
+
35
+ const AvatarFallback = React.forwardRef<
36
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
37
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
38
+ >(({ className, ...props }, ref) => (
39
+ <AvatarPrimitive.Fallback
40
+ ref={ref}
41
+ className={cn(
42
+ "flex h-full w-full items-center justify-center rounded-full bg-muted",
43
+ className
44
+ )}
45
+ {...props}
46
+ />
47
+ ))
48
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49
+
50
+ export { Avatar, AvatarImage, AvatarFallback }