Skip to content

[v1.4] VSCodeConnect 代码重构#1170

Open
cyfung1031 wants to merge 7 commits intoscriptscat:release/v1.3from
cyfung1031:pr-vsconnect-002
Open

[v1.4] VSCodeConnect 代码重构#1170
cyfung1031 wants to merge 7 commits intoscriptscat:release/v1.3from
cyfung1031:pr-vsconnect-002

Conversation

@cyfung1031
Copy link
Collaborator

@cyfung1031 cyfung1031 commented Jan 31, 2026

[v1.3/1.4] VSCodeConnect 重构:提升 WebSocket 连接稳定性与可维护性

本次 PR 对 src/app/service/offscreen/vscode-connect.ts 进行重构,目标是改善旧版在连接管理、重连逻辑与异常处理上的不稳定与难维护问题,使 VS Code 热重载流程在长时间运行与异常场景下表现更可靠。

新旧对比一览

项目 旧版(修改前) 新版(修改后) 改进效果
连接管理方式 直接使用原生 WebSocket,事件与状态分散在 connect / connectVSCode 集中管理 WebSocket 生命周期,统一在单一类中处理连接、事件与清理 状态更集中,逻辑更容易理解与维护
连接超时保护 无连接超时,WebSocket 可能长期停留在 CONNECTING 明确 30 秒连接超时(CONNECT_TIMEOUT),超时后自动 cleanup 避免无期限等待,提高异常场景下的可靠性
自动重连机制 使用 setInterval 每 30 秒重试,逻辑简单粗放 使用 setTimeout + 延迟重连,记录 lastParams 并支援指数退避 减少无意义重试,重连行为更可控
旧事件残留 / race condition 多次 connect 时,旧 socket 的 close / error 仍可能影响新连接 引入 epoch 机制,所有回调需验证当前连接代数 避免旧事件干扰新连接,行为更可预测
资源清理 仅在部分流程中 close socket / clearInterval,事件回调未移除 统一 cleanup(),清除 socket、timer,并移除所有 WebSocket 回调 降低记忆体泄漏与僵尸连线风险
稳定脚本 ID 生成 直接使用 uuidv5(data.data.uri),未验证 payload 完整性 明确验证 script / uri 存在后才生成 stableId,并记录成功日志 避免异常 payload 导致安装错误
讯息处理健壮性 直接 JSON.parse,异常或未知 action 未处理 try-catch 包装解析流程,未知 action 与异常皆有日志 降低 crash 风险,除错更容易
日志详细度 仅少量 debug 日志 增加 debug / info / warn / error,多记录 epoch、url、uri 等关键信息 问题定位更直观
核心功能保留 hello 握手 + onchange 安装脚本 完整保留原有行为,仅重构流程与安全性 无功能变更,仅稳定性与可维护性提升

总结:旧版痛点 vs 新版价值

旧版主要问题

  • WebSocket 状态与事件分散,流程复杂且容易互相干扰
  • 缺乏连接超时与完善清理,异常情况下容易残留连接
  • 重连机制粗糙,长时间运行时行为难以预测

新版带来的好处

  • 连接、超时、重连、清理逻辑集中,状态更清楚
  • 能正确处理 VS Code 重启、连线失败、网路中断等情况
  • 日志更完整,未来除错与扩充成本更低

影响范围:仅限 VSCode 开发模式连接逻辑,不影响一般 ScriptCat 使用者。

测试建议

  • 正常热重载:VS Code 储存 .user.js → ScriptCat 是否即时更新
  • 断线重连:关闭 VS Code → 重新开启 → 是否能自动重新连接
  • 快速连线测试:多次触发「连接开发模式」→ 是否无异常错误或重复行为
  • 超时测试:使用无效 URL → 30 秒后是否正确 timeout 并进入重连流程

@cyfung1031 cyfung1031 force-pushed the pr-vsconnect-002 branch 6 times, most recently from c83a2d9 to a6c016e Compare January 31, 2026 13:43
VSCodeConnect 代码重构
@CodFrm CodFrm changed the title [v1.3/1.4] VSCodeConnect 代码重构 [v1.4] VSCodeConnect 代码重构 Feb 2, 2026
@CodFrm
Copy link
Member

