Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/app/service/offscreen/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -42,7 +42,7 @@ export class VscodeConnectClient extends Client {
super(msgSender, "offscreen/vscodeConnect");
}

connect(params: Parameters<VSCodeConnect["connect"]>[0]): ReturnType<VSCodeConnect["connect"]> {
connect(params: VSCodeConnectParam): Promise<void> {
return this.do("connect", params);
}
}
4 changes: 2 additions & 2 deletions src/app/service/offscreen/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

变量命名不一致:在 index.ts 中使用了 camelCase 的 vsCodeConnect(小写 s),但在类型和其他地方都使用 VSCode(大写 VS)。为了保持一致性,建议使用 vscodeConnect(全小写)或 vsCodeConnect,但要在整个项目中保持统一。参考项目中其他地方的命名(如 VscodeConnectClient),应该使用 vscodeConnect

Copilot uses AI. Check for mistakes.
vsCodeConnect.init();

this.windowServer.on("createObjectURL", async (params: { blob: Blob; persistence: boolean }) => {
return makeBlobURL(params) as string;
Expand Down
199 changes: 123 additions & 76 deletions src/app/service/offscreen/vscode-connect.ts
Original file line number Diff line number Diff line change
@@ -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);
Comment on lines +31 to +33
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API 设计不一致:VSCodeConnect 类现在在构造函数中接收 Server 对象并自行创建 group,这与项目中其他 Service 的模式不一致。根据 Architecture Overview 中的描述,Service 应该在构造函数中接收 Group 对象作为依赖注入,而不是接收 Server 然后自己创建 Group。这种改变破坏了依赖注入的模式,降低了可测试性。应该保持原有的设计,在构造函数中接收 Group 对象。

Copilot uses AI. Check for mistakes.
}

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<void>((resolve, reject) => {
/* ---------- Public API ---------- */
/** 启动(或重新启动)与 VSCode 的连接 */
public connect({ url, reconnect }: VSCodeConnectParam): Promise<void> {
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);
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里使用了 setTimeout 100ms 延迟,但实际上应该立即调用 connectVSCode()。这个延迟会导致用户在首次连接时无故等待 100ms。如果是为了避免重连风暴,应该只在重连时添加延迟,而不是首次连接时。

Copilot uses AI. Check for mistakes.
};
Comment on lines +43 to +48
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doReconnect 函数的设计存在问题:它调用 setTimeout 设置延迟后执行 connectVSCode,但 setTimeout 返回的 timerId 被赋值给 this.timerId。然而在 clearTimer() 中使用的是 clearTimeout,这意味着如果在延迟期间调用 clearTimer(),会清除这个定时器。这个行为可能是期望的,但缺乏文档说明。更重要的是,如果 reconnect 为 false,doReconnect 根本不应该被调用,但代码中没有对此进行检查。

Copilot uses AI. Check for mistakes.
const connectVSCode = () => {
if (this.ws) return; // 已连接则忽略
Comment on lines +49 to +50
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

存在竞态条件:在 connectVSCode() 函数开始时检查 this.ws 是否存在,但在异步的 Promise 执行过程中,this.ws 可能在 open 事件中被设置。如果同时有多个连接尝试,可能会导致多个 WebSocket 实例同时存在。建议在函数开始时就设置一个连接中的标志位,或者使用互斥锁机制防止并发连接。

Copilot uses AI. Check for mistakes.
return new Promise<void>((resolve, reject) => {
let ws;
try {
ws = new WebSocket(url);
} catch (e: any) {
this.logger.debug("connect vscode faild", Logger.E(e));
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

注释中存在拼写错误:"faild" 应该是 "failed"。这个错误在多处出现(第 56、80 行),应该统一修正。

Copilot uses AI. Check for mistakes.
reject(e);
return;
}
});
let connectOK = false;
ws.addEventListener("open", () => {
ws.send('{"action":"hello"}');
connectOK = true;
// 如重复连接,则清除之前的
if (this.ws) {
this.closeExisting();
}
this.ws = ws;
Comment on lines +64 to +68
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这段逻辑存在问题:在 open 事件处理程序中检查 this.ws 是否已存在,如果存在就关闭旧连接。但此时新的 ws 对象应该就是要赋值给 this.ws 的对象,这个检查在逻辑上不合理。如果担心竞态条件,应该在 connectVSCode 函数开始时就进行检查和处理,而不是在 open 事件中。

Copilot uses AI. Check for mistakes.
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));
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

注释中存在拼写错误:"faild" 应该是 "failed"。

Copilot uses AI. Check for mistakes.
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();
Comment on lines +84 to +90
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

在多个地方(close 和 error 事件处理器)都调用了 doReconnect(),这会导致重复的重连尝试。WebSocket 在连接失败时可能同时触发 error 和 close 事件,这样会启动两个并发的重连定时器,造成资源浪费和潜在的竞态条件。建议添加一个状态标志来防止重复的重连尝试。

Copilot uses AI. Check for mistakes.
});
// 如 open, close, error 都不发生,30 秒后reject
this.clearTimer();
this.timerId = setTimeout(() => {
if (!connectOK) {
reject(new Error("Timeout"));
try {
ws.close();
} catch (e) {
console.error(e);
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

使用 console.error 不符合项目的日志规范。项目中已经有 this.logger 用于统一的日志记录,应该使用 this.logger.error() 替代 console.error(),以保持日志记录的一致性。

Copilot uses AI. Check for mistakes.
}
if (reconnect) doReconnect();
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

超时处理中也调用了 doReconnect(),这会与 error/close 事件中的重连逻辑冲突。如果连接超时,WebSocket 的 close 方法会触发 close 事件,该事件又会再次调用 doReconnect(),导致双重重连。应该通过状态管理来避免这种重复。

Copilot uses AI. Check for mistakes.
}
}, 30_000);
});
});
};
// 如果已经连接,断开重连
this.closeExisting();
// 清理老的定时器
this.clearTimer();
return Promise.resolve().then(() => connectVSCode());
Comment on lines +49 to +111
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

