refactor: proper websocket message types

This commit is contained in:
Elian Doran 2025-09-13 12:54:53 +03:00
parent 998688573d
commit 4cd0702cbb
No known key found for this signature in database
19 changed files with 164 additions and 170 deletions

View File

@ -116,7 +116,7 @@ export type CommandMappings = {
openedFileUpdated: CommandData & {
entityType: string;
entityId: string;
lastModifiedMs: number;
lastModifiedMs?: number;
filePath: string;
};
focusAndSelectTitle: CommandData & {

View File

@ -210,7 +210,7 @@ function makeToast(id: string, message: string): ToastOptions {
}
ws.subscribeToMessages(async (message) => {
if (message.taskType !== "deleteNotes") {
if (!("taskType" in message) || message.taskType !== "deleteNotes") {
return;
}
@ -228,7 +228,7 @@ ws.subscribeToMessages(async (message) => {
});
ws.subscribeToMessages(async (message) => {
if (message.taskType !== "undeleteNotes") {
if (!("taskType" in message) || message.taskType !== "undeleteNotes") {
return;
}

View File

@ -1,16 +1,8 @@
import ws from "./ws.js";
import appContext from "../components/app_context.js";
import { OpenedFileUpdateStatus } from "@triliumnext/commons";
// TODO: Deduplicate
interface Message {
type: string;
entityType: string;
entityId: string;
lastModifiedMs: number;
filePath: string;
}
const fileModificationStatus: Record<string, Record<string, Message>> = {
const fileModificationStatus: Record<string, Record<string, OpenedFileUpdateStatus>> = {
notes: {},
attachments: {}
};
@ -39,7 +31,7 @@ function ignoreModification(entityType: string, entityId: string) {
delete fileModificationStatus[entityType][entityId];
}
ws.subscribeToMessages(async (message: Message) => {
ws.subscribeToMessages(async message => {
if (message.type !== "openedFileUpdated") {
return;
}

View File

@ -4,6 +4,7 @@ import ws from "./ws.js";
import utils from "./utils.js";
import appContext from "../components/app_context.js";
import { t } from "./i18n.js";
import { WebSocketMessage } from "@triliumnext/commons";
type BooleanLike = boolean | "true" | "false";
@ -66,7 +67,7 @@ function makeToast(id: string, message: string): ToastOptions {
}
ws.subscribeToMessages(async (message) => {
if (message.taskType !== "importNotes") {
if (!("taskType" in message) || message.taskType !== "importNotes") {
return;
}
@ -81,14 +82,14 @@ ws.subscribeToMessages(async (message) => {
toastService.showPersistent(toast);
if (message.result.importedNoteId) {
if (typeof message.result === "object" && message.result.importedNoteId) {
await appContext.tabManager.getActiveContext()?.setNote(message.result.importedNoteId);
}
}
});
ws.subscribeToMessages(async (message) => {
if (message.taskType !== "importAttachments") {
ws.subscribeToMessages(async (message: WebSocketMessage) => {
if (!("taskType" in message) || message.taskType !== "importAttachments") {
return;
}
@ -103,7 +104,7 @@ ws.subscribeToMessages(async (message) => {
toastService.showPersistent(toast);
if (message.result.parentNoteId) {
if (typeof message.result === "object" && message.result.parentNoteId) {
await appContext.tabManager.getActiveContext()?.setNote(message.result.importedNoteId, {
viewScope: {
viewMode: "attachments"

View File

@ -107,7 +107,7 @@ function makeToast(message: Message, title: string, text: string): ToastOptions
}
ws.subscribeToMessages(async (message) => {
if (message.taskType !== "protectNotes") {
if (!("taskType" in message) || message.taskType !== "protectNotes") {
return;
}

View File

@ -6,8 +6,9 @@ import frocaUpdater from "./froca_updater.js";
import appContext from "../components/app_context.js";
import { t } from "./i18n.js";
import type { EntityChange } from "../server_types.js";
import { WebSocketMessage } from "@triliumnext/commons";
type MessageHandler = (message: any) => void;
type MessageHandler = (message: WebSocketMessage) => void;
const messageHandlers: MessageHandler[] = [];
let ws: WebSocket;

View File

@ -140,7 +140,7 @@ ws.subscribeToMessages(async (message) => {
};
}
if (message.taskType !== "export") {
if (!("taskType" in message) || message.taskType !== "export") {
return;
}

View File

@ -5,6 +5,7 @@ import options from "../services/options.js";
import syncService from "../services/sync.js";
import { escapeQuotes } from "../services/utils.js";
import { Tooltip } from "bootstrap";
import { WebSocketMessage } from "@triliumnext/commons";
const TPL = /*html*/`
<div class="sync-status-widget launcher-button">
@ -117,8 +118,7 @@ export default class SyncStatusWidget extends BasicWidget {
this.$widget.find(`.sync-status-${className}`).show();
}
// TriliumNextTODO: Use Type Message from "services/ws.ts"
processMessage(message: { type: string; lastSyncedPush: number; data: { lastSyncedPush: number } }) {
processMessage(message: WebSocketMessage) {
if (message.type === "sync-pull-in-progress") {
this.syncState = "in-progress";
this.lastSyncedPush = message.lastSyncedPush;

View File

@ -73,14 +73,14 @@ export default class WatchedFileUpdateStatusWidget extends NoteContextAwareWidge
async refreshWithNote(note: FNote) {
const { entityType, entityId } = this.getEntity();
if (!entityType || !entityId) {
return;
}
if (!entityType || !entityId) return;
const status = fileWatcher.getFileModificationStatus(entityType, entityId);
this.$filePath.text(status.filePath);
if (status.lastModifiedMs) {
this.$fileLastModified.text(dayjs.unix(status.lastModifiedMs / 1000).format("HH:mm:ss"));
}
}
getEntity() {
if (!this.noteContext) {

View File

@ -1,18 +1,9 @@
import type { Request, Response } from "express";
import log from "../../services/log.js";
import options from "../../services/options.js";
import restChatService from "../../services/llm/rest_chat_service.js";
import chatStorageService from '../../services/llm/chat_storage_service.js';
// Define basic interfaces
interface ChatMessage {
role: 'user' | 'assistant' | 'system';
content: string;
timestamp?: Date;
}
import { WebSocketMessage } from "@triliumnext/commons";
/**
* @swagger
@ -590,7 +581,7 @@ async function handleStreamingProcess(
chatNoteId: chatNoteId
},
streamCallback: (data, done, rawChunk) => {
const message = {
const message: WebSocketMessage = {
type: 'llm-stream' as const,
chatNoteId: chatNoteId,
done: done

View File

@ -2,8 +2,7 @@
import mimeTypes from "mime-types";
import path from "path";
import type { TaskData } from "../task_context_interface.js";
import type { NoteType } from "@triliumnext/commons";
import type { NoteType, TaskData } from "@triliumnext/commons";
const CODE_MIME_TYPES = new Set([
"application/json",

View File

@ -4,7 +4,6 @@
import log from "../../../log.js";
import type { Response } from "express";
import type { StreamChunk } from "../../ai_interface.js";
import type { LLMStreamMessage } from "../../interfaces/chat_ws_messages.js";
import type { ChatSession } from "../../interfaces/chat_session.js";
/**
@ -46,7 +45,7 @@ export class StreamHandler {
type: 'llm-stream',
chatNoteId,
thinking: 'Preparing response...'
} as LLMStreamMessage);
});
try {
// Import the tool handler
@ -66,7 +65,7 @@ export class StreamHandler {
type: 'llm-stream',
chatNoteId,
thinking: 'Analyzing tools needed for this request...'
} as LLMStreamMessage);
});
try {
// Execute the tools
@ -82,7 +81,7 @@ export class StreamHandler {
tool: toolResult.name,
result: toolResult.content.substring(0, 100) + (toolResult.content.length > 100 ? '...' : '')
}
} as LLMStreamMessage);
});
}
// Make follow-up request with tool results
@ -123,7 +122,7 @@ export class StreamHandler {
chatNoteId,
error: `Error executing tools: ${toolError instanceof Error ? toolError.message : 'Unknown error'}`,
done: true
} as LLMStreamMessage);
});
}
} else if (response.stream) {
// Handle standard streaming through the stream() method
@ -152,7 +151,7 @@ export class StreamHandler {
chatNoteId,
content: messageContent,
done: true
} as LLMStreamMessage);
});
log.info(`Complete response sent`);
@ -174,14 +173,14 @@ export class StreamHandler {
type: 'llm-stream',
chatNoteId,
error: `Error generating response: ${streamingError instanceof Error ? streamingError.message : 'Unknown error'}`
} as LLMStreamMessage);
});
// Signal completion
wsService.sendMessageToAllClients({
type: 'llm-stream',
chatNoteId,
done: true
} as LLMStreamMessage);
});
}
}
@ -218,7 +217,7 @@ export class StreamHandler {
done: !!chunk.done, // Include done flag with each chunk
// Include any raw data from the provider that might contain thinking/tool info
...(chunk.raw ? { raw: chunk.raw } : {})
} as LLMStreamMessage);
});
// Log the first chunk (useful for debugging)
if (messageContent.length === chunk.text.length) {
@ -232,7 +231,7 @@ export class StreamHandler {
type: 'llm-stream',
chatNoteId,
thinking: chunk.raw.thinking
} as LLMStreamMessage);
});
}
// If the provider indicates tool execution, relay that
@ -241,7 +240,7 @@ export class StreamHandler {
type: 'llm-stream',
chatNoteId,
toolExecution: chunk.raw.toolExecution
} as LLMStreamMessage);
});
}
// Handle direct tool_calls in the response (for OpenAI)
@ -252,7 +251,7 @@ export class StreamHandler {
wsService.sendMessageToAllClients({
type: 'tool_execution_start',
chatNoteId
} as LLMStreamMessage);
});
// Process each tool call
for (const toolCall of chunk.tool_calls) {
@ -277,7 +276,7 @@ export class StreamHandler {
toolCallId: toolCall.id,
args: args
}
} as LLMStreamMessage);
});
}
}
@ -337,7 +336,7 @@ export class StreamHandler {
type: 'llm-stream',
chatNoteId,
done: true
} as LLMStreamMessage);
});
}
// Store the full response in the session
@ -360,7 +359,7 @@ export class StreamHandler {
chatNoteId,
error: `Error during streaming: ${streamError instanceof Error ? streamError.message : 'Unknown error'}`,
done: true
} as LLMStreamMessage);
});
throw streamError;
}

View File

@ -7,7 +7,6 @@ import { ToolHandler } from './handlers/tool_handler.js';
import { StreamHandler } from './handlers/stream_handler.js';
import * as messageFormatter from './utils/message_formatter.js';
import type { ChatSession, ChatMessage, NoteSource } from '../interfaces/chat_session.js';
import type { LLMStreamMessage } from '../interfaces/chat_ws_messages.js';
// Export components
export {
@ -22,6 +21,5 @@ export {
export type {
ChatSession,
ChatMessage,
NoteSource,
LLMStreamMessage
NoteSource
};

View File

@ -4,18 +4,15 @@
*/
import log from "../../log.js";
import type { Request, Response } from "express";
import type { Message, ChatCompletionOptions } from "../ai_interface.js";
import type { Message } from "../ai_interface.js";
import aiServiceManager from "../ai_service_manager.js";
import { ChatPipeline } from "../pipeline/chat_pipeline.js";
import type { ChatPipelineInput } from "../pipeline/interfaces.js";
import options from "../../options.js";
import { ToolHandler } from "./handlers/tool_handler.js";
import type { LLMStreamMessage } from "../interfaces/chat_ws_messages.js";
import chatStorageService from '../chat_storage_service.js';
import {
isAIEnabled,
getSelectedModelConfig,
} from '../config/configuration_helpers.js';
import { getSelectedModelConfig } from '../config/configuration_helpers.js';
import { WebSocketMessage } from "@triliumnext/commons";
/**
* Simplified service to handle chat API interactions
@ -204,7 +201,7 @@ class RestChatService {
accumulatedContentRef: { value: string },
chat: { id: string; messages: Message[]; title: string }
) {
const message: LLMStreamMessage = {
const message: WebSocketMessage = {
type: 'llm-stream',
chatNoteId: chatNoteId,
done: done

View File

@ -1,24 +0,0 @@
/**
* Interfaces for WebSocket LLM streaming messages
*/
/**
* Interface for WebSocket LLM streaming messages
*/
export interface LLMStreamMessage {
type: 'llm-stream' | 'tool_execution_start' | 'tool_result' | 'tool_execution_error' | 'tool_completion_processing';
chatNoteId: string;
content?: string;
thinking?: string;
toolExecution?: {
action?: string;
tool?: string;
toolCallId?: string;
result?: string | Record<string, any>;
error?: string;
args?: Record<string, unknown>;
};
done?: boolean;
error?: string;
raw?: unknown;
}

View File

@ -1,6 +1,6 @@
"use strict";
import type { TaskData } from "./task_context_interface.js";
import type { TaskData } from "@triliumnext/commons";
import ws from "./ws.js";
// taskId => TaskContext
@ -61,7 +61,7 @@ class TaskContext {
taskId: this.taskId,
taskType: this.taskType,
data: this.data,
message: message
message
});
}
@ -71,7 +71,7 @@ class TaskContext {
taskId: this.taskId,
taskType: this.taskType,
data: this.data,
result: result
result
});
}
}

View File

@ -1,7 +0,0 @@
export interface TaskData {
safeImport?: boolean;
textImportedAsText?: boolean;
codeImportedAsCode?: boolean;
shrinkImages?: boolean;
replaceUnderscoresWithSpaces?: boolean;
}

View File

@ -1,5 +1,5 @@
import { WebSocketServer as WebSocketServer, WebSocket } from "ws";
import { isDev, isElectron, randomString } from "./utils.js";
import { isElectron, randomString } from "./utils.js";
import log from "./log.js";
import sql from "./sql.js";
import cls from "./cls.js";
@ -15,52 +15,6 @@ import { WebSocketMessage, type EntityChange } from "@triliumnext/commons";
let webSocketServer!: WebSocketServer;
let lastSyncedPush: number | null = null;
interface Message {
type: string;
data?: {
lastSyncedPush?: number | null;
entityChanges?: any[];
shrinkImages?: boolean;
} | null;
lastSyncedPush?: number | null;
progressCount?: number;
taskId?: string;
taskType?: string | null;
message?: string;
reason?: string;
result?: string | Record<string, string | undefined>;
script?: string;
params?: any[];
noteId?: string;
messages?: string[];
startNoteId?: string;
currentNoteId?: string;
entityType?: string;
entityId?: string;
originEntityName?: "notes";
originEntityId?: string | null;
lastModifiedMs?: number;
filePath?: string;
// LLM streaming specific fields
chatNoteId?: string;
content?: string;
thinking?: string;
toolExecution?: {
action?: string;
tool?: string;
toolCallId?: string;
result?: string | Record<string, any>;
error?: string;
args?: Record<string, unknown>;
};
done?: boolean;
error?: string;
raw?: unknown;
}
type SessionParser = (req: IncomingMessage, params: {}, cb: () => void) => void;
function init(httpServer: HttpServer, sessionParser: SessionParser) {
webSocketServer = new WebSocketServer({
@ -106,7 +60,7 @@ Stack: ${message.stack}`);
});
}
function sendMessage(client: WebSocket, message: Message) {
function sendMessage(client: WebSocket, message: WebSocketMessage) {
const jsonStr = JSON.stringify(message);
if (client.readyState === WebSocket.OPEN) {
@ -114,7 +68,7 @@ function sendMessage(client: WebSocket, message: Message) {
}
}
function sendMessageToAllClients(message: Message) {
function sendMessageToAllClients(message: WebSocketMessage) {
const jsonStr = JSON.stringify(message);
if (webSocketServer) {

View File

@ -270,3 +270,96 @@ export interface EntityChangeRecord {
entityChange: EntityChange;
entity?: EntityRow;
}
type TaskStatus<TypeT, DataT> = {
type: "taskProgressCount",
taskId: string;
taskType: TypeT;
data: DataT,
progressCount: number
} | {
type: "taskError",
taskId: string;
taskType: TypeT;
data: DataT;
message: string;
} | {
type: "taskSucceeded",
taskId: string;
taskType: TypeT;
data: DataT;
result?: string | Record<string, string | undefined>
}
type TaskDefinitions =
TaskStatus<"protectNotes", { protect: boolean; }>
| TaskStatus<"importNotes", null>
| TaskStatus<"importAttachments", null>
| TaskStatus<"deleteNotes", null>
| TaskStatus<"undeleteNotes", null>
| TaskStatus<"export", null>
;
export interface OpenedFileUpdateStatus {
entityType: string;
entityId: string;
lastModifiedMs?: number;
filePath: string;
}
export type WebSocketMessage = TaskDefinitions | {
type: "ping"
} | {
type: "frontend-update",
data: {
lastSyncedPush: number,
entityChanges: EntityChange[]
}
} | {
type: "openNote",
noteId: string
} | OpenedFileUpdateStatus & {
type: "openedFileUpdated"
} | {
type: "protectedSessionLogin"
} | {
type: "protectedSessionLogout"
} | {
type: "toast",
message: string;
} | {
type: "api-log-messages",
noteId: string,
messages: string[]
} | {
type: "execute-script";
script: string;
params: unknown[];
startNoteId?: string;
currentNoteId: string;
originEntityName: string;
originEntityId?: string | null;
} | {
type: "reload-frontend";
reason: string;
} | {
type: "sync-pull-in-progress" | "sync-push-in-progress" | "sync-finished" | "sync-failed";
lastSyncedPush: number;
} | {
type: "consistency-checks-failed"
} | {
type: "llm-stream",
chatNoteId: string;
done?: boolean;
error?: string;
thinking?: string;
content?: string;
toolExecution?: {
action?: string;
tool?: string;
toolCallId?: string;
result?: string | Record<string, any>;
error?: string;
args?: Record<string, unknown>;
}
}