@yeaft/webchat-agent 0.0.2
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/claude.js +405 -0
- package/cli.js +151 -0
- package/connection.js +391 -0
- package/context.js +26 -0
- package/conversation.js +452 -0
- package/encryption.js +105 -0
- package/history.js +283 -0
- package/index.js +159 -0
- package/package.json +75 -0
- package/proxy.js +169 -0
- package/sdk/index.js +9 -0
- package/sdk/query.js +396 -0
- package/sdk/stream.js +112 -0
- package/sdk/types.js +13 -0
- package/sdk/utils.js +194 -0
- package/service.js +587 -0
- package/terminal.js +176 -0
- package/workbench.js +907 -0
package/workbench.js
ADDED
|
@@ -0,0 +1,907 @@
|
|
|
1
|
+
import { existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import { readFile, writeFile, readdir, stat, unlink, rename, mkdir, rm, copyFile, cp } from 'fs/promises';
|
|
3
|
+
import { join, basename, dirname, extname, resolve, isAbsolute, relative } from 'path';
|
|
4
|
+
import { platform } from 'os';
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
import ctx from './context.js';
|
|
8
|
+
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
|
|
11
|
+
// 路径安全校验 - 确保路径格式正确
|
|
12
|
+
export function resolveAndValidatePath(filePath, workDir) {
|
|
13
|
+
const resolved = isAbsolute(filePath) ? resolve(filePath) : resolve(workDir, filePath);
|
|
14
|
+
return resolved;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Helper: resolve git root for a working directory
|
|
18
|
+
export async function getGitRoot(workDir) {
|
|
19
|
+
try {
|
|
20
|
+
const { stdout } = await execAsync('git rev-parse --show-toplevel', {
|
|
21
|
+
cwd: workDir, timeout: 5000, windowsHide: true
|
|
22
|
+
});
|
|
23
|
+
return stdout.trim();
|
|
24
|
+
} catch {
|
|
25
|
+
return workDir;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Helper: validate file path for shell safety
|
|
30
|
+
export function validateGitPath(filePath) {
|
|
31
|
+
return filePath && !/[`$;|&><!\n\r]/.test(filePath);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function handleReadFile(msg) {
|
|
35
|
+
const { conversationId, filePath, _requestUserId } = msg;
|
|
36
|
+
console.log('[Agent] handleReadFile received:', { filePath, conversationId, workDir: msg.workDir });
|
|
37
|
+
const conv = ctx.conversations.get(conversationId);
|
|
38
|
+
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const resolved = resolveAndValidatePath(filePath, workDir);
|
|
42
|
+
const content = await readFile(resolved, 'utf-8');
|
|
43
|
+
|
|
44
|
+
// 检测语言
|
|
45
|
+
const ext = extname(resolved).toLowerCase();
|
|
46
|
+
const langMap = {
|
|
47
|
+
'.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
|
|
48
|
+
'.ts': 'javascript', '.tsx': 'javascript', '.jsx': 'javascript',
|
|
49
|
+
'.py': 'python', '.pyw': 'python',
|
|
50
|
+
'.html': 'htmlmixed', '.htm': 'htmlmixed',
|
|
51
|
+
'.css': 'css', '.scss': 'css', '.less': 'css',
|
|
52
|
+
'.json': 'javascript',
|
|
53
|
+
'.md': 'markdown',
|
|
54
|
+
'.sh': 'shell', '.bash': 'shell', '.zsh': 'shell',
|
|
55
|
+
'.cs': 'text/x-csharp', '.java': 'text/x-java',
|
|
56
|
+
'.cpp': 'text/x-c++src', '.c': 'text/x-csrc', '.h': 'text/x-csrc',
|
|
57
|
+
'.xml': 'xml', '.svg': 'xml',
|
|
58
|
+
'.yaml': 'yaml', '.yml': 'yaml',
|
|
59
|
+
'.sql': 'sql',
|
|
60
|
+
'.go': 'go', '.rs': 'rust', '.rb': 'ruby',
|
|
61
|
+
'.php': 'php', '.swift': 'swift'
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
console.log('[Agent] Sending file_content:', { filePath: resolved, contentLen: content.length, conversationId });
|
|
65
|
+
ctx.sendToServer({
|
|
66
|
+
type: 'file_content',
|
|
67
|
+
conversationId,
|
|
68
|
+
_requestUserId,
|
|
69
|
+
filePath: resolved,
|
|
70
|
+
content,
|
|
71
|
+
language: langMap[ext] || null
|
|
72
|
+
});
|
|
73
|
+
} catch (e) {
|
|
74
|
+
ctx.sendToServer({
|
|
75
|
+
type: 'file_content',
|
|
76
|
+
conversationId,
|
|
77
|
+
_requestUserId,
|
|
78
|
+
filePath,
|
|
79
|
+
content: '',
|
|
80
|
+
error: e.message
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function handleWriteFile(msg) {
|
|
86
|
+
const { conversationId, filePath, content, _requestUserId } = msg;
|
|
87
|
+
const conv = ctx.conversations.get(conversationId);
|
|
88
|
+
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const resolved = resolveAndValidatePath(filePath, workDir);
|
|
92
|
+
await writeFile(resolved, content, 'utf-8');
|
|
93
|
+
|
|
94
|
+
ctx.sendToServer({
|
|
95
|
+
type: 'file_saved',
|
|
96
|
+
conversationId,
|
|
97
|
+
_requestUserId,
|
|
98
|
+
filePath: resolved,
|
|
99
|
+
success: true
|
|
100
|
+
});
|
|
101
|
+
} catch (e) {
|
|
102
|
+
ctx.sendToServer({
|
|
103
|
+
type: 'file_saved',
|
|
104
|
+
conversationId,
|
|
105
|
+
_requestUserId,
|
|
106
|
+
filePath,
|
|
107
|
+
success: false,
|
|
108
|
+
error: e.message
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function handleListDirectory(msg) {
|
|
114
|
+
const { conversationId, dirPath, _requestUserId } = msg;
|
|
115
|
+
const conv = ctx.conversations.get(conversationId);
|
|
116
|
+
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
117
|
+
|
|
118
|
+
// 空路径:列出驱动器(Windows)或根目录(Unix)
|
|
119
|
+
if (!dirPath || dirPath === '') {
|
|
120
|
+
try {
|
|
121
|
+
if (platform() === 'win32') {
|
|
122
|
+
const drives = [];
|
|
123
|
+
for (const letter of 'CDEFGHIJKLMNOPQRSTUVWXYZ'.split('')) {
|
|
124
|
+
const drivePath = letter + ':\\';
|
|
125
|
+
if (existsSync(drivePath)) {
|
|
126
|
+
drives.push({ name: letter + ':', type: 'directory', size: 0 });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
ctx.sendToServer({
|
|
130
|
+
type: 'directory_listing',
|
|
131
|
+
conversationId,
|
|
132
|
+
_requestUserId,
|
|
133
|
+
dirPath: '',
|
|
134
|
+
entries: drives
|
|
135
|
+
});
|
|
136
|
+
} else {
|
|
137
|
+
// Unix: 列出根目录
|
|
138
|
+
const entries = await readdir('/', { withFileTypes: true });
|
|
139
|
+
const result = entries
|
|
140
|
+
.filter(e => !e.name.startsWith('.'))
|
|
141
|
+
.map(e => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file', size: 0 }))
|
|
142
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
143
|
+
ctx.sendToServer({
|
|
144
|
+
type: 'directory_listing',
|
|
145
|
+
conversationId,
|
|
146
|
+
_requestUserId,
|
|
147
|
+
dirPath: '/',
|
|
148
|
+
entries: result
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
} catch (e) {
|
|
152
|
+
ctx.sendToServer({
|
|
153
|
+
type: 'directory_listing',
|
|
154
|
+
conversationId,
|
|
155
|
+
_requestUserId,
|
|
156
|
+
dirPath: '',
|
|
157
|
+
entries: [],
|
|
158
|
+
error: e.message
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const resolved = resolveAndValidatePath(dirPath, workDir);
|
|
166
|
+
const entries = await readdir(resolved, { withFileTypes: true });
|
|
167
|
+
const result = [];
|
|
168
|
+
|
|
169
|
+
for (const entry of entries) {
|
|
170
|
+
// 跳过隐藏文件和 node_modules
|
|
171
|
+
if (entry.name.startsWith('.') && entry.name !== '..') continue;
|
|
172
|
+
if (entry.name === 'node_modules') continue;
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const fullPath = join(resolved, entry.name);
|
|
176
|
+
const s = await stat(fullPath);
|
|
177
|
+
result.push({
|
|
178
|
+
name: entry.name,
|
|
179
|
+
type: entry.isDirectory() ? 'directory' : 'file',
|
|
180
|
+
size: s.size
|
|
181
|
+
});
|
|
182
|
+
} catch {
|
|
183
|
+
result.push({
|
|
184
|
+
name: entry.name,
|
|
185
|
+
type: entry.isDirectory() ? 'directory' : 'file',
|
|
186
|
+
size: 0
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 排序:目录在前,文件在后,各自按名称排序
|
|
192
|
+
result.sort((a, b) => {
|
|
193
|
+
if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
|
|
194
|
+
return a.name.localeCompare(b.name);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
ctx.sendToServer({
|
|
198
|
+
type: 'directory_listing',
|
|
199
|
+
conversationId,
|
|
200
|
+
_requestUserId,
|
|
201
|
+
dirPath: resolved,
|
|
202
|
+
entries: result
|
|
203
|
+
});
|
|
204
|
+
} catch (e) {
|
|
205
|
+
ctx.sendToServer({
|
|
206
|
+
type: 'directory_listing',
|
|
207
|
+
conversationId,
|
|
208
|
+
_requestUserId,
|
|
209
|
+
dirPath: dirPath || workDir,
|
|
210
|
+
entries: [],
|
|
211
|
+
error: e.message
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function handleGitStatus(msg) {
|
|
217
|
+
const { conversationId, _requestUserId } = msg;
|
|
218
|
+
const conv = ctx.conversations.get(conversationId);
|
|
219
|
+
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
// Get git repo root to ensure paths are consistent
|
|
223
|
+
let gitRoot = workDir;
|
|
224
|
+
try {
|
|
225
|
+
const { stdout: rootOut } = await execAsync('git rev-parse --show-toplevel', {
|
|
226
|
+
cwd: workDir,
|
|
227
|
+
timeout: 5000,
|
|
228
|
+
windowsHide: true
|
|
229
|
+
});
|
|
230
|
+
gitRoot = rootOut.trim();
|
|
231
|
+
} catch {}
|
|
232
|
+
|
|
233
|
+
const { stdout: statusOut } = await execAsync('git status --porcelain', {
|
|
234
|
+
cwd: gitRoot,
|
|
235
|
+
timeout: 10000,
|
|
236
|
+
windowsHide: true
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
let branch = '';
|
|
240
|
+
try {
|
|
241
|
+
const { stdout: branchOut } = await execAsync('git branch --show-current', {
|
|
242
|
+
cwd: gitRoot,
|
|
243
|
+
timeout: 5000,
|
|
244
|
+
windowsHide: true
|
|
245
|
+
});
|
|
246
|
+
branch = branchOut.trim();
|
|
247
|
+
} catch {}
|
|
248
|
+
|
|
249
|
+
// Get ahead/behind counts relative to upstream
|
|
250
|
+
let ahead = 0, behind = 0;
|
|
251
|
+
try {
|
|
252
|
+
const { stdout: abOut } = await execAsync('git rev-list --left-right --count HEAD...@{upstream}', {
|
|
253
|
+
cwd: gitRoot, timeout: 5000, windowsHide: true
|
|
254
|
+
});
|
|
255
|
+
const parts = abOut.trim().split(/\s+/);
|
|
256
|
+
ahead = parseInt(parts[0]) || 0;
|
|
257
|
+
behind = parseInt(parts[1]) || 0;
|
|
258
|
+
} catch {}
|
|
259
|
+
|
|
260
|
+
const files = [];
|
|
261
|
+
for (const line of statusOut.split('\n')) {
|
|
262
|
+
if (!line || line.length < 4) continue;
|
|
263
|
+
const indexStatus = line[0];
|
|
264
|
+
const workTreeStatus = line[1];
|
|
265
|
+
const path = line.slice(3);
|
|
266
|
+
// Handle renamed files: "R old -> new"
|
|
267
|
+
const displayPath = path.includes(' -> ') ? path.split(' -> ')[1] : path;
|
|
268
|
+
files.push({ path: displayPath, indexStatus, workTreeStatus });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
ctx.sendToServer({
|
|
272
|
+
type: 'git_status_result',
|
|
273
|
+
conversationId,
|
|
274
|
+
_requestUserId,
|
|
275
|
+
branch,
|
|
276
|
+
files,
|
|
277
|
+
ahead,
|
|
278
|
+
behind,
|
|
279
|
+
workDir,
|
|
280
|
+
gitRoot
|
|
281
|
+
});
|
|
282
|
+
} catch (e) {
|
|
283
|
+
ctx.sendToServer({
|
|
284
|
+
type: 'git_status_result',
|
|
285
|
+
conversationId,
|
|
286
|
+
_requestUserId,
|
|
287
|
+
error: e.message,
|
|
288
|
+
isGitRepo: !e.message.includes('not a git repository') && !e.message.includes('ENOENT')
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export async function handleGitDiff(msg) {
|
|
294
|
+
const { conversationId, filePath, staged, untracked, fullFile, _requestUserId } = msg;
|
|
295
|
+
const conv = ctx.conversations.get(conversationId);
|
|
296
|
+
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
// 安全检查:验证 filePath 不包含 shell 注入字符
|
|
300
|
+
if (!filePath || /[`$;|&><!\n\r]/.test(filePath)) {
|
|
301
|
+
ctx.sendToServer({
|
|
302
|
+
type: 'git_diff_result',
|
|
303
|
+
conversationId,
|
|
304
|
+
_requestUserId,
|
|
305
|
+
filePath,
|
|
306
|
+
error: 'Invalid file path'
|
|
307
|
+
});
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Get git repo root — git status paths are relative to this, not workDir
|
|
312
|
+
let gitRoot = workDir;
|
|
313
|
+
try {
|
|
314
|
+
const { stdout: rootOut } = await execAsync('git rev-parse --show-toplevel', {
|
|
315
|
+
cwd: workDir,
|
|
316
|
+
timeout: 5000,
|
|
317
|
+
windowsHide: true
|
|
318
|
+
});
|
|
319
|
+
gitRoot = rootOut.trim();
|
|
320
|
+
} catch {}
|
|
321
|
+
|
|
322
|
+
if (untracked) {
|
|
323
|
+
// Untracked files: resolve path relative to git root
|
|
324
|
+
const fullPath = resolve(gitRoot, filePath);
|
|
325
|
+
const resolved = resolveAndValidatePath(fullPath, gitRoot);
|
|
326
|
+
const content = await readFile(resolved, 'utf-8');
|
|
327
|
+
ctx.sendToServer({
|
|
328
|
+
type: 'git_diff_result',
|
|
329
|
+
conversationId,
|
|
330
|
+
_requestUserId,
|
|
331
|
+
filePath,
|
|
332
|
+
diff: null,
|
|
333
|
+
newFileContent: content
|
|
334
|
+
});
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// 使用 -- 分隔选项和路径, execAsync 的参数已被验证无注入字符
|
|
339
|
+
const contextFlag = fullFile ? '-U99999' : '';
|
|
340
|
+
const cmd = staged
|
|
341
|
+
? `git diff --cached ${contextFlag} -- "${filePath}"`
|
|
342
|
+
: `git diff ${contextFlag} -- "${filePath}"`;
|
|
343
|
+
|
|
344
|
+
const { stdout } = await execAsync(cmd, {
|
|
345
|
+
cwd: gitRoot,
|
|
346
|
+
timeout: 15000,
|
|
347
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
348
|
+
windowsHide: true
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// 如果 git diff 返回空,尝试用 --cached 或反之
|
|
352
|
+
if (!stdout.trim() && !staged) {
|
|
353
|
+
const cachedCmd = `git diff --cached ${contextFlag} -- "${filePath}"`;
|
|
354
|
+
const { stdout: cachedOut } = await execAsync(cachedCmd, {
|
|
355
|
+
cwd: gitRoot,
|
|
356
|
+
timeout: 15000,
|
|
357
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
358
|
+
windowsHide: true
|
|
359
|
+
});
|
|
360
|
+
if (cachedOut.trim()) {
|
|
361
|
+
ctx.sendToServer({
|
|
362
|
+
type: 'git_diff_result',
|
|
363
|
+
conversationId,
|
|
364
|
+
_requestUserId,
|
|
365
|
+
filePath,
|
|
366
|
+
staged: true,
|
|
367
|
+
diff: cachedOut
|
|
368
|
+
});
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
} else if (!stdout.trim() && staged) {
|
|
372
|
+
const wtCmd = `git diff ${contextFlag} -- "${filePath}"`;
|
|
373
|
+
const { stdout: wtOut } = await execAsync(wtCmd, {
|
|
374
|
+
cwd: gitRoot,
|
|
375
|
+
timeout: 15000,
|
|
376
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
377
|
+
windowsHide: true
|
|
378
|
+
});
|
|
379
|
+
if (wtOut.trim()) {
|
|
380
|
+
ctx.sendToServer({
|
|
381
|
+
type: 'git_diff_result',
|
|
382
|
+
conversationId,
|
|
383
|
+
_requestUserId,
|
|
384
|
+
filePath,
|
|
385
|
+
staged: false,
|
|
386
|
+
diff: wtOut
|
|
387
|
+
});
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
ctx.sendToServer({
|
|
393
|
+
type: 'git_diff_result',
|
|
394
|
+
conversationId,
|
|
395
|
+
_requestUserId,
|
|
396
|
+
filePath,
|
|
397
|
+
staged: !!staged,
|
|
398
|
+
diff: stdout
|
|
399
|
+
});
|
|
400
|
+
} catch (e) {
|
|
401
|
+
ctx.sendToServer({
|
|
402
|
+
type: 'git_diff_result',
|
|
403
|
+
conversationId,
|
|
404
|
+
_requestUserId,
|
|
405
|
+
filePath,
|
|
406
|
+
error: e.message
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export async function handleGitAdd(msg) {
|
|
412
|
+
const { conversationId, filePath, addAll, _requestUserId } = msg;
|
|
413
|
+
const conv = ctx.conversations.get(conversationId);
|
|
414
|
+
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
415
|
+
|
|
416
|
+
try {
|
|
417
|
+
const gitRoot = await getGitRoot(workDir);
|
|
418
|
+
|
|
419
|
+
if (addAll) {
|
|
420
|
+
await execAsync('git add -A', { cwd: gitRoot, timeout: 10000, windowsHide: true });
|
|
421
|
+
} else {
|
|
422
|
+
if (!validateGitPath(filePath)) {
|
|
423
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'add', success: false, error: 'Invalid file path' });
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
await execAsync(`git add -- "${filePath}"`, { cwd: gitRoot, timeout: 10000, windowsHide: true });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'add', success: true, message: addAll ? 'All files staged' : `Staged: ${filePath}` });
|
|
430
|
+
} catch (e) {
|
|
431
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'add', success: false, error: e.message });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export async function handleGitReset(msg) {
|
|
436
|
+
const { conversationId, filePath, resetAll, _requestUserId } = msg;
|
|
437
|
+
const conv = ctx.conversations.get(conversationId);
|
|
438
|
+
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
const gitRoot = await getGitRoot(workDir);
|
|
442
|
+
|
|
443
|
+
if (resetAll) {
|
|
444
|
+
await execAsync('git reset HEAD', { cwd: gitRoot, timeout: 10000, windowsHide: true });
|
|
445
|
+
} else {
|
|
446
|
+
if (!validateGitPath(filePath)) {
|
|
447
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'reset', success: false, error: 'Invalid file path' });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
await execAsync(`git reset HEAD -- "${filePath}"`, { cwd: gitRoot, timeout: 10000, windowsHide: true });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'reset', success: true, message: resetAll ? 'All files unstaged' : `Unstaged: ${filePath}` });
|
|
454
|
+
} catch (e) {
|
|
455
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'reset', success: false, error: e.message });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export async function handleGitRestore(msg) {
|
|
460
|
+
const { conversationId, filePath, _requestUserId } = msg;
|
|
461
|
+
const conv = ctx.conversations.get(conversationId);
|
|
462
|
+
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
if (!validateGitPath(filePath)) {
|
|
466
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'restore', success: false, error: 'Invalid file path' });
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const gitRoot = await getGitRoot(workDir);
|
|
471
|
+
await execAsync(`git restore -- "${filePath}"`, { cwd: gitRoot, timeout: 10000, windowsHide: true });
|
|
472
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'restore', success: true, message: `Restored: ${filePath}` });
|
|
473
|
+
} catch (e) {
|
|
474
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'restore', success: false, error: e.message });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export async function handleGitCommit(msg) {
|
|
479
|
+
const { conversationId, commitMessage, _requestUserId } = msg;
|
|
480
|
+
const conv = ctx.conversations.get(conversationId);
|
|
481
|
+
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
if (!commitMessage || !commitMessage.trim()) {
|
|
485
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'commit', success: false, error: 'Commit message is required' });
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const gitRoot = await getGitRoot(workDir);
|
|
490
|
+
|
|
491
|
+
// Write commit message to temp file to avoid shell injection
|
|
492
|
+
const tmpFile = join(gitRoot, '.git', 'WEBCHAT_COMMIT_MSG');
|
|
493
|
+
await writeFile(tmpFile, commitMessage.trim(), 'utf8');
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
const { stdout } = await execAsync(`git commit -F "${tmpFile}"`, {
|
|
497
|
+
cwd: gitRoot, timeout: 30000, windowsHide: true
|
|
498
|
+
});
|
|
499
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'commit', success: true, message: stdout.trim() });
|
|
500
|
+
} finally {
|
|
501
|
+
// Clean up temp file
|
|
502
|
+
try { await writeFile(tmpFile, '', 'utf8'); } catch {}
|
|
503
|
+
}
|
|
504
|
+
} catch (e) {
|
|
505
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'commit', success: false, error: e.stderr?.trim() || e.message });
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export async function handleGitPush(msg) {
|
|
510
|
+
const { conversationId, _requestUserId } = msg;
|
|
511
|
+
const conv = ctx.conversations.get(conversationId);
|
|
512
|
+
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
const gitRoot = await getGitRoot(workDir);
|
|
516
|
+
const { stdout, stderr } = await execAsync('git push', {
|
|
517
|
+
cwd: gitRoot, timeout: 60000, windowsHide: true
|
|
518
|
+
});
|
|
519
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'push', success: true, message: (stdout + '\n' + stderr).trim() || 'Push complete' });
|
|
520
|
+
} catch (e) {
|
|
521
|
+
ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'push', success: false, error: e.stderr?.trim() || e.message });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export async function handleFileSearch(msg) {
|
|
526
|
+
const { conversationId, query, _requestUserId } = msg;
|
|
527
|
+
const conv = ctx.conversations.get(conversationId);
|
|
528
|
+
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
529
|
+
const searchRoot = msg.dirPath || workDir;
|
|
530
|
+
|
|
531
|
+
try {
|
|
532
|
+
if (!query || query.trim().length === 0) {
|
|
533
|
+
ctx.sendToServer({ type: 'file_search_result', conversationId, _requestUserId, query, results: [] });
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const resolved = resolve(searchRoot);
|
|
538
|
+
const results = [];
|
|
539
|
+
const MAX_RESULTS = 100;
|
|
540
|
+
const lowerQuery = query.toLowerCase();
|
|
541
|
+
const skipDirs = new Set(['.git', 'node_modules', '__pycache__', '.next', '.nuxt', 'dist', 'build', '.cache', 'bin', 'obj']);
|
|
542
|
+
|
|
543
|
+
async function walk(dir, depth) {
|
|
544
|
+
if (depth > 10 || results.length >= MAX_RESULTS) return;
|
|
545
|
+
try {
|
|
546
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
547
|
+
for (const entry of entries) {
|
|
548
|
+
if (results.length >= MAX_RESULTS) return;
|
|
549
|
+
if (entry.name.startsWith('.') && depth > 0) continue;
|
|
550
|
+
if (skipDirs.has(entry.name) && entry.isDirectory()) continue;
|
|
551
|
+
|
|
552
|
+
const fullPath = join(dir, entry.name);
|
|
553
|
+
|
|
554
|
+
if (entry.name.toLowerCase().includes(lowerQuery)) {
|
|
555
|
+
let size = 0;
|
|
556
|
+
try { const s = await stat(fullPath); size = s.size; } catch {}
|
|
557
|
+
results.push({
|
|
558
|
+
name: entry.name,
|
|
559
|
+
path: relative(resolved, fullPath).replace(/\\/g, '/'),
|
|
560
|
+
fullPath: fullPath.replace(/\\/g, '/'),
|
|
561
|
+
type: entry.isDirectory() ? 'directory' : 'file',
|
|
562
|
+
size
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (entry.isDirectory()) {
|
|
567
|
+
await walk(fullPath, depth + 1);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
} catch {}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
await walk(resolved, 0);
|
|
574
|
+
|
|
575
|
+
ctx.sendToServer({
|
|
576
|
+
type: 'file_search_result',
|
|
577
|
+
conversationId,
|
|
578
|
+
_requestUserId,
|
|
579
|
+
query,
|
|
580
|
+
results,
|
|
581
|
+
truncated: results.length >= MAX_RESULTS
|
|
582
|
+
});
|
|
583
|
+
} catch (e) {
|
|
584
|
+
ctx.sendToServer({ type: 'file_search_result', conversationId, _requestUserId, query, results: [], error: e.message });
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export async function handleCreateFile(msg) {
|
|
589
|
+
const { conversationId, filePath, isDirectory, _requestUserId } = msg;
|
|
590
|
+
const conv = ctx.conversations.get(conversationId);
|
|
591
|
+
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
const resolved = resolveAndValidatePath(filePath, workDir);
|
|
595
|
+
if (isDirectory) {
|
|
596
|
+
await mkdir(resolved, { recursive: true });
|
|
597
|
+
} else {
|
|
598
|
+
// Ensure parent directory exists
|
|
599
|
+
const parentDir = dirname(resolved);
|
|
600
|
+
await mkdir(parentDir, { recursive: true });
|
|
601
|
+
// Create file only if it doesn't exist
|
|
602
|
+
if (existsSync(resolved)) {
|
|
603
|
+
throw new Error('File already exists: ' + resolved);
|
|
604
|
+
}
|
|
605
|
+
await writeFile(resolved, '', 'utf-8');
|
|
606
|
+
}
|
|
607
|
+
ctx.sendToServer({
|
|
608
|
+
type: 'file_op_result', conversationId, _requestUserId,
|
|
609
|
+
operation: 'create', success: true,
|
|
610
|
+
message: (isDirectory ? 'Directory' : 'File') + ' created: ' + basename(resolved)
|
|
611
|
+
});
|
|
612
|
+
} catch (e) {
|
|
613
|
+
ctx.sendToServer({
|
|
614
|
+
type: 'file_op_result', conversationId, _requestUserId,
|
|
615
|
+
operation: 'create', success: false, error: e.message
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export async function handleDeleteFiles(msg) {
|
|
621
|
+
const { conversationId, paths, _requestUserId } = msg;
|
|
622
|
+
const conv = ctx.conversations.get(conversationId);
|
|
623
|
+
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
624
|
+
|
|
625
|
+
try {
|
|
626
|
+
if (!paths || paths.length === 0) throw new Error('No paths specified');
|
|
627
|
+
const deleted = [];
|
|
628
|
+
const errors = [];
|
|
629
|
+
|
|
630
|
+
for (const p of paths) {
|
|
631
|
+
try {
|
|
632
|
+
const resolved = resolveAndValidatePath(p, workDir);
|
|
633
|
+
const s = await stat(resolved);
|
|
634
|
+
if (s.isDirectory()) {
|
|
635
|
+
await rm(resolved, { recursive: true, force: true });
|
|
636
|
+
} else {
|
|
637
|
+
await unlink(resolved);
|
|
638
|
+
}
|
|
639
|
+
deleted.push(basename(resolved));
|
|
640
|
+
} catch (e) {
|
|
641
|
+
errors.push(basename(p) + ': ' + e.message);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const message = deleted.length > 0
|
|
646
|
+
? 'Deleted: ' + deleted.join(', ') + (errors.length > 0 ? '; Errors: ' + errors.join(', ') : '')
|
|
647
|
+
: 'Failed: ' + errors.join(', ');
|
|
648
|
+
|
|
649
|
+
ctx.sendToServer({
|
|
650
|
+
type: 'file_op_result', conversationId, _requestUserId,
|
|
651
|
+
operation: 'delete', success: deleted.length > 0,
|
|
652
|
+
message, deletedCount: deleted.length, errorCount: errors.length
|
|
653
|
+
});
|
|
654
|
+
} catch (e) {
|
|
655
|
+
ctx.sendToServer({
|
|
656
|
+
type: 'file_op_result', conversationId, _requestUserId,
|
|
657
|
+
operation: 'delete', success: false, error: e.message
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export async function handleMoveFiles(msg) {
|
|
663
|
+
const { conversationId, paths, destination, newName, _requestUserId } = msg;
|
|
664
|
+
const conv = ctx.conversations.get(conversationId);
|
|
665
|
+
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
666
|
+
|
|
667
|
+
try {
|
|
668
|
+
if (!paths || paths.length === 0) throw new Error('No paths specified');
|
|
669
|
+
if (!destination) throw new Error('No destination specified');
|
|
670
|
+
|
|
671
|
+
const destResolved = resolveAndValidatePath(destination, workDir);
|
|
672
|
+
// Ensure destination directory exists
|
|
673
|
+
await mkdir(destResolved, { recursive: true });
|
|
674
|
+
|
|
675
|
+
const moved = [];
|
|
676
|
+
const errors = [];
|
|
677
|
+
|
|
678
|
+
for (const p of paths) {
|
|
679
|
+
try {
|
|
680
|
+
const srcResolved = resolveAndValidatePath(p, workDir);
|
|
681
|
+
const name = (newName && paths.length === 1) ? newName : basename(srcResolved);
|
|
682
|
+
const destPath = join(destResolved, name);
|
|
683
|
+
if (existsSync(destPath)) {
|
|
684
|
+
throw new Error('Target already exists: ' + name);
|
|
685
|
+
}
|
|
686
|
+
await rename(srcResolved, destPath);
|
|
687
|
+
moved.push(name);
|
|
688
|
+
} catch (e) {
|
|
689
|
+
errors.push(basename(p) + ': ' + e.message);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const message = moved.length > 0
|
|
694
|
+
? 'Moved: ' + moved.join(', ') + ' → ' + basename(destResolved) + (errors.length > 0 ? '; Errors: ' + errors.join(', ') : '')
|
|
695
|
+
: 'Failed: ' + errors.join(', ');
|
|
696
|
+
|
|
697
|
+
ctx.sendToServer({
|
|
698
|
+
type: 'file_op_result', conversationId, _requestUserId,
|
|
699
|
+
operation: 'move', success: moved.length > 0,
|
|
700
|
+
message, movedCount: moved.length, errorCount: errors.length
|
|
701
|
+
});
|
|
702
|
+
} catch (e) {
|
|
703
|
+
ctx.sendToServer({
|
|
704
|
+
type: 'file_op_result', conversationId, _requestUserId,
|
|
705
|
+
operation: 'move', success: false, error: e.message
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
export async function handleCopyFiles(msg) {
|
|
711
|
+
const { conversationId, paths, destination, _requestUserId } = msg;
|
|
712
|
+
const conv = ctx.conversations.get(conversationId);
|
|
713
|
+
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
714
|
+
|
|
715
|
+
try {
|
|
716
|
+
if (!paths || paths.length === 0) throw new Error('No paths specified');
|
|
717
|
+
if (!destination) throw new Error('No destination specified');
|
|
718
|
+
|
|
719
|
+
const destResolved = resolveAndValidatePath(destination, workDir);
|
|
720
|
+
await mkdir(destResolved, { recursive: true });
|
|
721
|
+
|
|
722
|
+
const copied = [];
|
|
723
|
+
const errors = [];
|
|
724
|
+
|
|
725
|
+
for (const p of paths) {
|
|
726
|
+
try {
|
|
727
|
+
const srcResolved = resolveAndValidatePath(p, workDir);
|
|
728
|
+
const name = basename(srcResolved);
|
|
729
|
+
let destPath = join(destResolved, name);
|
|
730
|
+
|
|
731
|
+
// If copying to same directory, generate a unique name
|
|
732
|
+
if (destPath === srcResolved) {
|
|
733
|
+
const ext = extname(name);
|
|
734
|
+
const base = basename(name, ext);
|
|
735
|
+
let counter = 1;
|
|
736
|
+
do {
|
|
737
|
+
destPath = join(destResolved, `${base} (copy${counter > 1 ? ' ' + counter : ''})${ext}`);
|
|
738
|
+
counter++;
|
|
739
|
+
} while (existsSync(destPath));
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const srcStat = await stat(srcResolved);
|
|
743
|
+
if (srcStat.isDirectory()) {
|
|
744
|
+
await cp(srcResolved, destPath, { recursive: true });
|
|
745
|
+
} else {
|
|
746
|
+
await copyFile(srcResolved, destPath);
|
|
747
|
+
}
|
|
748
|
+
copied.push(basename(destPath));
|
|
749
|
+
} catch (e) {
|
|
750
|
+
errors.push(basename(p) + ': ' + e.message);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const message = copied.length > 0
|
|
755
|
+
? 'Copied: ' + copied.join(', ') + (errors.length > 0 ? '; Errors: ' + errors.join(', ') : '')
|
|
756
|
+
: 'Failed: ' + errors.join(', ');
|
|
757
|
+
|
|
758
|
+
ctx.sendToServer({
|
|
759
|
+
type: 'file_op_result', conversationId, _requestUserId,
|
|
760
|
+
operation: 'copy', success: copied.length > 0,
|
|
761
|
+
message, copiedCount: copied.length, errorCount: errors.length
|
|
762
|
+
});
|
|
763
|
+
} catch (e) {
|
|
764
|
+
ctx.sendToServer({
|
|
765
|
+
type: 'file_op_result', conversationId, _requestUserId,
|
|
766
|
+
operation: 'copy', success: false, error: e.message
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
export async function handleUploadToDir(msg) {
|
|
772
|
+
const { conversationId, files, dirPath, _requestUserId } = msg;
|
|
773
|
+
const conv = ctx.conversations.get(conversationId);
|
|
774
|
+
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
775
|
+
|
|
776
|
+
try {
|
|
777
|
+
if (!files || files.length === 0) throw new Error('No files specified');
|
|
778
|
+
|
|
779
|
+
const targetDir = resolveAndValidatePath(dirPath || workDir, workDir);
|
|
780
|
+
await mkdir(targetDir, { recursive: true });
|
|
781
|
+
|
|
782
|
+
const saved = [];
|
|
783
|
+
const errors = [];
|
|
784
|
+
|
|
785
|
+
for (const file of files) {
|
|
786
|
+
try {
|
|
787
|
+
const dest = join(targetDir, file.name);
|
|
788
|
+
const buffer = Buffer.from(file.data, 'base64');
|
|
789
|
+
await writeFile(dest, buffer);
|
|
790
|
+
saved.push(file.name);
|
|
791
|
+
} catch (e) {
|
|
792
|
+
errors.push(file.name + ': ' + e.message);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const message = saved.length > 0
|
|
797
|
+
? 'Uploaded: ' + saved.join(', ') + (errors.length > 0 ? '; Errors: ' + errors.join(', ') : '')
|
|
798
|
+
: 'Failed: ' + errors.join(', ');
|
|
799
|
+
|
|
800
|
+
ctx.sendToServer({
|
|
801
|
+
type: 'file_op_result', conversationId, _requestUserId,
|
|
802
|
+
operation: 'upload', success: saved.length > 0,
|
|
803
|
+
message, uploadedCount: saved.length, errorCount: errors.length
|
|
804
|
+
});
|
|
805
|
+
} catch (e) {
|
|
806
|
+
ctx.sendToServer({
|
|
807
|
+
type: 'file_op_result', conversationId, _requestUserId,
|
|
808
|
+
operation: 'upload', success: false, error: e.message
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// 临时文件目录名 (不易冲突)
|
|
814
|
+
const TEMP_UPLOAD_DIR = '.claude-tmp-attachments';
|
|
815
|
+
|
|
816
|
+
export async function handleTransferFiles(msg) {
|
|
817
|
+
const { conversationId, files, prompt, workDir, claudeSessionId } = msg;
|
|
818
|
+
const { startClaudeQuery } = await import('./claude.js');
|
|
819
|
+
|
|
820
|
+
let state = ctx.conversations.get(conversationId);
|
|
821
|
+
const effectiveWorkDir = workDir || state?.workDir || ctx.CONFIG.workDir;
|
|
822
|
+
|
|
823
|
+
// 创建临时目录
|
|
824
|
+
const uploadDir = join(effectiveWorkDir, TEMP_UPLOAD_DIR);
|
|
825
|
+
if (!existsSync(uploadDir)) {
|
|
826
|
+
mkdirSync(uploadDir, { recursive: true });
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const savedFiles = [];
|
|
830
|
+
const imageFiles = [];
|
|
831
|
+
|
|
832
|
+
for (const file of files) {
|
|
833
|
+
try {
|
|
834
|
+
const timestamp = Date.now();
|
|
835
|
+
const ext = extname(file.name);
|
|
836
|
+
const baseName = basename(file.name, ext);
|
|
837
|
+
const uniqueName = `${baseName}_${timestamp}${ext}`;
|
|
838
|
+
const filePath = join(uploadDir, uniqueName);
|
|
839
|
+
const relativePath = join(TEMP_UPLOAD_DIR, uniqueName);
|
|
840
|
+
|
|
841
|
+
const buffer = Buffer.from(file.data, 'base64');
|
|
842
|
+
writeFileSync(filePath, buffer);
|
|
843
|
+
|
|
844
|
+
const isImage = file.mimeType.startsWith('image/');
|
|
845
|
+
savedFiles.push({
|
|
846
|
+
name: file.name,
|
|
847
|
+
path: relativePath,
|
|
848
|
+
mimeType: file.mimeType,
|
|
849
|
+
isImage
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
if (isImage) {
|
|
853
|
+
imageFiles.push({
|
|
854
|
+
mimeType: file.mimeType,
|
|
855
|
+
data: file.data
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
console.log(`Saved file: ${relativePath}`);
|
|
860
|
+
} catch (e) {
|
|
861
|
+
console.error(`Error saving file ${file.name}:`, e.message);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// 如果没有活跃的查询,启动新的
|
|
866
|
+
if (!state || !state.query || !state.inputStream) {
|
|
867
|
+
const resumeSessionId = claudeSessionId || state?.claudeSessionId || null;
|
|
868
|
+
console.log(`[SDK] Starting Claude for ${conversationId} (files), resume: ${resumeSessionId || 'none'}`);
|
|
869
|
+
state = await startClaudeQuery(conversationId, effectiveWorkDir, resumeSessionId);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// 构造带附件的消息
|
|
873
|
+
const fileListText = savedFiles.map(f =>
|
|
874
|
+
`- ${f.path} (${f.isImage ? '图片' : f.mimeType})`
|
|
875
|
+
).join('\n');
|
|
876
|
+
|
|
877
|
+
const fullPrompt = `用户上传了以下文件:\n${fileListText}\n\n用户说:${prompt}`;
|
|
878
|
+
|
|
879
|
+
// 构造 content 数组
|
|
880
|
+
const content = [];
|
|
881
|
+
|
|
882
|
+
for (const img of imageFiles) {
|
|
883
|
+
content.push({
|
|
884
|
+
type: 'image',
|
|
885
|
+
source: {
|
|
886
|
+
type: 'base64',
|
|
887
|
+
media_type: img.mimeType,
|
|
888
|
+
data: img.data
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
content.push({
|
|
894
|
+
type: 'text',
|
|
895
|
+
text: fullPrompt
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
// 发送用户消息到输入流
|
|
899
|
+
const userMessage = {
|
|
900
|
+
type: 'user',
|
|
901
|
+
message: { role: 'user', content }
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
console.log(`[${conversationId}] Sending with ${savedFiles.length} files, ${imageFiles.length} images`);
|
|
905
|
+
state.turnActive = true;
|
|
906
|
+
state.inputStream.enqueue(userMessage);
|
|
907
|
+
}
|