From 80ea2ea37bd33b97ff58d0602685245a625a88cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Mon, 2 Nov 2026 15:02:00 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20=E5=A4=96=E9=83=A8=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=EF=BC=9A=E9=9A=90=E8=BA=AB=EF=BC=88split=EF=BC=89?= =?UTF-8?q?=E8=BF=9B=E7=A8=8B=E4=B8=8D=E5=86=8D=E8=BF=9E=E6=8E=A5=20sctl?= =?UTF-8?q?=EF=BC=8C=E9=81=BF=E5=85=8D=E4=B8=8E=E6=99=AE=E9=80=9A=E8=BF=9B?= =?UTF-8?q?=E7=A8=8B=E6=AF=8F=202=20=E7=A7=92=E4=BA=92=E7=9B=B8=E9=A1=B6?= =?UTF-8?q?=E6=9B=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit split 模式下隐身进程与普通进程共用 chrome.storage.local 里的配对密钥和脚本, 两者都连 sctl daemon 时,daemon 只保留一条 ScriptCat 连接,被顶掉的一方 2 秒后重连 又顶掉另一方,日志和审计缓冲区被 handshake.ok 刷满(scriptscat/sctl#14)。 隐身进程不再启动连接,也不接受配对。 --- .../external_access/controller.test.ts | 28 +++++++++++++++++++ .../external_access/controller.ts | 5 ++++ 2 files changed, 33 insertions(+) diff --git a/src/app/service/service_worker/external_access/controller.test.ts b/src/app/service/service_worker/external_access/controller.test.ts index 267cdaba6..ca22731ad 100644 --- a/src/app/service/service_worker/external_access/controller.test.ts +++ b/src/app/service/service_worker/external_access/controller.test.ts @@ -92,6 +92,34 @@ describe("ExternalAccessController(外部接入 · 扁平信任)", () => { expect(connectClient.connect).toHaveBeenCalledWith({ url: DEFAULT_URL, auth: { mode: "session", key: "abc123" } }); }); + describe("隐身(split)进程", () => { + // split 模式下隐身进程与普通进程共用 chrome.storage.local 里的密钥和脚本,它再连一次只会与普通进程互相顶替(sctl#14)。 + const extension = chrome.extension as unknown as { inIncognitoContext: boolean }; + + beforeEach(() => { + extension.inIncognitoContext = true; + }); + + afterEach(() => { + extension.inIncognitoContext = false; + }); + + it("已启用且已接入时启动也不拨号,状态保持 disabled", async () => { + systemConfig.setExternalAccessEnabled(true); + const controller = await initEnrolled("abc123", "cid"); + expect(connectClient.connect).not.toHaveBeenCalled(); + expect(controller.getStatus().status).toBe("disabled"); + }); + + it("enroll(code) 不以配对模式拨号", async () => { + const controller = makeController(); + await controller.initialize(); + await controller.enroll("PAIR-CODE"); + expect(connectClient.connect).not.toHaveBeenCalled(); + expect(controller.getStatus().status).toBe("disabled"); + }); + }); + it("enroll(code) 以配对模式拨号", async () => { const controller = makeController(); await controller.initialize(); diff --git a/src/app/service/service_worker/external_access/controller.ts b/src/app/service/service_worker/external_access/controller.ts index 96661ff2a..250afc9a0 100644 --- a/src/app/service/service_worker/external_access/controller.ts +++ b/src/app/service/service_worker/external_access/controller.ts @@ -55,6 +55,10 @@ export class ExternalAccessController { this.group.on("paired", (payload: { key: string }) => this.onEnrolled(payload.key)); this.group.on("disconnected", () => this.onDisconnected()); + // split 模式下隐身进程与普通进程共用 chrome.storage.local 里的密钥和脚本,由普通进程代表这份安装连接 sctl; + // 隐身进程再连只会与普通进程互相顶替(sctl#14)。 + if (chrome.extension.inIncognitoContext) return; + this.systemConfig.addListener("external_access_enabled", (enabled) => { if (enabled) { void this.connect(); @@ -90,6 +94,7 @@ export class ExternalAccessController { // pairing mode with C; on success the daemon ships a fresh long-term key K, which offscreen // decrypts and relays back via `onEnrolled`. async enroll(code: string): Promise { + if (chrome.extension.inIncognitoContext) return; this.active = true; this.setStatus("connecting"); const url = await this.systemConfig.getExternalAccessUrl();