DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

YukimuraSol /

YukimuraSol/dsh-ui-block

Verified

Page block + element chip: render a ::dsh-ui <name> [res=WxH]:: line's ui-blocks file in a sandboxed canvas, and render

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@ce8e4574

dsh-ui-block — 页面块(Page Block)

2026-10-24:触发方式换成独立一行 ::dsh-ui <name>::。

  • 新写法(消息里单独一行,前后可有空白,不带反引号、不在围栏里、不带别的字): ::dsh-ui 名字:: 或 ::dsh-ui 名字 res=820x510::。该行会被隐藏,画布插在它原位; res= 决定画布尺寸,缺省 1440x900。
  • 旧写法(裸围栏 + 块体首行 dsh-ui ref: ui-blocks/<name>.uiblock)已彻底删除, 代码里没有任何兜底识别路径;scripts/check-meta.mjs、scripts/check-selectors.mjs 随之删除。
  • 自检:node self-check-uiblock-directive.mjs(当前 46 条断言,含"旧写法不命中"、 "代码块里不命中"、"非法 res 被拒",以及下一条的补丁链/时刻还原)。
  • 缩放、画布内选区、拖拽、读文件、报错处理未改。

2026-10-24 第二轮(用户拍板"就要和代码块一模一样"):外壳改回真·代码块—— 座位里用 React 挂载聊天自己的 CodeBlock(require('@deepseek-ai/dsh-client-ui-primitives')), 外框、底色、圆角、横栏、「复制」图标与 tooltip 全是同一个组件的同一份样式,不是仿制品。 横栏语言标签固定是 dsh-ui(走 toolbarLabels.codeLabel,不改写 DOM);复制按钮复制的是 ui-blocks/<name>.uiblock(code 传的就是路径,<pre> 只是 display:none 藏起来)。 下面「围栏块 / 横栏钩子」记的是旧围栏实现的来龙去脉,DOM 钩子与现在这套一致。

2026-10-25:引用按消息时刻还原(写法一个字都没改)。

  • 引用语法不变,仍是 ::dsh-ui <name>:: / ::dsh-ui <name> res=WxH::,本页其它章节 讲的解析规则照旧。
  • 一条消息里的引用显示的是那条消息第一次渲染时这一版块;之后改块只影响之后的新消息。 刷新页面也还是各显示各的版本(钉子存在宿主侧的 ui-blocks/.pins.json,不在浏览器内存里)。
  • 历史是补丁链,不是副本:ui-blocks/.history/<名字>/base.uiblock 只有首次见到的那一份基线, log.json 每行一条 {at, patch}(行级 hunk,自己写的 Myers,无依赖)。
  • 宿主路由:?at=<epoch毫秒> 还原那一刻的内容;?pin=<对话节点键>&i=<第几条> 首次请求钉住 "现在",之后沿用;两个都不带仍是最新(旧客户端兼容)。
  • 钉住的引用不再 1.5 秒重读(内容不会再变);读失败仍然重试。
  • 找不到 [data-chat-node-key] 祖先时(草稿预览、非对话场景)退回旧行为:不带 pin,显示最新。
  • 升级前已有的历史消息:首次渲染时才钉住,钉到的是"现在",所以它们仍显示最新——这是缺信息,补不回来。
  • 自检:node self-check-uiblock-directive.mjs 覆盖补丁链还原(sha 对齐)、?at= 取中间版本、 pin 幂等、以及"历史目录里没有整份副本"。

下面从「围栏块 / 横栏钩子」讲起的段落是已退役实现的历史记录(.md-code-block 三个钩子、A 路径"同壳替换"等),保留只为排查旧问题;当前代码不再从围栏里取内容, 但重新借用代码块组件当外壳(见上一段)。

v0.4.0(2026-10-10)画布内选区:横栏加一个选区按钮(分辨率按钮右边,只有 13×13 SVG 图标、无文字)。点亮后可在画布内部点选元素,选中的元素变成 element«…» chip 直接插进 composer。交互与浏览器选区插件 element-picker 对齐:鼠标移动高亮 / 滚轮切层 / ↑↓←→ 层导航 / 点击选中 / 再点一次图标退出。Enter 与 Esc 都不绑(选区不越界占用全局键)。

