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

AI 工作台 Uid-762/dsh-ai-workbench

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

说一句需求,让 AI 造出可缩放窗口并持续修改

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

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

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

状态速览

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

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

验证判据与等级说明 →

可信度证据

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

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

读写/删除本地文件发起外部网络请求
查看扫描证据(共 36 处,展示前 29 条)
  • 读取本地文件
    src/artifacts.ts:111
    realBase = await realpath(base)
  • 读取本地文件
    src/artifacts.ts:116
    realCandidate = await realpath(candidate)
  • 读取本地文件
    src/artifacts.ts:153
    info = await stat(resolved.path)
  • 读取本地文件
    src/artifacts.ts:163
    const nestedInfo = await stat(nested.path)
  • 读取本地文件
    src/artifacts.ts:182
    const bytes = await readFile(path)
  • 读取本地文件
    src/artifacts.ts:217
    text = await readFile(resolved.path, 'utf8')
  • 读取本地文件
    src/bundle.ts:127
    names = await readdir(resolved.path)
  • 读取本地文件
    src/bundle.ts:142
    info = await stat(child.path)
  • 读取本地文件
    src/bundle.ts:161
    text = await readFile(child.path, 'utf8')
  • 写入本地文件
    src/bundle.ts:346
    await mkdir(folder, { recursive: true })
  • 读取本地文件
    src/bundle.ts:354
    await stat(target)
  • 写入本地文件
    src/bundle.ts:362
    await mkdir(dirname(target), { recursive: true })
  • 写入本地文件
    src/bundle.ts:363
    await writeFile(target, file.text, 'utf8')
  • 发起网络请求
    src/client/store.ts:558
    response = await fetch(`${ROUTE_API}${path}`, init)
  • 写入本地文件
    src/index.ts:277
    await writeFile(path, JSON.stringify(exported.bundle, undefined, 2), 'utf8')
  • 写入本地文件
    src/ledger.ts:382
    await mkdir(dirname(path), { recursive: true })
  • 读取本地文件
    src/ledger.ts:385
    text = await readFile(path, 'utf8')
  • 写入本地文件
    src/ledger.ts:416
    await rename(path, quarantine)
  • 读取本地文件
    src/ledger.ts:449
    legacyText = await readFile(legacyPath, 'utf8')
  • 写入本地文件
    src/ledger.ts:912
    await writeFile(temp, text, 'utf8')
  • 写入本地文件
    src/ledger.ts:913
    await rename(temp, this.path)
  • 写入本地文件
    src/ledger.ts:923
    await writeFile(this.path, text, 'utf8')
  • 删除本地文件
    src/ledger.ts:924
    await unlink(temp)
  • 删除本地文件
    src/ledger.ts:929
    await unlink(temp)
  • 发起网络请求
    src/market.ts:170
    const response = await globalThis.fetch(url, init)
  • 读取本地文件
    src/routes/api.ts:568
    const entries = await readdir(target, { withFileTypes: true })
  • 删除本地文件
    src/routes/api.ts:575
    await rm(path, { force: true })
  • 删除本地文件
    src/routes/api.ts:579
    await rmdir(target)
  • 读取本地文件
    src/routes/api.ts:1424
    pkg = JSON.parse(await readFile(join(project.folder, 'package.json'), 'utf8')) as { version?: unknown; description?: unknown }
扫描于 2026/11/5(0 天前) · 范围 全包扫描(包内未声明入口) · 来源 GitHub 仓库

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

社区实装还没有人打卡 · 装过的第一个就是你
依赖的 DSH / Cordis 模块
@deepseek-ai/schemastery@deepseek-ai/cordis@deepseek-ai/dsh-agent@deepseek-ai/dsh-host-webserver@deepseek-ai/dsh-settings@deepseek-ai/dsh-skill@deepseek-ai/dsh-tools
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

用一句大白话让 AI 在 DSH 里造一个可缩放窗口,然后继续跟它聊 — DeepSeek Harness 插件

README

由 DeepSeek 最新模型翻译生成
dsh-ai-workbench · AI 工作台 一句话: 在 DSH 里说一句大白话,AI 就给你造一个能长期存在的窗口——然后你继续跟它聊,它继续改。 ▲ 图 1 · 中央面板:一句需求 → 几个问题 → 一个窗口 【贴图位 1】 你在侧边栏的「AI 工作台」里写下「我要一个记录每天喝水的打卡表」。工作台先问你几个小白也能答的问题(要不要每周自动总结?想要什么风格?主要用来干什么?),你确认之后,模型把一张自包含的单文件 HTML 写进项目文件夹,插件自动把它挂成一个可缩放、可拖动的窗口。窗口右边有一个绑定的对话窗,你可以在那儿继续让它改;窗口里点了关键按钮,这条动作会回传给同一个会话里的模型,模型改完页面,窗口自己刷新。 不用写代码,不用装构建工具,也不用离开 DSH。
展开完整文档(README 剩余 1 节)
dsh-ai-workbench · AI 工作台

一句话: 在 DSH 里说一句大白话,AI 就给你造一个能长期存在的窗口——然后你继续跟它聊,它继续改。

▲ 图 1 · 中央面板:一句需求 → 几个问题 → 一个窗口 【贴图位 1】

