Skip to content

✨ 外部接入:以独立实例身份连接 sctl,被拒绝时停止重连并说明原因,支持命名与改名 - #1821

Merged
CodFrm merged 7 commits into
mainfrom
feature/sctl-instance-identity
Nov 3, 2026
Merged

CodFrm merged 7 commits into
mainfrom
feature/sctl-instance-identity

Conversation

@CodFrm

@CodFrm CodFrm commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Checklist / 检查清单

  • Fixes mentioned issues / 修复已提及的问题
  • Code reviewed by human / 代码通过人工检查
  • Changes tested / 已完成测试

背景

scriptscat/sctl#16(已合并)让 sctl daemon 同时接入多个 ScriptCat 实例:按实例 ID 区分、按名称或 ID 前缀指定、合并 scripts.list。ScriptCat 之前不声明身份,只能作为唯一的“老版实例”连接,第二个 ScriptCat 会顶掉第一个(scriptscat/sctl#14 的根因之一)。另外 ScriptCat 被 sctl 拒绝时会无限重连:名称冲突发生在认证成功之后,按现有逻辑会每 2 秒重试一次;被 sctl scriptcats forget 的实例会每 30 秒白连一次,页面只显示“连接失败”。

本次改动

  • 实例身份:每份安装有稳定的 32 位十六进制实例 ID 和名称,只存本地(system_external_access_identity,Web Lock 保证并发首次读取只生成一份)。默认名称为 <chrome|edge|firefox|safari|scriptcat>-<ID 前 4 位>。“停止外部接入”只清密钥,ID 与名称保留,重新接入仍是同一实例。删除没有读取方的 clientId。
  • 握手:认证响应带 peer: {kind: "scriptcat", instanceId},HMAC 使用 scriptcat* 上下文并绑定实例 ID(与 sctl Go 实现生成的固定向量对拍);能力声明带 peer: {name, product, productVersion, extensionVersion},从浏览器 UA 读取的版本最多 64 字节。从 1.5 升级的已接入用户无需操作,sctl 会接管原密钥。
  • 被拒绝:新增状态 rejected,原因 refused(认证完成前被 1008 关闭)或 name-taken(会话能力声明收到 CONFLICT)。进入后停止自动重连;能力声明被接受前不重置退避。关开开关、重新接入或修改名称会离开 rejected。
  • 命名:enroll(code, name) / rename(name)。接入时名称冲突不保存密钥,并提示需要重新运行 sctl connect 获取新配对码(sctl 在认证时即消费配对码);已连接时改名冲突恢复原名并保持连接;未连接时改名在下次连接时生效。本地拦下保留名 scriptcat。配对中途 SW 重启不会丢弃新密钥。
  • 外部接入页:已接入区域显示实例名称与行内修改;接入对话框增加名称输入;rejected 两种原因的说明与对应操作(重新接入 / 修改名称)。新文案覆盖 10 种语言。
  • 接入时名称冲突恢复原名:冲突后恢复接入前的实例名称,下次接入不会预填一个已知被占用的名称。
  • 手机宽度布局:配对码格子在 768px 以下缩小,不再把对话框撑出屏幕;状态徽标不再被挤成竖排。
  • 测试替身 tests/mockWebLocks.ts(happy-dom 没有 Web Locks);event_page_manager.test.ts 在重置模块后重建 logger 单例(否则后续文件在共享模块环境里初始化 Server 失败)。

实现考虑

已知限制

建议审查重点

  • external-access-connect.ts 中 1008 判定与重连退避的时机(能力声明被接受前不重置)。
  • controller.ts 中配对密钥延后到能力声明被接受才保存、SW 重启后的状态恢复。
  • 实例身份生成的并发与持久化(config.ts)。

关联

验证

