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,309 @@
1
+ 'use client';
2
+
3
+ import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
4
+ import { AtSign, Sparkles, X, Paperclip, Loader2 } from 'lucide-react';
5
+
6
+ interface Skill {
7
+ name: string;
8
+ displayName?: string;
9
+ description?: string;
10
+ }
11
+
12
+ interface AttachedFile {
13
+ id: string;
14
+ name: string;
15
+ size: number;
16
+ }
17
+
18
+ interface MentionInputProps {
19
+ skills: Skill[];
20
+ value: string;
21
+ onChange: (value: string) => void;
22
+ onSend: () => void;
23
+ disabled?: boolean;
24
+ attachedFiles?: AttachedFile[];
25
+ onFileAttach?: (file: File) => void;
26
+ onFileRemove?: (fileId: string) => void;
27
+ uploading?: boolean;
28
+ }
29
+
30
+ function formatFileSize(bytes: number): string {
31
+ if (bytes === 0) return '0 B';
32
+ const k = 1024;
33
+ const sizes = ['B', 'KB', 'MB'];
34
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
35
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
36
+ }
37
+
38
+ export function MentionInput({
39
+ skills,
40
+ value,
41
+ onChange,
42
+ onSend,
43
+ disabled,
44
+ attachedFiles = [],
45
+ onFileAttach,
46
+ onFileRemove,
47
+ uploading = false,
48
+ }: MentionInputProps) {
49
+ const [showMentions, setShowMentions] = useState(false);
50
+ const [mentionIndex, setMentionIndex] = useState(0);
51
+ const [mentionQuery, setMentionQuery] = useState('');
52
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
53
+ const mentionsRef = useRef<HTMLDivElement>(null);
54
+ const fileInputRef = useRef<HTMLInputElement>(null);
55
+
56
+ const detectedMention = useMemo(() => {
57
+ const cursor = textareaRef.current?.selectionStart || 0;
58
+ const textBefore = value.slice(0, cursor);
59
+ const match = textBefore.match(/@([\w\u4e00-\u9fa5]*)$/);
60
+ if (match) {
61
+ return {
62
+ query: match[1],
63
+ startIndex: cursor - match[1].length - 1,
64
+ };
65
+ }
66
+ return null;
67
+ }, [value]);
68
+
69
+ const filteredSkills = useMemo(() => {
70
+ if (!detectedMention) return [];
71
+ const query = mentionQuery || detectedMention.query;
72
+ if (!query) return skills.slice(0, 5);
73
+ return skills.filter(skill => {
74
+ const name = (skill.displayName || skill.name).toLowerCase();
75
+ const q = query.toLowerCase();
76
+ return name.includes(q) || skill.name.toLowerCase().includes(q);
77
+ }).slice(0, 5);
78
+ }, [skills, detectedMention, mentionQuery]);
79
+
80
+ const selectSkill = useCallback((skill: Skill) => {
81
+ if (!detectedMention) return;
82
+
83
+ const before = value.slice(0, detectedMention.startIndex);
84
+ const after = value.slice(textareaRef.current?.selectionStart || 0);
85
+ const skillRef = `@${skill.name}`;
86
+ onChange(`${before}${skillRef}${after}`);
87
+ setShowMentions(false);
88
+ textareaRef.current?.focus();
89
+ }, [value, detectedMention, onChange]);
90
+
91
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
92
+ if (showMentions && filteredSkills.length > 0) {
93
+ if (e.key === 'Escape') {
94
+ e.preventDefault();
95
+ setShowMentions(false);
96
+ return;
97
+ }
98
+
99
+ if (e.key === 'ArrowDown') {
100
+ e.preventDefault();
101
+ setMentionIndex(i => Math.min(i + 1, filteredSkills.length - 1));
102
+ return;
103
+ }
104
+ if (e.key === 'ArrowUp') {
105
+ e.preventDefault();
106
+ setMentionIndex(i => Math.max(i - 1, 0));
107
+ return;
108
+ }
109
+ if (e.key === 'Enter') {
110
+ e.preventDefault();
111
+ selectSkill(filteredSkills[mentionIndex]);
112
+ return;
113
+ }
114
+ }
115
+
116
+ if (e.key === 'Enter' && !e.shiftKey) {
117
+ e.preventDefault();
118
+ onSend();
119
+ }
120
+ }, [showMentions, filteredSkills, mentionIndex, onSend, selectSkill]);
121
+
122
+ const extractMentions = useCallback((text: string): string[] => {
123
+ const skills: string[] = [];
124
+ const skillMatches = text.matchAll(/@(\w+)/g);
125
+ for (const match of skillMatches) {
126
+ skills.push(match[1]);
127
+ }
128
+ return skills;
129
+ }, []);
130
+
131
+ useEffect(() => {
132
+ if (detectedMention) {
133
+ setShowMentions(true);
134
+ setMentionQuery(detectedMention.query);
135
+ setMentionIndex(0);
136
+ } else {
137
+ setShowMentions(false);
138
+ }
139
+ }, [detectedMention]);
140
+
141
+ const mentionedSkills = useMemo(() => {
142
+ return extractMentions(value);
143
+ }, [value, extractMentions]);
144
+
145
+ const activeSkills = useMemo(() => {
146
+ return mentionedSkills
147
+ .map(s => {
148
+ const skill = skills.find(sk => sk.name === s);
149
+ return skill ? { name: s, displayName: skill.displayName || s } : null;
150
+ })
151
+ .filter(Boolean) as { name: string; displayName: string }[];
152
+ }, [mentionedSkills, skills]);
153
+
154
+ return (
155
+ <div className="relative">
156
+ {activeSkills.length > 0 && (
157
+ <div className="flex flex-wrap items-center gap-2 mb-2">
158
+ <span className="text-xs text-gray-500 flex items-center gap-1">
159
+ <Sparkles className="w-3 h-3" />
160
+ 已激活:
161
+ </span>
162
+ {activeSkills.map((item) => (
163
+ <span
164
+ key={item.name}
165
+ className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-purple-100 text-purple-700"
166
+ >
167
+ @{item.displayName}
168
+ </span>
169
+ ))}
170
+ </div>
171
+ )}
172
+
173
+ {attachedFiles.length > 0 && (
174
+ <div className="flex flex-wrap items-center gap-2 mb-2">
175
+ <span className="text-xs text-gray-500 flex items-center gap-1">
176
+ <Paperclip className="w-3 h-3" />
177
+ 附件:
178
+ </span>
179
+ {attachedFiles.map((file) => (
180
+ <span
181
+ key={file.id}
182
+ className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-blue-100 text-blue-700"
183
+ >
184
+ {file.name} ({formatFileSize(file.size)})
185
+ {onFileRemove && (
186
+ <button
187
+ type="button"
188
+ onClick={() => onFileRemove(file.id)}
189
+ className="ml-1 hover:text-blue-900"
190
+ >
191
+ <X className="w-3 h-3" />
192
+ </button>
193
+ )}
194
+ </span>
195
+ ))}
196
+ </div>
197
+ )}
198
+
199
+ <div className="relative">
200
+ <textarea
201
+ ref={textareaRef}
202
+ value={value}
203
+ onChange={(e) => onChange(e.target.value)}
204
+ onKeyDown={handleKeyDown}
205
+ placeholder={disabled ? 'AI 正在思考...' : '输入问题或命令... (输入 @ 呼出 Skill 列表)'}
206
+ disabled={disabled}
207
+ rows={1}
208
+ className="w-full px-3 py-2 pr-24 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 resize-none min-h-[40px] max-h-[120px]"
209
+ />
210
+
211
+ <div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1">
212
+ {onFileAttach && (
213
+ <>
214
+ <button
215
+ type="button"
216
+ onClick={() => fileInputRef.current?.click()}
217
+ disabled={disabled || uploading}
218
+ className="p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors"
219
+ title="添加附件"
220
+ >
221
+ {uploading ? (
222
+ <Loader2 className="w-4 h-4 animate-spin" />
223
+ ) : (
224
+ <Paperclip className="w-4 h-4" />
225
+ )}
226
+ </button>
227
+ <input
228
+ ref={fileInputRef}
229
+ type="file"
230
+ className="hidden"
231
+ onChange={(e) => {
232
+ const file = e.target.files?.[0];
233
+ if (file) {
234
+ onFileAttach(file);
235
+ }
236
+ e.target.value = '';
237
+ }}
238
+ />
239
+ </>
240
+ )}
241
+ <button
242
+ type="button"
243
+ onClick={() => {
244
+ const cursor = textareaRef.current?.selectionStart || 0;
245
+ const before = value.slice(0, cursor);
246
+ onChange(`${before}@`);
247
+ setTimeout(() => textareaRef.current?.focus(), 0);
248
+ }}
249
+ disabled={disabled}
250
+ className="p-1.5 text-gray-400 hover:text-purple-600 hover:bg-purple-50 rounded transition-colors"
251
+ title="插入 @ 提及"
252
+ >
253
+ <AtSign className="w-4 h-4" />
254
+ </button>
255
+ </div>
256
+ </div>
257
+
258
+ {showMentions && filteredSkills.length > 0 && (
259
+ <>
260
+ <div
261
+ className="fixed inset-0 z-40"
262
+ onClick={() => setShowMentions(false)}
263
+ />
264
+ <div
265
+ ref={mentionsRef}
266
+ className="absolute bottom-full left-0 mb-2 w-80 bg-white border border-gray-200 rounded-lg shadow-lg z-50 overflow-hidden"
267
+ >
268
+ <div className="px-3 py-2 bg-gray-50 border-b border-gray-100">
269
+ <span className="text-xs text-gray-500">
270
+ 选择要 @ 提及的 Skill
271
+ </span>
272
+ </div>
273
+ <div className="max-h-80 overflow-y-auto">
274
+ {filteredSkills.map((skill, index) => (
275
+ <button
276
+ key={`skill-${skill.name}`}
277
+ type="button"
278
+ onClick={() => selectSkill(skill)}
279
+ className={`w-full px-3 py-2 text-left flex items-start gap-2 hover:bg-purple-50 transition-colors ${
280
+ index === mentionIndex ? 'bg-purple-50' : ''
281
+ }`}
282
+ >
283
+ <span className="w-6 h-6 rounded bg-purple-100 flex items-center justify-center text-purple-600 text-xs font-medium shrink-0">
284
+ @{skill.name.charAt(0).toUpperCase()}
285
+ </span>
286
+ <div className="min-w-0">
287
+ <div className="font-medium text-sm text-gray-900 truncate">
288
+ {skill.displayName || skill.name}
289
+ </div>
290
+ {skill.description && (
291
+ <div className="text-xs text-gray-500 truncate">
292
+ {skill.description}
293
+ </div>
294
+ )}
295
+ </div>
296
+ </button>
297
+ ))}
298
+ </div>
299
+ <div className="px-3 py-1.5 bg-gray-50 border-t border-gray-100 text-xs text-gray-400 flex items-center justify-between">
300
+ <span>↑↓ 选择</span>
301
+ <span>↵ 确认</span>
302
+ <span>Esc 关闭</span>
303
+ </div>
304
+ </div>
305
+ </>
306
+ )}
307
+ </div>
308
+ );
309
+ }
@@ -0,0 +1,258 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { Sparkles, Loader2, AlertCircle } from 'lucide-react';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
7
+
8
+ interface Message {
9
+ role: 'user' | 'assistant' | 'system';
10
+ content: string;
11
+ }
12
+
13
+ interface OrganizeDialogProps {
14
+ sessionId: string;
15
+ userId: string;
16
+ messages: Message[];
17
+ open: boolean;
18
+ onOpenChange: (open: boolean) => void;
19
+ onConverted?: () => void;
20
+ }
21
+
22
+ interface PendingKnowledge {
23
+ title: string;
24
+ content: string;
25
+ category: string;
26
+ tags: string[];
27
+ type: string;
28
+ }
29
+
30
+ const ANALYSIS_PROMPT = `你是一个知识整理助手。请分析以下会话内容,提取有价值的知识并以JSON格式返回。
31
+
32
+ 会话内容:
33
+ {messages}
34
+
35
+ 请直接返回以下JSON格式(不要有其他内容):
36
+ {{
37
+ "title": "知识标题(简洁明了,50字内)",
38
+ "content": "知识内容(问题分析 + 解决方案,完整详细,使用 Markdown 格式)",
39
+ "category": "分类(运维/开发/排查/其他)",
40
+ "tags": ["标签1", "标签2"]
41
+ }}
42
+
43
+ 注意:
44
+ - title 要简洁,能概括核心内容
45
+ - content 要完整,包含问题描述、解决步骤、最佳实践
46
+ - category 只选一个:运维、开发、排查、其他
47
+ - tags 选2-4个标签`;
48
+
49
+ const PROGRESS_MESSAGES = [
50
+ '正在分析会话...',
51
+ '正在提取关键信息...',
52
+ '正在生成知识内容...',
53
+ '正在整理知识结构...',
54
+ ];
55
+
56
+ export function OrganizeDialog({
57
+ sessionId,
58
+ userId,
59
+ messages,
60
+ open,
61
+ onOpenChange,
62
+ onConverted
63
+ }: OrganizeDialogProps) {
64
+ const [loading, setLoading] = useState(false);
65
+ const [error, setError] = useState('');
66
+ const [progressIndex, setProgressIndex] = useState(0);
67
+
68
+ useEffect(() => {
69
+ if (!open) {
70
+ setError('');
71
+ setProgressIndex(0);
72
+ } else {
73
+ handleStartAnalysis();
74
+ }
75
+ }, [open]);
76
+
77
+ useEffect(() => {
78
+ if (!loading) return;
79
+
80
+ const interval = setInterval(() => {
81
+ setProgressIndex(prev => {
82
+ if (prev < PROGRESS_MESSAGES.length - 1) {
83
+ return prev + 1;
84
+ }
85
+ return prev;
86
+ });
87
+ }, 2000);
88
+
89
+ return () => clearInterval(interval);
90
+ }, [loading]);
91
+
92
+ const handleStartAnalysis = async () => {
93
+ setLoading(true);
94
+ setError('');
95
+ setProgressIndex(0);
96
+
97
+ const formattedMessages = messages
98
+ .map((m) => `${m.role === 'user' ? '用户' : 'AI'}:${m.content}`)
99
+ .join('\n\n');
100
+
101
+ const prompt = ANALYSIS_PROMPT.replace('{messages}', formattedMessages);
102
+
103
+ try {
104
+ const response = await fetch('/api/pi-chat', {
105
+ method: 'POST',
106
+ headers: { 'Content-Type': 'application/json' },
107
+ body: JSON.stringify({
108
+ message: prompt,
109
+ userId,
110
+ }),
111
+ });
112
+
113
+ if (!response.ok) {
114
+ throw new Error('请求失败');
115
+ }
116
+
117
+ const reader = response.body?.getReader();
118
+ if (!reader) {
119
+ throw new Error('无法读取响应');
120
+ }
121
+
122
+ const decoder = new TextDecoder();
123
+ let accumulatedContent = '';
124
+
125
+ while (true) {
126
+ const { done, value } = await reader.read();
127
+ if (done) break;
128
+
129
+ const chunk = decoder.decode(value, { stream: true });
130
+ const lines = chunk.split('\n');
131
+
132
+ for (const line of lines) {
133
+ if (line.startsWith('data: ')) {
134
+ try {
135
+ const event = JSON.parse(line.slice(6));
136
+
137
+ if (event.type === 'text') {
138
+ accumulatedContent += event.content;
139
+ }
140
+
141
+ if (event.type === 'agent_end') {
142
+ const messages = event.messages || [];
143
+ const lastMessage = messages[messages.length - 1];
144
+
145
+ // 正确提取文本内容
146
+ let fullContent = accumulatedContent;
147
+ if (lastMessage?.content) {
148
+ if (typeof lastMessage.content === 'string') {
149
+ fullContent = lastMessage.content;
150
+ } else if (Array.isArray(lastMessage.content)) {
151
+ // 处理 [{ type: 'text', text: '...' }] 格式
152
+ fullContent = lastMessage.content
153
+ .map((c: any) => (typeof c === 'string' ? c : c?.text || ''))
154
+ .join('');
155
+ }
156
+ }
157
+
158
+ parseAndNavigate(fullContent);
159
+ return;
160
+ }
161
+ } catch {
162
+ // 忽略解析错误
163
+ }
164
+ }
165
+ }
166
+ }
167
+
168
+ if (accumulatedContent) {
169
+ parseAndNavigate(accumulatedContent);
170
+ }
171
+ } catch (err: any) {
172
+ console.error('Analysis error:', err);
173
+ setError(err.message || '分析失败,请重试');
174
+ } finally {
175
+ setLoading(false);
176
+ }
177
+ };
178
+
179
+ const parseAndNavigate = (content: string) => {
180
+ try {
181
+ console.log('[OrganizeDialog] Parsing content:', content.substring(0, 200));
182
+
183
+ const jsonMatch = content.match(/\{[\s\S]*\}/);
184
+ let pendingKnowledge: PendingKnowledge;
185
+
186
+ if (jsonMatch) {
187
+ const data = JSON.parse(jsonMatch[0]);
188
+ console.log('[OrganizeDialog] Parsed JSON:', data);
189
+
190
+ pendingKnowledge = {
191
+ title: data.title || '会话总结',
192
+ content: typeof data.content === 'string' ? data.content : JSON.stringify(data.content),
193
+ category: data.category || '其他',
194
+ tags: Array.isArray(data.tags) ? data.tags : [],
195
+ type: 'conversation-summary',
196
+ };
197
+ } else {
198
+ pendingKnowledge = {
199
+ title: '会话总结',
200
+ content: content,
201
+ category: '其他',
202
+ tags: [],
203
+ type: 'conversation-summary',
204
+ };
205
+ }
206
+
207
+ console.log('[OrganizeDialog] Saving to localStorage:', pendingKnowledge);
208
+ localStorage.setItem('pendingKnowledge', JSON.stringify(pendingKnowledge));
209
+ onOpenChange(false);
210
+ onConverted?.();
211
+ window.location.href = '/my/knowledge/new';
212
+ } catch (err) {
213
+ console.error('[OrganizeDialog] Parse error:', err);
214
+ setError('解析失败,请重试');
215
+ }
216
+ };
217
+
218
+ return (
219
+ <Dialog open={open} onOpenChange={onOpenChange}>
220
+ <DialogContent className="max-w-md">
221
+ <DialogHeader>
222
+ <DialogTitle className="flex items-center gap-2">
223
+ <Sparkles className="w-5 h-5" />
224
+ 整理保存
225
+ </DialogTitle>
226
+ </DialogHeader>
227
+
228
+ <div className="py-8">
229
+ {loading ? (
230
+ <div className="text-center">
231
+ <Loader2 className="w-10 h-10 mx-auto mb-4 animate-spin text-blue-500" />
232
+ <p className="text-gray-600 font-medium">{PROGRESS_MESSAGES[progressIndex]}</p>
233
+ <p className="text-gray-400 text-sm mt-2">即将跳转到编辑页面</p>
234
+ </div>
235
+ ) : error ? (
236
+ <div className="text-center">
237
+ <AlertCircle className="w-10 h-10 mx-auto mb-4 text-red-500" />
238
+ <p className="text-red-500 font-medium">{error}</p>
239
+ <div className="flex justify-center gap-3 mt-4">
240
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
241
+ 取消
242
+ </Button>
243
+ <Button onClick={handleStartAnalysis}>
244
+ 重试
245
+ </Button>
246
+ </div>
247
+ </div>
248
+ ) : (
249
+ <div className="text-center">
250
+ <Loader2 className="w-8 h-8 mx-auto mb-3 animate-spin text-blue-500" />
251
+ <p className="text-gray-500">准备分析...</p>
252
+ </div>
253
+ )}
254
+ </div>
255
+ </DialogContent>
256
+ </Dialog>
257
+ );
258
+ }
@@ -0,0 +1,94 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { Sparkles, ArrowRight, X } from 'lucide-react';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Card, CardContent } from '@/components/ui/card';
7
+
8
+ interface RecommendedSkill {
9
+ name: string;
10
+ displayName?: string;
11
+ description?: string;
12
+ score: number;
13
+ reason: string;
14
+ }
15
+
16
+ interface RecommendationBannerProps {
17
+ userId: string;
18
+ onInstallAndUse: (skillName: string) => void;
19
+ }
20
+
21
+ export function RecommendationBanner({ userId, onInstallAndUse }: RecommendationBannerProps) {
22
+ const [recommendations, setRecommendations] = useState<RecommendedSkill[]>([]);
23
+ const [loading, setLoading] = useState(false);
24
+ const [dismissed, setDismissed] = useState(false);
25
+
26
+ useEffect(() => {
27
+ if (!userId || dismissed) return;
28
+
29
+ const fetchRecommendations = async () => {
30
+ setLoading(true);
31
+ try {
32
+ const res = await fetch(`/api/recommend?type=personalized&userId=${userId}&limit=3`);
33
+ if (res.ok) {
34
+ const data = await res.json();
35
+ setRecommendations(data.recommendations || []);
36
+ }
37
+ } catch (error) {
38
+ console.error('Failed to fetch recommendations:', error);
39
+ } finally {
40
+ setLoading(false);
41
+ }
42
+ };
43
+
44
+ // 延迟加载,让用户先开始对话
45
+ const timer = setTimeout(fetchRecommendations, 3000);
46
+ return () => clearTimeout(timer);
47
+ }, [userId, dismissed]);
48
+
49
+ if (loading || recommendations.length === 0 || dismissed) {
50
+ return null;
51
+ }
52
+
53
+ return (
54
+ <Card className="mb-4 bg-gradient-to-r from-blue-50 to-purple-50 border-blue-200">
55
+ <CardContent className="p-4">
56
+ <div className="flex items-start gap-3">
57
+ <Sparkles className="w-5 h-5 text-blue-600 mt-0.5" />
58
+ <div className="flex-1">
59
+ <div className="flex items-center justify-between mb-2">
60
+ <h4 className="font-medium text-sm text-blue-900">推荐Skill</h4>
61
+ <button
62
+ onClick={() => setDismissed(true)}
63
+ className="text-gray-400 hover:text-gray-600"
64
+ >
65
+ <X className="w-4 h-4" />
66
+ </button>
67
+ </div>
68
+ <div className="space-y-2">
69
+ {recommendations.slice(0, 2).map((skill) => (
70
+ <div
71
+ key={skill.name}
72
+ className="flex items-center justify-between bg-white rounded-lg p-2 border"
73
+ >
74
+ <div>
75
+ <div className="text-sm font-medium">{skill.displayName || skill.name}</div>
76
+ <div className="text-xs text-gray-500">{skill.reason}</div>
77
+ </div>
78
+ <Button
79
+ size="sm"
80
+ variant="outline"
81
+ onClick={() => onInstallAndUse(skill.name)}
82
+ >
83
+ 安装并使用
84
+ <ArrowRight className="w-3 h-3 ml-1" />
85
+ </Button>
86
+ </div>
87
+ ))}
88
+ </div>
89
+ </div>
90
+ </div>
91
+ </CardContent>
92
+ </Card>
93
+ );
94
+ }