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:
@@ -169,6 +169,22 @@ export interface TabDescriptor {
|
||||
* When omitted, a default `{ id, type, title }` tab is created.
|
||||
*/
|
||||
createTab?: (state: SidebarState) => { tab: SidebarTab; patch?: Partial<SidebarState> } | null
|
||||
/**
|
||||
* External-link target claim (v0.13.0+): when a GUI external-link click
|
||||
* is taken over (the `browserInterceptLinks` master AND the URL's
|
||||
* protocol flag — `browserInterceptHttp` / `browserInterceptHttps` —
|
||||
* are on), the first registered tab whose `urlTarget(url)` returns true
|
||||
* is opened with `openTab({ type, url, title: hostname })` — the URL is
|
||||
* the whole payload (the tab reads it from `tab.path`). Registration
|
||||
* order wins (first claim first served); a disabled tab type is skipped;
|
||||
* a throwing predicate is swallowed (console.error, the type is skipped).
|
||||
* The built-in browser tab declares NO urlTarget — it stays the implicit
|
||||
* fallback target, so plugins can never be shadowed by it. To host more
|
||||
* than one URL at a time, mint per-URL ids through `createTab` (the
|
||||
* browser builtin's pattern); otherwise the id safety net focuses the
|
||||
* existing tab of the same type and the new URL is not applied.
|
||||
*/
|
||||
urlTarget?: (url: URL) => boolean
|
||||
/**
|
||||
* Declarative settings shown in the Side card settings page: every
|
||||
* registered tab gets an enable/disable switch (icon + title + id), and
|
||||
@@ -373,6 +389,33 @@ function baseNameOf(path: string): string {
|
||||
return at === -1 ? path : path.slice(at + 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the tab type that claims an intercepted external-link URL (v0.13.0+).
|
||||
* Walks the descriptors in REGISTRATION order and returns the first one
|
||||
* that declares `urlTarget` and matches `url`; a throwing predicate is
|
||||
* swallowed (console.error, type skipped) so one broken plugin can never
|
||||
* break the whole link pipeline. The caller passes the ENABLED tab
|
||||
* descriptors (enablement is the caller's prefs domain — filter
|
||||
* `service.getTabs()` through `tabsEnabled` before matching) and falls
|
||||
* back to the built-in browser tab when nothing claims the URL (the
|
||||
* browser never declares `urlTarget` itself, so it can never shadow a
|
||||
* plugin claim).
|
||||
*/
|
||||
export function matchUrlTarget(tabs: readonly TabDescriptor[], url: URL): TabDescriptor | undefined {
|
||||
for (const tab of tabs) {
|
||||
if (tab.urlTarget === undefined) continue
|
||||
let claimed = false
|
||||
try {
|
||||
claimed = tab.urlTarget(url) === true
|
||||
} catch (error) {
|
||||
console.error('[dsh-better-sidebar] urlTarget error:', error)
|
||||
continue
|
||||
}
|
||||
if (claimed) return tab
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* The plugin version this service instance reports. Keep in lockstep with
|
||||
* `package.json`'s version — `tests/service.spec.ts` asserts the pair.
|
||||
@@ -390,6 +433,7 @@ export const SIDEBAR_SERVICE_VERSION = '0.12.1'
|
||||
* - 'stateSubscription': getSnapshot/subscribeState
|
||||
* - 'tabMeta': SidebarTab.meta (seeds, createTab, updateTab, persistence)
|
||||
* - 'pluginSettings': SidebarSettingsDeclaration.pluginToggles/render
|
||||
* - 'urlTarget' (v0.13.0): TabDescriptor.urlTarget (external-link claims)
|
||||
*/
|
||||
export const SIDEBAR_FEATURES = [
|
||||
'badge',
|
||||
@@ -400,6 +444,7 @@ export const SIDEBAR_FEATURES = [
|
||||
'stateSubscription',
|
||||
'tabMeta',
|
||||
'pluginSettings',
|
||||
'urlTarget',
|
||||
] as const
|
||||
|
||||
/** Run one plugin callback; a throw is logged and never breaks the caller. */
|
||||
|
||||
Reference in New Issue
Block a user