diff --git a/src/app/service/offscreen/client.ts b/src/app/service/offscreen/client.ts index a79421c08..918fe9818 100644 --- a/src/app/service/offscreen/client.ts +++ b/src/app/service/offscreen/client.ts @@ -2,7 +2,7 @@ import { type WindowMessage } from "@Packages/message/window_message"; import type { SCRIPT_RUN_STATUS, ScriptRunResource } from "@App/app/repo/scripts"; import { Client, sendMessage } from "@Packages/message/client"; import type { MessageSend } from "@Packages/message/types"; -import { type VSCodeConnect } from "./vscode-connect"; +import type { VSCodeConnectParam } from "./vscode-connect"; export function preparationSandbox(windowMessage: WindowMessage) { return sendMessage(windowMessage, "offscreen/preparationSandbox"); @@ -42,7 +42,7 @@ export class VscodeConnectClient extends Client { super(msgSender, "offscreen/vscodeConnect"); } - connect(params: Parameters[0]): ReturnType { + connect(params: VSCodeConnectParam): Promise { return this.do("connect", params); } } diff --git a/src/app/service/offscreen/index.ts b/src/app/service/offscreen/index.ts index 50c4d8c4d..0bf7d39ab 100644 --- a/src/app/service/offscreen/index.ts +++ b/src/app/service/offscreen/index.ts @@ -59,8 +59,8 @@ export class OffscreenManager { const gmApi = new GMApi(this.windowServer.group("gmApi")); gmApi.init(); - const vscodeConnect = new VSCodeConnect(this.windowServer.group("vscodeConnect"), this.extMsgSender); - vscodeConnect.init(); + const vsCodeConnect = new VSCodeConnect(this.windowServer, this.extMsgSender); + vsCodeConnect.init(); this.windowServer.on("createObjectURL", async (params: { blob: Blob; persistence: boolean }) => { return makeBlobURL(params) as string; diff --git a/src/app/service/offscreen/vscode-connect.ts b/src/app/service/offscreen/vscode-connect.ts index dcb3a31b9..a2080ffe4 100644 --- a/src/app/service/offscreen/vscode-connect.ts +++ b/src/app/service/offscreen/vscode-connect.ts @@ -1,103 +1,150 @@ import LoggerCore from "@App/app/logger/core"; import Logger from "@App/app/logger/logger"; -import { type Group } from "@Packages/message/server"; +import type { Server, Group } from "@Packages/message/server"; import type { MessageSend } from "@Packages/message/types"; import { ScriptClient } from "../service_worker/client"; import { v5 as uuidv5 } from "uuid"; +/* ---------- Types ---------- */ +export type VSCodeConnectParam = { url: string; reconnect: boolean }; + +/** Actions received from VSCode WebSocket */ +enum VSCodeAction { + Hello = "hello", + OnChange = "onchange", +} + +/* ---------- Main Class ---------- */ // 在offscreen下与scriptcat-vscode建立websocket连接 // 需要在vscode中安装scriptcat-vscode插件 export class VSCodeConnect { - logger: Logger = LoggerCore.logger().with({ service: "VSCodeConnect" }); + private readonly logger: Logger = LoggerCore.logger().with({ service: "VSCodeConnect" }); - reconnect: boolean = false; + private ws: WebSocket | undefined; - wsConnect: WebSocket | undefined; + private timerId: number | NodeJS.Timeout | undefined; - connectVSCodeTimer: any; + private readonly scriptClient: ScriptClient; - scriptClient: ScriptClient; + private readonly vscodeConnectGroup: Group; - constructor( - private group: Group, - private msgSender: MessageSend - ) { - this.scriptClient = new ScriptClient(this.msgSender); + constructor(windowServer: Server, msgSender: MessageSend) { + this.vscodeConnectGroup = windowServer.group("vscodeConnect"); + this.scriptClient = new ScriptClient(msgSender); } - connect({ url, reconnect }: { url: string; reconnect: boolean }) { - // 如果已经连接,断开重连 - if (this.wsConnect) { - this.wsConnect.close(); - } - // 清理老的定时器 - if (this.connectVSCodeTimer) { - clearInterval(this.connectVSCodeTimer); - this.connectVSCodeTimer = undefined; - } - const handler = () => { - if (!this.wsConnect) { - return this.connectVSCode({ url }); - } - return Promise.resolve(); - }; - if (reconnect) { - this.connectVSCodeTimer = setInterval(() => { - handler(); - }, 30 * 1000); - } - return handler(); + init() { + this.vscodeConnectGroup.on("connect", (param: VSCodeConnectParam) => this.connect(param)); } - // 连接到vscode - connectVSCode({ url }: { url: string }) { - return new Promise((resolve, reject) => { + /* ---------- Public API ---------- */ + /** 启动(或重新启动)与 VSCode 的连接 */ + public connect({ url, reconnect }: VSCodeConnectParam): Promise { + const doReconnect = () => { // 如果已经连接,断开重连 - if (this.wsConnect) { - this.wsConnect.close(); - } - try { - this.wsConnect = new WebSocket(url); - } catch (e: any) { - this.logger.debug("connect vscode faild", Logger.E(e)); - reject(e); - return; - } - let ok = false; - this.wsConnect.addEventListener("open", () => { - this.wsConnect!.send('{"action":"hello"}'); - ok = true; - resolve(); - }); - this.wsConnect.addEventListener("message", async (ev) => { - const data = JSON.parse(ev.data); - switch (data.action) { - case "onchange": { - // 调用安装脚本接口 - const code = data.data.script; - this.scriptClient.installByCode(uuidv5(data.data.uri, uuidv5.URL), code, "vscode"); - break; - } - default: + this.closeExisting(); + this.clearTimer(); + this.timerId = setTimeout(connectVSCode, 100); + }; + const connectVSCode = () => { + if (this.ws) return; // 已连接则忽略 + return new Promise((resolve, reject) => { + let ws; + try { + ws = new WebSocket(url); + } catch (e: any) { + this.logger.debug("connect vscode faild", Logger.E(e)); + reject(e); + return; } - }); + let connectOK = false; + ws.addEventListener("open", () => { + ws.send('{"action":"hello"}'); + connectOK = true; + // 如重复连接,则清除之前的 + if (this.ws) { + this.closeExisting(); + } + this.ws = ws; + resolve(); + this.clearTimer(); + }); + ws.addEventListener("message", (ev) => { + this.handleMessage(ev).catch((err) => { + this.logger.error("message handler error", Logger.E(err)); + }); + }); - this.wsConnect.addEventListener("error", (e) => { - this.wsConnect = undefined; - this.logger.debug("connect vscode faild", Logger.E(e)); - if (!ok) { - reject(new Error("connect fail")); - } - }); + ws.addEventListener("error", (e) => { + this.ws = undefined; + this.logger.debug("connect vscode faild", Logger.E(e)); + if (!connectOK) { + reject(new Error("connect fail")); + } + if (reconnect) doReconnect(); + }); - this.wsConnect.addEventListener("close", () => { - this.wsConnect = undefined; - this.logger.debug("vscode connection closed"); + ws.addEventListener("close", () => { + this.ws = undefined; + this.logger.debug("vscode connection closed"); + if (reconnect) doReconnect(); + }); + // 如 open, close, error 都不发生,30 秒后reject + this.clearTimer(); + this.timerId = setTimeout(() => { + if (!connectOK) { + reject(new Error("Timeout")); + try { + ws.close(); + } catch (e) { + console.error(e); + } + if (reconnect) doReconnect(); + } + }, 30_000); }); - }); + }; + // 如果已经连接,断开重连 + this.closeExisting(); + // 清理老的定时器 + this.clearTimer(); + return Promise.resolve().then(() => connectVSCode()); } - init() { - this.group.on("connect", this.connect.bind(this)); + /* ---------- Message Handling ---------- */ + private async handleMessage(ev: MessageEvent): Promise { + let data: any; + try { + data = JSON.parse(ev.data as string); + } catch { + return; // ignore malformed JSON + } + switch (data.action as VSCodeAction) { + case VSCodeAction.OnChange: { + // 调用安装脚本接口 + const { script, uri } = data.data; + const id = uuidv5(uri, uuidv5.URL); + await this.scriptClient.installByCode(id, script, "vscode"); + break; + } + default: + // ignore unknown actions + } + } + + /* ---------- Helpers ---------- */ + private closeExisting(): void { + try { + this.ws?.close(); + } catch (e: any) { + console.error(e); + } + this.ws = undefined; + } + private clearTimer(): void { + if (this.timerId) { + clearTimeout(this.timerId); + this.timerId = undefined; + } } } diff --git a/src/app/service/service_worker/client.ts b/src/app/service/service_worker/client.ts index da64bf805..cbf71fd19 100644 --- a/src/app/service/service_worker/client.ts +++ b/src/app/service/service_worker/client.ts @@ -12,7 +12,7 @@ import { v4 as uuidv4 } from "uuid"; import { cacheInstance } from "@App/app/cache"; import { CACHE_KEY_IMPORT_FILE } from "@App/app/cache_key"; import { type ResourceBackup } from "@App/pkg/backup/struct"; -import { type VSCodeConnect } from "../offscreen/vscode-connect"; +import type { VSCodeConnectParam, VSCodeConnect } from "../offscreen/vscode-connect"; import { type ScriptInfo } from "@App/pkg/utils/scriptInstall"; import type { ScriptService, TCheckScriptUpdateOption, TOpenBatchUpdatePageOption } from "./script"; import { encodeRValue, type TKeyValuePair } from "@App/pkg/utils/message_value"; @@ -400,7 +400,7 @@ export class SystemClient extends Client { super(msgSender, "serviceWorker/system"); } - connectVSCode(params: Parameters[0]): ReturnType { + connectVSCode(params: VSCodeConnectParam): ReturnType { return this.do("connectVSCode", params); } }