两份独立副本(2026-10-09 实测澄清):3080 的 profiles\web\node_modules\dsh-ui-block junction 指向 D:\dsh-plugins\dsh-ui-block;4000 的 junction 指向 D:\dsh-fallback\plugins\dsh-ui-block —— 后者是一个独立的真实目录(fsutil reparsepoint query 报 4390「不是重解析点」),两边不共享。改代码必须两处都同步,否则只有一边生效。改完 4000 重启即生效,3080 要用户自己重启。

A 路径:同壳替换(不碰任何聊天包)

用 MutationObserver 找到消息流里的围栏代码块 DOM,把内容节点换成 iframe,并往横栏右侧注入控件。没有任何包补丁要回滚,卸载 = 删插件目录 + 还原 profile 两处。

只依赖三个稳定钩子(每 1.5s 自愈一次)

钩子 来源 作用
.md-code-block CodeBlock 显式加的字面 class(不是哈希类名) 找块
[data-code-block-content] primitives 里注明 "consumers may turn the stable content node into a viewport" 内容节点
[data-code-block-banner] 横栏 注入分辨率控件 + 选区按钮(缩放/写回控件已删;0.2.3 起隐藏外壳自动换行开关)

node scripts/check-selectors.mjs 会把 client.js 里的 SEL_* 常量拿去 4000 自己的产物语料里对,缺一个就 exit 1。页面里另有运行时断言:window.__dshPageBlock.stats.selectorMiss(>0 说明钩子变了)。

尺寸模型(v2:地图视口,三个独立状态)

把块当成一扇看画布的窗户:窗宽由聊天栏决定,画布(iframe)在窗内做地图式变换。

  • 画布分辨率:res=1440x900 声明,改它 = 改 iframe 真实的 CSS 视口 ⇒ 内容重新排版(media query 生效)。横栏控件,改法与 v1 相同。
  • 缩放:光标悬在画布上直接滚轮,朝光标点缩放(newOffset = cursor − (cursor − offset) × newZ/oldZ),只加 transform: translate+scale ⇒ 永不重排;钳制 20%–400%;双击画布复位(适应宽度一次)。横栏不显示缩放控件与百分比。
  • 外框高度(窗户高度):h=300 声明默认值;缺省/非法回退 min(画布高×初始适应, 480px)。拖下缘把手调整(120px–视口高),双击把手复位。横栏不显示高度。

画布比窗小 → 居中;比窗大 → 拖拽平移(grab 光标;拖拽/调高的 move+up 挂在 window 上,不怕光标出块、不怕聊天壳的浮层盖住画布),没有原生滚动条。iframe 自身 pointer-events:none,交互全由插件自有的透明窗体层接管。

触发与元信息(v0.3.0)

触发与常量同在围栏块体的第一行:第一行以字面 dsh-ui 开头,后面跟可选的空格分隔常量。正文 = 第二行起(dsh-ui 那行永远不进沙箱)。

dsh-ui res=1440x900 h=300 title="登录弹窗"
<div class="pb pb-center">…</div>

dsh-ui 单独一行 = 全部用默认(1440×900):

dsh-ui
<div class="pb pb-center">…</div>

未加引号的写法同样有效,多词值一路读到下一个 key=:

