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,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工单创建工具
|
|
3
|
+
*
|
|
4
|
+
* 提供 create_ticket 工具,让 AI 可以创建需要审批的工单。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Type } from '@sinclair/typebox';
|
|
8
|
+
import type { ToolDefinition, AgentToolResult, ExtensionContext } from '@mariozechner/pi-coding-agent';
|
|
9
|
+
import { createTicket } from './db';
|
|
10
|
+
import { estimateRiskLevel } from './dangerous-commands';
|
|
11
|
+
import { getCurrentSessionId } from './pi-session';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 工单创建工具的参数 Schema
|
|
15
|
+
*/
|
|
16
|
+
const createTicketSchema = Type.Object({
|
|
17
|
+
command: Type.String({
|
|
18
|
+
description: '要执行的命令',
|
|
19
|
+
}),
|
|
20
|
+
title: Type.Optional(Type.String({
|
|
21
|
+
description: '工单标题(可选,默认根据命令生成)',
|
|
22
|
+
})),
|
|
23
|
+
description: Type.Optional(Type.String({
|
|
24
|
+
description: '工单描述(可选)',
|
|
25
|
+
})),
|
|
26
|
+
createdBy: Type.Optional(Type.String({
|
|
27
|
+
description: '创建者ID,AI应自动从会话上下文获取当前用户ID并传入',
|
|
28
|
+
})),
|
|
29
|
+
skillName: Type.Optional(Type.String({
|
|
30
|
+
description: '关联的 Skill 名称(可选)',
|
|
31
|
+
})),
|
|
32
|
+
affectedResources: Type.Optional(Type.Array(Type.String(), {
|
|
33
|
+
description: '受影响的资源列表(可选)',
|
|
34
|
+
})),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 工单创建工具的详情类型
|
|
39
|
+
*/
|
|
40
|
+
interface CreateTicketDetails {
|
|
41
|
+
success: boolean;
|
|
42
|
+
ticket?: {
|
|
43
|
+
id: string;
|
|
44
|
+
title: string;
|
|
45
|
+
type: string;
|
|
46
|
+
status: string;
|
|
47
|
+
priority: string;
|
|
48
|
+
command: string;
|
|
49
|
+
riskLevel: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
};
|
|
52
|
+
error?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 创建工单工具定义
|
|
57
|
+
*/
|
|
58
|
+
export const createTicketTool: ToolDefinition<typeof createTicketSchema, CreateTicketDetails> = {
|
|
59
|
+
name: 'create_ticket',
|
|
60
|
+
label: 'Create Ticket',
|
|
61
|
+
description: `创建一个需要审批的命令执行工单。
|
|
62
|
+
|
|
63
|
+
当需要执行危险或重要操作时,使用此工具创建工单。工单需要人工审批后才能执行。
|
|
64
|
+
|
|
65
|
+
危险命令示例:
|
|
66
|
+
- rm -rf 删除文件
|
|
67
|
+
- 系统关机/重启命令
|
|
68
|
+
- 数据库删除/修改操作
|
|
69
|
+
- 文件系统格式化
|
|
70
|
+
|
|
71
|
+
安全命令(不需要审批):
|
|
72
|
+
- ls/cat/head/tail 等文件查看
|
|
73
|
+
- git status/log 等只读操作
|
|
74
|
+
- echo/printf 等输出命令
|
|
75
|
+
|
|
76
|
+
注意:createdBy 参数必须传入当前用户ID,AI应自动从会话上下文获取。`,
|
|
77
|
+
|
|
78
|
+
parameters: createTicketSchema,
|
|
79
|
+
|
|
80
|
+
async execute(
|
|
81
|
+
toolCallId: string,
|
|
82
|
+
params: any,
|
|
83
|
+
signal: AbortSignal | undefined,
|
|
84
|
+
onUpdate: ((result: AgentToolResult<CreateTicketDetails>) => void) | undefined,
|
|
85
|
+
ctx: ExtensionContext
|
|
86
|
+
): Promise<AgentToolResult<CreateTicketDetails>> {
|
|
87
|
+
try {
|
|
88
|
+
const { command, title, description, createdBy, skillName, affectedResources } = params;
|
|
89
|
+
|
|
90
|
+
// 评估命令风险等级
|
|
91
|
+
const riskLevel = estimateRiskLevel(command);
|
|
92
|
+
|
|
93
|
+
// 生成默认标题
|
|
94
|
+
const ticketTitle = title || generateDefaultTitle(command, skillName);
|
|
95
|
+
|
|
96
|
+
// 生成默认描述
|
|
97
|
+
const ticketDescription = description || generateDefaultDescription(command, skillName, riskLevel);
|
|
98
|
+
|
|
99
|
+
// 创建工单
|
|
100
|
+
const ticket = await createTicket({
|
|
101
|
+
type: 'bash-execute',
|
|
102
|
+
title: ticketTitle,
|
|
103
|
+
description: ticketDescription,
|
|
104
|
+
status: 'pending',
|
|
105
|
+
priority: riskLevel as any,
|
|
106
|
+
command,
|
|
107
|
+
commandType: detectCommandType(command),
|
|
108
|
+
skillName,
|
|
109
|
+
riskLevel: riskLevel as any,
|
|
110
|
+
affectedResources: affectedResources || extractAffectedResources(command),
|
|
111
|
+
approvals: [],
|
|
112
|
+
createdBy: createdBy || 'ai-assistant',
|
|
113
|
+
aiSessionId: getCurrentSessionId() || undefined,
|
|
114
|
+
aiGenerated: true,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const result: AgentToolResult<CreateTicketDetails> = {
|
|
118
|
+
content: [
|
|
119
|
+
{
|
|
120
|
+
type: 'text',
|
|
121
|
+
text: `✅ 工单已创建
|
|
122
|
+
|
|
123
|
+
**工单 ID:** ${ticket.id}
|
|
124
|
+
**标题:** ${ticket.title}
|
|
125
|
+
**风险等级:** ${ticket.riskLevel}
|
|
126
|
+
**状态:** ${ticket.status}
|
|
127
|
+
|
|
128
|
+
此工单需要审批后才能执行。请在工单管理界面查看和审批。`,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
details: {
|
|
132
|
+
success: true,
|
|
133
|
+
ticket: {
|
|
134
|
+
id: ticket.id,
|
|
135
|
+
title: ticket.title,
|
|
136
|
+
type: ticket.type,
|
|
137
|
+
status: ticket.status,
|
|
138
|
+
priority: ticket.priority,
|
|
139
|
+
command: ticket.command || '',
|
|
140
|
+
riskLevel: ticket.riskLevel,
|
|
141
|
+
description: ticket.description,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// 通知更新
|
|
147
|
+
onUpdate?.(result);
|
|
148
|
+
|
|
149
|
+
return result;
|
|
150
|
+
} catch (error: any) {
|
|
151
|
+
const errorResult: AgentToolResult<CreateTicketDetails> = {
|
|
152
|
+
content: [
|
|
153
|
+
{
|
|
154
|
+
type: 'text',
|
|
155
|
+
text: `❌ 创建工单失败: ${error.message}`,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
details: {
|
|
159
|
+
success: false,
|
|
160
|
+
error: error.message,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
onUpdate?.(errorResult);
|
|
165
|
+
|
|
166
|
+
return errorResult;
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 生成默认工单标题
|
|
173
|
+
*/
|
|
174
|
+
function generateDefaultTitle(command: string, skillName?: string): string {
|
|
175
|
+
// 截取命令的前 50 个字符
|
|
176
|
+
const cmdPreview = command.split(' ').slice(0, 4).join(' ');
|
|
177
|
+
const truncated = cmdPreview.length > 50 ? cmdPreview.substring(0, 50) + '...' : cmdPreview;
|
|
178
|
+
|
|
179
|
+
if (skillName) {
|
|
180
|
+
return `[${skillName}] ${truncated}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return `执行命令: ${truncated}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 生成默认工单描述
|
|
188
|
+
*/
|
|
189
|
+
function generateDefaultDescription(command: string, skillName?: string, riskLevel?: string): string {
|
|
190
|
+
let desc = `## 命令详情\n\n\`\`\`bash\n${command}\n\`\`\`\n\n`;
|
|
191
|
+
|
|
192
|
+
if (skillName) {
|
|
193
|
+
desc += `**关联 Skill:** ${skillName}\n\n`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
desc += `**风险等级:** ${riskLevel || 'medium'}\n\n---\n\n此工单由 AI 助手创建,需要审批后才能执行。`;
|
|
197
|
+
|
|
198
|
+
return desc;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 检测命令类型
|
|
203
|
+
*/
|
|
204
|
+
function detectCommandType(command: string): string {
|
|
205
|
+
const trimmed = command.toLowerCase().trim();
|
|
206
|
+
|
|
207
|
+
if (trimmed.startsWith('kubectl')) return 'kubectl';
|
|
208
|
+
if (trimmed.startsWith('docker')) return 'docker';
|
|
209
|
+
if (trimmed.startsWith('helm')) return 'helm';
|
|
210
|
+
if (trimmed.startsWith('git')) return 'git';
|
|
211
|
+
if (trimmed.startsWith('npm') || trimmed.startsWith('yarn')) return 'package-manager';
|
|
212
|
+
if (trimmed.startsWith('curl') || trimmed.startsWith('wget')) return 'network';
|
|
213
|
+
|
|
214
|
+
return 'bash';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 从命令中提取受影响的资源
|
|
219
|
+
*/
|
|
220
|
+
function extractAffectedResources(command: string): string[] | undefined {
|
|
221
|
+
const resources: string[] = [];
|
|
222
|
+
|
|
223
|
+
// 提取 kubectl 资源
|
|
224
|
+
const kubectlMatch = command.match(/kubectl\s+.*?\s+(\S+)(?:\s+(-n|--namespace)\s+(\S+))?/);
|
|
225
|
+
if (kubectlMatch) {
|
|
226
|
+
const resource = kubectlMatch[1];
|
|
227
|
+
if (!resource.startsWith('-')) {
|
|
228
|
+
resources.push(resource);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 提取文件路径
|
|
233
|
+
const fileMatches = command.match(/(?:^|\s)(\/[\w\/.-]+)/g);
|
|
234
|
+
if (fileMatches) {
|
|
235
|
+
for (const match of fileMatches) {
|
|
236
|
+
const path = match.trim();
|
|
237
|
+
if (path.length > 1 && !path.startsWith('/dev/')) {
|
|
238
|
+
resources.push(path);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return resources.length > 0 ? resources : undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export default createTicketTool;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 用户私有技能类型定义
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface UserSkill {
|
|
6
|
+
name: string;
|
|
7
|
+
displayName: string;
|
|
8
|
+
description: string;
|
|
9
|
+
version: string;
|
|
10
|
+
author: string; // 用户ID
|
|
11
|
+
createdAt: string;
|
|
12
|
+
updatedAt: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CreateUserSkillRequest {
|
|
16
|
+
name: string;
|
|
17
|
+
displayName: string;
|
|
18
|
+
description: string;
|
|
19
|
+
content: string; // SKILL.md 内容
|
|
20
|
+
version?: string;
|
|
21
|
+
riskConfig?: any;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface UpdateUserSkillRequest {
|
|
25
|
+
displayName?: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
content?: string;
|
|
28
|
+
version?: string;
|
|
29
|
+
riskConfig?: any;
|
|
30
|
+
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 用户私有技能管理工具
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, unlinkSync, rmdirSync, readdirSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import type { UserSkill, CreateUserSkillRequest, UpdateUserSkillRequest } from './user-skill-types';
|
|
8
|
+
import { getDb } from './db';
|
|
9
|
+
|
|
10
|
+
const USER_SKILLS_BASE = join(process.cwd(), 'data', 'users');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 确保用户技能目录存在
|
|
14
|
+
*/
|
|
15
|
+
function ensureUserSkillsDir(userId: string): string {
|
|
16
|
+
const userDir = join(USER_SKILLS_BASE, userId, 'skills');
|
|
17
|
+
if (!existsSync(userDir)) {
|
|
18
|
+
mkdirSync(userDir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
return userDir;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 同步结果
|
|
25
|
+
*/
|
|
26
|
+
export interface SyncResult {
|
|
27
|
+
added: string[]; // 新增到数据库的技能
|
|
28
|
+
removed: string[]; // 从数据库删除的记录
|
|
29
|
+
unchanged: string[]; // 无变化的技能
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 同步用户技能到数据库
|
|
34
|
+
* - 扫描文件系统中的技能目录
|
|
35
|
+
* - 补全缺失的数据库记录
|
|
36
|
+
* - 清理已删除技能的数据库记录
|
|
37
|
+
*
|
|
38
|
+
* @param userId 用户ID
|
|
39
|
+
* @returns 同步结果
|
|
40
|
+
*/
|
|
41
|
+
export function syncUserSkills(userId: string): SyncResult {
|
|
42
|
+
const db = getDb();
|
|
43
|
+
const userDir = join(USER_SKILLS_BASE, userId, 'skills');
|
|
44
|
+
const result: SyncResult = { added: [], removed: [], unchanged: [] };
|
|
45
|
+
|
|
46
|
+
// 1. 获取文件系统中的技能
|
|
47
|
+
const fsSkills = new Map<string, { dirName: string; skillData: any }>();
|
|
48
|
+
if (existsSync(userDir)) {
|
|
49
|
+
const entries = readdirSync(userDir, { withFileTypes: true });
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (!entry.isDirectory()) continue;
|
|
52
|
+
|
|
53
|
+
const skillDir = join(userDir, entry.name);
|
|
54
|
+
const jsonPath = join(skillDir, 'SKILL.json');
|
|
55
|
+
|
|
56
|
+
if (!existsSync(jsonPath)) continue;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const jsonContent = readFileSync(jsonPath, 'utf-8');
|
|
60
|
+
const skillData = JSON.parse(jsonContent);
|
|
61
|
+
const skillName = skillData.name || entry.name;
|
|
62
|
+
fsSkills.set(skillName, { dirName: entry.name, skillData });
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error(`[syncUserSkills] Failed to read ${entry.name}:`, error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 2. 获取数据库中的技能
|
|
70
|
+
const dbSkills = new Map<string, { id: string; skillName: string; dirName: string }>();
|
|
71
|
+
const rows = db.prepare('SELECT id, skill_name, dir_name FROM user_skills WHERE user_id = ?').all(userId) as any[];
|
|
72
|
+
for (const row of rows) {
|
|
73
|
+
dbSkills.set(row.skill_name, { id: row.id, skillName: row.skill_name, dirName: row.dir_name });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 3. 对比差异
|
|
77
|
+
const now = new Date().toISOString();
|
|
78
|
+
|
|
79
|
+
// 文件存在但数据库无记录 → 插入
|
|
80
|
+
for (const [skillName, { dirName, skillData }] of fsSkills) {
|
|
81
|
+
if (!dbSkills.has(skillName)) {
|
|
82
|
+
const skillId = `us-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
83
|
+
db.prepare(`
|
|
84
|
+
INSERT INTO user_skills (id, user_id, skill_name, dir_name, source, use_count, installed_at)
|
|
85
|
+
VALUES (?, ?, ?, ?, 'personal', 0, ?)
|
|
86
|
+
`).run(skillId, userId, skillName, dirName, now);
|
|
87
|
+
result.added.push(skillName);
|
|
88
|
+
console.log(`[syncUserSkills] Added: ${skillName}`);
|
|
89
|
+
} else {
|
|
90
|
+
result.unchanged.push(skillName);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 数据库有记录但文件不存在 → 删除
|
|
95
|
+
for (const [skillName, { id }] of dbSkills) {
|
|
96
|
+
if (!fsSkills.has(skillName)) {
|
|
97
|
+
db.prepare('DELETE FROM user_skills WHERE id = ?').run(id);
|
|
98
|
+
result.removed.push(skillName);
|
|
99
|
+
console.log(`[syncUserSkills] Removed: ${skillName}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (result.added.length > 0 || result.removed.length > 0) {
|
|
104
|
+
console.log(`[syncUserSkills] Synced for ${userId}: +${result.added.length}, -${result.removed.length}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 创建用户私有技能
|
|
112
|
+
*/
|
|
113
|
+
export function createUserSkill(userId: string, request: CreateUserSkillRequest): UserSkill {
|
|
114
|
+
const db = getDb();
|
|
115
|
+
|
|
116
|
+
// 目录名直接使用 name(不再拼音转换)
|
|
117
|
+
const skillName = request.name;
|
|
118
|
+
const userDir = ensureUserSkillsDir(userId);
|
|
119
|
+
const skillDir = join(userDir, skillName);
|
|
120
|
+
|
|
121
|
+
// 检查是否已存在
|
|
122
|
+
if (existsSync(skillDir)) {
|
|
123
|
+
throw new Error(`Skill "${skillName}" already exists`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 创建技能目录
|
|
127
|
+
mkdirSync(skillDir, { recursive: true });
|
|
128
|
+
|
|
129
|
+
const now = new Date().toISOString();
|
|
130
|
+
const version = request.version || '1.0.0';
|
|
131
|
+
|
|
132
|
+
// 写入 SKILL.json
|
|
133
|
+
const skillJson = {
|
|
134
|
+
name: skillName,
|
|
135
|
+
displayName: request.displayName || skillName,
|
|
136
|
+
description: request.description,
|
|
137
|
+
version,
|
|
138
|
+
author: userId,
|
|
139
|
+
type: 'user',
|
|
140
|
+
createdAt: now,
|
|
141
|
+
updatedAt: now,
|
|
142
|
+
...(request.riskConfig && { riskConfig: request.riskConfig }),
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
writeFileSync(join(skillDir, 'SKILL.json'), JSON.stringify(skillJson, null, 2));
|
|
146
|
+
|
|
147
|
+
// 写入 SKILL.md(符合 agentskills.io 规范,包含 YAML frontmatter)
|
|
148
|
+
const skillMdWithFrontmatter = `---
|
|
149
|
+
name: ${skillName}
|
|
150
|
+
description: ${request.description}
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
${request.content}`;
|
|
154
|
+
writeFileSync(join(skillDir, 'SKILL.md'), skillMdWithFrontmatter);
|
|
155
|
+
|
|
156
|
+
// 保存到数据库
|
|
157
|
+
const skillId = `us-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
158
|
+
db.prepare(`
|
|
159
|
+
INSERT INTO user_skills (id, user_id, skill_name, dir_name, source, use_count, installed_at)
|
|
160
|
+
VALUES (?, ?, ?, ?, 'personal', 0, ?)
|
|
161
|
+
`).run(skillId, userId, skillName, skillName, now);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
name: skillName,
|
|
165
|
+
displayName: request.displayName || skillName,
|
|
166
|
+
description: request.description,
|
|
167
|
+
version,
|
|
168
|
+
author: userId,
|
|
169
|
+
createdAt: now,
|
|
170
|
+
updatedAt: now,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 获取用户私有技能列表
|
|
176
|
+
*/
|
|
177
|
+
export function listUserSkills(userId: string): UserSkill[] {
|
|
178
|
+
const userDir = join(USER_SKILLS_BASE, userId, 'skills');
|
|
179
|
+
|
|
180
|
+
if (!existsSync(userDir)) {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const skills: UserSkill[] = [];
|
|
185
|
+
const entries = readdirSync(userDir, { withFileTypes: true });
|
|
186
|
+
|
|
187
|
+
for (const entry of entries) {
|
|
188
|
+
if (!entry.isDirectory()) continue;
|
|
189
|
+
|
|
190
|
+
const skillDir = join(userDir, entry.name);
|
|
191
|
+
const jsonPath = join(skillDir, 'SKILL.json');
|
|
192
|
+
|
|
193
|
+
if (!existsSync(jsonPath)) continue;
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const jsonContent = readFileSync(jsonPath, 'utf-8');
|
|
197
|
+
const skillData = JSON.parse(jsonContent);
|
|
198
|
+
|
|
199
|
+
skills.push({
|
|
200
|
+
name: skillData.name,
|
|
201
|
+
displayName: skillData.displayName,
|
|
202
|
+
description: skillData.description,
|
|
203
|
+
version: skillData.version,
|
|
204
|
+
author: skillData.author,
|
|
205
|
+
createdAt: skillData.createdAt,
|
|
206
|
+
updatedAt: skillData.updatedAt,
|
|
207
|
+
});
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.error(`Failed to load skill metadata for ${entry.name}:`, error);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return skills;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 获取用户私有技能详情
|
|
218
|
+
*/
|
|
219
|
+
export function getUserSkill(userId: string, skillName: string): UserSkill | null {
|
|
220
|
+
const skillDir = join(USER_SKILLS_BASE, userId, 'skills', skillName);
|
|
221
|
+
const jsonPath = join(skillDir, 'SKILL.json');
|
|
222
|
+
|
|
223
|
+
if (!existsSync(jsonPath)) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const jsonContent = readFileSync(jsonPath, 'utf-8');
|
|
229
|
+
const skillData = JSON.parse(jsonContent);
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
name: skillData.name,
|
|
233
|
+
displayName: skillData.displayName,
|
|
234
|
+
description: skillData.description,
|
|
235
|
+
version: skillData.version,
|
|
236
|
+
author: skillData.author,
|
|
237
|
+
createdAt: skillData.createdAt,
|
|
238
|
+
updatedAt: skillData.updatedAt,
|
|
239
|
+
};
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.error(`Failed to load skill ${skillName}:`, error);
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 获取用户技能内容 (SKILL.md)
|
|
248
|
+
*/
|
|
249
|
+
export function getUserSkillContent(userId: string, skillName: string): string | null {
|
|
250
|
+
const skillDir = join(USER_SKILLS_BASE, userId, 'skills', skillName);
|
|
251
|
+
const mdPath = join(skillDir, 'SKILL.md');
|
|
252
|
+
|
|
253
|
+
if (!existsSync(mdPath)) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
return readFileSync(mdPath, 'utf-8');
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.error(`Failed to load skill content for ${skillName}:`, error);
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 更新用户私有技能
|
|
267
|
+
*/
|
|
268
|
+
export function updateUserSkill(userId: string, skillName: string, request: UpdateUserSkillRequest): UserSkill | null {
|
|
269
|
+
const skillDir = join(USER_SKILLS_BASE, userId, 'skills', skillName);
|
|
270
|
+
const jsonPath = join(skillDir, 'SKILL.json');
|
|
271
|
+
|
|
272
|
+
if (!existsSync(jsonPath)) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
// 读取现有数据
|
|
278
|
+
const jsonContent = readFileSync(jsonPath, 'utf-8');
|
|
279
|
+
const skillData = JSON.parse(jsonContent);
|
|
280
|
+
|
|
281
|
+
// 验证所有权
|
|
282
|
+
if (skillData.author !== userId) {
|
|
283
|
+
throw new Error('You do not have permission to update this skill');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 更新字段
|
|
287
|
+
const now = new Date().toISOString();
|
|
288
|
+
if (request.displayName !== undefined) skillData.displayName = request.displayName;
|
|
289
|
+
if (request.description !== undefined) skillData.description = request.description;
|
|
290
|
+
if (request.version !== undefined) skillData.version = request.version;
|
|
291
|
+
if (request.riskConfig !== undefined) skillData.riskConfig = request.riskConfig;
|
|
292
|
+
skillData.updatedAt = now;
|
|
293
|
+
|
|
294
|
+
// 写回 JSON
|
|
295
|
+
writeFileSync(jsonPath, JSON.stringify(skillData, null, 2));
|
|
296
|
+
|
|
297
|
+
// 更新内容(如果提供)
|
|
298
|
+
if (request.content !== undefined) {
|
|
299
|
+
const mdPath = join(skillDir, 'SKILL.md');
|
|
300
|
+
// 符合 agentskills.io 规范,包含 YAML frontmatter
|
|
301
|
+
const skillMdWithFrontmatter = `---
|
|
302
|
+
name: ${skillData.name}
|
|
303
|
+
description: ${skillData.description}
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
${request.content}`;
|
|
307
|
+
writeFileSync(mdPath, skillMdWithFrontmatter);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
name: skillData.name,
|
|
312
|
+
displayName: skillData.displayName,
|
|
313
|
+
description: skillData.description,
|
|
314
|
+
version: skillData.version,
|
|
315
|
+
author: skillData.author,
|
|
316
|
+
createdAt: skillData.createdAt,
|
|
317
|
+
updatedAt: now,
|
|
318
|
+
};
|
|
319
|
+
} catch (error: any) {
|
|
320
|
+
console.error(`Failed to update skill ${skillName}:`, error);
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* 删除用户私有技能
|
|
327
|
+
*/
|
|
328
|
+
export function deleteUserSkill(userId: string, skillName: string): boolean {
|
|
329
|
+
const db = getDb();
|
|
330
|
+
const skillDir = join(USER_SKILLS_BASE, userId, 'skills', skillName);
|
|
331
|
+
|
|
332
|
+
// 删除数据库记录
|
|
333
|
+
db.prepare('DELETE FROM user_skills WHERE user_id = ? AND skill_name = ?').run(userId, skillName);
|
|
334
|
+
|
|
335
|
+
if (!existsSync(skillDir)) {
|
|
336
|
+
return true; // 数据库已删除,目录不存在也算成功
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
// 验证所有权
|
|
341
|
+
const jsonPath = join(skillDir, 'SKILL.json');
|
|
342
|
+
const jsonContent = readFileSync(jsonPath, 'utf-8');
|
|
343
|
+
const skillData = JSON.parse(jsonContent);
|
|
344
|
+
|
|
345
|
+
if (skillData.author !== userId) {
|
|
346
|
+
throw new Error('You do not have permission to delete this skill');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 删除文件
|
|
350
|
+
const mdPath = join(skillDir, 'SKILL.md');
|
|
351
|
+
if (existsSync(mdPath)) unlinkSync(mdPath);
|
|
352
|
+
unlinkSync(jsonPath);
|
|
353
|
+
|
|
354
|
+
// 删除目录
|
|
355
|
+
rmdirSync(skillDir);
|
|
356
|
+
|
|
357
|
+
return true;
|
|
358
|
+
} catch (error: any) {
|
|
359
|
+
console.error(`Failed to delete skill ${skillName}:`, error);
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
}
|