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,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 审批 API - POST
|
|
3
|
+
*
|
|
4
|
+
* 安全措施:审批操作只能通过前端界面进行,不允许通过 API 自动调用。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
8
|
+
import { approveTicket, rejectTicket, submitTicket, resubmitTicket } from '@/lib/workflow';
|
|
9
|
+
import { getTicket } from '@/lib/db';
|
|
10
|
+
import { createAuditLog } from '@/lib/db';
|
|
11
|
+
|
|
12
|
+
type RouteContext = {
|
|
13
|
+
params: Promise<{ id: string }>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 检查请求是否来自浏览器(而非程序化调用如 curl)
|
|
18
|
+
*/
|
|
19
|
+
function isBrowserRequest(request: NextRequest): boolean {
|
|
20
|
+
const userAgent = request.headers.get('user-agent') || '';
|
|
21
|
+
|
|
22
|
+
// 检查是否是常见的程序化调用工具
|
|
23
|
+
const automatedAgents = ['curl', 'wget', 'python', 'node', 'axios', 'fetch', 'httpie'];
|
|
24
|
+
const isAutomated = automatedAgents.some(agent =>
|
|
25
|
+
userAgent.toLowerCase().includes(agent)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// 如果是程序化调用,返回 false
|
|
29
|
+
if (isAutomated) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 检查是否有 Origin 或 Referer header(浏览器请求通常会有)
|
|
34
|
+
const origin = request.headers.get('origin');
|
|
35
|
+
const referer = request.headers.get('referer');
|
|
36
|
+
|
|
37
|
+
// 如果有 Origin 或 Referer,认为是浏览器请求
|
|
38
|
+
return !!(origin || referer);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* POST /api/tickets/[id]/approve - 处理审批操作
|
|
43
|
+
*/
|
|
44
|
+
export async function POST(request: NextRequest, context: RouteContext) {
|
|
45
|
+
try {
|
|
46
|
+
const { id } = await context.params;
|
|
47
|
+
const body = await request.json();
|
|
48
|
+
const { action, approver, comment } = body;
|
|
49
|
+
|
|
50
|
+
// 安全检查:审批操作必须来自浏览器
|
|
51
|
+
if (action === 'approve' || action === 'reject') {
|
|
52
|
+
if (!isBrowserRequest(request)) {
|
|
53
|
+
console.warn(`[approve] Blocked automated approval request for ticket ${id}. User-Agent: ${request.headers.get('user-agent')}`);
|
|
54
|
+
|
|
55
|
+
// 记录审计日志
|
|
56
|
+
await createAuditLog({
|
|
57
|
+
action: 'blocked_automated_approval',
|
|
58
|
+
userId: approver || 'unknown',
|
|
59
|
+
ticketId: id,
|
|
60
|
+
details: {
|
|
61
|
+
action,
|
|
62
|
+
userAgent: request.headers.get('user-agent'),
|
|
63
|
+
reason: 'Approval must be done through the web interface'
|
|
64
|
+
},
|
|
65
|
+
status: 'failure',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return NextResponse.json(
|
|
69
|
+
{
|
|
70
|
+
error: '审批操作必须通过网页界面进行',
|
|
71
|
+
message: '出于安全考虑,审批和拒绝操作不能通过 API 自动调用。请在工单管理界面进行审批。'
|
|
72
|
+
},
|
|
73
|
+
{ status: 403 },
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!approver) {
|
|
79
|
+
return NextResponse.json(
|
|
80
|
+
{ error: '缺少必填字段: approver' },
|
|
81
|
+
{ status: 400 },
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const ticket = await getTicket(id);
|
|
86
|
+
if (!ticket) {
|
|
87
|
+
return NextResponse.json(
|
|
88
|
+
{ error: '工单不存在' },
|
|
89
|
+
{ status: 404 },
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let result;
|
|
94
|
+
switch (action) {
|
|
95
|
+
case 'submit':
|
|
96
|
+
result = await submitTicket(id, approver);
|
|
97
|
+
break;
|
|
98
|
+
case 'approve':
|
|
99
|
+
result = await approveTicket(id, approver, comment);
|
|
100
|
+
break;
|
|
101
|
+
case 'reject':
|
|
102
|
+
result = await rejectTicket(id, approver, comment);
|
|
103
|
+
break;
|
|
104
|
+
case 'resubmit':
|
|
105
|
+
result = await resubmitTicket(id, approver);
|
|
106
|
+
break;
|
|
107
|
+
default:
|
|
108
|
+
return NextResponse.json(
|
|
109
|
+
{ error: `无效的操作: ${action}。必须是: submit, approve, reject, resubmit` },
|
|
110
|
+
{ status: 400 },
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await createAuditLog({
|
|
115
|
+
action: `ticket_${action}`,
|
|
116
|
+
userId: approver,
|
|
117
|
+
ticketId: id,
|
|
118
|
+
details: { comment },
|
|
119
|
+
status: 'success',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return NextResponse.json({ ticket: result });
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return NextResponse.json(
|
|
125
|
+
{ error: error instanceof Error ? error.message : '处理审批失败' },
|
|
126
|
+
{ status: 500 },
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工单执行 API - POST /api/tickets/[id]/execute
|
|
3
|
+
*
|
|
4
|
+
* 执行已审批的工单中的命令。
|
|
5
|
+
*
|
|
6
|
+
* 安全措施:
|
|
7
|
+
* 1. 只能执行特定工单,不能传入任意命令
|
|
8
|
+
* 2. 工单必须是已审批状态
|
|
9
|
+
* 3. 请求必须来自浏览器(阻止 curl 等自动化工具)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
13
|
+
import { getTicket, updateTicket, createAuditLog } from '@/lib/db';
|
|
14
|
+
import { startExecution, failExecution } from '@/lib/workflow';
|
|
15
|
+
import { processCommand } from '@/lib/skill-engine';
|
|
16
|
+
|
|
17
|
+
type RouteContext = {
|
|
18
|
+
params: Promise<{ id: string }>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 检查请求是否来自浏览器(而非程序化调用如 curl)
|
|
23
|
+
*/
|
|
24
|
+
function isBrowserRequest(request: NextRequest): boolean {
|
|
25
|
+
const userAgent = request.headers.get('user-agent') || '';
|
|
26
|
+
|
|
27
|
+
// 检查是否是常见的程序化调用工具
|
|
28
|
+
const automatedAgents = ['curl', 'wget', 'python', 'node', 'axios', 'httpie'];
|
|
29
|
+
const isAutomated = automatedAgents.some(agent =>
|
|
30
|
+
userAgent.toLowerCase().includes(agent)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
if (isAutomated) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 检查是否有 Origin 或 Referer header(浏览器请求通常会有)
|
|
38
|
+
const origin = request.headers.get('origin');
|
|
39
|
+
const referer = request.headers.get('referer');
|
|
40
|
+
|
|
41
|
+
return !!(origin || referer);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* POST /api/tickets/[id]/execute - 执行已审批的工单
|
|
46
|
+
*/
|
|
47
|
+
export async function POST(request: NextRequest, context: RouteContext) {
|
|
48
|
+
try {
|
|
49
|
+
const { id } = await context.params;
|
|
50
|
+
const body = await request.json();
|
|
51
|
+
const { userId = 'user' } = body;
|
|
52
|
+
|
|
53
|
+
// 安全检查:执行请求必须来自浏览器
|
|
54
|
+
if (!isBrowserRequest(request)) {
|
|
55
|
+
console.warn(`[execute] Blocked automated execution request for ticket ${id}. User-Agent: ${request.headers.get('user-agent')}`);
|
|
56
|
+
|
|
57
|
+
await createAuditLog({
|
|
58
|
+
action: 'blocked_automated_execution',
|
|
59
|
+
userId,
|
|
60
|
+
ticketId: id,
|
|
61
|
+
details: {
|
|
62
|
+
userAgent: request.headers.get('user-agent'),
|
|
63
|
+
reason: 'Execution must be done through the web interface'
|
|
64
|
+
},
|
|
65
|
+
status: 'failure',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return NextResponse.json(
|
|
69
|
+
{
|
|
70
|
+
error: '执行操作必须通过网页界面进行',
|
|
71
|
+
message: '出于安全考虑,执行已审批的工单不能通过 API 自动调用。请在工单管理界面进行执行。'
|
|
72
|
+
},
|
|
73
|
+
{ status: 403 },
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 获取工单
|
|
78
|
+
const ticket = await getTicket(id);
|
|
79
|
+
if (!ticket) {
|
|
80
|
+
return NextResponse.json(
|
|
81
|
+
{ error: '工单不存在' },
|
|
82
|
+
{ status: 404 },
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 检查工单状态
|
|
87
|
+
if (ticket.status !== 'approved') {
|
|
88
|
+
return NextResponse.json(
|
|
89
|
+
{
|
|
90
|
+
error: '工单必须已批准才能执行',
|
|
91
|
+
message: `工单状态为 ${ticket.status},必须是已批准状态才能执行。`
|
|
92
|
+
},
|
|
93
|
+
{ status: 400 },
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 检查工单是否有命令
|
|
98
|
+
if (!ticket.command && !ticket.scriptContent) {
|
|
99
|
+
return NextResponse.json(
|
|
100
|
+
{ error: '工单缺少命令或脚本内容' },
|
|
101
|
+
{ status: 400 },
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 开始执行
|
|
106
|
+
await startExecution(id, userId);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
// 执行命令(forceExecute: true 因为工单已经审批通过)
|
|
110
|
+
const result = await processCommand({
|
|
111
|
+
command: ticket.command || ticket.scriptContent || '',
|
|
112
|
+
skillName: ticket.skillName,
|
|
113
|
+
userId,
|
|
114
|
+
sessionId: ticket.aiSessionId,
|
|
115
|
+
forceExecute: true,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (result.result?.success) {
|
|
119
|
+
// 执行成功
|
|
120
|
+
const updatedTicket = await updateTicket(id, {
|
|
121
|
+
status: 'completed',
|
|
122
|
+
output: result.result.output,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await createAuditLog({
|
|
126
|
+
action: 'ticket_executed',
|
|
127
|
+
userId,
|
|
128
|
+
ticketId: id,
|
|
129
|
+
sessionId: ticket.aiSessionId || undefined,
|
|
130
|
+
details: {
|
|
131
|
+
command: ticket.command,
|
|
132
|
+
skillName: ticket.skillName,
|
|
133
|
+
riskLevel: ticket.riskLevel,
|
|
134
|
+
output: result.result.output,
|
|
135
|
+
executionTime: result.result.executionTime,
|
|
136
|
+
},
|
|
137
|
+
status: 'success',
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return NextResponse.json({
|
|
141
|
+
success: true,
|
|
142
|
+
status: 'completed',
|
|
143
|
+
output: result.result.output,
|
|
144
|
+
executionTime: result.result.executionTime,
|
|
145
|
+
ticket: updatedTicket,
|
|
146
|
+
});
|
|
147
|
+
} else {
|
|
148
|
+
// 执行失败
|
|
149
|
+
const updatedTicket = await failExecution(id, userId, result.result?.error);
|
|
150
|
+
|
|
151
|
+
await createAuditLog({
|
|
152
|
+
action: 'ticket_execution_failed',
|
|
153
|
+
userId,
|
|
154
|
+
ticketId: id,
|
|
155
|
+
sessionId: ticket.aiSessionId || undefined,
|
|
156
|
+
details: {
|
|
157
|
+
command: ticket.command,
|
|
158
|
+
skillName: ticket.skillName,
|
|
159
|
+
error: result.result?.error,
|
|
160
|
+
},
|
|
161
|
+
status: 'failure',
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return NextResponse.json({
|
|
165
|
+
success: false,
|
|
166
|
+
status: 'failed',
|
|
167
|
+
error: result.result?.error,
|
|
168
|
+
ticket: updatedTicket,
|
|
169
|
+
}, { status: 500 });
|
|
170
|
+
}
|
|
171
|
+
} catch (execError) {
|
|
172
|
+
// 执行过程中发生异常
|
|
173
|
+
const updatedTicket = await failExecution(id, userId, execError instanceof Error ? execError.message : '执行异常');
|
|
174
|
+
|
|
175
|
+
await createAuditLog({
|
|
176
|
+
action: 'ticket_execution_error',
|
|
177
|
+
userId,
|
|
178
|
+
ticketId: id,
|
|
179
|
+
sessionId: ticket.aiSessionId || undefined,
|
|
180
|
+
details: {
|
|
181
|
+
command: ticket.command,
|
|
182
|
+
error: execError instanceof Error ? execError.message : '执行异常',
|
|
183
|
+
},
|
|
184
|
+
status: 'failure',
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return NextResponse.json({
|
|
188
|
+
success: false,
|
|
189
|
+
status: 'failed',
|
|
190
|
+
error: execError instanceof Error ? execError.message : '执行异常',
|
|
191
|
+
ticket: updatedTicket,
|
|
192
|
+
}, { status: 500 });
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error('[execute] Error:', error);
|
|
196
|
+
return NextResponse.json(
|
|
197
|
+
{ error: error instanceof Error ? error.message : '执行失败' },
|
|
198
|
+
{ status: 500 },
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工单详情 API - GET, PUT, DELETE
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
6
|
+
import { getTicket, updateTicket, deleteTicket } from '@/lib/db';
|
|
7
|
+
import { canTransition } from '@/lib/workflow';
|
|
8
|
+
import { createAuditLog } from '@/lib/db';
|
|
9
|
+
|
|
10
|
+
type RouteContext = {
|
|
11
|
+
params: Promise<{ id: string }>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* GET /api/tickets/[id] - 获取工单详情
|
|
16
|
+
*/
|
|
17
|
+
export async function GET(request: NextRequest, context: RouteContext) {
|
|
18
|
+
try {
|
|
19
|
+
const { id } = await context.params;
|
|
20
|
+
const ticket = await getTicket(id);
|
|
21
|
+
|
|
22
|
+
if (!ticket) {
|
|
23
|
+
return NextResponse.json(
|
|
24
|
+
{ error: '工单不存在' },
|
|
25
|
+
{ status: 404 },
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return NextResponse.json({ ticket });
|
|
30
|
+
} catch (error) {
|
|
31
|
+
return NextResponse.json(
|
|
32
|
+
{ error: error instanceof Error ? error.message : '获取工单失败' },
|
|
33
|
+
{ status: 500 },
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* PUT /api/tickets/[id] - 更新工单
|
|
40
|
+
*/
|
|
41
|
+
export async function PUT(request: NextRequest, context: RouteContext) {
|
|
42
|
+
try {
|
|
43
|
+
const { id } = await context.params;
|
|
44
|
+
const ticket = await getTicket(id);
|
|
45
|
+
|
|
46
|
+
if (!ticket) {
|
|
47
|
+
return NextResponse.json(
|
|
48
|
+
{ error: '工单不存在' },
|
|
49
|
+
{ status: 404 },
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const body = await request.json();
|
|
54
|
+
const { status } = body;
|
|
55
|
+
|
|
56
|
+
// 如果要更新状态,检查转换是否合法
|
|
57
|
+
if (status && status !== ticket.status) {
|
|
58
|
+
if (!canTransition(ticket.status, status)) {
|
|
59
|
+
return NextResponse.json(
|
|
60
|
+
{ error: `不能将工单从 ${ticket.status} 转换为 ${status}` },
|
|
61
|
+
{ status: 400 },
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 更新工单
|
|
67
|
+
const updated = await updateTicket(id, body);
|
|
68
|
+
|
|
69
|
+
return NextResponse.json({ ticket: updated });
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return NextResponse.json(
|
|
72
|
+
{ error: error instanceof Error ? error.message : '更新工单失败' },
|
|
73
|
+
{ status: 500 },
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* DELETE /api/tickets/[id] - 删除工单
|
|
80
|
+
*/
|
|
81
|
+
export async function DELETE(request: NextRequest, context: RouteContext) {
|
|
82
|
+
try {
|
|
83
|
+
const { id } = await context.params;
|
|
84
|
+
const ticket = await getTicket(id);
|
|
85
|
+
|
|
86
|
+
if (!ticket) {
|
|
87
|
+
return NextResponse.json(
|
|
88
|
+
{ error: '工单不存在' },
|
|
89
|
+
{ status: 404 },
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 只有草稿状态的工单可以删除
|
|
94
|
+
if (ticket.status !== 'draft') {
|
|
95
|
+
return NextResponse.json(
|
|
96
|
+
{ error: '只有草稿状态的工单可以删除' },
|
|
97
|
+
{ status: 400 },
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 记录审计日志
|
|
102
|
+
await createAuditLog({
|
|
103
|
+
action: 'ticket_deleted',
|
|
104
|
+
userId: 'user',
|
|
105
|
+
ticketId: id,
|
|
106
|
+
details: { title: ticket.title },
|
|
107
|
+
status: 'success',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// 删除工单
|
|
111
|
+
const deleted = await deleteTicket(id);
|
|
112
|
+
|
|
113
|
+
if (!deleted) {
|
|
114
|
+
return NextResponse.json(
|
|
115
|
+
{ error: '删除工单失败' },
|
|
116
|
+
{ status: 500 },
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return NextResponse.json({ success: true });
|
|
121
|
+
} catch (error) {
|
|
122
|
+
return NextResponse.json(
|
|
123
|
+
{ error: error instanceof Error ? error.message : '删除工单失败' },
|
|
124
|
+
{ status: 500 },
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工单 API - GET/POST
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
6
|
+
import { getTickets, createTicket } from '@/lib/db';
|
|
7
|
+
import { createAuditLog } from '@/lib/db';
|
|
8
|
+
import type { TicketType, RiskLevel } from '@/lib/db';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* GET /api/tickets - 获取工单列表
|
|
12
|
+
*/
|
|
13
|
+
export async function GET(request: NextRequest) {
|
|
14
|
+
try {
|
|
15
|
+
const { searchParams } = new URL(request.url);
|
|
16
|
+
const filters = {
|
|
17
|
+
status: searchParams.get('status') ?? undefined,
|
|
18
|
+
type: searchParams.get('type') as TicketType | undefined,
|
|
19
|
+
createdBy: searchParams.get('createdBy') ?? undefined,
|
|
20
|
+
aiSessionId: searchParams.get('aiSessionId') ?? undefined,
|
|
21
|
+
limit: searchParams.get('limit') ? parseInt(searchParams.get('limit')!) : undefined,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const tickets = await getTickets(filters);
|
|
25
|
+
|
|
26
|
+
return NextResponse.json({ tickets });
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return NextResponse.json(
|
|
29
|
+
{ error: error instanceof Error ? error.message : '获取工单失败' },
|
|
30
|
+
{ status: 500 },
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* POST /api/tickets - 创建新工单
|
|
37
|
+
*/
|
|
38
|
+
export async function POST(request: NextRequest) {
|
|
39
|
+
try {
|
|
40
|
+
const body = await request.json();
|
|
41
|
+
const {
|
|
42
|
+
type,
|
|
43
|
+
title,
|
|
44
|
+
description,
|
|
45
|
+
priority,
|
|
46
|
+
cluster,
|
|
47
|
+
command,
|
|
48
|
+
skillName,
|
|
49
|
+
scriptContent,
|
|
50
|
+
riskLevel,
|
|
51
|
+
createdBy
|
|
52
|
+
} = body;
|
|
53
|
+
|
|
54
|
+
// 验证必填字段
|
|
55
|
+
if (!type || !title || !createdBy) {
|
|
56
|
+
return NextResponse.json(
|
|
57
|
+
{ error: '缺少必填字段: type, title, createdBy' },
|
|
58
|
+
{ status: 400 },
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 验证工单类型
|
|
63
|
+
const validTypes: TicketType[] = ['bash-execute', 'skill-script'];
|
|
64
|
+
if (!validTypes.includes(type)) {
|
|
65
|
+
return NextResponse.json(
|
|
66
|
+
{ error: `无效的操作类型,必须是: ${validTypes.join(', ')}` },
|
|
67
|
+
{ status: 400 },
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 创建工单
|
|
72
|
+
const ticket = await createTicket({
|
|
73
|
+
type,
|
|
74
|
+
title,
|
|
75
|
+
description: description ?? '',
|
|
76
|
+
status: 'draft',
|
|
77
|
+
priority: (priority as RiskLevel) ?? 'medium',
|
|
78
|
+
command,
|
|
79
|
+
commandType: command ? 'kubectl' : undefined,
|
|
80
|
+
skillName,
|
|
81
|
+
scriptContent,
|
|
82
|
+
riskLevel: (riskLevel as RiskLevel) ?? 'medium',
|
|
83
|
+
approvals: [],
|
|
84
|
+
createdBy,
|
|
85
|
+
aiGenerated: false,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await createAuditLog({
|
|
89
|
+
action: 'ticket_created',
|
|
90
|
+
userId: createdBy,
|
|
91
|
+
ticketId: ticket.id,
|
|
92
|
+
details: { ticketId: ticket.id, type, title },
|
|
93
|
+
status: 'success',
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return NextResponse.json({ ticket }, { status: 201 });
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return NextResponse.json(
|
|
99
|
+
{ error: error instanceof Error ? error.message : '创建工单失败' },
|
|
100
|
+
{ status: 500 },
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|