mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(HTTP Request Node): Port optimizeResponse from httpRequest tool to standalone node (no-changelog) (#14307)
This commit is contained in:
@@ -7,6 +7,7 @@ export const CUSTOM_EXTENSION_ENV = 'N8N_CUSTOM_EXTENSIONS';
|
|||||||
export const PLACEHOLDER_EMPTY_EXECUTION_ID = '__UNKNOWN__';
|
export const PLACEHOLDER_EMPTY_EXECUTION_ID = '__UNKNOWN__';
|
||||||
export const PLACEHOLDER_EMPTY_WORKFLOW_ID = '__EMPTY__';
|
export const PLACEHOLDER_EMPTY_WORKFLOW_ID = '__EMPTY__';
|
||||||
export const HTTP_REQUEST_NODE_TYPE = 'n8n-nodes-base.httpRequest';
|
export const HTTP_REQUEST_NODE_TYPE = 'n8n-nodes-base.httpRequest';
|
||||||
|
export const HTTP_REQUEST_AS_TOOL_NODE_TYPE = 'n8n-nodes-base.httpRequestTool';
|
||||||
export const HTTP_REQUEST_TOOL_NODE_TYPE = '@n8n/n8n-nodes-langchain.toolHttpRequest';
|
export const HTTP_REQUEST_TOOL_NODE_TYPE = '@n8n/n8n-nodes-langchain.toolHttpRequest';
|
||||||
|
|
||||||
export const RESTRICT_FILE_ACCESS_TO = 'N8N_RESTRICT_FILE_ACCESS_TO';
|
export const RESTRICT_FILE_ACCESS_TO = 'N8N_RESTRICT_FILE_ACCESS_TO';
|
||||||
|
|||||||
@@ -28,7 +28,11 @@ import {
|
|||||||
NodeOperationError,
|
NodeOperationError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { HTTP_REQUEST_NODE_TYPE, HTTP_REQUEST_TOOL_NODE_TYPE } from '@/constants';
|
import {
|
||||||
|
HTTP_REQUEST_AS_TOOL_NODE_TYPE,
|
||||||
|
HTTP_REQUEST_NODE_TYPE,
|
||||||
|
HTTP_REQUEST_TOOL_NODE_TYPE,
|
||||||
|
} from '@/constants';
|
||||||
import { Memoized } from '@/decorators';
|
import { Memoized } from '@/decorators';
|
||||||
import { InstanceSettings } from '@/instance-settings';
|
import { InstanceSettings } from '@/instance-settings';
|
||||||
import { Logger } from '@/logging/logger';
|
import { Logger } from '@/logging/logger';
|
||||||
@@ -190,7 +194,11 @@ export abstract class NodeExecutionContext implements Omit<FunctionsBase, 'getCr
|
|||||||
|
|
||||||
// Hardcode for now for security reasons that only a single node can access
|
// Hardcode for now for security reasons that only a single node can access
|
||||||
// all credentials
|
// all credentials
|
||||||
const fullAccess = [HTTP_REQUEST_NODE_TYPE, HTTP_REQUEST_TOOL_NODE_TYPE].includes(node.type);
|
const fullAccess = [
|
||||||
|
HTTP_REQUEST_NODE_TYPE,
|
||||||
|
HTTP_REQUEST_TOOL_NODE_TYPE,
|
||||||
|
HTTP_REQUEST_AS_TOOL_NODE_TYPE,
|
||||||
|
].includes(node.type);
|
||||||
|
|
||||||
let nodeCredentialDescription: INodeCredentialDescription | undefined;
|
let nodeCredentialDescription: INodeCredentialDescription | undefined;
|
||||||
if (!fullAccess) {
|
if (!fullAccess) {
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ export const EXECUTE_COMMAND_NODE_TYPE = 'n8n-nodes-base.executeCommand';
|
|||||||
export const FORM_TRIGGER_NODE_TYPE = 'n8n-nodes-base.formTrigger';
|
export const FORM_TRIGGER_NODE_TYPE = 'n8n-nodes-base.formTrigger';
|
||||||
export const HTML_NODE_TYPE = 'n8n-nodes-base.html';
|
export const HTML_NODE_TYPE = 'n8n-nodes-base.html';
|
||||||
export const HTTP_REQUEST_NODE_TYPE = 'n8n-nodes-base.httpRequest';
|
export const HTTP_REQUEST_NODE_TYPE = 'n8n-nodes-base.httpRequest';
|
||||||
|
export const HTTP_REQUEST_TOOL_NODE_TYPE = 'n8n-nodes-base.httpRequestTool';
|
||||||
export const HUBSPOT_TRIGGER_NODE_TYPE = 'n8n-nodes-base.hubspotTrigger';
|
export const HUBSPOT_TRIGGER_NODE_TYPE = 'n8n-nodes-base.hubspotTrigger';
|
||||||
export const IF_NODE_TYPE = 'n8n-nodes-base.if';
|
export const IF_NODE_TYPE = 'n8n-nodes-base.if';
|
||||||
export const INTERVAL_NODE_TYPE = 'n8n-nodes-base.interval';
|
export const INTERVAL_NODE_TYPE = 'n8n-nodes-base.interval';
|
||||||
@@ -721,6 +722,7 @@ export const EXPRESSION_EDITOR_PARSER_TIMEOUT = 15_000; // ms
|
|||||||
|
|
||||||
export const KEEP_AUTH_IN_NDV_FOR_NODES = [
|
export const KEEP_AUTH_IN_NDV_FOR_NODES = [
|
||||||
HTTP_REQUEST_NODE_TYPE,
|
HTTP_REQUEST_NODE_TYPE,
|
||||||
|
HTTP_REQUEST_TOOL_NODE_TYPE,
|
||||||
WEBHOOK_NODE_TYPE,
|
WEBHOOK_NODE_TYPE,
|
||||||
WAIT_NODE_TYPE,
|
WAIT_NODE_TYPE,
|
||||||
DISCORD_NODE_TYPE,
|
DISCORD_NODE_TYPE,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { INodeProperties } from 'n8n-workflow';
|
import type { INodeProperties } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { optimizeResponseProperties } from '../shared/optimizeResponse';
|
||||||
|
|
||||||
export const mainProperties: INodeProperties[] = [
|
export const mainProperties: INodeProperties[] = [
|
||||||
{
|
{
|
||||||
displayName: '',
|
displayName: '',
|
||||||
@@ -1167,6 +1169,13 @@ export const mainProperties: INodeProperties[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
...optimizeResponseProperties.map((prop) => ({
|
||||||
|
...prop,
|
||||||
|
displayOptions: {
|
||||||
|
...prop.displayOptions,
|
||||||
|
show: { ...prop.displayOptions?.show, '@tool': [true] },
|
||||||
|
},
|
||||||
|
})),
|
||||||
{
|
{
|
||||||
displayName:
|
displayName:
|
||||||
"You can view the raw requests this node makes in your browser's developer console",
|
"You can view the raw requests this node makes in your browser's developer console",
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
sanitizeUiMessage,
|
sanitizeUiMessage,
|
||||||
setAgentOptions,
|
setAgentOptions,
|
||||||
} from '../GenericFunctions';
|
} from '../GenericFunctions';
|
||||||
|
import { configureResponseOptimizer } from '../shared/optimizeResponse';
|
||||||
|
|
||||||
function toText<T>(data: T) {
|
function toText<T>(data: T) {
|
||||||
if (typeof data === 'object' && data !== null) {
|
if (typeof data === 'object' && data !== null) {
|
||||||
@@ -832,6 +833,8 @@ export class HttpRequestV3 implements INodeType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// This is a no-op outside of tool usage
|
||||||
|
const optimizeResponse = configureResponseOptimizer(this, itemIndex);
|
||||||
|
|
||||||
if (autoDetectResponseFormat && !fullResponse) {
|
if (autoDetectResponseFormat && !fullResponse) {
|
||||||
delete response.headers;
|
delete response.headers;
|
||||||
@@ -839,9 +842,10 @@ export class HttpRequestV3 implements INodeType {
|
|||||||
delete response.statusMessage;
|
delete response.statusMessage;
|
||||||
}
|
}
|
||||||
if (!fullResponse) {
|
if (!fullResponse) {
|
||||||
response = response.body;
|
response = optimizeResponse(response.body);
|
||||||
|
} else {
|
||||||
|
response.body = optimizeResponse(response.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responseFormat === 'file') {
|
if (responseFormat === 'file') {
|
||||||
const outputPropertyName = this.getNodeParameter(
|
const outputPropertyName = this.getNodeParameter(
|
||||||
'options.response.response.outputPropertyName',
|
'options.response.response.outputPropertyName',
|
||||||
@@ -911,7 +915,6 @@ export class HttpRequestV3 implements INodeType {
|
|||||||
returnItem[outputPropertyName] = toText(response[property]);
|
returnItem[outputPropertyName] = toText(response[property]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
returnItem[property] = response[property];
|
returnItem[property] = response[property];
|
||||||
}
|
}
|
||||||
returnItems.push({
|
returnItems.push({
|
||||||
@@ -1001,11 +1004,17 @@ export class HttpRequestV3 implements INodeType {
|
|||||||
returnItems[0].json.data &&
|
returnItems[0].json.data &&
|
||||||
Array.isArray(returnItems[0].json.data)
|
Array.isArray(returnItems[0].json.data)
|
||||||
) {
|
) {
|
||||||
this.addExecutionHints({
|
const message =
|
||||||
message:
|
'To split the contents of ‘data’ into separate items for easier processing, add a ‘Split Out’ node after this one';
|
||||||
'To split the contents of ‘data’ into separate items for easier processing, add a ‘Split Out’ node after this one',
|
|
||||||
location: 'outputPane',
|
if (this.addExecutionHints) {
|
||||||
});
|
this.addExecutionHints({
|
||||||
|
message,
|
||||||
|
location: 'outputPane',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.logger.info(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [returnItems];
|
return [returnItems];
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { type IExecuteFunctions, NodeOperationError } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { configureResponseOptimizer } from './optimizeResponse';
|
||||||
|
|
||||||
|
describe('configureResponseOptimizer', () => {
|
||||||
|
const mockCtx = {
|
||||||
|
getNodeParameter: jest.fn(),
|
||||||
|
getNode: jest.fn(),
|
||||||
|
} as unknown as jest.Mocked<IExecuteFunctions>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the original response when optimizeResponse is false', () => {
|
||||||
|
mockCtx.getNodeParameter.mockImplementation((param) => {
|
||||||
|
if (param === 'optimizeResponse') return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const optimizer = configureResponseOptimizer(mockCtx, 0);
|
||||||
|
const response = { key: 'value' };
|
||||||
|
|
||||||
|
expect(optimizer(response)).toBe(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('htmlOptimizer', () => {
|
||||||
|
it('should optimize HTML response based on CSS selector', () => {
|
||||||
|
mockCtx.getNodeParameter.mockImplementation((param) => {
|
||||||
|
if (param === 'optimizeResponse') return true;
|
||||||
|
if (param === 'responseType') return 'html';
|
||||||
|
if (param === 'cssSelector') return 'div';
|
||||||
|
if (param === 'onlyContent') return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const optimizer = configureResponseOptimizer(mockCtx, 0);
|
||||||
|
const response = '<div>Hello</div><div>World</div>';
|
||||||
|
|
||||||
|
expect(optimizer(response)).toEqual('[\n "Hello",\n "World"\n]');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('textOptimizer', () => {
|
||||||
|
it('should extract readable text from HTML response', () => {
|
||||||
|
mockCtx.getNodeParameter.mockImplementation((param) => {
|
||||||
|
if (param === 'optimizeResponse') return true;
|
||||||
|
if (param === 'responseType') return 'text';
|
||||||
|
});
|
||||||
|
|
||||||
|
const optimizer = configureResponseOptimizer(mockCtx, 0);
|
||||||
|
const response = '<html><body><h1>Title</h1><p>Content</p></body></html>';
|
||||||
|
|
||||||
|
expect(optimizer(response)).toContain('Title');
|
||||||
|
expect(optimizer(response)).toContain('Content');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should truncate text if maxLength is set', () => {
|
||||||
|
mockCtx.getNodeParameter.mockImplementation((param) => {
|
||||||
|
if (param === 'optimizeResponse') return true;
|
||||||
|
if (param === 'responseType') return 'text';
|
||||||
|
if (param === 'truncateResponse') return true;
|
||||||
|
if (param === 'maxLength') return 5;
|
||||||
|
});
|
||||||
|
|
||||||
|
const optimizer = configureResponseOptimizer(mockCtx, 0);
|
||||||
|
const response = '<html><body><p>Content</p></body></html>';
|
||||||
|
|
||||||
|
expect(optimizer(response)).toEqual('Conte');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('jsonOptimizer', () => {
|
||||||
|
it('should parse JSON response and include all fields by default', () => {
|
||||||
|
mockCtx.getNodeParameter.mockImplementation((param) => {
|
||||||
|
if (param === 'optimizeResponse') return true;
|
||||||
|
if (param === 'responseType') return 'json';
|
||||||
|
if (param === 'fieldsToInclude') return 'all';
|
||||||
|
});
|
||||||
|
|
||||||
|
const optimizer = configureResponseOptimizer(mockCtx, 0);
|
||||||
|
const response = '{"key": "value"}';
|
||||||
|
|
||||||
|
expect(optimizer(response)).toEqual([{ key: 'value' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include only selected fields', () => {
|
||||||
|
mockCtx.getNodeParameter.mockImplementation((param) => {
|
||||||
|
if (param === 'optimizeResponse') return true;
|
||||||
|
if (param === 'responseType') return 'json';
|
||||||
|
if (param === 'fieldsToInclude') return 'selected';
|
||||||
|
if (param === 'fields') return 'key';
|
||||||
|
});
|
||||||
|
|
||||||
|
const optimizer = configureResponseOptimizer(mockCtx, 0);
|
||||||
|
const response = [{ key: 'value', otherKey: 'otherValue' }];
|
||||||
|
|
||||||
|
expect(optimizer(response)).toEqual([{ key: 'value' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exclude specified fields', () => {
|
||||||
|
mockCtx.getNodeParameter.mockImplementation((param) => {
|
||||||
|
if (param === 'optimizeResponse') return true;
|
||||||
|
if (param === 'responseType') return 'json';
|
||||||
|
if (param === 'fieldsToInclude') return 'except';
|
||||||
|
if (param === 'fields') return 'otherKey';
|
||||||
|
});
|
||||||
|
|
||||||
|
const optimizer = configureResponseOptimizer(mockCtx, 0);
|
||||||
|
const response = [{ key: 'value', otherKey: 'otherValue' }];
|
||||||
|
|
||||||
|
expect(optimizer(response)).toEqual([{ key: 'value' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if response is not a valid JSON object', () => {
|
||||||
|
mockCtx.getNodeParameter.mockImplementation((param) => {
|
||||||
|
if (param === 'optimizeResponse') return true;
|
||||||
|
if (param === 'responseType') return 'json';
|
||||||
|
});
|
||||||
|
|
||||||
|
const optimizer = configureResponseOptimizer(mockCtx, 0);
|
||||||
|
|
||||||
|
expect(() => optimizer('invalid json')).toThrow(NodeOperationError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
418
packages/nodes-base/nodes/HttpRequest/shared/optimizeResponse.ts
Normal file
418
packages/nodes-base/nodes/HttpRequest/shared/optimizeResponse.ts
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
import { Readability } from '@mozilla/readability';
|
||||||
|
import * as cheerio from 'cheerio';
|
||||||
|
import { convert } from 'html-to-text';
|
||||||
|
import { JSDOM } from 'jsdom';
|
||||||
|
import { get, set, unset } from 'lodash';
|
||||||
|
import {
|
||||||
|
type INodeProperties,
|
||||||
|
jsonParse,
|
||||||
|
NodeOperationError,
|
||||||
|
type IDataObject,
|
||||||
|
type IExecuteFunctions,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
type ResponseOptimizerFn = (
|
||||||
|
x: IDataObject | IDataObject[] | string,
|
||||||
|
) => IDataObject | IDataObject[] | string;
|
||||||
|
|
||||||
|
function htmlOptimizer(
|
||||||
|
ctx: IExecuteFunctions,
|
||||||
|
itemIndex: number,
|
||||||
|
maxLength: number,
|
||||||
|
): ResponseOptimizerFn {
|
||||||
|
const cssSelector = ctx.getNodeParameter('cssSelector', itemIndex, '') as string;
|
||||||
|
const onlyContent = ctx.getNodeParameter('onlyContent', itemIndex, false) as boolean;
|
||||||
|
let elementsToOmit: string[] = [];
|
||||||
|
|
||||||
|
if (onlyContent) {
|
||||||
|
const elementsToOmitUi = ctx.getNodeParameter('elementsToOmit', itemIndex, '') as
|
||||||
|
| string
|
||||||
|
| string[];
|
||||||
|
|
||||||
|
if (typeof elementsToOmitUi === 'string') {
|
||||||
|
elementsToOmit = elementsToOmitUi
|
||||||
|
.split(',')
|
||||||
|
.filter((s) => s)
|
||||||
|
.map((s) => s.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (response) => {
|
||||||
|
if (typeof response !== 'string') {
|
||||||
|
throw new NodeOperationError(
|
||||||
|
ctx.getNode(),
|
||||||
|
`The response type must be a string. Received: ${typeof response}`,
|
||||||
|
{ itemIndex },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const returnData: string[] = [];
|
||||||
|
|
||||||
|
const html = cheerio.load(response);
|
||||||
|
const htmlElements = html(cssSelector);
|
||||||
|
|
||||||
|
htmlElements.each((_, el) => {
|
||||||
|
let value = html(el).html() || '';
|
||||||
|
|
||||||
|
if (onlyContent) {
|
||||||
|
let htmlToTextOptions;
|
||||||
|
|
||||||
|
if (elementsToOmit?.length) {
|
||||||
|
htmlToTextOptions = {
|
||||||
|
selectors: elementsToOmit.map((selector) => ({
|
||||||
|
selector,
|
||||||
|
format: 'skip',
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
value = convert(value, htmlToTextOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
value = value
|
||||||
|
.trim()
|
||||||
|
.replace(/^\s+|\s+$/g, '')
|
||||||
|
.replace(/(\r\n|\n|\r)/gm, '')
|
||||||
|
.replace(/\s+/g, ' ');
|
||||||
|
|
||||||
|
returnData.push(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = JSON.stringify(returnData, null, 2);
|
||||||
|
|
||||||
|
if (maxLength > 0 && text.length > maxLength) {
|
||||||
|
return text.substring(0, maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const textOptimizer = (
|
||||||
|
ctx: IExecuteFunctions,
|
||||||
|
itemIndex: number,
|
||||||
|
maxLength: number,
|
||||||
|
): ResponseOptimizerFn => {
|
||||||
|
return (response) => {
|
||||||
|
if (typeof response === 'object') {
|
||||||
|
try {
|
||||||
|
response = JSON.stringify(response, null, 2);
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof response !== 'string') {
|
||||||
|
throw new NodeOperationError(
|
||||||
|
ctx.getNode(),
|
||||||
|
`The response type must be a string. Received: ${typeof response}`,
|
||||||
|
{ itemIndex },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dom = new JSDOM(response);
|
||||||
|
const article = new Readability(dom.window.document, {
|
||||||
|
keepClasses: true,
|
||||||
|
}).parse();
|
||||||
|
|
||||||
|
const text = article?.textContent || '';
|
||||||
|
|
||||||
|
if (maxLength > 0 && text.length > maxLength) {
|
||||||
|
return text.substring(0, maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const jsonOptimizer = (ctx: IExecuteFunctions, itemIndex: number): ResponseOptimizerFn => {
|
||||||
|
return (response) => {
|
||||||
|
let responseData: IDataObject | IDataObject[] | string | null = response;
|
||||||
|
|
||||||
|
if (typeof response === 'string') {
|
||||||
|
try {
|
||||||
|
responseData = jsonParse(response, { errorMessage: 'Invalid JSON response' });
|
||||||
|
} catch (error) {
|
||||||
|
throw new NodeOperationError(
|
||||||
|
ctx.getNode(),
|
||||||
|
`Received invalid JSON from response '${response}'`,
|
||||||
|
{ itemIndex },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof responseData !== 'object' || !responseData) {
|
||||||
|
throw new NodeOperationError(
|
||||||
|
ctx.getNode(),
|
||||||
|
'The response type must be an object or an array of objects',
|
||||||
|
{ itemIndex },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataField = ctx.getNodeParameter('dataField', itemIndex, '') as string;
|
||||||
|
let returnData: IDataObject[] = [];
|
||||||
|
|
||||||
|
if (!Array.isArray(responseData)) {
|
||||||
|
if (dataField) {
|
||||||
|
const data = responseData[dataField] as IDataObject | IDataObject[];
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
responseData = data;
|
||||||
|
} else {
|
||||||
|
responseData = [data];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
responseData = [responseData];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (dataField) {
|
||||||
|
responseData = responseData.map((data) => data[dataField]) as IDataObject[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldsToInclude = ctx.getNodeParameter('fieldsToInclude', itemIndex, 'all') as
|
||||||
|
| 'all'
|
||||||
|
| 'selected'
|
||||||
|
| 'except';
|
||||||
|
|
||||||
|
let fields: string | string[] = [];
|
||||||
|
|
||||||
|
if (fieldsToInclude !== 'all') {
|
||||||
|
fields = ctx.getNodeParameter('fields', itemIndex, []) as string[] | string;
|
||||||
|
|
||||||
|
if (typeof fields === 'string') {
|
||||||
|
fields = fields.split(',').map((field) => field.trim());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
returnData = responseData;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldsToInclude === 'selected') {
|
||||||
|
for (const item of responseData) {
|
||||||
|
const newItem: IDataObject = {};
|
||||||
|
|
||||||
|
for (const field of fields) {
|
||||||
|
set(newItem, field, get(item, field));
|
||||||
|
}
|
||||||
|
|
||||||
|
returnData.push(newItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldsToInclude === 'except') {
|
||||||
|
for (const item of responseData) {
|
||||||
|
for (const field of fields) {
|
||||||
|
unset(item, field);
|
||||||
|
}
|
||||||
|
|
||||||
|
returnData.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnData;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const configureResponseOptimizer = (
|
||||||
|
ctx: IExecuteFunctions,
|
||||||
|
itemIndex: number,
|
||||||
|
): ResponseOptimizerFn => {
|
||||||
|
const optimizeResponse = ctx.getNodeParameter('optimizeResponse', itemIndex, false) as boolean;
|
||||||
|
|
||||||
|
if (optimizeResponse) {
|
||||||
|
const responseType = ctx.getNodeParameter('responseType', itemIndex) as
|
||||||
|
| 'json'
|
||||||
|
| 'text'
|
||||||
|
| 'html';
|
||||||
|
|
||||||
|
let maxLength = 0;
|
||||||
|
const truncateResponse = ctx.getNodeParameter('truncateResponse', itemIndex, false) as boolean;
|
||||||
|
|
||||||
|
if (truncateResponse) {
|
||||||
|
maxLength = ctx.getNodeParameter('maxLength', itemIndex, 0) as number;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (responseType) {
|
||||||
|
case 'html':
|
||||||
|
return htmlOptimizer(ctx, itemIndex, maxLength);
|
||||||
|
case 'text':
|
||||||
|
return textOptimizer(ctx, itemIndex, maxLength);
|
||||||
|
case 'json':
|
||||||
|
return jsonOptimizer(ctx, itemIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (x) => x;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const optimizeResponseProperties: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Optimize Response',
|
||||||
|
name: 'optimizeResponse',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
noDataExpression: true,
|
||||||
|
description:
|
||||||
|
'Whether the optimize the tool response to reduce amount of data passed to the LLM that could lead to better result and reduce cost',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Expected Response Type',
|
||||||
|
name: 'responseType',
|
||||||
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
optimizeResponse: [true],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'JSON',
|
||||||
|
value: 'json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'HTML',
|
||||||
|
value: 'html',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Text',
|
||||||
|
value: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Field Containing Data',
|
||||||
|
name: 'dataField',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
placeholder: 'e.g. records',
|
||||||
|
description: 'Specify the name of the field in the response containing the data',
|
||||||
|
hint: 'leave blank to use whole response',
|
||||||
|
requiresDataPath: 'single',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
optimizeResponse: [true],
|
||||||
|
responseType: ['json'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Include Fields',
|
||||||
|
name: 'fieldsToInclude',
|
||||||
|
type: 'options',
|
||||||
|
description: 'What fields response object should include',
|
||||||
|
default: 'all',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
optimizeResponse: [true],
|
||||||
|
responseType: ['json'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'All',
|
||||||
|
value: 'all',
|
||||||
|
description: 'Include all fields',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Selected',
|
||||||
|
value: 'selected',
|
||||||
|
description: 'Include only fields specified below',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Except',
|
||||||
|
value: 'except',
|
||||||
|
description: 'Exclude fields specified below',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Fields',
|
||||||
|
name: 'fields',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
placeholder: 'e.g. field1,field2',
|
||||||
|
description:
|
||||||
|
'Comma-separated list of the field names. Supports dot notation. You can drag the selected fields from the input panel.',
|
||||||
|
requiresDataPath: 'multiple',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
optimizeResponse: [true],
|
||||||
|
responseType: ['json'],
|
||||||
|
},
|
||||||
|
hide: {
|
||||||
|
fieldsToInclude: ['all'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Selector (CSS)',
|
||||||
|
name: 'cssSelector',
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'Select specific element(e.g. body) or multiple elements(e.g. div) of chosen type in the response HTML.',
|
||||||
|
placeholder: 'e.g. body',
|
||||||
|
default: 'body',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
optimizeResponse: [true],
|
||||||
|
responseType: ['html'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Return Only Content',
|
||||||
|
name: 'onlyContent',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description:
|
||||||
|
'Whether to return only content of html elements, stripping html tags and attributes',
|
||||||
|
hint: 'Uses less tokens and may be easier for model to understand',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
optimizeResponse: [true],
|
||||||
|
responseType: ['html'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Elements To Omit',
|
||||||
|
name: 'elementsToOmit',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
optimizeResponse: [true],
|
||||||
|
responseType: ['html'],
|
||||||
|
onlyContent: [true],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
placeholder: 'e.g. img, .className, #ItemId',
|
||||||
|
description: 'Comma-separated list of selectors that would be excluded when extracting content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Truncate Response',
|
||||||
|
name: 'truncateResponse',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
hint: 'Helps save tokens',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
optimizeResponse: [true],
|
||||||
|
responseType: ['text', 'html'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Max Response Characters',
|
||||||
|
name: 'maxLength',
|
||||||
|
type: 'number',
|
||||||
|
default: 1000,
|
||||||
|
typeOptions: {
|
||||||
|
minValue: 1,
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
optimizeResponse: [true],
|
||||||
|
responseType: ['text', 'html'],
|
||||||
|
truncateResponse: [true],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -873,6 +873,7 @@
|
|||||||
"@n8n/di": "workspace:*",
|
"@n8n/di": "workspace:*",
|
||||||
"@n8n/imap": "workspace:*",
|
"@n8n/imap": "workspace:*",
|
||||||
"@n8n/vm2": "3.9.25",
|
"@n8n/vm2": "3.9.25",
|
||||||
|
"@mozilla/readability": "0.6.0",
|
||||||
"alasql": "4.4.0",
|
"alasql": "4.4.0",
|
||||||
"amqplib": "0.10.3",
|
"amqplib": "0.10.3",
|
||||||
"aws4": "1.11.0",
|
"aws4": "1.11.0",
|
||||||
@@ -895,6 +896,7 @@
|
|||||||
"isbot": "3.6.13",
|
"isbot": "3.6.13",
|
||||||
"iso-639-1": "2.1.15",
|
"iso-639-1": "2.1.15",
|
||||||
"js-nacl": "1.4.0",
|
"js-nacl": "1.4.0",
|
||||||
|
"jsdom": "23.0.1",
|
||||||
"jsonwebtoken": "catalog:",
|
"jsonwebtoken": "catalog:",
|
||||||
"kafkajs": "2.2.4",
|
"kafkajs": "2.2.4",
|
||||||
"ldapts": "4.2.6",
|
"ldapts": "4.2.6",
|
||||||
|
|||||||
@@ -1377,7 +1377,7 @@ export interface IDisplayOptions {
|
|||||||
};
|
};
|
||||||
show?: {
|
show?: {
|
||||||
'@version'?: Array<number | DisplayCondition>;
|
'@version'?: Array<number | DisplayCondition>;
|
||||||
'@tool'?: [boolean];
|
'@tool'?: boolean[];
|
||||||
[key: string]: Array<NodeParameterValue | DisplayCondition> | undefined;
|
[key: string]: Array<NodeParameterValue | DisplayCondition> | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
30
pnpm-lock.yaml
generated
30
pnpm-lock.yaml
generated
@@ -558,7 +558,7 @@ importers:
|
|||||||
version: 3.666.0(@aws-sdk/client-sts@3.666.0)
|
version: 3.666.0(@aws-sdk/client-sts@3.666.0)
|
||||||
'@getzep/zep-cloud':
|
'@getzep/zep-cloud':
|
||||||
specifier: 1.0.12
|
specifier: 1.0.12
|
||||||
version: 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(d1e86e144e3517fab3dbb7a92ab7f45a))
|
version: 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(fd386e1130022c8548c06dd951c5cbf0))
|
||||||
'@getzep/zep-js':
|
'@getzep/zep-js':
|
||||||
specifier: 0.9.0
|
specifier: 0.9.0
|
||||||
version: 0.9.0
|
version: 0.9.0
|
||||||
@@ -585,7 +585,7 @@ importers:
|
|||||||
version: 0.3.2(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)
|
version: 0.3.2(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)
|
||||||
'@langchain/community':
|
'@langchain/community':
|
||||||
specifier: 0.3.24
|
specifier: 0.3.24
|
||||||
version: 0.3.24(bae0580ee8bea2ce19e4657a460c92d0)
|
version: 0.3.24(c5fc7e11d6e6167a46cb8d3fd9b490a5)
|
||||||
'@langchain/core':
|
'@langchain/core':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
|
version: 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
|
||||||
@@ -684,7 +684,7 @@ importers:
|
|||||||
version: 23.0.1
|
version: 23.0.1
|
||||||
langchain:
|
langchain:
|
||||||
specifier: 0.3.11
|
specifier: 0.3.11
|
||||||
version: 0.3.11(d1e86e144e3517fab3dbb7a92ab7f45a)
|
version: 0.3.11(fd386e1130022c8548c06dd951c5cbf0)
|
||||||
lodash:
|
lodash:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
@@ -2016,6 +2016,9 @@ importers:
|
|||||||
'@kafkajs/confluent-schema-registry':
|
'@kafkajs/confluent-schema-registry':
|
||||||
specifier: 3.8.0
|
specifier: 3.8.0
|
||||||
version: 3.8.0
|
version: 3.8.0
|
||||||
|
'@mozilla/readability':
|
||||||
|
specifier: 0.6.0
|
||||||
|
version: 0.6.0
|
||||||
'@n8n/config':
|
'@n8n/config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../@n8n/config
|
version: link:../@n8n/config
|
||||||
@@ -2094,6 +2097,9 @@ importers:
|
|||||||
js-nacl:
|
js-nacl:
|
||||||
specifier: 1.4.0
|
specifier: 1.4.0
|
||||||
version: 1.4.0
|
version: 1.4.0
|
||||||
|
jsdom:
|
||||||
|
specifier: 23.0.1
|
||||||
|
version: 23.0.1
|
||||||
jsonwebtoken:
|
jsonwebtoken:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 9.0.2
|
version: 9.0.2
|
||||||
@@ -16149,7 +16155,7 @@ snapshots:
|
|||||||
'@gar/promisify@1.1.3':
|
'@gar/promisify@1.1.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@getzep/zep-cloud@1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(d1e86e144e3517fab3dbb7a92ab7f45a))':
|
'@getzep/zep-cloud@1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(fd386e1130022c8548c06dd951c5cbf0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
form-data: 4.0.0
|
form-data: 4.0.0
|
||||||
node-fetch: 2.7.0(encoding@0.1.13)
|
node-fetch: 2.7.0(encoding@0.1.13)
|
||||||
@@ -16158,7 +16164,7 @@ snapshots:
|
|||||||
zod: 3.24.1
|
zod: 3.24.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@langchain/core': 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
|
'@langchain/core': 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
|
||||||
langchain: 0.3.11(d1e86e144e3517fab3dbb7a92ab7f45a)
|
langchain: 0.3.11(fd386e1130022c8548c06dd951c5cbf0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
|
|
||||||
@@ -16673,7 +16679,7 @@ snapshots:
|
|||||||
- aws-crt
|
- aws-crt
|
||||||
- encoding
|
- encoding
|
||||||
|
|
||||||
'@langchain/community@0.3.24(bae0580ee8bea2ce19e4657a460c92d0)':
|
'@langchain/community@0.3.24(c5fc7e11d6e6167a46cb8d3fd9b490a5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@browserbasehq/stagehand': 1.9.0(@playwright/test@1.49.1)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))(zod@3.24.1)
|
'@browserbasehq/stagehand': 1.9.0(@playwright/test@1.49.1)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))(zod@3.24.1)
|
||||||
'@ibm-cloud/watsonx-ai': 1.1.2
|
'@ibm-cloud/watsonx-ai': 1.1.2
|
||||||
@@ -16684,7 +16690,7 @@ snapshots:
|
|||||||
flat: 5.0.2
|
flat: 5.0.2
|
||||||
ibm-cloud-sdk-core: 5.1.0
|
ibm-cloud-sdk-core: 5.1.0
|
||||||
js-yaml: 4.1.0
|
js-yaml: 4.1.0
|
||||||
langchain: 0.3.11(d1e86e144e3517fab3dbb7a92ab7f45a)
|
langchain: 0.3.11(fd386e1130022c8548c06dd951c5cbf0)
|
||||||
langsmith: 0.2.15(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
|
langsmith: 0.2.15(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
|
||||||
openai: 4.78.1(encoding@0.1.13)(zod@3.24.1)
|
openai: 4.78.1(encoding@0.1.13)(zod@3.24.1)
|
||||||
uuid: 10.0.0
|
uuid: 10.0.0
|
||||||
@@ -16699,7 +16705,7 @@ snapshots:
|
|||||||
'@aws-sdk/credential-provider-node': 3.666.0(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@aws-sdk/client-sts@3.666.0)
|
'@aws-sdk/credential-provider-node': 3.666.0(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@aws-sdk/client-sts@3.666.0)
|
||||||
'@azure/storage-blob': 12.18.0(encoding@0.1.13)
|
'@azure/storage-blob': 12.18.0(encoding@0.1.13)
|
||||||
'@browserbasehq/sdk': 2.0.0(encoding@0.1.13)
|
'@browserbasehq/sdk': 2.0.0(encoding@0.1.13)
|
||||||
'@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(d1e86e144e3517fab3dbb7a92ab7f45a))
|
'@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(fd386e1130022c8548c06dd951c5cbf0))
|
||||||
'@getzep/zep-js': 0.9.0
|
'@getzep/zep-js': 0.9.0
|
||||||
'@google-ai/generativelanguage': 2.6.0(encoding@0.1.13)
|
'@google-ai/generativelanguage': 2.6.0(encoding@0.1.13)
|
||||||
'@google-cloud/storage': 7.12.1(encoding@0.1.13)
|
'@google-cloud/storage': 7.12.1(encoding@0.1.13)
|
||||||
@@ -22891,7 +22897,7 @@ snapshots:
|
|||||||
'@types/debug': 4.1.12
|
'@types/debug': 4.1.12
|
||||||
'@types/node': 18.16.16
|
'@types/node': 18.16.16
|
||||||
'@types/tough-cookie': 4.0.2
|
'@types/tough-cookie': 4.0.2
|
||||||
axios: 1.8.2
|
axios: 1.8.2(debug@4.4.0)
|
||||||
camelcase: 6.3.0
|
camelcase: 6.3.0
|
||||||
debug: 4.4.0(supports-color@8.1.1)
|
debug: 4.4.0(supports-color@8.1.1)
|
||||||
dotenv: 16.4.5
|
dotenv: 16.4.5
|
||||||
@@ -22901,7 +22907,7 @@ snapshots:
|
|||||||
isstream: 0.1.2
|
isstream: 0.1.2
|
||||||
jsonwebtoken: 9.0.2
|
jsonwebtoken: 9.0.2
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
retry-axios: 2.6.0(axios@1.8.2(debug@4.4.0))
|
retry-axios: 2.6.0(axios@1.8.2)
|
||||||
tough-cookie: 4.1.3
|
tough-cookie: 4.1.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -23895,7 +23901,7 @@ snapshots:
|
|||||||
|
|
||||||
kuler@2.0.0: {}
|
kuler@2.0.0: {}
|
||||||
|
|
||||||
langchain@0.3.11(d1e86e144e3517fab3dbb7a92ab7f45a):
|
langchain@0.3.11(fd386e1130022c8548c06dd951c5cbf0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@langchain/core': 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
|
'@langchain/core': 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
|
||||||
'@langchain/openai': 0.3.17(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)
|
'@langchain/openai': 0.3.17(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)
|
||||||
@@ -26277,7 +26283,7 @@ snapshots:
|
|||||||
onetime: 5.1.2
|
onetime: 5.1.2
|
||||||
signal-exit: 3.0.7
|
signal-exit: 3.0.7
|
||||||
|
|
||||||
retry-axios@2.6.0(axios@1.8.2(debug@4.4.0)):
|
retry-axios@2.6.0(axios@1.8.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
axios: 1.8.2
|
axios: 1.8.2
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user