🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

标签页自动分组 DDDMUC/dsh-tab-groups

DeepSeek Harness
DDDMUC26 天前有提交已提供中文文档扫描:含敏感能力

把浏览器里所有 DSH 标签页自动收进同一分组

30/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/8 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add DDDMUC/dsh-tab-groups
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · browser
装得上吗
本站已在 dsh 0.2.1-rc.5 下真实安装成功
非静态推断
L5 · 运行时冒烟等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 26 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

读写/删除本地文件写入剪贴板(不计风险)
查看扫描证据(共 12 处,展示前 12 条)
  • 读取本地文件
    lib/index.js:61
    const manifest = JSON.parse(readFileSync(join(root, 'manifest.json'), 'utf8'))
  • 读取本地文件
    lib/index.js:77
    if (!existsSync(path)) return
  • 删除本地文件递归
    lib/index.js:79
    rmSync(path, { recursive: true, force: true, maxRetries: 2 })
  • 读取本地文件
    lib/index.js:87
    if (!existsSync(from)) return false
  • 写入本地文件
    lib/index.js:89
    mkdirSync(dirname(to), { recursive: true })
  • 读取本地文件
    lib/index.js:114
    if (!force && existsSync(targetDir)) {
  • 读取本地文件
    lib/index.js:116
    const stamp = readFileSync(join(targetDir, STAMP_NAME), 'utf8').trim()
  • 读取本地文件
    lib/index.js:117
    if (stamp === version && existsSync(join(targetDir, 'manifest.json'))) {
  • 写入本地文件
    lib/index.js:125
    mkdirSync(targetDir, { recursive: true })
  • 写入本地文件
    lib/index.js:130
    writeFileSync(join(targetDir, STAMP_NAME), `${version}\n`)
  • 写入剪贴板
    lib/client.js:527
    await navigator.clipboard.writeText(INSTALL_DIR)
  • 写入剪贴板
    lib/client.js:787
    await navigator.clipboard.writeText(INSTALL_DIR)
扫描于 2026/11/3(1 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 自动归组DSH标签页
  • 映射固定路径
  • GUI内识别驱动

README

由 DeepSeek 最新模型翻译生成
dsh-tab-groups 把浏览器里所有指向 DeepSeek Harness(DSH)Web GUI 的标签页自动收进同一个标签组。 你在 DSH 里同时开着好几个会话——侧栏每一行本身就是链接,中键一点就是一个新标签页——标签栏很快就乱了。 这个扩展会在 DSH 标签页出现的瞬间把它们归进一个名叫 DSH 的组,新开一个自动进去一个,全部关掉后空组被浏览器自动清掉。本仓库是一个包、两张脸:浏览器扩展那一半做全部归组动作, DSH 插件那一半负责把扩展镜像到固定路径、在 GUI 里检测它、引导安装并直接驱动它。 中文 · English ▲ DSH GUI 里的插件界面:默认收起成一颗状态点,点一下展开成面板——开关、组名、颜色、实时计数、立即归组 为什么需要它 - 会话一多,标签栏就废了。 dsh-session-url 让侧栏每一行都是真链接(中键 / Cmd+点击直接开新标签),于是"一个会话一个标签页"成了日常,而浏览器原生的标签组要一个个手动拖。 - 这件事 DSH 插件自己做不到。 浏览器半区插件是 GUI 页面里的普通网页 JS,整棵 SDK 里没有任何浏览器扩展 API(chrome.tabs / chrome.tabGroups / chrome.runtime 都不存在),宿主插件跑在 Node 里更碰不到浏览器。
展开完整文档(README 剩余 1 节)
dsh-tab-groups

把浏览器里所有指向 DeepSeek Harness(DSH)Web GUI 的标签页自动收进同一个标签组。

你在 DSH 里同时开着好几个会话——侧栏每一行本身就是链接,中键一点就是一个新标签页——标签栏很快就乱了。
这个扩展会在 DSH 标签页出现的瞬间把它们归进一个名叫 DSH 的组,新开一个自动进去一个,全部关掉后空组被浏览器自动清掉。本仓库是一个包、两张脸:浏览器扩展那一半做全部归组动作, DSH 插件那一半负责把扩展镜像到固定路径、在 GUI 里检测它、引导安装并直接驱动它。

中文 · English

▲ DSH GUI 里的插件界面:默认收起成一颗状态点,点一下展开成面板——开关、组名、颜色、实时计数、立即归组

为什么需要它

- 会话一多,标签栏就废了。 dsh-session-url 让侧栏每一行都是真链接(中键 / Cmd+点击直接开新标签),于是"一个会话一个标签页"成了日常,而浏览器原生的标签组要一个个手动拖。
- 这件事 DSH 插件自己做不到。 浏览器半区插件是 GUI 页面里的普通网页 JS,整棵 SDK 里没有任何浏览器扩展 API(chrome.tabs / chrome.tabGroups / chrome.runtime 都不存在),宿主插件跑在 Node 里更碰不到浏览器。标签栏只对浏览器扩展开放,所以"自动归组"这件事唯一的实现形态就是扩展。
- 可扩展又不该多装一个东西。 所以本仓库把两张脸放进同一个目录:装 DSH 插件时,扩展那半边也被镜像到一个固定路径,并立刻在 GUI 里告诉你该怎么加载它、装没装好。

特性

- 打开即归组:DSH 标签页一出现就进组,后开的自动跟上,全关后空组被浏览器自动清掉。
- 可选的专属窗口:打开后,所有 DSH 标签页会被收进同一个窗口,且那个窗口里只有 DSH 标签页;你自己的普通标签页一个都不会被搬走。
- 识别靠标题锚点 DeepSeek Harness:一条规则同时覆盖随机 --port、localhost vs 127.0.0.1、局域网地址、以及 cloudflared 隧道域名(端口规则永远追不上后者)。前缀匹配刻意收紧,另一个恰好叫 DeepSeek Harness Docs 的站点不会被误伤。
- 一个包两张脸:同一目录既是浏览器扩展(manifest.json + src/),又是 DSH 插件(lib/ + cordis.patch.yml);浏览器忽略插件文件,DSH 忽略扩展文件。
- 扩展 ID 钉死:manifest 里钉了公钥 key,ID 恒为 pnncehmieeobfabnbdepbldiknndjbhl,目录挪到哪都不变——这是两张脸能互相寻址的前提。
- 座位就在 DSH 里,两处:设置页多了个一级分区「DSH 标签页」;同时本插件在「插件」分区里的那一页 也多出一块同样的设置区(就在包描述与「包含的组件」之间)。两处都写同一份配置。
- 版本号不再含糊:卡片上写的是 插件 v0.5.0 · 扩展 v0.5.0 · 协议 v1——左边是本插件的版本,右边是你浏览器里实际加载的那个扩展的版本。只写一个版本号时它必然被误读成插件版本(我自己就这么误读过)。
- 外加一颗自愈的状态点:右下角芯片默认收起成一颗状态点(几乎不占地方),点开是同一套控制;扩展没装时它自己展开成三步引导。「隐藏」只在本次页面加载内有效,刷新即回来。
之所以两处都有:设置分区在最要紧的那个状态下恰好没用——扩展根本没装时,你不会想到去设置里找它,而芯片就飘在你眼前,还能直接把安装引导摊开。
- 纯本地:三个权限(tabs / tabGroups / storage)、没有任何 host 权限、不注入内容脚本、不发网络请求、不采集数据。
- 零依赖零构建:扩展直接加载即用,插件直接挂载即用。

截图

▲ DSH 设置里的「DSH 标签页」一级分区:开关、组名、组颜色、实时计数与「立即归组」都在这里

▲ 另一处:DSH「插件」分区里本插件自己的页面——设置区落在包描述与「包含的组件」之间(后者永远不可点)

▲ 左:DSH 里的状态点芯片。中:扩展弹窗(开关 / 统计 / 立即归组)。右:扩展自己的设置页(底部实时列出按当前规则命中哪些标签页)

浏览器自己的标签栏属于浏览器内核 UI,任何页面级 API 都截不到,所以这里没有「标签栏截图」——与其画一张示意图,不如不放。

安装

分两步。第二步只有一次点击,且只需做一次。

第一步:装 DSH 插件

cd <你的 dsh-tab-groups 目录>
dsh plugin --profile web add link:$PWD

宿主半区随后会把扩展镜像到 $DSH_HOME/dsh-tab-groups-extension(默认 ~/.dsh/dsh-tab-groups-extension),并在日志里打印这个路径。刷新 GUI 后右下角出现「DSH 分组」芯片。

从 npm 装也一样(dsh plugin --profile web add dsh-tab-groups),镜像路径固定,不依赖包安装在哪。

第二步:把镜像目录加载进浏览器

1. 地址栏打开 edge://extensions(Chrome 是 chrome://extensions),打开左下角开发人员模式
2. 点加载解压缩的扩展,选择 ~/.dsh/dsh-tab-groups-extension (Finder 里按 Cmd+Shift+G 可以粘贴这个路径)
3. 回到 DSH 页面,芯片会在几秒内自己变绿(也可以点「重新检测」)

一行命令直接打开那个页面:

open -a "Microsoft Edge" "edge://extensions/"

只想用扩展

不要 DSH 插件也行:直接对本仓库目录做「加载解压缩的扩展」即可,插件那半边不影响扩展的任何行为。

使用

- 什么都不用做:打开 DSH 标签页它就会进组。
- 弹窗(点浏览器工具栏的扩展图标):看统计、一键「立即归组」、进设置页。
- 设置页:组名、组颜色、是否自动折叠、识别规则(标题 / 来源)、是否从别的分组拉过来、是否跳过固定标签页、以及窗口模式;底部有实时预览。
- 专属窗口:DSH 设置 → 「DSH 标签页」、DSH 芯片面板、或扩展设置页的「窗口」卡片——三处写的是同一份配置。默认关闭。
- DSH 设置 → 「DSH 标签页」:常用项(启用、专属窗口、组名、组颜色、计数、立即归组)。
- DSH →「插件」→ 点 dsh-tab-groups 那一行:同一块设置区出现在该插件自己的页面上。
注意那页底部的「包含的组件」是状态清单、永远不可点(它的交互只有右侧的启用开关)——设置不在那一行里,而在它上面。
- 扩展自己的设置页:识别规则、是否从别的分组拉过来、是否跳过固定标签页等全部选项。
- DSH 里的芯片:默认一颗状态点(绿=已连接),点开即面板;「隐藏」可整个收掉(刷新恢复)。
- 从 DSH 页面驱动扩展(高级):页面侧协议为 ping / status / reconcile / set-config,详见工作原理。

工作原理

1. 怎么判断「一个标签页是 DSH」

识别锚点是网页标题 DeepSeek Harness。三个证据,都是查证的:

- @deepseek-ai/dsh-web-frontend/dist/index.html 里 写死为 DeepSeek Harness;
- 整个前端 bundle 里没有任何 document.title 赋值,运行期不会变;
- 宿主 webserver 只向 index.html 注入 window.__DSH_BOOT__,不重写 (dsh-host-webserver/lib/index.js 里连 title 这个词都不出现)。

前缀匹配只在「后续第一个字符不是字母数字」时才算命中,所以 DeepSeek Harness · 会话标题、DeepSeek Harness — 登录 命中,DeepSeek Harness Docs 不命中。
另有按 origin 的规则(默认 http://127.0.0.1:3080、http://localhost:3080),负责「标题还没加载出来的那一瞬间」就立刻归组。

2. 一个包,两张脸

| 脸 | 跑在哪 | 负责 |
| 浏览器扩展 manifest.json + src/ + icons/ | Edge / Chrome 等 Chromium 浏览器 | 全部标签组行为 |
| DSH 插件 lib/ + cordis.patch.yml | DSH 宿主 + Web GUI 页面 | 镜像扩展、检测它、引导安装、驱动它 |
宿主半区(lib/index.js)在插件激活时,把扩展那半边复制到 $DSH_HOME/dsh-tab-groups-extension:幂等(带版本戳,同版本不重复复制)、窄删除(只替换已知的那几个条目,绝不动目录里别的东西)。
浏览器半区(lib/client.js)是一颗自带的 shadow-DOM 芯片,不依赖任何官方 slot 或服务形状——因为「扩展没装」这个状态正是它存在的理由,它必须在那时也能工作。

3. 两张脸怎么互相找到

扩展 manifest 里声明了 externally_connectable:

"matches": ["http://127.0.0.1/", "http://localhost/", "https://.dsh-market.com/"]

Chromium 只向匹配的页面注入 chrome.runtime。于是:

- 页面看得到 chrome.runtime.sendMessage → 扩展在;
- 看不到 → 扩展没装或没启用,芯片显示引导。

不需要探测扩展 URL、不需要改 manifest、不需要额外权限。协议如下(src/protocol.js,纯逻辑、有单测):

| 消息 | 返回 |
| {type:'ping'} | {ok, installed, protocol, id, version} |
| {type:'status'} | 开关、组名、颜色、命中数、待归组数、窗口数、上次运行结果 |
| {type:'reconcile'} | 立即归组,返回 {moved, groups, matched, errors} |
| {type:'set-config', config} | 逐键白名单校验后写入并立即重新归组 |

它故意什么都不暴露:状态里只有计数,没有任何标签页地址、标题或 ID。

4. 专属窗口模式(默认关闭)

打开后,所有 DSH 标签页会被收进同一个窗口,而这个窗口里只有 DSH 标签页。规则:

- 只认领「整窗都已经是 DSH 标签页」的窗口当归属。你的窗口里只要还有普通标签页,它就永远不会被选中——把你自己开的标签页搬走,比这件事本身要粗暴得多。没有这样的窗口时,就新建一个。
- 新建窗口时用 tabId 播种(把第一个 DSH 标签页直接作为新窗口的初始标签页),而不是裸建一个空窗口:裸建会留下一个多余的 about:blank 标签页(实测),端到端里有专门断言盯着「前后 about:blank 数量不变」。
- 固定标签页不参与。实测:chrome.tabs.move 跨窗口移动固定标签页会成功但静默取消固定——悄悄丢掉用户的固定比不搬更糟,所以固定标签页原地不动。
- 原窗口被搬空后由浏览器自动关闭(实测),不会留下空壳窗口;扩展也会把记住的窗口 ID 一起丢掉,下次重新挑或重建。
- 不改焦点:新建窗口默认 focused: false,你在别的窗口干活时不会被弹走(想抢焦点可以在设置里打开)。
- 移动必然丢分组(标签组是窗口内的概念),所以窗口归并先于归组执行:搬完再在新窗口里归一次,顺序反了就得归两遍。

5. GUI 里的两处座位,以及一个真实的坑

| 座位 | 槽 | 为什么要有它 |
| 一级设置分区(设置 → 「DSH 标签页」) | settings.section(list) | DSH 生态的惯例:插件设置待在插件设置页 |
| 「插件」分区里本插件那一页 | plugins.bundle.config(keyed,key = 包名) | 装完插件的人会去插件页找,那里本来只有一段描述和不可点的组件清单 |
| 状态点芯片 | 自持 shadow DOM,不用 React | 扩展未装时唯一能看到的东西,且能自己摊开安装引导 |

仍然零构建:加载器会给客户端半区一个 require,require('react') 就能拿到 React (dsh-free-search 就是这么做的),所以这个文件既是源码也是产物。

两个踩过的坑,写在这里省得下次再踩:

- ctx.slots 是 cordis 服务,不声明 inject 就去访问会抛错,而 apply 一抛,客户端 entry 就变成 failed,整个 GUI 启动直接报 web boot: 1 entry did not activate——而且不告诉你原因。所以 inject = ['slots'],且所有取用都套了 try/catch。
- inject 只能写成数组:这个壳把 { optional: ['slots'] } 解析成一个名叫 optional 的服务,于是 entry 永远 pending (waiting for service: optional)。
- 「插件」分区里的那个槽不是 settings.plugins.tab:那个槽属于「内置插件」分区,用来放各家内置插件的视图;第三方 bundle 的配置归 plugins.bundle.config(按包名 key),渲染在自己那一页上。
- 已访问过的设置面板仍然留在 DOM 里但带 hidden:所以「元素存在」不等于「分区正在显示」,自动化里必须按可见判定(offsetParent/:visible),否则会读到上一屏残留的节点。

已知限制

- macOS 上装不了「静默」扩展。 唯一能静默装扩展的机制是企业策略 ExtensionInstallForcelist,而微软官方文档明确写着:

On macOS instances, apps and extensions from outside the Microsoft Edge Add-ons website
can only be force installed if the instance is managed via MDM, or joined to a domain via MCX.
— Microsoft Edge 策略文档而且它不只是「文档这么写」:本机实测 Enrolled via DEP: No、MDM enrollment: No、无任何受管策略文件,所以即使去配策略,非商店扩展在 macOS 上也不会被强制安装。
「加载解压缩的扩展」那一次点击是省不掉的;想彻底免掉只能上架 Edge 加载项商店。
- externally_connectable 只能按 host 匹配、不能按端口,因此本机上任何网页(任意 localhost 端口)都能连上这个扩展。作为补偿,配置写入走逐键白名单校验(未知键直接拒绝、类型不对直接报错),状态响应里只有计数。一个恶意本地页面最多能改组名/颜色/开关或触发一次归组。
- 改了组名又重启浏览器会另建一个新组:扩展把「自己拥有的组 ID」记在 chrome.storage.session,浏览器重启后组 ID 失效,只能按组名找回。想避免就保持组名不变,或把「始终使用配置的组名」打开。
- 不劫持别人的分组:只认领组名匹配或自己记录过的组;你手动建好、里面恰有一个 DSH 标签页的组不会被改名(那个标签页会被拉出来,除非关掉「从别的分组拉过来」)。
- 专属窗口模式的代价:开启后,你打开的第一个 DSH 标签页也会被搬进一个新窗口(因为「整窗只有 DSH 标签页」这个条件一开始不成立);固定标签页会留在原来的窗口里(原因见上)。默认关闭,就是为了让你自己权衡。
- 开发者模式提示:每次启动 Edge 可能提示「停用开发人员模式扩展」,这是浏览器正常提示,选择保留即可。
- Safari 用不了这个方案。 本机实测(macOS 26.7.1 + Safari 27.0): Safari 扩展 API 没有 tabGroups,连 tabs.Tab.groupId 都不存在;AppleScript 词典(/Applications/Safari.app/Contents/Resources/Safari.sdef,8710 字节全文)里只有一个 tab 类,属性仅 source/URL/index/text/visible/name,没有 tab group 类/元素/属性/命令。
唯一的一手能力是快捷指令:Safari 自带 Create Tab Group / Move Tabs to Tab Group / Search Tabs 这几个 App Intent,可用 /usr/bin/shortcuts run "<名称>" 无点击触发——但 macOS 没有「标签页被打开」触发器,所以做不到事件驱动。如果你只是想「别让 DSH 标签混进普通标签」,把 GUI 加成 Safari Web App (添加到程序坞)或给它独立 Profile 比任何脚本都稳。

验证

npm test # 74 项纯逻辑单测(node --test,无需浏览器)
npm run id # 复查扩展 ID:manifest 的 key、插件常量、client 内联值三者是否一致

真实浏览器端到端:加载扩展、开标签页,从扩展自己的 service worker 里读 chrome.tabGroups 状态做断言(不是 mock)
DSH_E2E_URL="http://127.0.0.1:3080/?token=..." \
E2E_CHANNEL=msedge \
PLAYWRIGHT_PATH="/path/to/node_modules/playwright" \
node tools/e2e.mjs

插件能不能在真实 DSH 宿主里挂载?(隔离 DSH_HOME,不碰你的 profile 与 ~/.dsh)
DSH_BIN=/path/to/dsh PLAYWRIGHT_PATH="/path/to/node_modules/playwright" npm run test:mount

does the plugin's own section under Settings really render, and can it write config back?
DSH_BIN=/path/to/dsh PLAYWRIGHT_PATH="/path/to/node_modules/playwright" npm run test:section

插件在设置页里的那个分区真的渲染出来、且能写回配置吗?
DSH_BIN=/path/to/dsh PLAYWRIGHT_PATH="/path/to/node_modules/playwright" npm run test:section

重新生成 README 截图(真机截图,非示意图)
npm run screenshots

四层验证,每层都在本机跑过:

| 通道 | 断言数 | 结果 |
| 纯逻辑单测(识别 / 分组决策 / 窗口归并决策 / 协议与信任边界 / 两张脸一致性 / 宿主镜像) | 74 | 74/74 |
| 真浏览器端到端(Chromium 与 Microsoft Edge 154.0.4258.53,含真实 DSH GUI 会话) | 53 | 53/53 |
| 插件挂载(隔离 DSH_HOME → 真宿主 → 真浏览器打开该实例 GUI) | 9 | 9/9 |
| 插件界面(隔离实例与用户实时实例都跑:一级设置分区 + 插件页那一块,覆盖可见性、版本显示、勾选保存) | 14 | 14/14 |

端到端覆盖:自动建组、组名与颜色、后开的 DSH 标签页自动入组、DeepSeek Harness Docs 不被误伤、普通网页不被归组、固定标签页被正确放过、弹窗与设置页改配置后真实标签组跟着变、重复触发幂等、关标签页后组自动清除、清除后再开能重建; 专属窗口(在设置页打开开关 → 分散在两个窗口的 3 个 DSH 标签页聚进同一个窗口、该窗口里只有 DSH 标签页、你自己的普通标签页一个都没被搬走、专属窗口里的分组也建好了、前后 about:blank 数量不变、关掉开关后恢复); 页面 → 扩展的协议全线(含非法配置被拒绝);以及真实 DSH GUI 里的芯片(零注入由宿主投递、默认收起、点开显示计数、「隐藏」生效、刷新后回来、点「立即归组」标签组真的恢复)。

挂载通道的第三段尤其能说明问题:用一个只含核心 web bundles + 本插件的临时 profile 起真实实例,宿主半区必须真的写出镜像,真实宿主必须把插件的浏览器半区投递给真浏览器(全程不做任何注入),芯片必须连上扩展且页面无 JS 报错。这三条通道都不往你的配置档或 ~/.dsh 里装任何东西。

兼容性

| 项 | 值 |
| DSH | >=0.2.0-rc.2 |
| 浏览器 | Edge / Chrome / Brave / Arc / Vivaldi 等 Chromium 内核(需 MV3 + 标签组 API,Chrome 89+) |
| Safari / Firefox | 不支持(Safari 无标签组 API;Firefox 需另写扩展) |
| Node(仅工具与测试) | >=22 |
| 依赖 | 运行时零依赖;扩展零构建 |

License

MIT

自动把每个打开 DeepSeek Harness (DSH) Web GUI 的浏览器标签收进同一个标签组。

你可能同时开着好几个 DSH 会话——侧边栏每一行都是真链接,中键一点就开新标签——几分钟后标签栏就乱成一团。这个扩展会在这些标签一出现时就把它们归进名为 DSH 的组,你继续开新标签它也跟着收,最后一个关掉时交给浏览器移除该组。这个仓库是一个包、两张面孔:浏览器扩展负责所有分组动作,DSH 插件把它镜像到固定路径,在 GUI 里检测它、引导安装并驱动它。

中文 · English

为什么你需要它

- 开几个会话,标签栏就没法用了。 dsh-session-url 把侧边栏每一行都变成真链接(中键 / Cmd+点击开新标签),于是「一个会话一个标签」成了常态——而原生标签组还得手动建。
- DSH 插件做不到这件事。 浏览器侧的插件就是普通页面 JavaScript,SDK 里根本没有浏览器扩展 API(chrome.tabs、chrome.tabGroups、chrome.runtime——一个都没有)。宿主插件跑在 Node 里,也够不到浏览器。标签栏只有扩展能碰到,所以这个功能只能做成扩展。
- 不该让人觉得装了两样东西。 两张面孔放在同一个目录:装 DSH 插件的同时也会把扩展镜像到固定路径,并在 GUI 里告诉你该加载什么、有没有成功。

功能

- 一出现就归组:DSH 标签一出现就加入组,后来的跟着进,最后一个关掉时浏览器移除空组。
- 可选的专用窗口:打开后,每个 DSH 标签都合并进一个窗口,里面只有 DSH 标签——你自己的标签一个都不动。
- 靠标题锚点 DeepSeek Harness 识别:一条规则就覆盖随机 --port、localhost 与 127.0.0.1、局域网绑定和 cloudflared 隧道主机名(这些端口规则永远匹配不到)。前缀匹配刻意收紧,所以标题为 DeepSeek Harness Docs 的无关站点不会被误抓。
- 一个包、两张面孔:同一个目录既是浏览器扩展(manifest.json + src/)也是 DSH 插件(lib/ + cordis.patch.yml)。浏览器忽略插件文件,DSH 忽略扩展。
- 固定的扩展 id:manifest 带一个公开 key,所以 id 始终 pnncehmieeobfabnbdepbldiknndjbhl 无论文件夹放在哪里——这是两个界面能互相寻址的前提。
- 坐落在 DSH 自己的设置里:Settings 下会出现一级的 DSH 标签页 区块,放着开关、组名、颜色、实时计数和「立即分组」——插件的设置就该待在插件设置页,DSH 生态就是这么安排的。
- 外加一个自愈状态点:角落的小标签会收成一个点,点开就是同一套控件。扩展缺失时,它会自己展开成三步引导。「隐藏」只在当前页面加载期间有效,刷新就会回来。两个入口都要有,因为设置区块在最要命的那种状态下毫无用处——没装扩展时你根本想不到去设置里找,而小标签就在眼前,还能把安装引导直接铺给你看。
- 纯本地:三个权限(tabs / tabGroups / storage),没有主机权限,没有内容脚本,没有网络请求,没有遥测。
- 零依赖,零构建:加载扩展就能跑;挂上插件就能跑。

截图

▲ 插件在 DSH Settings 下自己的一级区块:开关、组名、颜色、实时计数和「立即分组」

▲ 另一个入口:Plugins 区块里这个插件自己的页面,卡片落在包描述和(永远点不动的)组件行之间

▲ 左:DSH 里的状态点小标签。中:扩展弹窗(开关 / 计数 / 立即分组)。右:扩展自己的选项页,页脚实时列出当前规则匹配到哪些标签页

浏览器自带的标签栏属于浏览器 chrome,页面级 API 抓不到,所以这里没有标签栏截图——画一张示意图就是造假,不提供比提供更好。

安装

两步。第二步只需点一下,而且只做一次。

1. 安装 DSH 插件

cd <your dsh-tab-groups directory>
dsh plugin --profile web add link:$PWD

随后宿主端会把扩展镜像到 $DSH_HOME/dsh-tab-groups-extension(默认是 ~/.dsh/dsh-tab-groups-extension),并把这个路径打进日志。刷新 GUI,角落里就会出现一枚「DSH 分组」标签。

从 npm 安装也一样(dsh plugin --profile web add dsh-tab-groups);镜像路径是固定的,跟包安装在哪里无关。

2. 把镜像目录加载进浏览器

1. 打开 edge://extensions(Chrome 里是 chrome://extensions),开启 开发者模式
2. 点 加载解压缩的扩展,选中 ~/.dsh/dsh-tab-groups-extension (在 Finder 里按 Cmd+Shift+G 可以粘贴这个路径)
3. 回到 DSH 页面:几秒内标签就会变绿(也有一个「重新检查」按钮)

打开那个页面只需一条命令:

open -a "Microsoft Edge" "edge://extensions/"

只用扩展

DSH 插件是可选的:把「加载解压缩的扩展」直接指向本仓库目录,扩展的行为完全一样。

用法

- 什么都不用做:打开 DSH 标签页,它就会加入分组。
- 弹窗(点扩展图标):显示计数、「立即分组」按钮,以及选项页链接。
- 选项页:分组名称、颜色、自动折叠、匹配规则(标题 / origin)、是否把标签页从其他分组里拉出来、是否跳过固定标签页,以及窗口模式——底部有实时预览。
- 独立窗口:DSH 设置 → 标签页、DSH GUI 里的标签,或扩展自己的 Window 卡片——三处写的是同一项设置。默认关闭。
- DSH 设置 → 标签页:日常用的控制项(启用、独立窗口、分组名称、颜色、计数、立即分组)。
- 扩展自己的选项页:全部选项,包括匹配规则、是否把标签页从其他分组里拉出来、是否跳过固定标签页。
- DSH 里的标签:默认只显示一个状态点(绿色 = 已连接);点一下展开面板,点「隐藏」可以让它消失,直到下次页面加载。
- 从 DSH 页面驱动扩展(进阶):页面侧协议是 ping / status / reconcile / set-config;见 工作原理。

工作原理

1. 标签页如何被识别为 DSH

锚点是页面标题 DeepSeek Harness,这个选择经过验证,不是猜的:

- @deepseek-ai/dsh-web-frontend/dist/index.html 里 硬编码为 DeepSeek Harness;
- 整个前端 bundle 里没有任何 document.title 赋值,所以运行时改不了;
- 宿主 Web 服务器只向 index.html 注入 window.__DSH_BOOT__,从不改写 (dsh-host-webserver/lib/index.js 里根本没有 title 这个词)。

前缀匹配只在后面不是单词字符时生效,所以 DeepSeek Harness · session 和 DeepSeek Harness — sign-in 能匹配,而 DeepSeek Harness Docs 不能。配置的 origin(默认 http://127.0.0.1:3080、http://localhost:3080)是标题加载出来之前那一瞬间的兜底。

2. 一个包,两张面孔

| 面孔 | 运行在 | 负责 |
| 浏览器扩展 —— manifest.json + src/ + icons/ | Edge / Chrome 以及其他 Chromium 浏览器 | 全部标签组行为 |
| DSH 插件 —— lib/ + cordis.patch.yml | DSH 宿主和 Web GUI 页面 | 镜像扩展、检测扩展、引导安装、驱动扩展 |

宿主那一半(lib/index.js)在插件激活时把扩展面孔复制到 $DSH_HOME/dsh-tab-groups-extension:幂等(版本戳意味着同版本运行不复制任何东西),而且范围很窄(只替换已知的面孔条目,绝不动那个目录里的其他东西)。浏览器那一半(lib/client.js)是自包含的 shadow-DOM 小标签,不依赖任何官方插槽或服务形态——因为「扩展缺失」正是它存在要解释的状态。

3. 两张面孔如何找到彼此

扩展声明了 externally_connectable:

"matches": ["http://127.0.0.1/", "http://localhost/", "https://.dsh-market.com/"]

Chromium 只向匹配的页面注入 chrome.runtime。所以:

- 页面能看到 chrome.runtime.sendMessage → 扩展已安装;
- 看不到 → 扩展缺失或被禁用,小标签显示引导。

不探测扩展 URL,不搞 manifest 花招,不要额外权限。协议(src/protocol.js,纯逻辑,有单元测试):

| 消息 | 返回 |
| {type:'ping'} | {ok, installed, protocol, id, version} |
| {type:'status'} | 开关、组名、颜色、匹配数、待处理数、窗口数、上次运行 |
| {type:'reconcile'} | 立即分组,返回 {moved, groups, matched, errors} |
| {type:'set-config', config} | 逐键校验、写入,然后重新分组 |

它刻意不暴露其他任何东西:状态只带计数——没有标签 URL、标题或 id。

4. 专用窗口模式(默认关闭)

开启后,每个 DSH 标签都合并到一个窗口里,这个窗口只装 DSH 标签。规则如下:

- 只接管已经全是 DSH 的窗口。 只要你的某个窗口里还有普通标签,它就永远不能成为归宿——把你自己的标签挪到别处,比要求做的事侵入性强得多——所以改为新建一个窗口。
- 新窗口通过 tabId 播种(某个 DSH 标签成为它的初始标签),而不是裸创建:裸调 windows.create() 会留下一个多余的 about:blank 标签(已验证),而端到端
- 断言监视器会检查空白标签页数量始终不变。
- 固定标签页绝不参与。 已验证:跨窗口的 chrome.tabs.move 会成功,但会静默取消固定该标签页——悄悄丢掉别人的固定,比不动这个标签页更糟。
- 被移动操作清空的窗口会由浏览器关闭(已验证),所以不会留下空壳窗口;扩展也会随之忘掉记住的窗口 id。
- 焦点不受影响:新窗口以 focused: false 创建,所以你不会被从正在做的事情里拽出来(可在设置里选择开启抢焦点)。
- 移动必然会丢失分组归属(分组是按窗口划分的),所以窗口合并要在分组之前运行:先移动,再在新窗口里分组一次。反过来做就会分组两次。

5. GUI 里的两个位置,以及一个值得记下的坑

| 位置 | 它是什么 | 为什么必须存在 |
| 设置区块(Settings → DSH 标签页) | 通过 ctx.slots.inject('settings.section', …) 注册的 React 组件 | DSH 的约定:插件的设置放在插件设置页上 |
| 状态点小徽章 | 自包含的 shadow DOM,不用 React | 扩展缺失时唯一可见的东西——而且它能自己打开指南 |

依然是零构建:加载器给客户端那一半传了个 require,所以 require('react') 能用(dsh-free-search 就是这么做的)。这个文件既是源码也是产物。

两个坑,记下来免得再踩第二次:

- ctx.slots 是 cordis 服务,不声明 inject 就读它会抛异常。 apply 里抛异常会让客户端入口变成 failed,整个 GUI 启动直接挂掉,报 web boot: 1 entry did not activate——却不说原因。所以要写 inject = ['slots'],并且每次访问都包上 try/catch。
- 这里 inject 只接受数组形式。 { optional: ['slots'] } 会被解析成一个字面名叫 optional 的服务,让入口永远停在 pending (waiting for service: optional)。

已知限制

- macOS 上无法静默安装扩展。 唯一能静默安装扩展的机制是企业策略 ExtensionInstallForcelist,而微软自己的文档说得很明确:

在 macOS 实例上,来自 Microsoft Edge 加载项网站之外的应用程序和扩展,只有在实例通过 MDM 管理,或通过 MCX 加入域的情况下,才能强制安装。
— Microsoft Edge 策略文档

而且不只是文档这么说:这台机器报告 Enrolled via DEP: No、MDM enrollment: No,也完全没有受管理的偏好设置文件,所以在 macOS 上配置该策略并不会强制安装非商店扩展。因此那一次「Load unpacked」点击还得保留;要彻底去掉它,就得发布到 Edge 加载项商店。
- externally_connectable 按主机匹配,无法按端口匹配,所以这台机器上任何页面(任何 loopback 端口)都能访问该扩展。作为补偿,配置写入会逐键校验(未知键会被拒绝,类型错误会报告),状态只暴露计数。恶意本地页面最多只能重命名/改分组颜色、切换开关,或触发一次 reconcile。
- 重命名分组后重启浏览器会创建第二个分组:扩展把属于自己的分组 id 记在 chrome.storage.session 里,重启后只能按名称重新找到该分组。要么保留名称,要么让「始终使用配置的名称和颜色」保持开启。
- 不会劫持外部创建的分组:只有名称匹配或记住的 id 才会被认领,所以你手动创建、恰好包含 DSH 标签页的分组不会被重命名(除非你关掉「从其他分组拉取」,否则该标签页会被拉出来)。
- 专用窗口的代价:一旦开启,你打开的第一个 DSH 标签页也会被移进新窗口(此时「全 DSH 窗口」条件还不成立),而固定标签页会留在原窗口(见上文)。它默认关闭,正是为了让你自己权衡。
- 开发者模式提示:Edge 可能在启动时提示禁用开发者模式扩展。这是浏览器的正常行为;选择保留即可。
- Safari 做不到。 在这台机器上实测(macOS 26.7.1 + Safari 27.0):Safari 的扩展 API 没有 tabGroups,连 tabs.Tab.groupId 都没有;Safari 的 AppleScript 字典(/Applications/Safari.app/Contents/Resources/Safari.sdef,8710 字节,已完整读取)只有一个 tab 类,含 source/URL/index/text/visible/name,没有 tab group 类、元素、属性或命令。唯一的官方途径是 Shortcuts —— Safari 自带 Create Tab Group / Move Tabs to Tab Group / Search Tabs App Intents,可以用 /usr/bin/shortcuts run "<name>" 无头驱动 —— 但 macOS 没有「标签页打开」触发器,所以无法事件驱动。如果目标只是让 DSH 标签页不和其他标签页混在一起,把 GUI 做成 Safari Web App(Add to Dock)或给它单独的 Profile,比任何脚本都更稳。

验证

npm test # 74 pure-logic unit tests (node --test, no browser)
npm run id # check that the manifest key, the plugin constant and the inlined client literal agree

real-browser end to end: load the extension, open tabs, and read chrome.tabGroups
from the extension's own service worker — the browser's state, not a mock
DSH_E2E_URL="http://127.0.0.1:3080/?token=..." \
E2E_CHANNEL=msedge \
PLAYWRIGHT_PATH="/path/to/node_modules/playwright" \
node tools/e2e.mjs

does the plugin actually mount in a real DSH host? (throwaway DSH_HOME; your profile and ~/.dsh untouched)
DSH_BIN=/path/to/dsh PLAYWRIGHT_PATH="/path/to/node_modules/playwright" npm run test:mount

重新生成 README 截图(真实截图,不是效果图)

npm run screenshots

四条流水线,全部在本机运行:

| 流水线 | 断言数 | 结果 |
| 纯逻辑单元测试(匹配 / 分组方案 / 窗口合并方案 / 协议及其信任边界 / 双面一致性 / 宿主镜像) | 74 | 74/74 |
| 真实浏览器端到端(Chromium 和 Microsoft Edge 154.0.4258.53,包含一次实时 DSH GUI 会话) | 53 | 53/53 |

| the plugin's own UI (run against both an isolated instance and the live one: the Settings section and the Plugins-page card, covering visibility, version display and a saved toggle) | 14 | 14/14 |
| plugin mount (throwaway DSH_HOME → real host → real browser opening that instance's GUI) | 9 | 9/9 |

The end-to-end lane covers: automatic grouping, group name and colour, later DSH tabs joining by themselves,
DeepSeek Harness Docs not being caught, ordinary pages left alone, pinned tabs correctly skipped, changing
config in the popup or options page actually renaming/recolouring the browser group, repeated runs being
idempotent, the empty group disappearing when the last tab closes and being rebuilt afterwards, the
dedicated window (switching it on from the options page consolidates three DSH tabs spread over two
windows into one all-DSH window, not one ordinary tab is moved, the group is rebuilt inside it, the
about:blank count is unchanged, and turning it off restores the previous behaviour), the whole
page → extension protocol (including rejecting invalid config), and the chip inside a real DSH GUI
(delivered by the host with zero injection, collapsed by default, expanding to show counts, "hide" working,
returning after a reload, and its "group now" button really restoring the browser group).

The mount lane is the load-bearing one: a throwaway profile containing only the core web bundles plus this
package boots a real instance, the host half must really write the mirror, the real host must deliver the
plugin's browser half to a real browser (no injection anywhere), and the chip must connect to the extension
with no JavaScript error on the page. None of these lanes install anything into your profile or ~/.dsh.

Compatibility

| Item | Value |
| DSH | >=0.2.0-rc.2 |
| Browsers | Edge / Chrome / Brave / Arc / Vivaldi and other Chromium browsers (MV3 + tab groups, Chrome 89+) |
| Safari / Firefox | not supported (Safari has no tab-group API; Firefox needs a differently written extension) |
| Node (tools and tests only) | >=22 |
| Dependencies | none at runtime; no build step for the extension |

License

MIT
预览分享卡片长什么样 ↓DDDMUC/dsh-tab-groups
分享 · DeepSeek Harness Hub
DDDMUC/dsh-tab-groups

把浏览器里所有 DSH 标签页自动收进同一分组

这是你的仓库?认领条目

回到顶部