🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

设计画布 kukuruzovo/dsh-design-canvas

DeepSeek Harness
kukuruzovo12 天前有提交已提供中文文档扫描:含敏感能力

在项目里画界面稿,智能体与人共编同一份文件

30/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/24 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add kukuruzovo/dsh-design-canvas
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:仅索引
是什么[目录]—
dsh 原生插件 · ui
暂无时间戳·判据 →
装得上吗[实装验证]—
本站尚未做安装检查
暂无时间戳·判据 →
安全吗[自动扫描]—
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
数据截至 2026/11/04·判据 →
还在维护吗[仓库]✓
活跃:最近一次提交在 12 天前
暂无时间戳·判据 →
图例:✓ 通过! 需注意— 未判定 / 无数据· 「未判定」= 本站尚无数据,不代表插件有问题
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →

可信度证据

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件读取剪贴板或密钥环境变量发起外部网络请求
查看扫描证据(共 105 处,展示前 56 条)
  • 发起网络请求
    client.js:854
    response = await fetch(url, {
  • 读取本地文件
    store.js:81
    if (existsSync(configFile())) {
  • 读取本地文件
    store.js:110
    return readFileSync(path, 'utf8')
  • 读取本地文件
    store.js:130
    const hasDesign = existsSync(designPath)
  • 读取本地文件
    store.js:136
    exists: existsSync(project.root),
  • 读取本地文件
    store.js:197
    initialized: existsSync(join(root, DESIGN_DIR)),
  • 读取本地文件
    store.js:234
    if (existsSync(join(current, DESIGN_DIR))) {
  • 写入本地文件
    store.js:250
    await mkdir(designPath, { recursive: true })
  • 写入本地文件
    store.js:481
    await mkdir(project.designPath, { recursive: true })
  • 写入本地文件
    store.js:519
    await mkdir(target, { recursive: true })
  • 写入本地文件
    store.js:520
    await rename(path, join(target, basename(path)))
  • 写入本地文件
    store.js:593
    await mkdir(project.designPath, { recursive: true })
  • 写入本地文件
    store.js:594
    await rename(join(dir, files[0]), target)
  • 删除本地文件
    store.js:595
    await unlink(metaPath).catch(() => {})
  • 删除本地文件递归
    store.js:596
    await rm(dir, { recursive: true, force: true }).catch(() => {})
  • 删除本地文件递归
    demo.mjs:23
    await rm(join(root, DESIGN_DIR), { recursive: true, force: true })
  • 写入本地文件
    demo.mjs:24
    await mkdir(root, { recursive: true })
  • 读取本地文件
    demo.mjs:133
    const fixture = JSON.parse(await readFile(join(root, DESIGN_DIR, 'quiz-home.canvas.json'), 'utf8'))
  • 读取本地文件
    smoke-client.mjs:24
    const source = await readFile(join(here, 'client.js'), 'utf8')
  • 读取本地文件
    smoke-editor.mjs:36
    const dirs = await readdir(cache).catch(() => [])
  • 写入本地文件
    smoke-editor.mjs:51
    await mkdir(home, { recursive: true })
  • 执行外部命令来自配置/变量
    smoke-editor.mjs:64
    const child = spawn(process.execPath, args, {
  • 发起网络请求
    smoke-editor.mjs:177
    const external = await fetch(`${base}/api/dsh-design-canvas/apply${token ? `?token=${token}` : ''}`, {
  • 读取本地文件
    smoke-editor.mjs:192
    const probeRev = JSON.parse(await readFile(canvasFile, 'utf8')).rev
  • 发起网络请求
    smoke-editor.mjs:193
    const probeAdd = await fetch(`${base}/api/dsh-design-canvas/apply${token ? `?token=${token}` : ''}`, {
  • 读取本地文件
    smoke-editor.mjs:224
    const afterDoc = JSON.parse(await readFile(canvasFile, 'utf8'))
  • 删除本地文件递归
    smoke-editor.mjs:257
    await rm(home, { recursive: true, force: true })
  • 删除本地文件递归
    smoke-editor.mjs:258
    await rm(projectRoot, { recursive: true, force: true })
  • 读取本地文件
    smoke-gui.mjs:30
    const dirs = await readdir(cache).catch(() => [])
  • 读取密钥环境变量
    smoke-gui.mjs:62
    const token = process.env.DSH_WEB_TOKEN
  • 发起网络请求
    smoke-gui.mjs:144
    const external = await fetch(`${base}/api/dsh-design-canvas/apply`, {
  • 写入本地文件
    smoke-standalone.mjs:25
    await mkdir(home, { recursive: true })
  • 执行外部命令来自配置/变量
    smoke-standalone.mjs:60
    const child = spawn(process.execPath, [join(import.meta.dirname, 'standalone.js'), ...args], {
  • 执行外部命令固定命令
    smoke-standalone.mjs:89
    execFile('tailscale', ['ip', '-4'], { timeout: 4000 }, (error, stdout) => {
  • 发起网络请求
    smoke-standalone.mjs:103
    const page = await fetch(`${main.base}/`)
  • 发起网络请求
    smoke-standalone.mjs:116
    const shell = await (await fetch(`${main.base}/shell.js`)).text()
  • 发起网络请求
    smoke-standalone.mjs:122
    const react = await (await fetch(`${main.base}/vendor/react.js`)).text()
  • 发起网络请求
    smoke-standalone.mjs:124
    const reactDom = await (await fetch(`${main.base}/vendor/react-dom.js`)).text()
  • 发起网络请求
    smoke-standalone.mjs:128
    const theme = await (await fetch(`${main.base}/theme.css`)).text()
  • 发起网络请求
    smoke-standalone.mjs:136
    const client = await (await fetch(`${main.base}/client.js`)).text()
  • 执行外部命令来自配置/变量
    smoke-standalone.mjs:242
    execFile(process.execPath, [join(import.meta.dirname, 'standalone.js'), ...args], { env: process.env }, (error, stdout, stderr) => {
  • 读取本地文件
    smoke-standalone.mjs:283
    assert.equal((await stat(tokenPath)).mode & 0o777, 0o600, 'файл токена доступен не только владельцу')
  • 读取本地文件
    smoke-standalone.mjs:323
    const onDisk = JSON.parse(await readFile(join(projectRoot, '.design', 'quiz-home.canvas.json'), 'utf8'))
  • 删除本地文件递归
    smoke-standalone.mjs:329
    await rm(home, { recursive: true, force: true })
  • 删除本地文件递归
    smoke-standalone.mjs:330
    await rm(projectRoot, { recursive: true, force: true })
  • 写入本地文件
    smoke-tools.mjs:18
    await mkdir(home, { recursive: true })
  • 删除本地文件递归
    smoke-tools.mjs:338
    await rm(home, { recursive: true, force: true })
  • 删除本地文件递归
    smoke-tools.mjs:339
    await rm(projectRoot, { recursive: true, force: true })
  • 写入本地文件
    smoke.mjs:15
    await mkdir(home, { recursive: true })
  • 读取本地文件
    smoke.mjs:510
    const autoRaw = JSON.parse(await readFile(join(root, DESIGN_DIR, 'auto.canvas.json'), 'utf8'))
  • 写入本地文件
    smoke.mjs:646
    await mkdir(join(cloneRoot, DESIGN_DIR), { recursive: true })
  • 删除本地文件递归
    smoke.mjs:657
    await rm(cloneRoot, { recursive: true, force: true })
  • 读取本地文件
    smoke.mjs:699
    const entries = await readdir(join(root, DESIGN_DIR))
  • 读取本地文件
    smoke.mjs:702
    const raw = JSON.parse(await readFile(join(root, DESIGN_DIR, 'home.canvas.json'), 'utf8'))
  • 删除本地文件递归
    smoke.mjs:709
    await rm(root, { recursive: true, force: true })
  • 删除本地文件递归
    smoke.mjs:710
    await rm(home, { recursive: true, force: true })
扫描于 2026/11/4(1 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

社区实装还没有人打卡 · 装过的第一个就是你
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

为 DeepSeek Harness 提供类 Figma 设计画布:智能体工具 design_*、「Макеты」面板、独立编辑器和 HTML/CSS 导出。画布存放在 <project>/.design 中,随代码一起做版本管理。

README

由 DeepSeek 最新模型翻译生成
设计画布 — 为 DeepSeek Harness 提供可视化稿 插件提供同一套系统的两半: - 智能体工具 design_ —— 智能体把界面当代码来设计:创建画布、按操作批次拼装屏幕、声明组件、维护 token、校验并导出为 HTML/CSS; - 网页界面里的 「稿」面板 —— 类似 Figma 的编辑器,编辑同一批文件:图层树、拖拽、缩放、检查器、组件库、token 编辑器、导出。 人和智能体在磁盘上的同一份文档上工作,所以设计不会在「智能体画出来的」和「人改过的」之间走样。 同一个编辑器也能单独打开 —— 在自己的端口上开一个独立窗口,不需要 harness:node standalone.js(见独立编辑器)。 目录 - 环境要求 - 快速开始 - 稿放在哪里 - 文档模型 - 智能体工具 - 导出 - 「稿」面板 - 独立编辑器 - HTTP 路由 - 架构 - 演示 - 检查 - 开发 - 安装与更新 - 许可证 环境要求 - Node.js ≥ 20(ESM、node:fs/promises、fetch)。插件没有依赖:独立窗口用的 React 放在 vendor/ 里,Playwright 只在浏览器检查时需要,通过 npx 获取。 - DeepSeek Harness —— 只在「稿」面板和智能体工具里需要。独立编辑器不需要 harness。
展开完整文档(README 剩余 1 节)
设计画布 — 为 DeepSeek Harness 提供可视化稿

插件提供同一套系统的两半:

- 智能体工具 design_ —— 智能体把界面当代码来设计:创建画布、按操作批次拼装屏幕、声明组件、维护 token、校验并导出为 HTML/CSS;
- 网页界面里的 「稿」面板 —— 类似 Figma 的编辑器,编辑同一批文件:图层树、拖拽、缩放、检查器、组件库、token 编辑器、导出。

人和智能体在磁盘上的同一份文档上工作,所以设计不会在「智能体画出来的」和「人改过的」之间走样。

同一个编辑器也能单独打开 —— 在自己的端口上开一个独立窗口,不需要 harness:node standalone.js(见独立编辑器)。

目录

- 环境要求
- 快速开始
- 稿放在哪里
- 文档模型
- 智能体工具
- 导出
- 「稿」面板
- 独立编辑器
- HTTP 路由
- 架构
- 演示
- 检查
- 开发
- 安装与更新
- 许可证

环境要求

- Node.js ≥ 20(ESM、node:fs/promises、fetch)。插件没有依赖:独立窗口用的 React 放在 vendor/ 里,Playwright 只在浏览器检查时需要,通过 npx 获取。
- DeepSeek Harness —— 只在「稿」面板和智能体工具里需要。独立编辑器不需要 harness。

快速开始

git clone https://github.com/kukuruzovo/dsh-design-canvas.git
cd dsh-design-canvas
node standalone.js # 编辑器在 http://127.0.0.1:5178/

在窗口里:「新建画布」→ 选择项目目录 → 画布会出现在 <项目>/.design/。之后可以手动改、交给智能体改,或者导出。

要把插件接到 DSH(design_ 工具 + 「稿」面板):

dsh plugin --profile web add link:/绝对/路径/到/dsh-design-canvas

然后重启 dsh web。详情见安装与更新。

稿放在哪里

画布放在项目仓库里,而不是 DSH 的家目录:

<项目根目录>/.design/ project.json — 项目元数据(第一次写入画布时出现) home.canvas.json — 画布文档 settings-mobile.canvas.json exports/ — 生成的 HTML/CSS home.html home.css home.components.html

由此带来:设计随代码一起做版本管理,跟着 git 走,在……之间迁移随应用一起分发,就放在它所属应用的旁边。

根目录注册表是 ~/.dsh/design-canvas.json。没有内置的项目列表:只有你把某个目录初始化为项目(design_projects init、第一次 design_create,或 standalone.js --project),它才会进入注册表;而已经提交了 .design/ 文件夹的目录,插件会自己找到。找到的项目之后按 id 寻址——这就是「克隆一个提交了设计稿的仓库」的场景。已删除画布的回收站是 ~/.dsh/design-trash/(可恢复)。

文档模型

文档就是普通 JSON。坐标模型类似 Figma:每个节点有相对父级的绝对 x/y,以及自己的 w/h;框架有 layout.mode 模式(none / vertical / horizontal),开启后启用流式布局并重新计算子节点位置。画布文件 <id>.canvas.json 长这样:

{
"format": "dsh-design-canvas",
"version": 1,
"id": "home",
"name": "Home",
"device": "mobile",
"rev": 7, // 修订号:文件字段,不属于文档模型
"tokens": { "color": { "bg": "#0b0d10" }, "text": { "h1": { "size": 32, "weight": 700 } } },
"components": {
"button-primary": {
"key": "button-primary",
"name": "Button / Primary",
"node": { /* 不含坐标的节点模板 / },
"props": {
"label": { "type": "text", "default": "Кнопка", "target": "text", "property": "text.value" }
},
"variants": { "ghost": { "name": "Ghost", "node": { / 自己的模板 / }, "props": { "label": { "default": "Позже" } } } }
}
},
"guides": { "x": [120, 340], "y": [64] }, // 编辑器参考线;没有标记就没有该字段
"root": { "id": "root", "type": "frame", "children": [ / 屏幕框架 / ] }
}

节点类型:frame、group、rect、ellipse、line、text、image、instance。

rev 只存在于文件里:design_read mode=full 和 GET /canvas 返回的文档不含它,修订号本身放在响应的相邻字段里。

限制:嵌套最多 64 层(更深的子节点会被截断),节点最多 20 000 个,嵌套实例最多展开 32 层,超过 8 MB 的画布不读也不写(写入会被拒绝,以免文件变得无法读取)。

令牌

字段值可以是字面量(#0b0d10),也可以是对令牌的引用(token:color.bg)。引用在渲染、导出和校验时解析,在 CSS 里会变成变量:var(--color-bg, #0b0d10)。改一个令牌就能重绘整个设计稿——编辑器里和构建出的 HTML 里都是如此。

输出单位取自令牌路径:radius.、space.、排版尺寸和字距,以及阴影偏移都得到 px;字重和行高倍数保持无单位。

例外是 text.token 字段:它是排版令牌的裸路径(text.h1,不带 token: 前缀),因为令牌是复合的,一次描述尺寸、字重、行高和字距。显式的 text.size、text.weight、text.lineHeight、text.letterSpacing 会覆盖令牌的值,用于特定节点。

布局

layout.mode = 'none' — 自由定位:子节点保留作者设置的 x/y。vertical / horizontal 启用流式布局:子节点位置由顺序、gap、padding、align 和 justify 推导出来,帧尺寸按内容自适应(可用节点字段 autoW / autoH 设为 false 关闭——这些字段保存在 canvas.json 中,写入后依然保留)。

justify(start / center / end / space-between)沿主轴分配空闲空间,align: 'stretch' 沿交叉轴拉伸子节点。只有在该轴上具有作者设定尺寸的帧才会出现空闲空间:启用 autoW/autoH 时帧按内容收缩,没有可分配的空间。因此 justify: center 要配合 autoW: false(水平方向)和 autoH: false (垂直方向)使用,而 align: 'stretch' 要配合交叉轴上已设定的尺寸使用。
面板重复同样的计算,所以保存的文件与画面一致。

参考线

guides — 编辑器标尺:x 定义垂直线,y 定义水平线,使用画布坐标。该字段可选:没有标尺的画布不包含它,规范化也不会向旧的 canvas.json 添加空数组。值会排序、去重,每个轴最多保存一百条线。参考线不会进入导出:它是工作区标尺,不是版面的一部分。智能体通过 set-guides 操作设置它们。

组件

实例是活引用,不是副本。 实例没有自己的内容:只有组件键、坐标和 overrides;内容在渲染和导出时从模板展开,因此修改组件会立即更新所有实例。实例的作者子节点不会被渲染,也不会被导出,design_validate 会用 instance-children 提示报告它们。
design_components action=detach(或 op: detach)把实例变成普通节点。

编辑组件。 update-component 修改现有组件的名称、描述、参数、变体和模板,不会重新创建它:props 按参数名合并,参数本身
按字段合并({ label: { default: 'ОК' } } 只改默认值,不会丢失 target),
值为 null 会删除参数,node 会整体替换模板。

变体。 variants[name] — 同一个组件,但使用不同的模板(node)和/或不同的参数字段(props)。参数和实例覆盖对所有变体通用,因此切换变体不会丢失实例的修改:实例保存 variant 字段,展开时取变体模板(componentTemplate)和生效参数(componentProps)。引用不存在的变体会在 design_validate 中以 missing-variant 提示报告,导出时库清单会打印基础模板,每个变体单独占一个单元格。define-component 不仅能从画布节点(from)声明组件,还能从现成模板(node)声明——面板就是这样克隆组件,不在画布中留下多余节点。

实例覆盖有两种:

- 组件声明的参数——值写入目标节点 target 的 property 路径(root、#节点名、text,或模板内的路径 children[0].label);
- 任意节点路径(style.fill、text.value、w)——直接改值,无需声明参数。

声明组件时,参数 label 自动从第一个文本节点推导出来。

智能体工具

| 工具 | 用途 |
| design_projects | 项目与画布注册表;init——把目录初始化为项目 |
| design_create | 创建画布(设备、模板、直接批量操作) |
| design_read | 画布投影:summary、outline、node、tokens、components、full |
| design_apply | 主力工具:批量操作,事务式执行 |
| design_tokens | 读取和修改调色板、字体排印、圆角、间距、阴影 |
| design_components | list / get / define / update / instantiate / detach / delete |
| design_export | 导出 HTML + CSS + 组件库页面到 .design/exports/ |
| design_validate | 重复 id、损坏的实例、实例上的自定义子节点、缺失的 token、裁切、对比度(按文本大小设阈值,token 和 rgba() 都计入) |
| design_delete | 回收站:list / delete / restore |

design_apply 操作

| 操作 | 字段 |
| add | { parent, node \| type, index?, after? }——id 已被占用时替换为新 id,响应返回实际 id |
| update | { id \| ids, patch }——x、y、w、h、name、visible、locked、style.、text.、layout. |
| move | { id, parent?, index?, after? }——after:相邻节点的 id,插到它后面;不传 parent 时节点留在原父级下,只改顺序 |
| delete | { id \| ids } |
| duplicate | { id \| ids, count?, offsetX?, offsetY?, parent? } |
| group / ungroup | { ids, name?, as? } / { id }——as: frame 按外接尺寸把选中内容包进 frame(默认是 group);as 值未知则报错 |
| reorder | { id, index \| direction },direction:front / back / up / down |
| set-tokens | { tokens, remove?, replace? }——remove 删除路径(color.bg、text.h1.size) |
| define-component | { key, from?, node?, name?, description?, props?, convert? }——from 取画布节点,node 指定现成模板 |
| update-component | { key, name?, description?, props?, variants?, node? }——props 按名称合并(值为 null 移除参数),variants 按变体名合并(值为 null 移除变体),node 替换模板 |
| instantiate | { component, parent, x?, y?, w?, h?, name?, variant?, overrides?, index?, after? }——variant 选择变体 |
| delete-component | { key, detach? } |
| detach | { id } |
| set-canvas | { name?, device? } — mobile / tablet / desktop / component;未知值报错 |
| align | { ids, mode, axis? } — mode:left / center / right / top / middle / bottom;axis 指定轴(x / y),替代由 mode 推导出的轴;axis 与 mode 冲突时报错 |
| distribute | { ids, axis? } — x / y(默认 x),至少三个节点 |
| set-guides | { x?, y?, clear? } — 画布参考线:x 表示该坐标上的垂直线,y 表示水平线;轴整体替换,clear 清除标记 |

批量操作是事务性的:任一操作出错都会回滚整个批次,文档保持原样。修订号加 1;expectRev 防止覆盖面板中所做的修改(不匹配则拒绝,返回 409)。

add 操作添加的框架默认裁剪子节点(clip: true),与设备模板中的框架一致;clip: false 让容器透明。

示例:一次调用完成一个屏幕

// design_create { "project": "my-app", "canvas": "home", "device": "mobile" }
// design_apply
{
"project": "my-app",
"canvas": "home",
"ops": [
{ "op": "add", "parent": "page", "node": {
"id": "header", "type": "frame", "name": "Header",
"x": 0, "y": 0, "w": 390, "h": 88,
"style": { "fill": "token:color.surface" } } },
{ "op": "add", "parent": "header", "node": {
"id": "title", "type": "text", "name": "Title",
"x": 20, "y": 32, "w": 300, "h": 32,
"text": { "value": "Викторины", "token": "text.h1", "color": "token:color.fg" } } },
{ "op": "add", "parent": "page", "node": {
"id": "cta", "type": "frame", "name": "CTA",
"x": 20, "y": 120, "w": 350, "h": 52,
"style": { "fill": "token:color.accent", "radius": "token:radius.pill" },
"layout": { "mode": "horizontal", "align": "center", "justify": "center" } } },
{ "op": "add", "parent": "cta", "node": {
"type": "text", "name": "CTALabel", "x": 0, "y": 0, "w": 200, "h": 24,
"text": { "value": "Начать", "color": "token:color.accentFg", "align": "center" } } },
{ "op": "define-component", "key": "button-primary", "from": "cta", "name": "Button / Primary" },
{ "op": "instantiate", "component": "button-primary", "parent": "page",
"x": 20, "y": 190, "overrides": { "label": "Мои результаты" } }
]
}

导出

design_export 写入 <项目>/.design/exports/:

- .html — 包含所有框架的页面;
- .css — token 以 :root { --color-bg: … } 形式输出,外加基础类;
- .components.html — 组件库:每个组件只出现一次,其变体各占一个单元格。

画布顶层框架就是屏幕:在组装出的页面上,它们始终按画板流(.dc-board)排列,因此放在同一坐标 (0,0) 的两个屏幕不会互相重叠。模式决定框架内节点如何定位:

- fidelity — 框架内部节点绝对定位,像素级(预览,交接);顶层画框在这里也会按画板流式排列,因此它们在最终页面里的坐标不会保留;
- clean —— 带 layout.mode 的画框变成 flex 容器,其余为绝对定位(更接近应用自身的布局)。

每个元素都有 data-node="<节点 id>" —— 可以把 DOM 和设计稿关联起来。

「设计稿」面板

位于 Web 界面左栏的图标。里面有:工具栏(select、frame、rect、ellipse、text),「画板 / 图层 / 组件 / Token」面板,支持平移和缩放的可视区域,检查器,带修订冲突解决的自动保存,撤销/重做,导出和检查。line 和 image 节点无法从工具栏创建 —— 由智能体通过 add 操作放置。

编辑节点

- 框选 —— 在空白处或容器背景上按住左键拖动:在画框背景上拖动会选中它的子节点,而点击同一背景则选中画框本身。顶层画框用设计稿周围灰色区域里的框选来选中,子节点用画框内部的框选来选中。画布平移:鼠标中键,或按住空格拖动;缩放 —— Ctrl+滚轮或工具栏按钮。
- 多选 —— 在画布和图层树里 Shift+点击 或 Ctrl/Cmd+点击 会把节点加入选区,再次点击则移出;Ctrl+A 选中当前范围内的全部内容(「进入」的容器的子节点,否则是顶层画框)。选中的节点可以一起移动、缩放、复制和分组;如果选区里同时有祖先和它的后代,后代随祖先一起移动。
- 进入容器 —— 双击画框或组:在它的背景上拖动会按它的子节点框选,而不是移动容器。退出 —— Esc。
- 方向键 把选区移动 1px,加 Shift 移动 10px。拖动时 Shift 让节点严格沿一个轴移动 —— 沿光标偏移更大的那个方向,因此对象沿参考线滑动,不会被带偏。在带自动布局的画框内,子节点坐标由布局本身计算(layout.mode),所以在那里拖动子节点不会有任何变化:请改图层顺序、gap 和 padding。
- 复制 —— Ctrl+C / Ctrl+V,再制 —— Ctrl+D,剪切 —— Ctrl+X。副本放在原对象旁边(它的父级里),之后每次粘贴都偏移一步。
- 分组 —— Ctrl+G 和 Ctrl+Shift+G;把选区合并成新画框 —— Ctrl+Alt+G 或右键菜单里的「合并为画框」项(画框按选区的外接尺寸放置,不带自动布局)。图层顺序 —— Ctrl+] / Ctrl+(加 Shift 直接置顶或置底)。
- 对齐 —— 选中两个及以上节点时检查器里的区块:六种模式,以及水平和垂直分布(至少需要三个节点)。
- 右键菜单 在节点上(画布或图层树里):再制、复制、粘贴、分组、「合并为画框」、顺序、锁定、可见性、分离实例、「创建为组件」、删除。右键点击选区之外的节点会先选中它。

面板中的组件

- 库 — 卡片带真实预览:缩略图用与画布相同的 NodeView 绘制,所以不会和设计稿对不上。支持按名称和键搜索,显示实例数和参数数,鼠标悬停卡片会高亮画布上该组件的所有实例。
- 组件卡片 — 预览更大,显示名称和描述,还有参数编辑器(名称、类型、默认值、目标和属性;目标通过点击模板节点名填入), 变体(每个变体的预览、从选中节点添加变体、删除),以及「放置实例」「查找实例」「复制组件」按钮。
- 实例 — 检查器里能看到哪些参数在该实例上被覆盖,可以重置单个覆盖、重置全部、「把改动写回组件」(模板变成实例上画的样子)、替换实例的组件并迁移匹配的覆盖,以及组件声明了变体时的变体切换器。

工作区标记

- 悬停高亮光标下的节点、它的尺寸,以及直到顶层框架的整条容器链 —不用点图层树就能读懂结构。同一条链在图层树里也会高亮,在图层树里悬停则高亮画布上的节点。
- 标尺在上方和左侧显示画布坐标;刻度间距随缩放变化,每十格一个标注。
「标尺」按钮或 Shift+R 可以隐藏它们。
- 参考线:从标尺拖出即可放置一条线。线可以拖动移动,拖回标尺、双击或点「清除所有参考线」按钮即可删除。参考线由「线条」按钮控制显示,保存在 canvas.json 里。
- 吸附在移动、缩放和创建节点时生效:对象会贴住父级、相邻对象和相邻框架的边缘与中线,也会贴住参考线和像素网格(缩放和创建时按 Shift 为 10px 步长)。对齐的线会高亮。
吸附到对象优先于吸附到网格,Alt 会同时关掉两者;「吸附」按钮则整体关闭吸附。
移动时只吸附对象真正移动过的轴 — 手势开始时不会把节点拽偏。
- 测量显示距离:选中节点时显示到父级边缘和最近相邻对象的距离,拖动时显示实时数值,悬停时按住 Alt 显示。容器的 padding 用内容周围的底色标出,所以既能看到容器的内边距,也能看到对象之间的间隙。

独立编辑器

同一个编辑器可以在独立窗口、用自己的端口打开,不需要 harness — 只要有 Node:

node standalone.js # http://127.0.0.1:5178/
npm run editor # то же самое
node standalone.js --port 4180 --open # другой порт и сразу открыть браузер
node standalone.js --project ~/app/foo # зарегистрировать корень проекта в реестре
node standalone.js --tailscale # окно для телефона и других устройств tailnet
node standalone.js --host 0.0.0.0 --token # 对外暴露——必须带 token

用手机编辑:--tailscale

node standalone.js --tailscale # 或:npm run editor:tailscale

服务器监听 本节点在 tailnet 中的地址(100.x.y.z,从 tailscale ip -4 获取,没有命令行时则通过 tailscale0 接口获取),因此窗口可以从手机、笔记本或网络中的其他节点打开,而完全不会暴露到本地网络 192.168.。输出中会打印两个地址:一个按 IP,一个按 MagicDNS 名称(<节点>.<tailnet>.ts.net)——后者更方便,但需要开启 MagicDNS。

此时访问由 token 保护:token 会保存到 $DSH_HOME/design-canvas-token(权限 0600),因此窗口地址不会因每次启动而改变——可以把它存进手机书签。自定义 token 用 --token <值> 指定,要更换已保存的 token,删除该文件即可。

通过 tailnet 使用 HTTPS——加 --tailscale-serve 标志:服务器会自己调用 tailscale serve --bg <地址>,并打印 https://<节点>.<tailnet>.ts.net/。
该命令需要 root,因此没有 root 时服务器不会崩溃,而是打印一条可直接用于 sudo 的命令,并提醒在不再需要代理时执行 sudo tailscale serve reset。

这不是第二套实现:standalone.js 提供同一个编辑器页面(client.js),并挂载同一个 /api/dsh-design-canvas 处理器,与 DSH 内的插件完全一致。两部分都走同一个 service.js,编辑同一批 <项目>/.design/.canvas.json 文件,因此在独立窗口中打开的画布,智能体和「布局」面板都能看到——反之亦然。

独立窗口里有什么:

- standalone/index.html + standalone/shell.js —— 外壳:它安装 window.__ModuleLoader__ 垫片,以及带有 slots/locale 服务的上下文,这些是插件的客户端部分期望从 harness 获得的东西,另外还有明暗主题切换;
- standalone/theme.css —— 明暗主题的 --dsw- 变量(取值来自 harness 主题,让面板在这里和那里看起来一致);
- vendor/react.production.min.js、vendor/react-dom.production.min.js ——本地 React 18(MIT),因此窗口既不依赖网络,也不依赖 DSH 是否安装。

默认端口是 5178,地址只限本机:该路由能往项目的 .design/ 里写文件。
一旦窗口不再只有本机可见(--tailscale、--tailscale-serve 或非回环的 --host),访问就由 token 保护:可以用 --token 指定自己的 token (8–128 个字符,限拉丁字母、数字和 . _ ~ -),也可以使用自动保存的 token。
带 ?token= 的页面会下发 cookie,脚本则可以带 x-dsh-token 请求头访问。
项目注册表和回收站不变(~/.dsh/design-canvas.json、~/.dsh/design-trash/),因此窗口和 harness 看到的是同一份项目列表。

HTTP 路由

/api/dsh-design-canvas —— 与智能体工具用的是同一个 service.js。
主要接口:

GET /projects?canvases=1 GET /canvases?project=
GET /display?project=&id= GET /canvas?project=&id=&mode=full
GET /outline|tokens|components|validate|export|trash|config
POST /create | /init | /apply | /canvas | /tokens | /components | /export | /delete | /restore

GET /display 返回可直接渲染的完整树:所有实例都由服务端展开,实例的子节点以它的 id 作为命名空间(<instanceId>/<childId>)。这样组件展开规则只存在一处,客户端不必重复实现。

各适配器默认值不同:design_read 读 summary,GET /canvas 读 full;GET /export 始终返回内嵌 CSS 的预览,不写文件。

架构

| 文件 | 层 |
| schema.js | 模型:归一化、树遍历、token、组件、参考线、校验 |
| store.js | 文件:项目注册表、.design/、修订、回收站 |
| ops.js | 文档操作(事务引擎) |
| export.js | 生成 HTML/CSS |
| service.js | 场景:智能体与面板共用同一套逻辑 |
| tools.js | 9 个智能体工具(薄适配器) |
| route.js | 面板 HTTP 路由(薄适配器) |
| index.js | 插件入口:工具 + 路由 |
| client.js | 可视化编辑器(不用 JSX 的 React) |
| standalone.js | 独立宿主:外壳静态资源、同一路由跑在自己的端口、token、tailnet |
| standalone/ | 窗口外壳(index.html、shell.js)与 --dsw- 主题 |
| vendor/ | 本地 React 18 UMD(MIT)——窗口不依赖 harness 和网络 |

规则:逻辑放在 schema/store/ops/export/service,tools.js、route.js、standalone.js 只解析参数、渲染结果。

演示

node demo.mjs 生成画布 demo/.design/quiz-home.canvas.json——一个应用界面,含 token、组件 topic-card(三个实例,各自覆盖不同属性)和流式底部导航。它同时用作面板检查的固定数据。

| 界面 | 组件库 |
| 界面 | 组件 |

预览图来自同一份导出:npm run demo && npm run preview。npm run demo 只重建 demo/.design/,所以已有预览图不会丢;只有演示画布本身或导出逻辑变了,才需要跑 npm run preview。面板「布局」页的截图靠实际运行抓取——DSH_WEB_TOKEN=… node smoke-gui.mjs … .gui-shot.png。

检查

npm run check # 所有 .js/.mjs 的语法
npm test # 核心 + 工具与路由 + 客户端契约 + 独立服务器
npm run test:editor # Playwright 中的独立编辑器窗口
npm run test:editor -- --tailscale # 同一窗口走 tailnet 地址(需有 Tailscale)
DSH_WEB_TOKEN=<地址栏里的 token> \
node smoke-gui.mjs http://127.0.0.1:3080 .gui-shot.png # 实际 DSH 中的面板

- smoke.mjs——schema、存储、操作、事务性、导出、路径防护;
- smoke-tools.mjs——9 个工具的注册,以及走 HTTP 路由的完整流程;
- smoke-client.mjs——浏览器模块契约:插槽、字典、API 路由、无 JSX;
- smoke-standalone.mjs — 独立服务器:外壳静态资源、内置 React、主题、访问规则(只要窗口不只从本机可见,就要求 token)、token 保存、路径穿越防护、通过 /api/dsh-design-canvas 完成完整编辑流程,以及命令行参数;如果机器上装了 Tailscale,会单独检查 --tailscale,否则该步骤标记为跳过;
- smoke-editor.mjs — 在空闲端口上启动独立窗口,使用临时 DSH_HOME,在浏览器里重放编辑器场景,并截取 .editor-shot.png;不需要 harness, Playwright 从已安装版本或 npx 缓存中获取。加上 --tailscale 标志后,同一个窗口会通过 tailnet 地址打开——以此验证「从另一台设备访问」的场景;
- smoke-gui.mjs — 在运行中的 DSH 里打开面板并截图,同时收集控制台错误。
它需要已启动的 DSH、token 和构建好的演示 fixture(npm run demo)。

开发

- AGENTS.md — 面向智能体的项目规则:分层、不变量、命令、验收标准;
- docs/ARCHITECTURE.md — 内部结构:分层、模型、扩展配方、错误码;
- docs/BACKLOG.md — 已知缺陷和技术债,附证据等级;
- docs/PLAN-editor-ux.md — 编辑器改进计划与各阶段状态;
- .dsh/skills/ — 项目技能,DSH 会自动加载;
- agency/ — 插件开发用的专家和 The Agency 团队(npm run agency:check,然后 node tools/agency-sync.mjs)。

安装与更新

插件就是普通 npm 包:Host 部分(index.js + dsh.bundle.patch)和客户端部分(client.js + dsh.client)。没有依赖,所以安装只需把目录加入 DSH 配置档。

git clone https://github.com/kukuruzovo/dsh-design-canvas.git
dsh plugin --profile web add link:/абсолютный/путь/к/dsh-design-canvas
dsh web # перезапуск: Host-половина подхватывается при старте

dsh plugin 是配置档目录里 pnpm 的封装,所以 link: 安装包时不复制、也不发布到 npm 仓库。在 Web 界面里,plugin_manager 做同样的事(install_bundle,传入目录的绝对路径)。

安装之后:

- Host 部分的改动通过重启 dsh web 生效;
- client.js 的改动通过刷新页面生效(只有 harness 检出目录里运行着 pnpm run dev:web 时才能热更新);
- 更新——在插件目录里 git pull,再重启 dsh web。

独立窗口(node standalone.js)完全不需要安装到 DSH:它就是插件目录里的普通 Node 进程,没有依赖——React 就放在旁边的 vendor/ 里。
client.js 和外壳的改动刷新页面后可见,standalone.js 的改动重启进程后可见。

许可证

[MIT。vendor/ 里的内置 React 18 按 MIT 分发(许可证文本就在旁边)。
预览分享卡片长什么样 ↓kukuruzovo/dsh-design-canvas
分享 · DeepSeek Harness Hub
kukuruzovo/dsh-design-canvas

在项目里画界面稿,智能体与人共编同一份文件

这个插件是你维护的吗?
作者本人可认领并核对本条目;认领通过后,详情页会显示「作者已认领」标记。
认领条目 →
回到顶部