fix: 修复TypeScript配置错误并更新项目文档

详细说明:
- 修复了@n8n/config包的TypeScript配置错误
- 移除了不存在的jest-expect-message类型引用
- 清理了所有TypeScript构建缓存
- 更新了可行性分析文档,添加了技术实施方案
- 更新了Agent prompt文档
- 添加了会展策划工作流文档
- 包含了n8n-chinese-translation子项目
- 添加了exhibition-demo展示系统框架
This commit is contained in:
Yep_Q
2025-09-08 10:49:45 +08:00
parent 8cf9d36d81
commit 3db7af209c
426 changed files with 71699 additions and 4401 deletions

View File

@@ -1,60 +0,0 @@
{
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "test",
"options": {
"binaryData": false
}
},
"id": "ec188f16-b2c5-44e3-bd83-259a94f15c94",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"webhookId": "a59a3be7-6d43-47e7-951d-86b8172c2006"
}
],
"connections": {
"Webhook": {
"main": [[]]
}
},
"trigger": {
"mode": "webhook",
"input": {
"json": {
"headers": {
"host": "localhost:5678",
"user-agent": "curl/8.2.0",
"accept": "*/*",
"content-length": "137",
"content-type": "multipart/form-data; boundary=--boundary"
},
"params": { "path": "test" },
"query": {},
"body": { "a": ["b"] }
}
}
},
"pinData": {
"Webhook": [
{
"json": {
"headers": {
"host": "localhost:5678",
"user-agent": "curl/8.2.0",
"accept": "*/*",
"content-length": "137",
"content-type": "multipart/form-data; boundary=--boundary"
},
"params": { "path": "test" },
"query": {},
"body": {
"a": ["b"]
}
}
}
]
}
}

View File

@@ -1,137 +0,0 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import type { Request, Response } from 'express';
import fs from 'fs/promises';
import { mock } from 'jest-mock-extended';
import type { IWebhookFunctions } from 'n8n-workflow';
import { Webhook } from '../Webhook.node';
jest.mock('fs/promises');
const mockFs = jest.mocked(fs);
describe('Test Webhook Node', () => {
new NodeTestHarness().setupTests();
describe('handleFormData', () => {
const node = new Webhook();
const context = mock<IWebhookFunctions>({
nodeHelpers: mock(),
});
context.getNodeParameter.calledWith('options').mockReturnValue({});
context.getNode.calledWith().mockReturnValue({
type: 'n8n-nodes-base.webhook',
typeVersion: 1.1,
} as any);
const req = mock<Request>();
req.contentType = 'multipart/form-data';
context.getRequestObject.mockReturnValue(req);
it('should handle when no files are present', async () => {
req.body = {
files: {},
};
const returnData = await node.webhook(context);
expect(returnData.workflowData?.[0][0].binary).toBeUndefined();
expect(context.nodeHelpers.copyBinaryFile).not.toHaveBeenCalled();
});
it('should handle when files are present', async () => {
req.body = {
files: { file1: { filepath: '/tmp/test.txt' } },
};
const returnData = await node.webhook(context);
expect(returnData.workflowData?.[0][0].binary).not.toBeUndefined();
expect(context.nodeHelpers.copyBinaryFile).toHaveBeenCalled();
expect(mockFs.rm).toHaveBeenCalledWith('/tmp/test.txt', { force: true });
});
});
describe('streaming response mode', () => {
const node = new Webhook();
const context = mock<IWebhookFunctions>({
nodeHelpers: mock(),
});
const req = mock<Request>();
const res = mock<Response>();
beforeEach(() => {
jest.clearAllMocks();
context.getRequestObject.mockReturnValue(req);
context.getResponseObject.mockReturnValue(res);
context.getChildNodes.mockReturnValue([]);
context.getNode.mockReturnValue({
type: 'n8n-nodes-base.webhook',
typeVersion: 2,
name: 'Webhook',
} as any);
context.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'options') return {};
if (paramName === 'responseMode') return 'streaming';
return undefined;
});
req.headers = {};
req.params = {};
req.query = {};
req.body = { message: 'test' };
Object.defineProperty(req, 'ips', { value: [], configurable: true });
Object.defineProperty(req, 'ip', { value: '127.0.0.1', configurable: true });
res.writeHead.mockImplementation(() => res);
res.flushHeaders.mockImplementation(() => undefined);
});
it('should enable streaming when responseMode is "streaming"', async () => {
const result = await node.webhook(context);
// Verify streaming headers are set
expect(res.writeHead).toHaveBeenCalledWith(200, {
'Content-Type': 'application/json; charset=utf-8',
'Transfer-Encoding': 'chunked',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
expect(res.flushHeaders).toHaveBeenCalled();
// Verify response structure for streaming
expect(result).toEqual({
noWebhookResponse: true,
workflowData: expect.any(Array),
});
});
it('should not enable streaming when responseMode is not "streaming"', async () => {
context.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'options') return {};
if (paramName === 'responseMode') return 'onReceived';
return undefined;
});
const result = await node.webhook(context);
// Verify streaming headers are NOT set
expect(res.writeHead).not.toHaveBeenCalled();
expect(res.flushHeaders).not.toHaveBeenCalled();
// Verify normal response structure
expect(result).toEqual({
webhookResponse: undefined,
workflowData: expect.any(Array),
});
});
it('should handle multipart form data with streaming enabled', async () => {
req.contentType = 'multipart/form-data';
req.body = {
data: { message: 'Hello' },
files: {},
};
const result = await node.webhook(context);
// For multipart form data, streaming is handled in handleFormData method
// The current implementation returns normal workflowData for form data
expect(result).toEqual({
workflowData: expect.any(Array),
});
});
});
});

