feat: 服务化 API 新增 urlTarget 外链认领 + 浏览器 HTTP/HTTPS 二级设置开关

基于 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(外链分流)
This commit is contained in:
Menghuan1918
2026-08-15 15:05:06 +08:00
parent f842fa0029
commit 29f162dac0
18 changed files with 425 additions and 49 deletions
@@ -0,0 +1,89 @@
# 外链点击路由到插件 tab(urlTarget)+ 浏览器 HTTP/HTTPS 二级设置
**日期**:2026-08-15
**状态**:已实施(本文档含实施偏差记录)
**目标版本**:v0.13.0(API 兼容层,版本号未随本变更 bump,见 §7)
## 1. 目标
基于 [PR #74](https://github.com/omdsh-dev/DSH-better-sidebar/pull/74)(聊天区外链开关颗粒度讨论)向前兼容地拓展服务化 API:
1. **插件声明「点击打开的类型」**:`TabDescriptor` 新增可选 `urlTarget?: (url: URL) => boolean`——聊天区外链被拦截时,路由到第一个声明的、命中的、未被设置禁用的插件 tab 类型(`openTab({ type, url, title: hostname })`,URL 即全部载荷),内置浏览器 tab 退居隐式兜底。
2. **浏览器链接设置改为二级菜单**(浏览器卡片齿轮弹窗内):
- 侧边打开 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']`。
- `openTab` seed 已有 `url` 字段(新建 tab 的 path 预填),插件 tab 无需新渲染管道。
## 4. 设计
### 4.1 Prefs(`prefs-shared.ts` / `config.ts` / `client/prefs.ts`)
```ts
interface SidebarPrefs {
// …
browserInterceptLinks: boolean // 总闸,默认 true(保留,零迁移)
browserInterceptHttp: boolean // HTTP 外链侧边栏接管,默认 true
browserInterceptHttps: boolean // HTTPS 外链侧边栏接管,默认 false
}
```
三处同步维护(类型 + schemastery 默认 + parsePrefs 校验),旧文档无需迁移:总闸保留使显式关闭过的用户升级后仍不拦截。
### 4.2 服务 API(`service.ts`)
```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.tsx` gate:总闸 `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(常规区移动 + 其测试改动)被本方案取代,未合入。