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,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PI-Powered Chat API
|
|
3
|
+
*
|
|
4
|
+
* Intelligent chat endpoint using pi-coding-agent SDK for natural language understanding.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - General-purpose AI assistant with tool-based command execution
|
|
8
|
+
* - Dangerous commands are intercepted and require approval (via create_ticket tool or spawnHook)
|
|
9
|
+
* - All command executions are logged for audit
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { NextRequest } from 'next/server';
|
|
13
|
+
import { getOrCreateSession, eventToSSE, abortSession, searchKnowledgeContent } from '@/lib/pi-session';
|
|
14
|
+
import { loadSkillContentByName as loadSkillContentByNameLoader } from '@/lib/skill-loader';
|
|
15
|
+
import { updateSessionTitle, getUserByFeishuUnionId } from '@/lib/db';
|
|
16
|
+
import { randomUUID } from 'crypto';
|
|
17
|
+
|
|
18
|
+
// UUID 生成函数
|
|
19
|
+
function generateUUID(): string {
|
|
20
|
+
return randomUUID();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 解析消息中的 @mentions,提取 skill 和知识库引用
|
|
25
|
+
*/
|
|
26
|
+
function parseMentions(message: string, userId?: string): {
|
|
27
|
+
enhancedMessage: string;
|
|
28
|
+
mentionedSkills: string[];
|
|
29
|
+
mentionedKb: { keywords: string; title: string }[];
|
|
30
|
+
} {
|
|
31
|
+
const mentionedSkills: string[] = [];
|
|
32
|
+
const mentionedKb: { keywords: string; title: string }[] = [];
|
|
33
|
+
|
|
34
|
+
// 提取 skill 提及(如 @docker 或 @美欣达仿真平台)
|
|
35
|
+
const skillMatches = message.matchAll(/@([\w\u4e00-\u9fa5]+)/g);
|
|
36
|
+
for (const match of skillMatches) {
|
|
37
|
+
const skillName = match[1];
|
|
38
|
+
// 排除已经在 skills 参数中指定的
|
|
39
|
+
if (!mentionedSkills.includes(skillName)) {
|
|
40
|
+
mentionedSkills.push(skillName);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 提取知识库提及(如 @doc:Docker)
|
|
45
|
+
const kbMatches = message.matchAll(/@doc:([^@\s]+)/g);
|
|
46
|
+
for (const match of kbMatches) {
|
|
47
|
+
const title = match[1];
|
|
48
|
+
mentionedKb.push({ keywords: title, title });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 动态加载 skill 内容并构建增强消息
|
|
52
|
+
let enhancedMessage = message;
|
|
53
|
+
|
|
54
|
+
// 处理 skill 提及
|
|
55
|
+
for (const skillName of mentionedSkills) {
|
|
56
|
+
const skillContent = loadSkillContentByNameLoader(skillName, userId);
|
|
57
|
+
if (skillContent) {
|
|
58
|
+
// 在消息末尾注入 skill 内容作为参考
|
|
59
|
+
enhancedMessage += `\n\n--- 参考:Skill [${skillName}] ---\n${skillContent}`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 处理知识库提及
|
|
64
|
+
for (const kb of mentionedKb) {
|
|
65
|
+
const kbContent = searchKnowledgeContent(kb.keywords);
|
|
66
|
+
if (kbContent) {
|
|
67
|
+
enhancedMessage += `\n\n--- 参考:知识库 [${kb.title}] ---\n${kbContent}`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { enhancedMessage, mentionedSkills, mentionedKb };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const runtime = 'nodejs';
|
|
75
|
+
export const dynamic = 'force-dynamic';
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* POST /api/pi-chat - Intelligent chat with PI agent
|
|
79
|
+
*
|
|
80
|
+
* Request body:
|
|
81
|
+
* {
|
|
82
|
+
* message: string,
|
|
83
|
+
* sessionId?: string,
|
|
84
|
+
* userId?: string,
|
|
85
|
+
* skills?: string[],
|
|
86
|
+
* abort?: boolean // If true, abort current operation before sending
|
|
87
|
+
* }
|
|
88
|
+
*
|
|
89
|
+
* Returns: Server-Sent Events stream
|
|
90
|
+
*/
|
|
91
|
+
export async function POST(request: NextRequest) {
|
|
92
|
+
try {
|
|
93
|
+
const body = await request.json();
|
|
94
|
+
const { message, sessionId, userId, skills = [], abort = false, source, sourceUserId } = body as {
|
|
95
|
+
message: string;
|
|
96
|
+
sessionId?: string;
|
|
97
|
+
userId?: string;
|
|
98
|
+
skills?: string[];
|
|
99
|
+
abort?: boolean;
|
|
100
|
+
source?: 'web' | 'feishu_bot' | 'api';
|
|
101
|
+
sourceUserId?: string;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (!message || typeof message !== 'string') {
|
|
105
|
+
return Response.json(
|
|
106
|
+
{ error: 'Message is required and must be a string' },
|
|
107
|
+
{ status: 400 },
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 自动通过 unionId 映射系统用户ID
|
|
112
|
+
let effectiveUserId = userId || 'user';
|
|
113
|
+
if (source === 'feishu_bot' && sourceUserId) {
|
|
114
|
+
const feishuUser = getUserByFeishuUnionId(sourceUserId);
|
|
115
|
+
if (feishuUser) {
|
|
116
|
+
effectiveUserId = feishuUser.id;
|
|
117
|
+
console.log('[pi-chat] Mapped unionId to userId:', { sourceUserId, effectiveUserId });
|
|
118
|
+
} else {
|
|
119
|
+
console.log('[pi-chat] UnionId not found, using sourceUserId as userId:', { sourceUserId });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log('[pi-chat] Request:', { message, sessionId, userId: effectiveUserId, source, sourceUserId, skillsCount: skills.length, abort });
|
|
124
|
+
|
|
125
|
+
// 解析消息中的 @mentions(skill 和知识库)
|
|
126
|
+
const { enhancedMessage, mentionedSkills, mentionedKb } = parseMentions(message, userId);
|
|
127
|
+
console.log('[pi-chat] Parsed mentions:', { mentionedSkills, mentionedKb });
|
|
128
|
+
|
|
129
|
+
// Generate session ID if not provided
|
|
130
|
+
const effectiveSessionId = sessionId || generateUUID();
|
|
131
|
+
console.log('[pi-chat] Session ID:', effectiveSessionId);
|
|
132
|
+
|
|
133
|
+
// Get or create PI session
|
|
134
|
+
let sessionResult;
|
|
135
|
+
try {
|
|
136
|
+
sessionResult = await getOrCreateSession(effectiveSessionId, {
|
|
137
|
+
userId: effectiveUserId,
|
|
138
|
+
skills,
|
|
139
|
+
source,
|
|
140
|
+
sourceUserId,
|
|
141
|
+
});
|
|
142
|
+
console.log('[pi-chat] Session created/retrieved:', { isNew: sessionResult.isNew, restored: sessionResult.restored });
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('[pi-chat] Session creation error:', error);
|
|
145
|
+
return Response.json(
|
|
146
|
+
{ error: `Failed to create session: ${error instanceof Error ? error.message : 'Unknown error'}` },
|
|
147
|
+
{ status: 500 },
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const { session, isNew, restored } = sessionResult;
|
|
152
|
+
|
|
153
|
+
// If abort flag is set, abort current operation first
|
|
154
|
+
if (abort) {
|
|
155
|
+
try {
|
|
156
|
+
await abortSession(effectiveSessionId);
|
|
157
|
+
console.log('[pi-chat] Aborted previous operation');
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.error('[pi-chat] Abort error:', error);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Generate and save title for new sessions
|
|
164
|
+
// Use the user message directly since JSONL hasn't been written yet
|
|
165
|
+
if (isNew) {
|
|
166
|
+
try {
|
|
167
|
+
// Clean the message by removing @mentions and knowledge references
|
|
168
|
+
const cleanMessage = message
|
|
169
|
+
.replace(/@[\w\u4e00-\u9fa5]+/g, '') // Remove @mentions
|
|
170
|
+
.replace(/@doc:[^\s]+/g, '') // Remove @doc references
|
|
171
|
+
.trim();
|
|
172
|
+
|
|
173
|
+
// Generate title from cleaned message (max 40 chars)
|
|
174
|
+
const title = cleanMessage.length > 40
|
|
175
|
+
? cleanMessage.substring(0, 40) + '...'
|
|
176
|
+
: cleanMessage;
|
|
177
|
+
|
|
178
|
+
updateSessionTitle(effectiveSessionId, title);
|
|
179
|
+
console.log('[pi-chat] Generated title for new session:', title);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.warn('[pi-chat] Failed to generate title:', error);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Create SSE stream
|
|
186
|
+
const encoder = new TextEncoder();
|
|
187
|
+
const stream = new ReadableStream({
|
|
188
|
+
async start(controller) {
|
|
189
|
+
// Track controller state to prevent operations on closed controller
|
|
190
|
+
let isControllerClosed = false;
|
|
191
|
+
|
|
192
|
+
// Heartbeat configuration
|
|
193
|
+
const HEARTBEAT_INTERVAL = 10000; // 10 seconds
|
|
194
|
+
const PROMPT_TIMEOUT = 5 * 60 * 1000; // 5 minutes
|
|
195
|
+
let heartbeatTimer: NodeJS.Timeout | null = null;
|
|
196
|
+
let timeoutTimer: NodeJS.Timeout | null = null;
|
|
197
|
+
|
|
198
|
+
// Helper to start heartbeat
|
|
199
|
+
const startHeartbeat = () => {
|
|
200
|
+
if (heartbeatTimer) return; // Prevent duplicate timers
|
|
201
|
+
heartbeatTimer = setInterval(() => {
|
|
202
|
+
safeEnqueue(encoder.encode(': heartbeat\n\n'));
|
|
203
|
+
}, HEARTBEAT_INTERVAL);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Helper to stop heartbeat
|
|
207
|
+
const stopHeartbeat = () => {
|
|
208
|
+
if (heartbeatTimer) {
|
|
209
|
+
clearInterval(heartbeatTimer);
|
|
210
|
+
heartbeatTimer = null;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// Helper to start timeout protection
|
|
215
|
+
const startTimeout = (cleanup: () => void) => {
|
|
216
|
+
if (timeoutTimer) return;
|
|
217
|
+
timeoutTimer = setTimeout(() => {
|
|
218
|
+
console.error('[pi-chat] Prompt timeout after 5 minutes, aborting');
|
|
219
|
+
stopHeartbeat();
|
|
220
|
+
cleanup();
|
|
221
|
+
safeClose();
|
|
222
|
+
}, PROMPT_TIMEOUT);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// Helper to stop timeout protection
|
|
226
|
+
const stopTimeout = () => {
|
|
227
|
+
if (timeoutTimer) {
|
|
228
|
+
clearTimeout(timeoutTimer);
|
|
229
|
+
timeoutTimer = null;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Helper to safely enqueue data
|
|
234
|
+
const safeEnqueue = (data: Uint8Array) => {
|
|
235
|
+
if (!isControllerClosed) {
|
|
236
|
+
try {
|
|
237
|
+
controller.enqueue(data);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
// Controller may have been closed by client disconnect
|
|
240
|
+
isControllerClosed = true;
|
|
241
|
+
stopHeartbeat();
|
|
242
|
+
stopTimeout();
|
|
243
|
+
console.log('[pi-chat] Enqueue failed (controller likely closed):', error);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Helper to safely close controller
|
|
249
|
+
const safeClose = () => {
|
|
250
|
+
if (!isControllerClosed) {
|
|
251
|
+
isControllerClosed = true;
|
|
252
|
+
stopHeartbeat();
|
|
253
|
+
stopTimeout();
|
|
254
|
+
try {
|
|
255
|
+
controller.close();
|
|
256
|
+
} catch (error) {
|
|
257
|
+
// Already closed, ignore
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// Send session info if new or restored
|
|
263
|
+
if (isNew) {
|
|
264
|
+
safeEnqueue(
|
|
265
|
+
encoder.encode(
|
|
266
|
+
`data: ${JSON.stringify({ type: 'session_start', sessionId: effectiveSessionId })}\n\n`,
|
|
267
|
+
),
|
|
268
|
+
);
|
|
269
|
+
} else if (restored) {
|
|
270
|
+
// Session was restored after server restart
|
|
271
|
+
safeEnqueue(
|
|
272
|
+
encoder.encode(
|
|
273
|
+
`data: ${JSON.stringify({ type: 'session_restored', sessionId: effectiveSessionId })}\n\n`,
|
|
274
|
+
),
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let agentFinished = false;
|
|
279
|
+
|
|
280
|
+
// Subscribe to PI events
|
|
281
|
+
const unsubscribe = session.subscribe((event) => {
|
|
282
|
+
console.log('[pi-chat] SSE event:', event.type,
|
|
283
|
+
event.type === 'message_end' ? JSON.stringify(event.message) : '');
|
|
284
|
+
const sse = eventToSSE(event);
|
|
285
|
+
if (sse) {
|
|
286
|
+
safeEnqueue(encoder.encode(sse));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Detect when agent finishes
|
|
290
|
+
if (event.type === 'agent_end') {
|
|
291
|
+
agentFinished = true;
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Start heartbeat and timeout protection
|
|
296
|
+
startHeartbeat();
|
|
297
|
+
startTimeout(unsubscribe);
|
|
298
|
+
|
|
299
|
+
// Send the prompt to PI (使用增强后的消息)
|
|
300
|
+
try {
|
|
301
|
+
console.log('[pi-chat] Sending prompt:', enhancedMessage.substring(0, 100));
|
|
302
|
+
await session.prompt(enhancedMessage);
|
|
303
|
+
console.log('[pi-chat] Prompt completed');
|
|
304
|
+
|
|
305
|
+
// Wait a bit for any remaining events, then close
|
|
306
|
+
setTimeout(() => {
|
|
307
|
+
stopHeartbeat();
|
|
308
|
+
stopTimeout();
|
|
309
|
+
unsubscribe();
|
|
310
|
+
safeClose();
|
|
311
|
+
}, 500);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
console.error('[pi-chat] Prompt error:', error);
|
|
314
|
+
// Log full error details for debugging
|
|
315
|
+
if (error instanceof Error) {
|
|
316
|
+
console.error('[pi-chat] Error name:', error.name);
|
|
317
|
+
console.error('[pi-chat] Error message:', error.message);
|
|
318
|
+
console.error('[pi-chat] Error stack:', error.stack);
|
|
319
|
+
// Check for API error details
|
|
320
|
+
const anyError = error as any;
|
|
321
|
+
if (anyError.status) {
|
|
322
|
+
console.error('[pi-chat] Error status:', anyError.status);
|
|
323
|
+
}
|
|
324
|
+
if (anyError.response) {
|
|
325
|
+
console.error('[pi-chat] Error response:', anyError.response);
|
|
326
|
+
}
|
|
327
|
+
if (anyError.body) {
|
|
328
|
+
console.error('[pi-chat] Error body:', anyError.body);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
safeEnqueue(
|
|
332
|
+
encoder.encode(
|
|
333
|
+
`data: ${JSON.stringify({
|
|
334
|
+
type: 'error',
|
|
335
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
336
|
+
details: error instanceof Error ? error.name : undefined,
|
|
337
|
+
})}\n\n`,
|
|
338
|
+
),
|
|
339
|
+
);
|
|
340
|
+
stopHeartbeat();
|
|
341
|
+
stopTimeout();
|
|
342
|
+
unsubscribe();
|
|
343
|
+
safeClose();
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
return new Response(stream, {
|
|
349
|
+
headers: {
|
|
350
|
+
'Content-Type': 'text/event-stream',
|
|
351
|
+
'Cache-Control': 'no-cache',
|
|
352
|
+
'Connection': 'keep-alive',
|
|
353
|
+
'X-Accel-Buffering': 'no', // Disable nginx buffering
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
} catch (error) {
|
|
357
|
+
console.error('[pi-chat] Request error:', error);
|
|
358
|
+
return Response.json(
|
|
359
|
+
{
|
|
360
|
+
error: error instanceof Error ? error.message : 'Chat request failed',
|
|
361
|
+
details: error instanceof Error ? error.stack : undefined,
|
|
362
|
+
},
|
|
363
|
+
{ status: 500 },
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* GET /api/pi-chat - Get session info
|
|
370
|
+
*
|
|
371
|
+
* Query params:
|
|
372
|
+
* - sessionId: The session ID to query
|
|
373
|
+
*/
|
|
374
|
+
export async function GET(request: NextRequest) {
|
|
375
|
+
const { searchParams } = new URL(request.url);
|
|
376
|
+
const sessionId = searchParams.get('sessionId');
|
|
377
|
+
|
|
378
|
+
if (!sessionId) {
|
|
379
|
+
return Response.json(
|
|
380
|
+
{ error: 'sessionId is required' },
|
|
381
|
+
{ status: 400 },
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const { getActiveSessions, hasSession } = await import('@/lib/pi-session');
|
|
386
|
+
|
|
387
|
+
return Response.json({
|
|
388
|
+
sessionId,
|
|
389
|
+
exists: hasSession(sessionId),
|
|
390
|
+
allSessions: getActiveSessions(),
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* PUT /api/pi-chat - Abort current operation
|
|
396
|
+
*
|
|
397
|
+
* Query params:
|
|
398
|
+
* - sessionId: The session ID to abort
|
|
399
|
+
*/
|
|
400
|
+
export async function PUT(request: NextRequest) {
|
|
401
|
+
const { searchParams } = new URL(request.url);
|
|
402
|
+
const sessionId = searchParams.get('sessionId');
|
|
403
|
+
|
|
404
|
+
if (!sessionId) {
|
|
405
|
+
return Response.json(
|
|
406
|
+
{ error: 'sessionId is required' },
|
|
407
|
+
{ status: 400 },
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
await abortSession(sessionId);
|
|
413
|
+
return Response.json({ success: true, message: 'Session aborted' });
|
|
414
|
+
} catch (error) {
|
|
415
|
+
return Response.json(
|
|
416
|
+
{ error: error instanceof Error ? error.message : 'Abort failed' },
|
|
417
|
+
{ status: 500 },
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* DELETE /api/pi-chat - Clean up a session
|
|
424
|
+
*
|
|
425
|
+
* Query params:
|
|
426
|
+
* - sessionId: The session ID to delete
|
|
427
|
+
*/
|
|
428
|
+
export async function DELETE(request: NextRequest) {
|
|
429
|
+
const { searchParams } = new URL(request.url);
|
|
430
|
+
const sessionId = searchParams.get('sessionId');
|
|
431
|
+
|
|
432
|
+
if (!sessionId) {
|
|
433
|
+
return Response.json(
|
|
434
|
+
{ error: 'sessionId is required' },
|
|
435
|
+
{ status: 400 },
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const { cleanupSession } = await import('@/lib/pi-session');
|
|
440
|
+
cleanupSession(sessionId);
|
|
441
|
+
|
|
442
|
+
return Response.json({ success: true, sessionId });
|
|
443
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { recommendForContext, getPersonalizedRecommendations, getPopularSkills } from '@/lib/recommendation';
|
|
3
|
+
|
|
4
|
+
export async function GET(request: NextRequest) {
|
|
5
|
+
try {
|
|
6
|
+
const userId = request.nextUrl.searchParams.get('userId');
|
|
7
|
+
const query = request.nextUrl.searchParams.get('query');
|
|
8
|
+
const type = request.nextUrl.searchParams.get('type') || 'popular';
|
|
9
|
+
const limit = parseInt(request.nextUrl.searchParams.get('limit') || '5');
|
|
10
|
+
|
|
11
|
+
let recommendations;
|
|
12
|
+
|
|
13
|
+
if (type === 'context' && query) {
|
|
14
|
+
// 基于上下文的推荐
|
|
15
|
+
if (!userId) {
|
|
16
|
+
return NextResponse.json(
|
|
17
|
+
{ error: '需要userId参数' },
|
|
18
|
+
{ status: 400 }
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
recommendations = recommendForContext(query, userId);
|
|
22
|
+
} else if (type === 'personalized' && userId) {
|
|
23
|
+
// 个性化推荐
|
|
24
|
+
recommendations = getPersonalizedRecommendations(userId, limit);
|
|
25
|
+
} else {
|
|
26
|
+
// 热门推荐(默认)
|
|
27
|
+
recommendations = getPopularSkills(limit);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return NextResponse.json({ recommendations });
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('Get recommendations failed:', error);
|
|
33
|
+
return NextResponse.json(
|
|
34
|
+
{ error: '获取推荐失败' },
|
|
35
|
+
{ status: 500 }
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 手动执行定时任务 API - POST
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
6
|
+
import { getScheduledTask, createAuditLog } from '@/lib/db';
|
|
7
|
+
import { triggerTaskExecution } from '@/lib/scheduler';
|
|
8
|
+
|
|
9
|
+
type RouteContext = {
|
|
10
|
+
params: Promise<{ id: string }>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 检查请求是否来自浏览器
|
|
15
|
+
*/
|
|
16
|
+
function isBrowserRequest(request: NextRequest): boolean {
|
|
17
|
+
const userAgent = request.headers.get('user-agent') || '';
|
|
18
|
+
|
|
19
|
+
// 检查是否是常见的程序化调用工具
|
|
20
|
+
const automatedAgents = ['curl', 'wget', 'python', 'node', 'axios', 'fetch', 'httpie'];
|
|
21
|
+
const isAutomated = automatedAgents.some(agent =>
|
|
22
|
+
userAgent.toLowerCase().includes(agent)
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// 如果是程序化调用,返回 false
|
|
26
|
+
if (isAutomated) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 检查是否有 Origin 或 Referer header(浏览器请求通常会有)
|
|
31
|
+
const origin = request.headers.get('origin');
|
|
32
|
+
const referer = request.headers.get('referer');
|
|
33
|
+
|
|
34
|
+
// 有 origin 或 referer 的话很可能是浏览器
|
|
35
|
+
return !!(origin || referer);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* POST /api/scheduled-tasks/[id]/execute - 手动执行定时任务
|
|
40
|
+
*/
|
|
41
|
+
export async function POST(request: NextRequest, context: RouteContext) {
|
|
42
|
+
try {
|
|
43
|
+
const { id } = await context.params;
|
|
44
|
+
const body = await request.json();
|
|
45
|
+
const { userId } = body;
|
|
46
|
+
|
|
47
|
+
// CRITICAL-1: 安全检查 - 执行请求必须来自浏览器
|
|
48
|
+
if (!isBrowserRequest(request)) {
|
|
49
|
+
console.warn(`[execute] Blocked automated execution request for scheduled task ${id}. User-Agent: ${request.headers.get('user-agent')}`);
|
|
50
|
+
|
|
51
|
+
await createAuditLog({
|
|
52
|
+
action: 'blocked_automated_execution',
|
|
53
|
+
userId: userId || 'unknown',
|
|
54
|
+
details: {
|
|
55
|
+
taskId: id,
|
|
56
|
+
userAgent: request.headers.get('user-agent'),
|
|
57
|
+
reason: 'Execution must be done through the web interface'
|
|
58
|
+
},
|
|
59
|
+
status: 'failure',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return NextResponse.json(
|
|
63
|
+
{
|
|
64
|
+
error: '执行操作必须通过网页界面进行',
|
|
65
|
+
message: '出于安全考虑,执行定时任务不能通过 API 自动调用。请在定时任务管理界面进行执行。'
|
|
66
|
+
},
|
|
67
|
+
{ status: 403 },
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// IMPORTANT-3: 输入验证 - userId 必填
|
|
72
|
+
if (!userId || typeof userId !== 'string' || userId.trim().length === 0) {
|
|
73
|
+
return NextResponse.json(
|
|
74
|
+
{ error: '缺少必填字段: userId' },
|
|
75
|
+
{ status: 400 },
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 获取任务
|
|
80
|
+
const task = await getScheduledTask(id);
|
|
81
|
+
if (!task) {
|
|
82
|
+
return NextResponse.json(
|
|
83
|
+
{ error: '定时任务不存在' },
|
|
84
|
+
{ status: 404 },
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// IMPORTANT-1: 验证任务状态 - 只有激活状态的任务可以手动执行
|
|
89
|
+
if (task.status !== 'active') {
|
|
90
|
+
return NextResponse.json(
|
|
91
|
+
{
|
|
92
|
+
error: `任务状态为 ${task.status},无法执行。只有激活状态的任务可以手动执行。`
|
|
93
|
+
},
|
|
94
|
+
{ status: 400 },
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 触发执行
|
|
99
|
+
const result = await triggerTaskExecution(id);
|
|
100
|
+
|
|
101
|
+
if (!result.success) {
|
|
102
|
+
return NextResponse.json(
|
|
103
|
+
{ error: result.error || '执行失败' },
|
|
104
|
+
{ status: 500 },
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// IMPORTANT-2: 只在成功执行后才创建审计日志
|
|
109
|
+
await createAuditLog({
|
|
110
|
+
action: 'scheduled_task_manual_trigger',
|
|
111
|
+
userId,
|
|
112
|
+
details: {
|
|
113
|
+
taskId: id,
|
|
114
|
+
title: task.title,
|
|
115
|
+
command: task.command,
|
|
116
|
+
},
|
|
117
|
+
status: 'success',
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return NextResponse.json({
|
|
121
|
+
success: true,
|
|
122
|
+
message: '任务已开始执行',
|
|
123
|
+
});
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error('[scheduled-task-execute] Error executing task:', error);
|
|
126
|
+
|
|
127
|
+
return NextResponse.json(
|
|
128
|
+
{ error: error instanceof Error ? error.message : '执行定时任务失败' },
|
|
129
|
+
{ status: 500 },
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|