给 DeepSeek Harness 写了个 web UI 插件

发布时间:2026/10/20 6:12:40

给 DeepSeek Harness 写了个 web UI 插件 摘要AI 编程工具的插件生态大家都在写工具型插件。但想给界面换个皮肤、加只悬浮宠物得写另一类——客户端 UI 插件。本文以我自己开源的 DSH 咸鱼宠物插件为例拆开它的两层入口、槽位注入以及几个踩出来的前端工程坑背景透出、毛玻璃的包含块陷阱、穿透宿主 DOM。图服务端工具插件藏在背后供模型调用客户端 UI 插件浮在界面上给人看——后者通过shell.overlay槽位注入踩过背景透出、毛玻璃陷阱、穿透 DOM 三个坑。你每天盯着 AI 编程工具的界面八小时界面的样子却是别人定死的。想换张壁纸、加只悬浮宠物翻遍设置项找不到入口——因为这类皮肤层的改动工具厂商压根没留。DeepSeek Harness下称 dsh留了入口一切皆插件。但多数教程讲到插件都在教你怎么注册一个工具给模型调用。给界面动刀子的另一种插件——客户端 UI 插件讲的人少。本文以我自己开源的 dsh 咸鱼宠物插件为例把它从入口、槽位到几个前端坑完整拆一遍。插件跑在哪决定它是哪一类dsh 的插件分两类区别不在功能在代码跑在哪里、服务谁服务端工具插件默认形态跑在 Node 侧。它注册一个工具比如greet模型通过描述知道它存在、通过入参 schema 知道怎么调。我 8-17 那篇写的就是这种。客户端 UI 插件声明platform: web跑在浏览器里往 UI 槽位塞一个 React 组件。它不注册工具、不消费任何 Cordis 服务纯粹在界面上做文章。一句话区分前者给模型加能力后者给界面加 personality。咸鱼宠物是后者——它不替你写代码只在你写代码时陪你摸鱼。一个插件为什么要拆两层入口最反直觉的地方这个插件有两份入口文件。src/index.ts是宿主Node入口全文就一行exportfunctionapply():void{}空的。它存在只为了让 Cordis Loader 认它是个合法插件——能进 host 的 cordis.yml 插件树。它不干任何服务端的事。真正的逻辑在src/client/index.ts通过package.json的 exports 映射暴露出去{exports:{.:{default:./lib/index.js},./client:{default:./lib/client.js}},dsh:{client:{inject:[deepseek-ai/dsh-client-runtime,deepseek-ai/dsh-client-ui-slots],platform:web}}}exports[.]指向 host 那份空applyexports[./client]指向浏览器侧。dsh 的客户端运行时读到dsh.client.platform: web就知道该加载./client这份、并把它要注入的客户端服务dsh-client-runtime、dsh-client-ui-slots准备好。这个拆分的收益很实在同一个 npm 包在 Node 侧零成本空 apply 不引入 React在浏览器侧才真正加载组件。你不会因为装了个 UI 插件就给服务端进程塞进一堆前端依赖。槽位把组件挂进界面UI 不是随便往document.body一扔就完事。dsh 把界面切成若干槽位slot插件往指定槽位注册组件。咸鱼插件用的是shell.overlay——一个挂在 root 作用域的list型槽位多个插件都能往里加条目。插件先声明它需要slots服务再注入组件exportconstinject[slots]asconstdeclaremoduledeepseek-ai/dsh-client-ui-slots{interfaceSlotMap{shell.overlay:{kind:list;scope:root}}}exportfunctionapply(ctx:ClientContext){ctx.inject([slots],(scope){constdisposescope.slots.inject(shell.overlay,()scope.slots.register({name:shell.overlay,id:uiPet},SaltedFishPet),)return()dispose()})}slots.inject拿到槽位注册表slots.register把一个 React 组件绑到uiPet这个 id 上。咸鱼和壁纸选择器是两个独立的槽位条目uiPet和wallpaperPicker各自挂各自的组件。注册进 bundle一行命令背后开发时你 clone 源码、跑install.sh普通用户则是一行npx deepseek-ai/dsh plugin--profilewebaddtangyuewei/dsh-client-ui-pet这条命令本质是在持久化的 profile 目录~/.dsh/profiles/web里pnpm add这个包并把它的cordis.patch.yml追加进 bundle 清单。那个 patch 文件极简-insert:-id:ui-petname:tangyuewei/dsh-client-ui-petdsh 启动时按 bundle 顺序叠加各插件的 patch在shell.overlay里注入咸鱼。profile 目录与 npx 缓存无关重开终端、重跑 npx 依然生效——这是它比每次手动 patch 源码稳的地方。真实运行效果默认yu7毛玻璃背景与咸鱼 → 打开壁纸面板 → 切换 Porsche 718 → 置顶背景图全屏置顶→ 取消置顶 → 切换 Macan S深色系壁纸亦可选深浅色不限制。跨槽位状态两块 UI 怎么联动咸鱼和背景是两个独立槽位条目意味着它们不在同一棵 React 组件树里不能直接传 props、不能共享 context。但点一下隐藏咸鱼要把背景也收起来这个联动必须成立。解法是抽一个模块级共享 storevisibility.tslethiddenfalseconstlistenersnewSet()void()exportfunctionsetPetHidden(next:boolean):void{if(hiddennext)returnhiddennextfor(constlisteneroflisteners)listener()}exportfunctionsubscribePetHidden(listener:()void){listeners.add(listener)return()listeners.delete(listener)}一个布尔 一个监听器集合。宠物按钮调用setPetHidden背景模块的订阅者收到通知后同步收起。宠物组件本身用useSyncExternalStore接这个 store所以即使槽位被重新挂载读到的仍是同一份持久化的可见状态不会宠物没了、背景还在。代价我说在前面召唤按钮目前靠查找页面上含 “Session log” 文字的按钮来定位自己Shell 结构一变就可能失效。这是已知限制不是特性。壁纸 API丢图即生效壁纸不区分深浅主题用户随便挑。源图丢进src/client/wallpapers/构建脚本build-wallpapers.mjs自动缩放到 1920px、base64 编码进bg-images.generated.tsgitignored每次构建重生于是加一张壁纸等于丢一个文件不用手敲 base64。选择态用localStoragekeydsh-ui-pet.wallpaper持久化并通过window上的CustomEvent广播exportfunctionsetCurrentWallpaperId(id:string):void{localStorage.setItem(STORAGE_KEY,id)window.dispatchEvent(newCustomEvent(CHANGE_EVENT,{detail:{id}}))}背景模块和选择器都订阅这个事件换壁纸时两边同时刷新刷新和重启后选择还在。几个前端坑架构落地的代价插件能跑起来靠几处对症的前端技巧。它们不性感但少一个界面就废。背景透出。主题背景默认不透明会盖住 body 上的壁纸。解法是把主题背景底色强行透明让 body 的壁纸透出来body.dsh-bg-glow, body.dsh-bg-glow #root, body.dsh-bg-glow #root *{--dsw-alias-bg-base:transparent!important;}一个 CSS 变量加!important覆盖掉主题对background简写设的不透明底色。毛玻璃的包含块陷阱。侧栏想做毛玻璃直觉是给列加backdrop-filter。但backdrop-filter会让那个元素变成其内部position: fixed后代的包含块——设置面板是 portal 渲染的会被困在 280px 宽的列里。改用isolation: isolate也不行它会创建层叠上下文Chrome 会裁剪上下文内fixed后代到自身overflow。正解是滤镜放::before伪元素、z-index: -1逃逸到 body 背景层body.dsh-bg-glow [class$sidebarCol]::before{content:;position:absolute;inset:0;z-index:-1;-webkit-backdrop-filter:blur(26px)saturate(160%);backdrop-filter:blur(26px)saturate(160%);}这俩雷在 MDN 的backdrop-filter条目里都有记载但只有真写一遍才知道它会咬人。穿透宿主 DOM。宿主的列用了 CSS Modules类名是带哈希的sidebarCol__abc123。我没法 import 宿主组件去加样式只能用后缀属性选择器命中本地名body.dsh-bg-glow [class$sidebarCol]{background:rgba(255,255,255,0.55)!important;}[class$sidebarCol]匹配以 sidebarCol 结尾的类名不依赖哈希前缀。代价是选择器写得宽未来宿主改本地名规则就得跟着改。主题与光晕。MutationObserver监听body的data-ds-dark-theme属性切深浅色鼠标跟随光晕只往--bg-mx/--bg-my两个 CSS 变量写坐标由合成器compositor处理不触发重绘——mousemove用passive监听不阻塞滚动。已知限制收益和代价一起给这个插件不完美我把它写进 README 的已知限制零持久化宠物位置、饱腹度、心情都是会话级内存刷新即重置无配置面板所有参数衰减速度、边距、尺寸要改源码重新构建没暴露给用户召唤按钮依赖 DOM 查找Shell 结构变更可能失效壁纸内嵌 base64无外部请求想动态加载得自己改。这些不是 bug 清单是纯前端展示插件这个定位下的取舍。它本来就不打算做服务端、不做账号体系、不做云同步。收尾写这个插件最大的收获不是多了只咸鱼而是被迫把 dsh 的宿主/浏览器边界、槽位契约、宿主 DOM 穿透摸清了一遍。插件化的另一种形态就在这它不只想给模型加能力也想给界面加 personality。当 AI 编程工具成为你每天盯八小时的东西能不能把它改顺手本身就是工程问题。客户端 UI 插件是这块拼图里被讲得最少的一块——希望这篇把它补上。参考资料开源仓库 tangyuewei/dsh-client-ui-pet2026 年 8 月。MDN Web Docsbackdrop-filter。作者唐悦玮 | 从后端出发用 AI 拓展到全栈的工程师。
延伸阅读

