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,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 安全 Bash 工具
|
|
3
|
+
*
|
|
4
|
+
* 扩展默认的 bash 工具,在执行前检查命令是否危险。
|
|
5
|
+
* 危险命令会被拦截并自动创建工单,而不是直接执行。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createBashTool, type BashSpawnContext, type BashSpawnHook } from '@mariozechner/pi-coding-agent';
|
|
9
|
+
import { checkDangerousCommand, estimateRiskLevel } from './dangerous-commands';
|
|
10
|
+
import { createTicket } from './db';
|
|
11
|
+
import { resolve } from 'path';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 待处理的工单缓存(用于追踪最近创建的工单)
|
|
15
|
+
*/
|
|
16
|
+
const pendingTickets = new Map<string, {
|
|
17
|
+
ticketId: string;
|
|
18
|
+
command: string;
|
|
19
|
+
createdAt: Date;
|
|
20
|
+
}>();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 最近的命令结果缓存
|
|
24
|
+
*/
|
|
25
|
+
const recentResults = new Map<string, {
|
|
26
|
+
type: 'blocked' | 'executed';
|
|
27
|
+
command: string;
|
|
28
|
+
ticketId?: string;
|
|
29
|
+
output?: string;
|
|
30
|
+
error?: string;
|
|
31
|
+
}>();
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 创建安全的 Bash 工具
|
|
35
|
+
*
|
|
36
|
+
* 使用 spawnHook 在命令执行前进行检查:
|
|
37
|
+
* - 安全命令:正常执行
|
|
38
|
+
* - API 调用命令:返回错误提示,告诉 AI 使用 create_ticket 工具
|
|
39
|
+
* - 危险命令:创建工单,返回提示信息
|
|
40
|
+
* - 本地操作(非管理员):限制只能执行远程命令
|
|
41
|
+
*
|
|
42
|
+
* @param cwd 工作目录
|
|
43
|
+
* @param userId 用户ID(可选,用于日志)
|
|
44
|
+
* @param isAdmin 是否管理员(管理员无限制)
|
|
45
|
+
*/
|
|
46
|
+
export function createSecureBashTool(cwd: string, userId?: string, isAdmin: boolean = false) {
|
|
47
|
+
console.error('[secure-bash] Creating secure bash tool for cwd:', cwd, 'userId:', userId, 'isAdmin:', isAdmin);
|
|
48
|
+
|
|
49
|
+
// 创建 spawnHook
|
|
50
|
+
const spawnHook: BashSpawnHook = (context: BashSpawnContext): BashSpawnContext => {
|
|
51
|
+
console.error('[secure-bash] ========== spawnHook called! ==========');
|
|
52
|
+
console.error('[secure-bash] Command:', context.command);
|
|
53
|
+
|
|
54
|
+
const { command } = context;
|
|
55
|
+
|
|
56
|
+
// 非管理员用户:检查操作范围
|
|
57
|
+
let skipDangerousCheck = false; // 是否跳过危险命令检查(用于允许目录内的文件操作)
|
|
58
|
+
|
|
59
|
+
if (!isAdmin) {
|
|
60
|
+
// 远程命令允许执行
|
|
61
|
+
const isRemote = checkIsRemoteCommand(command);
|
|
62
|
+
if (isRemote) {
|
|
63
|
+
console.error('[secure-bash] Remote command allowed for non-admin user');
|
|
64
|
+
// 继续后续的危险命令检查
|
|
65
|
+
} else {
|
|
66
|
+
// 本地命令:检查路径是否在允许目录内
|
|
67
|
+
const allowedDirs = getAllowedDirectories(userId);
|
|
68
|
+
const isPathAllowed = areAllPathsAllowed(command, cwd, allowedDirs);
|
|
69
|
+
|
|
70
|
+
if (!isPathAllowed) {
|
|
71
|
+
console.error('[secure-bash] Local operation outside allowed directories blocked');
|
|
72
|
+
return {
|
|
73
|
+
...context,
|
|
74
|
+
command: `echo "❌ 本地操作被限制在以下目录内:
|
|
75
|
+
- /tmp
|
|
76
|
+
- /var/tmp
|
|
77
|
+
- /data/users/{userId}
|
|
78
|
+
|
|
79
|
+
当前命令涉及不允许的路径。
|
|
80
|
+
|
|
81
|
+
如需在其他目录执行操作,请使用 create_ticket 工具创建工单。"`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 路径在允许目录内,但删除命令仍需审批
|
|
86
|
+
if (isDeleteCommand(command)) {
|
|
87
|
+
console.error('[secure-bash] Delete command in allowed dir still requires approval');
|
|
88
|
+
// 继续后续的危险命令检查,rm 会被检测为危险命令
|
|
89
|
+
} else {
|
|
90
|
+
console.error('[secure-bash] Local operation in allowed directory, skipping dangerous check');
|
|
91
|
+
// 允许目录内的非删除操作,跳过危险命令检查
|
|
92
|
+
skipDangerousCheck = true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 检查是否是 API 调用(AI 尝试绕过工具系统)
|
|
98
|
+
if (isApiCallCommand(command)) {
|
|
99
|
+
console.error('[secure-bash] API call detected - blocking');
|
|
100
|
+
return {
|
|
101
|
+
...context,
|
|
102
|
+
command: `echo "❌ 不允许直接调用内部 API
|
|
103
|
+
|
|
104
|
+
请使用 create_ticket 工具创建工单,而不是通过 curl 调用 API。
|
|
105
|
+
|
|
106
|
+
示例:
|
|
107
|
+
create_ticket(command=\\"kubectl create namespace my-namespace\\", title=\\"创建命名空间\\")
|
|
108
|
+
|
|
109
|
+
所有危险操作必须通过工单系统进行审批。"`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 如果标记跳过危险检查,直接返回原命令
|
|
114
|
+
if (skipDangerousCheck) {
|
|
115
|
+
console.error('[secure-bash] Skipping dangerous command check, executing directly');
|
|
116
|
+
return context;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = checkDangerousCommand(command);
|
|
120
|
+
console.error('[secure-bash] Check result:', JSON.stringify(result));
|
|
121
|
+
|
|
122
|
+
if (result.isDangerous && result.rule) {
|
|
123
|
+
const { riskLevel } = result.rule;
|
|
124
|
+
|
|
125
|
+
if (riskLevel === 'medium') {
|
|
126
|
+
console.log('[secure-bash] MEDIUM risk command - warning but executing');
|
|
127
|
+
const warningMessage = `⚠️ [警告] 检测到修改操作: ${result.rule.description}
|
|
128
|
+
操作将被记录,请谨慎执行。`;
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
...context,
|
|
132
|
+
command: `echo "${warningMessage.replace(/"/g, '\\"').replace(/\n/g, '\\n')}" && ${command}`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
console.log('[secure-bash] HIGH/CRITICAL command detected! Creating ticket...');
|
|
137
|
+
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
try {
|
|
140
|
+
const ticket = createTicket({
|
|
141
|
+
type: 'bash-execute',
|
|
142
|
+
title: `执行命令: ${command.substring(0, 50)}${command.length > 50 ? '...' : ''}`,
|
|
143
|
+
description: `## 命令详情\n\n\`\`\`bash\n${command}\n\`\`\`\n\n**风险等级:** ${result.rule!.riskLevel}\n**规则匹配:** ${result.rule!.description}${result.rule!.category ? `\n**分类:** ${result.rule!.category}` : ''}\n\n---\n\n此工单由系统自动创建,因为检测到危险命令。需要审批后才能执行。`,
|
|
144
|
+
status: 'pending',
|
|
145
|
+
priority: result.rule!.riskLevel,
|
|
146
|
+
command,
|
|
147
|
+
commandType: detectCommandType(command),
|
|
148
|
+
riskLevel: result.rule!.riskLevel,
|
|
149
|
+
affectedResources: extractAffectedResources(command),
|
|
150
|
+
approvals: [],
|
|
151
|
+
createdBy: 'ai-assistant',
|
|
152
|
+
aiGenerated: true,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
pendingTickets.set(command, {
|
|
156
|
+
ticketId: ticket.id,
|
|
157
|
+
command,
|
|
158
|
+
createdAt: new Date(),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
recentResults.set(command, {
|
|
162
|
+
type: 'blocked',
|
|
163
|
+
command,
|
|
164
|
+
ticketId: ticket.id,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
cleanupCache();
|
|
168
|
+
console.log('[secure-bash] Ticket created:', ticket.id);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error('[secure-bash] Failed to create ticket:', error);
|
|
171
|
+
}
|
|
172
|
+
}, 0);
|
|
173
|
+
|
|
174
|
+
const warningMessage = `⚠️ 此命令需要审批
|
|
175
|
+
|
|
176
|
+
检测到危险命令: ${result.rule.description}
|
|
177
|
+
风险等级: ${result.rule.riskLevel}
|
|
178
|
+
|
|
179
|
+
工单正在创建中,请稍后在工单管理界面查看和审批。`;
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
...context,
|
|
183
|
+
command: `echo "${warningMessage.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 安全命令,正常执行
|
|
188
|
+
return context;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// 创建并返回 bash 工具
|
|
192
|
+
return createBashTool(cwd, {
|
|
193
|
+
spawnHook,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 获取最近创建的工单
|
|
199
|
+
*/
|
|
200
|
+
export function getRecentTicket(command: string): string | undefined {
|
|
201
|
+
const entry = pendingTickets.get(command);
|
|
202
|
+
return entry?.ticketId;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 获取命令执行结果
|
|
207
|
+
*/
|
|
208
|
+
export function getCommandResult(command: string) {
|
|
209
|
+
return recentResults.get(command);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 检查命令是否是 API 调用(AI 尝试绕过工具系统)
|
|
214
|
+
*/
|
|
215
|
+
function isApiCallCommand(command: string): boolean {
|
|
216
|
+
const lowerCommand = command.toLowerCase();
|
|
217
|
+
|
|
218
|
+
// 检查是否是调用 localhost 或 127.0.0.1 的 API
|
|
219
|
+
const apiPatterns = [
|
|
220
|
+
/curl\s+.*localhost/i,
|
|
221
|
+
/curl\s+.*127\.0\.0\.1/i,
|
|
222
|
+
/curl\s+.*:11024/i, // 默认端口
|
|
223
|
+
/curl\s+.*\/api\/tickets/i,
|
|
224
|
+
/curl\s+.*\/api\/bash/i,
|
|
225
|
+
/wget\s+.*localhost/i,
|
|
226
|
+
/wget\s+.*127\.0\.0\.1/i,
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
return apiPatterns.some(pattern => pattern.test(command));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 清理旧缓存
|
|
234
|
+
*/
|
|
235
|
+
function cleanupCache(): void {
|
|
236
|
+
const now = Date.now();
|
|
237
|
+
const maxAge = 5 * 60 * 1000; // 5 分钟
|
|
238
|
+
|
|
239
|
+
// 清理 pendingTickets
|
|
240
|
+
for (const [key, entry] of pendingTickets) {
|
|
241
|
+
if (now - entry.createdAt.getTime() > maxAge) {
|
|
242
|
+
pendingTickets.delete(key);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 清理 recentResults(保持最近 100 条)
|
|
247
|
+
if (recentResults.size > 100) {
|
|
248
|
+
const keys = Array.from(recentResults.keys());
|
|
249
|
+
for (let i = 0; i < keys.length - 100; i++) {
|
|
250
|
+
recentResults.delete(keys[i]);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 检测命令类型
|
|
257
|
+
*/
|
|
258
|
+
function detectCommandType(command: string): string {
|
|
259
|
+
const trimmed = command.toLowerCase().trim();
|
|
260
|
+
|
|
261
|
+
if (trimmed.startsWith('kubectl')) return 'kubectl';
|
|
262
|
+
if (trimmed.startsWith('docker')) return 'docker';
|
|
263
|
+
if (trimmed.startsWith('helm')) return 'helm';
|
|
264
|
+
if (trimmed.startsWith('git')) return 'git';
|
|
265
|
+
if (trimmed.startsWith('npm') || trimmed.startsWith('yarn')) return 'package-manager';
|
|
266
|
+
if (trimmed.startsWith('curl') || trimmed.startsWith('wget')) return 'network';
|
|
267
|
+
|
|
268
|
+
return 'bash';
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 从命令中提取受影响的资源
|
|
273
|
+
*/
|
|
274
|
+
function extractAffectedResources(command: string): string[] | undefined {
|
|
275
|
+
const resources: string[] = [];
|
|
276
|
+
|
|
277
|
+
// 提取 kubectl 资源
|
|
278
|
+
const kubectlMatch = command.match(/kubectl\s+.*?\s+(\S+)(?:\s+(-n|--namespace)\s+(\S+))?/);
|
|
279
|
+
if (kubectlMatch) {
|
|
280
|
+
const resource = kubectlMatch[1];
|
|
281
|
+
if (!resource.startsWith('-')) {
|
|
282
|
+
resources.push(resource);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 提取文件路径
|
|
287
|
+
const fileMatches = command.match(/(?:^|\s)(\/[\w\/.-]+)/g);
|
|
288
|
+
if (fileMatches) {
|
|
289
|
+
for (const match of fileMatches) {
|
|
290
|
+
const path = match.trim();
|
|
291
|
+
if (path.length > 1 && !path.startsWith('/dev/')) {
|
|
292
|
+
resources.push(path);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return resources.length > 0 ? resources : undefined;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* 导出工具类型
|
|
302
|
+
*/
|
|
303
|
+
export type SecureBashTool = ReturnType<typeof createSecureBashTool>;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* 获取允许操作的目录列表
|
|
307
|
+
*/
|
|
308
|
+
function getAllowedDirectories(userId?: string): string[] {
|
|
309
|
+
const allowed = ['/tmp', '/var/tmp'];
|
|
310
|
+
if (userId) {
|
|
311
|
+
allowed.push(`/data/users/${userId}`);
|
|
312
|
+
}
|
|
313
|
+
return allowed;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* 从命令中提取文件路径
|
|
318
|
+
*/
|
|
319
|
+
function extractPathsFromCommand(command: string, cwd: string): string[] {
|
|
320
|
+
const paths: string[] = [];
|
|
321
|
+
|
|
322
|
+
// 匹配绝对路径: /xxx
|
|
323
|
+
const absolutePaths = command.match(/(?:^|\s|=)(\/[^\s;|&<>'"]+)/g);
|
|
324
|
+
if (absolutePaths) {
|
|
325
|
+
for (const p of absolutePaths) {
|
|
326
|
+
const cleanPath = p.trim().replace(/^[=:]/, '');
|
|
327
|
+
if (cleanPath && cleanPath !== '/') {
|
|
328
|
+
paths.push(cleanPath);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 匹配相对路径: ./xxx, ../xxx, 或文件名
|
|
334
|
+
const relativePathPattern = /(?:^|\s)((?:\.\.?\/|[^\s/>|&]+\.\w+)(?:[^\s;|&<>'"]*)?)/g;
|
|
335
|
+
let match;
|
|
336
|
+
while ((match = relativePathPattern.exec(command)) !== null) {
|
|
337
|
+
const relativePath = match[1].trim();
|
|
338
|
+
if (relativePath && !relativePath.startsWith('-') && !relativePath.startsWith('/')) {
|
|
339
|
+
// 跳过明显的命令参数
|
|
340
|
+
if (!['true', 'false', 'null', 'undefined'].includes(relativePath)) {
|
|
341
|
+
paths.push(resolve(cwd, relativePath));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// 匹配重定向目标: > xxx, >> xxx
|
|
347
|
+
const redirectPattern = />>?\s*([^\s;|&]+)/g;
|
|
348
|
+
while ((match = redirectPattern.exec(command)) !== null) {
|
|
349
|
+
const target = match[1].trim();
|
|
350
|
+
if (target && !target.startsWith('/dev/')) {
|
|
351
|
+
if (target.startsWith('/')) {
|
|
352
|
+
paths.push(target);
|
|
353
|
+
} else {
|
|
354
|
+
paths.push(resolve(cwd, target));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// 去重
|
|
360
|
+
return [...new Set(paths)];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* 检查路径是否在允许目录内
|
|
365
|
+
*/
|
|
366
|
+
function isPathAllowed(filePath: string, allowedDirs: string[]): boolean {
|
|
367
|
+
const normalized = resolve(filePath);
|
|
368
|
+
return allowedDirs.some(dir => {
|
|
369
|
+
const normalizedDir = resolve(dir);
|
|
370
|
+
return normalized === normalizedDir || normalized.startsWith(normalizedDir + '/');
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* 检查命令中的所有路径是否都在允许目录内
|
|
376
|
+
*/
|
|
377
|
+
function areAllPathsAllowed(command: string, cwd: string, allowedDirs: string[]): boolean {
|
|
378
|
+
const paths = extractPathsFromCommand(command, cwd);
|
|
379
|
+
if (paths.length === 0) {
|
|
380
|
+
return false; // 没有提取到路径,保守处理
|
|
381
|
+
}
|
|
382
|
+
return paths.every(p => isPathAllowed(p, allowedDirs));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* 检查是否是删除命令
|
|
387
|
+
*/
|
|
388
|
+
function isDeleteCommand(command: string): boolean {
|
|
389
|
+
const trimmed = command.trim().toLowerCase();
|
|
390
|
+
return /^\s*rm\s/.test(trimmed);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* 检查是否是远程命令
|
|
395
|
+
*
|
|
396
|
+
* 远程命令:kubectl, docker, ssh, sshpass, helm, scp, rsync
|
|
397
|
+
* 这些命令都是操作远程资源,允许普通用户执行
|
|
398
|
+
*/
|
|
399
|
+
function checkIsRemoteCommand(command: string): boolean {
|
|
400
|
+
const trimmed = command.trim().toLowerCase();
|
|
401
|
+
|
|
402
|
+
const remotePrefixes = [
|
|
403
|
+
'kubectl',
|
|
404
|
+
'docker',
|
|
405
|
+
'ssh ',
|
|
406
|
+
'sshpass',
|
|
407
|
+
'helm',
|
|
408
|
+
'scp',
|
|
409
|
+
'rsync',
|
|
410
|
+
];
|
|
411
|
+
|
|
412
|
+
return remotePrefixes.some(prefix => trimmed.startsWith(prefix));
|
|
413
|
+
}
|