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,290 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { useRouter, useParams } from 'next/navigation';
5
+ import { ArrowLeft, Save, Terminal } from 'lucide-react';
6
+ import Link from 'next/link';
7
+ import { Button } from '@/components/ui/button';
8
+ import { Input } from '@/components/ui/input';
9
+ import { Label } from '@/components/ui/label';
10
+ import { Textarea } from '@/components/ui/textarea';
11
+ import {
12
+ Select,
13
+ SelectContent,
14
+ SelectItem,
15
+ SelectTrigger,
16
+ SelectValue,
17
+ } from '@/components/ui/select';
18
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
19
+ import type { ScheduledTask } from '@/lib/db';
20
+
21
+ // Cron presets
22
+ const CRON_PRESETS = {
23
+ hourly: { label: '每小时', expression: '0 * * * *' },
24
+ every_6_hours: { label: '每6小时', expression: '0 */6 * * *' },
25
+ daily_8am: { label: '每天上午8点', expression: '0 8 * * *' },
26
+ daily_9am: { label: '每天上午9点', expression: '0 9 * * *' },
27
+ daily_midnight: { label: '每天凌晨', expression: '0 0 * * *' },
28
+ weekly_monday_9am: { label: '每周一早上9点', expression: '0 9 * * 1' },
29
+ weekly_friday_6pm: { label: '每周五下午6点', expression: '0 18 * * 5' },
30
+ monthly_1st_midnight: { label: '每月1号凌晨', expression: '0 0 1 * *' },
31
+ monthly_15th_9am: { label: '每月15号上午9点', expression: '0 9 15 * *' },
32
+ };
33
+
34
+ export default function EditScheduledTaskPage() {
35
+ const router = useRouter();
36
+ const params = useParams();
37
+ const taskId = params.id as string;
38
+
39
+ const [loading, setLoading] = useState(true);
40
+ const [saving, setSaving] = useState(false);
41
+ const [error, setError] = useState<string | null>(null);
42
+ const [task, setTask] = useState<ScheduledTask | null>(null);
43
+
44
+ // 表单状态
45
+ const [formData, setFormData] = useState({
46
+ title: '',
47
+ description: '',
48
+ command: '',
49
+ scheduleType: 'cron' as 'cron' | 'interval' | 'once',
50
+ scheduleExpression: '',
51
+ cronPreset: '',
52
+ status: 'active' as 'active' | 'paused' | 'disabled',
53
+ });
54
+
55
+ // 加载任务数据
56
+ useEffect(() => {
57
+ async function fetchTask() {
58
+ try {
59
+ const response = await fetch(`/api/scheduled-tasks/${taskId}`);
60
+ if (!response.ok) throw new Error('获取任务失败');
61
+
62
+ const data = await response.json();
63
+ const taskData = data.task as ScheduledTask;
64
+ setTask(taskData);
65
+
66
+ setFormData({
67
+ title: taskData.title,
68
+ description: taskData.description || '',
69
+ command: taskData.command,
70
+ scheduleType: taskData.scheduleType,
71
+ scheduleExpression: taskData.scheduleExpression,
72
+ cronPreset: '',
73
+ status: taskData.status,
74
+ });
75
+ } catch (err) {
76
+ setError(err instanceof Error ? err.message : '加载失败');
77
+ } finally {
78
+ setLoading(false);
79
+ }
80
+ }
81
+
82
+ fetchTask();
83
+ }, [taskId]);
84
+
85
+ const handleInputChange = (field: string, value: string) => {
86
+ setFormData(prev => ({ ...prev, [field]: value }));
87
+
88
+ // 如果选择的是预设,同步更新 scheduleExpression
89
+ if (field === 'cronPreset' && value) {
90
+ const preset = CRON_PRESETS[value as keyof typeof CRON_PRESETS];
91
+ if (preset) {
92
+ setFormData(prev => ({ ...prev, scheduleExpression: preset.expression }));
93
+ }
94
+ }
95
+ };
96
+
97
+ const handleSubmit = async (e: React.FormEvent) => {
98
+ e.preventDefault();
99
+ setSaving(true);
100
+ setError(null);
101
+
102
+ try {
103
+ const response = await fetch(`/api/scheduled-tasks/${taskId}`, {
104
+ method: 'PUT',
105
+ headers: { 'Content-Type': 'application/json' },
106
+ body: JSON.stringify({
107
+ ...formData,
108
+ updatedBy: 'user',
109
+ }),
110
+ });
111
+
112
+ if (!response.ok) {
113
+ const data = await response.json();
114
+ throw new Error(data.error || '更新失败');
115
+ }
116
+
117
+ router.push(`/my/scheduled-tasks/${taskId}`);
118
+ } catch (err) {
119
+ setError(err instanceof Error ? err.message : '更新失败');
120
+ } finally {
121
+ setSaving(false);
122
+ }
123
+ };
124
+
125
+ if (loading) {
126
+ return (
127
+ <div className="flex items-center justify-center py-16">
128
+ <div className="flex flex-col items-center gap-4">
129
+ <div className="w-10 h-10 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
130
+ <p className="text-muted-foreground">加载中...</p>
131
+ </div>
132
+ </div>
133
+ );
134
+ }
135
+
136
+ return (
137
+ <div className="p-6">
138
+ <div className="max-w-3xl mx-auto">
139
+ {/* 页面标题 */}
140
+ <div className="flex items-center gap-4 mb-6">
141
+ <Button variant="ghost" size="icon" asChild>
142
+ <Link href={`/my/scheduled-tasks/${taskId}`}>
143
+ <ArrowLeft className="h-5 w-5" />
144
+ </Link>
145
+ </Button>
146
+ <div>
147
+ <h1 className="text-2xl font-bold">编辑定时任务</h1>
148
+ <p className="text-muted-foreground text-sm mt-1">修改定时任务配置</p>
149
+ </div>
150
+ </div>
151
+
152
+ {error && (
153
+ <div className="bg-destructive/10 border border-destructive/20 rounded-xl p-4 mb-6">
154
+ <p className="text-destructive">❌ {error}</p>
155
+ </div>
156
+ )}
157
+
158
+ <form onSubmit={handleSubmit} className="space-y-6">
159
+ {/* 基本信息 */}
160
+ <Card>
161
+ <CardHeader>
162
+ <CardTitle>基本信息</CardTitle>
163
+ <CardDescription>设置定时任务的基本属性</CardDescription>
164
+ </CardHeader>
165
+ <CardContent className="space-y-4">
166
+ <div>
167
+ <Label htmlFor="title">任务标题 *</Label>
168
+ <Input
169
+ id="title"
170
+ value={formData.title}
171
+ onChange={(e) => handleInputChange('title', e.target.value)}
172
+ placeholder="例如:每天清理临时文件"
173
+ required
174
+ />
175
+ </div>
176
+ <div>
177
+ <Label htmlFor="description">描述</Label>
178
+ <Textarea
179
+ id="description"
180
+ value={formData.description}
181
+ onChange={(e) => handleInputChange('description', e.target.value)}
182
+ placeholder="详细描述此定时任务的用途..."
183
+ rows={3}
184
+ />
185
+ </div>
186
+ <div>
187
+ <Label htmlFor="status">状态 *</Label>
188
+ <Select value={formData.status} onValueChange={(v) => handleInputChange('status', v as 'active' | 'paused' | 'disabled')}>
189
+ <SelectTrigger id="status">
190
+ <SelectValue />
191
+ </SelectTrigger>
192
+ <SelectContent>
193
+ <SelectItem value="active">运行中</SelectItem>
194
+ <SelectItem value="paused">已暂停</SelectItem>
195
+ <SelectItem value="disabled">已禁用</SelectItem>
196
+ </SelectContent>
197
+ </Select>
198
+ </div>
199
+ </CardContent>
200
+ </Card>
201
+
202
+ {/* 命令配置 */}
203
+ <Card>
204
+ <CardHeader>
205
+ <CardTitle className="flex items-center gap-2">
206
+ <Terminal className="h-5 w-5" />
207
+ 命令配置
208
+ </CardTitle>
209
+ <CardDescription>设置要执行的命令</CardDescription>
210
+ </CardHeader>
211
+ <CardContent className="space-y-4">
212
+ <div>
213
+ <Label htmlFor="command">执行命令 *</Label>
214
+ <Textarea
215
+ id="command"
216
+ value={formData.command}
217
+ onChange={(e) => handleInputChange('command', e.target.value)}
218
+ placeholder="例如:rm -rf /tmp/*.log 或 kubectl get pods -n default"
219
+ rows={3}
220
+ required
221
+ className="font-mono"
222
+ />
223
+ <p className="text-xs text-muted-foreground mt-1">
224
+ 支持任意 shell 命令,危险命令会被自动拦截并创建工单
225
+ </p>
226
+ </div>
227
+ </CardContent>
228
+ </Card>
229
+
230
+ {/* 调度配置 */}
231
+ <Card>
232
+ <CardHeader>
233
+ <CardTitle>调度配置</CardTitle>
234
+ <CardDescription>设置任务执行的时间和频率</CardDescription>
235
+ </CardHeader>
236
+ <CardContent className="space-y-4">
237
+ <div>
238
+ <Label htmlFor="preset">常用预设</Label>
239
+ <Select value={formData.cronPreset} onValueChange={(v) => handleInputChange('cronPreset', v)}>
240
+ <SelectTrigger id="preset">
241
+ <SelectValue placeholder="选择预设..." />
242
+ </SelectTrigger>
243
+ <SelectContent>
244
+ {Object.entries(CRON_PRESETS).map(([key, preset]) => (
245
+ <SelectItem key={key} value={key}>
246
+ {preset.label}
247
+ </SelectItem>
248
+ ))}
249
+ </SelectContent>
250
+ </Select>
251
+ </div>
252
+ <div>
253
+ <Label htmlFor="expression">Cron 表达式 *</Label>
254
+ <Input
255
+ id="expression"
256
+ value={formData.scheduleExpression}
257
+ onChange={(e) => handleInputChange('scheduleExpression', e.target.value)}
258
+ placeholder="0 9 * * *"
259
+ pattern="^(\S+\s+){4}\S+$"
260
+ required
261
+ className="font-mono"
262
+ />
263
+ <p className="text-xs text-muted-foreground mt-1">
264
+ 格式:分 时 日 月 周(例如:0 9 * * * 表示每天早上9点)
265
+ </p>
266
+ </div>
267
+ <div className="bg-muted rounded-lg p-3">
268
+ <p className="text-sm font-medium">Cron 表达式</p>
269
+ <p className="text-sm text-muted-foreground mt-1 font-mono">
270
+ {formData.scheduleExpression}
271
+ </p>
272
+ </div>
273
+ </CardContent>
274
+ </Card>
275
+
276
+ {/* 提交按钮 */}
277
+ <div className="flex gap-3">
278
+ <Button type="submit" disabled={saving}>
279
+ <Save className="mr-2 h-4 w-4" />
280
+ {saving ? '保存中...' : '保存修改'}
281
+ </Button>
282
+ <Button type="button" variant="outline" asChild>
283
+ <Link href={`/my/scheduled-tasks/${taskId}`}>取消</Link>
284
+ </Button>
285
+ </div>
286
+ </form>
287
+ </div>
288
+ </div>
289
+ );
290
+ }
@@ -0,0 +1,275 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { useParams, useRouter } from 'next/navigation';
5
+ import Link from 'next/link';
6
+ import { ArrowLeft, CheckCircle, XCircle, Clock, AlertCircle } from 'lucide-react';
7
+ import { Button } from '@/components/ui/button';
8
+ import { Badge } from '@/components/ui/badge';
9
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
10
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
11
+ import type { TaskExecution, ScheduledTask } from '@/lib/db';
12
+
13
+ export default function TaskExecutionsPage() {
14
+ const params = useParams();
15
+ const router = useRouter();
16
+ const taskId = params.id as string;
17
+
18
+ const [task, setTask] = useState<ScheduledTask | null>(null);
19
+ const [executions, setExecutions] = useState<TaskExecution[]>([]);
20
+ const [loading, setLoading] = useState(true);
21
+ const [error, setError] = useState<string | null>(null);
22
+
23
+ useEffect(() => {
24
+ async function fetchData() {
25
+ try {
26
+ // 并行获取任务和执行历史
27
+ const [taskRes, execRes] = await Promise.all([
28
+ fetch(`/api/scheduled-tasks/${taskId}`),
29
+ fetch(`/api/task-executions?taskId=${taskId}`),
30
+ ]);
31
+
32
+ if (!taskRes.ok) throw new Error('获取任务失败');
33
+ if (!execRes.ok) throw new Error('获取执行历史失败');
34
+
35
+ const taskData = await taskRes.json();
36
+ const execData = await execRes.json();
37
+
38
+ setTask(taskData.task);
39
+ setExecutions(execData.executions);
40
+ } catch (err) {
41
+ setError(err instanceof Error ? err.message : '加载失败');
42
+ } finally {
43
+ setLoading(false);
44
+ }
45
+ }
46
+
47
+ fetchData();
48
+ }, [taskId]);
49
+
50
+ function StatusIcon({ status }: { status: TaskExecution['status'] }) {
51
+ switch (status) {
52
+ case 'completed':
53
+ return <CheckCircle className="h-4 w-4 text-green-600" />;
54
+ case 'failed':
55
+ return <XCircle className="h-4 w-4 text-destructive" />;
56
+ case 'running':
57
+ return <Clock className="h-4 w-4 text-blue-600 animate-pulse" />;
58
+ default:
59
+ return <AlertCircle className="h-4 w-4 text-muted-foreground" />;
60
+ }
61
+ }
62
+
63
+ function StatusBadge({ status }: { status: TaskExecution['status'] }) {
64
+ const config = {
65
+ pending: { label: '待执行', variant: 'secondary' as const, className: undefined as string | undefined },
66
+ running: { label: '执行中', variant: 'outline' as const, className: undefined as string | undefined },
67
+ completed: { label: '已完成', variant: 'default' as const, className: 'bg-green-500/10 text-green-700 border-green-500/20' },
68
+ failed: { label: '失败', variant: 'destructive' as const, className: undefined as string | undefined },
69
+ };
70
+ const { label, variant, className } = config[status] || config.pending;
71
+ return <Badge variant={variant} className={className}>{label}</Badge>;
72
+ }
73
+
74
+ if (loading) {
75
+ return (
76
+ <div className="flex items-center justify-center py-16">
77
+ <div className="flex flex-col items-center gap-4">
78
+ <div className="w-10 h-10 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
79
+ <p className="text-muted-foreground">加载中...</p>
80
+ </div>
81
+ </div>
82
+ );
83
+ }
84
+
85
+ if (error || !task) {
86
+ return (
87
+ <div className="bg-destructive/10 border border-destructive/20 rounded-xl p-6">
88
+ <p className="text-destructive">❌ {error || '任务不存在'}</p>
89
+ </div>
90
+ );
91
+ }
92
+
93
+ return (
94
+ <div className="p-6">
95
+ <div className="max-w-5xl mx-auto">
96
+ {/* 页面标题 */}
97
+ <div className="flex items-center gap-4 mb-6">
98
+ <Button variant="ghost" size="icon" asChild>
99
+ <Link href={`/my/scheduled-tasks/${taskId}`}>
100
+ <ArrowLeft className="h-5 w-5" />
101
+ </Link>
102
+ </Button>
103
+ <div>
104
+ <h1 className="text-2xl font-bold">执行历史</h1>
105
+ <p className="text-muted-foreground text-sm mt-1">
106
+ {task.title} 的执行记录
107
+ </p>
108
+ </div>
109
+ </div>
110
+
111
+ {/* 统计卡片 */}
112
+ <div className="grid grid-cols-4 gap-4 mb-6">
113
+ <Card>
114
+ <CardHeader className="pb-2">
115
+ <CardTitle className="text-sm font-medium text-muted-foreground">总执行次数</CardTitle>
116
+ </CardHeader>
117
+ <CardContent>
118
+ <div className="text-2xl font-bold">{executions.length}</div>
119
+ </CardContent>
120
+ </Card>
121
+ <Card>
122
+ <CardHeader className="pb-2">
123
+ <CardTitle className="text-sm font-medium text-muted-foreground">成功</CardTitle>
124
+ </CardHeader>
125
+ <CardContent>
126
+ <div className="text-2xl font-bold text-green-600">
127
+ {executions.filter(e => e.status === 'completed').length}
128
+ </div>
129
+ </CardContent>
130
+ </Card>
131
+ <Card>
132
+ <CardHeader className="pb-2">
133
+ <CardTitle className="text-sm font-medium text-muted-foreground">失败</CardTitle>
134
+ </CardHeader>
135
+ <CardContent>
136
+ <div className="text-2xl font-bold text-destructive">
137
+ {executions.filter(e => e.status === 'failed').length}
138
+ </div>
139
+ </CardContent>
140
+ </Card>
141
+ <Card>
142
+ <CardHeader className="pb-2">
143
+ <CardTitle className="text-sm font-medium text-muted-foreground">执行中</CardTitle>
144
+ </CardHeader>
145
+ <CardContent>
146
+ <div className="text-2xl font-bold text-blue-600">
147
+ {executions.filter(e => e.status === 'running').length}
148
+ </div>
149
+ </CardContent>
150
+ </Card>
151
+ </div>
152
+
153
+ {/* 执行历史表格 */}
154
+ {executions.length === 0 ? (
155
+ <div className="text-center py-16 bg-card rounded-xl border shadow-sm">
156
+ <div className="text-6xl mb-4">📭</div>
157
+ <p className="text-muted-foreground text-lg">暂无执行记录</p>
158
+ <p className="text-muted-foreground/70 text-sm mt-2">
159
+ 定时任务尚未执行过
160
+ </p>
161
+ </div>
162
+ ) : (
163
+ <div className="bg-card rounded-xl border shadow-sm overflow-hidden">
164
+ <Table>
165
+ <TableHeader>
166
+ <TableRow>
167
+ <TableHead>执行 ID</TableHead>
168
+ <TableHead>状态</TableHead>
169
+ <TableHead>开始时间</TableHead>
170
+ <TableHead>完成时间</TableHead>
171
+ <TableHead>耗时</TableHead>
172
+ <TableHead>关联工单</TableHead>
173
+ </TableRow>
174
+ </TableHeader>
175
+ <TableBody>
176
+ {executions.map((execution) => {
177
+ const completedAt = execution.completedAt ? new Date(execution.completedAt) : null;
178
+ const startedAt = execution.startedAt ? new Date(execution.startedAt) : null;
179
+ const duration = completedAt && startedAt
180
+ ? completedAt.getTime() - startedAt.getTime()
181
+ : null;
182
+
183
+ return (
184
+ <TableRow key={execution.id}>
185
+ <TableCell>
186
+ <span className="font-mono text-sm">
187
+ {execution.id.slice(-12)}
188
+ </span>
189
+ </TableCell>
190
+ <TableCell>
191
+ <div className="flex items-center gap-2">
192
+ <StatusIcon status={execution.status} />
193
+ <StatusBadge status={execution.status} />
194
+ </div>
195
+ </TableCell>
196
+ <TableCell>
197
+ {startedAt ? (
198
+ <span className="text-sm">
199
+ {startedAt.toLocaleString('zh-CN')}
200
+ </span>
201
+ ) : (
202
+ <span className="text-sm text-muted-foreground">-</span>
203
+ )}
204
+ </TableCell>
205
+ <TableCell>
206
+ {completedAt ? (
207
+ <span className="text-sm">
208
+ {completedAt.toLocaleString('zh-CN')}
209
+ </span>
210
+ ) : (
211
+ <span className="text-sm text-muted-foreground">-</span>
212
+ )}
213
+ </TableCell>
214
+ <TableCell>
215
+ {duration ? (
216
+ <span className="text-sm">
217
+ {duration < 1000
218
+ ? `${duration}ms`
219
+ : `${(duration / 1000).toFixed(1)}s`}
220
+ </span>
221
+ ) : (
222
+ <span className="text-sm text-muted-foreground">-</span>
223
+ )}
224
+ </TableCell>
225
+ <TableCell>
226
+ {execution.ticketId ? (
227
+ <Link
228
+ href={`/tickets/${execution.ticketId}`}
229
+ className="text-sm text-primary hover:underline"
230
+ >
231
+ {execution.ticketId.slice(-12)}
232
+ </Link>
233
+ ) : (
234
+ <span className="text-sm text-muted-foreground">-</span>
235
+ )}
236
+ </TableCell>
237
+ </TableRow>
238
+ );
239
+ })}
240
+ </TableBody>
241
+ </Table>
242
+ </div>
243
+ )}
244
+
245
+ {/* 错误信息展示 */}
246
+ {executions.some(e => e.errorMessage) && (
247
+ <Card className="mt-6">
248
+ <CardHeader>
249
+ <CardTitle className="text-base">错误信息</CardTitle>
250
+ </CardHeader>
251
+ <CardContent>
252
+ <div className="space-y-3">
253
+ {executions
254
+ .filter(e => e.errorMessage)
255
+ .map((execution) => (
256
+ <div key={execution.id} className="bg-destructive/10 rounded-lg p-3">
257
+ <div className="flex items-center gap-2 mb-1">
258
+ <XCircle className="h-4 w-4 text-destructive" />
259
+ <span className="text-sm font-mono">
260
+ {execution.id.slice(-12)}
261
+ </span>
262
+ </div>
263
+ <p className="text-sm text-destructive pl-6">
264
+ {execution.errorMessage}
265
+ </p>
266
+ </div>
267
+ ))}
268
+ </div>
269
+ </CardContent>
270
+ </Card>
271
+ )}
272
+ </div>
273
+ </div>
274
+ );
275
+ }