CodFrm commented Feb 2, 2026

能放1.4就1.4了,现在两个版本内容挺多了,时间也挺久了

@cyfung1031
Copy link
Collaborator Author

AI 再重构

以下是对这两个版本 VSCodeConnect 类的比较分析:

项目 v0 (原版) v1 (重构版) 评价 / 谁更好
epoch 机制实现 每次 connect 递增 epoch,旧回调被忽略 每次 startSession 递增 epoch,同样用来废弃旧回调 平手(逻辑等价)
重连逻辑的触发控制 较松散,onerror / onclose 都会 queueReconnect 使用 isReconnecting 锁 + 更明确的条件判断 v1 明显更好
重连定时器重复注册防护 仅检查 reconnectTimerId 是否存在 结合 isReconnecting 锁 + timer 存在检查 v1 更安全
connect() 函数的职责 包含建立 ws + 绑定事件 + 超时逻辑 只负责建立 ws + 绑定事件,超时独立看门狗 v1 职责更单一
超时清理位置 集中在 openSocket 的 finish() 内 分散在 handleOpen / handleClose 内 v0 更集中,v1 更分散
dispose / cleanup 彻底性 清理 timer + 移除事件 + close ws 同左,但命名为 dispose 更符合「销毁会话」语义 v1 语义更清晰
reconnectDelay 重置时机 只在连接成功时重置 收到 connect 请求时就重置(更积极) v1 更合理
程式码结构与可读性 一个大 connect 方法 + 较长的 Promise 拆分成 startSession / connect / 多个 handler v1 明显更好
事件处理函数的 epoch 检查 大多数有检查,但 onerror/onclose 内逻辑重复 每个 handler 开头统一检查 epoch v1 更一致
防范「同时多个重连定时器」 仅靠 reconnectTimerId != null 防护 双重防护(锁 + timer存在) v1 更稳健
错误处理的细腻度 较粗糙,部分错误直接 reject 更统一走 handleError → scheduleReconnect v1 更好
整体维护性 / 未来扩展性 中等 较高(职责拆分清晰、状态管理更明确) v1 胜出
引入的新 bug 风险 增加 isReconnecting 锁,可能误锁或漏解锁 v1 略高(但目前看正确)
程式码行数 较少 略多(因为拆分更多小函数) v0 更精简

总结评价

v1(重构版)整体明显优于 v0,尤其在以下几个关键点:

  1. 状态管理更安全:引入连接锁(isReconnecting) + epoch 双重防护,大幅降低「多个重连定时器同时触发」或「旧会话干扰新会话」的风险。
  2. 职责分离更好:把「建立连接」与「超时管理」「重连调度」进一步拆分,单个函数更短、更专注。
  3. 可读性与可维护性提升明显:事件处理统一在开头做 epoch 过滤,逻辑路径更线性。
  4. 语义更清晰:dispose()、startSession() 等命名比 cleanup()、connect() 更能表达意图。

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

本次 PR 聚焦于重构 Offscreen 环境下的 VSCodeConnect 连接管理(VS Code 扩展 scriptcat-vscode ↔ ScriptCat),目标是提升 WebSocket 连接生命周期管理、重连稳定性与可维护性,服务于开发模式下的热重载/即时安装流程。

Changes:

  • 将 VSCodeConnect 由“分散的 WebSocket + interval 重试”重构为集中式状态机:epoch、超时、重连退避、统一清理等。
  • 抽出并复用 VSCodeConnectParam 类型,更新 service worker / offscreen 客户端调用签名。
  • 增强日志与消息解析健壮性(try-catch、unknown action 日志等)。

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
src/app/service/service_worker/client.ts SystemClient.connectVSCode 改为使用 VSCodeConnectParam 类型,简化/固定调用签名。
src/app/service/offscreen/vscode-connect.ts VSCodeConnect 核心重构:引入 epoch、connect timeout、重连退避、统一 dispose 与更健壮的消息处理。
src/app/service/offscreen/client.ts VscodeConnectClient.connect 改为使用 VSCodeConnectParam 类型,与新的连接管理器对齐。

