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.
- package/README.md +234 -0
- package/app/(admin)/approvals/page.tsx +16 -0
- package/app/(admin)/audit/page.tsx +18 -0
- package/app/(admin)/layout.tsx +47 -0
- package/app/(admin)/scheduled-tasks/page.tsx +17 -0
- package/app/(admin)/settings/page.tsx +46 -0
- package/app/(admin)/skills/[name]/page.tsx +378 -0
- package/app/(admin)/skills/page.tsx +406 -0
- package/app/(admin)/statistics/page.tsx +416 -0
- package/app/(admin)/tickets/[id]/page.tsx +348 -0
- package/app/(admin)/tickets/new/page.tsx +309 -0
- package/app/(admin)/tickets/page.tsx +27 -0
- package/app/api/audit/route.ts +30 -0
- package/app/api/auth/feishu/callback/route.ts +72 -0
- package/app/api/auth/feishu/login/route.ts +17 -0
- package/app/api/auth/feishu/sso/route.ts +78 -0
- package/app/api/auth/login/route.ts +85 -0
- package/app/api/auth/oauth/route.ts +168 -0
- package/app/api/config/providers/route.ts +105 -0
- package/app/api/config/route.ts +115 -0
- package/app/api/config/status/route.ts +56 -0
- package/app/api/config/test/route.ts +212 -0
- package/app/api/documents/[id]/route.ts +88 -0
- package/app/api/documents/route.ts +53 -0
- package/app/api/health/route.ts +32 -0
- package/app/api/knowledge/[id]/route.ts +152 -0
- package/app/api/knowledge/from-session/route.ts +27 -0
- package/app/api/knowledge/route.ts +100 -0
- package/app/api/market/knowledge/[id]/route.ts +92 -0
- package/app/api/market/knowledge/route.ts +130 -0
- package/app/api/marketplace/skills/[id]/approve/route.ts +68 -0
- package/app/api/marketplace/skills/[id]/certify/route.ts +54 -0
- package/app/api/marketplace/skills/[id]/install/route.ts +180 -0
- package/app/api/marketplace/skills/[id]/promote-to-system/route.ts +219 -0
- package/app/api/marketplace/skills/[id]/rate/route.ts +90 -0
- package/app/api/marketplace/skills/[id]/ratings/route.ts +55 -0
- package/app/api/marketplace/skills/[id]/reject/route.ts +68 -0
- package/app/api/marketplace/skills/[id]/route.ts +177 -0
- package/app/api/marketplace/skills/route.ts +235 -0
- package/app/api/memory/route.ts +40 -0
- package/app/api/my/files/[id]/route.ts +52 -0
- package/app/api/my/files/route.ts +230 -0
- package/app/api/my/knowledge/route.ts +36 -0
- package/app/api/pi-chat/route.ts +443 -0
- package/app/api/recommend/route.ts +38 -0
- package/app/api/scheduled-tasks/[id]/execute/route.ts +132 -0
- package/app/api/scheduled-tasks/[id]/route.ts +165 -0
- package/app/api/scheduled-tasks/[id]/toggle/route.ts +53 -0
- package/app/api/scheduled-tasks/route.ts +101 -0
- package/app/api/sessions/[id]/messages/route.ts +212 -0
- package/app/api/sessions/route.ts +101 -0
- package/app/api/share/file/[id]/route.ts +37 -0
- package/app/api/skills/[name]/execute/route.ts +121 -0
- package/app/api/skills/[name]/route.ts +167 -0
- package/app/api/skills/create/route.ts +65 -0
- package/app/api/skills/generate/route.ts +405 -0
- package/app/api/skills/installed/route.ts +151 -0
- package/app/api/skills/route.ts +174 -0
- package/app/api/skills/translate/route.ts +40 -0
- package/app/api/skills/user/[name]/route.ts +159 -0
- package/app/api/skills/user/route.ts +90 -0
- package/app/api/statistics/route.ts +94 -0
- package/app/api/task-executions/[id]/route.ts +34 -0
- package/app/api/task-executions/route.ts +29 -0
- package/app/api/tickets/[id]/approve/route.ts +129 -0
- package/app/api/tickets/[id]/execute/route.ts +201 -0
- package/app/api/tickets/[id]/route.ts +127 -0
- package/app/api/tickets/route.ts +103 -0
- package/app/api/user/skills/route.ts +175 -0
- package/app/api/users/route.ts +80 -0
- package/app/chat/page.tsx +5 -0
- package/app/globals.css +84 -0
- package/app/h5/layout.tsx +5 -0
- package/app/h5/mobile-approvals-page.tsx +167 -0
- package/app/h5/mobile-chat-page.tsx +951 -0
- package/app/h5/mobile-profile-page.tsx +147 -0
- package/app/h5/mobile-tickets-page.tsx +121 -0
- package/app/h5/page.tsx +23 -0
- package/app/h5/ticket-action-buttons.tsx +80 -0
- package/app/layout.tsx +26 -0
- package/app/login/page.tsx +318 -0
- package/app/market/knowledge/[id]/page.tsx +77 -0
- package/app/market/knowledge/page.tsx +358 -0
- package/app/market/layout.tsx +29 -0
- package/app/market/page.tsx +18 -0
- package/app/market/skills/page.tsx +397 -0
- package/app/my/files/page.tsx +511 -0
- package/app/my/knowledge/[id]/page.tsx +271 -0
- package/app/my/knowledge/new/page.tsx +234 -0
- package/app/my/knowledge/page.tsx +248 -0
- package/app/my/layout.tsx +32 -0
- package/app/my/memory/page.tsx +164 -0
- package/app/my/page.tsx +18 -0
- package/app/my/scheduled-tasks/[id]/edit/page.tsx +290 -0
- package/app/my/scheduled-tasks/[id]/executions/page.tsx +275 -0
- package/app/my/scheduled-tasks/[id]/page.tsx +284 -0
- package/app/my/scheduled-tasks/new/page.tsx +230 -0
- package/app/my/scheduled-tasks/page.tsx +27 -0
- package/app/my/skills/[name]/page.tsx +320 -0
- package/app/my/skills/new/page.tsx +394 -0
- package/app/my/skills/page.tsx +303 -0
- package/app/page.tsx +2288 -0
- package/app/share/[sessionId]/page.tsx +226 -0
- package/app/share/file/[id]/page.tsx +140 -0
- package/bin/README.md +63 -0
- package/bin/generate-api-system +300 -0
- package/bin/postinstall.js +95 -0
- package/bin/work-agent.js +173 -0
- package/components/ai-elements/agent.tsx +142 -0
- package/components/ai-elements/artifact.tsx +149 -0
- package/components/ai-elements/attachments.tsx +427 -0
- package/components/ai-elements/audio-player.tsx +232 -0
- package/components/ai-elements/canvas.tsx +26 -0
- package/components/ai-elements/chain-of-thought.tsx +223 -0
- package/components/ai-elements/checkpoint.tsx +72 -0
- package/components/ai-elements/code-block.tsx +555 -0
- package/components/ai-elements/commit.tsx +449 -0
- package/components/ai-elements/confirmation.tsx +173 -0
- package/components/ai-elements/connection.tsx +28 -0
- package/components/ai-elements/context.tsx +410 -0
- package/components/ai-elements/controls.tsx +19 -0
- package/components/ai-elements/conversation.tsx +167 -0
- package/components/ai-elements/edge.tsx +144 -0
- package/components/ai-elements/environment-variables.tsx +325 -0
- package/components/ai-elements/file-tree.tsx +298 -0
- package/components/ai-elements/image.tsx +25 -0
- package/components/ai-elements/inline-citation.tsx +294 -0
- package/components/ai-elements/jsx-preview.tsx +250 -0
- package/components/ai-elements/message.tsx +367 -0
- package/components/ai-elements/mic-selector.tsx +372 -0
- package/components/ai-elements/model-selector.tsx +214 -0
- package/components/ai-elements/node.tsx +72 -0
- package/components/ai-elements/open-in-chat.tsx +367 -0
- package/components/ai-elements/package-info.tsx +235 -0
- package/components/ai-elements/panel.tsx +16 -0
- package/components/ai-elements/persona.tsx +280 -0
- package/components/ai-elements/plan.tsx +144 -0
- package/components/ai-elements/prompt-input.tsx +1341 -0
- package/components/ai-elements/queue.tsx +275 -0
- package/components/ai-elements/reasoning.tsx +355 -0
- package/components/ai-elements/sandbox.tsx +133 -0
- package/components/ai-elements/schema-display.tsx +473 -0
- package/components/ai-elements/shimmer.tsx +78 -0
- package/components/ai-elements/snippet.tsx +141 -0
- package/components/ai-elements/sources.tsx +78 -0
- package/components/ai-elements/speech-input.tsx +324 -0
- package/components/ai-elements/stack-trace.tsx +531 -0
- package/components/ai-elements/suggestion.tsx +58 -0
- package/components/ai-elements/task.tsx +88 -0
- package/components/ai-elements/terminal.tsx +277 -0
- package/components/ai-elements/test-results.tsx +497 -0
- package/components/ai-elements/tool.tsx +174 -0
- package/components/ai-elements/toolbar.tsx +17 -0
- package/components/ai-elements/transcription.tsx +126 -0
- package/components/ai-elements/voice-selector.tsx +525 -0
- package/components/ai-elements/web-preview.tsx +282 -0
- package/components/audit-log-list.tsx +114 -0
- package/components/chat/EmptyPreviewState.tsx +12 -0
- package/components/chat/KnowledgePickerDialog.tsx +464 -0
- package/components/chat/KnowledgePreview.tsx +70 -0
- package/components/chat/KnowledgePreviewPanel.tsx +86 -0
- package/components/chat/MentionInput.tsx +309 -0
- package/components/chat/OrganizeDialog.tsx +258 -0
- package/components/chat/RecommendationBanner.tsx +94 -0
- package/components/chat/SaveToKnowledgeDialog.tsx +193 -0
- package/components/chat/SkillSelector.tsx +305 -0
- package/components/chat/SkillSwitcher.tsx +163 -0
- package/components/client-layout.tsx +15 -0
- package/components/knowledge/KnowledgeMetadataPanel.tsx +293 -0
- package/components/layout-wrapper.tsx +18 -0
- package/components/mobile-layout.tsx +62 -0
- package/components/scheduled-task-list.tsx +356 -0
- package/components/setup-guide.tsx +484 -0
- package/components/sub-nav.tsx +54 -0
- package/components/ticket-detail-content.tsx +383 -0
- package/components/ticket-list.tsx +366 -0
- package/components/top-nav.tsx +132 -0
- package/components/ui/accordion.tsx +58 -0
- package/components/ui/alert.tsx +59 -0
- package/components/ui/avatar.tsx +50 -0
- package/components/ui/badge.tsx +36 -0
- package/components/ui/button-group.tsx +83 -0
- package/components/ui/button.tsx +57 -0
- package/components/ui/card.tsx +91 -0
- package/components/ui/carousel.tsx +262 -0
- package/components/ui/collapsible.tsx +11 -0
- package/components/ui/command.tsx +153 -0
- package/components/ui/dialog.tsx +122 -0
- package/components/ui/dropdown-menu.tsx +200 -0
- package/components/ui/hover-card.tsx +29 -0
- package/components/ui/input-group.tsx +170 -0
- package/components/ui/input.tsx +22 -0
- package/components/ui/label.tsx +26 -0
- package/components/ui/popover.tsx +31 -0
- package/components/ui/progress.tsx +28 -0
- package/components/ui/scroll-area.tsx +48 -0
- package/components/ui/select.tsx +174 -0
- package/components/ui/separator.tsx +31 -0
- package/components/ui/spinner.tsx +16 -0
- package/components/ui/switch.tsx +29 -0
- package/components/ui/table.tsx +120 -0
- package/components/ui/tabs.tsx +55 -0
- package/components/ui/textarea.tsx +22 -0
- package/components/ui/tooltip.tsx +30 -0
- package/components/welcome-guide.tsx +182 -0
- package/components.json +24 -0
- package/lib/command-parser.ts +331 -0
- package/lib/dangerous-commands.ts +672 -0
- package/lib/db.ts +2250 -0
- package/lib/feishu-auth.ts +135 -0
- package/lib/file-storage.ts +306 -0
- package/lib/file-tool.ts +583 -0
- package/lib/knowledge-tool.ts +152 -0
- package/lib/knowledge-types.ts +66 -0
- package/lib/market-client.ts +313 -0
- package/lib/market-db.ts +736 -0
- package/lib/market-types.ts +51 -0
- package/lib/memory-tool.ts +211 -0
- package/lib/memory.ts +197 -0
- package/lib/pi-config.ts +436 -0
- package/lib/pi-session.ts +799 -0
- package/lib/pinyin.ts +13 -0
- package/lib/recommendation.ts +227 -0
- package/lib/risk-estimator.ts +350 -0
- package/lib/scheduled-task-tool.ts +184 -0
- package/lib/scheduler-init.ts +43 -0
- package/lib/scheduler.ts +416 -0
- package/lib/secure-bash-tool.ts +413 -0
- package/lib/skill-engine.ts +396 -0
- package/lib/skill-generator.ts +269 -0
- package/lib/skill-loader.ts +234 -0
- package/lib/skill-tool.ts +188 -0
- package/lib/skill-types.ts +82 -0
- package/lib/skills-init.ts +58 -0
- package/lib/ticket-tool.ts +246 -0
- package/lib/user-skill-types.ts +30 -0
- package/lib/user-skills.ts +362 -0
- package/lib/utils.ts +6 -0
- package/lib/workflow.ts +154 -0
- package/lib/zip-tool.ts +191 -0
- package/next.config.js +8 -0
- package/package.json +106 -0
- package/public/.gitkeep +1 -0
- package/public/icon.svg +1 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,284 @@
|
|
|
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, Edit, History, Play, Pause, Trash2, Terminal, Zap } from 'lucide-react';
|
|
7
|
+
import { Button } from '@/components/ui/button';
|
|
8
|
+
import { Badge } from '@/components/ui/badge';
|
|
9
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
10
|
+
import type { ScheduledTask } from '@/lib/db';
|
|
11
|
+
|
|
12
|
+
export default function ScheduledTaskDetailPage() {
|
|
13
|
+
const params = useParams();
|
|
14
|
+
const router = useRouter();
|
|
15
|
+
const taskId = params.id as string;
|
|
16
|
+
|
|
17
|
+
const [task, setTask] = useState<ScheduledTask | null>(null);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
const [error, setError] = useState<string | null>(null);
|
|
20
|
+
const [executing, setExecuting] = useState(false);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
async function fetchTask() {
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(`/api/scheduled-tasks/${taskId}`);
|
|
26
|
+
if (!response.ok) throw new Error('获取任务失败');
|
|
27
|
+
|
|
28
|
+
const data = await response.json();
|
|
29
|
+
setTask(data.task);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
setError(err instanceof Error ? err.message : '加载失败');
|
|
32
|
+
} finally {
|
|
33
|
+
setLoading(false);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fetchTask();
|
|
38
|
+
}, [taskId]);
|
|
39
|
+
|
|
40
|
+
async function toggleTask() {
|
|
41
|
+
if (!task) return;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetch(`/api/scheduled-tasks/${taskId}/toggle`, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { 'Content-Type': 'application/json' },
|
|
47
|
+
body: JSON.stringify({ userId: 'user' }),
|
|
48
|
+
});
|
|
49
|
+
if (!response.ok) throw new Error('切换状态失败');
|
|
50
|
+
|
|
51
|
+
const data = await response.json();
|
|
52
|
+
setTask(data.task);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.error(err);
|
|
55
|
+
alert('切换状态失败');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function executeTask() {
|
|
60
|
+
if (!task) return;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
setExecuting(true);
|
|
64
|
+
const response = await fetch(`/api/scheduled-tasks/${taskId}/execute`, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { 'Content-Type': 'application/json' },
|
|
67
|
+
body: JSON.stringify({ userId: 'user' }),
|
|
68
|
+
});
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
const error = await response.json();
|
|
71
|
+
throw new Error(error.error || '执行失败');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Reset state before navigation to avoid race condition
|
|
75
|
+
setExecuting(false);
|
|
76
|
+
|
|
77
|
+
// Show success and navigate to execution history
|
|
78
|
+
alert('任务已开始执行,正在跳转到执行历史...');
|
|
79
|
+
router.push(`/my/scheduled-tasks/${taskId}/executions`);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.error(err);
|
|
82
|
+
alert(err instanceof Error ? err.message : '执行失败');
|
|
83
|
+
setExecuting(false);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function deleteTask() {
|
|
88
|
+
if (!task || !confirm(`确定删除定时任务 "${task.title}" 吗?`)) return;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const response = await fetch(`/api/scheduled-tasks/${taskId}?deletedBy=user`, {
|
|
92
|
+
method: 'DELETE',
|
|
93
|
+
});
|
|
94
|
+
if (!response.ok) throw new Error('删除失败');
|
|
95
|
+
|
|
96
|
+
router.push('/my/scheduled-tasks');
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error(err);
|
|
99
|
+
alert('删除失败');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (loading) {
|
|
104
|
+
return (
|
|
105
|
+
<div className="flex items-center justify-center py-16">
|
|
106
|
+
<div className="flex flex-col items-center gap-4">
|
|
107
|
+
<div className="w-10 h-10 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
|
|
108
|
+
<p className="text-muted-foreground">加载中...</p>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (error || !task) {
|
|
115
|
+
return (
|
|
116
|
+
<div className="bg-destructive/10 border border-destructive/20 rounded-xl p-6">
|
|
117
|
+
<p className="text-destructive">❌ {error || '任务不存在'}</p>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const statusConfig = {
|
|
123
|
+
active: { label: '运行中', className: 'bg-green-500/10 text-green-700 border-green-500/20' },
|
|
124
|
+
paused: { label: '已暂停', className: 'bg-amber-500/10 text-amber-700 border-amber-500/20' },
|
|
125
|
+
disabled: { label: '已禁用', className: 'bg-gray-500/10 text-gray-700 border-gray-500/20' },
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div className="p-6">
|
|
130
|
+
<div className="max-w-4xl mx-auto">
|
|
131
|
+
{/* 页面标题 */}
|
|
132
|
+
<div className="flex items-center justify-between mb-6">
|
|
133
|
+
<div className="flex items-center gap-4">
|
|
134
|
+
<Button variant="ghost" size="icon" asChild>
|
|
135
|
+
<Link href="/my/scheduled-tasks">
|
|
136
|
+
<ArrowLeft className="h-5 w-5" />
|
|
137
|
+
</Link>
|
|
138
|
+
</Button>
|
|
139
|
+
<div>
|
|
140
|
+
<h1 className="text-2xl font-bold">{task.title}</h1>
|
|
141
|
+
<p className="text-muted-foreground text-sm mt-1">{task.id}</p>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
<div className="flex gap-2">
|
|
145
|
+
<Button
|
|
146
|
+
variant="outline"
|
|
147
|
+
size="icon"
|
|
148
|
+
onClick={toggleTask}
|
|
149
|
+
title={task.status === 'active' ? '暂停' : '启动'}
|
|
150
|
+
>
|
|
151
|
+
{task.status === 'active' ? (
|
|
152
|
+
<Pause className="h-4 w-4 text-amber-600" />
|
|
153
|
+
) : (
|
|
154
|
+
<Play className="h-4 w-4 text-green-600" />
|
|
155
|
+
)}
|
|
156
|
+
</Button>
|
|
157
|
+
<Button
|
|
158
|
+
variant="default"
|
|
159
|
+
size="icon"
|
|
160
|
+
onClick={executeTask}
|
|
161
|
+
disabled={executing}
|
|
162
|
+
title="立即执行"
|
|
163
|
+
>
|
|
164
|
+
{executing ? (
|
|
165
|
+
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
|
166
|
+
) : (
|
|
167
|
+
<Zap className="h-4 w-4" />
|
|
168
|
+
)}
|
|
169
|
+
</Button>
|
|
170
|
+
<Button variant="outline" size="icon" asChild title="执行历史">
|
|
171
|
+
<Link href={`/my/scheduled-tasks/${taskId}/executions`}>
|
|
172
|
+
<History className="h-4 w-4" />
|
|
173
|
+
</Link>
|
|
174
|
+
</Button>
|
|
175
|
+
<Button variant="outline" size="icon" asChild title="编辑">
|
|
176
|
+
<Link href={`/my/scheduled-tasks/${taskId}/edit`}>
|
|
177
|
+
<Edit className="h-4 w-4" />
|
|
178
|
+
</Link>
|
|
179
|
+
</Button>
|
|
180
|
+
<Button
|
|
181
|
+
variant="outline"
|
|
182
|
+
size="icon"
|
|
183
|
+
onClick={deleteTask}
|
|
184
|
+
className="text-destructive hover:text-destructive"
|
|
185
|
+
title="删除"
|
|
186
|
+
>
|
|
187
|
+
<Trash2 className="h-4 w-4" />
|
|
188
|
+
</Button>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div className="grid gap-6">
|
|
193
|
+
{/* 基本信息卡片 */}
|
|
194
|
+
<Card>
|
|
195
|
+
<CardHeader>
|
|
196
|
+
<CardTitle>基本信息</CardTitle>
|
|
197
|
+
</CardHeader>
|
|
198
|
+
<CardContent>
|
|
199
|
+
<dl className="grid grid-cols-2 gap-4">
|
|
200
|
+
<div>
|
|
201
|
+
<dt className="text-sm text-muted-foreground">状态</dt>
|
|
202
|
+
<dd className="mt-1">
|
|
203
|
+
<Badge className={statusConfig[task.status].className}>
|
|
204
|
+
{statusConfig[task.status].label}
|
|
205
|
+
</Badge>
|
|
206
|
+
</dd>
|
|
207
|
+
</div>
|
|
208
|
+
<div>
|
|
209
|
+
<dt className="text-sm text-muted-foreground">创建者</dt>
|
|
210
|
+
<dd className="mt-1">{task.createdBy}</dd>
|
|
211
|
+
</div>
|
|
212
|
+
<div>
|
|
213
|
+
<dt className="text-sm text-muted-foreground">创建时间</dt>
|
|
214
|
+
<dd className="mt-1">{new Date(task.createdAt).toLocaleString('zh-CN')}</dd>
|
|
215
|
+
</div>
|
|
216
|
+
<div>
|
|
217
|
+
<dt className="text-sm text-muted-foreground">更新时间</dt>
|
|
218
|
+
<dd className="mt-1">{new Date(task.updatedAt).toLocaleString('zh-CN')}</dd>
|
|
219
|
+
</div>
|
|
220
|
+
{task.description && (
|
|
221
|
+
<div className="col-span-2">
|
|
222
|
+
<dt className="text-sm text-muted-foreground">描述</dt>
|
|
223
|
+
<dd className="mt-1">{task.description}</dd>
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
</dl>
|
|
227
|
+
</CardContent>
|
|
228
|
+
</Card>
|
|
229
|
+
|
|
230
|
+
{/* 命令配置卡片 */}
|
|
231
|
+
<Card>
|
|
232
|
+
<CardHeader>
|
|
233
|
+
<CardTitle className="flex items-center gap-2">
|
|
234
|
+
<Terminal className="h-5 w-5" />
|
|
235
|
+
执行命令
|
|
236
|
+
</CardTitle>
|
|
237
|
+
<CardDescription>定时任务将执行的命令</CardDescription>
|
|
238
|
+
</CardHeader>
|
|
239
|
+
<CardContent>
|
|
240
|
+
<pre className="bg-muted p-4 rounded-lg overflow-x-auto text-sm font-mono whitespace-pre-wrap">
|
|
241
|
+
{task.command}
|
|
242
|
+
</pre>
|
|
243
|
+
</CardContent>
|
|
244
|
+
</Card>
|
|
245
|
+
|
|
246
|
+
{/* 调度配置卡片 */}
|
|
247
|
+
<Card>
|
|
248
|
+
<CardHeader>
|
|
249
|
+
<CardTitle>调度配置</CardTitle>
|
|
250
|
+
</CardHeader>
|
|
251
|
+
<CardContent>
|
|
252
|
+
<dl className="grid grid-cols-2 gap-4">
|
|
253
|
+
<div>
|
|
254
|
+
<dt className="text-sm text-muted-foreground">Cron 表达式</dt>
|
|
255
|
+
<dd className="mt-1">
|
|
256
|
+
<code className="bg-muted px-2 py-1 rounded text-sm font-mono">
|
|
257
|
+
{task.scheduleExpression}
|
|
258
|
+
</code>
|
|
259
|
+
</dd>
|
|
260
|
+
</div>
|
|
261
|
+
<div>
|
|
262
|
+
<dt className="text-sm text-muted-foreground">下次执行</dt>
|
|
263
|
+
<dd className="mt-1">
|
|
264
|
+
{task.nextRunAt ? (
|
|
265
|
+
new Date(task.nextRunAt).toLocaleString('zh-CN')
|
|
266
|
+
) : (
|
|
267
|
+
<span className="text-muted-foreground">未设置</span>
|
|
268
|
+
)}
|
|
269
|
+
</dd>
|
|
270
|
+
</div>
|
|
271
|
+
{task.lastRunAt && (
|
|
272
|
+
<div>
|
|
273
|
+
<dt className="text-sm text-muted-foreground">上次执行</dt>
|
|
274
|
+
<dd className="mt-1">{new Date(task.lastRunAt).toLocaleString('zh-CN')}</dd>
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
277
|
+
</dl>
|
|
278
|
+
</CardContent>
|
|
279
|
+
</Card>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useRouter } 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 { Textarea } from '@/components/ui/textarea';
|
|
10
|
+
import { Label } from '@/components/ui/label';
|
|
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
|
+
|
|
20
|
+
// Cron presets
|
|
21
|
+
const CRON_PRESETS = {
|
|
22
|
+
hourly: { label: '每小时', expression: '0 * * * *' },
|
|
23
|
+
every_6_hours: { label: '每6小时', expression: '0 */6 * * *' },
|
|
24
|
+
daily_8am: { label: '每天上午8点', expression: '0 8 * * *' },
|
|
25
|
+
daily_9am: { label: '每天上午9点', expression: '0 9 * * *' },
|
|
26
|
+
daily_midnight: { label: '每天凌晨', expression: '0 0 * * *' },
|
|
27
|
+
weekly_monday_9am: { label: '每周一早上9点', expression: '0 9 * * 1' },
|
|
28
|
+
weekly_friday_6pm: { label: '每周五下午6点', expression: '0 18 * * 5' },
|
|
29
|
+
monthly_1st_midnight: { label: '每月1号凌晨', expression: '0 0 1 * *' },
|
|
30
|
+
monthly_15th_9am: { label: '每月15号上午9点', expression: '0 9 15 * *' },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default function NewScheduledTaskPage() {
|
|
34
|
+
const router = useRouter();
|
|
35
|
+
const [loading, setLoading] = useState(false);
|
|
36
|
+
const [error, setError] = useState<string | null>(null);
|
|
37
|
+
|
|
38
|
+
// 表单状态
|
|
39
|
+
const [formData, setFormData] = useState({
|
|
40
|
+
title: '',
|
|
41
|
+
description: '',
|
|
42
|
+
command: '',
|
|
43
|
+
scheduleType: 'cron' as 'cron' | 'interval' | 'once',
|
|
44
|
+
scheduleExpression: CRON_PRESETS.daily_9am.expression,
|
|
45
|
+
cronPreset: '',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const handleInputChange = (field: string, value: string) => {
|
|
49
|
+
setFormData(prev => ({ ...prev, [field]: value }));
|
|
50
|
+
|
|
51
|
+
// 如果选择的是预设,同步更新 scheduleExpression
|
|
52
|
+
if (field === 'cronPreset' && value) {
|
|
53
|
+
const preset = CRON_PRESETS[value as keyof typeof CRON_PRESETS];
|
|
54
|
+
if (preset) {
|
|
55
|
+
setFormData(prev => ({ ...prev, scheduleExpression: preset.expression }));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
setLoading(true);
|
|
63
|
+
setError(null);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const response = await fetch('/api/scheduled-tasks', {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: { 'Content-Type': 'application/json' },
|
|
69
|
+
body: JSON.stringify({
|
|
70
|
+
...formData,
|
|
71
|
+
createdBy: 'user',
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
const data = await response.json();
|
|
77
|
+
throw new Error(data.error || '创建失败');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const data = await response.json();
|
|
81
|
+
router.push(`/my/scheduled-tasks/${data.task.id}`);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
setError(err instanceof Error ? err.message : '创建失败');
|
|
84
|
+
} finally {
|
|
85
|
+
setLoading(false);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className="p-6">
|
|
91
|
+
<div className="max-w-3xl mx-auto">
|
|
92
|
+
{/* 页面标题 */}
|
|
93
|
+
<div className="flex items-center gap-4 mb-6">
|
|
94
|
+
<Button variant="ghost" size="icon" asChild>
|
|
95
|
+
<Link href="/my/scheduled-tasks">
|
|
96
|
+
<ArrowLeft className="h-5 w-5" />
|
|
97
|
+
</Link>
|
|
98
|
+
</Button>
|
|
99
|
+
<div>
|
|
100
|
+
<h1 className="text-2xl font-bold">新建定时任务</h1>
|
|
101
|
+
<p className="text-muted-foreground text-sm mt-1">配置定时执行的命令任务</p>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{error && (
|
|
106
|
+
<div className="bg-destructive/10 border border-destructive/20 rounded-xl p-4 mb-6">
|
|
107
|
+
<p className="text-destructive">❌ {error}</p>
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
112
|
+
{/* 基本信息 */}
|
|
113
|
+
<Card>
|
|
114
|
+
<CardHeader>
|
|
115
|
+
<CardTitle>基本信息</CardTitle>
|
|
116
|
+
<CardDescription>设置定时任务的基本属性</CardDescription>
|
|
117
|
+
</CardHeader>
|
|
118
|
+
<CardContent className="space-y-4">
|
|
119
|
+
<div>
|
|
120
|
+
<Label htmlFor="title">任务标题 *</Label>
|
|
121
|
+
<Input
|
|
122
|
+
id="title"
|
|
123
|
+
value={formData.title}
|
|
124
|
+
onChange={(e) => handleInputChange('title', e.target.value)}
|
|
125
|
+
placeholder="例如:每天清理临时文件"
|
|
126
|
+
required
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
<div>
|
|
130
|
+
<Label htmlFor="description">描述</Label>
|
|
131
|
+
<Textarea
|
|
132
|
+
id="description"
|
|
133
|
+
value={formData.description}
|
|
134
|
+
onChange={(e) => handleInputChange('description', e.target.value)}
|
|
135
|
+
placeholder="详细描述此定时任务的用途..."
|
|
136
|
+
rows={3}
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
</CardContent>
|
|
140
|
+
</Card>
|
|
141
|
+
|
|
142
|
+
{/* 命令配置 */}
|
|
143
|
+
<Card>
|
|
144
|
+
<CardHeader>
|
|
145
|
+
<CardTitle className="flex items-center gap-2">
|
|
146
|
+
<Terminal className="h-5 w-5" />
|
|
147
|
+
命令配置
|
|
148
|
+
</CardTitle>
|
|
149
|
+
<CardDescription>设置要执行的命令</CardDescription>
|
|
150
|
+
</CardHeader>
|
|
151
|
+
<CardContent className="space-y-4">
|
|
152
|
+
<div>
|
|
153
|
+
<Label htmlFor="command">执行命令 *</Label>
|
|
154
|
+
<Textarea
|
|
155
|
+
id="command"
|
|
156
|
+
value={formData.command}
|
|
157
|
+
onChange={(e) => handleInputChange('command', e.target.value)}
|
|
158
|
+
placeholder="例如:rm -rf /tmp/*.log 或 kubectl get pods -n default"
|
|
159
|
+
rows={3}
|
|
160
|
+
required
|
|
161
|
+
className="font-mono"
|
|
162
|
+
/>
|
|
163
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
164
|
+
支持任意 shell 命令,危险命令会被自动拦截并创建工单
|
|
165
|
+
</p>
|
|
166
|
+
</div>
|
|
167
|
+
</CardContent>
|
|
168
|
+
</Card>
|
|
169
|
+
|
|
170
|
+
{/* 调度配置 */}
|
|
171
|
+
<Card>
|
|
172
|
+
<CardHeader>
|
|
173
|
+
<CardTitle>调度配置</CardTitle>
|
|
174
|
+
<CardDescription>设置任务执行的时间和频率</CardDescription>
|
|
175
|
+
</CardHeader>
|
|
176
|
+
<CardContent className="space-y-4">
|
|
177
|
+
<div>
|
|
178
|
+
<Label htmlFor="preset">常用预设</Label>
|
|
179
|
+
<Select value={formData.cronPreset} onValueChange={(v) => handleInputChange('cronPreset', v)}>
|
|
180
|
+
<SelectTrigger id="preset">
|
|
181
|
+
<SelectValue placeholder="选择预设..." />
|
|
182
|
+
</SelectTrigger>
|
|
183
|
+
<SelectContent>
|
|
184
|
+
{Object.entries(CRON_PRESETS).map(([key, preset]) => (
|
|
185
|
+
<SelectItem key={key} value={key}>
|
|
186
|
+
{preset.label}
|
|
187
|
+
</SelectItem>
|
|
188
|
+
))}
|
|
189
|
+
</SelectContent>
|
|
190
|
+
</Select>
|
|
191
|
+
</div>
|
|
192
|
+
<div>
|
|
193
|
+
<Label htmlFor="expression">Cron 表达式 *</Label>
|
|
194
|
+
<Input
|
|
195
|
+
id="expression"
|
|
196
|
+
value={formData.scheduleExpression}
|
|
197
|
+
onChange={(e) => handleInputChange('scheduleExpression', e.target.value)}
|
|
198
|
+
placeholder="0 9 * * *"
|
|
199
|
+
pattern="^(\S+\s+){4}\S+$"
|
|
200
|
+
required
|
|
201
|
+
className="font-mono"
|
|
202
|
+
/>
|
|
203
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
204
|
+
格式:分 时 日 月 周(例如:0 9 * * * 表示每天早上9点)
|
|
205
|
+
</p>
|
|
206
|
+
</div>
|
|
207
|
+
<div className="bg-muted rounded-lg p-3">
|
|
208
|
+
<p className="text-sm font-medium">Cron 表达式</p>
|
|
209
|
+
<p className="text-sm text-muted-foreground mt-1 font-mono">
|
|
210
|
+
{formData.scheduleExpression}
|
|
211
|
+
</p>
|
|
212
|
+
</div>
|
|
213
|
+
</CardContent>
|
|
214
|
+
</Card>
|
|
215
|
+
|
|
216
|
+
{/* 提交按钮 */}
|
|
217
|
+
<div className="flex gap-3">
|
|
218
|
+
<Button type="submit" disabled={loading}>
|
|
219
|
+
<Save className="mr-2 h-4 w-4" />
|
|
220
|
+
{loading ? '创建中...' : '创建定时任务'}
|
|
221
|
+
</Button>
|
|
222
|
+
<Button type="button" variant="outline" asChild>
|
|
223
|
+
<Link href="/my/scheduled-tasks">取消</Link>
|
|
224
|
+
</Button>
|
|
225
|
+
</div>
|
|
226
|
+
</form>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import { Plus } from 'lucide-react';
|
|
3
|
+
import { ScheduledTaskList } from '@/components/scheduled-task-list';
|
|
4
|
+
import { Button } from '@/components/ui/button';
|
|
5
|
+
|
|
6
|
+
export default function ScheduledTasksPage() {
|
|
7
|
+
return (
|
|
8
|
+
<div className="p-6">
|
|
9
|
+
<div className="max-w-7xl mx-auto">
|
|
10
|
+
<div className="flex items-center justify-between mb-6">
|
|
11
|
+
<div>
|
|
12
|
+
<h1 className="text-2xl font-bold">定时任务</h1>
|
|
13
|
+
<p className="text-muted-foreground text-sm mt-1">配置和管理定时执行的命令任务</p>
|
|
14
|
+
</div>
|
|
15
|
+
<Button asChild>
|
|
16
|
+
<Link href="/my/scheduled-tasks/new">
|
|
17
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
18
|
+
新建定时任务
|
|
19
|
+
</Link>
|
|
20
|
+
</Button>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<ScheduledTaskList />
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|