diff --git a/server/index.ts b/server/index.ts index bf96884..30407e2 100644 --- a/server/index.ts +++ b/server/index.ts @@ -997,6 +997,52 @@ async function runStartupDataPatches() { ON CONFLICT (file_key) DO NOTHING `).catch(() => {}); + // Дозаполнение task_id/field_id у старых строк, вставленных без привязки + // (ранние версии трекинга). ON CONFLICT DO NOTHING их не обновляет. + await db.execute(sql` + UPDATE file_uploads fu + SET task_id = src.task_id, field_id = src.field_id + FROM ( + SELECT DISTINCT + CASE + WHEN elem->>'url' LIKE '%/api/files/%' THEN regexp_replace(elem->>'url', '^.*/api/files/', '') + WHEN elem->>'url' LIKE '%/uploads/%' THEN regexp_replace(elem->>'url', '^.*/uploads/', '') + END AS fkey, + tfv.task_id, + tfv.field_id + FROM task_field_values tfv + JOIN form_fields ff ON tfv.field_id = ff.id + CROSS JOIN LATERAL jsonb_array_elements( + CASE WHEN jsonb_typeof(tfv.value) = 'array' THEN tfv.value ELSE jsonb_build_array(tfv.value) END + ) elem + WHERE ff.type = 'file' + AND (elem->>'url' LIKE '%/api/files/%' OR elem->>'url' LIKE '%/uploads/%') + ) src + WHERE fu.file_key = src.fkey + AND src.fkey IS NOT NULL + AND fu.task_id IS NULL + `).catch(() => {}); + await db.execute(sql` + UPDATE file_uploads fu + SET task_id = src.task_id + FROM ( + SELECT DISTINCT + CASE + WHEN att->>'url' LIKE '%/api/files/%' THEN regexp_replace(att->>'url', '^.*/api/files/', '') + WHEN att->>'url' LIKE '%/uploads/%' THEN regexp_replace(att->>'url', '^.*/uploads/', '') + END AS fkey, + tm.task_id + FROM task_messages tm + CROSS JOIN LATERAL jsonb_array_elements(tm.attachments) att + WHERE tm.attachments IS NOT NULL + AND jsonb_typeof(tm.attachments) = 'array' + AND (att->>'url' LIKE '%/api/files/%' OR att->>'url' LIKE '%/uploads/%') + ) src + WHERE fu.file_key = src.fkey + AND src.fkey IS NOT NULL + AND fu.task_id IS NULL + `).catch(() => {}); + // RLS policies are created by migration 0011_rls_tasks_notnull.sql. // Startup only activates ENABLE + FORCE ROW LEVEL SECURITY when ENABLE_RLS=true, // then verifies all tables are correctly protected before allowing traffic. diff --git a/server/mcp.ts b/server/mcp.ts index bb5d18c..8bad017 100644 --- a/server/mcp.ts +++ b/server/mcp.ts @@ -4288,7 +4288,12 @@ To block task creation from task.before_create, set: ctx.result = { allow: false } if (!belongsToTask) { const fieldValues = await storage.getTaskFieldValues(taskId, organizationId); - belongsToTask = fieldValues.some((fv) => typeof fv.value === 'string' && fv.value.includes(fileKey)); + // value может быть объектом/массивом (file-поля), а не строкой — + // сериализуем перед поиском ключа + belongsToTask = fieldValues.some((fv) => { + const raw = typeof fv.value === 'string' ? fv.value : JSON.stringify(fv.value ?? ''); + return raw.includes(fileKey); + }); } if (!belongsToTask) { return mcpError(`Файл не относится к задаче ${taskId}`); diff --git a/server/utils/file-tracking.ts b/server/utils/file-tracking.ts index fb19d47..b129aec 100644 --- a/server/utils/file-tracking.ts +++ b/server/utils/file-tracking.ts @@ -78,13 +78,27 @@ interface FileReferenceRow { */ export async function trackFileOnDemand(fileKey: string): Promise { const existing = await db - .select({ id: fileUploads.id }) + .select() .from(fileUploads) .where(eq(fileUploads.fileKey, fileKey)) .limit(1); - if (existing.length > 0) return true; try { + // Уже отслеживается, но без привязки к задаче (старые backfill-строки) — + // пытаемся дозаполнить taskId/fieldId из ссылок + if (existing.length > 0) { + const row = existing[0]; + if (row.taskId == null) { + const ref = await findFileReference(fileKey); + if (ref?.taskId != null) { + await db.update(fileUploads) + .set({ taskId: ref.taskId, fieldId: row.fieldId ?? ref.fieldId }) + .where(eq(fileUploads.id, row.id)); + } + } + return true; + } + const ref = await findFileReference(fileKey); // uploadedBy NOT NULL в схеме — без владельца записать не сможем if (!ref || ref.uploadedBy == null) return false;