View File

@@ -1,474 +0,0 @@
import jwt from 'jsonwebtoken';
import { ApplicationError, type IWebhookFunctions } from 'n8n-workflow';
import type { WebhookParameters } from '../utils';
import {
checkResponseModeConfiguration,
configuredOutputs,
getResponseCode,
getResponseData,
isIpWhitelisted,
setupOutputConnection,
validateWebhookAuthentication,
} from '../utils';
jest.mock('jsonwebtoken', () => ({
verify: jest.fn(),
}));
describe('Webhook Utils', () => {
describe('getResponseCode', () => {
it('should return the response code if it exists', () => {
const parameters: WebhookParameters = {
responseCode: 404,
httpMethod: '',
responseMode: '',
responseData: '',
};
const responseCode = getResponseCode(parameters);
expect(responseCode).toBe(404);
});
it('should return the custom response code if it exists', () => {
const parameters: WebhookParameters = {
options: {
responseCode: {
values: {
responseCode: 200,
customCode: 201,
},
},
},
httpMethod: '',
responseMode: '',
responseData: '',
};
const responseCode = getResponseCode(parameters);
expect(responseCode).toBe(201);
});
it('should return the default response code if no response code is provided', () => {
const parameters: WebhookParameters = {
httpMethod: '',
responseMode: '',
responseData: '',
};
const responseCode = getResponseCode(parameters);
expect(responseCode).toBe(200);
});
});
describe('getResponseData', () => {
it('should return the response data if it exists', () => {
const parameters: WebhookParameters = {
responseData: 'Hello World',
httpMethod: '',
responseMode: '',
};
const responseData = getResponseData(parameters);
expect(responseData).toBe('Hello World');
});
it('should return the options response data if response mode is "onReceived"', () => {
const parameters: WebhookParameters = {
responseMode: 'onReceived',
options: {
responseData: 'Hello World',
},
httpMethod: '',
responseData: '',
};
const responseData = getResponseData(parameters);
expect(responseData).toBe('Hello World');
});
it('should return "noData" if options noResponseBody is true', () => {
const parameters: WebhookParameters = {
responseMode: 'onReceived',
options: {
noResponseBody: true,
},
httpMethod: '',
responseData: '',
};
const responseData = getResponseData(parameters);
expect(responseData).toBe('noData');
});
it('should return undefined if no response data is provided', () => {
const parameters: WebhookParameters = {
responseMode: 'onReceived',
httpMethod: '',
responseData: '',
};
const responseData = getResponseData(parameters);
expect(responseData).toBeUndefined();
});
});
describe('configuredOutputs', () => {
it('should return an array with a single output if httpMethod is not an array', () => {
const parameters: WebhookParameters = {
httpMethod: 'GET',
responseMode: '',
responseData: '',
};
const outputs = configuredOutputs(parameters);
expect(outputs).toEqual([
{
type: 'main',
displayName: 'GET',
},
]);
});
it('should return an array of outputs if httpMethod is an array', () => {
const parameters: WebhookParameters = {
httpMethod: ['GET', 'POST'],
responseMode: '',
responseData: '',
};
const outputs = configuredOutputs(parameters);
expect(outputs).toEqual([
{
type: 'main',
displayName: 'GET',
},
{
type: 'main',
displayName: 'POST',
},
]);
});
});
describe('setupOutputConnection', () => {
it('should return a function that sets the webhookUrl and executionMode in the output data', () => {
const ctx: Partial<IWebhookFunctions> = {
getNodeParameter: jest.fn().mockReturnValue('GET'),
getNodeWebhookUrl: jest.fn().mockReturnValue('https://example.com/webhook/'),
getMode: jest.fn().mockReturnValue('manual'),
};
const method = 'GET';
const additionalData = {
jwtPayload: {
userId: '123',
},
};
const outputData = {
json: {},
};
const setupOutput = setupOutputConnection(ctx as IWebhookFunctions, method, additionalData);
const result = setupOutput(outputData);
expect(result).toEqual([
[
{
json: {
webhookUrl: 'https://example.com/webhook-test/',
executionMode: 'test',
jwtPayload: { userId: '123' },
},
},
],
]);
});
it('should return a function that sets the webhookUrl and executionMode in the output data for multiple methods', () => {
const ctx: Partial<IWebhookFunctions> = {
getNodeParameter: jest.fn().mockReturnValue(['GET', 'POST']),
getNodeWebhookUrl: jest.fn().mockReturnValue('https://example.com/webhook/'),
getMode: jest.fn().mockReturnValue('manual'),
};
const method = 'POST';
const additionalData = {
jwtPayload: {
userId: '123',
},
};
const outputData = {
json: {},
};
const setupOutput = setupOutputConnection(ctx as IWebhookFunctions, method, additionalData);
const result = setupOutput(outputData);
expect(result).toEqual([
[],
[
{
json: {
webhookUrl: 'https://example.com/webhook-test/',
executionMode: 'test',
jwtPayload: { userId: '123' },
},
},
],
]);
});
});
describe('isIpWhitelisted', () => {
it('should return true if whitelist is undefined', () => {
expect(isIpWhitelisted(undefined, ['192.168.1.1'], '192.168.1.1')).toBe(true);
});
it('should return true if whitelist is an empty string', () => {
expect(isIpWhitelisted('', ['192.168.1.1'], '192.168.1.1')).toBe(true);
});
it('should return true if ip is in the whitelist', () => {
expect(isIpWhitelisted('192.168.1.1', ['192.168.1.2'], '192.168.1.1')).toBe(true);
});
it('should return true if any ip in ips is in the whitelist', () => {
expect(isIpWhitelisted('192.168.1.1', ['192.168.1.1', '192.168.1.2'])).toBe(true);
});
it('should return false if ip and ips are not in the whitelist', () => {
expect(isIpWhitelisted('192.168.1.3', ['192.168.1.1', '192.168.1.2'], '192.168.1.4')).toBe(
false,
);
});
it('should return true if any ip in ips matches any address in the whitelist array', () => {
expect(isIpWhitelisted(['192.168.1.1', '192.168.1.2'], ['192.168.1.2', '192.168.1.3'])).toBe(
true,
);
});
it('should return true if ip matches any address in the whitelist array', () => {
expect(isIpWhitelisted(['192.168.1.1', '192.168.1.2'], ['192.168.1.3'], '192.168.1.2')).toBe(
true,
);
});
it('should return false if ip and ips do not match any address in the whitelist array', () => {
expect(
isIpWhitelisted(
['192.168.1.4', '192.168.1.5'],
['192.168.1.1', '192.168.1.2'],
'192.168.1.3',
),
).toBe(false);
});
it('should handle comma-separated whitelist string', () => {
expect(isIpWhitelisted('192.168.1.1, 192.168.1.2', ['192.168.1.3'], '192.168.1.2')).toBe(
true,
);
});
it('should trim whitespace in comma-separated whitelist string', () => {
expect(isIpWhitelisted(' 192.168.1.1 , 192.168.1.2 ', ['192.168.1.3'], '192.168.1.2')).toBe(
true,
);
});
});
describe('checkResponseModeConfiguration', () => {
it('should throw an error if response mode is "responseNode" but no Respond to Webhook node is found', () => {
const context: Partial<IWebhookFunctions> = {
getNodeParameter: jest.fn().mockReturnValue('responseNode'),
getChildNodes: jest.fn().mockReturnValue([]),
getNode: jest.fn().mockReturnValue({ name: 'Webhook' }),
};
expect(() => {
checkResponseModeConfiguration(context as IWebhookFunctions);
}).toThrowError('No Respond to Webhook node found in the workflow');
});
it('should throw an error if response mode is not "responseNode" but a Respond to Webhook node is found', () => {
const context: Partial<IWebhookFunctions> = {
getNodeParameter: jest.fn().mockReturnValue('onReceived'),
getChildNodes: jest.fn().mockReturnValue([{ type: 'n8n-nodes-base.respondToWebhook' }]),
getNode: jest.fn().mockReturnValue({ name: 'Webhook' }),
};
expect(() => {
checkResponseModeConfiguration(context as IWebhookFunctions);
}).toThrowError('Webhook node not correctly configured');
});
});
describe('validateWebhookAuthentication', () => {
it('should return early if authentication is "none"', async () => {
const ctx: Partial<IWebhookFunctions> = {
getNodeParameter: jest.fn().mockReturnValue('none'),
};
const authPropertyName = 'authentication';
const result = await validateWebhookAuthentication(
ctx as IWebhookFunctions,
authPropertyName,
);
expect(result).toBeUndefined();
});
it('should throw an error if basicAuth is enabled but no authentication data is defined on the node', async () => {
const headers = {
authorization: 'Basic some-token',
};
const ctx: Partial<IWebhookFunctions> = {
getNodeParameter: jest.fn().mockReturnValue('basicAuth'),
getCredentials: jest.fn().mockRejectedValue(new Error()),
getRequestObject: jest.fn().mockReturnValue({
headers,
}),
getHeaderData: jest.fn().mockReturnValue(headers),
};
const authPropertyName = 'authentication';
await expect(
validateWebhookAuthentication(ctx as IWebhookFunctions, authPropertyName),
).rejects.toThrowError('No authentication data defined on node!');
});
it('should throw an error if basicAuth is enabled but the provided authentication data is wrong', async () => {
const headers = {
authorization: 'Basic some-token',
};
const ctx: Partial<IWebhookFunctions> = {
getNodeParameter: jest.fn().mockReturnValue('basicAuth'),
getCredentials: jest.fn().mockResolvedValue({
user: 'admin',
password: 'password',
}),
getRequestObject: jest.fn().mockReturnValue({
headers,
}),
getHeaderData: jest.fn().mockReturnValue(headers),
};
const authPropertyName = 'authentication';
await expect(
validateWebhookAuthentication(ctx as IWebhookFunctions, authPropertyName),
).rejects.toThrowError('Authorization is required!');
});
it('should throw an error if headerAuth is enabled but no authentication data is defined on the node', async () => {
const ctx: Partial<IWebhookFunctions> = {
getNodeParameter: jest.fn().mockReturnValue('headerAuth'),
getCredentials: jest
.fn()
.mockRejectedValue(new Error('No authentication data defined on node!')),
getRequestObject: jest.fn().mockReturnValue({
headers: {},
}),
getHeaderData: jest.fn().mockReturnValue({}),
};
const authPropertyName = 'authentication';
await expect(
validateWebhookAuthentication(ctx as IWebhookFunctions, authPropertyName),
).rejects.toThrowError('No authentication data defined on node!');
});
it('should throw an error if headerAuth is enabled but the provided authentication data is wrong', async () => {
const headers = {
authorization: 'Bearer invalid-token',
};
const ctx: Partial<IWebhookFunctions> = {
getNodeParameter: jest.fn().mockReturnValue('headerAuth'),
getCredentials: jest.fn().mockResolvedValue({
name: 'Authorization',
value: 'Bearer token',
}),
getRequestObject: jest.fn().mockReturnValue({
headers,
}),
getHeaderData: jest.fn().mockReturnValue(headers),
};
const authPropertyName = 'authentication';
await expect(
validateWebhookAuthentication(ctx as IWebhookFunctions, authPropertyName),
).rejects.toThrowError('Authorization data is wrong!');
});
it('should throw an error if jwtAuth is enabled but no authentication data is defined on the node', async () => {
const ctx: Partial<IWebhookFunctions> = {
getNodeParameter: jest.fn().mockReturnValue('jwtAuth'),
getCredentials: jest
.fn()
.mockRejectedValue(new Error('No authentication data defined on node!')),
getRequestObject: jest.fn().mockReturnValue({}),
getHeaderData: jest.fn().mockReturnValue({}),
};
const authPropertyName = 'authentication';
await expect(
validateWebhookAuthentication(ctx as IWebhookFunctions, authPropertyName),
).rejects.toThrowError('No authentication data defined on node!');
});
it('should throw an error if jwtAuth is enabled but no token is provided', async () => {
const ctx: Partial<IWebhookFunctions> = {
getNodeParameter: jest.fn().mockReturnValue('jwtAuth'),
getCredentials: jest.fn().mockResolvedValue({
keyType: 'passphrase',
publicKey: '',
secret: 'secret',
algorithm: 'HS256',
}),
getRequestObject: jest.fn().mockReturnValue({
headers: {},
}),
getHeaderData: jest.fn().mockReturnValue({}),
};
const authPropertyName = 'authentication';
await expect(
validateWebhookAuthentication(ctx as IWebhookFunctions, authPropertyName),
).rejects.toThrowError('No token provided');
});
it('should throw an error if jwtAuth is enabled but the provided token is invalid', async () => {
const headers = {
authorization: 'Bearer invalid-token',
};
const ctx: Partial<IWebhookFunctions> = {
getNodeParameter: jest.fn().mockReturnValue('jwtAuth'),
getCredentials: jest.fn().mockResolvedValue({
keyType: 'passphrase',
publicKey: '',
secret: 'secret',
algorithm: 'HS256',
}),
getRequestObject: jest.fn().mockReturnValue({
headers,
}),
getHeaderData: jest.fn().mockReturnValue(headers),
};
(jwt.verify as jest.Mock).mockImplementationOnce(() => {
throw new ApplicationError('jwt malformed');
});
const authPropertyName = 'authentication';
await expect(
validateWebhookAuthentication(ctx as IWebhookFunctions, authPropertyName),
).rejects.toThrowError('jwt malformed');
});
it('should return the decoded JWT payload if jwtAuth is enabled and the token is valid', async () => {
const decodedPayload = {
sub: '1234567890',
name: 'John Doe',
iat: 1516239022,
};
(jwt.verify as jest.Mock).mockReturnValue(decodedPayload);
const headers = {
authorization:
'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c',
};
const ctx: Partial<IWebhookFunctions> = {
getNodeParameter: jest.fn().mockReturnValue('jwtAuth'),
getCredentials: jest.fn().mockResolvedValue({
keyType: 'passphrase',
publicKey: '',
secret: 'secret',
algorithm: 'HS256',
}),
getRequestObject: jest.fn().mockReturnValue({
headers,
}),
getHeaderData: jest.fn().mockReturnValue(headers),
};
const authPropertyName = 'authentication';
const result = await validateWebhookAuthentication(
ctx as IWebhookFunctions,
authPropertyName,
);
expect(result).toEqual(decodedPayload);
});
});
});