work-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +234 -0
- package/app/(admin)/approvals/page.tsx +16 -0
- package/app/(admin)/audit/page.tsx +18 -0
- package/app/(admin)/layout.tsx +47 -0
- package/app/(admin)/scheduled-tasks/page.tsx +17 -0
- package/app/(admin)/settings/page.tsx +46 -0
- package/app/(admin)/skills/[name]/page.tsx +378 -0
- package/app/(admin)/skills/page.tsx +406 -0
- package/app/(admin)/statistics/page.tsx +416 -0
- package/app/(admin)/tickets/[id]/page.tsx +348 -0
- package/app/(admin)/tickets/new/page.tsx +309 -0
- package/app/(admin)/tickets/page.tsx +27 -0
- package/app/api/audit/route.ts +30 -0
- package/app/api/auth/feishu/callback/route.ts +72 -0
- package/app/api/auth/feishu/login/route.ts +17 -0
- package/app/api/auth/feishu/sso/route.ts +78 -0
- package/app/api/auth/login/route.ts +85 -0
- package/app/api/auth/oauth/route.ts +168 -0
- package/app/api/config/providers/route.ts +105 -0
- package/app/api/config/route.ts +115 -0
- package/app/api/config/status/route.ts +56 -0
- package/app/api/config/test/route.ts +212 -0
- package/app/api/documents/[id]/route.ts +88 -0
- package/app/api/documents/route.ts +53 -0
- package/app/api/health/route.ts +32 -0
- package/app/api/knowledge/[id]/route.ts +152 -0
- package/app/api/knowledge/from-session/route.ts +27 -0
- package/app/api/knowledge/route.ts +100 -0
- package/app/api/market/knowledge/[id]/route.ts +92 -0
- package/app/api/market/knowledge/route.ts +130 -0
- package/app/api/marketplace/skills/[id]/approve/route.ts +68 -0
- package/app/api/marketplace/skills/[id]/certify/route.ts +54 -0
- package/app/api/marketplace/skills/[id]/install/route.ts +180 -0
- package/app/api/marketplace/skills/[id]/promote-to-system/route.ts +219 -0
- package/app/api/marketplace/skills/[id]/rate/route.ts +90 -0
- package/app/api/marketplace/skills/[id]/ratings/route.ts +55 -0
- package/app/api/marketplace/skills/[id]/reject/route.ts +68 -0
- package/app/api/marketplace/skills/[id]/route.ts +177 -0
- package/app/api/marketplace/skills/route.ts +235 -0
- package/app/api/memory/route.ts +40 -0
- package/app/api/my/files/[id]/route.ts +52 -0
- package/app/api/my/files/route.ts +230 -0
- package/app/api/my/knowledge/route.ts +36 -0
- package/app/api/pi-chat/route.ts +443 -0
- package/app/api/recommend/route.ts +38 -0
- package/app/api/scheduled-tasks/[id]/execute/route.ts +132 -0
- package/app/api/scheduled-tasks/[id]/route.ts +165 -0
- package/app/api/scheduled-tasks/[id]/toggle/route.ts +53 -0
- package/app/api/scheduled-tasks/route.ts +101 -0
- package/app/api/sessions/[id]/messages/route.ts +212 -0
- package/app/api/sessions/route.ts +101 -0
- package/app/api/share/file/[id]/route.ts +37 -0
- package/app/api/skills/[name]/execute/route.ts +121 -0
- package/app/api/skills/[name]/route.ts +167 -0
- package/app/api/skills/create/route.ts +65 -0
- package/app/api/skills/generate/route.ts +405 -0
- package/app/api/skills/installed/route.ts +151 -0
- package/app/api/skills/route.ts +174 -0
- package/app/api/skills/translate/route.ts +40 -0
- package/app/api/skills/user/[name]/route.ts +159 -0
- package/app/api/skills/user/route.ts +90 -0
- package/app/api/statistics/route.ts +94 -0
- package/app/api/task-executions/[id]/route.ts +34 -0
- package/app/api/task-executions/route.ts +29 -0
- package/app/api/tickets/[id]/approve/route.ts +129 -0
- package/app/api/tickets/[id]/execute/route.ts +201 -0
- package/app/api/tickets/[id]/route.ts +127 -0
- package/app/api/tickets/route.ts +103 -0
- package/app/api/user/skills/route.ts +175 -0
- package/app/api/users/route.ts +80 -0
- package/app/chat/page.tsx +5 -0
- package/app/globals.css +84 -0
- package/app/h5/layout.tsx +5 -0
- package/app/h5/mobile-approvals-page.tsx +167 -0
- package/app/h5/mobile-chat-page.tsx +951 -0
- package/app/h5/mobile-profile-page.tsx +147 -0
- package/app/h5/mobile-tickets-page.tsx +121 -0
- package/app/h5/page.tsx +23 -0
- package/app/h5/ticket-action-buttons.tsx +80 -0
- package/app/layout.tsx +26 -0
- package/app/login/page.tsx +318 -0
- package/app/market/knowledge/[id]/page.tsx +77 -0
- package/app/market/knowledge/page.tsx +358 -0
- package/app/market/layout.tsx +29 -0
- package/app/market/page.tsx +18 -0
- package/app/market/skills/page.tsx +397 -0
- package/app/my/files/page.tsx +511 -0
- package/app/my/knowledge/[id]/page.tsx +271 -0
- package/app/my/knowledge/new/page.tsx +234 -0
- package/app/my/knowledge/page.tsx +248 -0
- package/app/my/layout.tsx +32 -0
- package/app/my/memory/page.tsx +164 -0
- package/app/my/page.tsx +18 -0
- package/app/my/scheduled-tasks/[id]/edit/page.tsx +290 -0
- package/app/my/scheduled-tasks/[id]/executions/page.tsx +275 -0
- package/app/my/scheduled-tasks/[id]/page.tsx +284 -0
- package/app/my/scheduled-tasks/new/page.tsx +230 -0
- package/app/my/scheduled-tasks/page.tsx +27 -0
- package/app/my/skills/[name]/page.tsx +320 -0
- package/app/my/skills/new/page.tsx +394 -0
- package/app/my/skills/page.tsx +303 -0
- package/app/page.tsx +2288 -0
- package/app/share/[sessionId]/page.tsx +226 -0
- package/app/share/file/[id]/page.tsx +140 -0
- package/bin/README.md +63 -0
- package/bin/generate-api-system +300 -0
- package/bin/postinstall.js +95 -0
- package/bin/work-agent.js +173 -0
- package/components/ai-elements/agent.tsx +142 -0
- package/components/ai-elements/artifact.tsx +149 -0
- package/components/ai-elements/attachments.tsx +427 -0
- package/components/ai-elements/audio-player.tsx +232 -0
- package/components/ai-elements/canvas.tsx +26 -0
- package/components/ai-elements/chain-of-thought.tsx +223 -0
- package/components/ai-elements/checkpoint.tsx +72 -0
- package/components/ai-elements/code-block.tsx +555 -0
- package/components/ai-elements/commit.tsx +449 -0
- package/components/ai-elements/confirmation.tsx +173 -0
- package/components/ai-elements/connection.tsx +28 -0
- package/components/ai-elements/context.tsx +410 -0
- package/components/ai-elements/controls.tsx +19 -0
- package/components/ai-elements/conversation.tsx +167 -0
- package/components/ai-elements/edge.tsx +144 -0
- package/components/ai-elements/environment-variables.tsx +325 -0
- package/components/ai-elements/file-tree.tsx +298 -0
- package/components/ai-elements/image.tsx +25 -0
- package/components/ai-elements/inline-citation.tsx +294 -0
- package/components/ai-elements/jsx-preview.tsx +250 -0
- package/components/ai-elements/message.tsx +367 -0
- package/components/ai-elements/mic-selector.tsx +372 -0
- package/components/ai-elements/model-selector.tsx +214 -0
- package/components/ai-elements/node.tsx +72 -0
- package/components/ai-elements/open-in-chat.tsx +367 -0
- package/components/ai-elements/package-info.tsx +235 -0
- package/components/ai-elements/panel.tsx +16 -0
- package/components/ai-elements/persona.tsx +280 -0
- package/components/ai-elements/plan.tsx +144 -0
- package/components/ai-elements/prompt-input.tsx +1341 -0
- package/components/ai-elements/queue.tsx +275 -0
- package/components/ai-elements/reasoning.tsx +355 -0
- package/components/ai-elements/sandbox.tsx +133 -0
- package/components/ai-elements/schema-display.tsx +473 -0
- package/components/ai-elements/shimmer.tsx +78 -0
- package/components/ai-elements/snippet.tsx +141 -0
- package/components/ai-elements/sources.tsx +78 -0
- package/components/ai-elements/speech-input.tsx +324 -0
- package/components/ai-elements/stack-trace.tsx +531 -0
- package/components/ai-elements/suggestion.tsx +58 -0
- package/components/ai-elements/task.tsx +88 -0
- package/components/ai-elements/terminal.tsx +277 -0
- package/components/ai-elements/test-results.tsx +497 -0
- package/components/ai-elements/tool.tsx +174 -0
- package/components/ai-elements/toolbar.tsx +17 -0
- package/components/ai-elements/transcription.tsx +126 -0
- package/components/ai-elements/voice-selector.tsx +525 -0
- package/components/ai-elements/web-preview.tsx +282 -0
- package/components/audit-log-list.tsx +114 -0
- package/components/chat/EmptyPreviewState.tsx +12 -0
- package/components/chat/KnowledgePickerDialog.tsx +464 -0
- package/components/chat/KnowledgePreview.tsx +70 -0
- package/components/chat/KnowledgePreviewPanel.tsx +86 -0
- package/components/chat/MentionInput.tsx +309 -0
- package/components/chat/OrganizeDialog.tsx +258 -0
- package/components/chat/RecommendationBanner.tsx +94 -0
- package/components/chat/SaveToKnowledgeDialog.tsx +193 -0
- package/components/chat/SkillSelector.tsx +305 -0
- package/components/chat/SkillSwitcher.tsx +163 -0
- package/components/client-layout.tsx +15 -0
- package/components/knowledge/KnowledgeMetadataPanel.tsx +293 -0
- package/components/layout-wrapper.tsx +18 -0
- package/components/mobile-layout.tsx +62 -0
- package/components/scheduled-task-list.tsx +356 -0
- package/components/setup-guide.tsx +484 -0
- package/components/sub-nav.tsx +54 -0
- package/components/ticket-detail-content.tsx +383 -0
- package/components/ticket-list.tsx +366 -0
- package/components/top-nav.tsx +132 -0
- package/components/ui/accordion.tsx +58 -0
- package/components/ui/alert.tsx +59 -0
- package/components/ui/avatar.tsx +50 -0
- package/components/ui/badge.tsx +36 -0
- package/components/ui/button-group.tsx +83 -0
- package/components/ui/button.tsx +57 -0
- package/components/ui/card.tsx +91 -0
- package/components/ui/carousel.tsx +262 -0
- package/components/ui/collapsible.tsx +11 -0
- package/components/ui/command.tsx +153 -0
- package/components/ui/dialog.tsx +122 -0
- package/components/ui/dropdown-menu.tsx +200 -0
- package/components/ui/hover-card.tsx +29 -0
- package/components/ui/input-group.tsx +170 -0
- package/components/ui/input.tsx +22 -0
- package/components/ui/label.tsx +26 -0
- package/components/ui/popover.tsx +31 -0
- package/components/ui/progress.tsx +28 -0
- package/components/ui/scroll-area.tsx +48 -0
- package/components/ui/select.tsx +174 -0
- package/components/ui/separator.tsx +31 -0
- package/components/ui/spinner.tsx +16 -0
- package/components/ui/switch.tsx +29 -0
- package/components/ui/table.tsx +120 -0
- package/components/ui/tabs.tsx +55 -0
- package/components/ui/textarea.tsx +22 -0
- package/components/ui/tooltip.tsx +30 -0
- package/components/welcome-guide.tsx +182 -0
- package/components.json +24 -0
- package/lib/command-parser.ts +331 -0
- package/lib/dangerous-commands.ts +672 -0
- package/lib/db.ts +2250 -0
- package/lib/feishu-auth.ts +135 -0
- package/lib/file-storage.ts +306 -0
- package/lib/file-tool.ts +583 -0
- package/lib/knowledge-tool.ts +152 -0
- package/lib/knowledge-types.ts +66 -0
- package/lib/market-client.ts +313 -0
- package/lib/market-db.ts +736 -0
- package/lib/market-types.ts +51 -0
- package/lib/memory-tool.ts +211 -0
- package/lib/memory.ts +197 -0
- package/lib/pi-config.ts +436 -0
- package/lib/pi-session.ts +799 -0
- package/lib/pinyin.ts +13 -0
- package/lib/recommendation.ts +227 -0
- package/lib/risk-estimator.ts +350 -0
- package/lib/scheduled-task-tool.ts +184 -0
- package/lib/scheduler-init.ts +43 -0
- package/lib/scheduler.ts +416 -0
- package/lib/secure-bash-tool.ts +413 -0
- package/lib/skill-engine.ts +396 -0
- package/lib/skill-generator.ts +269 -0
- package/lib/skill-loader.ts +234 -0
- package/lib/skill-tool.ts +188 -0
- package/lib/skill-types.ts +82 -0
- package/lib/skills-init.ts +58 -0
- package/lib/ticket-tool.ts +246 -0
- package/lib/user-skill-types.ts +30 -0
- package/lib/user-skills.ts +362 -0
- package/lib/utils.ts +6 -0
- package/lib/workflow.ts +154 -0
- package/lib/zip-tool.ts +191 -0
- package/next.config.js +8 -0
- package/package.json +106 -0
- package/public/.gitkeep +1 -0
- package/public/icon.svg +1 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,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
|
+
}
|