fix: deduplicate attachment file writes (P0-1)

Upload route now passes persisted filePath to claude-client, which
reuses it instead of writing the file again.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
7418andClaude Opus 4.6 committed 2026-02-13 01:24:12 +08:00
1 parent 5878b36ff6
commit f3c49a9e49
2 files changed
+40 -27

No files matched your search

+16 -9
View File
@@ -30,13 +30,14 @@ export async function POST(request: NextRequest) {
// Save user message — persist file metadata so attachments survive page reload
let savedContent = content;
let fileMeta: Array<{ id: string; name: string; type: string; size: number; filePath: string }> | undefined;
if (files && files.length > 0) {
const workDir = session.working_directory || process.cwd();
const uploadDir = path.join(workDir, '.codepilot-uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
const fileMeta = files.map((f) => {
fileMeta = files.map((f) => {
const safeName = path.basename(f.name).replace(/[^a-zA-Z0-9._-]/g, '_');
const filePath = path.join(uploadDir, `${Date.now()}-${safeName}`);
const buffer = Buffer.from(f.data, 'base64');
@@ -81,15 +82,21 @@ export async function POST(request: NextRequest) {
abortController.abort();
});
// Convert file attachments to the format expected by streamClaude
// Convert file attachments to the format expected by streamClaude.
// Include filePath from the already-saved files so claude-client can
// reference the on-disk copies instead of writing them again.
const fileAttachments: FileAttachment[] | undefined = files && files.length > 0
? files.map((f, i) => ({
id: f.id || `file-${Date.now()}-${i}`,
name: f.name,
type: f.type,
size: f.size,
data: f.data,
}))
? files.map((f, i) => {
const meta = fileMeta?.find((m: { id: string }) => m.id === f.id);
return {
id: f.id || `file-${Date.now()}-${i}`,
name: f.name,
type: f.type,
size: f.size,
data: f.data,
filePath: meta?.filePath,
};
})
: undefined;
// Stream Claude response, using SDK session ID for resume if available
+24 -18
View File
@@ -135,26 +135,32 @@ function extractTokenUsage(msg: SDKResultMessage): TokenUsage | null {
* Returns a ReadableStream of SSE-formatted strings.
*/
/**
* Save non-image file attachments to a temporary upload directory
* and return the file paths. The files are placed in .codepilot-uploads/
* under the working directory so Claude's Read tool can access them.
* Get file paths for non-image attachments. If the file already has a
* persisted filePath (written by the uploads route), reuse it. Otherwise
* fall back to writing the file to .codepilot-uploads/.
*/
function saveUploadedFiles(files: FileAttachment[], workDir: string): string[] {
const uploadDir = path.join(workDir, '.codepilot-uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
const savedPaths: string[] = [];
function getUploadedFilePaths(files: FileAttachment[], workDir: string): string[] {
const paths: string[] = [];
let uploadDir: string | undefined;
for (const file of files) {
// Sanitize filename to prevent directory traversal
const safeName = path.basename(file.name).replace(/[^a-zA-Z0-9._-]/g, '_');
const timestamp = Date.now();
const filePath = path.join(uploadDir, `${timestamp}-${safeName}`);
const buffer = Buffer.from(file.data, 'base64');
fs.writeFileSync(filePath, buffer);
savedPaths.push(filePath);
if (file.filePath) {
paths.push(file.filePath);
} else {
// Fallback: write file to disk (should not happen in normal flow)
if (!uploadDir) {
uploadDir = path.join(workDir, '.codepilot-uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
}
const safeName = path.basename(file.name).replace(/[^a-zA-Z0-9._-]/g, '_');
const filePath = path.join(uploadDir, `${Date.now()}-${safeName}`);
const buffer = Buffer.from(file.data, 'base64');
fs.writeFileSync(filePath, buffer);
paths.push(filePath);
}
}
return savedPaths;
return paths;
}
export function streamClaude(options: ClaudeStreamOptions): ReadableStream<string> {
@@ -409,7 +415,7 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream<strin
let textPrompt = prompt;
if (nonImageFiles.length > 0) {
const workDir = workingDirectory || process.cwd();
const savedPaths = saveUploadedFiles(nonImageFiles, workDir);
const savedPaths = getUploadedFilePaths(nonImageFiles, workDir);
const fileReferences = savedPaths
.map((p, i) => `[User attached file: ${p} (${nonImageFiles[i].name})]`)
.join('\n');