dsh-ui res=1440x900 h=760 title=首页 · 改版 (非对称 · 分级卡片)
<div class="pb pb-center">…</div>
  • 触发:第一行匹配 /^dsh-ui(?:\s|$)/(trim() 之后)。dsh-ui 必须是完整的首词 ⇒ dsh-uires=1、dsh-ui2 不触发。
  • 围栏语言无关:写 dsh-ui`、````(无语言)还是 text 完全一样(宿主只印它认得的语言名,dsh-ui` 会显示成"代码块"——见下)。
  • 未加引号的多词值现在也能解析:值的边界是下一个合法键(^[a-z][a-z0-9-]*=),所以 res=1440x900 h=760 title=首页 · 改版 (非对称 · 分级卡片) 解析为 {res:"1440x900", h:"760", title:"首页 · 改版 (非对称 · 分级卡片)"}。引号形式 title="a b" / title='a b' 行为不变(引号值在闭合引号处结束,后面不再续接)。
  • 坏 token 跳过,不再连坐:·、改版、(非对称、空值的 key=、未闭合引号这类 token 只被丢弃,不影响触发(触发只看 dsh-ui 前缀);被丢弃的只是那个常量的值。
  • 只有 title 的值会续接多词;res / h / width / height / zoom 仍是单 token ⇒ res=1440x900 随便 垃圾 取 1440x900、其余丢弃。未知键(x=1)保留在 meta 上但没人读。一个常量都没有(dsh-ui 单独一行)也照常渲染,只是全用默认。
  • 诊断(只进控制台,不影响渲染):迁移诊断 —— 第一行像旧 meta 行但没有 dsh-ui 前缀时(looksLikeMetaLine() 命中,或该行能解析出至少一个已知键),window.__dshPageBlock.stats.metaBail 计数 +1、stats.lastMetaBail = {line, reason:'missing-dsh-ui-prefix'},并 console.warn 一行提示加前缀(同一条只 warn 一次,不随 1.5s 扫描刷屏)。这类块不渲染。普通散文/代码不打印任何东西。res 非法不走这条路,一行也不打印。
  • res 缺失或非法 ⇒ 同一条路(2026-10-04 契约变更,2026-10-09 复核仍成立):res=abc、res=default、res=1180、width=x 与完全不写 res 等价,统一用默认 1440×900 正常渲染,不再退回普通代码块,也不加任何 UI 提示(用户原话:「就直接用那个默认的吧」)。旧版"声明了却解析不出来 ⇒ 整块退普通代码块"的 resBails() 闸门已删除,那条硬契约作废。
  • h 缺省或非法(h=abc)⇒ 不炸块,回退高度 min(画布高×初始适应, 480px)。
  • 缺 res(或 res 非法,同上)但有其它键 ⇒ 默认 1440×900,横栏沿用既有那条"未声明,使用默认"的显示;非法值不另做文案。
  • html 别名:v0.3.0 已删除(语料实测 0 次使用)。现在 ```html+
    ` 的普通 HTML 示例不渲染。
  • 旧写法(首行没有 dsh-ui 前缀)一律不渲染,只留一条 missing-dsh-ui-prefix warn。

画布内选区(v0.4.0)

按钮

分辨率按钮右边,只有 SVG 图标、无文字,13×13,箭头形状 M2.5 2.5l5 11 1.8-4.2 4.2-1.8z。

项 未点亮 点亮
内容 只有 SVG 同
图标尺寸 13×13 同
底色 透明 透明(不变)
边框颜色 var(--dsw-alias-border-l2) #22d3ee(青蓝)
线条颜色 var(--dsw-alias-label-secondary) #22d3ee(青蓝)
线宽 1.3 1.5
title 在画布内选取元素 同

点亮 = 图标线条 + 圆角边框都变青蓝,底色仍透明(整个按钮的"线"高亮,不加实心背景)。

⚠️ 不要用 var(--dsw-alias-brand-primary) 做高亮色。实测它映射到 --dsw-static-neutral-bluish-*(灰蓝色),深色主题下观感接近白色 —— 这就是"高亮像白色"的原因。要真青蓝只能写死色值,本插件用 #22d3ee。

交互(与 element-picker 同一套心智)

操作 行为
鼠标移动 高亮指针下的元素(蓝框)
滚轮 在层叠栈里上下切换(穿透遮挡,选到被盖住的元素);已 preventDefault,不会同时触发画布缩放
↑ 选父元素
↓ 选第一个子元素
← / → 选前/后兄弟
点击 确认选中 → token 直接进 composer
Enter 不绑(无意义,留给页面)
Esc 不绑(交给上层)
结束选区 再点一次选区图标

层叠栈用 doc.elementsFromPoint(x, y)(iframe 内部的 doc),并按 element-picker 的写法补上 pointer-events:none 子树漏掉的祖先。

方向键前提:keydown 绑在 iframe 的 contentWindow 上,所以点亮时插件会 frame.focus({preventScroll:true})、并在 pointerdown 里补一次 win.focus()(preventDefault 顺带压掉了本该转移焦点的兼容 mousedown)。焦点在画布内时方向键才收得到。

一次只属于一个块:点亮第二个块的选区会自动熄灭第一个(模块级 armedPick),同页多个页面块不会互相抢指针。

插入路径(乙:直接插 composer,不走剪贴板)

  1. 注册一个 conversation.input.overlay 座位,id dsh-ui-block-picker(与 dsh-element-chip 的 dsh-element-chip 不同;该座位是 additive list,两个共存),组件返回 null,只用来接住 props.sessionId 与 props.inputActions。
  2. 选中时取 inputActions.captureInsertion() 拿带 draftRev 的插入 span,然后 emit(sessionId, 'slash/input-insert-reference', { reference: {source:'element', ref:token, label:tag, clipboardText:token}, span })。
  3. source 复用 'element' —— chip 的渲染、序列化、复制全部复用 dsh-element-chip 的既有实现,零重复。
  4. 座位注册走 ctx.get('slots')(不是 ctx.slots),并在 1.5s sweep 里惰性重试:slots 服务缺席时页面块照常工作,只是选区会提示"插入失败",不会因为缺服务整块停摆。

token(3 行,复用 element-picker 格式与截断规则)

element«DOM Path: #lbtn
Position: top=0px, left=0px, width=72px, height=27px
HTML Element: <button id="lbtn" class="cta" data-role="primary">实时按钮</button>»
  • 选择器:有 #id 用 #id,否则最多 4 层 tag.class:nth-of-type。
  • 位置:getBoundingClientRect(),在画布自己的坐标系里(即 res= 声明的那个逻辑视口)。
  • HTML 行:只印元素自己的属性(≤8,多出的记 ... +N more)与自己的文本(≤80 字符),子节点标记不展开。
  • « / » / 反斜杠按 element-picker 的规则转义,保证第一个裸闭合书名号永远是 token 结尾。
  • Styles 行已删(与 element-picker 同步,token 现在是 3 行)。

沙箱(v0.4.0 变更)

  • sandbox="allow-same-origin":只有 allow-same-origin,绝不加 allow-scripts(两者同时 = 沙箱失效,规范明示)。
    • 为什么加:画布选区要读 iframe 内部的 DOM(contentDocument + elementsFromPoint);没有同源就是 null,选区做不了。
    • 代价:父页面能访问草稿 DOM(草稿能用 CSS 做视觉欺骗,但它本来就能)。
    • 没有放开的:<script> 仍不执行(window.__ran === undefined,实测);CSP 仍生效(外链 <img> naturalWidth === 0,实测);iframe 读父页面仍失败。三条都在 tmp\dshui-canvas-picker-20261010 的验证里逐条断言过。
  • 旧值 sandbox=""(v0.3.0 及以前)已不再使用。
  • srcdoc 里 CSP default-src none; style-src unsafe-inline; img-src data: blob:; font-src data:; media-src data:; form-action none; base-uri none,referrerpolicy="no-referrer"。
  • 主题令牌:读宿主 document.body / documentElement 上的全部 --dsw-*(0.1.7 时 384 个,0.2.0-rc.2 实测 413 个)注入 srcdoc 的 :root,并写 color-scheme。主题切换后 1.5s 内自动重绘(已验证:不刷新页面,深→浅会自己重绘)。

安装到 4000

文件清单:

  • D:\dsh-fallback\plugins\dsh-ui-block\package.json
  • D:\dsh-fallback\plugins\dsh-ui-block\cordis.patch.yml
  • D:\dsh-fallback\plugins\dsh-ui-block\index.js(宿主半边,空 apply)
  • D:\dsh-fallback\plugins\dsh-ui-block\client.js(全部实现)
  • D:\dsh-fallback\plugins\dsh-ui-block\scripts\check-selectors.mjs
  • D:\dsh-fallback\plugins\dsh-ui-block\scripts\check-meta.mjs(v3:把 client.js 的纯解析区抽进沙箱,50 断言盯 dsh-ui 触发 + res=/h=/title= 契约,含首词边界、正文剥离、未加引号多词 title、坏 token 跳过,以及 2026-10-04 契约 res 缺失/非法 ⇒ 默认 1440×900 不退块 + "旧 metaTrigger/htmlTrigger 已删除"的结构断言)
  • D:\dsh-fallback\plugins\dsh-ui-block\README.md

步骤:

  1. profile 的 package.json 加依赖 "dsh-ui-block": "link:D:/dsh-fallback/plugins/dsh-ui-block",并把 dsh-ui-block 追加进 dsh.profile.bundles。
  2. 在 D:\dsh-fallback\home\profiles\web 跑 pnpm install --config.auto-install-peers=false --store-dir D:\dsh-fallback\pnpm-store。
  3. 重启 4000(client bundle 图只在启动时合成):按端口杀 Get-NetTCPConnection -State Listen -LocalPort 4000 的 pid,再 Start-Process cmd.exe -ArgumentList /c,D:\dsh-fallback\start.cmd -WindowStyle Hidden。
  4. 浏览器刷新页面(URL 见启动日志最后一行)。

⚠️ 改完 profile package.json 后、pnpm install 之前,运行中的实例会热加载 manifest 并打印两条 skipping profile bundle "dsh-ui-block" —— 那是装的先后顺序,不是失败;重启后就没了。

卸载 / 回滚

  1. 从 profile package.json 删依赖与 bundles 行(同目录 .bak-pageblock-20260928 是原样备份)。
  2. pnpm install(同上)。
  3. 重启 4000。插件目录可留可删。

与计划的冲突(照实说明)

计划写的是条目行元信息:dsh-ui res=1440x900 title=…。这在本版做不到,证据:

  • dsh-client-ui-primitives/lib/index.js 的 renderCode() 只把 /^[\w-]+/.exec(info)(第一段)当 lang 传给 CodeBlock,其余 token 直接丢弃;
  • 横栏标签走 shell 产物里的 I6(lang) ? lang : labels.codeLabel,I6 是产物自带的语言表(含 html、ts、mermaid…,不含 dsh-ui),所以 dsh-ui 落到 codeLabel;
  • 2026-10-24 起直接把 toolbarLabels.codeLabel 传成 dsh-ui,于是横栏左边就印 dsh-ui(字号/颜色/位置与 bash 这类真语言名完全一致,实测 dx=22、dy=13 相同),不需要改写 DOM。

替代做法(本次实现):元信息写进块体第一行(仍是纯文本、可 diff、可复用)。升级路径:若要条目行形式,需要走 B 路径(读会话事件的原始 markdown / ctx.sessions)或 C 路径(改聊天包),另议。

已知上限(ponytail 记录)

  • 旧块全部失效(v0.3.0 的既定代价):首行没有 dsh-ui 前缀的历史块在重启后不再渲染,只留一条 warn。用户已接受(插件在测试期)。

  • 依赖 DOM 钩子:聊天包升级换钩子会失效 → 由 scripts/check-selectors.mjs + stats.selectorMiss 兜住,不静默坏掉。

  • React 重渲染(点复制/换行按钮)可能把我注入的节点摘掉 → 1.5s 常驻 sweep + MutationObserver 自愈;用户改过的分辨率/缩放存在 WeakMap 里不会丢。

  • 块被改写后不再触发时,已渲染的宿主不会自动拆除(只有内容变化的块才重解析)。升级路径:在 ensure() 的早退分支里比对 textKey 并 teardown。

  • res 写错不再报错(2026-10-04 用户决定):res=1180 这类漏写尺寸的值会安静地按默认 1440×900 渲染,界面无提示、控制台不打印,stats.metaBail 也不计(它只统计"像旧 meta 行但缺 dsh-ui 前缀"那种)。想知道生效分辨率只能看横栏上那个数字。升级路径:要恢复"写错就退普通块",把删掉的 resBails() 判定加回 ensure() 即可。

  • 多词续接只认 title:其它键(含未知键)的值仍是单个 token,所以 zoom= 之类的多词值用不上;要放开就改 parseMetaLine() 里那行 runOn 判断。

  • 画布上的点击被窗体层吞掉:iframe 默认 pointer-events:none(沙箱无脚本,静态稿无感)。v0.4.0 选区点亮时会临时改成 auto,退出时还原 —— 所以选区模式下的点击被选区接管,退出后仍由窗体层吞掉。阶段 L 上真交互时要改成转发点击(client.js 里 ponytail 注释标了位置)。

  • 滚轮被劫持:光标在画布上时滚轮属于缩放(用户已接受);滚聊天页移出块即可。选区点亮时滚轮改为切层(preventDefault,不会同时缩放)—— 这是刻意的,见上。

  • 选区焦点:点亮选区会把焦点移进 iframe(方向键需要);再点图标退出后焦点不主动还回 composer。

  • 选区依赖同源:sandbox 去掉 allow-same-origin 会让选区整体失效(contentDocument 变 null,会提示"画布还没准备好")。

  • 计划卡片内的块走同一套 primitives,预期同样生效,但本次未单独验证(试点范围是聊天消息)。

  • 交互回流(点击经 HTTP 桥 agent.steer)是计划的阶段 L,本次未做。沙箱自 v0.4.0 起是 allow-same-origin(见「沙箱」节)。

—/ 5

No ratings yet

Verified DSH bundle

Commit ce8e4574f08a

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout