Необходимо исправить в этом скрипте удаление дубликатов по сдельности в каждом файле (функционал с перенесением данных из одной таблицы в другую и потом снова проверка и удаление дубликатов не трогать). Сейчас удаление дубликатов происходит по непонятной логике. Например у меня есть таблица, где 4 строки (шапку не считаем) и эти строки разные. В столбцах есть дубликаты, но в целом строки с наполнением разные. Но скрипт удаляет все равно после 2, хотя если смотреть по строкам, в столбце URL все различается. Вот пример:

URL	      |                            Итоговый URL      	|  Код ответа
http://selectel.ru/	|     https://selectel.ru/   |	301 → 200
https://selectel.ru/	 |      https://selectel.ru/	  |     200
http://www.selectel.ru/	|  https://selectel.ru/	 |  301 → 200
https://www.selectel.ru/	 |  https://selectel.ru/  |	301 → 200

Необходимо исправить скрипт так, чтобы проверялись дубликаты построчно и удалялись именно эти строки, а не если есть совпадения в каком то из столбов, то удаляется просто строка. Вот скрипт:

import ExcelJS from 'exceljs';
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import config from "./config.js"; 

const directoryPath = config.directoryPath;

// Конфигурация пар файлов и соответствующих столбцов
const filePairs = [
  {
    sourceFileName: 'Смешанный контент.xlsx', // Из какого документа
    targetFileName: 'Безопасность. Смешанный контент.xlsx', // В какой документ
    sourceColumns: ['Где найдено', 'Содержимое, загружаемое через HTTP'], // Из каких столбцов перенести данные
    targetColumns: ['Источник', 'Назначение'], // В какие столбцы перенести данные
    keyColumns: ['Источник', 'Назначение'], // Столбцы для проверки дубликатов
  },
  {
    sourceFileName: 'Страницы содержащие битые ссылки.xlsx',
    targetFileName: 'Коды ответа. Ошибка (4xx).xlsx',
    sourceColumns: ['Страница с битой ссылкой', 'Битая ссылка','Код ответа'],
    targetColumns: ['Источник', 'Назначение','Код ответа'],
    keyColumns: ['Источник', 'Назначение', 'Код ответа'], 
  },
  {
    sourceFileName: 'Закрытые от индексации, но есть в sitemap.xml.xlsx',
    targetFileName: 'Sitemap. Неиндексируемые URL в Sitemap.xlsx',
    sourceColumns: ['Страница с битой ссылкой', 'Битая ссылка','Код ответа'],
    targetColumns: ['Источник', 'Назначение','Код ответа'],
    keyColumns: ['Источник', 'Назначение', 'Код ответа'], 
  },
  {
    sourceFileName: 'Разрешённые для индексации, но нет в sitemap.xml.xlsx',
    targetFileName: 'Sitemap. URL не в Sitemap.xlsx',
    sourceColumns: ['Нет в sitemap, но разрешены для индексации', 'HTTP Code'],
    targetColumns: ['Адрес', 'Код ответа'],
    keyColumns: ['Адрес', 'Код ответа'], 
  },
  {
    sourceFileName: 'Изображения с пустым Alt.xlsx',
    targetFileName: 'Изображения. Отсутствует Alt.xlsx',
    sourceColumns: ['SRC изображения', 'Страница, где находится'],
    targetColumns: ['Назначение', 'Источник'],
    keyColumns: ['Назначение', 'Источник'], 
  },
  {
    sourceFileName: 'Ссылки. Страницы без внутренних исходящих ссылок.xlsx',
    targetFileName: 'Нет исходящих.xlsx',
    sourceColumns: ['Адрес', 'Кол-во входящих ссылок'],
    targetColumns: ['Страница', 'Внутренние ссылки'],
    keyColumns: ['Страница', 'Внутренние ссылки'], 
  },
  {
    sourceFileName: 'Страницы с тегом noindex.xlsx',
    targetFileName: 'Директивы. Noindex.xlsx',
    sourceColumns: ['URL'],
    targetColumns: ['Источник'],
    keyColumns: ['Источник'], 
  },
  {
    sourceFileName: 'Тощие страницы.xlsx',
    targetFileName: 'Контент. Страницы где мало контента.xlsx',
    sourceColumns: ['Страница', 'Кол-во слов'],
    targetColumns: ['Адрес', 'Количество слов'],
    keyColumns: ['Адрес', 'Количество слов'], 
  },
  {
    sourceFileName: 'Страницы с одинаковым Title.xlsx',
    targetFileName: 'Title. Дубликаты.xlsx',
    sourceColumns: ['Действие', 'Title'],
    targetColumns: ['Адрес', 'Title 1'],
    keyColumns: ['Адрес', 'Title 1'], 
  },
  {
    sourceFileName: 'Страницы с одинаковым Description.xlsx',
    targetFileName: 'Description. Дубликаты.xlsx',
    sourceColumns: ['Действие', 'Description'],
    targetColumns: ['Адрес', 'Description 1'],
    keyColumns: ['Адрес', 'Description 1'], 
  },
  {
    sourceFileName: 'Страницы c Title = H1.xlsx',
    targetFileName: 'Title. Такой же как и H1.xlsx',
    sourceColumns: ['Страница', 'TITLE'],
    targetColumns: ['Адрес', 'Title 1'],
    keyColumns:  ['Адрес', 'Title 1'], 
  },
  {
    sourceFileName: 'Страницы без Title.xlsx',
    targetFileName: 'Title. Отсутствует.xlsx',
    sourceColumns: ['Страница'],
    targetColumns: ['Адрес'],
    keyColumns:  ['Адрес'], 
  },
  {
    sourceFileName: 'Страницы без H1.xlsx',
    targetFileName: 'H1. Пропущенные.xlsx',
    sourceColumns: ['Страница'],
    targetColumns: ['Адрес'],
    keyColumns: ['Адрес'], 
  },
  {
    sourceFileName: 'Страницы без Description.xlsx',
    targetFileName: 'Description. Отсутствуют.xlsx',
    sourceColumns: ['Страница'],
    targetColumns: ['Адрес'],
    keyColumns: ['Адрес'], 
  },
];

