Фикс предпросмотра файлов с абсолютными self-URL

- extractFileKey понимает абсолютные URL (по pathname) — превью работает для уже записанных файлов
- normalizeFileUrl: абсолютные self-URL нормализуются в относительные при записи file-полей (REST field-values, MCP update_task_fields/upload-инструменты, bot-api, create task)
This commit is contained in:
2026-07-24 11:10:55 +03:00
parent b981614095
commit 59079992dc
6 changed files with 81 additions and 16 deletions

View File

@@ -15,6 +15,7 @@ import { withSuperAdmin, openTenantCtx, _tenantCtx, db } from "../db";
import { eq, and, desc, sql, lt, inArray } from "drizzle-orm";
import { enrichMessage, getMembersForSSE } from "./messenger.helpers";
import { notificationService } from "../services/notification.service";
import { normalizeFileFieldUrls } from "../utils/upload";
export function registerBotApiRoutes(app: import("express").Express): void {
// =====================================================
@@ -341,9 +342,11 @@ export function registerBotApiRoutes(app: import("express").Express): void {
const field = fieldMap.get(code);
if (!field) continue;
// Нормализация self-URL в file-полях: абсолютные https://.../api/files/<key> → относительные
const storedValue = field.type === 'file' ? normalizeFileFieldUrls(value) : value;
try {
await storage.updateTaskFieldValue(taskId, field.id, req.organizationId!, {
value: value as string | number | boolean | null | Record<string, unknown>
value: storedValue as string | number | boolean | null | Record<string, unknown>
});
} catch {
// Если запись не существует, создаём
@@ -351,7 +354,7 @@ export function registerBotApiRoutes(app: import("express").Express): void {
taskId,
fieldId: field.id,
formId: task.formId,
value: value as string | number | boolean | null | Record<string, unknown>
value: storedValue as string | number | boolean | null | Record<string, unknown>
});
}
}

View File

@@ -16,6 +16,7 @@ import { validateRequiredFields } from "../utils/validate-required-fields";
import { shiftRelatedTasks, calculateDateShift } from "../services/gantt-shift.service";
import { runAutomationsByTrigger, type AutomationRunResult } from "./automation.routes";
import { normalizeFieldValueForStorage } from "../utils/normalize-field-value";
import { normalizeFileFieldUrls } from "../utils/upload";
import { resolveRestActor, checkApiKeyWriteAccess } from "../utils/api-key-actor";
import { db, withTenant } from "../db";
import { eq, and, sql } from "drizzle-orm";
@@ -261,6 +262,10 @@ export function registerTaskCrudWriteRoutes(router: ReturnType<typeof import("ex
// Auto-generate contract number for 'mine' mode
let finalValue = normalizeFieldValueForStorage(value, field.type);
// Нормализация self-URL в file-полях: абсолютные https://.../api/files/<key> → относительные
if (field.type === 'file') {
finalValue = normalizeFileFieldUrls(finalValue) as typeof finalValue;
}
if (field.type === 'contract-number') {
const parsed = typeof value === 'string' ? JSON.parse(value) : value;
if (parsed && parsed.mode === 'mine' && (!parsed.value || parsed.value === '')) {

View File

@@ -12,6 +12,7 @@ import { validateRequiredFields } from "../utils/validate-required-fields";
import { validateImmutableFields } from "../utils/validate-immutable-fields";
import { shiftRelatedTasks, calculateDateShift } from "../services/gantt-shift.service";
import { normalizeFieldValueForStorage } from "../utils/normalize-field-value";
import { normalizeFileFieldUrls } from "../utils/upload";
function extractUserId(value: unknown): number | null {
if (value === null || value === undefined || value === '') return null;
@@ -103,6 +104,12 @@ export function registerTaskFieldRoutes(router: ReturnType<typeof import("expres
const formFields = await storage.getFormFields(existingTask.formId, req.organizationId!);
const fieldMap = new Map(formFields.map(f => [f.id, f]));
// Нормализация self-URL в file-полях: абсолютные https://.../api/files/<key>
// от внешних агентов сводятся к относительным /api/files/<key>
const normalizedFieldValues = (fieldValues as Array<{ fieldId: number; value: unknown }>).map((fv) =>
fieldMap.get(fv.fieldId)?.type === 'file' ? { ...fv, value: normalizeFileFieldUrls(fv.value) } : fv
);
// Pre-load data for audit log display-text resolution (and reuse later for title template)
const [auditUsers, auditRoles] = await Promise.all([
storage.getUsersByOrganization(req.organizationId!),
@@ -166,7 +173,7 @@ export function registerTaskFieldRoutes(router: ReturnType<typeof import("expres
}
const createdValues = [];
for (const fieldValue of fieldValues) {
for (const fieldValue of normalizedFieldValues) {
const field = fieldMap.get(fieldValue.fieldId);
const normalizedValue = normalizeFieldValueForStorage(fieldValue.value, field?.type);
@@ -234,7 +241,7 @@ export function registerTaskFieldRoutes(router: ReturnType<typeof import("expres
const fieldActor = await resolveRestActor(req, req.organizationId!);
const offlineEnqueuedAtPost = parseOfflineTimestamp(req);
for (const fieldValue of fieldValues) {
for (const fieldValue of normalizedFieldValues) {
const field = fieldMap.get(fieldValue.fieldId);
if (!field) continue;
const oldVal = oldValueMap.has(fieldValue.fieldId) ? oldValueMap.get(fieldValue.fieldId) : undefined;
@@ -411,7 +418,9 @@ export function registerTaskFieldRoutes(router: ReturnType<typeof import("expres
return res.status(404).json({ success: false, error: 'Поле не найдено' });
}
const normalizedValue = normalizeFieldValueForStorage(value, field.type);
const normalizedValueRaw = normalizeFieldValueForStorage(value, field.type);
// Нормализация self-URL в file-полях: абсолютные https://.../api/files/<key> → относительные
const normalizedValue = field.type === 'file' ? normalizeFileFieldUrls(normalizedValueRaw) : normalizedValueRaw;
// Validate conditional required fields before applying the change
const existingValuesSingle = await storage.getTaskFieldValues(taskId, req.organizationId!);
@@ -630,7 +639,9 @@ export function registerTaskFieldRoutes(router: ReturnType<typeof import("expres
const normalizedEntries = entries.map(([k, value]): [string, unknown] => {
const fid = parseInt(k, 10);
const fld = fieldMap.get(fid);
return [k, normalizeFieldValueForStorage(value, fld?.type)];
const normalized = normalizeFieldValueForStorage(value, fld?.type);
// Нормализация self-URL в file-полях: абсолютные https://.../api/files/<key> → относительные
return [k, fld?.type === 'file' ? normalizeFileFieldUrls(normalized) : normalized];
});
// Validate conditional required fields before applying bulk changes