base acec65cb(main)…head bc7620db;git diff origin/main...HEAD 涉及 26 个文件:src/app/service/offscreen/、src/app/service/service_worker/(含 external_access/)、src/pkg/config/、src/pages/options/routes/Tools/sections/ExternalAccessSection*、10 个 src/locales/*/external_access.json、src/pages/components/ui/pairing-code-input.tsx、tests/mockWebLocks.ts。

  • pnpm exec tsc --noEmit 通过;pnpm lint 通过。
  • CI=1 pnpm test:ci(head f7ba8feb):377 个文件中 376 通过,1 个用例 850ms 超时(Agent/Provider/index.test.tsx,本分支未改动,本机负载高),单独重跑 7/7 通过。此前在 638a90b4 上 377/377、5100 个测试全部通过。本机全量结果受负载影响,以 CI 为准。
  • head bc7620db(新增两个提交):CI=1 pnpm test:ci 377 个文件中 376 通过,1 个用例 850ms 超时(src/pages/import/components.test.tsx,本分支未改动),单独重跑 22/22 通过;pnpm lint 通过。名称恢复先写失败测试(冲突后名称仍为 work),修复后通过;手机布局在构建产物中实测:390px 下对话框 scrollWidth = clientWidth(388),配对码格 32px,桌面仍为 40px;徽标高度 22px 单行。
  • 运行验证(构建产物在一次性 Chromium 中,对接 sctl main 2939485 的独立 daemon;三个独立配置代表三份安装):
    • 接入:默认名 chrome-6f6c,sctl 列出实例 ID、Chrome 156、扩展版本。
    • 接入时名称冲突:对话框保持、提示需新配对码、配对码清空、未保存密钥、sctl 无新登记;改名并用新码后接入成功。
    • 两份安装同时在线 20 秒无新握手;sctl get 合并并标注来源,--scriptcat 分别指定。
    • 写操作审批只在收到请求的实例弹出,允许后只改动该实例的脚本。
    • 已连接改名成功 / 冲突(恢复原名、保持连接);未连接改名提示下次生效。
    • sctl scriptcats forget 后进入 rejected(refused),40 秒内无重连;重新接入后以同一实例 ID 回到在线。
    • 离线改成被占用的名称后重连:进入 rejected(name-taken),20 秒内无重试;通知里“修改名称”改名后立即连上。
    • 模拟 1.5 已接入状态(1.5 格式配对数据 + 对应 pairing.key):打开开关后以实例身份在线,sctl 删除 pairing.key。
    • 停止外部接入后实例 ID 与名称保留、密钥清除。
    • 亮色、暗色与 390px 宽度下的名称行、编辑态与 rejected 通知可用,无横向溢出。

Screenshots / 截图

构建产物在一次性 Chromium 中、连接 sctl main 独立 daemon 时截取(亮色为主,另附暗色与 390px 手机宽度)。

接入对话框:新增实例名称 接入时名称被占用
01-enroll-dialog 02-enroll-name-taken
已接入,显示实例名称 改名:名称不合规
03-connected-name 04-rename-invalid
已连接时改名被占用(保持原名与连接) 被拒绝:名称被占用
05-rename-taken 06-rejected-name-taken
被拒绝:已被 sctl 忘记或版本过旧 同上(暗色)
07-rejected-refused 08-rejected-refused-dark
手机宽度:接入对话框 手机宽度:已接入
09-enroll-dialog-mobile 10-connected-mobile

getBrowserVersion() 只认 UA 中的 Chrome/ 标记,Firefox 与 Safari 上 productVersion 总被省略;改为按产品读取 Firefox/ 与 Version/ 标记,Chromium 系不变。
顺带修正随本功能过期的注释与测试标题(clientId、paired、接入提示),并去掉 en-US 文案中会原样显示的反引号。
- onEstablished 改以 disabled 状态而非内存里的 active 判断是否接收:SW 在配对握手期间重启后,daemon 已登记的新密钥仍会落盘并转为已连接;stop() 之后迟到的通知仍被忽略
- 实例名称规则收敛为 config 中唯一的 isValidExternalAccessName,页面与控制器共用,并拒绝 sctl 保留给旧版 ScriptCat 的 scriptcat(否则接入会在认证后被拒、白白消耗一次性配对码),各语言提示同步说明
- 接入请求本身失败(doThrow 拒绝)时页面提示失败,不再产生未处理的 Promise 拒绝
- 能力声明只对来自 UA、可被改写的浏览器版本截断到 64 字节;名称与扩展版本均为受控输入,删去不可达的 UTF-8 截断
- Web Locks 测试替身从生产目录移到 tests/mockWebLocks.ts;连接测试改经 Group 下发能力声明、以 hello 转发观察握手完成、以重连后的会话模式握手验证密钥切换,不再读私有字段
@CodFrm
CodFrm merged commit 1e5d41d into main Nov 3, 2026
11 checks passed
@CodFrm
CodFrm deleted the feature/sctl-instance-identity branch November 3, 2026 09:02
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.

1 participant