Comment on lines +220 to +236
private connect(sessionEpoch: number): void {
const url = this.currentParams?.url;
if (!url) return;

try {
this.logger.debug(`Attempting connection (Epoch: ${sessionEpoch})`, { url });
this.isReconnecting = false; // 开始新连接时重置锁
this.ws = new WebSocket(url);

// 设置连接超时看门狗
this.connectTimeoutTimer = setTimeout(() => {
if (sessionEpoch === this.epoch) {
this.logger.warn("Connection timeout");
this.ws?.close();
}
}, CONFIG.CONNECT_TIMEOUT);

Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

connect() 每次都会新建 WebSocket 并覆盖 this.ws,但没有在重连场景下先清理旧的 connectTimeoutTimer/旧 ws。若 scheduleReconnect 在同一 epoch 内多次触发(例如 onerror 后又触发 timeout/watchdog),旧的 timeout 回调仍会运行并对“新的”this.ws 执行 close,造成新连接被旧定时器误杀。建议在 connect() 开始处先 clear 现有 connectTimeoutTimer,并对当前 this.ws 做一次统一的 close+解绑(或抽成 cleanupSocket() 在每次重连前调用)。

Copilot uses AI. Check for mistakes.
Comment on lines +310 to +315
this.logger.error("WebSocket error", {
event: ev instanceof Event ? ev.type : undefined,
error: ev instanceof Error ? ev.message : String(ev),
});
// 发生错误时立即尝试介入重连,无需等待 onclose 事件。
// 内部锁会拦截后续 handleClose 发起的重复请求。
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

handleError() 里直接调用 scheduleReconnect(),但没有清理当前连接尝试对应的 connectTimeoutTimer。在“error 但未 close”的情况下,timeout 仍可能在稍后触发并关闭(或干扰)后续重连创建的新 ws。建议在进入重连前同步 clear connectTimeoutTimer,并视情况主动 close 当前 ws(再由 onclose 触发后续流程),以避免定时器/连接状态残留。

Suggested change
this.logger.error("WebSocket error", {
event: ev instanceof Event ? ev.type : undefined,
error: ev instanceof Error ? ev.message : String(ev),
});
// 发生错误时立即尝试介入重连,无需等待 onclose 事件。
// 内部锁会拦截后续 handleClose 发起的重复请求。
this.logger.error("WebSocket error", {
event: ev instanceof Event ? ev.type : undefined,
error: ev instanceof Error ? ev.message : String(ev),
});
// 出现 error 时,当前连接对应的超时计时器已经失去意义,需要立即清理,
// 否则旧定时器可能在后续重连成功后才触发,误操作新的连接。
if (this.connectTimeoutTimer) {
clearTimeout(this.connectTimeoutTimer);
this.connectTimeoutTimer = null;
}
// 优先尝试关闭当前 ws,由 onclose → handleClose 统一收敛清理与重连逻辑。
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
try {
this.ws.close();
// 后续重连由 handleClose 触发,无需在此重复 scheduleReconnect。
return;
} catch (e) {
this.logger.warn("Failed to close WebSocket after error", Logger.E(e));
}
}
// 若当前不存在可关闭的 ws(或关闭失败),直接介入重连作为保底。

Copilot uses AI. Check for mistakes.
Comment on lines +199 to +213
this.startSession(params);
});
}

/**
* 启动一个新的连接会话
* 每次调用都会递增 epoch,自动使旧的连接和定时器失效
*/
private startSession(params: VSCodeConnectParam): void {
this.dispose(); // 彻底清理旧资源
this.currentParams = params;

// 开启新一轮连接
this.epoch++;
this.connect(this.epoch);
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

目前 messageGroup.on("connect", ...) 的处理函数没有返回 Promise(直接 startSession 后返回 void)。在 message server 中这会立刻回传 {code:0,data:undefined},导致 SystemClient.connectVSCode() / Tools 页面会“立即显示连接成功”,即使 WebSocket 还没 open、甚至随后失败也不会 reject。建议让 connect handler 返回一个会话级 Promise:在 handleOpen resolve,在 handleError/timeout reject(并在 epoch 变更时取消/忽略旧 Promise)。

Suggested change
this.startSession(params);
});
}
/**
* 启动一个新的连接会话
* 每次调用都会递增 epoch,自动使旧的连接和定时器失效
*/
private startSession(params: VSCodeConnectParam): void {
this.dispose(); // 彻底清理旧资源
this.currentParams = params;
// 开启新一轮连接
this.epoch++;
this.connect(this.epoch);
// 返回会话级 Promise,让调用方能够感知连接是否成功建立
return this.startSession(params);
});
}
/**
* 启动一个新的连接会话
* 每次调用都会递增 epoch,自动使旧的连接和定时器失效
*
* 返回值为当前会话级的 Promise:
* - 预计在 WebSocket open/hello 完成时 resolve
* - 在连接出错或超时时 reject
*/
private startSession(params: VSCodeConnectParam): Promise<void> {
this.dispose(); // 彻底清理旧资源
this.currentParams = params;
// 开启新一轮连接
this.epoch++;
return this.connect(this.epoch);

Copilot uses AI. Check for mistakes.
Comment on lines +349 to +356
private scheduleReconnect(): void {
if (this.isReconnecting) return;
// 如果不允许重连,或者已经在重连中,或者 Socket 还是开启状态,则跳过
if (!this.currentParams?.reconnect || this.reconnectTimer) return;
const sessionEpoch = this.epoch; // 锁定当前的 epoch
this.isReconnecting = true; // 上锁
this.logger.debug(`Scheduling reconnect in ${this.reconnectDelay}ms`);

Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

scheduleReconnect() 的注释提到“Socket 还是开启状态则跳过”,但实际未检查 this.ws.readyState。更关键的是:handleError() 可能在 socket 仍处于 OPEN/CONNECTING 时触发,此时会直接 schedule reconnect,后续 connect() 会覆盖 this.ws 而不关闭旧连接,旧连接回调仍会以相同 epoch 生效,可能引发并发双连接/重复安装等竞态。建议在 scheduleReconnect 前先关闭并清理现有 ws(或仅在 readyState 为 CLOSED/CLOSING 时才重连),并确保旧回调失效。

Copilot uses AI. Check for mistakes.
}

try {
const stableId = uuidv5(uri, CONFIG.NAMESPACE);
Copy link
Member

Choose a reason for hiding this comment

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

直接用 uuidv5.URL 就行,没必要再来一次常量

Comment on lines +135 to +143
*/

/**
* VSCode ↔ ScriptCat 连接管理器
*
* ⚠️ 维护者注意:
* 本类是一个「强状态 + 并发敏感」的 WebSocket 管理器。
* 修改 epoch / timeout / cleanup 逻辑前,请完整理解事件顺序。
*/
Copy link
Member

Choose a reason for hiding this comment

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

AI写的注释?感觉有点又臭又长,最好精简一下

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

不写这堆的话不知道这个 vscode-connect.ts 是在干嘛

Copy link
Member

Choose a reason for hiding this comment

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

但是很多废话


public init(): void {
this.messageGroup.on("connect", (params: VSCodeConnectParam) => {
// this.logger.info("Received connect request", params);
Copy link
Member

Choose a reason for hiding this comment

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

可以删除这个注释

private scheduleReconnect(): void {
if (this.isReconnecting) return;
// 如果不允许重连,或者已经在重连中,或者 Socket 还是开启状态,则跳过
if (!this.currentParams?.reconnect || this.reconnectTimer) return;
Copy link
Member

Choose a reason for hiding this comment

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

可以删掉this.isReconnecting,用reconnectTimer来判断,两个有点重复冗余

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants