← Back to home@tokai-teiodh

dsh-script-cards

剧本卡片面板:卡片排成方片网格和两级分支画布,改完写回卡片文件。 A card grid and a two-level branch canvas for a project's Markdown cards.

Stars
0
Language
JavaScript
Created
Sep 23, 2026
Updated
Oct 2, 2026

Introduction

dsh-script-cards

DSH 的剧本卡片面板:卡片排成方片网格和两级分支画布,改完写回文件。 A card grid and a two-level branch canvas for a project's Markdown cards in DeepSeek Harness; edits go straight back to the files.

剧本要跨很多次会话,人物、设定、待定问题、章节和情节分支散在一堆小文件里,总得有个 地方能一眼看完、顺手改。这个面板就是那个地方:方片网格看全貌,分支画布理章节和情节, 在浏览器里直接改,改完写回卡片文件(结构跟着项目走、能进 git,不是只存在浏览器里)。

上手怎么点,看 操作指南;下面是功能与实现的清单。


它做什么

两个视图,两族卡片,互不串门。

视图收哪些卡片
方片存档族:project character setting scene beat dialogue open decision reference
分支分支族:chapter node condition result
  • 方片:自适应圆角方片网格、按类型分组、标签单行横滑(滚动条平时一点都看不见, 滑动时才浮出一根细条,也没有 Windows 那两个三角箭头;滑动就是按住标签条左右拖, 所以左边那半页(卡片和标签)不能拖选文字,右边详情里的正文照样能选)、星标收藏 + 置顶、点开看正文。
  • 分支(两级菜单):上级排章节、下级排该章节里的情节,配一条浏览器式导航栏 (后退 / 前进 / 回到上级 / 刷新 + 面包屑地址栏)。双击章节卡片进下级。
  • 画布:滚轮缩放(以指针为中心,走 25/33/50/67/80/100/125/150/200% 这些档位)、 空白处左键拖动平移(中键、Alt+左键 也行;Shift+滚轮左右移、Alt+滚轮上下移); 从卡片边缘的圆点(16px 空心整圆、1px 细圈)拖出贝塞尔连线 —— 拉的时候其它卡片的 入口会点亮表示可以落,松手即连;拖动卡片时连线实时跟着走(不是松手才跳过去); 线宽统一 1.5px,箭头固定大小。连线的名字:线上平时什么都不显示,点一下那条线 就在线上浮出输入框(回车或点别处保存,Esc 放弃);起过名字的线名字一直挂着,点名字 接着改;右键那条线还有重命名 / 删除的菜单。标签和这个输入框的层级比卡片高, 线中点正好压在卡片上时也看得全。底部是 iOS 后台式的堆叠条, 把没上画的卡片拖进去或点一下落位。
  • 框选多张(只在画布上):右键拖出一个虚线框,框到的卡片一起选中;按住其中任何一张 拖,整组跟着走(相对位置不变)。在选中的卡片上点右键是批量菜单 —— 移出画布、染色、 删除这 N 张卡片文件,另有「选中这一层全部 / 取消选择」;键盘 Delete 整组移出画布、 Ctrl+A 全选这一层。右键没拖动的那一下还是原来的菜单。
  • 展开的卡片是浮动窗口:默认浮在画布上,不是盖住整块画布的全屏遮罩 —— 按住标题栏拖动、右下角手柄缩放(最小 320×220,拖不出画布),画布照样能拖能滚; 标题栏上两个开关:图钉(钉住之后切卡片、切页都不会自动收起,可以同时开好几个) 与全屏 / 浮动(全屏就是老的那套:背景变暗、画布锁定,大段写东西时更专注)。 窗口的位置和模式按卡片记在这台浏览器里,下次展开同一张回到原处。 指针在窗口里面时滚轮只滚这一页,不再缩放画布;指针在外面滚轮照旧缩放。
  • 每张卡片一份独立文档(只有章节与节点):展开窗口和卡片编辑弹窗里都有 「卡片 | 文档」/「字段 | 文档」页签。文档是独立文件(<档案目录>/文档/<卡片文件名>), 不是卡片正文 —— 正文照旧在「字段」页里改。等宽输入框、Ctrl+S 保存、状态行显示相对 路径与「已保存 HH:MM」;空文档不落盘(清空后保存会把文件删掉,没写过就不建文件); 有文档的卡片右上角有一个小角标。写不回磁盘时输入框变只读,并写明原因。
  • 文档里直接录台词 / 旁白:在文档的输入框里右键弹人物菜单 —— 全部人物 (取自方片页的 type: character 卡片;名字取标题里第一个括号之前那一段: 勿忘我(ワスレナグサ) → 勿忘我,人物多了可以在菜单里现筛)+「旁白」+「结束当前角色」。 选完就进入「该角色说话」状态(状态栏写着「正在写:勿忘我」),这一行的开头自动带前缀:
    • 台词:「勿忘我」:你终于来了。(中文直角引号 + 全角冒号,字面写进 .md)
    • 旁白:行首两个全角空格、不写人名(用户的要求:「要加旁白,但是左侧不显示人名」)
    • Enter 结束这个角色的话(换行回到普通行);Shift+Enter 只换行 —— 同一个角色连着说的时候直接往下写,不再重复一遍名字,行首补一串跟前缀同宽的全角 空格加一个冒号,冒号落在第一句那个冒号正下方、台词也对齐第一句
    • 光标停在行首标记里(「名字」:、续行那串对齐空格加冒号、旁白的两个全角空格) 按退格或 Delete,整块一起删,不会只掉一个冒号留下半截;没有标记的普通行照旧 一个字一个字删
    • 人物菜单按主角 / 配角分组(标签里写着 主角 真女主 攻略对象 的进主角组, 写着 配角 的进配角组,两类都不沾的进「其他」),上面照旧有筛选框
  • 文档写一半不会丢:输入框里改过的内容随手存在这台浏览器里(一张卡一条,最多 50 条), 切卡片、关窗口、关掉整个面板再回来内容都还在,状态行写「草稿 14:07 未保存」,旁边多一个 「放弃草稿」回到磁盘上那一份;保存成功、点「重载」之后就清掉。它不写进档案目录、 也不产生任何文件。
  • 方片页给人物卡设「角色色」:右键人物卡 → 挑一个色(也能清除)。色值写进卡片文件的 color,跟项目一起进 git;设完回文档里,那个人物的台词名字立刻用这个色。color 没有 就回落到标签里的 印象色#xxxxxx,两样都没有就照正文颜色。
  • 画布上摆引用卡(「存档卡」抽屉):分支画布边上的「存档卡」按钮打开一列抽屉 —— 上面是存档族的卡片(按类型分组),下面是「别的级别」(节点画布上给各个章节卡, 章节画布上给各章节的节点 / 条件 / 结果)。按住一张卡拖到画布上松手就摆一张引用: 左键右键都能拖,拖到画布外松手就是取消。引用不复制卡片、也不动卡片文件,位置记在 分支.json 的 refs 里(按「哪一层」分开存:顶层画布是 top,某个章节的节点画布是 那张章节卡片的键),关掉再打开还在原处。引用卡是虚线边 + 左下角一个「引用」角标、 没有连线圆点;能拖、能框选、能染色(不设就用角色色),双击只看只读详情, 要改内容回方片页。右键菜单只有「染色」和「从画布移开」;批量删除时引用卡只被移开, 不会删掉卡片文件;自动排列不碰引用卡。鼠标停在抽屉上滚轮滚抽屉,画布不跟着缩放。
  • 四种卡片形态(都是竖版的:章节 176×120、节点 156×112、条件 / 结果 144×86。 流程往右一层层排,卡片太扁就会拖成一条又长又窄的带子,跟竖着的侧栏不合身)
    • 章节卡片:章节序号 + 章节名 分两段显示,下面标签,最下简介;展开后是节点列表。 章节是容器,所以没有「故事内时间」这一项。
    • 节点卡片:上面节点名,时间另起一行(小字靠左,跟名字错开,窄卡片上不会互相压), 下方简介(简介钉在卡片下沿);展开后简介让位,依次排 角色 / 场景 / 内容 (读正文里的 ## 角色 ## 场景 ## 内容 小节)。
    • 分歧节点:在节点基础上,右侧一列选项(156 宽,整列上下居中于卡片,+ 选项 挂在 最后一行下面、不参与居中),每个选项各带一个可拖的出口;接通后标出走向(→), 展开后选项在下面单列一块,带一键增删。
    • 条件 / 结果卡片:只有两行(类型 + 内容);条件卡只有右出口,结果卡只有左入口, 右键可以互切。
  • 画布上的独立对象:文本框与矩形方框:右键空白处 →「新建文本框」/「新建矩形方框」。 两个都是独立对象、互不绑定(不是卡片的附属,也不跟着卡片走):文本框双击就地写字 (Esc 或点别处保存),矩形方框没有内容、只有底色(右键它调色),两个都能拖、 拉右下角改大小、框选、染色、编组。它们不是卡片:没有文件、不连线、不参与自动排列, 只记在 分支.json 的 objects 里(按「哪一层」分开存,跟引用卡同一套规则)。 文本框没有底色(只有文字和一圈淡虚线,下面的色块透得上来)。层序从下往上是: 有颜色的方框 → 文字 → 其他所有卡片 —— 方框和文本框都垫在卡片下面,卡片永远压在最上面、 点得到的也一定是卡片;三层互不穿插。
  • 编成一组(逻辑分组,不是画框):框选或 shift+右键选好几样之后,批量菜单里 「编成一组」;之后悬停在这一组附近,组底下会浮出一层淡背景(标出这一片是一个组), 这时候右键那块空白处就是整组菜单:整组复制一份、整组染色、整组移出画布、 删除组里的卡片文件、取消编组。拖动组里任何一个=整组一起走(相对位置不变, 按住 Alt 只拖你按住的那一个,方便把它拽出组);点某一张卡片本身仍然只走单卡菜单。 自动排列把一组当成一块,组内部的相对位置不会被拆开。组只记成员,写在 分支.json 的 groups 里;剩下不到两个人的组自动散掉。
  • shift+右键加选:shift + 右键点一张卡片=把它加进 / 移出当前选择,不弹菜单; 不带 shift 的右键照旧弹菜单。旧的右键拖框选照旧保留,框选现在也能框到引用卡与 文本框 / 方框。
  • 每张卡片可以有自己的大小:卡片右下角的手柄拖着就能改这张卡的宽高(上下限挡着), 右键菜单里「重置为默认大小」回到这一档的默认尺寸。改过的宽高写进 分支.json 里 这张卡片的记录,跟项目走;自动排列按实际宽高算占位 —— 一张卡变宽了,右边的层 会跟着让开。
  • 对齐要按住 Shift:拖卡片(或文本框 / 方框)的时候按住 Shift,边或中线跟别的卡片 差在 6px 以内就自动贴上去,并在那条线上画一根细辅助线 —— 线只画到和它对齐的那张卡片 为止、两端渐变淡掉,不横贯整块画布;拖右下角手柄改大小的时候也吸(只动宽高、不动位置); 整组拖动按整组包围盒吸。不按 Shift 就完全不吸、也不画线,卡片停在你放的那儿, 想自由摆就不会被悄悄拽走。拖到一半再按住 Shift 也算数,下一次鼠标移动就吸上。
  • 卡片之间不会重叠:拖卡片(或引用卡)松手时,只要压到了别的卡片,它就自己挪到最近的空位 —— 四个方向里挑挪得最少的那个(往左 / 往右 / 往上 / 往下贴住对方);同样近时优先挑整张 还留在画面里的那个方向(四条边都看,不然大卡片会被一头推出右下角、松手就没影了)。 拖动的时候可以压着走,改大小撑到别人身上也一样会让开(尺寸照你改的算,位置让开)。 画布上的文本框和方框不受这条约束:卡片可以压在它们上面,它们也不会把卡片推开。 画布上已经压着的老卡片不动,等你拖一下、或者点一次自动排列。
  • 手里那张在最上面:同一种卡片之间本来只按摆放顺序叠,拖起来的(或正在改大小的)那几张 —— 整组拖动就是整组 —— 会临时浮到所有卡片之上,松手回到原来的层。辅助线和连线名字仍然 压在它上面。
  • 右键双击那条线=直接删掉它:第一下右键照旧弹菜单(重命名 / 删除),紧接着再右键一下 (落在那个小菜单上、或者线的另一段都算)这条线就没了;隔了两秒再点不算双击,只关菜单。
  • 右下角的「缩略图」:画布右下角那颗按钮按下去,整块画布换成一屏小牌子 —— 每张卡 只剩标题(最多两行),相对位置一个不差、距离整体按比例缩到一屏里,连线也跟着画。 牌子的大小照每张卡自己的长宽比来:加宽过的卡片就是一块宽牌子,不会一律画成同样的小方块。 点一张牌子=退出缩略图并回到那张卡(视口把它摆到画布中间,缩放不动),点空白处或 再按一次按钮也是退出。画布上什么都没有时这颗按钮是灰的。
  • 右键菜单:复制 / 剪切 / 粘贴 / 原地复制 / 染色 / 编成一组 / 重置为默认大小 / 展开 / 移出画布 / 删除,空白处右键是新建卡片 / 新建文本框 / 新建矩形方框 / 粘贴 / 自动排列(竖排)与横排 / 编成一组。自动排列按「卡片 + 右侧选项列」整块让位, 分歧卡片那列选项不会压到别的卡片上;卡片自己的位置只由卡片决定,选项列只影响卡片 之间的间距;选中两张以上时只排选中的那几张,别的卡片不动、排完还落回原处附近。 两种排法:竖排=层往右推进、同一层的卡片上下堆叠;横排=同一层的卡片左右并排、 层与层往下走。快捷键 Ctrl+C X V D、空格、Delete、Ctrl+0/1、Esc —— 这些快捷键只在最近一次点击 落在画布上时生效,点过别处(比如对话输入框)就还给宿主,不会抢对话里的粘贴。
  • 展开动画:先把视角平移到卡片居中(整块画布一个 transform,别的卡片相对位置不变) → 卡片长成窗口 → 全屏遮罩模式下背景(含其他卡片)虚化。

安装

这个包没有被 npm 发布,直接以源码形式放进 DSH 的 profile:

# 1) 克隆进 profile 的 node_modules
git clone https://github.com/tokai-teiodh/dsh-script-cards "$env:DSH_HOME\profiles\desktop\node_modules\dsh-script-cards"

# 2) 把包名加进该 profile 的 dsh.profile.bundles(见 profiles/<profile>/package.json)
#    "dsh": { "profile": { "bundles": [ ..., "dsh-script-cards" ] } }

# 3) 重启 DSH

为什么必须重启:DSH 的浏览器客户端名单在 boot 时由 profile 的 dsh.profile.bundles 组装,Typert 清单也是启动时扫描的。改完不重启不生效。

装好之后:会话头会出现「剧本档案」按钮,右侧栏多一个可停靠的页(与对话并排), 左侧栏也有一页全屏的。


目录约定(可改)

<项目根>/
  剧本档案/
    卡片/*.md        ← 一张卡片一个文件
    归档/*.md        ← 会话总结
    文档/*.md        ← 每张章节 / 节点卡片一份的独立文档(与卡片文件同名)
    分支.json        ← 结构:章节归属 / 画布坐标 / 分歧选项与连线 / 染色 / 引用卡摆在哪

内容在卡片文件里,结构在 分支.json 里,两个都落盘、都能进 git。 文档是第三样东西:独立文件(同名、同目录风格),卡片正文和它互不影响。

四个目录名不是写死的:面板右上角「设置」里可以改(按项目存在浏览器本地), 所以英文项目可以用 script-archive/cards/archive/docs。


卡片格式

frontmatter 白名单(与 cards.py 保持一致):

id / type / title / code / chapter / mode / when / order /
summary / tags / color / created / updated / source
---
id: node-n1
type: node
title: 神社训练
when: 第4天
order: 10
summary: 一行简介(面板卡片最下面那行)
tags: 日常, 支线
---

## 角色
- 甲

## 场景
- 某地

## 内容
- 发生了什么
  • code:章节序号,例如 G1.1(用于排序与地址栏)。
  • chapter:节点所属章节的卡片键,形如 card/<文件名去掉 .md>。
  • mode: branch:把这个节点画成分歧节点。
  • color:人物卡的角色色(文档里台词名字的颜色取它;没有就回落到 tags 里的 印象色#RRGGBB)。画布上的染色是图谱里的另一份值,优先于这里。

配套工具(可选)

cards.py 是一个零依赖的 Python 小工具,负责建档案、加卡片、归档会话、刷新索引, 以及给模型生成简介留的两个子命令:

python cards.py init    --root .
python cards.py add     --spec card.json --root .      # 新增卡片(--force 覆盖)
python cards.py archive --spec summary.json --root .   # 归档一次会话
python cards.py index   --root .                       # 重建索引
python cards.py list    --root . [--type open]
python cards.py summaries   --root . --missing-only --out summaries.json
python cards.py set-summary --spec summaries.json --root .   # 只改 summary/updated

它不属于这个仓库的依赖,面板也不依赖它 —— 手工写卡片文件一样能用。


它是怎么把改动写回磁盘的

浏览器端本来只有一条只读文件通道(remote.workspaceFiles),改不动磁盘。 所以这个包有两半:

  • 客户端半边 lib/client.js:全部界面。手写、不走打包, 用 DSH 的 window.__ModuleLoader__.load({ id, factory }) 约定。
  • 宿主半边 lib/index.js + lib/typert.js:注册一个 Cordis 服务 scriptCardsFs (fsList / fsRead / fsWrite / fsMkdir / fsRemove / fsStat), 经 Typert 清单 exports["./typert"] 暴露给浏览器,客户端用 ctx.remote.$mount(...) 挂上自己的 descriptors,之后 ctx.get('remote.scriptCardsFs') 就是可调用的代理。

边界:每个方法第一个参数 root 就是边界,所有读写都必须落在它之内 (.. 逃逸、相对路径、磁盘根目录都会被拒)。宿主不需要知道目录叫什么名字。

桥挂不上时(网关没起来、版本不匹配……)面板会退化成只读模式, 并在顶部把原始失败原因写出来,而不是静默地把你的改动丢掉。

踩过的坑,写在 lib/typert.js 顶部:网关会拒绝任何与它自己那个 RemoteNamespaceService 原型同名的办法(remove 就在原型上),撞上就 $mount 抛错、整个命名空间装不上、面板静默只读。所以六个方法统一带 fs 前缀, 并且测试里断言它们不撞保留名。


开发

npm run build   # src/*.js → lib/client.js(产物,别手改;**带自检**,见下)
npm run check   # 构建一致性 + 三个 node --check + 两套无头测试
npm run visual  # 可选:真 CSS + 无头 Edge 渲染十三张截图(.visual/*.png)

⚠ 改完必须跑 npm run check,别把「改到一半」的产物留在盘上。 npm run build 现在会自检:写完产物立刻跑一遍 tests/smoke.mjs,不通过就把 lib/client.js 还原成构建前那一份、打印红字并以非零退出(约多花 6~7 秒)。 起因是一次真实事故:把 speakerNames 改名成 speakerList 之后只改了一半就 build, 产物里 CardsPanel 调的还是旧名字,应用一重启整个面板白掉(渲染期错误连崩溃日志 都不写)。这条自检让「能 build 但会白屏」的产物永远落不到盘上。 --check 那条路不跑自检(它只比对产物与源码是否一致,不写文件)。

客户端源码拆成 src/ 下 14 个片段,由 scripts/build.mjs 拼成一个 __ModuleLoader__.load(...) 调用(客户端半边必须是一个文件)。改代码请改 src/。

测试都是零依赖、纯 Node:

  • tests/host.mjs —— 宿主半边:六个文件原语在真临时目录上跑通、UTF-8 往返、 root 边界越界全被拒、清单形状、保留名不冲突,以及浏览器半边挂上去的 descriptors 与宿主清单逐端点对齐。
  • tests/smoke.mjs —— 客户端半边:自带一个极小的 React 替身 + 假 DOM, 真的驱动 lib/client.js,覆盖两族卡片、两级画布、四种卡片形态、右键菜单、 滚轮缩放与左键平移、展开动画、浮动窗口(拖动 / 缩放 / 最小尺寸 / 拖不出画布 / 图钉多开 / 位置与模式记在浏览器里)、每张卡片一份独立文档(页签、脏标记、 Ctrl+S、空文档不落盘、角标、删卡片时的文档三选一)、文档草稿缓存、 行首标记整体删除、画布上的引用卡与「存档卡」抽屉(拖放落点只写图谱、双击只读、 批量只移开不删文件、自动排列不碰它、抽屉自己吃滚轮)、 目录名可配置、色卡可自定义,以及编辑真的写回了卡片文件、文档文件与图谱文件。 这个替身会专门造两种时间差:setState 不是同步渲染 (defer / flush)、以及组件卸载后 hook state 归零 —— 两类都对应真机上报过的 问题(拖动不落盘、对话框带着上一次的输入);它也能派发插件用 ref + addEventListener 自己挂的监听器(滚轮和标签条的拖动都是自己挂的)。 它还照真 React 的规矩盯着 hook 的顺序和数量:一个组件两次渲染调用的 hook 不一样 就报错 —— 组件被当普通函数调用时会这样,真机上的表现是整个面板白掉。
  • 这个替身没有布局引擎,也不跑 CSS,「元素被裁掉了 / 画到屏幕外了 / 被压在下面了」 这类问题它看不见。 所以画布那几条关键样式是用契约断言钉住的:卡片不许 overflow:hidden(选项列挂在 卡片盒子外面)、选项行高必须固定 30px、接口圆点必须是 16px 的空心圆且偏移 ±8px、 连线颜色不许用 --dsw-alias-border-l2(那是 12% 白,画成线等于透明)、 连线层那个 <g> 必须补回 +4000(.sc-edges 挂在 -4000)、连线标签不许是 SVG 的后代(浏览器不画 SVG 里的 HTML 元素,标签会是 0×0、看不见也点不到)、 连线标签的 z-index 要高过卡片和选项列(线中点常落在卡片底下)、 标签条的滚动条整个藏掉(scrollbar-width:none,滑动时才由 JS 浮出一根绝对定位的 细条,不占位也就不会抖)、方片页不许能拖选文字、框选的框不许挡点击且层级高过卡片、 画布层不许 will-change、 translate 必须整像素、浮窗标题栏与缩放手柄不许能选中文字(拖动的地方一律 user-select:none)、文档输入框必须等宽(它写的是正文,不是表单)。 真机上真出过的六个「看不见」的 bug 都是这一层(选项列整列被裁、画布糊、连线画到屏幕外、 连线名字根本没画出来、对话框里那条多余的横向滚动条、线上那个输入框被卡片压掉一半)。
  • tests/visual.mjs 补的正是这一层:把真 CSS 放进无头浏览器里渲染 + 截十三张图,顺便 读出计算样式(线是什么颜色、圆点有没有描边),并量一量连线标签画没画出来、点不点得到 —— 探测里专门摆一张压住连线中点的卡片,用 elementFromPoint 看标签和输入框还在不在最上面、 选项列的实测高度(自动排列的占位是按它算的)、对话框宽度和里面那个 width:100% 的输入框有没有撑出横向滚动条、标签条的原生滚动条藏干净没有、 拖的时候会不会选中文字。 十三张图分别是:canvas-*.png(画布本体)、float-*.png(浮动窗口)、 pinned-*.png(钉住多开)、full-*.png(全屏遮罩模式)、doc-*.png(展开态的「文档」 页签:脏标记、已保存、相对路径、等宽输入框)、talk-*.png(文档里的台词 / 旁白 + 右键人物菜单)、grid-*.png(方片页:人物卡标题染着角色色 + 右键色卡菜单)、 refs-*.png(「存档卡」抽屉 + 画布上的引用卡)、statusbar-*.png(面板被拖窄时 底部状态栏:提示从最上面开始藏、高度不再长高)、statuswide-*.png(同一个状态栏 在宽面板下:一行都不许藏)、mini-*.png(缩略图:只留标题的小牌子,加宽过的那张 明显更宽、长标题封在两行)、lift-*.png(拖动中的卡片抬到最上面:两对叠在一起的卡片, 一对里带 .lift 的那张故意排在 DOM 前面,不抬就必输)。后几张也各自带探测: 标题栏是不是抓手(cursor:move + user-select:none)、手柄是不是缩放手柄 (16×16 + nwse-resize)、遮罩有没有盖住、画布有没有虚化、 文档输入框够不够高、能不能自己滚、人物菜单有没有被 overflow:hidden 的文档体裁掉 (菜单是 position:fixed 逃出来的,用 elementFromPoint 量它在不在最上面)、 状态栏那几颗按钮有没有被裁掉(量每颗按钮的 rect 是不是整个落在状态栏里)、 缩略图的牌子有没有跑出画布、标题是不是真的封在两行、宽度有没有跟着卡片的长宽比走、 这一层是不是不透明(不透明才谈得上「只显示标题」)、带 .lift 的卡片是不是真的 压在 DOM 更靠后的卡片上(elementFromPoint 摸叠在一起的那一点,另外摆一对没 lift 的 当对照:那一对必须由 DOM 顺序决定)。 要人眼看效果时用它,别靠猜。主题色优先从装好的 DSH 的 app.asar 里取真值, 找不到就用内置兜底色。

⚠ 静态 HTML 里看不出「标签塞进 SVG」这个坑:HTML 解析器遇到 <g> 里的 <div> 会把它 弹出 SVG,页面照样正常;只有 React 走 createElementNS 才会中招。所以那个探测是用 DOM API 造元素、并且检查标签有没有 SVG 祖先,不是靠眼睛看截图。

tests/host.mjs 的 D / E 两段会加载 lib/typert.js,而它跟运行时一样从安装它的 那个 DSH profile 解析 zod。所以在裸克隆里跑,这两段会打印 SKIP 并跳过; 想让它们真正跑起来,请在 profiles/<profile>/node_modules/dsh-script-cards 里执行。

测不到的部分(鼠标手势的真实手感、真实 Typert 网关、宿主与浏览器的版本组合)要靠真机重启验证。


License

MIT © tokai-teiodh