你在侧边栏的「AI 工作台」里写下「我要一个记录每天喝水的打卡表」。工作台先问你几个小白也能答的问题(要不要每周自动总结?想要什么风格?主要用来干什么?),你确认之后,模型把一张自包含的单文件 HTML 写进项目文件夹,插件自动把它挂成一个可缩放、可拖动的窗口。窗口右边有一个绑定的对话窗,你可以在那儿继续让它改;窗口里点了关键按钮,这条动作会回传给同一个会话里的模型,模型改完页面,窗口自己刷新。

不用写代码,不用装构建工具,也不用离开 DSH。

▲ 图 2 · 浮层窗口:左边是产物,右边是绑定的对话窗 【贴图位 2】

| 想做什么 | 去哪一节 |
| 30 秒装上跑起来 | 安装 |
| 看它长什么样、能干什么 | 四个区域 + 上面的图 |
| 一句话 → 一个窗口 是怎么闭环的 | 动作回传:只发关键动作 |
| 装别人做好的工作台 | 模板市场 |
| 给模型配的 workbench_window 工具 | 给模型用的工具 workbench_window |
| 所有可改的配置项 | 配置 |
| 为什么这样设计、踩过哪些坑 | 设计取舍 · 踩坑清单 |

它解决什么问题

DSH 里已经有对话,但对话是线性的。你想要的是一个长期存在的界面:一个看板、一个打卡表、一个小计算器、一个能盯着某件事的面板。这类东西让模型每次用文字回答都很别扭——它会重复输出同一段 HTML,你还得自己另存、自己打开。

这个插件把那条路打通了:

1. 你说一句需求。
2. 工作台问几个具体问题(可选,可跳过),你确认。
3. 模型在项目文件夹里写一个 HTML 文件。
4. 插件读同目录的 _workbench.json,知道入口文件和哪些动作值得回传。
5. 窗口以 iframe 挂进 DSH 的浮层,跟着主题走,可拖动、可缩放、可全屏。
6. 窗口右侧的对话窗绑定同一个会话,你可以说「把颜色改成蓝色」。
7. 你在窗口里点了一个被声明过的按钮 → 动作回到当前会话 → 模型改文件 → 窗口刷新。

第 7 步是闭环的关键,也是这个插件和「让模型输出一段 HTML」最大的区别。第 2 步是「小白能用」的关键:绝大多数人说不出自己想要什么,但能回答「你主要用它记录什么」。

▲ 图 3 · 先问后建:5–7 个带推荐选项的问题,可跳过 【贴图位 3】

四个区域

侧边栏「AI 工作台」行

sidebar.panellist 上的一个图标行(order 56,排在已装插件之后)。点它,中央面板切换成工作台;再点一次会话图标就回到对话。

中央面板(创作面)

- 需求输入框:唯一必须填的东西。Enter 发送,Shift+Enter 换行。
- 提问区(intake):写完需求后,面板先列出 5–7 个问题,每题带几个推荐选项,也可以自己补一句话。确认后才发给模型。点「就按描述做」可以跳过,让模型在构建时自己问。
- 项目列表:一个项目 = 一个文件夹 = 一个模型会话。换项目就是换上下文。
- 窗口列表:每个窗口显示标题、入口文件、声明的动作、状态徽章、💬 对话按钮。
- 导入 / 导出:把整个工作台导出成一个 .dshwb.json 文件(页面已内联,可直接发给别人),或从文件导入。同一个文件导入两次会得到两个工作台。
- 模板市场:拉 market.dshdesktop.com 的目录,一键装别人做好的工作台。
- 动态:Host 记的日志,实时推送(SSE),显示「已发出 / 已送达」计数。
- 底部状态:SSE 是否连着、当前项目绑的是哪个会话。

浮层窗口(产物面 + 对话面)

shell.overlay 上的浮层。每个窗口是一张可拖动的卡片,里面左边是 iframe,右边(点 💬 展开)是绑定的对话窗。标题栏有对话、最小化、全屏、重新加载、关闭。右下角可拖拽改尺寸。关掉窗口只是不显示,文档还在账本里,随时能再打开。

对话窗绑的是项目而不是窗口:一个项目一份文件夹、一个会话、一份 manifest,所以同项目的两个窗口共享同一份转录——这是诚实的模型,不是偷懒。

窗口标题栏的「打开方式」有两种,鼠标悬停在芯片上会显示各自适合的场景:

| 打开方式 | 悬停说明 | 什么时候用 |
| 应用内浮层 | 浮在本面板上方显示;适合边聊边看的窗口 | 要盯着它同时继续聊天的看板、通知板 |
| 独立标签页 | 在浏览器独立标签页打开;适合要全屏用一阵的窗口 | 表格、编辑器、要专心用十分钟的工具 |

内置作者技能

插件启动时会往 DSH 的技能注册表里注册一个内置技能 ai-workbench-authoring。它包含产物契约、主题变量白名单、沙箱陷阱、动作回传协议、响应式要求、前端设计原则、反模式清单和 10 个可查的资源站点。模型在写工作台页面时能直接读到它,不需要你单独安装任何技能。

模板市场

除了自己造,还能一键装别人做好的工作台。面板上的「市场」拉 market.dshdesktop.com/index.json,把每条列成一张卡片:标题、作者、版本、体积、 md5/sha256、有没有能直接跑的静态页。点「安装」走的是和「导入 .tgz」完全相同的解包路径:内置零依赖 tar 解包器解开,按排名挑出真正的页面入口,落成本地项目。