更多相关文章

2026/10/20 21:41:14

三步把 NCM 转成 MP3:ncmdump 免费本地无损转换教程

三步把 NCM 转成 MP3:ncmdump 免费本地无损转换教程 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 车载 U 盘里塞满网易云下载的 .ncm 文件,插上车却一个都放不了?问题不在歌,在格式。…

2026/10/20 21:38:56

从Copilot到Agent:AI编程范式转移与小白实战指南

刚接触AI编程那会儿,我一度觉得Copilot已经够神奇了:敲个函数名它能帮你补完大半段逻辑,写个注释它能生成对应的代码块。但用了一段时间后才发现,这其实只是“快一点”的变化,并不是工作方式真正的转变。真正让我觉得“…

2026/10/20 21:38:56

Windows 10下Poppler库源码编译指南:依赖、CMake与排错

做 PDF 相关开发的人,绕不开 Poppler 这个名字。我手上的一个项目需要做页面渲染、文本抽取和 PDF 元数据解析,Linux 环境下一个apt install就能把全套工具和库装好,但切到 Windows 10 之后才发现事情没那么简单:官方不提供开箱即…

2026/10/20 21:38:56

降AIGC实战:从检测原理到工具应用,彻底解决AI疑似文本问题

学长亲荐!总算有人把降AIGC这件事说明白了又是一个深夜,对着屏幕上那篇改了七八遍的稿子,我盯着检测报告里那刺眼的疑似AI生成比例,心里只剩一个念头:这玩意儿到底怎么才能降下来?相信不少朋友都有同感——…

2026/10/20 21:38:56

Flutter鸿蒙适配全流程实战:从环境搭建到打包上架

这些年跨平台开发的工具箱里,Flutter 算是我用得最顺手的一个,渲染一致性好、热重载效率高,社区生态也够丰富。最近因为项目需要,我把一套 Flutter 应用移植到鸿蒙设备上跑通,整个过程踩了不少坑,也沉淀了一…

2026/10/20 21:38:56

大模型聚合平台:从API转发到智能调度的工程化演进

1. 为什么“聚合平台”正在成为大模型落地的隐形枢纽最近帮某高校实验室做一次AI工具选型调研,发现一个有意思的现象:团队里三位研究员,用的都是同一类任务——文本摘要、多轮对话、结构化信息抽取,但各自安装了五六个不同界面的客…

2026/10/20 21:33:53

WUJI Hand 2机械手转笔背后的灵巧操作工程原理

1. 为什么一只机械手转笔能刷屏?——从“WUJI Hand 2”看灵巧操作的工程本质你有没有在短视频里刷到过那个画面:一只银灰色的五指机械手,指尖轻巧地拨动一支中性笔,笔身在指关节间连续翻转三圈半,稳稳停在食指与中指之…

2026/10/19 18:06:35

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/20 1:05:37

单片机C运行时从启动到多任务:中断安全与共享资源冲突

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:05:37

GCC编译四步详解:预处理、编译、汇编、链接实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:05:37

FIW线是什么?FEP注塑线与普通氟塑料线的区别及应用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