函数 connectVSCode 作为嵌套函数定义在 connect 方法内部,但没有返回 Promise。在第 50 行有 return 语句,但在第 111 行调用时使用了 Promise.resolve().then(() => connectVSCode()),这个调用会忽略 connectVSCode 的返回值。应该将 return 语句添加到 connectVSCode 函数声明中,使其显式返回 Promise<void>。

Copilot uses AI. Check for mistakes.
}

init() {
this.group.on("connect", this.connect.bind(this));
/* ---------- Message Handling ---------- */
private async handleMessage(ev: MessageEvent): Promise<void> {
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);
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

使用 console.error 不符合项目的日志规范。应该使用 this.logger.error() 来保持日志记录的一致性。

Copilot uses AI. Check for mistakes.
}
this.ws = undefined;
}
private clearTimer(): void {
if (this.timerId) {
clearTimeout(this.timerId);
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clearTimeout 应该改为 clearInterval。虽然当前代码中 timerId 是通过 setTimeout 设置的,但从语义上看,这个定时器在某些情况下可能需要支持周期性重连(从旧代码的 setInterval 可以看出)。使用 clearTimeout 可能无法正确清理所有类型的定时器。建议明确定时器的类型,或者同时调用 clearTimeout 和 clearInterval 以确保清理。

Copilot uses AI. Check for mistakes.
this.timerId = undefined;
}
}
}
4 changes: 2 additions & 2 deletions src/app/service/service_worker/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -400,7 +400,7 @@ export class SystemClient extends Client {
super(msgSender, "serviceWorker/system");
}

connectVSCode(params: Parameters<VSCodeConnect["connect"]>[0]): ReturnType<VSCodeConnect["connect"]> {
connectVSCode(params: VSCodeConnectParam): ReturnType<VSCodeConnect["connect"]> {
return this.do("connectVSCode", params);
}
}
Loading