dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
把 DSH Web GUI 换成昔涟(《崩坏:星穹铁道》)的粉白渐变毛玻璃主题,附带侧边栏可编辑的人格设定、昔涟表情包、新会话欢迎页形象,以及可开关 / 选图 / 轮换 / 调淡的整页背景图。非官方非商业同人作品;昔涟及相关美术素材版权归米哈游(miHoYo / HoYoverse)所有,与官方无隶属或背书关系。
把 DSH Web GUI 换成《崩坏:星穹铁道》昔涟(Cyrene)的粉白渐变毛玻璃主题,把 agent 的人格设定改成昔涟——人格提示词可以直接在侧边栏里改——让她在该有情绪的时候贴一张自己的表情包,把新会话欢迎页的鱼换成她、顺手把输入框的默认提示换成她的话,还能把整页背景换成 Background/ 里放的那几张图(可以在面板里开关、选起点、自动轮换,也能自己上传新的;深色模式下还能自动换成指定的那一张)。
English — A pink-and-white frosted-glass skin for the DSH Web GUI, themed after Cyrene from *Honkai: Star Rail*. It also rewrites the agent persona (editable from the sidebar), teaches the agent to post Cyrene reaction stickers, replaces the new-session hero fish with her, and adds a full-page background manager (enable / pick / rotate / fade the images in Background/, upload your own, and bind one image to dark mode).Install:dsh plugin --profile web add dsh-cyrene-theme(npm, pinned version) or… add github:berhbro/dsh-cyrene-theme(tracks the latest commit) — then reload the page. UI text is Chinese.
Unofficial fan project. Cyrene and all *Honkai: Star Rail* character art, names and assets are © miHoYo / HoYoverse, used here as non-commercial fan work. Not affiliated with or endorsed by miHoYo, HoYoverse or DeepSeek — see 「版权与致谢」 below.
部分 · 实现 主题 UI · 客户端半注入一份作用域化样式表(body[data-dsh-cyrene][data-cyrene-skin="on"]):整体渐变背景、半透明表面 token、backdrop-filter 毛玻璃、粉色描边与柔和阴影、粉色滚动条/选区。全部随插件卸载自动还原。--dsw-alias-bg-base 被设成 transparent,所以渐变由 body::before 铺满视口;桌面版窗口带原生材质(backgroundMaterial: "acrylic"),因此 html 画布上也补了一层不透明底色,避免整窗透出系统模糊。 人格设定 · 宿主半注册 systemPrompt.section({ name: 'cyrene:persona', order: 部署人格前缀 order + 100 })——运行版 dsh-system-prompt 的键是 DEPLOYMENT_PERSONA_PREFIX(0),旧版是 DEPLOYMENT_PERSONA(0),两个都试;都取不到就落到 100,仍然排在通用人格之后、工具策略 PLAN_POLICY(500) 之前。每次保存后重挂这个 section,所以改完立刻对之后每一轮对话(含新对话)生效,不需要重载插件。section 名用本包自己的 cyrene:persona,绝不占用保留名 deployment:persona(那是 @deepseek-ai/dsh-persona 的位置,全局重名注册会直接失败)。 侧边栏编辑 · 客户端半向 sidebar.footer.action 注册「✦ 昔涟」按钮(点开浮动面板),并额外向 settings.section 注册一整页「昔涟 · 主题」,两处用的是同一个编辑器。 表情包 · 素材放在包内 meme/ 下(文件名就是表情名字),stickers.json 是清单(id / 名称 / 什么时候用 / 文件)。宿主半把清单折算成一段附在人格之后的「【表情包】」说明——每张图的绝对路径整行写好,模型直接照抄(路径里带括号、空格或非 ASCII 时,自己拼很容易抄错)——并开两条只读路由:GET /cyrene/stickers(清单,支持 ?reload=1)与 GET /cyrene/sticker/(图片字节,带 ETag/304)。设置页里能看到缩略图与「允许发表情包」开关;关掉后这段说明就从系统提示词里消失。 表情包管理 · 面板里只有一行入口(「✦ 表情包管理 N 张」+「打开管理器」),点开是一整页管理器(挂在 body 上的覆盖层,Escape 或右上「关闭」退出):里面加图(选文件 → 转成 data URL → POST /cyrene/stickers)、删图(只删自己加的,POST /cyrene/stickers/remove)、调大小(48–240px 滑杆,落进 state.stickerSize)。页面本身封顶 + 内层滚动,素材不会撑出管理器范围。自添加的图落在 stickers-custom/(gitignore,属运行时数据),条目存在状态文件的 customStickers 里,与包内清单合并成同一份「【表情包】」说明——加完立刻能贴,不用重载插件行。 背景图 · 两处素材来源:随包的 Background/(自己丢图进去)和面板里上传的 backgrounds-custom/。宿主半扫描两边(GET /cyrene/backgrounds,支持 ?reload=1)、按文件名把字节发出去(GET /cyrene/background/,与表情包同一套响应头),并开 POST /cyrene/backgrounds(上传)、POST /cyrene/backgrounds/remove(只删自传的)。客户端半在 body 上挂一层 .cyre-bg(position:fixed;inset:0;z-index:-1;pointer-events:none):两层图交叉淡入 + 一层色纱,有图时铺满视口;整列对话区不刷白,照片直接透上来,只把用户气泡的底色调厚、再给正文一圈淡光晕。控制板里只留一行入口(「✦ 背景图管理 N 张」+「打开管理器」);管理器是一整页覆盖层:顶部的外观两档「浅色 / 深色」(调宿主自己的主题服务,整个应用跟着换)、「背景图 / 简约(无图)」二选一、上传、点缩略图选起点、开自动轮换(15–3600 秒)、拖浓度(0.2–0.85)、删掉自传的图。详见「背景图」。 长期记忆 · 记忆存在自己的文件里($DSH_HOME/cyrene-memory.json,和设定分开——它会长大,而设定必须保持小且可重置),宿主半开四条路由:GET /cyrene/memory(清单 + 记忆自己的 rev)、POST /cyrene/memory(add / remove / clear / toggle,同样带 rev 的乐观并发,缺 rev → 400、过期 → 409)、GET /cyrene/memory/raw(text/plain 原文,浏览器里直接看得到)、POST /cyrene/memory/reveal(让宿主在系统文件管理器里定位那个 JSON)。另注册三个工具给模型:cyrene_memory_add、cyrene_memory_search、cyrene_memory_forget;记忆正文以独立 section(cyrene:memory,排在人格之后 20)注入系统提示词,有 2600 字预算、按时间倒序,超出预算的老条目不再进提示词(但留在文件里、搜索照样找得到)。控制板里只留一行入口(「✦ 长期记忆 N 条」+「打开管理器」),管理器是一整页覆盖层:启用开关、搜索、手动加一条、删单条、清空。宿主没有 tools 服务时工具整块安静跳过,注入与面板照常。 持久化 · 宿主半自带 fenced 路由 /cyrene/state(GET 读 / POST 写),落盘到 $DSH_HOME/cyrene-theme.json(默认 ~/.dsh/cyrene-theme.json)。写入走乐观并发:写请求必须带上 GET 拿到的 rev,缺 rev(旧版客户端半 / 手搓脚本)→ 400 client-outdated,rev 过期 → 409 stale —— 没刷新的旧页面不可能再把别处的改动盖回去。 对话界面 · 新会话欢迎页:客户端半占用 conversation.hero.brand.mark 这个单占位槽(官方 brand 包不占它,默认渲染才是那个动态鱼;DSH Desktop 的桌面模块在 p0 占着同一个槽,所以本包用 priority:-1 盖住),改成昔涟的整张贴纸(带 alpha 的 Q 版立绘,不画边框底盘)+ 一句粉白渐变艺术字欢迎语,整行是纯装饰(点不响、选不中、拖不动);贴纸字节走宿主半的 GET /cyrene/hero,图片路径来自 state.hero.image。输入框占位提示:原生那句写在 [data-composer-placeholder] 的文本节点里、没有 i18n 覆盖的口子,所以用纯 CSS(把原字染透明 + 自己的 ::after 写一句「有问题?有任务?来找昔涟♪」)替换,不动 DOM、不碰宿主词典。悬浮毛玻璃:输入框卡片的 backdrop-filter 画在 ::before 上而不是卡片本身,否则会破坏发送键那个未开 portal 的 fixed Tooltip 的定位、鼠标停上去时界面闪烁(详见「已知坑」)。详见「对话界面微调」。
从本地目录(开发时就地改):
dsh plugin --profile desktop add D:\dshWorkPlace\dsh-cyrene-theme从 npm(发布版,版本号固定;--profile 换成你自己的 profile 名,Web 端一般是 web,桌面端是 desktop):
dsh plugin --profile desktop add dsh-cyrene-theme从 GitHub(跟随 main 的最新提交,能拿到还没发版的改动):
dsh plugin --profile desktop add github:berhbro/dsh-cyrene-theme装完刷新一次页面:客户端半的 bundle 是页面加载时读的,不刷新还是旧的。(files 里已包含 Background/、meme/、stickers.json,所以从 GitHub 装也带素材。)
卸载:dsh plugin --profile desktop remove dsh-cyrene-theme。
深色主题(night-reading.jpeg)与新会话欢迎页:
浅色主题(the-longest-night.jpeg)与欢迎页艺术字:
控制板(设置 → 昔涟 · 主题:人格底稿与三个开关)与背景管理器(外观两档 / 二选一 / 轮换 / 间隔 / 淡化 / 缩略图):
人格与主题 · 背景 ·
给插件市场用的截图清单在仓库根的 screenshots.json(市场读它、不读本页;放哪几张、什么顺序都由它定,第一张就是封面——现在是深色那张)。
package.json 的 version,装了新版才会变)。stickers.json 才是它们的入口)。backgrounds-custom/)、点缩略图把它定为起点、自己传的那几张角上有「删除」、还有自动轮换(间隔 15–600 秒)与淡化(0.2–0.85,越大照片越淡、正文越清楚)、「下一张」只是当场翻页(不写状态文件)。随包的图丢进 Background/,加删图刷新页面即可认。记住)、列表按时间倒序显示(正文 + 日期 + 标签 + 删除),最下面「清空全部」会先确认一次。平时不用管它:模型觉得该记的会自己调 cyrene_memory_add 写进来,需要翻旧事时自己调 cyrene_memory_search。改的是什么 · 生效方式 人格底稿 / 开关 / 背景设定(在面板里保存) · 立刻,之后每一轮对话都按新设定;旧对话也一样 lib/client.js(样式表、面板本身) · 刷新页面(F5)——客户端半是页面加载时读的 bundle lib/index.js(路由、section 逻辑) · 重载插件行或重启 DSH——宿主半在 DSH 启动时载入后一直缓存 cordis.patch.yml · 重载插件行或重启 DSH hero.image / 欢迎语(状态文件或 DEFAULT_HERO) · 重载插件行或重启 DSH(宿主半启动时读一次) Background/ 里加图 / 删图 / 改名 · 刷新页面即可——页面每次都会带 reload=1 让宿主重扫那个目录,不需要动宿主半 面板里上传 / 删除背景图、切浅色 / 深色外观 · 立刻——上传与删除走插件自己的路由,外观走宿主主题服务,都和点主题开关同一条路 手工改状态文件里的 background · 重载插件行或重启 DSH 面板里加 / 删记忆、开关长期记忆 · 立刻——走插件自己的 /cyrene/memory 路由,下一轮对话就带新记忆 手工改 cyrene-memory.json · 重载插件行或重启 DSH
本插件以 link: 方式装在工作区里,所以改文件不需要重装,但上面这张表仍然成立。 如果保存时看到「这个页面还是旧版本,请按 F5 刷新后重试」,就是页面里的客户端半比宿主的 rev 旧了一代:刷新一下即可。
{ "persona": "……人格提示词……", "enabled": true, "theme": true, "stickers": true,
"stickerSize": 96, "customStickers": [], "customBackgrounds": [],
"hero": { "image": "meme/俏皮眨眼.png", "title": "让昔涟来帮帮你吧🎵" },
"background": { "enabled": true, "current": "", "rotate": false, "interval": 90, "dim": 0.6 },
"rev": 3 }persona:空字符串表示不注入人格。enabled:人格总开关。theme:粉白主题开关(关掉只留人格,UI 回到原样)。stickers:表情包开关(关掉后那段「【表情包】」说明不注入)。stickerSize:聊天里贴图的最大边长(px),落盘前夹在 48–320 之间。它不跟主题开关走——贴图大小是插件设定,关掉主题也照样生效。customStickers:面板里自添加的表情包({ id, file, label, when },file 只是 stickers-custom/ 下的文件名,越界或类型不认识的一律丢弃)。hero:新会话欢迎页的形象路径(相对插件目录,只认包内的图片)与欢迎语。形象读不出来(文件不在 / 类型不认识 / 超过 8MB)时 GET /cyrene/state 里的 hero.ready 是 false,页面就只显示欢迎语、不挂坏图;改完要重载插件行或重启 DSH 才认(宿主半只在启动时读一次磁盘)。background:整页背景。enabled 是总开关(还要求主题开关也是开着的),current 是起点文件名(空字符串=按目录顺序的第一张;填的名字不在清单里也不会出事,只是被当作没指定),rotate 是自动轮换,interval 是轮换间隔秒数(落盘前夹在 15–3600),dim 是色纱浓度(夹在 0.2–0.85,越大越淡)。非字符串、带 / ` .. 的 current` 一律不写。customBackgrounds:面板里上传的背景图({ id, file, label },file 只是 backgrounds-custom/ 下的文件名)。扫描目录时把这些图并进清单、custom: true,显示名用 label;删掉一条时同时清掉指向它的 current。rev:写入版本号,每次成功写入 +1。手工编辑这个文件时把 rev 一起调大(或直接删掉这行)即可;留着小值会让还开着旧版本的页面写不进来(那正是它存在的意义)。出问题时最快的还原手段:删掉这个文件并重载插件行,人格回到内置默认、主题按默认值。
长期记忆不在这里:它单独存在同一个目录下的 cyrene-memory.json({ enabled, rev, items: [{ id, text, tags, created, importance }] }),删掉它就等于清空记忆,主题设定一点不受影响;反过来删设定文件也不会把记忆删掉。记忆有自己的 rev,和这个文件的 rev 互不干扰。
素材(11 张,meme/)与清单(stickers.json)都在包里,清单是唯一真相:
{ "version": 1, "dir": "meme", "maxPerTurn": 1,
"items": [ { "id": "happy", "label": "超开心", "file": "meme/超开心.png",
"when": "事情成了、她真的替你高兴的时候", "note": "……" } ] }dir 相对包根,file 相对插件目录;只认包内的文件(越界的路径会进 missing,不会被执行)。超开心.png、点赞.jpeg……)——文件名跟着 label 走,原始的时间戳/哈希名已经用 node tools/rename-stickers.mjs 改掉了(先 --dry 看一眼)。加新图时把文件丢进 meme/、在清单里按同样的命名写好 file,再跑一次这个脚本就能对齐;改名失败会整批回滚,不会留下"文件改了、清单没改"的半截状态。png/jpg/jpeg/gif/webp/avif 里,也会进 missing 并在 GET /cyrene/stickers 的响应里报出来。模型是怎么"知道"这些图的:宿主半把清单拼成一段文字,追加在人格底稿之后(同一段系统提示词 section,所以在同一个 enabled 开关之下、独立于 stickers 开关)。里面每张图都是一行可以直接复制的 Markdown:
- 超开心|事情成了、她真的替你高兴的时候 → 规矩也写在里面:最多 `maxPerTurn` 张、只在情绪真的到了的时候贴、代码/命令/报错/技术结论旁边不贴、拿不准就不贴,而且必须单独占一行(dsh-client-ui-chat 只把独立成行的图片渲染成预览,混在句子里的图片路径会原样显示)。
图片本身由客户端半的预览网格与 GET /cyrene/sticker/ 提供:页面是 http 来源,`file://` 的本地路径在页面里根本加载不出来,所以缩略图必须走宿主半的字节路由(宿主半读磁盘、带 Content-Type/Content-Length/ETag)。而模型粘贴在回复里的那一行用的是文件绝对路径——那是聊天面板自己的 Markdown 图片解析(按"当前查看的工作区"解析本地路径),跟浏览器同源策略无关。
设置页那一行「✦ 表情包管理」只是个入口,点「打开管理器」会弹出一整页覆盖层(挂 document.body 上、Escape 或右上「关闭」退出;插件没有自己的路由,这是"跳转到对应页面"在本环境里的等价物)。页面里:填「名字 / 什么时候贴」→ 选文件 → 「添加」。上传路径是 POST /cyrene/stickers,body 是 { rev, name, label, when, data },data 是浏览器 FileReader 读出来的 data:image/*;base64,…:
max-height + overflow:hidden、宽 min(560px, calc(100vw - 48px));里面 .cyre-manager-scroll 才是滚动层(flex:1 1 auto;min-height:0);卡片锁 min-width:0;max-width:100%;overflow:hidden,长名字只换行不撑格子。test/client.smoke.mjs 里有专门盯这条的正则断言。MAX_ASSET_BYTES(8MB)回 400 too-large;data 不是 data URL 回 400 bad-image;mime 与文件名后缀都不在 png/jpg/jpeg/gif/webp/avif 里回 400 bad-type。stickers-custom/ → 条目推进 state.customStickers → rev+1 落盘 → 重新解析清单 → 重挂人格 section,所以刚加的图这一轮就能贴。响应回 { state, stickers } 两个快照,面板一次刷新到位。POST /cyrene/stickers/remove(body { rev, id }),按 id 找条目、从磁盘 rm 文件、同样 rev+1 并重挂人格;id 认不出来(包括 ../../… 这种写法)一律 404 not-found,不会碰到目录外的任何东西。stickers-custom/(已 gitignore,属运行时数据)。聊天里的贴图就是 Markdown 图片,DOM 是 button[class*="_imageButton"] > img[class*="_image"],宿主自己那条 [class*="_imageButton"] [class*="_image"]{max-width:min(100%,640px);max-height:360px}。我们不跟它抢源码,而是按 `alt` 认自己的图:
body[data-dsh-cyrene]{--cyre-sticker-size:96px}
body[data-dsh-cyrene] img[alt^="昔涟·"]{max-width:var(--cyre-sticker-size);max-height:var(--cyre-sticker-size);width:auto;height:auto}alt 而不是 src:alt 是「昔涟·」,由 Markdown 原样保留,src 会被宿主按工作区重写。特异度 (0,2,2) 高于宿主的 (0,2,0),不需要 !important。--cyre-sticker-size 由客户端半在 applySkin() 里按 state.stickerSize 写到 body 上;拖动滑杆时先就地改变量做预览、松手(change)才 save({ stickerSize })。素材就放在 Background/(包内目录,图自己丢进去就行)。宿主半把它当唯一真相:启动时扫一遍,另有 GET /cyrene/backgrounds(带 ?reload=1 强制重扫)回清单、GET /cyrene/background/ 回字节——只有清单里有的名字才发,其余一律 404。它跟表情包共用同一套响应头(ETag/304、Cache-Control: private, max-age=300、nosniff),单张上限 24MB,扩展名按 ASSET_TYPES 认。
层 · 是什么 · 什么时候在 渐变底 · body::before(粉白 / 紫夜渐变)+ body::after(顶部珠光) · 主题自带的,一直都在 .cyre-bg · 两层图片交叉淡入 + 一层色纱 · 有图时才铺满视口,没图整层 display:none,渐变照旧露出来 对话内容 · 有图时把用户气泡的底色(--dsw-specific-bubble)调厚一点,再给正文(段落 / 列表 / 表格)一圈很淡的光晕 · 背景图开着时才挂上;整列本身不刷白,照片直接透上来
.cyre-bg 是 body 的真实子元素,position:fixed;inset:0;z-index:-1;pointer-events:none:不占布局、不接指针,任何点击都到不了它;树序排在 body::before 之后,所以有图时它盖住渐变,body::after 那层珠光仍在最上面。
三件事同时成立才会铺出来:主题开关开着、管理器里选的是「背景图」(不是「简约(无图)」)、Background/ 里至少有一张能用的图。所以关掉主题就等于连同背景图一起收掉(渐变底回来),不用担心深色/浅色主题和背景图色调打架。
早先的版本给 [data-conversation-scroll] 铺了一层 90% 的白纸来"把对话和背景分开",结果照片基本看不见了——现在整列保持透明,只对对话本身做处理:有图时把用户气泡的底色调厚一点,再给正文一圈很淡的光晕(段落 / 列表 / 表格;预格式化的代码块自带深色底,不参与)。照片该看见就看见,字也压得住。
另外,绝不能给 `[data-conversation-scroll]` 加 `backdrop-filter`。 输入框卡片是它的后代,而发送键那个 Tooltip 没开 portal、气泡是 position:fixed——滚动容器一旦因为 backdrop-filter 成为 fixed 后代的包含块,就会重新触发「鼠标停在发送键上界面闪烁」那个 bug。要是觉得照片太抢眼,把「淡化」往上拖——那层色纱才是真正管"照片有多淡"的旋钮。
rotate 打开后,客户端半按 interval 秒在页面里换下一张,不写状态文件——current 是你点的那张起点,轮换只管"这一会儿看哪张"。所以刷新页面后一定回到你选的那张,而不是上次轮换停在哪。「下一张」同理,只是当场翻页。dim 越大色纱越厚:浅色主题偏粉白、深色主题偏紫夜,免得正文压在亮部或暗部上读不动。
两张自带素材正好一明一暗:the-longest-night.jpeg 是浅色主视觉(配浅色主题),night-reading.jpeg 是深蓝夜景(配深色主题)。文件名只是排序用的,想换成自己的图就直接丢进 Background/(认 png/jpg/jpeg/gif/webp/avif,单张上限 24 MB),刷新页面就能在缩略图条里看到。
面板里的「背景图管理 → 打开管理器」可以上传自己的照片(png / jpg / gif / webp / avif,单张 ≤24MB):图片转成 data URL 交给 POST /cyrene/backgrounds,宿主半写进 backgrounds-custom/(包内目录,这样字节路由才服务得到它;.gitignore 忽略,属运行时数据),再把 { id, file, label } 记进状态文件的 customBackgrounds。扫描目录时这两处的图并成同一份清单:同名以 `Background/` 为准,自传的那几条带 custom: true、显示名用你起的名。删除只对自传的生效(POST /cyrene/backgrounds/remove,随包的那几张永远删不掉);删掉的正好是起点时,这个引用会被一起清空,不留悬空 id。上限按 24MB 算,所以请求体上限放宽到 34MB(base64 会涨 ~37%)。
管理器最上面是「浅色 / 深色」两档。点了走的是宿主自己的主题服务(ctx.get('theme').setTheme('light' | 'dark'),和点界面右上角那套主题开关同一条路),所以切的是整个应用、偏好也由宿主落盘——插件不自己记一份,避免两处状态打架。当前是哪一档从 body 上的 data-ds-dark-theme 读(宿主切深浅色都会先落这个标记),管理器里的高亮和「现在是深色 / 浅色模式」那行字跟着走。外壳没有暴露主题服务时退而直接增删同一个 body 属性,界面照常可用,只是偏好不会被记住。
深浅色和背景图不绑定:切外观不换图,选图也不动外观——外观归外观、背景归背景,各按各的喜好来。所以这里只放"切深浅色"这一个开关,不再有"给深色指定一张图"那种设定。
管理器里每张卡片仍然只有缩略图(点它选定点)和自传项上的「删除」。
两处针对性处理:
url(backgroundSrc(),相对 document.baseURI 解析)——手机外壳的页面基址和桌面不一样,硬拼 /cyrene/background/... 在那种环境里可能落到别的路径上。另外每换一张会用一个 Image 探针试取字节(CSS 背景的加载失败不会冒到 JS),取不到就写一条 背景图取不到: 的 warn 到控制台,排查时不用靠猜。@media (max-width: 620px) 里把欢迎页那一行改成可换行(flex-wrap:wrap)、贴纸收小(height:52px;max-width:64px)、艺术字改成 clamp(19px, 6.2vw, 26px) 且允许 overflow-wrap:anywhere;浮层与管理器改成 calc(100vw - 20px) 宽。@media (max-height: 480px) 再把贴纸压一档,横屏也不至于把输入框顶出屏幕。这两处不跟主题开关走(主题关掉也还在,因为它们是"昔涟在这儿"的标识,不是皮肤的一部分)。
那个"新会话里的鲸鱼"是 @deepseek-ai/dsh-client-ui-conversation 里 HeroShell 的内置兜底:鱼来自槽位 conversation.hero.brand.mark 的 fallback,旁边的内置文案是 i18n 键 hero.headline(zh「探索未至之境」)。
这个槽是 kind `single`、scope `root` 的单占位槽,而"槽位空着"这件事只在纯 `dsh web` 构建里成立:官方 brand 包确实不占它,但宿主不一定让它空着——DSH Desktop 0.10.0 的 dsh-desktop-client-ui 就在 priority 0 注册了它(内容就是那条鱼)。single 槽不允许同优先级重复注册,撞上就 throw:
Error: single slot "conversation.hero.brand.mark" already has a registration at priority 0
(registered by cf) — register at a different priority to shadow it (lowest renders)所以本包注册时带 priority: -1:p0 那条鱼被盖住;槽位真空着时 -1 也是唯一的一个,照常渲染。别把这行改回默认优先级——在桌面版上会直接和宿主撞车,后果见「已知坑」第一条。
这一行的 DOM 随宿主构建改过两次,选择器要同时兼容(类名哈希前缀每次构建都会变,所以一律按 [class*="_xxx"] 匹配):
div[class*="_headline"](flex 行,gap:12px 10px)里依次是槽位外壳 span[class*="_fishHitbox"](flex:none)和 span[class*="_titleGroup"];内置文案是 titleGroup 里一个没有 class 的 span,带 class 的那个是「预览版」徽标。div[class*="_headline"] 是 grid-template-columns:34px auto auto 的 grid(鱼 / 文案 / 徽标),内置文案带 _headlineText。客户端半注册 conversation.hero.brand.mark(单占位契约,priority: -1 盖住宿主在 p0 的那条鱼;不带 id/order),渲染:
img.cyre-hero-mark:72px 高的贴纸形象,src 指向宿主半的 GET /cyrene/hero——页面是 http 来源,`file://` 在这里加载不出来,所以字节必须由宿主半发(ETag/304、Cache-Control: private, max-age=300、nosniff)。素材直接用 meme/ 里本来就有透明通道的 Q 版立绘(DEFAULT_HERO.image,默认 meme/俏皮眨眼.png),页面这边什么都不再画:没有 border、没有底盘、没有 border-radius、没有 mask-image,抠好的轮廓自己就是边界,和背景之间没有过渡痕迹——就是"把图单独扣出来贴上去"。唯一加的是一层 drop-shadow(跟着 alpha 轮廓走,不是方框影),让她像贴纸一样落在页面上;不想要就删掉那一行。meme/IMG_20260612_155829.jpg 那张特写里,皮肤和头发高光的亮度跟白底差不多(floor>=240 且 max-min<=14 也拦不住),洪水填充要么把脸挖出洞、要么留着方框;而且她的头发在左 / 右 / 下三条边都跑出画外,抠完那三条边是齐的。要换这类素材得手工修,或者用 tools/hero-cutout.py 调参数慢慢试。ready:false 时这一格直接不渲染,只留欢迎语。.cyre-hero 上写着 pointer-events:none(点不响,连 :hover 都不会触发)、user-select:none + -webkit-user-select:none(文字选不中)、cursor:default(不出现手型光标),img 另加 -webkit-user-drag:none 与 React 的 draggable:false(浏览器里拖不出影子)。[class*="_headline"]:has(.cyre-hero) 这一层也同样设了 pointer-events:none + user-select:none,免得宿主那层槽位外壳把点击接走。这几条都会被子元素继承。span.cyre-hero-title:艺术字欢迎语。字体栈 FZShuTi(方正舒体)/ YouYuan(幼圆)/ STHupo(华文琥珀),末尾一定收在 Microsoft YaHei UI / 微软雅黑 / system-ui / sans-serif——不留衬线兜底,免得某个字重的字体没命中时掉回宋体那一类。文字用 background-clip:text 走粉 → 白的渐变(linear-gradient(168deg,#ff7cbb,#ffa9d0 34%,#ffd8ea 68%,#fff7fc)),白尾部分在白底上会飘,所以描一圈 1.1px 的粉边(-webkit-text-stroke:1.1px #e8699f)+ 一层浅粉投影把它托住;不支持渐变裁字时有纯色兜底(@supports not (background-clip:text))。末尾那个音符(♪ ♫ 🎵 🎶)会被拆成单独的 span.cyre-hero-note 轻轻晃动,prefers-reduced-motion: reduce 时不动。python tools/hero-preview.py 会在粉白渐变底上用实际配色画出「让昔涟来帮帮你吧♪」的六个候选字体(幼圆 / 华文琥珀 / 方正舒体 / 华文新魏 / 华文行楷 / 微软雅黑),输出 tools/hero-font-preview.png。改字栈只要改 .cyre-hero-title 的 font-family。python tools/hero-mock.py [素材路径] 按页面的真实渐变(body::before 那五层)与 2 倍尺寸画出「贴纸 + 艺术字」整行,形象高度做成 64 / 72 / 88 三档,输出 tools/hero-mock.png。现在 CSS 用的是 72px。[class*="_headline"]:has(.cyre-hero){grid-template-columns:auto} /* grid 那版:别把 64px 头像压进 34px 那格 */
[class*="_headline"]:has(.cyre-hero) [class*="_headlineText"],
[class*="_headline"]:has(.cyre-hero) [class*="_titleGroup"] > span:not([class]){display:none}
[class*="_headline"]:has(.cyre-hero) [class*="_previewBadge"]{display:none}