dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Script-archive card panel for DeepSeek Harness: rounded card grid, two-level branch canvas, and a host-side Typert bridge that keeps structure inside your own project folder.
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 |
滚轮缩放(以指针为中心,走 25/33/50/67/80/100/125/150/200% 这些档位)、
空白处左键拖动平移(中键、Alt+左键 也行;Shift+滚轮左右移、Alt+滚轮上下移);
从卡片边缘的圆点(16px 空心整圆、1px 细圈)拖出贝塞尔连线 —— 拉的时候其它卡片的
入口会点亮表示可以落,松手即连;拖动卡片时连线实时跟着走(不是松手才跳过去);
线宽统一 1.5px,箭头固定大小。连线的名字:线上平时什么都不显示,点一下那条线
就在线上浮出输入框(回车或点别处保存,Esc 放弃);起过名字的线名字一直挂着,点名字
接着改;右键那条线还有重命名 / 删除的菜单。标签和这个输入框的层级比卡片高,
线中点正好压在卡片上时也看得全。底部是 iOS 后台式的堆叠条,
把没上画的卡片拖进去或点一下落位。Delete 整组移出画布、
Ctrl+A 全选这一层。右键没拖动的那一下还是原来的菜单。<档案目录>/文档/<卡片文件名>),
不是卡片正文 —— 正文照旧在「字段」页里改。等宽输入框、Ctrl+S 保存、状态行显示相对
路径与「已保存 HH:MM」;空文档不落盘(清空后保存会把文件删掉,没写过就不建文件);
有文档的卡片右上角有一个小角标。写不回磁盘时输入框变只读,并写明原因。type: character 卡片;名字取标题里第一个括号之前那一段:
勿忘我(ワスレナグサ) → 勿忘我,人物多了可以在菜单里现筛)+「旁白」+「结束当前角色」。
选完就进入「该角色说话」状态(状态栏写着「正在写:勿忘我」),这一行的开头自动带前缀:
「勿忘我」:你终于来了。(中文直角引号 + 全角冒号,字面写进 .md)「名字」:、续行那串对齐空格加冒号、旁白的两个全角空格)
按退格或 Delete,整块一起删,不会只掉一个冒号留下半截;没有标记的普通行照旧
一个字一个字删主角 真女主 攻略对象 的进主角组,
写着 配角 的进配角组,两类都不沾的进「其他」),上面照旧有筛选框color,跟项目一起进 git;设完回文档里,那个人物的台词名字立刻用这个色。color 没有
就回落到标签里的 印象色#xxxxxx,两样都没有就照正文颜色。分支.json 的 refs 里(按「哪一层」分开存:顶层画布是 top,某个章节的节点画布是
那张章节卡片的键),关掉再打开还在原处。引用卡是虚线边 + 左下角一个「引用」角标、
没有连线圆点;能拖、能框选、能染色(不设就用角色色),双击只看只读详情,
要改内容回方片页。右键菜单只有「染色」和「从画布移开」;批量删除时引用卡只被移开,
不会删掉卡片文件;自动排列不碰引用卡。鼠标停在抽屉上滚轮滚抽屉,画布不跟着缩放。章节序号 + 章节名 分两段显示,下面标签,最下简介;展开后是节点列表。
章节是容器,所以没有「故事内时间」这一项。角色 / 场景 / 内容
(读正文里的 ## 角色 ## 场景 ## 内容 小节)。+ 选项 挂在
最后一行下面、不参与居中),每个选项各带一个可拖的出口;接通后标出走向(→),
展开后选项在下面单列一块,带一键增删。Esc 或点别处保存),矩形方框没有内容、只有底色(右键它调色),两个都能拖、
拉右下角改大小、框选、染色、编组。它们不是卡片:没有文件、不连线、不参与自动排列,
只记在 分支.json 的 objects 里(按「哪一层」分开存,跟引用卡同一套规则)。
文本框没有底色(只有文字和一圈淡虚线,下面的色块透得上来)。层序从下往上是:
有颜色的方框 → 文字 → 其他所有卡片 —— 方框和文本框都垫在卡片下面,卡片永远压在最上面、
点得到的也一定是卡片;三层互不穿插。Alt 只拖你按住的那一个,方便把它拽出组);点某一张卡片本身仍然只走单卡菜单。
自动排列把一组当成一块,组内部的相对位置不会被拆开。组只记成员,写在 分支.json
的 groups 里;剩下不到两个人的组自动散掉。shift + 右键点一张卡片=把它加进 / 移出当前选择,不弹菜单;
不带 shift 的右键照旧弹菜单。旧的右键拖框选照旧保留,框选现在也能框到引用卡与
文本框 / 方框。分支.json 里
这张卡片的记录,跟项目走;自动排列按实际宽高算占位 —— 一张卡变宽了,右边的层
会跟着让开。Shift:拖卡片(或文本框 / 方框)的时候按住 Shift,边或中线跟别的卡片
差在 6px 以内就自动贴上去,并在那条线上画一根细辅助线 —— 线只画到和它对齐的那张卡片
为止、两端渐变淡掉,不横贯整块画布;拖右下角手柄改大小的时候也吸(只动宽高、不动位置);
整组拖动按整组包围盒吸。不按 Shift 就完全不吸、也不画线,卡片停在你放的那儿,
想自由摆就不会被悄悄拽走。拖到一半再按住 Shift 也算数,下一次鼠标移动就吸上。Ctrl+C X V D、空格、Delete、Ctrl+0/1、Esc —— 这些快捷键只在最近一次点击
落在画布上时生效,点过别处(比如对话输入框)就还给宿主,不会抢对话里的粘贴。这个包没有被 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 不一样
就报错 —— 组件被当普通函数调用时会这样,真机上的表现是整个面板白掉。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 网关、宿主与浏览器的版本组合)要靠真机重启验证。
MIT © tokai-teiodh