基于 PR #74 的颗粒度讨论向前兼容拓展服务化 API: - TabDescriptor 新增可选 urlTarget(url) 谓词:聊天/界面外链被拦截时按注册 顺序路由到第一个命中的插件 tab 类型(openTab({ type, url, title })), 内置 browser 退居隐式兜底;SIDEBAR_FEATURES 追加 'urlTarget' - 浏览器链接设置改为二级菜单(齿轮弹窗):新增 browserInterceptHttp (默认开)+ browserInterceptHttps(默认关);browserInterceptLinks 保留为总闸(默认开,旧文档零迁移) - 拦截管线:registerLinkInterception 的 takeoverEnabled 改收解析后的 URL,协议/同源纯判定(shouldInterceptLink)签名不变 - 测试:matchUrlTarget(注册顺序/抛错吞掉/browser 永不命中)、jsdom 点击接管门控、parsePrefs 新字段默认值;全默认字面量同步 - 文档:设计文档 + AGENTS.md(字段/示例/设置键)+ README(外链分流)
5.8 KiB
5.8 KiB
外链点击路由到插件 tab(urlTarget)+ 浏览器 HTTP/HTTPS 二级设置
日期:2026-08-15 状态:已实施(本文档含实施偏差记录) 目标版本:v0.13.0(API 兼容层,版本号未随本变更 bump,见 §7)
1. 目标
基于 PR #74(聊天区外链开关颗粒度讨论)向前兼容地拓展服务化 API:
- 插件声明「点击打开的类型」:
TabDescriptor新增可选urlTarget?: (url: URL) => boolean——聊天区外链被拦截时,路由到第一个声明的、命中的、未被设置禁用的插件 tab 类型(openTab({ type, url, title: hostname }),URL 即全部载荷),内置浏览器 tab 退居隐式兜底。 - 浏览器链接设置改为二级菜单(浏览器卡片齿轮弹窗内):
- 侧边打开 HTTP 网页(默认开)
- 侧边打开 HTTPS 网页(默认关)
browserInterceptLinks保留为总闸(默认开,零迁移:旧文档显式 false 语义完整保留)。
2. 非目标
- 不实施 PR #74 的「外链开关提升到常规区」(用户确认:二级设置留在浏览器卡片齿轮弹窗,常规区不加行)。
- 不做 per-host / per-domain 路由规则(只有 per-protocol 开关 + 插件谓词)。
- 不改 + 菜单、不改手动打开的浏览器 tab、不改同源/修饰键点击放行规则。
- 不改 host 半逻辑(仅 PrefsSchema 加字段);不修改 DSH 源码。
- 不做版本号 bump(发布时决定;向前兼容机制 =
SIDEBAR_FEATURES追加'urlTarget')。
3. 现状回顾
link-intercept.ts的registerLinkInterception无参takeoverEnabled()只做布尔门;index.tsx硬编码openTab({ type: 'browser', url, title: hostname })。browserInterceptLinks(默认 true)与browserNoSandbox并列于浏览器卡片的settings.toggles;拦截 gating = 该 pref &&tabsEnabled['browser']。openTabseed 已有url字段(新建 tab 的 path 预填),插件 tab 无需新渲染管道。
4. 设计
4.1 Prefs(prefs-shared.ts / config.ts / client/prefs.ts)
interface SidebarPrefs {
// …
browserInterceptLinks: boolean // 总闸,默认 true(保留,零迁移)
browserInterceptHttp: boolean // HTTP 外链侧边栏接管,默认 true
browserInterceptHttps: boolean // HTTPS 外链侧边栏接管,默认 false
}
三处同步维护(类型 + schemastery 默认 + parsePrefs 校验),旧文档无需迁移:总闸保留使显式关闭过的用户升级后仍不拦截。
4.2 服务 API(service.ts)
interface TabDescriptor {
// …
urlTarget?: (url: URL) => boolean
}
/** 注册顺序先到先得;谓词抛错吞掉(console.error)继续;返回 undefined 由调用方回退 browser。 */
export function matchUrlTarget(tabs: readonly TabDescriptor[], url: URL): TabDescriptor | undefined
- 匹配函数为模块级纯函数(不进
BetterSidebarService接口,KISS:不扩公开面);启用态过滤由调用方(index.tsx)按tabsEnabled过滤后传入。 - 内置 browser 不声明
urlTarget,永远不可能遮蔽插件声明;插件多 URL 并存需用createTab铸造 per-URL id(browser 同款模式),否则 id 安全网聚焦既有 tab 不覆写 path。 SIDEBAR_FEATURES追加'urlTarget'(消费者按能力 gate)。
4.3 拦截管线(link-intercept.ts / index.tsx)
shouldInterceptLink签名不变(协议/同源纯判定);takeoverEnabled改为(url: URL) => boolean,onClick 顺序:纯判定 → gate(持 URL)→ preventDefault → open。index.tsxgate:总闸browserInterceptLinks→ 协议开关(https 读browserInterceptHttps,否则读browserInterceptHttp)→ 目标可开(插件命中即目标,否则 browser 的tabsEnabled)。open 时再次解析目标并openTab({ type, url, title: hostname })。
4.4 设置 UI(builtins/tabs.tsx / locales.ts)
浏览器卡片 settings.toggles 收敛为四行:browserNoSandbox / browserInterceptLinks(总闸)/ browserInterceptHttp / browserInterceptHttps;新增 zh/en 文案(HTTP 默认开、HTTPS 默认关及理由)。
5. 行为变化
- 默认设置下:HTTP 外链 → 侧边栏(不变);HTTPS 外链 → 系统浏览器新窗口(变化,符合「默认关闭」)。
- 声明
urlTarget的插件 tab 认领被拦截的链接(优先于浏览器兜底);插件被禁用/谓词抛错时跳过。 - 总闸关闭 → 一律不拦截(旧文档语义保留)。
6. 测试
tests/builtins.spec.ts:浏览器 toggles 断言收敛为四行 + 各行 title/desc 存在。tests/service.spec.ts:matchUrlTarget(无声明 → undefined / 注册顺序 / 抛错吞掉 / browser 永不命中)。tests/link-intercept.spec.ts(新增 jsdom):takeoverEnabled(url)收到解析后的 URL、gate 决定是否接管、同源/非 http(s) 不 consult gate、修饰键绕过。tests/unit.spec.ts:parsePrefs 新字段默认值(http true / https false / 非法回退);总闸与协议开关相互独立。tests/plugin-shape.spec.ts/tests/smoke.spec.ts:全默认字面量补两个字段。
7. 实施偏差记录
- 计划曾考虑把
matchUrlTarget作为BetterSidebarService接口方法(镜像matchFileViewer);按 KISS 收敛为模块级导出纯函数,启用态过滤由调用方完成(匹配函数不感知 prefs),公开 API 面只剩urlTarget字段 +'urlTarget'feature 字符串。 - 版本号未 bump(保持 0.12.1):
SIDEBAR_SERVICE_VERSION与package.json的配对断言继续成立;'urlTarget'feature 已入SIDEBAR_FEATURES,发布时再决定版本。 - PR #74 的 diff(常规区移动 + 其测试改动)被本方案取代,未合入。