mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57:29 -08:00
WIP: setup chat handling via websockets
This commit is contained in:
parent
a71194c7c2
commit
8a4509d7ce
153
packages/cli/src/chat/chat-service.ts
Normal file
153
packages/cli/src/chat/chat-service.ts
Normal file
|
@ -0,0 +1,153 @@
|
|||
import type { Application, Request } from 'express';
|
||||
import type { Server } from 'http';
|
||||
import { ServerResponse } from 'http';
|
||||
import type { IWorkflowDataProxyAdditionalKeys } from 'n8n-workflow';
|
||||
import { jsonParse, jsonStringify, Workflow } from 'n8n-workflow';
|
||||
import type { Socket } from 'net';
|
||||
import { Service } from 'typedi';
|
||||
import { parse as parseUrl } from 'url';
|
||||
import { type RawData, type WebSocket, Server as WebSocketServer } from 'ws';
|
||||
|
||||
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
||||
import { NodeTypes } from '@/node-types';
|
||||
|
||||
type ChatRequest = Request<{ workflowId: string }, {}, {}, { sessionId: string }> & {
|
||||
ws: WebSocket;
|
||||
};
|
||||
type Session = {
|
||||
connection: WebSocket;
|
||||
workflowId: string;
|
||||
executionId?: string;
|
||||
};
|
||||
|
||||
function heartbeat(this: WebSocket) {
|
||||
this.isAlive = true;
|
||||
}
|
||||
|
||||
@Service()
|
||||
export class ChatService {
|
||||
private readonly sessions = new Map<string, Session>();
|
||||
|
||||
constructor(
|
||||
private readonly workflowRepository: WorkflowRepository,
|
||||
private readonly nodeTypes: NodeTypes,
|
||||
) {
|
||||
// Ping all connected clients every 60 seconds
|
||||
setInterval(() => this.pingAll(), 60 * 1000);
|
||||
}
|
||||
|
||||
setup(server: Server, app: Application) {
|
||||
const wsServer = new WebSocketServer({ noServer: true });
|
||||
server.on('upgrade', (request: ChatRequest, socket: Socket, head) => {
|
||||
if (parseUrl(request.url).pathname === '/chat') {
|
||||
wsServer.handleUpgrade(request, socket, head, (ws) => {
|
||||
request.ws = ws;
|
||||
|
||||
const response = new ServerResponse(request);
|
||||
response.writeHead = (statusCode) => {
|
||||
if (statusCode > 200) ws.close();
|
||||
return response;
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
app.handle(request, response);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.use('/chat', async (req: ChatRequest) => await this.startSession(req));
|
||||
}
|
||||
|
||||
async startSession(req: ChatRequest) {
|
||||
const {
|
||||
ws,
|
||||
query: { sessionId },
|
||||
params: { workflowId },
|
||||
} = req;
|
||||
if (!sessionId) {
|
||||
ws.send('The query parameter "sessionId" is missing!');
|
||||
ws.close(1008);
|
||||
return;
|
||||
}
|
||||
|
||||
const workflowData = await this.workflowRepository.findOne({
|
||||
where: { id: workflowId },
|
||||
relations: { shared: { project: { projectRelations: true } } },
|
||||
});
|
||||
|
||||
if (workflowData === null) {
|
||||
ws.send(`Could not find workflow with id "${workflowId}"`);
|
||||
ws.close(1008);
|
||||
return;
|
||||
}
|
||||
|
||||
const session: Session = this.sessions.get(sessionId) ?? { connection: ws, workflowId };
|
||||
// Make sure that the session always points to the latest websocket connection
|
||||
session.connection = ws;
|
||||
|
||||
const workflow = new Workflow({
|
||||
id: workflowId,
|
||||
name: workflowData.name,
|
||||
nodes: workflowData.nodes,
|
||||
connections: workflowData.connections,
|
||||
active: workflowData.active,
|
||||
nodeTypes: this.nodeTypes,
|
||||
staticData: workflowData.staticData,
|
||||
settings: workflowData.settings,
|
||||
});
|
||||
// @ts-expect-error TODO: get the chat node here
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const startNode = {} as unknown as INode;
|
||||
|
||||
const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
|
||||
$executionId: session.executionId,
|
||||
};
|
||||
|
||||
// TODO: setup a trigger context to call `.trigger` on the chat node on every message
|
||||
|
||||
ws.isAlive = true;
|
||||
ws.on('pong', heartbeat);
|
||||
this.sessions.set(sessionId, session);
|
||||
|
||||
const onMessage = this.messageHandler(sessionId, session);
|
||||
ws.once('close', () => {
|
||||
ws.off('pong', heartbeat);
|
||||
ws.off('message', onMessage);
|
||||
this.sessions.delete(sessionId);
|
||||
});
|
||||
ws.on('message', onMessage);
|
||||
|
||||
ws.send(
|
||||
jsonStringify({
|
||||
type: 'chat_started',
|
||||
data: { sessionId },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private messageHandler(sessionId: string, { workflowId, executionId }: Session) {
|
||||
return (data: RawData) => {
|
||||
// TODO: handle closed sessions
|
||||
const buffer = Array.isArray(data) ? Buffer.concat(data) : Buffer.from(data);
|
||||
// TODO: start a new execution, or resume an existing one
|
||||
// TODO: Add executionId to the session
|
||||
// TODO: Call `.trigger` on the chat node
|
||||
console.log(sessionId, workflowId, executionId, jsonParse(buffer.toString('utf8')));
|
||||
};
|
||||
}
|
||||
|
||||
private pingAll() {
|
||||
for (const { connection, executionId } of this.sessions.values()) {
|
||||
// If a connection did not respond with a `PONG` in the last 60 seconds, disconnect
|
||||
if (!connection.isAlive) {
|
||||
return connection.terminate();
|
||||
if (executionId) {
|
||||
// TODO: schedule the execution for cancellation
|
||||
}
|
||||
}
|
||||
connection.isAlive = false;
|
||||
connection.ping();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import { resolve } from 'path';
|
|||
import { Container, Service } from 'typedi';
|
||||
|
||||
import { AbstractServer } from '@/abstract-server';
|
||||
import { ChatService } from '@/chat/chat-service';
|
||||
import config from '@/config';
|
||||
import {
|
||||
CLI_DIR,
|
||||
|
@ -402,5 +403,7 @@ export class Server extends AbstractServer {
|
|||
protected setupPushServer(): void {
|
||||
const { restEndpoint, server, app } = this;
|
||||
setupPushServer(restEndpoint, server, app);
|
||||
|
||||
Container.get(ChatService).setup(server, app);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2320,6 +2320,7 @@ export type WorkflowExecuteMode =
|
|||
| 'retry'
|
||||
| 'trigger'
|
||||
| 'webhook'
|
||||
| 'chat'
|
||||
| 'evaluation';
|
||||
|
||||
export type WorkflowActivateMode =
|
||||
|
|
Loading…
Reference in a new issue