// Функция для получения индексов столбцов (объявлена ТОЛЬКО ОДИН РАЗ)
function getColumnIndices(headerRow, columns) {
  const columnIndices = {};
  headerRow.eachCell((cell, colNumber) => {
    const header = cell.value;
    if (columns.includes(header)) {
      columnIndices[header] = colNumber;
    }
  });
  // Добавляем недостающие столбцы
  columns.forEach((column) => {
    if (!columnIndices[column]) {
      const newColNumber = headerRow.cellCount + 1;
      headerRow.getCell(newColNumber).value = column;
      columnIndices[column] = newColNumber;
    }
  });
  return columnIndices;
}

// Функция для удаления дубликатов в одном файле (во всех листах)
async function removeDuplicatesInFile(filePath) {
  console.log(chalk.blue(`\nОбрабатывается файл для удаления дубликатов: ${path.basename(filePath)}`));

  const workbook = new ExcelJS.Workbook();
  try {
    await workbook.xlsx.readFile(filePath);
  } catch (readError) {
    console.error(chalk.red(`Не удалось прочитать файл ${path.basename(filePath)}: ${readError.message}`));
    return;
  }

  if (workbook.worksheets.length === 0) {
    console.error(chalk.red(`В файле ${path.basename(filePath)} нет ни одного листа.`));
    return;
  }

  let totalDuplicatesRemoved = 0;

  for (const worksheet of workbook.worksheets) {
    console.log(chalk.blue(`Обрабатывается лист: "${worksheet.name}"`));
    try {
      const rowValuesSet = new Set();
      const rowsToRemove = [];

      worksheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
        if (rowNumber === 1) return; // Пропустить заголовок
        const rowValues = row.values.slice(1).join('||'); // Объединяем значения ячеек
        if (rowValuesSet.has(rowValues)) {
          rowsToRemove.push(rowNumber);
        } else {
          rowValuesSet.add(rowValues);
        }
      });

      // Удаляем дубликаты, начиная с конца, чтобы не сместить индексы
      rowsToRemove.reverse().forEach((rowNumber) => {
        worksheet.spliceRows(rowNumber, 1);
      });

      if (rowsToRemove.length > 0) {
        totalDuplicatesRemoved += rowsToRemove.length;
        console.log(chalk.green(
          `Удалено ${rowsToRemove.length} дубликат(ов) на листе "${worksheet.name}"`
        ));
      } else {
        console.log(chalk.yellow(`Дубликаты не найдены на листе "${worksheet.name}"`));
      }
    } catch (processError) {
      console.error(chalk.red(`Ошибка при обработке листа "${worksheet.name}" в файле ${path.basename(filePath)}: ${processError.message}`));
    }
  }

  if (totalDuplicatesRemoved > 0) {
    try {
      await workbook.xlsx.writeFile(filePath);
      console.log(chalk.green(`Общее количество удалённых дубликатов в файле ${path.basename(filePath)}: ${totalDuplicatesRemoved}`));
    } catch (writeError) {
      console.error(chalk.red(`Не удалось сохранить изменения в файле ${path.basename(filePath)}: ${writeError.message}`));
    }
  } else {
    console.log(chalk.yellow(`В файле ${path.basename(filePath)} не было удалено ни одного дубликата.`));
  }
}

// Функция для удаления дубликатов во всех файлах
async function removeDuplicatesInAllFiles() {
  const files = fs.readdirSync(directoryPath);
  for (const file of files) {
    const filePath = path.join(directoryPath, file);
    const ext = path.extname(file).toLowerCase();
    if (ext === '.xlsx' || ext === '.xls') {
      await removeDuplicatesInFile(filePath);
    }
  }
}