▲ 图 4 · 模板市场:标题、作者、版本、能否直接运行一目了然 【贴图位 4】

安装和删除都会立即刷新市场列表——卡片上的「已安装」徽章、可运行标记跟着账本实时重算,不用刷新页面,也不会出现「删了还显示已装」的旧状态。

市场包分三种形态,插件不猜、不装样子,卡片上直接标出来:

| 形态 | 卡片显示 | 装完得到 |
| 自带构建产物的静态页 | 可直接运行 | 一个能立刻打开的窗口 |
| 纯 dsh-desktop 组件(需要 desktopWorkbenches 服务) | 仅描述页 | 一页「这是谁做的、怎么装回去」+ 「让 AI 搭建」按钮 |
| 页面壳引用缺失的构建产物 | 仅描述页 | 同上——这种比上一种更危险,因为它假装 ready |

第三种是真实存在的(比如 standalone/index.html 引用 /assets/standalone.js,而那个文件要 npm run build 才会有)。插件用 referencesMissingAssets() 把这种壳降级成描述页路线,而不是端给用户一个卡死的加载页。「让 AI 搭建」按钮就是它的出路:模型读包源码,在你的项目文件夹里写出一个自包含的 index.html。

网络说明: 市场包托管在 GitHub Releases。如果 github.com 不通(常见于国内网络),插件会自动改用镜像重试,并把镜像的耗时放宽到 180 秒——直连仍保持 30 秒快速失败,不会让面板卡住。镜像偶发截断时,sha256 校验会拒绝而不是给你装一个坏工作台。

动作回传:只发关键动作

这是刻意做窄的一条通道。页面里写:

今天喝了

并在 _workbench.json 里声明:

{
"title": "喝水打卡", "entry": "index.html", "ready": true,
"actions": [
{ "name": "check-in", "label": "打卡", "description": "用户记录今天喝水" } ] }

只有 check-in 会到模型那里。页面里其他所有交互——展开、切换标签、翻页、输入——都留在本地,不消耗 token。

未声明的动作会被丢弃,并且记一条 warn 日志。这个判断在父窗口做,不在 iframe 里做:iframe 是不可信内容,它自己说自己「已声明」不算数。声明列表每次都从账本重读,所以模型改了 _workbench.json 之后不需要刷新页面就生效。

消息里的 source 字段用于区分来源,父窗口还会校验 event.source 确实是某个已知窗口的 contentWindow——别的 iframe 或上层页面伪造不了。

文件放在哪

| 东西 | 路径 |
| 账本(项目、窗口、日志、对话) | <DSH home>/dsh-ai-workbench.json |
| 产物 | <DSH home>/ai-workbench/<项目名 slug>/ |
| 页面清单 | 产物文件夹里的 _workbench.json |
| 导出的工作台 | 你自己选的位置,后缀 .dshwb.json |
| 路由 | /apps/ai-workbench/api/、/apps/ai-workbench/events |
| iframe 源 | /apps/ai-workbench/api/site/<projectId>/<相对路径> |

<DSH home> 的解析顺序:配置里的 dshHome → $DSH_HOME → ~/.dsh。

产物文件夹是你的:用编辑器、用 git、用别的工具直接改都行,插件下次读页面时就会看到。账本只记「这个窗口指向哪个文件」,不记文件内容。

从上一版升级: 账本文件名从 dsh-window-forge.json 改成了 dsh-ai-workbench.json。首次启动时如果新文件不存在、旧文件存在,会读旧文件继续用(旧文件保留不删),所以已有项目不会丢。清单文件名同理:先找 _workbench.json,找不到再找 _forge.json。

安装

1. 打包(在项目根目录)
pnpm pack --pack-destination .

2. 装进某个 profile
dsh plugin --profile <profile> add ./dsh-ai-workbench-0.8.1.tgz

桌面版(Electron)不能用 CLI 装。 它对 --profile desktop 会直接报:

error: profile "desktop" is managed exclusively by the Electron application

桌面场景请走应用内的插件安装入口,选择上面那个 .tgz。

升级注意: 这个包改过名字(原 dsh-window-forge)。如果旧包还装着,请先卸旧再装新。两个包同时启用会各自注册 /apps/ 前缀路由,虽然前缀不同不会撞车,但会同时出现两个侧边栏图标、两套账本。见踩坑第 9 条。

想先在隔离环境里试,可以造一个一次性 profile:

dsh workbench-lab --from-default-profile web --dump-config
dsh plugin --profile workbench-lab add ./dsh-ai-workbench-0.8.1.tgz
dsh --profile workbench-lab --port 0 --no-open

最后一行会打印 http://127.0.0.1:<port>/?token=<token>,浏览器打开就能看到。

装完重启(或刷新页面),侧边栏底部会多出一个「AI 工作台」图标。

构建

node scripts/clean.mjs
node scripts/css-types.mjs # 生成 CSS Module 的类型 sidecar
node node_modules/typescript/bin/tsc -p tsconfig.json
node node_modules/tsdown/dist/run.mjs
node scripts/verify.mjs

