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,356 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import Link from 'next/link';
5
+ import { usePathname } from 'next/navigation';
6
+ import { Terminal, Trash2, Clock, History, Play, Pause, Calendar, AlertCircle } from 'lucide-react';
7
+ import type { ScheduledTask } from '@/lib/db';
8
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
9
+ import { Button } from '@/components/ui/button';
10
+ import { Input } from '@/components/ui/input';
11
+ import { Badge } from '@/components/ui/badge';
12
+ import {
13
+ Select,
14
+ SelectContent,
15
+ SelectItem,
16
+ SelectTrigger,
17
+ SelectValue,
18
+ } from '@/components/ui/select';
19
+ import {
20
+ Card,
21
+ CardContent,
22
+ } from '@/components/ui/card';
23
+
24
+ export function ScheduledTaskList() {
25
+ const pathname = usePathname();
26
+ // 根据当前路由判断使用哪个路径前缀
27
+ // 管理员路由 (/scheduled-tasks) 使用空字符串
28
+ // 用户路由 (/my/scheduled-tasks) 使用 '/my'
29
+ const basePath = pathname.startsWith('/scheduled-tasks') ? '' : '/my';
30
+
31
+ const [tasks, setTasks] = useState<ScheduledTask[]>([]);
32
+ const [filteredTasks, setFilteredTasks] = useState<ScheduledTask[]>([]);
33
+ const [loading, setLoading] = useState(true);
34
+ const [error, setError] = useState<string | null>(null);
35
+
36
+ // 搜索和筛选状态
37
+ const [searchQuery, setSearchQuery] = useState('');
38
+ const [statusFilter, setStatusFilter] = useState('all');
39
+
40
+ async function fetchTasks() {
41
+ try {
42
+ const response = await fetch('/api/scheduled-tasks');
43
+ if (!response.ok) throw new Error('获取定时任务失败');
44
+
45
+ const data = await response.json();
46
+ setTasks(data.tasks);
47
+ setFilteredTasks(data.tasks);
48
+ } catch (err) {
49
+ setError(err instanceof Error ? err.message : '未知错误');
50
+ } finally {
51
+ setLoading(false);
52
+ }
53
+ }
54
+
55
+ useEffect(() => {
56
+ fetchTasks();
57
+ }, []);
58
+
59
+ // 筛选和搜索逻辑
60
+ useEffect(() => {
61
+ let filtered = tasks;
62
+
63
+ // 状态筛选
64
+ if (statusFilter !== 'all') {
65
+ filtered = filtered.filter(t => t.status === statusFilter);
66
+ }
67
+
68
+ // 搜索
69
+ if (searchQuery) {
70
+ const query = searchQuery.toLowerCase();
71
+ filtered = filtered.filter(t =>
72
+ t.title.toLowerCase().includes(query) ||
73
+ t.description?.toLowerCase().includes(query) ||
74
+ t.id.toLowerCase().includes(query) ||
75
+ t.command.toLowerCase().includes(query)
76
+ );
77
+ }
78
+
79
+ setFilteredTasks(filtered);
80
+ }, [tasks, statusFilter, searchQuery]);
81
+
82
+ // 切换任务状态
83
+ async function toggleTask(taskId: string, _currentStatus: string) {
84
+ try {
85
+ const response = await fetch(`/api/scheduled-tasks/${taskId}/toggle`, {
86
+ method: 'POST',
87
+ headers: { 'Content-Type': 'application/json' },
88
+ body: JSON.stringify({ userId: 'user' }),
89
+ });
90
+ if (!response.ok) throw new Error('切换状态失败');
91
+
92
+ const data = await response.json();
93
+ setTasks(tasks.map(t => t.id === taskId ? data.task : t));
94
+ } catch (err) {
95
+ console.error(err);
96
+ alert('切换状态失败');
97
+ }
98
+ }
99
+
100
+ // 删除任务
101
+ async function deleteTask(taskId: string, title: string) {
102
+ if (!confirm(`确定删除定时任务 "${title}" 吗?`)) return;
103
+
104
+ try {
105
+ const response = await fetch(`/api/scheduled-tasks/${taskId}?deletedBy=user`, {
106
+ method: 'DELETE',
107
+ });
108
+ if (!response.ok) throw new Error('删除失败');
109
+
110
+ setTasks(tasks.filter(t => t.id !== taskId));
111
+ } catch (err) {
112
+ console.error(err);
113
+ alert('删除失败');
114
+ }
115
+ }
116
+
117
+ if (loading) {
118
+ return (
119
+ <Card>
120
+ <CardContent className="flex items-center justify-center py-16">
121
+ <div className="flex flex-col items-center gap-4">
122
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
123
+ <p className="text-muted-foreground text-sm">加载中...</p>
124
+ </div>
125
+ </CardContent>
126
+ </Card>
127
+ );
128
+ }
129
+
130
+ if (error) {
131
+ return (
132
+ <Card className="border-destructive">
133
+ <CardContent className="p-6">
134
+ <div className="flex items-center gap-3">
135
+ <AlertCircle className="w-5 h-5 text-destructive flex-shrink-0" />
136
+ <div>
137
+ <p className="text-destructive font-medium">{error}</p>
138
+ <p className="text-sm text-muted-foreground mt-1">请检查网络连接后重试</p>
139
+ </div>
140
+ </div>
141
+ </CardContent>
142
+ </Card>
143
+ );
144
+ }
145
+
146
+ return (
147
+ <div className="space-y-4">
148
+ {/* 搜索和筛选栏 */}
149
+ <Card>
150
+ <CardContent className="p-4">
151
+ <div className="flex flex-col md:flex-row gap-4">
152
+ <div className="flex-1">
153
+ <Input
154
+ type="text"
155
+ value={searchQuery}
156
+ onChange={(e) => setSearchQuery(e.target.value)}
157
+ placeholder="搜索任务标题、描述、命令..."
158
+ className="w-full"
159
+ />
160
+ </div>
161
+ <div className="flex gap-2">
162
+ <Select value={statusFilter} onValueChange={setStatusFilter}>
163
+ <SelectTrigger className="w-[140px]">
164
+ <SelectValue placeholder="所有状态" />
165
+ </SelectTrigger>
166
+ <SelectContent>
167
+ <SelectItem value="all">所有状态</SelectItem>
168
+ <SelectItem value="active">运行中</SelectItem>
169
+ <SelectItem value="paused">已暂停</SelectItem>
170
+ <SelectItem value="disabled">已禁用</SelectItem>
171
+ </SelectContent>
172
+ </Select>
173
+ </div>
174
+ </div>
175
+ {filteredTasks.length !== tasks.length && (
176
+ <p className="text-sm text-muted-foreground mt-3">
177
+ 找到 <span className="font-medium text-foreground">{filteredTasks.length}</span> 个任务(共 {tasks.length} 个)
178
+ </p>
179
+ )}
180
+ </CardContent>
181
+ </Card>
182
+
183
+ {/* 任务表格 */}
184
+ {filteredTasks.length === 0 ? (
185
+ <Card>
186
+ <CardContent className="flex flex-col items-center justify-center py-16">
187
+ <div className="w-16 h-16 rounded-full bg-muted flex items-center justify-center mb-4">
188
+ <Clock className="w-8 h-8 text-muted-foreground" />
189
+ </div>
190
+ <h3 className="text-lg font-medium mb-2">暂无定时任务</h3>
191
+ <p className="text-muted-foreground text-center">
192
+ {searchQuery || statusFilter !== 'all'
193
+ ? '没有符合条件的任务,请调整筛选条件'
194
+ : '点击上方"新建定时任务"创建第一个定时任务,或通过对话让 AI 创建'}
195
+ </p>
196
+ </CardContent>
197
+ </Card>
198
+ ) : (
199
+ <Card>
200
+ <div className="overflow-x-auto">
201
+ <Table>
202
+ <TableHeader>
203
+ <TableRow>
204
+ <TableHead className="w-[220px]">任务</TableHead>
205
+ <TableHead className="w-[200px]">命令</TableHead>
206
+ <TableHead>调度</TableHead>
207
+ <TableHead>状态</TableHead>
208
+ <TableHead>下次执行</TableHead>
209
+ <TableHead className="text-right">操作</TableHead>
210
+ </TableRow>
211
+ </TableHeader>
212
+ <TableBody>
213
+ {filteredTasks.map((task) => (
214
+ <TableRow key={task.id}>
215
+ <TableCell>
216
+ <div className="flex items-start gap-3">
217
+ <div className={`w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0 ${
218
+ task.status === 'active'
219
+ ? 'bg-green-500/10'
220
+ : task.status === 'paused'
221
+ ? 'bg-amber-500/10'
222
+ : 'bg-muted'
223
+ }`}>
224
+ <Terminal className={`w-4 h-4 ${
225
+ task.status === 'active'
226
+ ? 'text-green-600 dark:text-green-400'
227
+ : task.status === 'paused'
228
+ ? 'text-amber-600 dark:text-amber-400'
229
+ : 'text-muted-foreground'
230
+ }`} />
231
+ </div>
232
+ <div className="min-w-0">
233
+ <Link
234
+ href={`${basePath}/scheduled-tasks/${task.id}`}
235
+ className="font-medium hover:text-primary transition-colors block truncate"
236
+ >
237
+ {task.title}
238
+ </Link>
239
+ {task.description && (
240
+ <p className="text-xs text-muted-foreground line-clamp-1 mt-0.5">{task.description}</p>
241
+ )}
242
+ </div>
243
+ </div>
244
+ </TableCell>
245
+ <TableCell>
246
+ <code className="text-xs bg-muted px-2 py-1 rounded font-mono block truncate max-w-[200px]" title={task.command}>
247
+ {task.command}
248
+ </code>
249
+ </TableCell>
250
+ <TableCell>
251
+ <div className="flex items-center gap-1.5 text-sm">
252
+ <Calendar className="w-3.5 h-3.5 text-muted-foreground" />
253
+ <code className="text-xs font-mono">{task.scheduleExpression}</code>
254
+ </div>
255
+ </TableCell>
256
+ <TableCell>
257
+ <StatusBadge status={task.status} />
258
+ </TableCell>
259
+ <TableCell>
260
+ {task.nextRunAt ? (
261
+ <div className="flex items-center gap-1.5 text-sm text-muted-foreground">
262
+ <Clock className="w-3.5 h-3.5" />
263
+ <span>{formatNextRunTime(task.nextRunAt)}</span>
264
+ </div>
265
+ ) : (
266
+ <span className="text-sm text-muted-foreground">-</span>
267
+ )}
268
+ </TableCell>
269
+ <TableCell className="text-right">
270
+ <div className="flex items-center justify-end gap-1">
271
+ <Button
272
+ variant="ghost"
273
+ size="icon"
274
+ onClick={() => toggleTask(task.id, task.status)}
275
+ className="h-8 w-8"
276
+ title={task.status === 'active' ? '暂停' : '启动'}
277
+ >
278
+ {task.status === 'active' ? (
279
+ <Pause className="h-4 w-4 text-amber-600" />
280
+ ) : (
281
+ <Play className="h-4 w-4 text-green-600" />
282
+ )}
283
+ </Button>
284
+ <Button
285
+ variant="ghost"
286
+ size="icon"
287
+ asChild
288
+ className="h-8 w-8"
289
+ title="执行历史"
290
+ >
291
+ <Link href={`${basePath}/scheduled-tasks/${task.id}/executions`}>
292
+ <History className="h-4 w-4" />
293
+ </Link>
294
+ </Button>
295
+ <Button
296
+ variant="ghost"
297
+ size="icon"
298
+ asChild
299
+ className="h-8 w-8"
300
+ title="编辑"
301
+ >
302
+ <Link href={`${basePath}/scheduled-tasks/${task.id}/edit`}>
303
+ <Terminal className="h-4 w-4" />
304
+ </Link>
305
+ </Button>
306
+ <Button
307
+ variant="ghost"
308
+ size="icon"
309
+ onClick={() => deleteTask(task.id, task.title)}
310
+ className="h-8 w-8 text-destructive hover:text-destructive hover:bg-destructive/10"
311
+ title="删除"
312
+ >
313
+ <Trash2 className="h-4 w-4" />
314
+ </Button>
315
+ </div>
316
+ </TableCell>
317
+ </TableRow>
318
+ ))}
319
+ </TableBody>
320
+ </Table>
321
+ </div>
322
+ </Card>
323
+ )}
324
+ </div>
325
+ );
326
+ }
327
+
328
+ // 状态徽章组件
329
+ function StatusBadge({ status }: { status: ScheduledTask['status'] }) {
330
+ const config = {
331
+ active: { label: '运行中', className: 'bg-green-500/10 text-green-700 dark:text-green-400 border-green-500/20' },
332
+ paused: { label: '已暂停', className: 'bg-amber-500/10 text-amber-700 dark:text-amber-400 border-amber-500/20' },
333
+ disabled: { label: '已禁用', className: 'bg-muted text-muted-foreground' },
334
+ };
335
+ const { label, className } = config[status] || config.paused;
336
+ return <Badge variant="outline" className={className}>{label}</Badge>;
337
+ }
338
+
339
+ // 格式化下次执行时间
340
+ function formatNextRunTime(date: Date | string): string {
341
+ const now = new Date();
342
+ const targetDate = typeof date === 'string' ? new Date(date) : date;
343
+ const diff = targetDate.getTime() - now.getTime();
344
+
345
+ if (diff < 60000) return '即将执行';
346
+ if (diff < 3600000) return `${Math.floor(diff / 60000)} 分钟后`;
347
+ if (diff < 86400000) return `${Math.floor(diff / 3600000)} 小时后`;
348
+ if (diff < 604800000) return `${Math.floor(diff / 86400000)} 天后`;
349
+
350
+ return targetDate.toLocaleDateString('zh-CN', {
351
+ month: 'short',
352
+ day: 'numeric',
353
+ hour: '2-digit',
354
+ minute: '2-digit',
355
+ });
356
+ }