@yeaft/webchat-agent 0.0.233 → 0.0.235
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/connection/buffer.js +87 -0
- package/connection/heartbeat.js +47 -0
- package/connection/index.js +89 -0
- package/connection/message-router.js +271 -0
- package/connection/upgrade-worker-template.js +103 -0
- package/connection/upgrade.js +294 -0
- package/connection.js +14 -777
- package/crew/control.js +364 -0
- package/crew/human-interaction.js +115 -0
- package/crew/persistence.js +287 -0
- package/crew/role-management.js +131 -0
- package/crew/role-output.js +315 -0
- package/crew/role-query.js +309 -0
- package/crew/routing.js +194 -0
- package/crew/session.js +474 -0
- package/crew/shared-dir.js +116 -0
- package/crew/task-files.js +370 -0
- package/crew/ui-messages.js +246 -0
- package/crew/worktree.js +130 -0
- package/package.json +6 -2
- package/service/config.js +133 -0
- package/service/index.js +99 -0
- package/service/linux.js +111 -0
- package/service/macos.js +137 -0
- package/service/windows.js +181 -0
- package/service.js +23 -624
- package/workbench/file-ops.js +436 -0
- package/workbench/file-search.js +66 -0
- package/workbench/git-ops.js +313 -0
- package/workbench/transfer.js +99 -0
- package/workbench/utils.js +41 -0
- package/workbench.js +15 -938
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
2
|
+
import { join, resolve } from 'path';
|
|
3
|
+
import ctx from '../context.js';
|
|
4
|
+
import { execAsync, resolveAndValidatePath, getGitRoot, validateGitPath } from './utils.js';
|
|
5
|
+
|
|
6
|
+
export async function handleGitStatus(msg) {
|
|
7
|
+
const { conversationId, _requestUserId } = msg;
|
|
8
|
+
const conv = ctx.conversations.get(conversationId);
|
|
9
|
+
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
// Get git repo root to ensure paths are consistent
|
|
13
|
+
let gitRoot = workDir;
|
|
14
|
+
try {
|
|
15
|
+
const { stdout: rootOut } = await execAsync('git rev-parse --show-toplevel', {
|
|
16
|
+
cwd: workDir,
|
|
17
|
+
timeout: 5000,
|
|
18
|
+
windowsHide: true
|
|
19
|
+
});
|
|
20
|
+
gitRoot = rootOut.trim();
|
|
21
|
+
} catch {}
|
|
22
|
+
|
|
23
|
+
const { stdout: statusOut } = await execAsync('git status --porcelain', {
|
|
24
|
+
cwd: gitRoot,
|
|
25
|
+
timeout: 10000,
|
|
26
|
+
windowsHide: true
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
let branch = '';
|
|
30
|
+
try {
|
|
31
|
+
const { stdout: branchOut } = await execAsync('git branch --show-current', {
|
|
32
|
+
cwd: gitRoot,
|
|
33
|
+
timeout: 5000,
|
|
34
|
+
windowsHide: true
|
|
35
|
+
});
|
|
36
|
+
branch = branchOut.trim();
|
|
37
|
+
} catch {}
|
|
38
|
+
|
|
39
|
+
// Get ahead/behind counts relative to upstream
|
|
40
|
+
let ahead = 0, behind = 0;
|
|
41
|
+
try {
|
|
42
|
+
const { stdout: abOut } = await execAsync('git rev-list --left-right --count HEAD...@{upstream}', {
|
|
43
|
+
cwd: gitRoot, timeout: 5000, windowsHide: true
|
|
44
|
+
});
|
|
45
|
+
const parts = abOut.trim().split(/\s+/);
|
|
46
|
+
ahead = parseInt(parts[0]) || 0;
|
|
47
|
+
behind = parseInt(parts[1]) || 0;
|
|
48
|
+
} catch {}
|
|
49
|
+
|
|
50
|
+
const files = [];
|
|
51
|
+
for (const line of statusOut.split('\n')) {
|
|
52
|
+
if (!line || line.length < 4) continue;
|
|
53
|
+
const indexStatus = line[0];
|
|
54
|
+
const workTreeStatus = line[1];
|
|
55
|
+
const path = line.slice(3);
|
|
56
|
+
// Handle renamed files: "R old -> new"
|
|
57
|
+
const displayPath = path.includes(' -> ') ? path.split(' -> ')[1] : path;
|
|
58
|
+
files.push({ path: displayPath, indexStatus, workTreeStatus });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
ctx.sendToServer({
|
|
62
|
+
type: 'git_status_result',
|
|
63
|
+
conversationId,
|
|
64
|
+
_requestUserId,
|
|
65
|
+
branch,
|
|
66
|
+
files,
|
|
67
|
+
ahead,
|
|
68
|
+
behind,
|
|
69
|
+
workDir,
|
|
70
|
+
gitRoot
|
|
71
|
+
});
|
|
72
|
+
} catch (e) {
|
|
73
|
+
ctx.sendToServer({
|
|
74
|
+
type: 'git_status_result',
|
|
75
|
+
conversationId,
|
|
76
|
+
_requestUserId,
|
|
77
|
+
error: e.message,
|
|
78
|
+
isGitRepo: !e.message.includes('not a git repository') && !e.message.includes('ENOENT')
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function handleGitDiff(msg) {
|
|
84
|
+
const { conversationId, filePath, staged, untracked, fullFile, _requestUserId } = msg;
|
|
85
|
+
const conv = ctx.conversations.get(conversationId);
|
|
86
|
+
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
// 安全检查:验证 filePath 不包含 shell 注入字符
|
|
90
|
+
if (!filePath || /[`$;|&><!\n\r]/.test(filePath)) {
|
|
91
|
+
ctx.sendToServer({
|
|
92
|
+
type: 'git_diff_result',
|
|
93
|
+
conversationId,
|
|
94
|
+
_requestUserId,
|
|
95
|
+
filePath,
|
|
96
|
+
error: 'Invalid file path'
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Get git repo root — git status paths are relative to this, not workDir
|
|
102
|
+
let gitRoot = workDir;
|
|
103
|
+
try {
|
|
104
|
+
const { stdout: rootOut } = await execAsync('git rev-parse --show-toplevel', {
|
|
105
|
+
cwd: workDir,
|
|
106
|
+
timeout: 5000,
|
|
107
|
+
windowsHide: true
|
|
108
|
+
});
|
|
109
|
+
gitRoot = rootOut.trim();
|
|
110
|
+
} catch {}
|
|
111
|
+
|
|
112
|
+
if (untracked) {
|
|
113
|
+
// Untracked files: resolve path relative to git root
|
|
114
|
+
const fullPath = resolve(gitRoot, filePath);
|
|
115
|
+
const resolved = resolveAndValidatePath(fullPath, gitRoot);
|
|
116
|
+
const content = await readFile(resolved, 'utf-8');
|
|
117
|
+
ctx.sendToServer({
|
|
118
|
+
type: 'git_diff_result',
|
|
119
|
+
conversationId,
|
|
120
|
+
_requestUserId,
|
|
121
|
+
filePath,
|
|
122
|
+
diff: null,
|
|
123
|
+
newFileContent: content
|
|
124
|
+
});
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 使用 -- 分隔选项和路径, execAsync 的参数已被验证无注入字符
|
|
129
|
+
const contextFlag = fullFile ? '-U99999' : '';
|
|
130
|
+
const cmd = staged
|
|
131
|
+
? `git diff --cached ${contextFlag} -- "${filePath}"`
|
|
132
|
+
: `git diff ${contextFlag} -- "${filePath}"`;
|
|
133
|
+
|
|
134
|
+
const { stdout } = await execAsync(cmd, {
|
|
135
|
+
cwd: gitRoot,
|
|
136
|
+
timeout: 15000,
|
|
137
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
138
|
+
windowsHide: true
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// 如果 git diff 返回空,尝试用 --cached 或反之
|
|
142
|
+
if (!stdout.trim() && !staged) {
|
|
143
|
+
const cachedCmd = `git diff --cached ${contextFlag} -- "${filePath}"`;
|
|
144
|
+
const { stdout: cachedOut } = await execAsync(cachedCmd, {
|
|
145
|
+
cwd: gitRoot,
|
|
146
|
+
timeout: 15000,
|
|
147
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
148
|
+
windowsHide: true
|
|
149
|
+
});
|
|
150
|
+
if (cachedOut.trim()) {
|
|
151
|
+
ctx.sendToServer({
|
|
152
|
+
type: 'git_diff_result',
|
|
153
|
+
conversationId,
|
|
154
|
+
_requestUserId,
|
|
155
|
+
filePath,
|
|
156
|
+
staged: true,
|
|
157
|
+
diff: cachedOut
|
|
158
|
+
});
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
} else if (!stdout.trim() && staged) {
|
|
162
|
+
const wtCmd = `git diff ${contextFlag} -- "${filePath}"`;
|
|
163
|
+
const { stdout: wtOut } = await execAsync(wtCmd, {
|
|
164
|
+
cwd: gitRoot,
|
|
165
|
+
timeout: 15000,
|
|
166
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
167
|
+
windowsHide: true
|
|
168
|
+
});
|
|
169
|
+
if (wtOut.trim()) {
|
|
170
|
+
ctx.sendToServer({
|
|
171
|
+
type: 'git_diff_result',
|
|
172
|
+
conversationId,
|
|
173
|
+
_requestUserId,
|
|
174
|
+
filePath,
|
|
175
|
+
staged: false,
|
|
176
|
+
diff: wtOut
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
ctx.sendToServer({
|
|
183
|
+
type: 'git_diff_result',
|
|
184
|
+
conversationId,
|
|
185
|
+
_requestUserId,
|
|
186
|
+
filePath,
|
|
187
|
+
staged: !!staged,
|
|
188
|
+
diff: stdout
|
|
189
|
+
});
|
|
190
|
+
} catch (e) {
|
|
191
|
+
ctx.sendToServer({
|
|
192
|
+
type: 'git_diff_result',
|
|
193
|
+
conversationId,
|
|
194
|
+
_requestUserId,
|
|
195
|
+
filePath,
|
|
196
|
+
error: e.message
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function handleGitAdd(msg) {
|
|
202
|
+
const { conversationId, filePath, addAll, _requestUserId } = msg;
|
|
203
|
+
const conv = ctx.conversations.get(conversationId);
|
|
204
|
+
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const gitRoot = await getGitRoot(workDir);
|
|
208
|
+
|
|
209
|
+
if (addAll) {
|
|
210
|
+
await execAsync('git add -A', { cwd: gitRoot, timeout: 10000, windowsHide: true });
|
|
211
|
+
} else {
|
|
212
|
+
if (!validateGitPath(filePath)) {
|
|
213
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'add', success: false, error: 'Invalid file path' });
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
await execAsync(`git add -- "${filePath}"`, { cwd: gitRoot, timeout: 10000, windowsHide: true });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'add', success: true, message: addAll ? 'All files staged' : `Staged: ${filePath}` });
|
|
220
|
+
} catch (e) {
|
|
221
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'add', success: false, error: e.message });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function handleGitReset(msg) {
|
|
226
|
+
const { conversationId, filePath, resetAll, _requestUserId } = msg;
|
|
227
|
+
const conv = ctx.conversations.get(conversationId);
|
|
228
|
+
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const gitRoot = await getGitRoot(workDir);
|
|
232
|
+
|
|
233
|
+
if (resetAll) {
|
|
234
|
+
await execAsync('git reset HEAD', { cwd: gitRoot, timeout: 10000, windowsHide: true });
|
|
235
|
+
} else {
|
|
236
|
+
if (!validateGitPath(filePath)) {
|
|
237
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'reset', success: false, error: 'Invalid file path' });
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
await execAsync(`git reset HEAD -- "${filePath}"`, { cwd: gitRoot, timeout: 10000, windowsHide: true });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'reset', success: true, message: resetAll ? 'All files unstaged' : `Unstaged: ${filePath}` });
|
|
244
|
+
} catch (e) {
|
|
245
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'reset', success: false, error: e.message });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export async function handleGitRestore(msg) {
|
|
250
|
+
const { conversationId, filePath, _requestUserId } = msg;
|
|
251
|
+
const conv = ctx.conversations.get(conversationId);
|
|
252
|
+
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
if (!validateGitPath(filePath)) {
|
|
256
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'restore', success: false, error: 'Invalid file path' });
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const gitRoot = await getGitRoot(workDir);
|
|
261
|
+
await execAsync(`git restore -- "${filePath}"`, { cwd: gitRoot, timeout: 10000, windowsHide: true });
|
|
262
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'restore', success: true, message: `Restored: ${filePath}` });
|
|
263
|
+
} catch (e) {
|
|
264
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'restore', success: false, error: e.message });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export async function handleGitCommit(msg) {
|
|
269
|
+
const { conversationId, commitMessage, _requestUserId } = msg;
|
|
270
|
+
const conv = ctx.conversations.get(conversationId);
|
|
271
|
+
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
if (!commitMessage || !commitMessage.trim()) {
|
|
275
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'commit', success: false, error: 'Commit message is required' });
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const gitRoot = await getGitRoot(workDir);
|
|
280
|
+
|
|
281
|
+
// Write commit message to temp file to avoid shell injection
|
|
282
|
+
const tmpFile = join(gitRoot, '.git', 'WEBCHAT_COMMIT_MSG');
|
|
283
|
+
await writeFile(tmpFile, commitMessage.trim(), 'utf8');
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
const { stdout } = await execAsync(`git commit -F "${tmpFile}"`, {
|
|
287
|
+
cwd: gitRoot, timeout: 30000, windowsHide: true
|
|
288
|
+
});
|
|
289
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'commit', success: true, message: stdout.trim() });
|
|
290
|
+
} finally {
|
|
291
|
+
// Clean up temp file
|
|
292
|
+
try { await writeFile(tmpFile, '', 'utf8'); } catch {}
|
|
293
|
+
}
|
|
294
|
+
} catch (e) {
|
|
295
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'commit', success: false, error: e.stderr?.trim() || e.message });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function handleGitPush(msg) {
|
|
300
|
+
const { conversationId, _requestUserId } = msg;
|
|
301
|
+
const conv = ctx.conversations.get(conversationId);
|
|
302
|
+
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
const gitRoot = await getGitRoot(workDir);
|
|
306
|
+
const { stdout, stderr } = await execAsync('git push', {
|
|
307
|
+
cwd: gitRoot, timeout: 60000, windowsHide: true
|
|
308
|
+
});
|
|
309
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'push', success: true, message: (stdout + '\n' + stderr).trim() || 'Push complete' });
|
|
310
|
+
} catch (e) {
|
|
311
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'push', success: false, error: e.stderr?.trim() || e.message });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join, basename, extname } from 'path';
|
|
3
|
+
import ctx from '../context.js';
|
|
4
|
+
|
|
5
|
+
// 临时文件目录名 (不易冲突)
|
|
6
|
+
const TEMP_UPLOAD_DIR = '.claude-tmp-attachments';
|
|
7
|
+
|
|
8
|
+
export async function handleTransferFiles(msg) {
|
|
9
|
+
const { conversationId, files, prompt, workDir, claudeSessionId } = msg;
|
|
10
|
+
const { startClaudeQuery } = await import('../claude.js');
|
|
11
|
+
|
|
12
|
+
let state = ctx.conversations.get(conversationId);
|
|
13
|
+
const effectiveWorkDir = workDir || state?.workDir || ctx.CONFIG.workDir;
|
|
14
|
+
|
|
15
|
+
// 创建临时目录
|
|
16
|
+
const uploadDir = join(effectiveWorkDir, TEMP_UPLOAD_DIR);
|
|
17
|
+
if (!existsSync(uploadDir)) {
|
|
18
|
+
mkdirSync(uploadDir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const savedFiles = [];
|
|
22
|
+
const imageFiles = [];
|
|
23
|
+
|
|
24
|
+
for (const file of files) {
|
|
25
|
+
try {
|
|
26
|
+
const timestamp = Date.now();
|
|
27
|
+
const ext = extname(file.name);
|
|
28
|
+
const baseName = basename(file.name, ext);
|
|
29
|
+
const uniqueName = `${baseName}_${timestamp}${ext}`;
|
|
30
|
+
const filePath = join(uploadDir, uniqueName);
|
|
31
|
+
const relativePath = join(TEMP_UPLOAD_DIR, uniqueName);
|
|
32
|
+
|
|
33
|
+
const buffer = Buffer.from(file.data, 'base64');
|
|
34
|
+
writeFileSync(filePath, buffer);
|
|
35
|
+
|
|
36
|
+
const isImage = file.mimeType.startsWith('image/');
|
|
37
|
+
savedFiles.push({
|
|
38
|
+
name: file.name,
|
|
39
|
+
path: relativePath,
|
|
40
|
+
mimeType: file.mimeType,
|
|
41
|
+
isImage
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (isImage) {
|
|
45
|
+
imageFiles.push({
|
|
46
|
+
mimeType: file.mimeType,
|
|
47
|
+
data: file.data
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(`Saved file: ${relativePath}`);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.error(`Error saving file ${file.name}:`, e.message);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 如果没有活跃的查询,启动新的
|
|
58
|
+
if (!state || !state.query || !state.inputStream) {
|
|
59
|
+
const resumeSessionId = claudeSessionId || state?.claudeSessionId || null;
|
|
60
|
+
console.log(`[SDK] Starting Claude for ${conversationId} (files), resume: ${resumeSessionId || 'none'}`);
|
|
61
|
+
state = await startClaudeQuery(conversationId, effectiveWorkDir, resumeSessionId);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 构造带附件的消息
|
|
65
|
+
const fileListText = savedFiles.map(f =>
|
|
66
|
+
`- ${f.path} (${f.isImage ? '图片' : f.mimeType})`
|
|
67
|
+
).join('\n');
|
|
68
|
+
|
|
69
|
+
const fullPrompt = `用户上传了以下文件:\n${fileListText}\n\n用户说:${prompt}`;
|
|
70
|
+
|
|
71
|
+
// 构造 content 数组
|
|
72
|
+
const content = [];
|
|
73
|
+
|
|
74
|
+
for (const img of imageFiles) {
|
|
75
|
+
content.push({
|
|
76
|
+
type: 'image',
|
|
77
|
+
source: {
|
|
78
|
+
type: 'base64',
|
|
79
|
+
media_type: img.mimeType,
|
|
80
|
+
data: img.data
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
content.push({
|
|
86
|
+
type: 'text',
|
|
87
|
+
text: fullPrompt
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// 发送用户消息到输入流
|
|
91
|
+
const userMessage = {
|
|
92
|
+
type: 'user',
|
|
93
|
+
message: { role: 'user', content }
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
console.log(`[${conversationId}] Sending with ${savedFiles.length} files, ${imageFiles.length} images`);
|
|
97
|
+
state.turnActive = true;
|
|
98
|
+
state.inputStream.enqueue(userMessage);
|
|
99
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { resolve, isAbsolute } from 'path';
|
|
4
|
+
|
|
5
|
+
export const execAsync = promisify(exec);
|
|
6
|
+
|
|
7
|
+
// 路径安全校验 - 确保路径格式正确
|
|
8
|
+
export function resolveAndValidatePath(filePath, workDir) {
|
|
9
|
+
const resolved = isAbsolute(filePath) ? resolve(filePath) : resolve(workDir, filePath);
|
|
10
|
+
return resolved;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Helper: resolve git root for a working directory
|
|
14
|
+
export async function getGitRoot(workDir) {
|
|
15
|
+
try {
|
|
16
|
+
const { stdout } = await execAsync('git rev-parse --show-toplevel', {
|
|
17
|
+
cwd: workDir, timeout: 5000, windowsHide: true
|
|
18
|
+
});
|
|
19
|
+
return stdout.trim();
|
|
20
|
+
} catch {
|
|
21
|
+
return workDir;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Helper: validate file path for shell safety
|
|
26
|
+
export function validateGitPath(filePath) {
|
|
27
|
+
return filePath && !/[`$;|&><!\n\r]/.test(filePath);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Binary file extensions → MIME type mapping
|
|
31
|
+
export const BINARY_EXTENSIONS = {
|
|
32
|
+
'.pdf': 'application/pdf',
|
|
33
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
34
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
35
|
+
'.xls': 'application/vnd.ms-excel',
|
|
36
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
37
|
+
'.ppt': 'application/vnd.ms-powerpoint',
|
|
38
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
39
|
+
'.gif': 'image/gif', '.webp': 'image/webp', '.bmp': 'image/bmp',
|
|
40
|
+
'.ico': 'image/x-icon'
|
|
41
|
+
};
|