mirror of
https://github.com/MCSManager/MCSManager.git
synced 2024-11-27 06:59:54 +08:00
Feat: socket.io support
This commit is contained in:
parent
655c3c27ef
commit
fa5c5b8fd6
11
frontend/package-lock.json
generated
11
frontend/package-lock.json
generated
@ -14,6 +14,7 @@
|
||||
"axios": "^1.4.0",
|
||||
"crc": "^4.3.2",
|
||||
"dayjs": "^1.11.9",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"i18next-scanner": "^4.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "^7.0.3",
|
||||
@ -4123,6 +4124,11 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
@ -11848,6 +11854,11 @@
|
||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||
"dev": true
|
||||
},
|
||||
"eventemitter3": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
|
||||
},
|
||||
"events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
|
@ -20,6 +20,7 @@
|
||||
"axios": "^1.4.0",
|
||||
"crc": "^4.3.2",
|
||||
"dayjs": "^1.11.9",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"i18next-scanner": "^4.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "^7.0.3",
|
||||
|
86
frontend/src/hooks/useTerminal.ts
Normal file
86
frontend/src/hooks/useTerminal.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { setUpTerminalStreamChannel } from "@/services/apis/instance";
|
||||
import { parseForwardAddress } from "@/tools/protocol";
|
||||
import { onUnmounted, ref, unref } from "vue";
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { t } from "@/lang/i18n";
|
||||
import EventEmitter from "eventemitter3";
|
||||
import type { DefaultEventsMap } from "@socket.io/component-emitter";
|
||||
import type { InstanceDetail } from "@/types";
|
||||
|
||||
export interface UseTerminalParams {
|
||||
instanceId: string;
|
||||
daemonId: string;
|
||||
}
|
||||
|
||||
export function useTerminal() {
|
||||
const events = new EventEmitter();
|
||||
let socket: Socket<DefaultEventsMap, DefaultEventsMap>;
|
||||
const state = ref<InstanceDetail>();
|
||||
const isReady = ref<boolean>(false);
|
||||
|
||||
const execute = async (config: UseTerminalParams) => {
|
||||
isReady.value = false;
|
||||
const res = await setUpTerminalStreamChannel().execute({
|
||||
params: {
|
||||
remote_uuid: config.daemonId,
|
||||
uuid: config.instanceId
|
||||
}
|
||||
});
|
||||
const remoteInfo = unref(res.value);
|
||||
if (!remoteInfo) throw new Error(t("无法获取远程节点信息"));
|
||||
|
||||
const addr = parseForwardAddress(remoteInfo?.addr, "ws");
|
||||
const password = remoteInfo.password;
|
||||
|
||||
socket = io(addr, {});
|
||||
socket.on("connect", () => {
|
||||
socket.emit("stream/auth", {
|
||||
data: { password }
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("stream/auth", (packet) => {
|
||||
const data = packet.data;
|
||||
if (data === true) {
|
||||
socket.emit("stream/detail", {});
|
||||
events.emit("connect");
|
||||
isReady.value = true;
|
||||
} else {
|
||||
events.emit("error", new Error("Stream/auth error!"));
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("reconnect", () => {
|
||||
socket.emit("stream/auth", {
|
||||
data: { password }
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
events.emit("disconnect");
|
||||
socket.close();
|
||||
});
|
||||
|
||||
socket.on("instance/stdout", (packet) => events.emit("stdout", packet?.data));
|
||||
socket.on("stream/detail", (packet) => {
|
||||
const v = packet?.data as InstanceDetail | undefined;
|
||||
state.value = v;
|
||||
events.emit("detail", v);
|
||||
});
|
||||
|
||||
socket.connect();
|
||||
|
||||
return socket;
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
events.removeAllListeners();
|
||||
socket.close();
|
||||
});
|
||||
|
||||
return {
|
||||
execute,
|
||||
events,
|
||||
state
|
||||
};
|
||||
}
|
22
frontend/src/services/apis/instance.ts
Normal file
22
frontend/src/services/apis/instance.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { useDefineApi } from "@/stores/useDefineApi";
|
||||
|
||||
// 此处 API 接口可以用中文写注释,后期再统一翻译成英语。
|
||||
|
||||
export interface MissionPassportResponse {
|
||||
addr: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
// 请求建立终端 Socket 连接
|
||||
export const setUpTerminalStreamChannel = useDefineApi<
|
||||
{
|
||||
params: {
|
||||
remote_uuid: string;
|
||||
uuid: string;
|
||||
};
|
||||
},
|
||||
MissionPassportResponse
|
||||
>({
|
||||
url: "/api/protected_instance/stream_channel",
|
||||
method: "POST"
|
||||
});
|
85
frontend/src/tools/protocol.ts
Normal file
85
frontend/src/tools/protocol.ts
Normal file
@ -0,0 +1,85 @@
|
||||
export function parseForwardAddress(addr: string, require: "http" | "ws") {
|
||||
// save its protocol header
|
||||
//ws://127.0.0.1:25565
|
||||
let protocol = `${window.location.protocol}//`;
|
||||
const addrProtocolString = addr.toLocaleLowerCase();
|
||||
if (require === "http") {
|
||||
if (addrProtocolString.indexOf("ws://") === 0) protocol = "http://";
|
||||
else if (addrProtocolString.indexOf("wss://") === 0) protocol = "https://";
|
||||
else if (addrProtocolString.indexOf("http://") === 0) protocol = "http://";
|
||||
else if (addrProtocolString.indexOf("https://") === 0) protocol = "https://";
|
||||
else if (protocol === "https://") protocol = "https://";
|
||||
else protocol = "http://";
|
||||
}
|
||||
if (require === "ws") {
|
||||
if (addrProtocolString.indexOf("http://") === 0) protocol = "ws://";
|
||||
else if (addrProtocolString.indexOf("https://") === 0) protocol = "wss://";
|
||||
else if (addrProtocolString.indexOf("ws://") === 0) protocol = "ws://";
|
||||
else if (addrProtocolString.indexOf("wss://") === 0) protocol = "wss://";
|
||||
else if (protocol === "https://") protocol = "wss://";
|
||||
else protocol = "ws://";
|
||||
}
|
||||
|
||||
// remove potentially redundant headers
|
||||
addr = deleteWebsocketHeader(deleteHttpHeader(addr));
|
||||
|
||||
// port and ip are separated
|
||||
let daemonPort = null;
|
||||
let onlyAddr = null;
|
||||
if (addr.split(":").length === 2) {
|
||||
onlyAddr = addr.split(":")[0];
|
||||
daemonPort = parseInt(addr.split(":")[1]);
|
||||
if (isNaN(daemonPort))
|
||||
throw new Error(`The address ${addr} failed to resolve, the port is incorrect`);
|
||||
} else {
|
||||
onlyAddr = addr;
|
||||
}
|
||||
|
||||
// Reassemble the address based on the separated port and ip
|
||||
const checkAddr = onlyAddr.toLocaleLowerCase();
|
||||
if (checkAddr.indexOf("localhost") === 0 || checkAddr.indexOf("127.0.0.") === 0) {
|
||||
addr = `${protocol}${window.location.hostname}${daemonPort ? `:${daemonPort}` : ""}`;
|
||||
} else {
|
||||
addr = `${protocol}${onlyAddr}${daemonPort ? `:${daemonPort}` : ""}`;
|
||||
}
|
||||
return addr;
|
||||
}
|
||||
|
||||
// The ws address on the Daemon side is converted into an http address
|
||||
export function daemonWsAddressToHttp(wsAddr = "") {
|
||||
if (wsAddr.toLocaleLowerCase().indexOf("ws://") === 0) {
|
||||
return `http://${wsAddr.slice(5)}`;
|
||||
} else if (wsAddr.toLocaleLowerCase().indexOf("wss://") === 0) {
|
||||
return `https://${wsAddr.slice(6)}`;
|
||||
}
|
||||
return wsAddr;
|
||||
}
|
||||
|
||||
export function deleteWebsocketHeader(wsAddr: string) {
|
||||
if (wsAddr.toLocaleLowerCase().indexOf("ws://") === 0) {
|
||||
return `${wsAddr.slice(5)}`;
|
||||
} else if (wsAddr.toLocaleLowerCase().indexOf("wss://") === 0) {
|
||||
return `${wsAddr.slice(6)}`;
|
||||
}
|
||||
return wsAddr;
|
||||
}
|
||||
|
||||
export function deleteHttpHeader(addr: string) {
|
||||
if (addr.toLocaleLowerCase().indexOf("http://") === 0) {
|
||||
return `${addr.slice(7)}`;
|
||||
} else if (addr.toLocaleLowerCase().indexOf("https://") === 0) {
|
||||
return `${addr.slice(8)}`;
|
||||
}
|
||||
return addr;
|
||||
}
|
||||
|
||||
// The ws address on the Daemon side is converted to the local ws address
|
||||
export function daemonWsAddressToWs(wsAddr = "") {
|
||||
if (
|
||||
wsAddr.toLocaleLowerCase().indexOf("ws://") !== 0 &&
|
||||
wsAddr.toLocaleLowerCase().indexOf("wss://") !== 0
|
||||
) {
|
||||
return `ws://${wsAddr}`;
|
||||
}
|
||||
return wsAddr;
|
||||
}
|
@ -5,6 +5,9 @@ import type { LayoutCard } from "@/types";
|
||||
import { DownOutlined, PlaySquareOutlined } from "@ant-design/icons-vue";
|
||||
import { arrayFilter } from "../../tools/array";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useTerminal } from "../../hooks/useTerminal";
|
||||
import { onMounted } from "vue";
|
||||
import type { InstanceDetail } from "../../types/index";
|
||||
|
||||
const props = defineProps<{
|
||||
card: LayoutCard;
|
||||
@ -59,6 +62,19 @@ const instanceOperations = arrayFilter([
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
const { execute, events, state } = useTerminal();
|
||||
|
||||
// events.on("detail", (v: InstanceDetail) => {
|
||||
// console.debug("XZXZX:", v);
|
||||
// });
|
||||
|
||||
onMounted(async () => {
|
||||
await execute({
|
||||
instanceId,
|
||||
daemonId
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -94,6 +110,10 @@ const instanceOperations = arrayFilter([
|
||||
|
||||
<p>实例ID: {{ instanceId }}</p>
|
||||
<p>守护进程ID: {{ daemonId }}</p>
|
||||
|
||||
<p>
|
||||
{{ state }}
|
||||
</p>
|
||||
</template>
|
||||
</CardPanel>
|
||||
</template>
|
||||
|
@ -12,8 +12,13 @@ export default defineConfig({
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:23333",
|
||||
changeOrigin: true
|
||||
changeOrigin: true,
|
||||
ws: true
|
||||
// rewrite: (path) => path.replace(/^\/api/, "")
|
||||
},
|
||||
"/socket.io": {
|
||||
target: "ws://localhost:23333",
|
||||
ws: true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
180
package-lock.json
generated
Normal file
180
package-lock.json
generated
Normal file
@ -0,0 +1,180 @@
|
||||
{
|
||||
"name": "MCSManager",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"socket.io-client": "^4.7.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.2.tgz",
|
||||
"integrity": "sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.11.0",
|
||||
"xmlhttprequest-ssl": "~2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz",
|
||||
"integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/socket.io-client": {
|
||||
"version": "4.7.2",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz",
|
||||
"integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io-client": "~6.5.2",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.11.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
||||
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xmlhttprequest-ssl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
|
||||
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
},
|
||||
"engine.io-client": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.2.tgz",
|
||||
"integrity": "sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==",
|
||||
"requires": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.11.0",
|
||||
"xmlhttprequest-ssl": "~2.0.0"
|
||||
}
|
||||
},
|
||||
"engine.io-parser": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz",
|
||||
"integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"socket.io-client": {
|
||||
"version": "4.7.2",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz",
|
||||
"integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==",
|
||||
"requires": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io-client": "~6.5.2",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
}
|
||||
},
|
||||
"socket.io-parser": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||
"requires": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.11.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
||||
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
|
||||
"requires": {}
|
||||
},
|
||||
"xmlhttprequest-ssl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
|
||||
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A=="
|
||||
}
|
||||
}
|
||||
}
|
5
package.json
Normal file
5
package.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"socket.io-client": "^4.7.2"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user