← 返回列表
未验证
Platforms: desktop, iOS, Android
尚未跑自动兼容性验证,可查看页面内的依赖与入口分析。 · 最近上游提交 2026/9/18 · 已提供中文文档
一个 Tauri 2 客户端——支持桌面端、iOS 和 Android——连接到 DeepSeek Harness 主机,让你用一个控制平面对所有主机上运行的智能体会话进行统一管理。
综合分
29.8
GitHub 分
29.8
用户评分
—
★ Stars
0
周下载量
—
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add d4551/DeepTail该插件未发布到 npm,走 GitHub 源安装(pnpm 若拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑)
数据截至 2026/9/20(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动
README
DeepTail
TypeScript 7.0.2
Bun 1.4
Tauri 2.11
Rust edition 2024
Vite 8
Playwright 1.63
Platforms: desktop, iOS, Android
License: MIT
一个 Tauri 2 客户端——支持桌面端、iOS 和 Android——可连接到
DeepSeek Harness 主机,
并让你通过一个控制平面对所有主机上运行的智能体会话进行管理。
跨两台主机的集群名册
用五岁小孩也能听懂的话解释
你有机器人在好几台电脑上同时帮你干活。每台电脑各自保存着一份清单,
记录它的机器人正在做什么,而要想查看它们,你得挨个走到每台电脑前。
DeepTail 是一个窗口,能同时显示所有清单。你打开它,就能看到每台电脑上的
每个机器人、哪些此刻正忙、以及每个机器人最后一次干活是什么时候。你可以
启动一个新机器人、告诉某个机器人接下来做什么,或者让某个机器人停下来——
而不用走到那台电脑前。
当你想阅读某个机器人写下的全部内容时,DeepTail 会打开那台电脑自己的
阅读器,因为完整的故事保存在那台电脑上。
它遵循三条规则:
- 每台电脑自己作答。 如果某台电脑关机了,它的那一行会如实显示,并提供
重试。其他电脑照常工作——一台电脑坏掉绝不会让整个窗口一片空白。
- 密码绝不接触页面。 你看到的部分只负责绘制。一个独立的 Rust 部分持有
每台电脑的密钥并负责通信。
- 它可以帮你找到电脑。 如果它们在你的 Tailscale 网络上,DeepTail 会按
名称列出它们,而不用你手动输入地址。
- 每个方向只有一个滚动条。 面板不会嵌套在其他面板里,所以滚轮总是滚动
你正指向的那个东西。
工作原理
flowchart TB
subgraph device["你的设备——一个 DeepTail 应用"]
direction LR
ui["Webview外壳 · 名册 · 对话框只负责绘制,不打开任何套接字"]
rust["Rust 核心主机注册表 · reqwest · 多路复用客户端"]
store[("密钥库设备令牌tailnet 凭据")]
ui -->|"Tauri IPC"| rust
rust -->|"读取密钥"| store
end
subgraph network["通过网络访问,始终只从 Rust 发起"]
direction LR
tailscale["Tailscale API存在哪些机器"]
hosts["每个已配对的 dsh 主机列出 · 启动 · 发消息 · 停止"]
end
rust -->|"列出 tailnet"| tailscale
rust -->|"一元调用"| hosts
hosts -.->|"名册事件"| rust
ui ==>|"打开一个会话"| client["该主机的 harness 客户端对话记录 · 审批 · 计划"]
classDef inside fill:aliceblue,stroke:indigo,color:midnightblue
classDef host fill:honeydew,stroke:seagreen,color:darkgreen
classDef hand fill:lemonchiffon,stroke:chocolate,color:maroon
class ui,rust,store inside
class hosts,tailscale host
class client hand
这张图要展示两件事。每一条跨越设备边界的箭头都从 Rust 出发,从不从 webview 出发——实线箭头是 DeepTail 发起的一元调用,虚线箭头是让名册保持最新而无需轮询的流,而在这两者中,设备令牌都留在 IPC 的 Rust 一侧。粗箭头则是产品边界:DeepTail 拥有整个机群,harness 客户端拥有单个对话,而选择一个会话正是前者移交给后者的地方。
它做什么
DeepTail 是控制平面;harness 客户端是阅读器。 DeepTail 所做的一切都是一元调用,如今已能通过它自己的载体工作。阅读一个对话是流式的,而这属于已经为此构建好的客户端。
| DeepTail | harness 客户端 |
|---|---|
| 配对、切换和取消配对主机 | 对话记录和工具卡片 |
| 在你的 tailnet 上查找主机 | |
| 跨所有已配对主机的实时统一名册 | 审批、项目、计划 |
| 从 agent 预设启动会话 | 一切会话本地的内容 |
| 向会话发消息或引导会话 | |
| 停止正在运行的回合 | |
选择一个会话即完成移交:DeepTail 会启动该主机自己的客户端。它会在客户端的默认视图打开——其启动界面中没有任何东西接受会话 id——文案也如实说明这一点,而不是暗示一个它无法提供的深链接。
Tailscale
输入 URL 并不是任何人查找自己机器的方式。DeepTail 通过 Tailscale 自己的 API 列出你的 tailnet,并显示哪些机器已经配对,因此主机是从列表中选择的,而不是手动转录的。
Tailscale 签发的两种凭据都可用,因为它们不可互换:一个
API 密钥 是一个在固定日期过期的单一密钥,以 HTTP Basic 配对的用户名进行认证;一个
OAuth 客户端 是 Tailscale 为长期运行的集成所记录的 id 和 secret 对,在
/api/v2/oauth/token 处换取短期 bearer,并且需要 devices:core
作用域。两者都会列出 /api/v2/tailnet/{tailnet}/devices?fields=all,其中 tailnet 默认为 -——即该凭据所属的那个。凭据与每个设备令牌一样,存放在同一个平台密钥库中,并且永远不会跨越
IPC 边界;页面接收的是机器,而绝不是列出这些机器的密钥。
列出 tailnet 并不等于配对。 Tailscale 可以说明哪些机器存在;
只有 dsh web 才能铸造出 harness 所接受的启动令牌。因此,选择一台机器
会为它打开配对表单——源已经固定,所以唯一还需要提供的
就是那台机器打印出的令牌。
发现机制也正是 http:// 得以被接受的原因。tailnet 对等节点是通过
WireGuard 访问的,因此在 URL 方案还来不及置喙之前,数据包就已经被加密并
经过对等方认证;tailnet 上的 dsh web 不终止 TLS,也不需要终止。
canonical_origin 允许明文连接到 tailnet 对等节点和回环地址,
并拒绝其他所有情况,它读取 Tailscale 自己的地址范围——100.64.0.0/10
减去 ChromeOS 用于其容器的 100.115.92.0/23 片段、
fd7a:115c:a1e0::/48,以及 .ts.net 下的 MagicDNS 名称。
为什么网络连接放在 Rust 中
webview 从不打开网络连接。 Tauri 从安全上下文源提供页面,
因此它根本无法打开 ws:// 套接字,也无法在 WebSocket 握手中
附加 Authorization 头。将网络连接放在 Rust 中消除了这两个限制,
让设备令牌远离 JavaScript,并让应用能够发布一个不指定任何主机的 CSP:
default-src 'self'; script-src 'self' blob:; connect-src ipc: http://ipc.localhost
设计
UI 是纯 DOM——它在任何 harness 包加载之前就已绘制——但并非一套
独立的设计语言。src/styles/tokens.css 保存了 harness 自己解析后的
--dsw-alias- 和 --dsw-specific- 值及其
body[data-ds-dark-theme] 机制;几何样式遵循 ui-sidebar、
ui-primitives/Menu、ui-primitives/Modal 和 ui-workspace/rows。
没有内联样式。 每个视觉元素都是一个类,由
scripts/check-no-inline-styles.ts 强制执行。它读取的是解析结果而非行——脚本用 oxc,
即 oxlint 使用的解析器,标记用 parse5——因此规则是针对节点陈述的,
没有任何拼写方式可以绕过它们:点属性、索引写入、批量赋值、解构、
CSS Typed OM、源码中任何位置写入标记的 style 属性,以及任何在该名称下
设置属性或特性的调用。名称会被常量折叠,因此一个保存在常量中、用 + 拼接、
在模板中构建、改变大小写、连接或由字符码拼出的名称,都会被读取为它
所产生的名称;门禁完全无法读取的名称会被拒绝,而不是被允许。两个在不指名的情况下
设置属性的调用——setAttributeNode 和 setNamedItem——会被直接拒绝。
它读取的文件是 git 表明仓库所发布的文件,因此它的覆盖范围
不会偏离手写列表,其数量也不会随是否运行过构建而变化。
scripts/ban-gate.ts 读取同一份解析结果,以查找项目已经不再使用的惯用法
以及关闭检查器的指令;指令永远只是注释,因此它搜索的是注释,
Rust 也会被搜索。
两道门禁都由 tests/gates.spec.ts 驱动,它会针对每个规则分别运行一个违反该规则的源文件和一个仅仅与之相似的源文件。
样式表不是内联样式,也不被禁止:injections.ts 会构建一个样式表,因为宿主的启动表要求这样做。color-scheme 同样不被禁止,它是一个 CSS 属性,与调色板一样以 body[data-ds-dark-theme] 属性为键,因此原生 UA 界面不会与主题产生偏差。除此之外,该门禁没有任何豁免。
样式表中的值也要对照令牌表来读取。scripts/sheet-gate.ts 会拒绝 tokens.css 之外的裸长度值——间距、圆角、字号和网格档位——任何写法的原始颜色(函数写法或命名颜色,或十六进制字面量)、级联覆盖标志、遗留的 float、裸堆叠数值、第二种语法中的第二个断点、隐藏焦点环却不重新绘制一个的规则,以及重复声明的规则集。已发布样式表携带的每个值都经过令牌,tests/sheet-gate.spec.ts 会双向驱动每条规则。
标记门禁会读取每个元素,无论由什么写入:除了 style 属性之外,内联事件处理器、内联脚本主体和内联样式块各自都是每页一次性的,没有任何模块会发布它们,一旦它们位于标签内部也没有任何门禁会读取它们,因此都不得发布。禁令门禁针对项目已经不再使用的惯用法携带相同形态的规则:React 19 移除项(ReactDOM.render 及其同类、findDOMNode、字符串 ref、defaultProps、遗留 context)、Tauri v1 API 路径和 __TAURI__ 全局变量,以及对 var、require、标记重写属性、eval 等旧有禁令。tests/gates.spec.ts 和 tests/style-gate.spec.ts 会针对违反每条规则的源文件和仅仅与之相似的源文件来驱动这两道门禁。
| | |
|---|---|
| 连接菜单 | 深色 |
| 宿主切换器——选中状态是尾随的勾选标记,从不用填充表示 | 测试框架的深色调色板 |
| 撰写 | 移动端 |
| 向会话发送消息或进行引导 | 390×844,侧边栏作为抽屉 |
| 部分失败 | 配对验证 |
| 一台主机宕机,其余仍被列出,失败的那一行上有重试 | 拒绝信息指明需要修复的内容,位于相关字段下方 |
| 配对 | 中文 |
| 已配对的主机,在 shell 存在之前 | 中文的空选择器——每个字符串都来自字典 |
| 连接 tailnet | Tailnet 机器 |
| Tailscale 签发的任一凭据,以及一个可选的 tailnet | 你的机器,其中等待批准的那台会变暗而非隐藏 |
| 加载中 | 未配对 |
| 在首次名单读取完成之前 | 未配对:配对一台主机,或从 Tailscale 中选择一台 |
| 深色选择器 | 未授权 |
| 未配对,深色调色板 | 已配对但测试框架不再接受其设备令牌的主机 |
每个状态都已构建并截图:加载中、稳定后为空、错误、
部分失败(一台主机宕机,其余仍列出)、未授权,以及
离线,还有两个 tailnet 界面。apps/deeptail/tests/screenshots/ 存放全部
22 张,每张都由断言该状态的用例写出,因此没有一张会
与代码脱节。
布局
apps/deeptail/
src/ shell、连接菜单、名单、对话框、载体、流客户端
src-tauri/ 整个网络与凭据界面,包括 Tailscale
packages/host-fleet/ Cordis 插件:sessions_* 编排工具
profile/ DeepTail 在主机上安装的 dsh profile
凭据
每台主机一个设备令牌,存放在平台自有的存储中。一体化的 keyring
crate 在其默认特性下仅支持桌面端,因此这里使用 keyring-core,为每个目标平台配一个原生存储——这正是让 iOS 和 Android 得以工作的原因:
apple-native-keyring-store、android-native-keyring-store(Keystore +
SharedPreferences)、windows-native-keyring-store、
dbus-secret-service-keyring-store。
配对会消耗 dsh web 已经打印的启动令牌。在非回环地址上
明文配对会被拒绝。
测试框架接缝
读取对话需要 harness 客户端,而该客户端在上游落地三项改动之前
无法启动。控制平面不依赖它们。
| 接缝 | 原因 |
|---|---|
| ClientTransportHooks 上的 socket 工厂 | remoteStreamUrl() 从 location.origin 推导 mux 授权机构,而安全上下文页面无法打开 ws:// |
| 通过已认证的 /api 路由提供 collectIndexInjections() | 启动表只能以注入 HTML 的形式访问 |
| 将 applyIndexInjections 从 packages/experimental/ 中提升出来 | 非服务型 shell 需要它,而该包未发布 |
开发
bun install
bun run validate # biome、oxlint、样式门禁、类型检查、测试、knip、clippy、cargo test、浏览器、axe
bun run tauri dev # 桌面端
bun run tauri android dev # Android Studio、JAVA_HOME、ANDROID_HOME、NDK_HOME
bun run tauri ios dev # 仅限 macOS + Xcode + CocoaPods
浏览器测试在 Chromium 中驱动真实构建产物,仅替换
Tauri IPC 边界,因为任何浏览器都不提供该边界。它们对渲染文本和
角色进行断言,并写出上述每张截图。脚本化的 IPC 覆盖 mux socket
以及一元调用,因此实时名册——一行通过 $events 到达、一次移除、一次状态翻转、一个中断的流——是被实际执行的,而不是被假定的。
两个测试套件读取渲染后的页面,而不是源代码。a11y.browser.spec.ts 在 wcag2a、wcag2aa、wcag21a、wcag21aa、wcag22aa 和 best-practice 上运行 axe,并将 incomplete 视为违规,因此 axe 无法裁定的规则是失败而非通过。structure.ts 覆盖了没有规则引擎会报告的内容:重复的 id、交互式元素嵌套在另一个交互式元素内、跳过的标题层级、指向空处的 ARIA 引用、位于其列表之外的项、横向滚动的文档、被自身盒子裁剪的文本、在同样滚动的窗格内滚动的窗格,以及低于其指针下限的目标——根据 WCAG 2.2 SC 2.5.8,任何指针上为 24 CSS px,手指上为 44,这正是 Apple 的 Human Interface Guidelines 和 SC 2.5.5 所要求的。几何部分位于 structure-layout.ts;两者都作为各自的源文本被送入页面,因此它们所依据的任何东西都不会被遗漏。
测试套件以同样的方式划分。structure.browser.spec.ts 遍历各个界面并断言每个都是干净的;structure-geometry.browser.spec.ts 测量手指能够触及的范围,并且,因为一个从不报告的检查与一个无法报告的检查无法区分,它让外壳在自身内部滚动并断言窗格规则将其命名。44px 下限只在以 hasTouch 打开的页面上测量:pointer: coarse 在窄桌面窗口中不成立,因此针对它进行测量会测试一种不存在的配对。
每个依赖都保持在此工作区能够安装的最新版本。check:outdated 读取 bun outdated --filter "*"——包管理器自己的报告,它已经了解范围和 workspaces——而不是询问注册表本身,并拒绝任何落后于已发布版本的固定版本。没有任何东西阻止发布被解析:bunfig.toml 没有设置发布保留,并且 tests/stack.spec.ts 拒绝保留,因此落后的固定版本是需要移动的固定版本,而不是需要解释的策略。tests/stack.spec.ts 中的下限与固定版本保持相等,因此降级会在那里失败,而升级必须被记录下来。
Playwright 解析它所安装的 Chromium。
Pipeline
两项检查决定合并,分支保护要求两者都通过。
gate(.github/workflows/ci.yml)在四个作业——static、types-and-tests、browser 和 rust——中不加修改地运行仓库自己的 gate 脚本,以及第五个作业 gate,它等待所有四个作业,并且除非每一个都报告绿色,否则以非零退出。一个在该聚合之外的作业会运行、报告,但不阻止任何东西,因此 scripts/pipeline-guard-rules.ts 中的 aggregationViolations 会拒绝它。每个 action 都固定到完整的提交 sha,每个作业都受超时限制,每次 checkout 都丢弃其令牌,每次安装都冻结到 lockfile。
pipeline-guard(scripts/pipeline-guard.ts)读取工作流定义,
清单和代码所有者名单以审查者的方式列出,并采用失败关闭:
一个无法读取的定义就是违规,绝不是没有定义。决定是否值得发布的关卡
在 MERGE_GATES 中按名称固定,并由 validate 链和 ci.yml 约束到
同一份列表,因此删掉其中一个,就会在删掉它的同一个提交上让这项检查变红。
tests/pipeline-guard.spec.ts 会针对带有作弊行为的定义驱动每一条规则,
然后针对这个仓库实际发布的流水线一次性驱动所有规则。
mutation(.github/workflows/mutation.yml)是按时钟进行的审计,而不是
合并决策。五个 Stryker 范围覆盖仓库发布的每一个源文件——scripts/、
apps/deeptail/src/ 和 packages/host-fleet/src/——每个范围低于 99 分都会
失败。tests/mutation-config.spec.ts 会逐文件地根据目录树读取这些范围,
因此不在任何范围内的文件会被点名,而不是由同类文件代为交代,并且拒绝
四种不触碰产品代码就能移动分数的修改:降低阈值、收窄 mutate、把文件
留在所有范围之外,以及就地禁用注释。
工具链
TypeScript 7.0.2 · Bun 1.4.2 · Node 24 LTS · Tauri 2.11 · Rust edition 2024 ·
Vite 8 · Playwright 1.63.0。
tests/stack.spec.ts 会把那一行上的版本与清单相互核对,因此工具链行
写出的内容如果与仓库实际安装的不符,就是失败,而不是一段没人再读的
段落。
许可证
MIT同作者(d4551)的其他插件
扫码进群