work-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/README.md +234 -0
  2. package/app/(admin)/approvals/page.tsx +16 -0
  3. package/app/(admin)/audit/page.tsx +18 -0
  4. package/app/(admin)/layout.tsx +47 -0
  5. package/app/(admin)/scheduled-tasks/page.tsx +17 -0
  6. package/app/(admin)/settings/page.tsx +46 -0
  7. package/app/(admin)/skills/[name]/page.tsx +378 -0
  8. package/app/(admin)/skills/page.tsx +406 -0
  9. package/app/(admin)/statistics/page.tsx +416 -0
  10. package/app/(admin)/tickets/[id]/page.tsx +348 -0
  11. package/app/(admin)/tickets/new/page.tsx +309 -0
  12. package/app/(admin)/tickets/page.tsx +27 -0
  13. package/app/api/audit/route.ts +30 -0
  14. package/app/api/auth/feishu/callback/route.ts +72 -0
  15. package/app/api/auth/feishu/login/route.ts +17 -0
  16. package/app/api/auth/feishu/sso/route.ts +78 -0
  17. package/app/api/auth/login/route.ts +85 -0
  18. package/app/api/auth/oauth/route.ts +168 -0
  19. package/app/api/config/providers/route.ts +105 -0
  20. package/app/api/config/route.ts +115 -0
  21. package/app/api/config/status/route.ts +56 -0
  22. package/app/api/config/test/route.ts +212 -0
  23. package/app/api/documents/[id]/route.ts +88 -0
  24. package/app/api/documents/route.ts +53 -0
  25. package/app/api/health/route.ts +32 -0
  26. package/app/api/knowledge/[id]/route.ts +152 -0
  27. package/app/api/knowledge/from-session/route.ts +27 -0
  28. package/app/api/knowledge/route.ts +100 -0
  29. package/app/api/market/knowledge/[id]/route.ts +92 -0
  30. package/app/api/market/knowledge/route.ts +130 -0
  31. package/app/api/marketplace/skills/[id]/approve/route.ts +68 -0
  32. package/app/api/marketplace/skills/[id]/certify/route.ts +54 -0
  33. package/app/api/marketplace/skills/[id]/install/route.ts +180 -0
  34. package/app/api/marketplace/skills/[id]/promote-to-system/route.ts +219 -0
  35. package/app/api/marketplace/skills/[id]/rate/route.ts +90 -0
  36. package/app/api/marketplace/skills/[id]/ratings/route.ts +55 -0
  37. package/app/api/marketplace/skills/[id]/reject/route.ts +68 -0
  38. package/app/api/marketplace/skills/[id]/route.ts +177 -0
  39. package/app/api/marketplace/skills/route.ts +235 -0
  40. package/app/api/memory/route.ts +40 -0
  41. package/app/api/my/files/[id]/route.ts +52 -0
  42. package/app/api/my/files/route.ts +230 -0
  43. package/app/api/my/knowledge/route.ts +36 -0
  44. package/app/api/pi-chat/route.ts +443 -0
  45. package/app/api/recommend/route.ts +38 -0
  46. package/app/api/scheduled-tasks/[id]/execute/route.ts +132 -0
  47. package/app/api/scheduled-tasks/[id]/route.ts +165 -0
  48. package/app/api/scheduled-tasks/[id]/toggle/route.ts +53 -0
  49. package/app/api/scheduled-tasks/route.ts +101 -0
  50. package/app/api/sessions/[id]/messages/route.ts +212 -0
  51. package/app/api/sessions/route.ts +101 -0
  52. package/app/api/share/file/[id]/route.ts +37 -0
  53. package/app/api/skills/[name]/execute/route.ts +121 -0
  54. package/app/api/skills/[name]/route.ts +167 -0
  55. package/app/api/skills/create/route.ts +65 -0
  56. package/app/api/skills/generate/route.ts +405 -0
  57. package/app/api/skills/installed/route.ts +151 -0
  58. package/app/api/skills/route.ts +174 -0
  59. package/app/api/skills/translate/route.ts +40 -0
  60. package/app/api/skills/user/[name]/route.ts +159 -0
  61. package/app/api/skills/user/route.ts +90 -0
  62. package/app/api/statistics/route.ts +94 -0
  63. package/app/api/task-executions/[id]/route.ts +34 -0
  64. package/app/api/task-executions/route.ts +29 -0
  65. package/app/api/tickets/[id]/approve/route.ts +129 -0
  66. package/app/api/tickets/[id]/execute/route.ts +201 -0
  67. package/app/api/tickets/[id]/route.ts +127 -0
  68. package/app/api/tickets/route.ts +103 -0
  69. package/app/api/user/skills/route.ts +175 -0
  70. package/app/api/users/route.ts +80 -0
  71. package/app/chat/page.tsx +5 -0
  72. package/app/globals.css +84 -0
  73. package/app/h5/layout.tsx +5 -0
  74. package/app/h5/mobile-approvals-page.tsx +167 -0
  75. package/app/h5/mobile-chat-page.tsx +951 -0
  76. package/app/h5/mobile-profile-page.tsx +147 -0
  77. package/app/h5/mobile-tickets-page.tsx +121 -0
  78. package/app/h5/page.tsx +23 -0
  79. package/app/h5/ticket-action-buttons.tsx +80 -0
  80. package/app/layout.tsx +26 -0
  81. package/app/login/page.tsx +318 -0
  82. package/app/market/knowledge/[id]/page.tsx +77 -0
  83. package/app/market/knowledge/page.tsx +358 -0
  84. package/app/market/layout.tsx +29 -0
  85. package/app/market/page.tsx +18 -0
  86. package/app/market/skills/page.tsx +397 -0
  87. package/app/my/files/page.tsx +511 -0
  88. package/app/my/knowledge/[id]/page.tsx +271 -0
  89. package/app/my/knowledge/new/page.tsx +234 -0
  90. package/app/my/knowledge/page.tsx +248 -0
  91. package/app/my/layout.tsx +32 -0
  92. package/app/my/memory/page.tsx +164 -0
  93. package/app/my/page.tsx +18 -0
  94. package/app/my/scheduled-tasks/[id]/edit/page.tsx +290 -0
  95. package/app/my/scheduled-tasks/[id]/executions/page.tsx +275 -0
  96. package/app/my/scheduled-tasks/[id]/page.tsx +284 -0
  97. package/app/my/scheduled-tasks/new/page.tsx +230 -0
  98. package/app/my/scheduled-tasks/page.tsx +27 -0
  99. package/app/my/skills/[name]/page.tsx +320 -0
  100. package/app/my/skills/new/page.tsx +394 -0
  101. package/app/my/skills/page.tsx +303 -0
  102. package/app/page.tsx +2288 -0
  103. package/app/share/[sessionId]/page.tsx +226 -0
  104. package/app/share/file/[id]/page.tsx +140 -0
  105. package/bin/README.md +63 -0
  106. package/bin/generate-api-system +300 -0
  107. package/bin/postinstall.js +95 -0
  108. package/bin/work-agent.js +173 -0
  109. package/components/ai-elements/agent.tsx +142 -0
  110. package/components/ai-elements/artifact.tsx +149 -0
  111. package/components/ai-elements/attachments.tsx +427 -0
  112. package/components/ai-elements/audio-player.tsx +232 -0
  113. package/components/ai-elements/canvas.tsx +26 -0
  114. package/components/ai-elements/chain-of-thought.tsx +223 -0
  115. package/components/ai-elements/checkpoint.tsx +72 -0
  116. package/components/ai-elements/code-block.tsx +555 -0
  117. package/components/ai-elements/commit.tsx +449 -0
  118. package/components/ai-elements/confirmation.tsx +173 -0
  119. package/components/ai-elements/connection.tsx +28 -0
  120. package/components/ai-elements/context.tsx +410 -0
  121. package/components/ai-elements/controls.tsx +19 -0
  122. package/components/ai-elements/conversation.tsx +167 -0
  123. package/components/ai-elements/edge.tsx +144 -0
  124. package/components/ai-elements/environment-variables.tsx +325 -0
  125. package/components/ai-elements/file-tree.tsx +298 -0
  126. package/components/ai-elements/image.tsx +25 -0
  127. package/components/ai-elements/inline-citation.tsx +294 -0
  128. package/components/ai-elements/jsx-preview.tsx +250 -0
  129. package/components/ai-elements/message.tsx +367 -0
  130. package/components/ai-elements/mic-selector.tsx +372 -0
  131. package/components/ai-elements/model-selector.tsx +214 -0
  132. package/components/ai-elements/node.tsx +72 -0
  133. package/components/ai-elements/open-in-chat.tsx +367 -0
  134. package/components/ai-elements/package-info.tsx +235 -0
  135. package/components/ai-elements/panel.tsx +16 -0
  136. package/components/ai-elements/persona.tsx +280 -0
  137. package/components/ai-elements/plan.tsx +144 -0
  138. package/components/ai-elements/prompt-input.tsx +1341 -0
  139. package/components/ai-elements/queue.tsx +275 -0
  140. package/components/ai-elements/reasoning.tsx +355 -0
  141. package/components/ai-elements/sandbox.tsx +133 -0
  142. package/components/ai-elements/schema-display.tsx +473 -0
  143. package/components/ai-elements/shimmer.tsx +78 -0
  144. package/components/ai-elements/snippet.tsx +141 -0
  145. package/components/ai-elements/sources.tsx +78 -0
  146. package/components/ai-elements/speech-input.tsx +324 -0
  147. package/components/ai-elements/stack-trace.tsx +531 -0
  148. package/components/ai-elements/suggestion.tsx +58 -0
  149. package/components/ai-elements/task.tsx +88 -0
  150. package/components/ai-elements/terminal.tsx +277 -0
  151. package/components/ai-elements/test-results.tsx +497 -0
  152. package/components/ai-elements/tool.tsx +174 -0
  153. package/components/ai-elements/toolbar.tsx +17 -0
  154. package/components/ai-elements/transcription.tsx +126 -0
  155. package/components/ai-elements/voice-selector.tsx +525 -0
  156. package/components/ai-elements/web-preview.tsx +282 -0
  157. package/components/audit-log-list.tsx +114 -0
  158. package/components/chat/EmptyPreviewState.tsx +12 -0
  159. package/components/chat/KnowledgePickerDialog.tsx +464 -0
  160. package/components/chat/KnowledgePreview.tsx +70 -0
  161. package/components/chat/KnowledgePreviewPanel.tsx +86 -0
  162. package/components/chat/MentionInput.tsx +309 -0
  163. package/components/chat/OrganizeDialog.tsx +258 -0
  164. package/components/chat/RecommendationBanner.tsx +94 -0
  165. package/components/chat/SaveToKnowledgeDialog.tsx +193 -0
  166. package/components/chat/SkillSelector.tsx +305 -0
  167. package/components/chat/SkillSwitcher.tsx +163 -0
  168. package/components/client-layout.tsx +15 -0
  169. package/components/knowledge/KnowledgeMetadataPanel.tsx +293 -0
  170. package/components/layout-wrapper.tsx +18 -0
  171. package/components/mobile-layout.tsx +62 -0
  172. package/components/scheduled-task-list.tsx +356 -0
  173. package/components/setup-guide.tsx +484 -0
  174. package/components/sub-nav.tsx +54 -0
  175. package/components/ticket-detail-content.tsx +383 -0
  176. package/components/ticket-list.tsx +366 -0
  177. package/components/top-nav.tsx +132 -0
  178. package/components/ui/accordion.tsx +58 -0
  179. package/components/ui/alert.tsx +59 -0
  180. package/components/ui/avatar.tsx +50 -0
  181. package/components/ui/badge.tsx +36 -0
  182. package/components/ui/button-group.tsx +83 -0
  183. package/components/ui/button.tsx +57 -0
  184. package/components/ui/card.tsx +91 -0
  185. package/components/ui/carousel.tsx +262 -0
  186. package/components/ui/collapsible.tsx +11 -0
  187. package/components/ui/command.tsx +153 -0
  188. package/components/ui/dialog.tsx +122 -0
  189. package/components/ui/dropdown-menu.tsx +200 -0
  190. package/components/ui/hover-card.tsx +29 -0
  191. package/components/ui/input-group.tsx +170 -0
  192. package/components/ui/input.tsx +22 -0
  193. package/components/ui/label.tsx +26 -0
  194. package/components/ui/popover.tsx +31 -0
  195. package/components/ui/progress.tsx +28 -0
  196. package/components/ui/scroll-area.tsx +48 -0
  197. package/components/ui/select.tsx +174 -0
  198. package/components/ui/separator.tsx +31 -0
  199. package/components/ui/spinner.tsx +16 -0
  200. package/components/ui/switch.tsx +29 -0
  201. package/components/ui/table.tsx +120 -0
  202. package/components/ui/tabs.tsx +55 -0
  203. package/components/ui/textarea.tsx +22 -0
  204. package/components/ui/tooltip.tsx +30 -0
  205. package/components/welcome-guide.tsx +182 -0
  206. package/components.json +24 -0
  207. package/lib/command-parser.ts +331 -0
  208. package/lib/dangerous-commands.ts +672 -0
  209. package/lib/db.ts +2250 -0
  210. package/lib/feishu-auth.ts +135 -0
  211. package/lib/file-storage.ts +306 -0
  212. package/lib/file-tool.ts +583 -0
  213. package/lib/knowledge-tool.ts +152 -0
  214. package/lib/knowledge-types.ts +66 -0
  215. package/lib/market-client.ts +313 -0
  216. package/lib/market-db.ts +736 -0
  217. package/lib/market-types.ts +51 -0
  218. package/lib/memory-tool.ts +211 -0
  219. package/lib/memory.ts +197 -0
  220. package/lib/pi-config.ts +436 -0
  221. package/lib/pi-session.ts +799 -0
  222. package/lib/pinyin.ts +13 -0
  223. package/lib/recommendation.ts +227 -0
  224. package/lib/risk-estimator.ts +350 -0
  225. package/lib/scheduled-task-tool.ts +184 -0
  226. package/lib/scheduler-init.ts +43 -0
  227. package/lib/scheduler.ts +416 -0
  228. package/lib/secure-bash-tool.ts +413 -0
  229. package/lib/skill-engine.ts +396 -0
  230. package/lib/skill-generator.ts +269 -0
  231. package/lib/skill-loader.ts +234 -0
  232. package/lib/skill-tool.ts +188 -0
  233. package/lib/skill-types.ts +82 -0
  234. package/lib/skills-init.ts +58 -0
  235. package/lib/ticket-tool.ts +246 -0
  236. package/lib/user-skill-types.ts +30 -0
  237. package/lib/user-skills.ts +362 -0
  238. package/lib/utils.ts +6 -0
  239. package/lib/workflow.ts +154 -0
  240. package/lib/zip-tool.ts +191 -0
  241. package/next.config.js +8 -0
  242. package/package.json +106 -0
  243. package/public/.gitkeep +1 -0
  244. package/public/icon.svg +1 -0
  245. package/tsconfig.json +42 -0
@@ -0,0 +1,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
+ }