Commit 53dd682a by YeYuheng Committed by GitHub

fix: preserve xlsx merged cells in read file worker (#7083)

parent 1ac361c0
...@@ -74,12 +74,7 @@ const markdownTableSplit = (props: SplitProps): SplitResponse => { ...@@ -74,12 +74,7 @@ const markdownTableSplit = (props: SplitProps): SplitResponse => {
} }
const header = splitText2Lines[0]; const header = splitText2Lines[0];
const headerSize = header.split('|').length - 2; const mdSplitString = splitText2Lines[1];
const mdSplitString = `| ${new Array(headerSize > 0 ? headerSize : 1)
.fill(0)
.map(() => '---')
.join(' | ')} |`;
const chunks: string[] = []; const chunks: string[] = [];
const defaultChunk = `${header} const defaultChunk = `${header}
......
...@@ -64,7 +64,6 @@ ...@@ -64,7 +64,6 @@
"next": "catalog:", "next": "catalog:",
"nextjs-cors": "2.2.1", "nextjs-cors": "2.2.1",
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"node-xlsx": "^0.24.0",
"p-limit": "^7.2.0", "p-limit": "^7.2.0",
"papaparse": "5.4.1", "papaparse": "5.4.1",
"pg": "^8.10.0", "pg": "^8.10.0",
...@@ -76,6 +75,7 @@ ...@@ -76,6 +75,7 @@
"turndown": "^7.1.2", "turndown": "^7.1.2",
"undici": "^7.18.2", "undici": "^7.18.2",
"winston": "^3.17.0", "winston": "^3.17.0",
"xlsx": "0.18.5",
"zod": "catalog:" "zod": "catalog:"
}, },
"devDependencies": { "devDependencies": {
......
...@@ -201,6 +201,12 @@ describe('readFileContentByBuffer', () => { ...@@ -201,6 +201,12 @@ describe('readFileContentByBuffer', () => {
it('should return formatText when getFormatText is true', async () => { it('should return formatText when getFormatText is true', async () => {
const buffer = Buffer.from('content'); const buffer = Buffer.from('content');
mockReadRawContentFromBuffer.mockResolvedValueOnce({
rawText: 'raw-text-with-|',
formatText: '| escaped\\|cell |',
imageList: []
});
const result = await readFileContentByBuffer({ const result = await readFileContentByBuffer({
teamId, teamId,
tmbId, tmbId,
...@@ -210,7 +216,7 @@ describe('readFileContentByBuffer', () => { ...@@ -210,7 +216,7 @@ describe('readFileContentByBuffer', () => {
getFormatText: true getFormatText: true
}); });
expect(result.rawText).toBeDefined(); expect(result.rawText).toBe('| escaped\\|cell |');
}); });
it('should return rawText when getFormatText is false', async () => { it('should return rawText when getFormatText is false', async () => {
......
...@@ -378,3 +378,30 @@ dsgsgfsgs22sddddddd`, ...@@ -378,3 +378,30 @@ dsgsgfsgs22sddddddd`,
expect(formatChunks(data)).toEqual(formatResult(mock.result)); expect(formatChunks(data)).toEqual(formatResult(mock.result));
}); });
it('should preserve escaped pipe in markdown table cells when splitting', async () => {
const text = `| 项目系数 | 垫付首年考核费 \\| cc | 4.90% |
| --- | --- | --- |
| 项目系数 | 投资回报率 \\| abcd | 6.86% |
| 项目系数 | 当年运营管理成本,cc | 1,500,000.00 |`;
const data = await rawText2Chunks({
rawText: text,
chunkTriggerType: ChunkTriggerConfigTypeEnum.forceChunk,
chunkTriggerMinSize: 10,
maxSize: 10000,
chunkSize: 80,
backupParse: false
});
expect(data.length).toBeGreaterThan(1);
for (const chunk of data) {
const lines = chunk.q.split('\n');
expect(lines[0]).toBe('| 项目系数 | 垫付首年考核费 \\| cc | 4.90% |');
expect(lines[1]).toBe('| --- | --- | --- |');
expect(lines[1]).not.toBe('| --- | --- | --- | --- |');
}
expect(data.map((chunk) => chunk.q).join('\n')).toContain('投资回报率 \\| abcd');
});
import { describe, expect, it } from 'vitest';
import { readCsvRawText } from '@fastgpt/service/worker/readFile/extension/csv';
describe('readCsvRawText', () => {
it('should keep raw text and remove empty rows and columns from format text', async () => {
const rawText = [
',"name|alias",,note,',
',"Alice|A",,"line1\nline2",',
',,,,',
',Bob,,normal,'
].join('\n');
const result = await readCsvRawText({
extension: 'csv',
buffer: Buffer.from(rawText, 'utf-8'),
encoding: 'utf-8'
});
expect(result.rawText).toBe(rawText);
expect(result.formatText).toBe(`| name\\|alias | note |
| --- | --- |
| Alice\\|A | line1\\nline2 |
| Bob | normal |`);
});
});
import { describe, expect, it } from 'vitest';
import {
filterEmptyTableData,
formatMarkdownTableCell,
formatMarkdownTableRow
} from '@fastgpt/service/worker/readFile/extension/utils';
describe('filterEmptyTableData', () => {
it('should remove empty rows and columns', () => {
const result = filterEmptyTableData([
['', 'name', '', 'age', ''],
['', 'Alice', '', 30, ''],
['', '', '', '', ''],
['', 'Bob', '', 25, '']
]);
expect(result).toEqual([
['name', 'age'],
['Alice', 30],
['Bob', 25]
]);
});
it('should return empty data when all rows are empty', () => {
const result = filterEmptyTableData([
['', undefined],
[null, ' ']
]);
expect(result).toEqual([]);
});
});
describe('formatMarkdownTableCell', () => {
it('should escape markdown table separators and line breaks', () => {
expect(formatMarkdownTableCell('name|alias')).toBe('name\\|alias');
expect(formatMarkdownTableCell('line1\r\nline2\nline3\rline4')).toBe(
'line1\\nline2\\nline3\\nline4'
);
expect(formatMarkdownTableCell(null)).toBe('');
});
});
describe('formatMarkdownTableRow', () => {
it('should format escaped markdown table row', () => {
expect(formatMarkdownTableRow(['name|alias', 'line1\nline2'])).toBe(
'| name\\|alias | line1\\nline2 |'
);
});
});
import { describe, expect, it } from 'vitest';
import XLSX from 'xlsx';
import { readXlsxRawText } from '@fastgpt/service/worker/readFile/extension/xlsx';
describe('readXlsxRawText', () => {
it('should skip empty rows when formatting xlsx content', async () => {
const worksheet = XLSX.utils.aoa_to_sheet([
['', 'name|alias', '', 'age', 'city', ''],
['', 'Alice|A', '', 30, 'Bei\njing', ''],
[],
['', '', '', '', '', ''],
[undefined, undefined, undefined],
['', 'Bob', '', 25, 'Shanghai', '']
]);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
const result = await readXlsxRawText({
extension: 'xlsx',
buffer,
encoding: 'utf-8'
});
expect(result.rawText).toBe(
',name|alias,,age,city,\n,Alice|A,,30,Bei\njing,\n,,,,,\n,,,,,\n,,,,,\n,Bob,,25,Shanghai,'
);
expect(result.formatText).toContain('| name\\|alias | age | city |');
expect(result.formatText).toContain('| Alice\\|A | 30 | Bei\\njing |');
expect(result.formatText).toContain('| Bob | 25 | Shanghai |');
expect(result.formatText).not.toContain('| | | |');
});
it('should fill merged cells before formatting xlsx content', async () => {
const worksheet = XLSX.utils.aoa_to_sheet([
['部门', '姓名', '区域', '', ''],
['销售', '张三', '华东', '', ''],
['', '李四', '', '', ''],
['技术', '王五', '华南', '', ''],
['', '', '', '', '']
]);
worksheet['!merges'] = [
{ s: { r: 1, c: 0 }, e: { r: 2, c: 0 } },
{ s: { r: 0, c: 2 }, e: { r: 0, c: 4 } },
{ s: { r: 1, c: 2 }, e: { r: 2, c: 4 } }
];
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
const result = await readXlsxRawText({
extension: 'xlsx',
buffer,
encoding: 'utf-8'
});
expect(result.formatText).toContain('| 部门 | 姓名 | 区域 | 区域 | 区域 |');
expect(result.formatText).toContain('| 销售 | 张三 | 华东 | 华东 | 华东 |');
expect(result.formatText).toContain('| 销售 | 李四 | 华东 | 华东 | 华东 |');
expect(result.formatText).toContain('| 技术 | 王五 | 华南 | | |');
});
it('should fill merged cells when sheet data starts from a non-A1 range', async () => {
const worksheet = XLSX.utils.aoa_to_sheet([
[],
['', '部门', '姓名'],
['', '销售', '张三'],
['', '', '李四']
]);
worksheet['!ref'] = 'B2:C4';
worksheet['!merges'] = [{ s: { r: 2, c: 1 }, e: { r: 3, c: 1 } }];
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
const result = await readXlsxRawText({
extension: 'xlsx',
buffer,
encoding: 'utf-8'
});
expect(result.formatText).toContain('| 部门 | 姓名 |');
expect(result.formatText).toContain('| 销售 | 张三 |');
expect(result.formatText).toContain('| 销售 | 李四 |');
});
});
import { describe, it, expect, beforeAll, afterEach, afterAll, vi } from 'vitest'; import { describe, it, expect, beforeAll, afterEach, afterAll, vi } from 'vitest';
import path from 'path'; import path from 'path';
import { existsSync, readFileSync } from 'fs'; import { existsSync, readFileSync } from 'fs';
import XLSX from 'xlsx';
import { JSZip } from '@fastgpt/service/core/ai/skill/package'; import { JSZip } from '@fastgpt/service/core/ai/skill/package';
const { mockUploadImage2S3Bucket } = vi.hoisted(() => ({ const { mockUploadImage2S3Bucket } = vi.hoisted(() => ({
...@@ -231,6 +232,26 @@ describeIfEnabled('readFile worker (real spawn integration)', () => { ...@@ -231,6 +232,26 @@ describeIfEnabled('readFile worker (real spawn integration)', () => {
expect(result.rawText).toContain('Shanghai'); expect(result.rawText).toContain('Shanghai');
}); });
it('解析 xlsx 时应转义 Markdown 表格分隔符', async () => {
const worksheet = XLSX.utils.aoa_to_sheet([
['name|alias', 'fullwidth|pipe'],
['Alice|A', '保留|字符']
]);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
const result = await readRawContentFromBuffer({
extension: 'xlsx',
encoding: 'utf-8',
buffer
});
expect(result.rawText).toContain('name|alias,fullwidth|pipe');
expect(result.formatText).toContain('| name\\|alias | fullwidth|pipe |');
expect(result.formatText).toContain('| Alice\\|A | 保留|字符 |');
});
it('解析带图片 docx 时通过主线程 uploadFile handler 上传图片', async () => { it('解析带图片 docx 时通过主线程 uploadFile handler 上传图片', async () => {
mockUploadImage2S3Bucket.mockResolvedValueOnce('dataset/test/docx-parsed/image.png'); mockUploadImage2S3Bucket.mockResolvedValueOnce('dataset/test/docx-parsed/image.png');
......
import Papa from 'papaparse'; import Papa from 'papaparse';
import { type ReadRawTextByBuffer, type ReadFileResponse } from '../type'; import { type ReadRawTextByBuffer, type ReadFileResponse } from '../type';
import { readFileRawText } from './rawText'; import { readFileRawText } from './rawText';
import { filterEmptyTableData, formatMarkdownTableRow } from './utils';
// 加载源文件内容 // 加载源文件内容
export const readCsvRawText = async (params: ReadRawTextByBuffer): Promise<ReadFileResponse> => { export const readCsvRawText = async (params: ReadRawTextByBuffer): Promise<ReadFileResponse> => {
...@@ -8,15 +9,19 @@ export const readCsvRawText = async (params: ReadRawTextByBuffer): Promise<ReadF ...@@ -8,15 +9,19 @@ export const readCsvRawText = async (params: ReadRawTextByBuffer): Promise<ReadF
const csvArr = Papa.parse(rawText).data as string[][]; const csvArr = Papa.parse(rawText).data as string[][];
const header = csvArr[0]; const filteredData = filterEmptyTableData(csvArr);
const header = filteredData[0];
if (!header) {
return {
rawText,
formatText: ''
};
}
// format to md table // format to md table
const formatText = `| ${header.join(' | ')} | const formatText = `${formatMarkdownTableRow(header)}
| ${header.map(() => '---').join(' | ')} | | ${header.map(() => '---').join(' | ')} |
${csvArr ${filteredData.slice(1).map(formatMarkdownTableRow).join('\n')}`;
.slice(1)
.map((row) => `| ${row.map((item) => item.replace(/\n/g, '\\n')).join(' | ')} |`)
.join('\n')}`;
return { return {
rawText, rawText,
......
/**
* 清理表格二维数组中的全空行和全空列,保留原始行列顺序。
* 这里只判断单元格文本是否有有效内容,不做类型转换或业务格式化。
*/
export const filterEmptyTableData = (data: unknown[][]) => {
const filteredRows = data.filter((row) => row.some((cell) => String(cell ?? '').trim() !== ''));
const maxColumnLength = Math.max(0, ...filteredRows.map((row) => row.length));
const columnIndexes = Array.from({ length: maxColumnLength }, (_, index) => index).filter(
(index) => filteredRows.some((row) => String(row[index] ?? '').trim() !== '')
);
return filteredRows.map((row) => columnIndexes.map((index) => row[index] ?? ''));
};
/**
* 转义 Markdown table 单元格中的结构字符,避免单元格内容破坏表格列结构。
*/
export const formatMarkdownTableCell = (cell: unknown) => {
return String(cell ?? '')
.replace(/\r\n|\r|\n/g, '\\n')
.replace(/\|/g, '\\|');
};
export const formatMarkdownTableRow = (row: unknown[]) => {
return `| ${row.map(formatMarkdownTableCell).join(' | ')} |`;
};
import { CUSTOM_SPLIT_SIGN } from '@fastgpt/global/common/string/textSplitter'; import { CUSTOM_SPLIT_SIGN } from '@fastgpt/global/common/string/textSplitter';
import { type ReadRawTextByBuffer, type ReadFileResponse } from '../type'; import { type ReadRawTextByBuffer, type ReadFileResponse } from '../type';
import xlsx from 'node-xlsx'; import XLSX from 'xlsx';
import Papa from 'papaparse'; import { filterEmptyTableData, formatMarkdownTableRow } from './utils';
export const readXlsxRawText = async ({ export const readXlsxRawText = async ({
buffer buffer
}: ReadRawTextByBuffer): Promise<ReadFileResponse> => { }: ReadRawTextByBuffer): Promise<ReadFileResponse> => {
const result = xlsx.parse(buffer, { const workbook = XLSX.read(buffer, {
skipHidden: false, type: 'buffer',
defval: '' cellDates: true
}); });
const result = workbook.SheetNames.map((name) => {
const worksheet = workbook.Sheets[name];
const data = XLSX.utils.sheet_to_json<unknown[]>(worksheet, {
header: 1,
defval: '',
blankrows: true,
raw: false
});
const merges = worksheet['!merges'] ?? [];
const sheetRange = worksheet['!ref'] ? XLSX.utils.decode_range(worksheet['!ref']) : undefined;
const startRow = sheetRange?.s.r ?? 0;
const startColumn = sheetRange?.s.c ?? 0;
if (merges.length > 0) {
// 合并单元格只有左上角存值;!merges 使用 Excel 绝对坐标,
// 但 sheet_to_json 生成的二维数组从 !ref 起点开始,所以填充前要扣掉起始偏移。
// 必须先补齐合并区域,再做空行空列过滤,否则会丢失用户在 Excel 中表达的结构语义。
for (const merge of merges) {
const startDataRow = merge.s.r - startRow;
const startDataColumn = merge.s.c - startColumn;
const endDataRow = merge.e.r - startRow;
const endDataColumn = merge.e.c - startColumn;
const value = data[startDataRow]?.[startDataColumn] ?? '';
if (String(value).trim() === '') continue;
for (let rowIndex = startDataRow; rowIndex <= endDataRow; rowIndex++) {
if (rowIndex < 0) continue;
data[rowIndex] ??= [];
for (let columnIndex = startDataColumn; columnIndex <= endDataColumn; columnIndex++) {
if (columnIndex < 0) continue;
data[rowIndex][columnIndex] = value;
}
}
}
}
return {
name,
data
};
});
const filteredResult = result.map(({ name, data }) => ({
name,
data: filterEmptyTableData(data)
}));
const format2Csv = result.map(({ name, data }) => { const format2Csv = result.map(({ name, data }) => {
return { return {
title: `#${name}`, title: `#${name}`,
...@@ -20,17 +70,14 @@ export const readXlsxRawText = async ({ ...@@ -20,17 +70,14 @@ export const readXlsxRawText = async ({
const rawText = format2Csv.map((item) => item.csvText).join('\n'); const rawText = format2Csv.map((item) => item.csvText).join('\n');
const formatText = result const formatText = filteredResult
.map(({ data }) => { .map(({ data }) => {
const header = data[0]; const header = data[0];
if (!header) return; if (!header) return;
const formatText = `| ${header.join(' | ')} | const formatText = `${formatMarkdownTableRow(header)}
| ${header.map(() => '---').join(' | ')} | | ${header.map(() => '---').join(' | ')} |
${data ${data.slice(1).map(formatMarkdownTableRow).join('\n')}`;
.slice(1)
.map((row) => `| ${row.map((cell) => String(cell).replace(/\n/g, '\\n')).join(' | ')} |`)
.join('\n')}`;
return formatText; return formatText;
}) })
......
...@@ -602,9 +602,6 @@ importers: ...@@ -602,9 +602,6 @@ importers:
node-cron: node-cron:
specifier: ^3.0.3 specifier: ^3.0.3
version: 3.0.3 version: 3.0.3
node-xlsx:
specifier: ^0.24.0
version: 0.24.0
p-limit: p-limit:
specifier: ^7.2.0 specifier: ^7.2.0
version: 7.2.0 version: 7.2.0
...@@ -638,6 +635,9 @@ importers: ...@@ -638,6 +635,9 @@ importers:
winston: winston:
specifier: ^3.17.0 specifier: ^3.17.0
version: 3.17.0 version: 3.17.0
xlsx:
specifier: 0.18.5
version: 0.18.5
zod: zod:
specifier: 'catalog:' specifier: 'catalog:'
version: 4.1.12 version: 4.1.12
...@@ -7152,6 +7152,10 @@ packages: ...@@ -7152,6 +7152,10 @@ packages:
resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
adler-32@1.3.1:
resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
engines: {node: '>=0.8'}
adm-zip@0.5.17: adm-zip@0.5.17:
resolution: {integrity: sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==} resolution: {integrity: sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==}
engines: {node: '>=12.0'} engines: {node: '>=12.0'}
...@@ -7749,6 +7753,10 @@ packages: ...@@ -7749,6 +7753,10 @@ packages:
ccount@2.0.1: ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
cfb@1.2.2:
resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==}
engines: {node: '>=0.8'}
chai@6.2.2: chai@6.2.2:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
engines: {node: '>=18'} engines: {node: '>=18'}
...@@ -7907,6 +7915,10 @@ packages: ...@@ -7907,6 +7915,10 @@ packages:
codemirror@6.0.2: codemirror@6.0.2:
resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==}
codepage@1.15.0:
resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==}
engines: {node: '>=0.8'}
collapse-white-space@2.1.0: collapse-white-space@2.1.0:
resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==}
...@@ -9341,6 +9353,10 @@ packages: ...@@ -9341,6 +9353,10 @@ packages:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
frac@1.1.2:
resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==}
engines: {node: '>=0.8'}
fraction.js@5.3.4: fraction.js@5.3.4:
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
...@@ -11659,11 +11675,6 @@ packages: ...@@ -11659,11 +11675,6 @@ packages:
node-releases@2.0.37: node-releases@2.0.37:
resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==}
node-xlsx@0.24.0:
resolution: {integrity: sha512-1olwK48XK9nXZsyH/FCltvGrQYvXXZuxVitxXXv2GIuRm51aBi1+5KwR4rWM4KeO61sFU+00913WLZTD+AcXEg==}
engines: {node: '>=10.0.0'}
hasBin: true
nodemailer@7.0.13: nodemailer@7.0.13:
resolution: {integrity: sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==} resolution: {integrity: sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
...@@ -13350,6 +13361,10 @@ packages: ...@@ -13350,6 +13361,10 @@ packages:
resolution: {integrity: sha512-JPopy3jfNmPcUz5Ru6skKhHNRJbsvcEW6Z4SirKkucLS8Jya1Bmf4FVX8giOkLm8xQJ7kK68P6GXoVSTkbedUA==} resolution: {integrity: sha512-JPopy3jfNmPcUz5Ru6skKhHNRJbsvcEW6Z4SirKkucLS8Jya1Bmf4FVX8giOkLm8xQJ7kK68P6GXoVSTkbedUA==}
engines: {node: '>= 14.19.3'} engines: {node: '>= 14.19.3'}
ssf@0.11.2:
resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==}
engines: {node: '>=0.8'}
sshpk@1.18.0: sshpk@1.18.0:
resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
...@@ -14591,10 +14606,18 @@ packages: ...@@ -14591,10 +14606,18 @@ packages:
resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==} resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
wmf@1.0.2:
resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==}
engines: {node: '>=0.8'}
word-wrap@1.2.5: word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
word@0.3.0:
resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==}
engines: {node: '>=0.8'}
wrap-ansi@6.2.0: wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'} engines: {node: '>=8'}
...@@ -14649,9 +14672,8 @@ packages: ...@@ -14649,9 +14672,8 @@ packages:
resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
xlsx@https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz: xlsx@0.18.5:
resolution: {tarball: https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz} resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==}
version: 0.20.2
engines: {node: '>=0.8'} engines: {node: '>=0.8'}
hasBin: true hasBin: true
...@@ -21755,6 +21777,8 @@ snapshots: ...@@ -21755,6 +21777,8 @@ snapshots:
address@1.2.2: {} address@1.2.2: {}
adler-32@1.3.1: {}
adm-zip@0.5.17: {} adm-zip@0.5.17: {}
agent-base@6.0.2: agent-base@6.0.2:
...@@ -22454,6 +22478,11 @@ snapshots: ...@@ -22454,6 +22478,11 @@ snapshots:
ccount@2.0.1: {} ccount@2.0.1: {}
cfb@1.2.2:
dependencies:
adler-32: 1.3.1
crc-32: 1.2.2
chai@6.2.2: {} chai@6.2.2: {}
chalk@1.1.3: chalk@1.1.3:
...@@ -22629,6 +22658,8 @@ snapshots: ...@@ -22629,6 +22658,8 @@ snapshots:
'@codemirror/state': 6.5.2 '@codemirror/state': 6.5.2
'@codemirror/view': 6.38.6 '@codemirror/view': 6.38.6
codepage@1.15.0: {}
collapse-white-space@2.1.0: {} collapse-white-space@2.1.0: {}
color-convert@1.9.3: color-convert@1.9.3:
...@@ -24525,6 +24556,8 @@ snapshots: ...@@ -24525,6 +24556,8 @@ snapshots:
forwarded@0.2.0: {} forwarded@0.2.0: {}
frac@1.1.2: {}
fraction.js@5.3.4: {} fraction.js@5.3.4: {}
framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
...@@ -27479,10 +27512,6 @@ snapshots: ...@@ -27479,10 +27512,6 @@ snapshots:
node-releases@2.0.37: {} node-releases@2.0.37: {}
node-xlsx@0.24.0:
dependencies:
xlsx: https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz
nodemailer@7.0.13: {} nodemailer@7.0.13: {}
non-layered-tidy-tree-layout@2.0.2: {} non-layered-tidy-tree-layout@2.0.2: {}
...@@ -29594,6 +29623,10 @@ snapshots: ...@@ -29594,6 +29623,10 @@ snapshots:
sse-decoder@1.0.0: {} sse-decoder@1.0.0: {}
ssf@0.11.2:
dependencies:
frac: 1.1.2
sshpk@1.18.0: sshpk@1.18.0:
dependencies: dependencies:
asn1: 0.2.6 asn1: 0.2.6
...@@ -31085,8 +31118,12 @@ snapshots: ...@@ -31085,8 +31118,12 @@ snapshots:
triple-beam: 1.4.1 triple-beam: 1.4.1
winston-transport: 4.9.0 winston-transport: 4.9.0
wmf@1.0.2: {}
word-wrap@1.2.5: {} word-wrap@1.2.5: {}
word@0.3.0: {}
wrap-ansi@6.2.0: wrap-ansi@6.2.0:
dependencies: dependencies:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
...@@ -31136,7 +31173,15 @@ snapshots: ...@@ -31136,7 +31173,15 @@ snapshots:
xdg-basedir@5.1.0: {} xdg-basedir@5.1.0: {}
xlsx@https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz: {} xlsx@0.18.5:
dependencies:
adler-32: 1.3.1
cfb: 1.2.2
codepage: 1.15.0
crc-32: 1.2.2
ssf: 0.11.2
wmf: 1.0.2
word: 0.3.0
xml-crypto@6.1.2: xml-crypto@6.1.2:
dependencies: dependencies:
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment