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,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 创建定时任务工具
|
|
3
|
+
* 让 AI 可以通过对话创建定时任务
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Type } from '@sinclair/typebox';
|
|
7
|
+
import type { ToolDefinition, AgentToolResult, ExtensionContext } from '@mariozechner/pi-coding-agent';
|
|
8
|
+
import { createScheduledTask, createAuditLog } from './db';
|
|
9
|
+
import { getNextRunTime, isValidCronExpression, parseScheduleDescription } from './scheduler';
|
|
10
|
+
import { getCurrentSessionId } from './pi-session';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 创建定时任务工具的参数 Schema
|
|
14
|
+
*/
|
|
15
|
+
const createScheduledTaskSchema = Type.Object({
|
|
16
|
+
title: Type.String({
|
|
17
|
+
description: '任务标题,简短描述这个定时任务',
|
|
18
|
+
}),
|
|
19
|
+
command: Type.String({
|
|
20
|
+
description: '要执行的命令',
|
|
21
|
+
}),
|
|
22
|
+
schedule: Type.String({
|
|
23
|
+
description: "调度表达式,可以是自然语言(如'每天早上9点')或 cron 表达式(如'0 9 * * *')",
|
|
24
|
+
}),
|
|
25
|
+
description: Type.Optional(Type.String({
|
|
26
|
+
description: '任务详细描述',
|
|
27
|
+
})),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 创建定时任务工具的详情类型
|
|
32
|
+
*/
|
|
33
|
+
interface CreateScheduledTaskDetails {
|
|
34
|
+
success: boolean;
|
|
35
|
+
task?: {
|
|
36
|
+
id: string;
|
|
37
|
+
title: string;
|
|
38
|
+
command: string;
|
|
39
|
+
scheduleExpression: string;
|
|
40
|
+
nextRunAt?: string;
|
|
41
|
+
status: string;
|
|
42
|
+
};
|
|
43
|
+
error?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 创建定时任务工具定义
|
|
48
|
+
*/
|
|
49
|
+
export const createScheduledTaskTool: ToolDefinition<typeof createScheduledTaskSchema, CreateScheduledTaskDetails> = {
|
|
50
|
+
name: 'create_scheduled_task',
|
|
51
|
+
label: 'Create Scheduled Task',
|
|
52
|
+
description: `创建一个定时任务,用于在指定时间自动执行命令。
|
|
53
|
+
|
|
54
|
+
使用场景:
|
|
55
|
+
- 用户要求定时执行某个命令
|
|
56
|
+
- 用户要求周期性执行某个操作
|
|
57
|
+
- 用户说"每天..."、"每周..."、"每小时..."等
|
|
58
|
+
|
|
59
|
+
调度表达式的常用格式:
|
|
60
|
+
- "每小时" 或 "0 * * * *"
|
|
61
|
+
- "每天早上9点" 或 "0 9 * * *"
|
|
62
|
+
- "每周一早上9点" 或 "0 9 * * 1"
|
|
63
|
+
- "每月1号凌晨" 或 "0 0 1 * *"
|
|
64
|
+
|
|
65
|
+
你可以直接使用自然语言描述,如"每天早上9点",系统会自动转换为 cron 表达式。`,
|
|
66
|
+
|
|
67
|
+
parameters: createScheduledTaskSchema,
|
|
68
|
+
|
|
69
|
+
async execute(
|
|
70
|
+
toolCallId: string,
|
|
71
|
+
params: any,
|
|
72
|
+
signal: AbortSignal | undefined,
|
|
73
|
+
onUpdate: ((result: AgentToolResult<CreateScheduledTaskDetails>) => void) | undefined,
|
|
74
|
+
ctx: ExtensionContext
|
|
75
|
+
): Promise<AgentToolResult<CreateScheduledTaskDetails>> {
|
|
76
|
+
try {
|
|
77
|
+
const { title, command, schedule, description } = params;
|
|
78
|
+
|
|
79
|
+
// 解析调度表达式
|
|
80
|
+
let scheduleExpression = schedule;
|
|
81
|
+
const parsed = parseScheduleDescription(schedule);
|
|
82
|
+
if (parsed) {
|
|
83
|
+
scheduleExpression = parsed.expression;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 验证 cron 表达式
|
|
87
|
+
if (!isValidCronExpression(scheduleExpression)) {
|
|
88
|
+
const errorResult: AgentToolResult<CreateScheduledTaskDetails> = {
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: 'text',
|
|
92
|
+
text: `❌ 无效的调度表达式: ${schedule}\n\n支持的格式:'每天早上9点'、'每周一9点'、'0 9 * * *' 等`,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
details: {
|
|
96
|
+
success: false,
|
|
97
|
+
error: `无效的调度表达式: ${schedule}`,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
onUpdate?.(errorResult);
|
|
101
|
+
return errorResult;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 计算下次执行时间
|
|
105
|
+
const nextRunAt = getNextRunTime(scheduleExpression);
|
|
106
|
+
|
|
107
|
+
// 创建定时任务
|
|
108
|
+
const task = await createScheduledTask({
|
|
109
|
+
title,
|
|
110
|
+
description: description || '',
|
|
111
|
+
command,
|
|
112
|
+
status: 'active',
|
|
113
|
+
scheduleType: 'cron',
|
|
114
|
+
scheduleExpression,
|
|
115
|
+
nextRunAt: nextRunAt || undefined,
|
|
116
|
+
createdBy: 'ai-assistant',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// 创建审计日志
|
|
120
|
+
await createAuditLog({
|
|
121
|
+
action: 'scheduled_task_created',
|
|
122
|
+
userId: 'ai-assistant',
|
|
123
|
+
sessionId: getCurrentSessionId() || undefined,
|
|
124
|
+
details: {
|
|
125
|
+
taskId: task.id,
|
|
126
|
+
title,
|
|
127
|
+
command,
|
|
128
|
+
scheduleExpression,
|
|
129
|
+
createdByConversation: true,
|
|
130
|
+
},
|
|
131
|
+
status: 'success',
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const result: AgentToolResult<CreateScheduledTaskDetails> = {
|
|
135
|
+
content: [
|
|
136
|
+
{
|
|
137
|
+
type: 'text',
|
|
138
|
+
text: `✅ 定时任务已创建成功!
|
|
139
|
+
|
|
140
|
+
**任务ID:** ${task.id}
|
|
141
|
+
**标题:** ${title}
|
|
142
|
+
**命令:** \`${command}\`
|
|
143
|
+
**调度:** ${scheduleExpression}
|
|
144
|
+
**下次执行:** ${nextRunAt?.toLocaleString('zh-CN') || '待计算'}
|
|
145
|
+
|
|
146
|
+
你可以在[定时任务](/scheduled-tasks)页面查看和管理这个任务。`,
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
details: {
|
|
150
|
+
success: true,
|
|
151
|
+
task: {
|
|
152
|
+
id: task.id,
|
|
153
|
+
title: task.title,
|
|
154
|
+
command: task.command,
|
|
155
|
+
scheduleExpression: task.scheduleExpression,
|
|
156
|
+
nextRunAt: task.nextRunAt?.toISOString(),
|
|
157
|
+
status: task.status,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
onUpdate?.(result);
|
|
163
|
+
return result;
|
|
164
|
+
} catch (error: any) {
|
|
165
|
+
console.error('Failed to create scheduled task:', error);
|
|
166
|
+
const errorResult: AgentToolResult<CreateScheduledTaskDetails> = {
|
|
167
|
+
content: [
|
|
168
|
+
{
|
|
169
|
+
type: 'text',
|
|
170
|
+
text: `❌ 创建定时任务失败: ${error.message}`,
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
details: {
|
|
174
|
+
success: false,
|
|
175
|
+
error: error.message,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
onUpdate?.(errorResult);
|
|
179
|
+
return errorResult;
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export default createScheduledTaskTool;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 调度器初始化模块
|
|
3
|
+
* 在应用启动时初始化定时任务调度器
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { startScheduler, initializeScheduledTaskNextRunTimes } from '@/lib/scheduler';
|
|
7
|
+
|
|
8
|
+
// 标记调度器是否已初始化
|
|
9
|
+
let initialized = false;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 初始化调度器
|
|
13
|
+
* 此函数应在应用启动时调用一次
|
|
14
|
+
*/
|
|
15
|
+
export async function initializeScheduler(): Promise<void> {
|
|
16
|
+
if (initialized) {
|
|
17
|
+
console.log('Scheduler already initialized');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
console.log('Initializing scheduler...');
|
|
23
|
+
|
|
24
|
+
// 首先初始化所有活动任务的下次执行时间
|
|
25
|
+
await initializeScheduledTaskNextRunTimes();
|
|
26
|
+
|
|
27
|
+
// 启动调度器(每分钟检查一次)
|
|
28
|
+
startScheduler(60000);
|
|
29
|
+
|
|
30
|
+
initialized = true;
|
|
31
|
+
console.log('Scheduler initialized successfully');
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('Failed to initialize scheduler:', error);
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 获取调度器初始化状态
|
|
40
|
+
*/
|
|
41
|
+
export function isSchedulerInitialized(): boolean {
|
|
42
|
+
return initialized;
|
|
43
|
+
}
|
package/lib/scheduler.ts
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 调度器模块 - 负责定时任务的调度和执行
|
|
3
|
+
*
|
|
4
|
+
* 设计理念:
|
|
5
|
+
* - 定时任务是预定义的命令执行计划
|
|
6
|
+
* - 每次执行都创建完整的审计日志
|
|
7
|
+
* - 执行记录与工单系统关联,但不是每次都创建新工单
|
|
8
|
+
* - 支持通用命令执行,不限于特定类型
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import parser from 'cron-parser';
|
|
12
|
+
import { exec } from 'child_process';
|
|
13
|
+
import { promisify } from 'util';
|
|
14
|
+
import {
|
|
15
|
+
getScheduledTasksDueForExecution,
|
|
16
|
+
updateScheduledTask,
|
|
17
|
+
createTaskExecution,
|
|
18
|
+
updateTaskExecution,
|
|
19
|
+
createTicket,
|
|
20
|
+
createAuditLog,
|
|
21
|
+
} from './db';
|
|
22
|
+
import { ScheduledTask } from './db';
|
|
23
|
+
|
|
24
|
+
const execAsync = promisify(exec);
|
|
25
|
+
|
|
26
|
+
// 调度器状态
|
|
27
|
+
let schedulerInterval: NodeJS.Timeout | null = null;
|
|
28
|
+
let isRunning = false;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Cron 预设表达式
|
|
32
|
+
*/
|
|
33
|
+
export const CRON_PRESETS = {
|
|
34
|
+
hourly: {
|
|
35
|
+
label: '每小时',
|
|
36
|
+
expression: '0 * * * *',
|
|
37
|
+
description: '每小时的第0分钟执行',
|
|
38
|
+
},
|
|
39
|
+
every_6_hours: {
|
|
40
|
+
label: '每6小时',
|
|
41
|
+
expression: '0 */6 * * *',
|
|
42
|
+
description: '每6小时执行一次',
|
|
43
|
+
},
|
|
44
|
+
daily_8am: {
|
|
45
|
+
label: '每天上午8点',
|
|
46
|
+
expression: '0 8 * * *',
|
|
47
|
+
description: '每天早上8点执行',
|
|
48
|
+
},
|
|
49
|
+
daily_9am: {
|
|
50
|
+
label: '每天上午9点',
|
|
51
|
+
expression: '0 9 * * *',
|
|
52
|
+
description: '每天早上9点执行',
|
|
53
|
+
},
|
|
54
|
+
daily_midnight: {
|
|
55
|
+
label: '每天凌晨',
|
|
56
|
+
expression: '0 0 * * *',
|
|
57
|
+
description: '每天凌晨执行',
|
|
58
|
+
},
|
|
59
|
+
weekly_monday_9am: {
|
|
60
|
+
label: '每周一早上9点',
|
|
61
|
+
expression: '0 9 * * 1',
|
|
62
|
+
description: '每周一早上9点执行',
|
|
63
|
+
},
|
|
64
|
+
weekly_friday_6pm: {
|
|
65
|
+
label: '每周五下午6点',
|
|
66
|
+
expression: '0 18 * * 5',
|
|
67
|
+
description: '每周五下午6点执行',
|
|
68
|
+
},
|
|
69
|
+
monthly_1st_midnight: {
|
|
70
|
+
label: '每月1号凌晨',
|
|
71
|
+
expression: '0 0 1 * *',
|
|
72
|
+
description: '每月1号凌晨执行',
|
|
73
|
+
},
|
|
74
|
+
monthly_15th_9am: {
|
|
75
|
+
label: '每月15号上午9点',
|
|
76
|
+
expression: '0 9 15 * *',
|
|
77
|
+
description: '每月15号上午9点执行',
|
|
78
|
+
},
|
|
79
|
+
} as const;
|
|
80
|
+
|
|
81
|
+
export type CronPresetKey = keyof typeof CRON_PRESETS;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 计算 cron 表达式的下次执行时间
|
|
85
|
+
*/
|
|
86
|
+
export function getNextRunTime(cronExpression: string): Date | null {
|
|
87
|
+
try {
|
|
88
|
+
const interval = parser.parse(cronExpression);
|
|
89
|
+
return interval.next().toDate();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error('Invalid cron expression:', cronExpression, error);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 验证 cron 表达式是否有效
|
|
98
|
+
*/
|
|
99
|
+
export function isValidCronExpression(expression: string): boolean {
|
|
100
|
+
try {
|
|
101
|
+
parser.parse(expression);
|
|
102
|
+
return true;
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 解析自然语言调度描述
|
|
110
|
+
*/
|
|
111
|
+
export function parseScheduleDescription(description: string): { expression: string; type: 'cron' } | null {
|
|
112
|
+
const desc = description.toLowerCase().trim();
|
|
113
|
+
|
|
114
|
+
// 每小时
|
|
115
|
+
if (desc.includes('每小时') || desc.includes('每个小时') || desc.match(/每\s*\d+\s*分钟/)) {
|
|
116
|
+
return { expression: CRON_PRESETS.hourly.expression, type: 'cron' };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 每天X点
|
|
120
|
+
const dailyMatch = desc.match(/(?:每天|每日)\s*(上午|下午|早上)?\s*(\d{1,2})\s*点/);
|
|
121
|
+
if (dailyMatch) {
|
|
122
|
+
let hour = parseInt(dailyMatch[2]);
|
|
123
|
+
if (dailyMatch[1] === '下午' && hour !== 12) hour += 12;
|
|
124
|
+
if (dailyMatch[1] === '上午' && hour === 12) hour = 0;
|
|
125
|
+
if (hour >= 0 && hour <= 23) {
|
|
126
|
+
return { expression: `0 ${hour} * * *`, type: 'cron' };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 匹配预设
|
|
131
|
+
for (const [, preset] of Object.entries(CRON_PRESETS)) {
|
|
132
|
+
if (desc.includes(preset.label) || desc.includes(preset.description)) {
|
|
133
|
+
return { expression: preset.expression, type: 'cron' };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 每周X
|
|
138
|
+
const weekMatch = desc.match(/每周([一二三四五六日天])\s*(?:早上|上午|下午)?\s*(\d{1,2})?\s*点?/);
|
|
139
|
+
if (weekMatch) {
|
|
140
|
+
const dayMap: Record<string, number> = { '一': 1, '二': 2, '三': 3, '四': 4, '五': 5, '六': 6, '日': 0, '天': 0 };
|
|
141
|
+
const day = dayMap[weekMatch[1]] ?? 1;
|
|
142
|
+
const hour = weekMatch[2] ? parseInt(weekMatch[2]) : 9;
|
|
143
|
+
return { expression: `0 ${hour} * * ${day}`, type: 'cron' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 每月X号
|
|
147
|
+
const monthlyMatch = desc.match(/(?:每月|每月的)\s*(\d{1,2})号/);
|
|
148
|
+
if (monthlyMatch) {
|
|
149
|
+
const day = parseInt(monthlyMatch[1]);
|
|
150
|
+
if (day >= 1 && day <= 31) {
|
|
151
|
+
const hourMatch = desc.match(/(\d{1,2})\s*点/);
|
|
152
|
+
const hour = hourMatch ? parseInt(hourMatch[1]) : 0;
|
|
153
|
+
return { expression: `0 ${hour} ${day} * *`, type: 'cron' };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 尝试直接解析为 cron 表达式
|
|
158
|
+
if (isValidCronExpression(desc)) {
|
|
159
|
+
return { expression: desc, type: 'cron' };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 从任务中提取命令
|
|
167
|
+
*/
|
|
168
|
+
function extractCommand(task: ScheduledTask): string {
|
|
169
|
+
// 直接返回 command 字段
|
|
170
|
+
return task.command;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 执行单个定时任务
|
|
175
|
+
*/
|
|
176
|
+
async function executeScheduledTask(task: ScheduledTask): Promise<void> {
|
|
177
|
+
console.log(`[scheduler] Executing task: ${task.title} (${task.id})`);
|
|
178
|
+
|
|
179
|
+
const startTime = Date.now();
|
|
180
|
+
const command = extractCommand(task);
|
|
181
|
+
|
|
182
|
+
// 创建执行记录
|
|
183
|
+
const execution = await createTaskExecution({
|
|
184
|
+
taskId: task.id,
|
|
185
|
+
status: 'running',
|
|
186
|
+
startedAt: new Date(),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// 创建审计日志 - 任务开始
|
|
190
|
+
await createAuditLog({
|
|
191
|
+
action: 'scheduled_task_start',
|
|
192
|
+
userId: task.createdBy,
|
|
193
|
+
ticketId: undefined,
|
|
194
|
+
details: {
|
|
195
|
+
taskId: task.id,
|
|
196
|
+
taskTitle: task.title,
|
|
197
|
+
command,
|
|
198
|
+
scheduleExpression: task.scheduleExpression,
|
|
199
|
+
},
|
|
200
|
+
status: 'success',
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
// 执行命令
|
|
205
|
+
console.log(`[scheduler] Executing command: ${command}`);
|
|
206
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
207
|
+
timeout: 60000, // 60秒超时
|
|
208
|
+
maxBuffer: 1024 * 1024, // 1MB 缓冲
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const executionTime = Date.now() - startTime;
|
|
212
|
+
const output = stdout || stderr;
|
|
213
|
+
|
|
214
|
+
// 创建工单记录(用于审计追踪)
|
|
215
|
+
const ticket = await createTicket({
|
|
216
|
+
type: 'bash-execute',
|
|
217
|
+
title: `[定时任务] ${task.title}`,
|
|
218
|
+
description: `由定时任务自动执行\n\n**命令:** \`${command}\`\n**执行时间:** ${executionTime}ms`,
|
|
219
|
+
status: 'completed',
|
|
220
|
+
priority: 'medium',
|
|
221
|
+
command,
|
|
222
|
+
output,
|
|
223
|
+
riskLevel: 'medium',
|
|
224
|
+
approvals: [
|
|
225
|
+
{
|
|
226
|
+
id: `auto-${Date.now()}`,
|
|
227
|
+
approver: 'scheduler',
|
|
228
|
+
decision: 'approved',
|
|
229
|
+
comment: '定时任务自动执行',
|
|
230
|
+
timestamp: new Date(),
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
createdBy: 'scheduler',
|
|
234
|
+
aiGenerated: false,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// 更新执行记录为成功
|
|
238
|
+
await updateTaskExecution(execution.id, {
|
|
239
|
+
status: 'completed',
|
|
240
|
+
completedAt: new Date(),
|
|
241
|
+
ticketId: ticket.id,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// 创建审计日志 - 任务完成
|
|
245
|
+
await createAuditLog({
|
|
246
|
+
action: 'scheduled_task_complete',
|
|
247
|
+
userId: task.createdBy,
|
|
248
|
+
ticketId: ticket.id,
|
|
249
|
+
details: {
|
|
250
|
+
taskId: task.id,
|
|
251
|
+
taskTitle: task.title,
|
|
252
|
+
command,
|
|
253
|
+
executionTime,
|
|
254
|
+
outputLength: output?.length || 0,
|
|
255
|
+
},
|
|
256
|
+
status: 'success',
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
console.log(`[scheduler] Task completed: ${task.title}, execution time: ${executionTime}ms`);
|
|
260
|
+
} catch (error: any) {
|
|
261
|
+
const executionTime = Date.now() - startTime;
|
|
262
|
+
const errorMessage = error.message || String(error);
|
|
263
|
+
|
|
264
|
+
console.error(`[scheduler] Task failed: ${task.title}`, errorMessage);
|
|
265
|
+
|
|
266
|
+
// 更新执行记录为失败
|
|
267
|
+
await updateTaskExecution(execution.id, {
|
|
268
|
+
status: 'failed',
|
|
269
|
+
completedAt: new Date(),
|
|
270
|
+
errorMessage,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// 创建审计日志 - 任务失败
|
|
274
|
+
await createAuditLog({
|
|
275
|
+
action: 'scheduled_task_failed',
|
|
276
|
+
userId: task.createdBy,
|
|
277
|
+
ticketId: undefined,
|
|
278
|
+
details: {
|
|
279
|
+
taskId: task.id,
|
|
280
|
+
taskTitle: task.title,
|
|
281
|
+
command,
|
|
282
|
+
executionTime,
|
|
283
|
+
error: errorMessage,
|
|
284
|
+
},
|
|
285
|
+
status: 'failure',
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 更新任务的下次执行时间
|
|
290
|
+
const nextRunTime = getNextRunTime(task.scheduleExpression);
|
|
291
|
+
await updateScheduledTask(task.id, {
|
|
292
|
+
lastRunAt: new Date(),
|
|
293
|
+
nextRunAt: nextRunTime || undefined,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* 检查并执行到期的定时任务
|
|
299
|
+
*/
|
|
300
|
+
async function checkAndExecuteDueTasks(): Promise<void> {
|
|
301
|
+
if (isRunning) {
|
|
302
|
+
console.log('[scheduler] Already running, skipping this cycle');
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
isRunning = true;
|
|
308
|
+
const dueTasks = await getScheduledTasksDueForExecution();
|
|
309
|
+
|
|
310
|
+
if (dueTasks.length === 0) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
console.log(`[scheduler] Found ${dueTasks.length} tasks due for execution`);
|
|
315
|
+
|
|
316
|
+
// 串行执行任务
|
|
317
|
+
for (const task of dueTasks) {
|
|
318
|
+
try {
|
|
319
|
+
await executeScheduledTask(task);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
console.error(`[scheduler] Error executing task ${task.id}:`, error);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} catch (error) {
|
|
325
|
+
console.error('[scheduler] Error in scheduler cycle:', error);
|
|
326
|
+
} finally {
|
|
327
|
+
isRunning = false;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* 更新所有活动定时任务的下次执行时间
|
|
333
|
+
*/
|
|
334
|
+
export async function initializeScheduledTaskNextRunTimes(): Promise<void> {
|
|
335
|
+
const { getScheduledTasks } = await import('./db');
|
|
336
|
+
const tasks = await getScheduledTasks({ status: 'active' });
|
|
337
|
+
|
|
338
|
+
for (const task of tasks) {
|
|
339
|
+
const nextRunTime = getNextRunTime(task.scheduleExpression);
|
|
340
|
+
if (nextRunTime) {
|
|
341
|
+
await updateScheduledTask(task.id, {
|
|
342
|
+
nextRunAt: nextRunTime,
|
|
343
|
+
});
|
|
344
|
+
console.log(`[scheduler] Initialized: ${task.title} -> ${nextRunTime.toISOString()}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
console.log(`[scheduler] Initialized ${tasks.length} active tasks`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* 手动触发执行指定任务
|
|
353
|
+
*/
|
|
354
|
+
export async function triggerTaskExecution(taskId: string): Promise<{ success: boolean; error?: string }> {
|
|
355
|
+
const { getScheduledTask } = await import('./db');
|
|
356
|
+
const task = await getScheduledTask(taskId);
|
|
357
|
+
|
|
358
|
+
if (!task) {
|
|
359
|
+
return { success: false, error: '任务不存在' };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
await executeScheduledTask(task);
|
|
364
|
+
return { success: true };
|
|
365
|
+
} catch (error: any) {
|
|
366
|
+
return { success: false, error: error.message };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* 启动调度器
|
|
372
|
+
*/
|
|
373
|
+
export function startScheduler(intervalMs: number = 60000): void {
|
|
374
|
+
if (schedulerInterval) {
|
|
375
|
+
console.log('[scheduler] Already running');
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
console.log(`[scheduler] Starting with interval ${intervalMs}ms`);
|
|
380
|
+
|
|
381
|
+
// 立即执行一次检查
|
|
382
|
+
checkAndExecuteDueTasks();
|
|
383
|
+
|
|
384
|
+
// 设置定时器
|
|
385
|
+
schedulerInterval = setInterval(() => {
|
|
386
|
+
checkAndExecuteDueTasks();
|
|
387
|
+
}, intervalMs);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* 停止调度器
|
|
392
|
+
*/
|
|
393
|
+
export function stopScheduler(): void {
|
|
394
|
+
if (schedulerInterval) {
|
|
395
|
+
clearInterval(schedulerInterval);
|
|
396
|
+
schedulerInterval = null;
|
|
397
|
+
console.log('[scheduler] Stopped');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* 获取调度器状态
|
|
403
|
+
*/
|
|
404
|
+
export function getSchedulerStatus(): { running: boolean; interval: number | null } {
|
|
405
|
+
return {
|
|
406
|
+
running: schedulerInterval !== null,
|
|
407
|
+
interval: schedulerInterval ? 60000 : null,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* 手动触发执行检查(用于测试)
|
|
413
|
+
*/
|
|
414
|
+
export async function triggerExecutionCheck(): Promise<void> {
|
|
415
|
+
await checkAndExecuteDueTasks();
|
|
416
|
+
}
|