// Функция для обработки пар файлов
async function processFiles() {
  await removeDuplicatesInAllFiles();

  for (const filePair of filePairs) {
    const sourceFilePath = path.join(directoryPath, filePair.sourceFileName);
    const targetFilePath = path.join(directoryPath, filePair.targetFileName);

    const sourceFileExists = fs.existsSync(sourceFilePath);
    const targetFileExists = fs.existsSync(targetFilePath);

    if (sourceFileExists && targetFileExists) {
      console.log(
        `\nОбработка файлов: ${filePair.sourceFileName} и ${filePair.targetFileName}`
      );

      // Чтение исходного файла
      const sourceWorkbook = new ExcelJS.Workbook();
      try {
        await sourceWorkbook.xlsx.readFile(sourceFilePath);
      } catch (err) {
        console.error(chalk.red(`Не удалось прочитать исходный файл ${filePair.sourceFileName}: ${err.message}`));
        continue; // Переход к следующей паре файлов
      }

      if (sourceWorkbook.worksheets.length === 0) {
        console.error(chalk.red(`В исходном файле ${filePair.sourceFileName} нет ни одного листа.`));
        continue;
      }

      // Чтение целевого файла
      const targetWorkbook = new ExcelJS.Workbook();
      try {
        await targetWorkbook.xlsx.readFile(targetFilePath);
      } catch (err) {
        console.error(chalk.red(`Не удалось прочитать целевой файл ${filePair.targetFileName}: ${err.message}`));
        continue;
      }

      if (targetWorkbook.worksheets.length === 0) {
        console.error(chalk.red(`В целевом файле ${filePair.targetFileName} нет ни одного листа.`));
        continue;
      }

      // Итерация по всем листам исходного файла
      for (const sourceWorksheet of sourceWorkbook.worksheets) {
        console.log(chalk.blue(`\nОбрабатывается лист исходного файла "${sourceWorksheet.name}"`));

        try {
          // Получение индексов столбцов в исходном листе
          const sourceHeaderRow = sourceWorksheet.getRow(1);
          const sourceColumnIndices = getColumnIndices(
            sourceHeaderRow,
            filePair.sourceColumns
          );

          // Итерация по всем листам целевого файла
          for (const targetWorksheet of targetWorkbook.worksheets) {
            console.log(chalk.blue(`Обрабатывается лист целевого файла "${targetWorksheet.name}"`));

            // Получение индексов столбцов в целевом листе
            const targetHeaderRow = targetWorksheet.getRow(1);
            const targetColumnIndices = getColumnIndices(
              targetHeaderRow,
              filePair.targetColumns
            );

            // Извлечение данных из исходного листа
            const dataToAppend = [];
            sourceWorksheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
              if (rowNumber === 1) return; // Пропустить заголовок
              const dataRow = {};
              filePair.sourceColumns.forEach((col) => {
                const colIndex = sourceColumnIndices[col];
                dataRow[col] = row.getCell(colIndex).value;
              });
              dataToAppend.push(dataRow);
            });

            // Отображение данных на целевые столбцы
            const mappedData = dataToAppend.map((dataRow) => {
              const mappedRow = {};
              filePair.sourceColumns.forEach((sourceCol, index) => {
                const targetCol = filePair.targetColumns[index];
                mappedRow[targetCol] = dataRow[sourceCol];
              });
              return mappedRow;
            });

            // Удаление дубликатов
            const existingRowsSet = new Set();
            targetWorksheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
              if (rowNumber === 1) return; // Пропустить заголовок
              const keyValues = filePair.keyColumns
                .map((col) => {
                  const colIndex = targetColumnIndices[col];
                  return row.getCell(colIndex).value;
                })
                .join('||');
              existingRowsSet.add(keyValues);
            });

            let newRowsAdded = 0;
            mappedData.forEach((dataRow) => {
              const keyValues = filePair.keyColumns
                .map((col) => dataRow[col])
                .join('||');
              if (!existingRowsSet.has(keyValues)) {
                existingRowsSet.add(keyValues);
                const newRow = targetWorksheet.addRow();
                Object.entries(dataRow).forEach(([header, value]) => {
                  const colIndex = targetColumnIndices[header];
                  newRow.getCell(colIndex).value = value;
                });
                newRowsAdded++;
              }
            });

            console.log(
              `Добавлено ${newRowsAdded} новых строк в лист "${targetWorksheet.name}" файла ${filePair.targetFileName}`
            );
          }

          // Сохранение и удаление исходного файла
          try {
            await targetWorkbook.xlsx.writeFile(targetFilePath);
            fs.unlinkSync(sourceFilePath);
            console.log(chalk.green(`Удален файл ${filePair.sourceFileName}`));
          } catch (err) {
            console.error(chalk.red(`Ошибка при сохранении или удалении файлов: ${err.message}`));
          }
        } catch (processingError) {
          console.error(chalk.red(`Ошибка при обработке листа "${sourceWorksheet.name}" в файле ${filePair.sourceFileName}: ${processingError.message}`));
        }
      }
    } else if (sourceFileExists) {
      console.log(chalk.yellow(`Только исходный файл существует: ${filePair.sourceFileName}`));
    } else if (targetFileExists) {
      console.log(chalk.yellow(`Только целевой файл существует: ${filePair.targetFileName}`));
    } else {
      console.log(
        `Файлы не найдены: ${filePair.sourceFileName} и ${filePair.targetFileName}`
      );
    }
  }
}

// Запуск процесса обработки файлов с обработкой ошибок
processFiles().catch((error) => {
  console.error('Произошла ошибка:', error);
});