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,396 @@
1
+ /**
2
+ * Skill Engine
3
+ *
4
+ * Manages skill execution, command routing, and risk assessment.
5
+ */
6
+
7
+ import { exec, execSync, spawn, SpawnOptions } from 'child_process';
8
+ import { promisify } from 'util';
9
+ import { existsSync, readFileSync } from 'fs';
10
+ import { join, dirname } from 'path';
11
+ import { parseCommand, extractAffectedResources, type ParsedCommand } from './command-parser';
12
+ import { estimateRisk, type RiskLevel, getTimeoutForRisk } from './risk-estimator';
13
+
14
+ const execAsync = promisify(exec);
15
+
16
+ export type CommandStatus = 'pending' | 'approved' | 'rejected' | 'executing' | 'completed' | 'failed';
17
+
18
+ export interface SkillResult {
19
+ success: boolean;
20
+ output?: string;
21
+ error?: string;
22
+ exitCode?: number;
23
+ executionTime?: number;
24
+ }
25
+
26
+ export interface ExecutionRequest {
27
+ command: string;
28
+ skillName?: string;
29
+ userId?: string;
30
+ sessionId?: string;
31
+ requireApproval?: boolean;
32
+ /** Force execution even for dangerous commands (used for approved tickets) */
33
+ forceExecute?: boolean;
34
+ }
35
+
36
+ export interface ExecutionResult {
37
+ ticketId?: string;
38
+ status: CommandStatus;
39
+ result?: SkillResult;
40
+ riskLevel: RiskLevel;
41
+ requiresApproval: boolean;
42
+ affectedResources?: string[];
43
+ command: string;
44
+ skillName?: string;
45
+ }
46
+
47
+ export interface SkillInfo {
48
+ name: string;
49
+ displayName?: string;
50
+ description?: string;
51
+ version?: string;
52
+ commandPatterns?: string[];
53
+ }
54
+
55
+ let skillCache: Map<string, SkillInfo> = new Map();
56
+
57
+ export async function processCommand(request: ExecutionRequest): Promise<ExecutionResult> {
58
+ const { command, skillName, userId, sessionId, forceExecute } = request;
59
+
60
+ // Parse the command
61
+ const parsed = parseCommand(command);
62
+
63
+ // Estimate risk
64
+ const riskAssessment = estimateRisk(command, skillName);
65
+
66
+ // Extract affected resources
67
+ const affectedResources = extractAffectedResources(parsed);
68
+
69
+ // Check if approval is required
70
+ const requiresApproval = riskAssessment.requiresApproval || riskAssessment.shouldBlock;
71
+
72
+ // If not forcing execution, check approval requirements
73
+ if (!forceExecute) {
74
+ // If blocking (critical risk), return immediately without execution
75
+ if (riskAssessment.shouldBlock) {
76
+ return {
77
+ status: 'rejected',
78
+ riskLevel: riskAssessment.riskLevel,
79
+ requiresApproval: true,
80
+ affectedResources,
81
+ command,
82
+ skillName,
83
+ result: {
84
+ success: false,
85
+ error: `Command blocked: ${riskAssessment.reason}`,
86
+ },
87
+ };
88
+ }
89
+
90
+ // If approval required, return pending status
91
+ if (requiresApproval) {
92
+ return {
93
+ ticketId: generateTicketId(),
94
+ status: 'pending',
95
+ riskLevel: riskAssessment.riskLevel,
96
+ requiresApproval: true,
97
+ affectedResources,
98
+ command,
99
+ skillName,
100
+ };
101
+ }
102
+ }
103
+
104
+ // Execute the command directly (low/medium risk without approval requirement, or forced execution)
105
+ const result = await executeCommand(command, riskAssessment.riskLevel);
106
+
107
+ return {
108
+ status: result.success ? 'completed' : 'failed',
109
+ riskLevel: riskAssessment.riskLevel,
110
+ requiresApproval: false,
111
+ affectedResources,
112
+ command,
113
+ skillName,
114
+ result,
115
+ };
116
+ }
117
+
118
+ export async function executeCommand(
119
+ command: string,
120
+ riskLevel: RiskLevel
121
+ ): Promise<SkillResult> {
122
+ const startTime = Date.now();
123
+ const timeout = getTimeoutForRisk(riskLevel);
124
+
125
+ try {
126
+ // Determine if this is a complex command that needs spawning
127
+ const needsSpawn = command.includes('|') || command.includes('&&') || command.includes('||') || command.includes('>') || command.includes('<');
128
+
129
+ if (needsSpawn) {
130
+ return await spawnCommand(command, timeout, startTime);
131
+ } else {
132
+ const { stdout, stderr } = await execAsync(command, {
133
+ timeout,
134
+ maxBuffer: 10 * 1024 * 1024, // 10MB
135
+ });
136
+
137
+ return {
138
+ success: true,
139
+ output: stdout || stderr,
140
+ executionTime: Date.now() - startTime,
141
+ };
142
+ }
143
+ } catch (error: any) {
144
+ return {
145
+ success: false,
146
+ error: error.message,
147
+ exitCode: error.exitCode,
148
+ executionTime: Date.now() - startTime,
149
+ };
150
+ }
151
+ }
152
+
153
+ async function spawnCommand(
154
+ command: string,
155
+ timeout: number,
156
+ startTime: number
157
+ ): Promise<SkillResult> {
158
+ return new Promise((resolve) => {
159
+ const child = spawn(command, {
160
+ shell: '/bin/bash',
161
+ timeout,
162
+ });
163
+
164
+ let stdout = '';
165
+ let stderr = '';
166
+
167
+ child.stdout?.on('data', (data) => {
168
+ stdout += data.toString();
169
+ });
170
+
171
+ child.stderr?.on('data', (data) => {
172
+ stderr += data.toString();
173
+ });
174
+
175
+ child.on('close', (code) => {
176
+ resolve({
177
+ success: code === 0,
178
+ output: stdout || stderr,
179
+ exitCode: code || undefined,
180
+ executionTime: Date.now() - startTime,
181
+ });
182
+ });
183
+
184
+ child.on('error', (error) => {
185
+ resolve({
186
+ success: false,
187
+ error: error.message,
188
+ executionTime: Date.now() - startTime,
189
+ });
190
+ });
191
+
192
+ // Set timeout
193
+ setTimeout(() => {
194
+ child.kill('SIGTERM');
195
+ resolve({
196
+ success: false,
197
+ error: 'Command timed out',
198
+ executionTime: Date.now() - startTime,
199
+ });
200
+ }, timeout);
201
+ });
202
+ }
203
+
204
+ export function loadSkillInfo(skillName: string): SkillInfo | null {
205
+ // Check cache first
206
+ if (skillCache.has(skillName)) {
207
+ return skillCache.get(skillName)!;
208
+ }
209
+
210
+ const skillDir = join(process.cwd(), '.pi', 'skills', skillName);
211
+ const skillJsonPath = join(skillDir, 'SKILL.json');
212
+ const skillMdPath = join(skillDir, 'SKILL.md');
213
+
214
+ // Try to load from SKILL.json first
215
+ if (existsSync(skillJsonPath)) {
216
+ try {
217
+ const content = readFileSync(skillJsonPath, 'utf-8');
218
+ const skillInfo = JSON.parse(content);
219
+ skillCache.set(skillName, skillInfo);
220
+ return skillInfo;
221
+ } catch (error) {
222
+ console.error(`Failed to load skill JSON for ${skillName}:`, error);
223
+ }
224
+ }
225
+
226
+ // Fallback: try to load from SKILL.md frontmatter
227
+ if (existsSync(skillMdPath)) {
228
+ try {
229
+ const content = readFileSync(skillMdPath, 'utf-8');
230
+ const skillInfo = parseSkillMarkdown(skillName, content);
231
+ if (skillInfo) {
232
+ skillCache.set(skillName, skillInfo);
233
+ return skillInfo;
234
+ }
235
+ } catch (error) {
236
+ console.error(`Failed to load skill markdown for ${skillName}:`, error);
237
+ }
238
+ }
239
+
240
+ return null;
241
+ }
242
+
243
+ /**
244
+ * Parse skill info from SKILL.md frontmatter
245
+ */
246
+ function parseSkillMarkdown(skillName: string, content: string): SkillInfo | null {
247
+ // Parse YAML frontmatter
248
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
249
+ if (!frontmatterMatch) {
250
+ // No frontmatter, create basic info from name
251
+ return {
252
+ name: skillName,
253
+ displayName: skillName,
254
+ description: `Skill: ${skillName}`,
255
+ };
256
+ }
257
+
258
+ const frontmatter = frontmatterMatch[1];
259
+ const info: SkillInfo = { name: skillName };
260
+
261
+ // Parse simple YAML key-value pairs
262
+ const lines = frontmatter.split('\n');
263
+ for (const line of lines) {
264
+ const match = line.match(/^(\w+):\s*(.+)$/);
265
+ if (match) {
266
+ const [, key, value] = match;
267
+ const cleanValue = value.replace(/^["']|["']$/g, '').trim();
268
+
269
+ switch (key) {
270
+ case 'name':
271
+ info.name = cleanValue;
272
+ break;
273
+ case 'displayName':
274
+ case 'display_name':
275
+ info.displayName = cleanValue;
276
+ break;
277
+ case 'description':
278
+ info.description = cleanValue;
279
+ break;
280
+ case 'version':
281
+ info.version = cleanValue;
282
+ break;
283
+ case 'commandPatterns':
284
+ // Handle array format
285
+ if (cleanValue.startsWith('[')) {
286
+ try {
287
+ info.commandPatterns = JSON.parse(cleanValue);
288
+ } catch {
289
+ // Ignore parse errors
290
+ }
291
+ }
292
+ break;
293
+ }
294
+ }
295
+ }
296
+
297
+ // Set defaults
298
+ if (!info.displayName) {
299
+ info.displayName = skillName;
300
+ }
301
+ if (!info.description) {
302
+ // Try to extract first paragraph from content
303
+ const bodyMatch = content.match(/---[\s\S]*?---\n\n(.+?)(?:\n\n|$)/);
304
+ info.description = bodyMatch ? bodyMatch[1].substring(0, 200) : `Skill: ${skillName}`;
305
+ }
306
+
307
+ return info;
308
+ }
309
+
310
+ export function loadSkillMarkdown(skillName: string): string | null {
311
+ const skillMdPath = join(process.cwd(), '.pi', 'skills', skillName, 'SKILL.md');
312
+
313
+ if (!existsSync(skillMdPath)) {
314
+ return null;
315
+ }
316
+
317
+ try {
318
+ return readFileSync(skillMdPath, 'utf-8');
319
+ } catch (error) {
320
+ console.error(`Failed to load skill markdown for ${skillName}:`, error);
321
+ return null;
322
+ }
323
+ }
324
+
325
+ export function getAvailableSkills(): SkillInfo[] {
326
+ const skills: SkillInfo[] = [];
327
+ const skillsDir = join(process.cwd(), '.pi', 'skills');
328
+
329
+ if (!existsSync(skillsDir)) {
330
+ return skills;
331
+ }
332
+
333
+ const entries = require('fs').readdirSync(skillsDir, { withFileTypes: true });
334
+
335
+ for (const entry of entries) {
336
+ if (entry.isDirectory()) {
337
+ const skillInfo = loadSkillInfo(entry.name);
338
+ if (skillInfo) {
339
+ skills.push(skillInfo);
340
+ }
341
+ }
342
+ }
343
+
344
+ return skills;
345
+ }
346
+
347
+ export function matchSkill(command: string): string | null {
348
+ const skills = getAvailableSkills();
349
+
350
+ for (const skill of skills) {
351
+ if (skill.commandPatterns) {
352
+ for (const pattern of skill.commandPatterns) {
353
+ const regex = new RegExp(pattern, 'i');
354
+ if (regex.test(command)) {
355
+ return skill.name;
356
+ }
357
+ }
358
+ }
359
+ }
360
+
361
+ return null;
362
+ }
363
+
364
+ export function clearSkillCache(): void {
365
+ skillCache.clear();
366
+ }
367
+
368
+ function generateTicketId(): string {
369
+ const timestamp = Date.now().toString(36);
370
+ const random = Math.random().toString(36).substring(2, 8);
371
+ return `T-${timestamp}-${random}`;
372
+ }
373
+
374
+ export interface CommandPreview {
375
+ command: string;
376
+ riskLevel: RiskLevel;
377
+ requiresApproval: boolean;
378
+ affectedResources: string[];
379
+ skillName?: string;
380
+ reason: string;
381
+ }
382
+
383
+ export function previewCommand(command: string, skillName?: string): CommandPreview {
384
+ const riskAssessment = estimateRisk(command, skillName);
385
+ const parsed = parseCommand(command);
386
+ const affectedResources = extractAffectedResources(parsed);
387
+
388
+ return {
389
+ command,
390
+ riskLevel: riskAssessment.riskLevel,
391
+ requiresApproval: riskAssessment.requiresApproval || riskAssessment.shouldBlock,
392
+ affectedResources,
393
+ skillName,
394
+ reason: riskAssessment.reason,
395
+ };
396
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * 通用Skill生成器
3
+ *
4
+ * 根据请求生成符合通用规范的skill文件(SKILL.json + SKILL.md)
5
+ */
6
+
7
+ import { writeFileSync, existsSync, mkdirSync } from 'fs';
8
+ import { join } from 'path';
9
+ import type {
10
+ CreateSkillRequest,
11
+ CreateSkillResult,
12
+ SkillType,
13
+ SkillMetadata
14
+ } from './skill-types';
15
+ import { installUserSkill } from './db';
16
+
17
+ /**
18
+ * 自动检测skill类型
19
+ */
20
+ export function detectSkillType(description: string): SkillType {
21
+ const lower = description.toLowerCase();
22
+
23
+ if (lower.includes('workflow') ||
24
+ lower.includes('pipeline') ||
25
+ lower.includes('multi-step') ||
26
+ lower.includes('自动化流程') ||
27
+ lower.includes('多步骤')) {
28
+ return 'workflow';
29
+ }
30
+
31
+ if (lower.includes('kubectl') ||
32
+ lower.includes('docker') ||
33
+ lower.includes('helm') ||
34
+ lower.includes('git ') ||
35
+ lower.includes('命令') ||
36
+ lower.includes('command')) {
37
+ return 'command';
38
+ }
39
+
40
+ return 'documentation'; // 默认类型
41
+ }
42
+
43
+ /**
44
+ * 验证skill名称格式
45
+ */
46
+ export function validateSkillName(name: string): { valid: boolean; error?: string } {
47
+ // 必须是小写字母、数字和短横线
48
+ if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(name)) {
49
+ return {
50
+ valid: false,
51
+ error: 'Skill名称必须由小写字母、数字和短横线组成,且不能以短横线开头或结尾'
52
+ };
53
+ }
54
+
55
+ // 长度限制
56
+ if (name.length < 2 || name.length > 50) {
57
+ return {
58
+ valid: false,
59
+ error: 'Skill名称长度必须在2-50个字符之间'
60
+ };
61
+ }
62
+
63
+ return { valid: true };
64
+ }
65
+
66
+ /**
67
+ * 生成SKILL.json内容
68
+ */
69
+ export function generateSkillJson(request: CreateSkillRequest): SkillMetadata {
70
+ const type = request.type || detectSkillType(request.description);
71
+
72
+ const metadata: SkillMetadata = {
73
+ name: request.name,
74
+ displayName: request.displayName,
75
+ description: request.description,
76
+ version: '1.0.0',
77
+ author: request.author || 'user',
78
+ type: type,
79
+ createdAt: new Date().toISOString(),
80
+ };
81
+
82
+ // 添加命令模式(如果有)
83
+ if (request.commandPatterns && request.commandPatterns.length > 0) {
84
+ metadata.commandPatterns = request.commandPatterns;
85
+ }
86
+
87
+ // 添加风险配置(如果有)
88
+ if (request.riskConfig) {
89
+ metadata.riskConfig = request.riskConfig;
90
+ }
91
+
92
+ return metadata;
93
+ }
94
+
95
+ /**
96
+ * 生成SKILL.md内容
97
+ */
98
+ export function generateSkillMarkdown(request: CreateSkillRequest): string {
99
+ const type = request.type || detectSkillType(request.description);
100
+
101
+ let content = `---
102
+ name: ${request.name}
103
+ description: ${request.description}
104
+ version: 1.0.0
105
+ author: ${request.author || 'user'}
106
+ type: ${type}
107
+ ---
108
+
109
+ # ${request.displayName}
110
+
111
+ ${request.description}
112
+
113
+ `;
114
+
115
+ // 添加类型说明
116
+ content += `## 类型\n\n`;
117
+ switch (type) {
118
+ case 'documentation':
119
+ content += `此Skill为**知识型**,为AI提供领域知识和指令。\n\n`;
120
+ break;
121
+ case 'command':
122
+ content += `此Skill为**命令型**,定义了命令模式和风险配置。\n\n`;
123
+ break;
124
+ case 'workflow':
125
+ content += `此Skill为**工作流型**,定义了多步骤自动化操作。\n\n`;
126
+ break;
127
+ case 'hybrid':
128
+ content += `此Skill为**混合型**,包含多种类型的特性。\n\n`;
129
+ break;
130
+ }
131
+
132
+ // 添加知识内容
133
+ if (request.knowledge) {
134
+ content += `## 知识与指令\n\n${request.knowledge}\n\n`;
135
+ }
136
+
137
+ // 添加命令模式
138
+ if (request.commandPatterns && request.commandPatterns.length > 0) {
139
+ content += `## 命令模式\n\n`;
140
+ content += `此Skill识别以下命令模式:\n\n`;
141
+ request.commandPatterns.forEach(p => {
142
+ content += `- \`${p}\`\n`;
143
+ });
144
+ content += '\n';
145
+ }
146
+
147
+ // 添加示例
148
+ if (request.examples && request.examples.length > 0) {
149
+ content += `## 示例\n\n`;
150
+ request.examples.forEach((ex, index) => {
151
+ content += `### 示例 ${index + 1}: ${ex.description}\n\n`;
152
+ content += `\`\`\`bash\n${ex.command}\n\`\`\`\n`;
153
+ if (ex.riskLevel) {
154
+ content += `\n**风险级别:** ${ex.riskLevel}\n`;
155
+ }
156
+ content += '\n';
157
+ });
158
+ }
159
+
160
+ // 添加风险配置
161
+ if (request.riskConfig) {
162
+ content += `## 风险配置\n\n`;
163
+ content += `**默认风险级别:** ${request.riskConfig.default}\n\n`;
164
+
165
+ if (request.riskConfig.overrides && Object.keys(request.riskConfig.overrides).length > 0) {
166
+ content += `### 命令风险覆盖\n\n`;
167
+ content += `| 命令模式 | 风险级别 |\n|----------|----------|\n`;
168
+ Object.entries(request.riskConfig.overrides).forEach(([cmd, level]) => {
169
+ content += `| \`${cmd}\` | ${level} |\n`;
170
+ });
171
+ content += '\n';
172
+ }
173
+ }
174
+
175
+ // 添加使用说明
176
+ content += `## 使用方式\n\n`;
177
+ content += `在会话中激活此Skill后,AI将根据上述配置处理相关请求。\n`;
178
+
179
+ return content;
180
+ }
181
+
182
+ /**
183
+ * 创建skill主函数
184
+ */
185
+ export async function createSkill(request: CreateSkillRequest): Promise<CreateSkillResult> {
186
+ // 根据是否有 userId 决定保存到用户目录还是系统目录
187
+ const baseDir = request.userId
188
+ ? join(process.cwd(), 'data', 'users', request.userId, 'skills')
189
+ : join(process.cwd(), '.pi', 'skills');
190
+
191
+ const skillDir = join(baseDir, request.name);
192
+
193
+ // 验证名称格式
194
+ const nameValidation = validateSkillName(request.name);
195
+ if (!nameValidation.valid) {
196
+ return {
197
+ success: false,
198
+ skillName: request.name,
199
+ skillPath: skillDir,
200
+ type: request.type || 'documentation',
201
+ files: [],
202
+ error: nameValidation.error,
203
+ };
204
+ }
205
+
206
+ // 检查是否已存在
207
+ if (existsSync(skillDir)) {
208
+ return {
209
+ success: false,
210
+ skillName: request.name,
211
+ skillPath: skillDir,
212
+ type: request.type || detectSkillType(request.description),
213
+ files: [],
214
+ error: `Skill '${request.name}' already exists at ${skillDir}`,
215
+ };
216
+ }
217
+
218
+ try {
219
+ // 创建目录
220
+ mkdirSync(skillDir, { recursive: true });
221
+
222
+ // 生成文件
223
+ const skillJson = generateSkillJson(request);
224
+ const skillMd = generateSkillMarkdown(request);
225
+
226
+ writeFileSync(
227
+ join(skillDir, 'SKILL.json'),
228
+ JSON.stringify(skillJson, null, 2)
229
+ );
230
+ writeFileSync(
231
+ join(skillDir, 'SKILL.md'),
232
+ skillMd
233
+ );
234
+
235
+ const detectedType = request.type || detectSkillType(request.description);
236
+
237
+ // 如果是用户技能,写入数据库
238
+ if (request.userId) {
239
+ try {
240
+ installUserSkill(request.userId, request.name, 'personal', {
241
+ displayName: request.displayName || request.name,
242
+ description: request.description,
243
+ });
244
+ } catch (dbError: any) {
245
+ console.error(`[skill-generator] Failed to write to database: ${dbError.message}`);
246
+ // 不影响返回结果,文件已创建成功
247
+ }
248
+ }
249
+
250
+ console.log(`[skill-generator] Created skill '${request.name}' (type: ${detectedType}) at ${skillDir}`);
251
+
252
+ return {
253
+ success: true,
254
+ skillName: request.name,
255
+ skillPath: skillDir,
256
+ type: detectedType,
257
+ files: ['SKILL.json', 'SKILL.md'],
258
+ };
259
+ } catch (error: any) {
260
+ return {
261
+ success: false,
262
+ skillName: request.name,
263
+ skillPath: skillDir,
264
+ type: request.type || 'documentation',
265
+ files: [],
266
+ error: `Failed to create skill: ${error.message}`,
267
+ };
268
+ }
269
+ }