tsc 只产出 lib/types/*(声明文件);tsdown 产出 lib/index.js(Host,ESM)和 lib/client.js(浏览器端,CJS 包装成 __ModuleLoader__ 握手)。

scripts/css-types.mjs --check 只比对不写盘,适合 CI。scripts/verify.mjs 是静态契约检查:文件树、manifest、patch 守卫、客户端 bundle 纯净度、三个席位都走 registry.inject 注册、协议常量、内置技能是否进包、两份字典的键对齐、CSS 类名对账。
它不构建、不安装、不启动任何东西。

本机的坑

pnpm 的全局 shim 是坏的(pnpm: the global target of the pnpm shim points back at
the shim)。所以:

- 构建脚本一律用直接调 node 的方式,不要 npx / pnpm run。
- dsh plugin 需要 pnpm,把真实 pnpm 目录放在 PATH 最前面即可: powershell $env:PATH = 'C:\Users\<you>\AppData\Local\pnpm\global\v11\<hash>\node_modules\pnpm;' + $env:PATH

桌面应用内的插件管理器不受影响——它用自己的 pnpm,不走 PATH。

配置

Config 会被 DSH 自动投影成设置页,不需要自己写界面。面板上的开关状态由 Host 通过 GET /state 的 features 字段下发,不在浏览器里存第二份——你在设置页改完,刷新面板就生效。

| 键 | 默认 | 说明 |
| dshHome | '' | 覆盖 DSH 家目录;空则按上面的顺序解析 |
| ledgerFile | dsh-ai-workbench.json | 账本文件名 |
| serveHttp | true | 关掉就不注册任何 HTTP 路由 |
| registerTool | true | 关掉就不注册 workbench_window 工具 |
| registerSkill | true | 关掉就不注册内置作者技能 |
| interviewFirst | true | 关掉则需求直接进构建,不先提问 |
| chatPane | true | 关掉则不显示窗口右侧的对话窗 |
| marketUrl | https://market.dshdesktop.com/index.json | 模板市场目录地址 |
| journalActions | true | 是否记录动作日志 |
| maxJournalEntries | 200 | 日志上限(0–2000) |
| forwardActionsToSession | true | 关掉则动作只记账本不发给模型 |
| maxProjects | 20 | 项目上限(1–200) |
| maxWindowsPerProject | 12 | 单项目窗口上限(1–100) |
| defaultProjectName | 工作台项目 | 自动建项目时的名字 |

给模型用的工具:workbench_window

模型可以主动调用它,在你还没打开面板时就把工作台拉起来:

[workbench-window]
status=opened
projectId=project-...
project=喝水打卡
folder=C:\Users\you\.dsh\ai-workbench\喝水打卡
reused=false
windows=1
listeners=1
manifest=_workbench.json
ledger=C:\Users\you\.dsh\dsh-ai-workbench.json
revision=12
next=...

action 有五个值:

| action | 作用 |
| open(默认) | 打开面板、绑定会话、返回项目信息 |
| create | 同上,但允许新建项目 |
| ask | 把 question 放进工作台的对话窗,然后停下等你回答 |
| building | 把最新窗口标成「生成中」,告诉你它开始动手了 |
| export | 把项目导出成 .dshwb.json 写到项目文件夹,返回路径 |

reused=true 表示那个文件夹本来就是一个项目——模型此时应该编辑现有文件,而不是
推倒重来。listeners 是诚实的订阅者数量——0 表示没人在看,面板没开。工具调用时会把调用方的 sessionId 绑到这个项目上,这是插件唯一可靠地知道「用户在看哪个会话」的途径(shell.overlay 是 root 作用域的席位,拿不到会话上下文)。

工具不接收页面内容。页面由模型自己用文件工具写进 folder,这样:一份内容不会在上下文里出现两次;文件是你的,可以改、可以提交、可以复用。export 同理——它写盘并只返回路径,不把 bundle 内容塞进工具结果。

和 dataelement/dsh-desktop 工作台的关系

dsh-desktop 定义了一套工作台包规范(docs/workbench-standard.zh.md):一个工作台是一个 npm 包,带 dsh.bundle.patch、exports["./client"],客户端在 apply 里调
ctx.desktopWorkbenches.register({title, repository, …}, BusinessPanel) 把自己注册进
桌面壳的分栏里。

本插件跑在官方 DSH 0.2.0-rc.2 上,这个 build 没有 desktopWorkbenches 服务,所以做不到运行时注册。做的是格式级兼容:
- 能读 dsh-desktop 的包:POST /apps/ai-workbench/api/import/package 下载 .tgz(或 .tar.gz),用内置的零依赖 tar 解包器解开,取出里面的 HTML 入口和资源文件,落成一个本插件的工作台。包里没有 HTML 时,会生成一页「这是谁做的、怎么装回去」的描述页,而不是假装成功。
- 能读市场目录:GET /api/market 拉 market.dshdesktop.com/index.json,校验 schemaVersion: 2、id === owner/repository、url、distribution.version 等字段(fail closed,任何一条不符就整份目录作废),面板上逐条列出,点「安装」走同一条解包路径。
- 能导出成自包含的 .dshwb.json,可以发给别人、也可以用文件工具手动塞进任何项目。

做不到的部分会如实标注:市场卡片上 runnable: false 的条目会显示「仅描述页」徽章,因为本机没有那个运行时。README 不宣称「已兼容 dsh-desktop 的注册协议」。

设计取舍(为什么是这样)

用 iframe,不自己渲染。 模型产出的是 HTML/CSS/JS,用自己的 DOM 渲染等于写一个浏览器。iframe 天然隔离样式和脚本,也让「模型写的代码出错」不会拖垮 DSH 本身。

单文件 HTML。 没有构建步骤,没有依赖解析,没有 CDN。模型能一次写对的东西,就不要引入一个工具链。需要多文件也行(entry 指向任意相对路径),只是默认推荐单文件。

先问后建。 直接拿一句模糊描述去生成,产出的是「基于猜测的通用页面」,用户丢掉重来的概率很高。先问 5–7 个具体问题,多一次往返,但答案是真的。这一层在面板里做而不是让模型做,因为面板不需要会话就能用、每次问题一致,而且用户能看见自己在填什么——这是表单和对话的区别。用户如果跳过了,buildInterviewText 会让模型在构建前自己问一次。

项目 = 文件夹 = 会话。 这三者绑定,是因为模型需要一个稳定的工作目录来放产物,而人需要一个稳定的上下文来追问。换项目 = 换文件夹 = 换会话,语义一致。

对话窗绑项目,不绑窗口。 一个项目一份文件夹、一个会话、一份 manifest,所以同一项目的两个窗口本来就共享上下文。假装每个窗口独立反而会让人以为它们互不影响。

动作通道做窄。 每个声明过的动作都会往会话里塞一段文字,消耗 token。如果所有交互都回传,一个翻页就能烧掉半轮上下文。所以默认什么都不发,页面自己声明。

账本单文件 + 原子写。 项目和窗口的元数据很小(几十 KB),一个 JSON 文件够用,比引入数据库简单得多。写入走临时文件 + rename,读坏了就隔离成 <name>.corrupt-<timestamp> 再重建,不阻塞启动。

客户端不 import 任何 harness 包。 浏览器端的模块表是冻结的九项(react、react/jsx-runtime、react-dom、react-dom/client、@deepseek-ai/cordis、@deepseek-ai/dsh-client-store、@deepseek-ai/dsh-client-ui-slots、@deepseek-ai/dsh-client-ui-primitives、@deepseek-ai/dsh-client-ui-dockkit)。服务通过结构化接口 + 运行时类型守卫获取,@deepseek-ai/ 只做 import type,编译后不留任何引用。tsdown.config.ts 里的 purityGate() 会在构建期拒绝任何越界的值导入。

tar 和 market 自己写,不引依赖。 node:zlib 已经给了 gunzip,tar 的 512 字节头格式四十年没变过,写一个只读解包器(src/tar.ts,328 行,带路径逃逸和 gzip bomb 防护)比引一个依赖更可控。市场目录的校验同理——宁可自己写 200 行 fail-closed 的 schema 检查,也不让一份坏目录把面板搞崩。

踩坑清单

这些都是真的踩过、并且已经在代码或检查里处理掉的。

1. dsh.client.inject 必须写有 ./client 导出的包。
@deepseek-ai/dsh-client-ui-slots 在浏览器的模块表里,require() 能解析,但它没有 ./client 导出,所以启动清单不会为它建行,写进 inject 等于什么都没排序——静默无效,不报错。slots 这个服务真正的提供者是 @deepseek-ai/dsh-client-ui-renderer。scripts/verify.mjs 会逐个检查这件事。

2. 别人的席位必须用 ctx.slots.inject(name, () => register(...)) 注册。
main、sidebar.panellist、shell.overlay 全是 shell 自己声明的席位,而第三方 client half 的 apply() 跑在 shell 半体组合之前——裸调 registry.register() 直接抛 slot "…" is not declared(只出现在 console 里,容易漏看)。inject 会等声明到达再执行注册,声明重建时自动重挂。另外 sidebar.panellist 和 main 要成对:shell 会拒绝选中一个「面板还不存在」的侧边栏行。

3. main 是 keyed 槽,replaceRisk 是 shadows-shipped-ui。
它的 key 域里已经占了 conversation。用 ai-workbench 这样的自有 key,绝不复用别人的。

4. shell.overlay 是 list 槽,而且 .layer 必须 pointer-events: none。
整层是覆盖全屏的 fixed 容器,只有 .window 和 .launcher 重新打开指针事件。忘了这一条,工作台一挂载整个应用就点不动了。

5. SSE 的每个事件都有名字,onmessage 永远不触发。
Host 发的是 event: state\ndata: {...},客户端必须对 hello/state/project/ window/action/log/chat/open 逐个 addEventListener。新增事件时最容易忘的就是这个数组。

6. CSS Module 的类型要 sidecar。
declare module '.module.css' 给的是 Record<string, string>,在 noUncheckedIndexedAccess 下每个 styles.x 都读成 string | undefined,赋给 className 就报错。scripts/css-types.mjs 生成的 styles.module.css.d.ts 用显式键覆盖掉通配声明——既修类型,又让拼错类名变成编译错误。加类名之后记得重跑。

7. 客户端 bundle 的 id 必须等于包名。 启动清单按包名建图,对不上就是加载了一个谁也解析不到的 id。tsdown.config.ts 的 const ID 是唯一来源。

8. Host 的 inject 故意为空。 硬 inject 是激活门:声明了但没被提供的服务会把 fiber 永久 park,apply() 永远不跑,整个页面还会报 web boot: N entries did not activate。这个插件的每个服务都有降级路径(没有 webServer 就只做账本,没有 tools 就不注册工具,没有 skills 就不注册技能),所以一个都不硬依赖,
全走 ctx.inject([...], (scope) => ...)。

9. 重复挂载会让整个应用起不来。 一个 profile 里如果有两行指向同一个包名,两条
路由前缀会在组合期撞上,重复的 (kind, path) 注册是直接抛错,不是跳过。所以
cordis.patch.yml 里那一行带了 disabled: !!js 守卫,并且 guardRegistration() 把注册期的冲突降级成一条 warning。改包名(本版从 dsh-window-forge 改过来)时,务必先卸旧包再装新包,否则两个包各注册一套前缀路由、各开一套账本。

10. ctx.sessionController.prompt() 只接受非空内容。 空字符串会被 gateway/bad-request 拒掉,而且 requestId 是幂等键——重试用同一个 id 不会产生第二次投递。

11. resolveArtifactPath() 做两次包含性检查。 一次是词法上的(挡 ..),一次是 realpath 之后的(挡符号链接)。只做前者,一个指向外部的软链接就能读到整个磁盘。另外 .exe/.dll/.bat/.cmd/.ps1/.sh/.node/.env 一律拒绝。

12. iframe 的 CSP 要放开 frame-ancestors。 产物是从同源的另一条路由加载的,但它要嵌在 DSH 页面里。默认的 frame-ancestors 'none' 会让它白屏。

13. 账本写入要防抖 + 原子。 拖窗口会一秒触发几十次 PATCH,每次都写盘既慢又
容易写坏。flush() 合并写入,落盘走临时文件 + rename。

14. 拖动窗口的事件监听要挂在 window 上。 指针一动就会离开 38px 高的标题栏,挂在元素上会中途丢事件。

15. pnpm 全局 shim 坏了(见上面的「本机的坑」)。这是本机环境问题,不是插件的问题,但会让 dsh plugin add 以一个和插件无关的错误失败。

16. 客户端服务在 apply() 时还没就绪,必须惰性解析。 shell 的客户端服务(sessions/conversation/uiWorkspace)比第三方 client half 晚约半秒组合。
在 apply() 里 ctx.get() 一次存下来,拿到的永远是 undefined——而且不报错。
存的必须是 resolver(() => ctx.get(name)),用的时候再读。

17. conversation.sendSession 返回 {kind:'success'|'error'},不是 RemoteResult。
按 ok===false 判失败会把真失败当成成功。SessionFace.prompt 才是
{ok:true,value}/{ok:false,error}——两个返回形状不同,别混。

18. 模型写完文件不会产生任何账本写入,所以不会有 SSE 事件。 客户端会永远停在「生成中」。修法是读时对账(src/reconcile.ts):GET /state 时 Host 读一遍 _workbench.json 和 entry 文件,与账本有差异才写(只写真差异,避免轮询自激);客户端在 building 期间每 2 秒轮询一次 load()。

19. 任务文本里绝不能让模型裸用 localStorage。 沙箱 iframe 故意不给 allow-same-origin,origin 是不透明的,任何 localStorage 访问都抛 SecurityError。最恶性的形态是异常发生在 complete() 内部、把 postAction 炸断在动作发出去之前——页面看着正常,闭环就是不通。task-text.ts 和内置技能都明确要求:状态存内存;要持久化必须整段 try/catch 静默退回内存,绝不打断主循环。

20. 一个文件夹只能是一个项目。 模型收到建窗任务后会自己调 workbench_window {action:'create', folder:<任务里的文件夹>},工具若无条件建项目,账本里就会出现第二个指向同一文件夹的空壳,manifest 查询从此有歧义。查重在账本层做(normalizeFolder 归一化后查重复用),HTTP 面板和模型工具两条路都过这里。

21. 客户端这一代没有 sessions.create()。 想新建会话只有两条路:uiWorkspace workspace 入口(会把用户拖进一个新工作区),或者让 Host 建。
sessionController.create({cwd}) 在 Host 侧可用,所以 POST /session 由 Host
建会话、把 id 写回项目行,客户端只负责调用。cwd 是关键:不传它,模型会把产物写到进程工作目录,而不是项目文件夹。

22. 对话窗里模型的回复只能从会话事件日志里读。 没有「列出这条会话的聊天记录」的客户端 API,但 Host 侧 sessionController.inspect(id) 能在不激活 agent 的前提下读全量事件(它不会开启新回合)。从 assistant/message 事件里取 text 块,时间戳用事件自带的 time 而不是 Date.now()——这样轮询第二次时按 (role, at, text) 找到同一行,不会把同一条回复每轮追加一遍。

22b. 别把用户在 DSH 里的正常对话吸进工作台。 项目绑的会话就是用户平时聊天的那一个,所以「把最近的 assistant 消息都当工作台回复」会把用户和模型的私聊抄进对话窗——0.3.3 实测抓回来两段无关对话。修法是按出站标记配对:只有以 [workbench-chat] / [workbench-action] / 【工作台 · 开头的用户消息才算工作台提问,其后第一条 assistant 消息才是回答。这里的 armed 标志只能被带标记的用户消息置位、只能被消费掉的那条回复清除,不能被「无标记用户消息」清掉: harness 会在真实 prompt 之后追加 3 条注入的 user/message(runtime context、技能目录、时间戳),第一版实现把标志清掉了,结果每条回复都取不到(added=0)。
排查方法:会话存储是 <DSH home>/sessions/<cwd-slug>/<sessionId>/session.v4.jsonl.zstd,多帧 zstd,要按 magic 28 b5 2f fd 切帧再逐帧 zlib.zstdDecompressSync (整文件一次解只出第一帧,容易误判成「日志里没有这条消息」)。

23. 面板上的开关不要在浏览器里存第二份。 interviewFirst / chatPane 来自用户改的 Config,Host 通过 GET /state 的 features 下发。客户端自己存一份的后果是:用户在设置页关掉了,面板还在按旧值工作,而且没有任何报错。

24. descriptorPage 里的 rows 要显式标注类型。 在 noUncheckedIndexedAccess 下,Object.entries 解构出来的元素是 [string, string | undefined],直接传给模板会报 TS2345。

25. 改名不能只改包名。 真正要一起动的是:package.json 的 name、 cordis.patch.yml 的 name 与 id、tsdown.config.ts 的 const ID、 protocol.ts 里的 ROUTE_API/ROUTE_EVENTS/LEDGER_FILE_NAME/PANEL_ID/ SEAT_ID/ACTION_SOURCE、locale/.json 的命名空间、messages.ts 的 LOCALE_NS、verify.mjs 里的断言字符串。漏一个的表现各不相同(有的是静默不加载,有的是 404),所以 verify.mjs 把这些常量都钉住了。

26. 有页面、没窗口行的项目会永远看不见。 这是 0.3.4 之前真实发生的:模型把 index.html 和 _workbench.json 写进了文件夹,但窗口行只在「用户从面板提交需求」时才创建,所以从工具路径建出来的项目(以及从旧版迁移过来的项目)有项目行却没有任何窗口,面板里列着项目、点进去什么都不显示。根因是 reconcileWindows 原本对没有窗口行的项目直接 continue——它只修补已存在的窗口,从不补建窗口。
现在的做法是:没有窗口行、但 entry 文件真的能解析出来的项目,由对账收养(adoptProject)建一行并记一条 adopted "…" 日志。条件是「页面真的存在」,空文件夹和缺失文件夹保持无窗口,因为那才是诚实状态。

27. 收养必须能被删除按钮否决。 上面那条自愈逻辑有个直接的反作用:用户删掉窗口,下一次轮询发现文件夹里还有页面,就会把它收养回来——删除按钮看起来是坏的。所以删除走 dismissWindow() 而不是 removeWindow(),它同时在项目行上写 windowDismissed: true,收养路径见到这个标记就不动;而显式新建窗口会清掉标记,于是「删掉、再造一个新的」仍然成立。这个字段只认字面量 true(手改账本不能用 truthy 字符串压制收养),清除时删字段而不是写 false,好让内存形状和落盘形状一致。

28. 描述页 v1 的自愈必须发生在 serve 时刻,而不是 reconcile 时刻。 reconcile 的自愈只写账本里的 inlineHtml,但 handleSite 是磁盘优先的——磁盘上那一份 v1 描述页(带 desktopWorkbenches 标记、没有 wb-build 按钮)会继续被端给用户。用户的文件夹不该被插件重写(那是模型的领地),所以 serve 侧在「磁盘命中 + text/html +
isDescriptorPath(entry) + 内容含 v1 标记」四个条件同时成立时,现场生成 v2 页返回;
磁盘文件保持原样。

29. 'no such file' 是 missing,不是 forbidden。 resolveArtifactPath 对 ENOENT
返回 {reason:'no such file'},而旧版 readArtifact 把一切 resolve 失败都归为
forbidden——inline 描述页回退只对 missing 生效,于是「磁盘无根 index.html」的包(如 beianbai,真页面在 standalone/ 且缺构建产物)显式请求 /site/:id/index.html 时拿到 403 而不是描述页。判别标准是原因而非状态码:只有越权(路径逃出项目文件夹)和扩展名拒绝才是真正的 forbidden,文件不存在统一归 missing。

30. 判断市场包能不能独立运行,看的是包内容,不是愿望。 dsh-desktop 市场的包有三种形态:纯 React 组件(需要 desktopWorkbenches 服务,我们跑不了)、自带构建产物的静态页(能直接开)、页面壳引用缺失构建产物(开了也是卡死的加载页—— beianbai 的 standalone/index.html 引用 /assets/standalone.js,而 lib/standalone.js 要 npm run build 才有)。第三种比第一种更危险,因为它假 ready。所以导入路径用 referencesMissingAssets() 把坏壳降级成描述页路线,收养路径用磁盘版 referencesMissingAssetsOnDisk() 做同一件事;「让 AI 搭建」按钮就是为这两种包准备的出路——模型读包源码,在项目文件夹里写出自包含的 index.html。

更新日志

0.8.1(当前)

模板市场相关的五个问题一次修完,都是用户实际撞到的:

| # | 问题 | 根因 | 修法 |
| 1 | 装完市场包,卡片还显示「未安装」 | installFromMarket() 置了 marketBusy=true 后调 loadMarket(),而 loadMarket() 第一行就是 if (state.marketBusy) return——刷新被自己挡死 | finally 里先清 marketBusy,再 await loadMarket() |
| 2 | 删掉工作台,卡片还显示「已安装」 | deleteProject() 只刷新项目列表,从不刷新市场,徽标用的是账本里的旧 installedProjectId | 删除后 await load() 再 await loadMarket() |
| 3 | 某些市场工作台装完白屏/半屏 | 市场页的 localStorage 调用在外部 JS 模块里,而 storage shim 的门禁只在入口 HTML 文本里搜 localStorage 字样——搜不到就不注入。沙箱 iframe 是不透明 origin,setItem 直接抛 SecurityError,模块顶层炸掉,页面半绘 | 去掉文本门禁,shim 注入到每一个 served HTML 文档 |
| 4 | 「打开方式」两个芯片没有任何说明 | 只有 aria-pressed,没有 title,字典里也没有解释文案 | PLACEMENT_CHIPS 加 descKey,悬停显示「浮在本面板上方…」「在浏览器独立标签页…」 |
| 5 | 部分市场包装不上(did not finish within 30000ms / fetch failed) | 国内网络到 github.com 超时,插件有镜像兜底,但镜像复用直连的 30 秒预算,10–90 秒的任务被直接掐断 | 新增 PROXY_FETCH_TIMEOUT_MS = 180_000,镜像单独计时;两个都超时时报错区分是谁超时 |

顺带修掉的两个账本健壮性问题:flush() 遇到 EPERM: operation not permitted, rename
(杀毒软件/OneDrive 瞬时占用)会重试 3 次并退回原地写;重复安装同一个市场包现在复用它的文件夹并覆盖写入,而不是生成 <name> 2 再报 409。

目录

dsh-ai-workbench/
├── package.json manifest:入口、dsh.bundle.patch、dsh.client ├── cordis.patch.yml bundle patch(带重复挂载守卫) ├── tsconfig.json 严格模式 + noUncheckedIndexedAccess ├── tsdown.config.ts Host(ESM)+ Client(CJS 握手)两套配置 ├── locale/ │ ├── en.json 安装器读 meta.title / meta.description │ └── zh.json ├── scripts/ │ ├── clean.mjs 删 lib/(tsdown 是 clean:false,必须先跑) │ ├── css-types.mjs 从 stylesheet 生成 CSS Module 类型 sidecar │ └── verify.mjs 静态契约检查 └── src/ ├── index.ts Host 装配:账本、路由、工具、技能、设置 ├── config.ts schemastery Config schema ├── protocol.ts 两半共享的线上契约(常量 + 类型) ├── ledger.ts 单文件账本(原子写、损坏隔离、旧账本接管) ├── artifacts.ts 产物读取 + 路径安全边界 + 内联页回退 ├── reconcile.ts 读时对账:manifest/产物 → 账本(building→ready) ├── summary.ts 给模型的工作台上下文摘要 + 回复抽取 ├── bundle.ts 工作台 ↔ .dshwb.json ↔ dsh-desktop 包 的双向映射 ├── market.ts 市场目录拉取 + fail-closed 校验 + 分发地址推导 ├── tar.ts 零依赖只读 tar 解包(防路径逃逸、防 gzip bomb) ├── skill.ts 内置作者技能正文 + 资源清单 + 精简摘要 ├── routes/ │ ├── api.ts JSON 路由(mini-router,23 条) │ └── sse.ts 事件推送 ├── tools/ │ └── workbench_window.ts 模型可调用的工具(5 个 action) └── client/ ├── index.tsx 注册 main / sidebar.panellist / shell.overlay ├── Panel.tsx 中央面板(需求 + 提问 + 项目 + 窗口 + 导入导出 + 市场) ├── Intake.tsx 先问后建的问题表单 ├── Chat.tsx 窗口右侧的绑定对话窗 ├── Market.tsx 模板市场列表 ├── Windows.tsx 浮层窗口(iframe + postMessage 桥 + 对话分栏) ├── store.ts 客户端状态容器 + SSE + 会话发送 ├── task-text.ts 一句话 → 发给模型的提示词(建窗 / 追问 / 精修) ├── messages.ts en/zh 字典 + t() ├── use-store.ts useSyncExternalStore 绑定 ├── styles.module.css └── css-modules.d.ts

许可

MIT

截图占位清单

README 里预留了 4 个贴图位。截图后按下面的名字放进 docs/screenshots/,GitHub 会自动渲染,不用再改 README:

| 文件 | 对应 README 位置 | 建议内容 | 建议尺寸 |
| docs/screenshots/01-panel.png | 开头封面图 | 中央面板全景:需求输入框、问题区、项目/窗口列表、底部状态栏 | 1400×900 左右 |
| docs/screenshots/02-window.png | 开头第 2 张 | 一个真实工作台窗口(浮层)+ 右侧展开的对话窗 | 1400×900 左右 |
| docs/screenshots/03-intake.png | 「先问后建」段 | 问题表单填到一半、带推荐选项的状态 | 900×700 左右 |
| docs/screenshots/04-market.png | 「模板市场」段 | 市场列表:若干卡片,最好有一张已安装徽章 + 一张仅描述页徽章 | 1400×900 左右 |

深色/浅色各截一套更好:把 01-panel.png 存成 01-panel-dark.png,在 README 的 里换源即可(当前预留的是单图,换图不影响文字)。
预览分享卡片长什么样 ↓Uid-762/dsh-ai-workbench
分享 · DeepSeek Harness Hub
Uid-762/dsh-ai-workbench

说一句需求,让 AI 造出可缩放窗口并持续修改

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