feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { useSpaceId } from './use-space-id';
export { useProjectIDEServices } from './use-project-ide-services';
export { useCurrentWidgetContext } from './use-current-widget-context';
export { useActivateWidgetContext } from './use-activate-widget-context';
export { useIDENavigate } from './use-ide-navigate';
export { useCurrentModeType } from './use-current-mode-type';
export { useProjectId } from './use-project-id';
export { useSplitScreenArea } from './use-current-split-screen';
export { useTitle } from './use-title';
export { useIDELocation, useIDEParams } from './use-ide-location';
export { useIDEServiceInBiz } from './use-ide-service-in-biz';
export { useShortcuts } from './use-shortcuts';
export { useCommitVersion } from './use-commit-version';
export { useWsListener } from './use-ws-listener';
export {
useSendMessageEvent,
useListenMessageEvent,
} from './use-message-event';
export { useViewService } from './use-view-service';
export { useGetUIWidgetFromId } from './use-get-ui-widget-from-id';

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
useCurrentWidgetFromArea,
LayoutPanelType,
} from '@coze-project-ide/client';
import { type ProjectIDEWidget } from '@/widgets/project-ide-widget';
import { type WidgetContext } from '@/context/widget-context';
/**
* 用于提供当前 focus 的 widget 上下文
*/
export const useActivateWidgetContext = (): WidgetContext => {
const currentWidget = useCurrentWidgetFromArea(LayoutPanelType.MAIN_PANEL);
return (currentWidget as ProjectIDEWidget)?.context;
};

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useIDEGlobalStore } from '../context';
export const useCommitVersion = () => {
// 内置了 shallow 操作,无需 useShallow
// eslint-disable-next-line @coze-arch/zustand/prefer-shallow
const { version, patch } = useIDEGlobalStore(store => ({
version: store.version,
patch: store.patch,
}));
return {
version,
patch,
};
};

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { type ModeType } from '../types';
import { UI_BUILDER_URI } from '../constants';
export const useCurrentModeType = () => {
const { pathname } = useLocation();
const type: ModeType = useMemo(() => {
if (pathname.includes(UI_BUILDER_URI.path.toString())) {
return 'ui-builder';
}
return 'dev';
}, [pathname]);
return type;
};

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, useState } from 'react';
import {
ApplicationShell,
useIDEService,
type URI,
type DockLayout,
type ReactWidget,
type TabBar,
type Widget,
} from '@coze-project-ide/client';
import { compareURI } from '@/utils';
type Area = 'left' | 'right';
const getTabArea = (shell: ApplicationShell, uri?: URI): Area | undefined => {
let currentTabIndex = -1;
const area = (shell.mainPanel?.layout as DockLayout)?.saveLayout?.().main;
const children = (area as DockLayout.ISplitAreaConfig)?.children || [area];
children.forEach((child, idx) => {
const containCurrent =
uri &&
((child as DockLayout.ITabAreaConfig)?.widgets || []).some(
widget => (widget as ReactWidget).uri?.toString?.() === uri.toString(),
);
if (containCurrent) {
currentTabIndex = idx;
}
});
// 右边分屏不展示 hover icon
if (children?.length === 1) {
return undefined;
} else if (currentTabIndex === 1) {
return 'right';
} else {
return 'left';
}
};
/**
* 获取当前 uri 的资源在哪个分屏下
* left: 左边分屏
* right: 右边分屏
* undefined: 未分屏
*/
export const useSplitScreenArea = (
uri?: URI,
tabBar?: TabBar<Widget>,
): Area | undefined => {
const shell = useIDEService<ApplicationShell>(ApplicationShell);
const [area, setArea] = useState(getTabArea(shell, uri));
useEffect(() => {
setArea(getTabArea(shell, uri));
const listener = () => {
// 本次 uri 是否在当前 tab不是不执行
// 分屏过程中会出现中间态,布局变更时盲目执行会导致时序异常问题
const uriInCurrentTab = tabBar?.titles.some(title =>
compareURI((title.owner as ReactWidget)?.uri, uri),
);
if (uriInCurrentTab) {
setArea(getTabArea(shell, uri));
}
};
shell.mainPanel.layoutModified.connect(listener);
return () => {
shell.mainPanel.layoutModified.disconnect(listener);
};
}, [uri?.toString?.()]);
return area;
};

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useCurrentWidget } from '@coze-project-ide/client';
import { type ProjectIDEWidget } from '@/widgets/project-ide-widget';
import { type WidgetContext } from '../context/widget-context';
/**
* 获取当前的 WidgetContext
* 在 registry 的 renderContent 内调用
*/
export function useCurrentWidgetContext<T>(): WidgetContext<T> {
const currentWidget = useCurrentWidget() as ProjectIDEWidget;
if (!currentWidget.context) {
throw new Error(
'[useWidgetContext] Undefined widgetContext from ide context',
);
}
return currentWidget.context as WidgetContext<T>;
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { URI, useIDEService, WidgetManager } from '@coze-project-ide/client';
import { type ProjectIDEWidget } from '../widgets/project-ide-widget';
import { URI_SCHEME } from '../constants';
export const useGetUIWidgetFromId = (
value: string,
): ProjectIDEWidget | undefined => {
const widgetManager = useIDEService<WidgetManager>(WidgetManager);
const uri = new URI(`${URI_SCHEME}://${value}`);
const widget = widgetManager.getWidgetFromURI(uri) as ProjectIDEWidget;
return widget;
};

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { type URI, useCurrentWidget } from '@coze-project-ide/client';
import { type ProjectIDEWidget } from '../widgets/project-ide-widget';
type ActivateCallback = (widget: ProjectIDEWidget) => void;
interface WidgetLocation {
uri: URI;
pathname: string;
params: { [key: string]: string | undefined };
}
const genLocationByURI = (uri: URI): WidgetLocation => ({
uri,
pathname: uri.path.toString(),
params: uri.queryObject,
});
const useCurrentWidgetActivate = (cb: ActivateCallback) => {
const currentWidget = useCurrentWidget() as ProjectIDEWidget;
useLayoutEffect(() => {
const dispose = currentWidget.onActivate(() => {
cb(currentWidget);
});
return () => dispose.dispose();
}, [currentWidget, cb]);
};
/**
* 获取当前 widget 的 location
*/
export const useIDELocation = () => {
const currentWidget = useCurrentWidget() as ProjectIDEWidget;
const [location, setLocation] = useState(
genLocationByURI(currentWidget.uri!),
);
const uriRef = useRef(currentWidget.uri?.toString());
const callback = useCallback<ActivateCallback>(
widget => {
if (uriRef.current !== widget.uri?.toString()) {
uriRef.current = widget.uri?.toString();
setLocation(genLocationByURI(widget.uri!));
}
},
[setLocation, uriRef],
);
useCurrentWidgetActivate(callback);
return location;
};
/**
* 获取当前 widget 的 query 参数
*/
export const useIDEParams = () => {
const currentWidget = useCurrentWidget() as ProjectIDEWidget;
const [params, setParams] = useState(currentWidget.uri?.queryObject || {});
const queryRef = useRef(currentWidget.uri?.query);
const callback = useCallback<ActivateCallback>(
widget => {
const query = widget.uri?.query;
if (queryRef.current !== query) {
queryRef.current = query;
setParams(widget.uri?.queryObject || {});
}
},
[queryRef, setParams],
);
useCurrentWidgetActivate(callback);
return params;
};

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useNavigate, type NavigateOptions } from 'react-router-dom';
import { useCallback } from 'react';
import { URI } from '@coze-project-ide/client';
import { addPreservedSearchParams } from '../utils';
import { URI_SCHEME, UI_BUILDER_URI } from '../constants';
import { useSpaceId } from './use-space-id';
import { useProjectIDEServices } from './use-project-ide-services';
import { useProjectId } from './use-project-id';
export const useIDENavigate = () => {
const { view } = useProjectIDEServices();
const spaceId = useSpaceId();
const projectId = useProjectId();
const navigate = useNavigate();
/**
* value(string): /:resourceType/:resourceId?a=a&b=b
*/
const IDENavigate = useCallback(
(value: string, options?: NavigateOptions) => {
const url = `/space/${spaceId}/project-ide/${projectId}${value}`;
const uri = new URI(`${URI_SCHEME}://${value}`);
const isUIBuilder = uri.displayName === UI_BUILDER_URI.displayName;
if (value && value !== '/' && !isUIBuilder) {
// 调用 openService
view.open(uri);
} else {
// 如果没有要打开的 widget就只打开主面板
view.openPanel(isUIBuilder ? 'ui-builder' : 'dev');
}
navigate(addPreservedSearchParams(url), options);
},
[spaceId, projectId, view, navigate],
);
return IDENavigate;
};

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type interfaces } from 'inversify';
import { useIDEContainer } from '@coze-project-ide/client';
/**
* 获取 IDE 的 IOC 模块
* 和 flow-ide/client 包内容相同,但可以支持在业务侧如 workflow 内调用
* @param identifier
*/
export function useIDEServiceInBiz<T>(
identifier: interfaces.ServiceIdentifier,
): T | undefined {
const container = useIDEContainer();
if (container.isBound(identifier)) {
return container.get(identifier) as T;
} else {
return undefined;
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useCallback, useEffect, useRef } from 'react';
import { useMemoizedFn } from 'ahooks';
import { URI, useIDEService } from '@coze-project-ide/client';
import { getURLByURI } from '../utils';
import { MessageEventService, type MessageEvent } from '../services';
import { URI_SCHEME } from '../constants';
import { useIDENavigate } from './use-ide-navigate';
export const useMessageEventService = () =>
useIDEService<MessageEventService>(MessageEventService);
/**
* 获取向 widget 发送信息函数的 hooks
*/
export const useSendMessageEvent = () => {
const messageEventService = useMessageEventService();
const navigate = useIDENavigate();
/**
* 向以 uri 为索引的 widget 发送信息
*/
const send = useCallback(
<T>(target: string | URI, data: MessageEvent<T>) => {
const uri =
typeof target === 'string'
? new URI(`${URI_SCHEME}://${target}`)
: target;
messageEventService.send(uri, data);
},
[messageEventService],
);
/**
* 向以 uri 为索引的 widget 发送信息,并且打开/激活此 widget
* 此函数比较常用
*/
const sendOpen = useCallback(
<T>(target: string | URI, data: MessageEvent<T>) => {
const uri =
typeof target === 'string'
? new URI(`${URI_SCHEME}://${target}`)
: target;
messageEventService.send(uri, data);
navigate(getURLByURI(uri));
},
[messageEventService, navigate],
);
return { send, sendOpen };
};
/**
* 监听向指定 uri 对应的唯一 widget 发送消息的 hook
* 监听消息的 widget 一定是知道 this.uri所以入参无须支持 string
* 注:虽然 widget.uri 的值是会变得,但其 withoutQuery().toString() 一定是不变的,所以 uri 可以认定为不变
*/
export const useListenMessageEvent = (
uri: URI,
cb: (e: MessageEvent) => void,
) => {
const messageEventService = useMessageEventService();
// 尽管 uri 对应的唯一 key 不会变化,但 uri 内存地址仍然会变化,这里显式的固化 uri 的不变性
const uriRef = useRef(uri);
// 保证 callback 函数的可变性
const listener = useMemoizedFn(() => {
const queue = messageEventService.on(uri);
queue.forEach(cb);
});
useEffect(() => {
// 组件挂在时去队列中取一次,有可能在组件未挂载前已经被发送了消息
listener();
const disposable = messageEventService.onSend(e => {
if (messageEventService.compare(e.uri, uriRef.current)) {
listener();
}
});
return () => disposable.dispose();
}, [messageEventService, listener, uriRef]);
};

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useIDEGlobalContext } from '../context';
export const useProjectId = () => {
const store = useIDEGlobalContext();
const projectId = store(state => state.projectId);
return projectId;
};

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useIDEService } from '@coze-project-ide/client';
import { ProjectIDEServices } from '../plugins/create-preset-plugin/project-ide-services';
export const useProjectIDEServices = (): ProjectIDEServices => {
const projectIDEServices =
useIDEService<ProjectIDEServices>(ProjectIDEServices);
return projectIDEServices;
};

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
useIDEService,
ShortcutsService,
CommandRegistry,
} from '@coze-project-ide/client';
export const useShortcuts = (commandId: string) => {
const commandRegistry = useIDEService<CommandRegistry>(CommandRegistry);
const shortcutsService = useIDEService<ShortcutsService>(ShortcutsService);
const shortcut = shortcutsService.getShortcutByCommandId(commandId);
const keybinding = shortcut.map(item => item.join(' ')).join('/');
const label = commandRegistry.getCommand(commandId)?.label;
return {
keybinding,
label,
};
};

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useIDEGlobalContext } from '../context';
export const useSpaceId = () => {
const store = useIDEGlobalContext();
const spaceId = store(state => state.spaceId);
return spaceId;
};

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, useState } from 'react';
import { useCurrentWidgetContext } from './use-current-widget-context';
export const useTitle = () => {
const currentWidgetContext = useCurrentWidgetContext();
const { widget } = currentWidgetContext;
const [title, setTitle] = useState(widget.getTitle());
useEffect(() => {
const disposable = widget.onTitleChanged(_title => {
setTitle(_title);
});
return () => {
disposable?.dispose?.();
};
}, []);
return title;
};

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ViewService } from '@/plugins/create-preset-plugin/view-service';
import { useProjectIDEServices } from './use-project-ide-services';
/**
* 获取 ProjectIDE 所有视图操作
*/
export const useViewService = (): ViewService => {
const projectIDEServices = useProjectIDEServices();
return projectIDEServices.view;
};

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useCallback, useEffect } from 'react';
import { useIDEService } from '@coze-project-ide/client';
import { type WsMessageProps } from '@/types';
import { WsService } from '@/services';
export const useWsListener = (listener: (props: WsMessageProps) => void) => {
const wsService = useIDEService<WsService>(WsService);
useEffect(() => {
const disposable = wsService.onMessageSend(listener);
return () => {
disposable.dispose();
};
}, []);
const send = useCallback(
data => {
wsService.send(data);
},
[wsService],
);
return {
send,
};
};