← 返回列表
⚠ 装前注意
把 MCP 应用装进侧边栏,各自独立会话并内联渲染
基本兼容但装前注意:未发布到 npm registry,仅可从源码安装 · 最近上游提交 2026/8/16 · 已提供中文文档
将 open-mcp-apps 引入 deepseek-harness:应用作为侧边栏容器,拥有自己的会话、一个代理状态条,以及内联小组件渲染
综合分
31.8
GitHub 分
31.8
用户评分
—
★ Stars
7
周下载量
—
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add 2nd1st/dsh-plugin-open-app未发布到 npm registry,仅可从源码安装,改用 GitHub 源安装
数据截至 2026/9/15(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
安装兼容性检查⚠ 装前注意
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✗npm 包@2nd1st/dsh-plugin-open-app(未发布到 npm,仅可源码安装)
✓Node 引擎要求 >=22 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明
未发布到 npm registry,仅可从源码安装
验证方式:npm registry 存在性 + package.json 静态校验 · 最后验证 2026/9/18 21:09:27
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动
README
@2nd1st/dsh-plugin-open-app
npm
license
node
open-mcp-apps
让每一个 open-mcp-apps 应用都成为你在 DeepSeek Harness(dsh)中可以前往的地方:侧边栏中的 Apps 区域,每个应用一个容器,拥有各自的工作区和对话,并且当模型在聊天中途打开应用时进行内联渲染。
| | |
|---|---|
| 包 | npm 上的 @2nd1st/dsh-plugin-open-app — MIT(LICENSE) |
| 安装 | dsh plugin --profile web add @2nd1st/dsh-plugin-open-app — 一步完成 |
| 要求 | 带 web profile 的 dsh · PATH 上有 pnpm(dsh plugin 通过它管理 profile 依赖)· 一个正在运行的 open-mcp-apps v0.5.1+ · Node 22+ |
| 平台 | dsh web(dsh web)— 它占用的每个界面都是 web 插槽 |
| 发布 | CHANGELOG.md |
开发者预览版。 dsh 本身就是一个 0.1.0-rc 预览版,而本插件紧密跟随它——包括六处它通过未公开的接缝深入 dsh 的地方(这六处都列在它深入了什么,以及如果 dsh 变动会破坏什么下,并附有各自的降级表现)。预计各版本之间会有破坏性变更,如果你需要一个稳定的版本,请固定版本号。
一个应用容器——应用在它自己的标签页中,agent 的发言在它下方,输入框在最下面
安装
添加它——这就是整个安装过程
dsh plugin --profile web add @2nd1st/dsh-plugin-open-app
重启;插件元数据在进程生命周期内按名称缓存
dsh web
该包声明了 dsh.bundle,因此 dsh plugin add 不仅会把它放进 profile 的依赖中——它还会把它追加到 dsh.profile.bundles,而它附带的补丁会成为你 profile 树的一层。该层插入两行:open-app(本插件)和 mcp-oma(dsh 的 MCP 客户端,指向同一个引擎,这样模型也能像你一样打开和构建应用)。Bundle 层应用在你 profile 自己的 cordis.patch.yml 之下,因此这两行仍然由你配置或关闭。
这里没有任何东西改变应用被允许做什么:引擎是你启动的本地进程,而这些行只说明它在哪里监听。
从手写安装升级(在 0.1.1 之前,第二步是你自己粘贴一个 - insert: 块):从你 profile 的 cordis.patch.yml 中删除 open-app 和 mcp-oma 的那些块,并移除旧包名——
dsh plugin --profile web remove dsh-plugin-open-appmarkdown
dsh plugin --profile web add @2nd1st/dsh-plugin-open-app
补丁 insert 总是追加:两个层插入同一个 id 就是两行,dsh 会拒绝启动这样的树
(duplicate loader entry id: open-app),而不是把插件运行两次。请改为将你的设置保留为
以 id 为目标的覆盖项。
要求
- 带有 web profile 的 dsh(dsh web)。此插件占用的每个界面都是 web 插槽。
- pnpm 位于 PATH 上。 这不是此插件所依赖的东西:dsh plugin 是一个轻量转发器,
它在 profile 目录中运行 pnpm,因此任何 dsh 插件都是这样安装的。
没有它,上面的安装命令会停在
dsh: pnpm not found on PATH — install pnpm to manage profile plugins 并以 127 退出。
此处未说明最低版本,因为没有测量过。
- 一个正在运行的 open-mcp-apps 引擎,且其 HTTP 接口已启动 — node src/http.mjs,默认
端口 8787。该引擎是单独安装的(其 README):
它为模型提供持久、可交互的 UI 应用,由持久数据集合支撑,
并与所有连接到同一引擎的其他宿主共享。此插件是它们在 dsh 中的界面。
- 引擎 v0.5.1 或更新版本。 该版本包含面板所依赖的三个接缝:
?chrome=0(使用裸 widget,而不是引擎自带的查看器栏)、?nav=intent
(应用→应用的链接会变成发给宿主的消息,而不是在框架内导航),
以及独立查看器根元素的 overflow-y:auto。面对较旧的引擎,插件
仍可工作并优雅降级 — 每种情况都在
已知限制 下说明。
- 可选,用于每个应用的内联视图: 以 OMA_DYNAMIC_TOOLS=1 启动引擎,这
正是让它为每个应用发布一个 open_ 工具的原因。通用的 open_app 工具
始终会被覆盖。
两个可选加入的查询参数都是增量式的:没听说过其中任何一个的引擎会忽略
它并保持自己的行为。对引擎没有其他要求,也没有更改任何 dsh 源代码 —
该插件完全在树外运行。
配置
每个设置都是可选的;默认值与原装本地引擎以及该 bundle 附带的 mcp-oma 行
相匹配。
通过在你的 profile 自己的 cordis.patch.yml 中命名该 bundle 的行 id 来配置它 —
绝不要再次插入该行。以 id 为目标的补丁会替换它所命名的键,
而不是合并到其中,因此请写出你想要的完整 config:
yaml
- id: open-app
config:
引擎 HTTP 接口的监听位置。
engineBase: 'http://127.0.0.1:8787'
引擎 mcp-client 条目的 serverName。它决定
内联工具视图所依据的 mcp____ 前缀。
serverName: 'oma'
每个应用工作目录的创建位置。每个应用一个目录,
以其名称命名。默认为 $DSH_HOME/storages/open-app/apps。
appsRoot: '~/.dsh/storages/open-app/apps'
容器的规则,作为系统提示词的一部分,随容器内智能体发出的每个请求一同携带。
它完全取代了随附的仅应用提示词(关于它必须继续履行的职责,见下文)。{app} 是
应用名称,{card} 是宿主端根据引擎注册表构建的声明卡片。空字符串会停用这些规则:
此时容器将作为普通的 dsh 会话运行,只是恰好有一个应用位于某个标签页中。
containerPrompt: |
你生活在 {app} 应用中——这段对话就是它的容器。
{card}
绝不要在这里调用 open_app、app_html 或任何 open_* 工具;此聊天旁边的
面板已经显示了该应用。用一句话回答——这一行就是用户在下方状态栏上读到的内容。
全新容器开启时的那一行内容,它的存在只是为了结束空白状态,并在条带上
绘制第一张回执。它不携带任何规则。{app} 会被替换。将其设为空字符串可
静默开启容器——此时它们会保持空白,直到你开口,而应用会等待在输入框
上方的一行中,而不是在它自己的标签页里。
installMessage: '应用里现在有什么?'
每个应用的固定状态以及每个应用的位置由什么构成,都按浏览器保存在
localStorage 中,键为 dsh-plugin-open-app:pins 和 dsh-plugin-open-app:containers
(未加作用域,并且会一直如此——该包在 0.1.1 中迁移到了某个作用域下,但重命名
存储键会悄无声息地取消掉所有人已固定的每个应用)。
只有固定状态是不可替代的:工作区注册才是持久绑定,因此清空容器映射没有任何代价——
下次访问会重新采用同一个目录、同一个工作区,以及已经归在其下的对话。
用法
应用是一个场所,而不是一个页面。打开 shopping-list 会打开购物清单以及
关于它的对话;明天打开它会恢复两者。因此每个应用都会获得一个属于自己的 dsh
工作区——一个由插件在其应用根目录下创建、并向 dsh 注册的目录——它的对话
就在该工作区内创建,绝不会在你的某个工作区中创建。在其中:
- 应用——应用的 UI,位于视图环的第一位,其下方有一条单行条带,
说明智能体当前正在做什么。
- 聊天——此应用的对话,你回来时它仍在那里。
- 轨迹——不变。
- 输入框停靠在所有这些的下方,因此你可以一边看着应用,一边在同一次操作中
让模型修改它。
在应用容器中,应用本身就是回复:你说“把牛奶标记为已购买”,那一行就会自己打上勾,
因为该组件与引擎保持着各自的实时连接。这就是为什么它下方的条带不是一个小型聊天
窗口——它是智能体的在场。它让你在同一眼看到被调用的工具及其引发的变化,并让
两件你本来可能错过的事变得不可能错过:一个等待你的问题,以及一次失败。
容器是排他的:在某个应用的会话内部,Apps 标签页只显示该应用,别无他物。无法从中浏览出去,因为能逃出去的容器就不是容器——切换应用意味着前往另一个应用的节点。应用目录存在于两个不属于任何容器的地方:侧边栏自身的浮层,以及普通聊天会话的 Apps 标签页。
它增加了什么
| | |
| --- | --- |
| 侧边栏中的 Apps 区域 | All apps(目录)、App Store(新应用的来源),以及每个已固定应用的一个节点。 |
| 应用容器 | 点击应用节点会打开该应用的工作区及其自己的会话,应用 UI 在上方,其历史记录在下方。 |
| 应用模式 | 容器的 agent 会获得自己的提示词——它在这里是谁、应用的卡片(它是什么、其行数据所在的集合、它们的形状、它声明的函数)、它不得调用什么、回复应有多短。这是一个系统提示词部分,为位于应用目录中的会话的每个请求重新组装,且对普通聊天会话毫无开销。容器的会话还运行在 App mode 预设上,因此 dsh 在它命名模式的地方命名该模式。 |
| 一句开场白 | 一个全新的容器会被问一个问题——“What is in the app right now?”——因为一个 dsh 从未与之交谈过的会话不会获得视图环,也因为该答案就是应用下方条带的第一行。 |
| Apps 标签页排在 Chat 之前 | 注册顺序为 -10,因此应用在前,对话在后。Chat 和 Trajectory 保留其位置。 |
| agent 的存在感 | 应用下方的一行条带:agent 此刻在做什么(它正在调用的工具、正在到达的答案)、它最后说了什么,以及——不容忽视地——它何时在等你或已失败。它读起来像言语,而非源码:该行设置在 dsh 自己的内容列中,模型的 markdown 被平铺读出,因为这里没有任何东西能对它进行排版。点击它可查看整个对话。 |
| 应用工作区不碍你的事 | 插件注册的目录会从工作区树和 New Session 选择器中隐藏,且 New Session 永远不会落在应用内部。 |
| 应用 → 应用停留在模型内部 | 从 App Store 中“Open X”会打开 X 自己的容器,而不是导航商店的框架(引擎 ?nav=intent)。 |
| 内联应用渲染 | 在普通聊天会话中,当模型调用 open_app(或每个应用的 open_ 工具)时,工具卡片会变成正在运行的应用,其下方有一个 Open in Apps 按钮,可带你进入该应用自己的容器。那个位置是给普通聊天用的:在容器内部,同样的调用正是开场提示词所禁止的,因为面板已经显示了该应用。 |
最后一个位置是普通聊天最常见到的:没有容器,没有自己的工作区——应用就直接出现在工具调用所在之处,对话继续围绕
它。框架下方的 Open in Apps 胶囊是继续前进的方式:这张卡片是别人对话中的一个实时应用,你在那里能对它做的一切,在它自己的位置也同样能做,下方有它的历史记录,还有一个针对它的输入框。
内联应用渲染——一次 open_app 工具调用变成了正在运行的应用,就在一个普通聊天中
故障排除
下面每一行都会在更下方详细解释;这张表是索引,不是答案的副本。
| 症状 | 它是什么 | 位置 |
|---|---|---|
| dsh 拒绝启动:duplicate loader entry id: open-app | 手写的 - insert: 块和 bundle 中的行是两行,却共用一个 id | 安装 → 升级 |
| 已安装,但什么都没出现 | 插件元数据按名称缓存,持续整个进程生命周期——重启 dsh | 安装 |
| 应用面板是空的,或者目录是空的 | 引擎没有启动,或者不在 engineBase 上 | 要求 |
| 会话中途创建的应用还没有 open_ 工具 | dsh 的工具表不会重新同步;通用的 open_app 可以覆盖它 | 已知限制 |
| App Store 的 Open* 在它自己的框架内导航 | 引擎早于 v0.5.1(?nav=intent) | 已知限制 |
| 一个很高的应用在折线处被截断 | 引擎早于 v0.5.1(查看器根节点 overflow-y:auto) | 已知限制 |
| 一个新容器没有标签环,应用位于输入框上方 | 会话仍然是空白的;开场白还没有发出 | 已知限制 |
| 容器的 agent 忽略了规则 | containerPrompt: '' 会停用它们;规则是一个系统区段,不是一条消息 | 容器提示词 |
工作原理
这个包提供了 dsh 插件的两半。
浏览器那一半占用了 dsh 专为这类扩展发布的五个槽位:用于 Apps 标签页的 conversation.view、用于会话还没有视图环之前同一界面的 conversation.input.dock、用于 Apps 区域的 sidebar.footer.action、用于浏览的 shell.overlay,以及用于内联接管的 tool.call.toolview。没有任何东西被遮蔽或替换——每个席位都是附加性的。
每个应用都是一个 dsh 工作区:宿主那一半创建 //,浏览器那一半注册它(workspaces.create({path}),它会以幂等方式采用一个已存在的目录),然后在该目录内创建应用的会话。这不是装饰。dsh 的 New Session 会复用一个工作区中空闲的空白会话——所以一个空置在你的工作区中的容器,正是你下一次 New Session 会接手的那个会话,而你崭新的聊天会悄无声息地变成应用的聊天。给应用它们自己的工作区,就把这种复用放到了它该在的地方:应用内部,在那里复用它自己空闲的会话才是正确答案。
应用的数据并不存放在那个目录中——它存放在引擎的存储里,共享
与其他所有主机都与同一个引擎通信。该目录是工作区的锚点,也是应用导出文件的落点。
每个应用界面都是一个指向引擎自身 /view/ 页面的 。这是硬性要求,而非便利之举:引擎只对来源为自身的调用者响应其 /rpc 和 /events 端点,因此加载引擎 URL 的框架是被信任的,而 srcdoc 框架——它会继承 dsh 页面的来源——则不被信任。加载真实 URL 也是保持应用实时更新正常工作的原因:该框架持有自己的 SSE 连接,因此模型写入的变更会出现在小组件中,无需 dsh 进行任何中介。
主机侧之所以存在,是因为同源规则同样阻止了插件自身的 React 树读取应用注册表。它在 dsh 的 Web 服务器上发布一个小型 API——与页面同源——并从 dsh 进程内部转发一份白名单的只读引擎工具(list_apps、app_store_list、get_app),在进程内部,服务器到服务器的请求完全不携带 Origin 头。白名单正是关键所在:这些路由没有引擎自身的任何保护,因此所有写入操作仍走 MCP,由主机侧的权限提示把关。
它还拥有两样浏览器完全无法拥有的东西:应用目录和容器提示词。提示词必须放在这里,因为提示词区段是主机平面的注册——dsh 从注册在其自身上下文上的区段组装每个请求——也因为其中的卡片是浏览器侧只能跨源访问的引擎数据。它的依赖关系也说明了这一点:路由需要 webServer,区段需要 systemPrompt(硬依赖:一个无法承载这些规则的 dsh 将运行没有任何规则的容器),而 agentPresets 则是可选接入的,因为按会话组合智能体的界面是 Web 界面。
容器提示词
应用容器的智能体表现得像它身处某处,这来自它自己的提示词。open-mcp-apps 为聊天主机编写其指导,其中核心动词是 open_app:一个没有自己屏幕的模型通过调用它把应用放到用户的屏幕上。而在这里,屏幕先于一切存在,因此容器提示词取代了那种姿态——一个身份、一组有界的动词、一条禁令,以及一个回复契约。
它是一个系统提示词区段,而不是一条消息。 主机侧向 dsh 的提示词注册表(ctx.systemPrompt.section)注册一个区段,dsh 将其渲染进每个请求。该区段是全局的,并按请求决定自己是否有话可说:dsh 将正在组装的智能体交给该区段(assembleContextFor、dsh-agent),该区段询问该智能体的会话是否正在此插件的某个应用目录内工作,如果不是,则回答空字符串——组装器在提示词拼接之前会将其丢弃。因此,普通的聊天会话不会携带此插件的任何痕迹,
甚至没有空行,而容器在请求的 system 字段中携带规则,这正是它们该在的地方。
由此得出三件事,而这也是它不再是插件最初几个开发构建中那条开场消息的原因:
- 它是最新的。 提示词会为每个请求重新组装,因此升级此插件会在下一个回合重新为每个已存在的容器制定规则。日志中的消息一旦发送就被冻结了。
- 它能在压缩后存活。 消息历史中的规则是可被摘要的;系统提示词不属于被摘要的历史的一部分。
- 第一个回合属于用户。 容器不再花费一次模型调用来回答自己的安装。
它并非与日志无关,假装不是这样是错的:每当渲染后的系统提示词和工具目录发生变化时(原因有 initial、resume、change),dsh 会将它们快照到会话中,作为一个 request/header 事件。那是一份审计记录,而非对话——它不是发送给模型的消息历史的一部分——但这正是为什么应用的卡片由一个只会向前移动的缓存提供:一张会随每次引擎打嗝而闪烁的卡片,每次都会写入一份头部快照。
空白会话正是开场白存在的意义。 一个 dsh 从未与之交谈过的会话是 blank,而空白会话没有视图环、没有标签栏、也没有头部:应用必须存在于输入框上方的一行中,位于主视觉问候语之下,在一个为尚未开始的聊天而布局的列中。那种状态下所有别扭之处都源于一个事实——会话是空的。因此,一个新容器会被问一个问题,而从那一刻起,它就是一个普通的会话,应用位于自己的标签页中。这个问题也是该栏的第一行:模型所回答的内容,就是用户在应用下方读到的内容。
应用模式是用 dsh 自己的词汇表达的同一事实。容器的会话在仍为空白时(dsh 只在那时接受预设)会被置于 app-mode 代理预设上,这就是新会话标签和会话头部所报告的内容。该预设是你的部署所默认的任何预设的一份副本,在此插件首次启动时通过 dsh 自己的创作路径制作一次——容器是一个普通代理,前面有一个应用,而手写一份组合会悄悄夺走它今天拥有的工具。它只是一个标签,没有任何承重的东西依赖于它:规则以应用目录为键,因此将容器切回标准模式只会改变头部显示的内容,别无其他。删除该预设,下次启动时会从当前默认值复制回来;一个完全不组合预设名册的部署(TUI)则根本没有标签可显示。
有一个默认值无法以那种方式复制。预设每个进程只组合一次,而 dsh 的 cordis 预设(创造模式)会将 Host Cordis 检查提供者注册到一个进程全局注册表中——因此它的副本会抛出 already registered,并且永远无法挂载,因为
原始挂载始终排在第一位:会话就是基于它创建的。当它是你的默认值时,第一个被打上标签的容器会告知宿主半边,副本会从你的部署所列出、既不是默认也不是当前这个的第一个预设重新生成(它自己的普通 agent,在标准 dsh 中为 standard),标签落在同一个容器上。组合旁边的 preset.yml 说明了它来自哪个预设以及原因。在修复之前创建的容器保留它们启动时所用的 agent——dsh 在会话不再为空的那一刻就固定了它的预设——因此是下一个容器才会显示该标签。
卡片由宿主半边根据 list_apps 和应用自身的声明(get_app 槽位 manifest)组装而成:名称、版本、用途、其行数据所在的集合以及这些行的结构,并且——仅当应用声明了它们时——它的函数,以及写明的 call_function 调用形式。它刻意保持简短,因为它会搭载在这个容器发出的每一个请求中,所以它只陈述 schema,绝不包含示例数据。在已发布的应用上测量,卡片长度为 194–474 个字符,这使得整个提示词在最大的应用上约为 1,300 个字符(约 330 个 token)。它从引擎读取、被缓存,并在超过十秒时于后台刷新——某个 section 会同步渲染,因此请求不能等待引擎,而引擎宕机的容器会保留它最后拥有的卡片,而不是忘记自己身处何处。
为什么这里禁止 open_app。 对话上方的面板已经是该应用,并且它持有自己到引擎的实时连接,所以再次打开它不会渲染出任何新内容。它真正做的是昂贵且永久的事:结果是完整的 widget HTML(在 settings 上测得约 17K token),而容器的对话是持久的,因此该负载会在该容器之后的每一轮中重新发送。在测试装置上测量:第一轮在没有该调用时花费 23.1K 输入 token,有它时花费 46.6K。因此该禁令涵盖 open_app、app_html 以及各应用的 open_ 工具,并且它被直白地声明——即使工具结果告诉你这么做也不行——因为工具结果确实会告诉你这么做:get_app_guide 说“用 open_app {app} 打开它;保存后会立即渲染”,而 save_app 自己的结果以 Show it NOW with: open_app {…} 结尾。它成立:在这个面板上对 DeepSeek-V4-Pro 测量,一个读取了数据、编辑了应用的 HTML 并添加了一行的容器进行了十二次工具调用,其中没有一次是 open_——包括在 get_app_guide 告诉它打开一个之后的那一步。这是仅限容器的规则;在普通聊天会话中,open_app 完全正确,插件会将其内联渲染。
它不必与引擎的 initialize 指令争论,因为 dsh 从不读取它们:packages/mcp/mcp-client/src/connection.ts:272 丢弃了 SDK 客户端的连接结果,并且代码树中没有任何东西调用 getInstructions()。唯一
到达模型的服务器撰写文本是工具名称、描述和模式
(packages/mcp/mcp-client/src/tools.ts:146-152 → ctx.tools.register)。如果之后的 dsh
开始转发它们,这里不会有任何变化:该禁令已经是无条件的。
回复契约是经过精心设计以适配该条带的。在容器中,应用就是回复
——你说“把牛奶标记为已购买”,那一行就会自己打上勾——所以模型的句子就是一张
收据,而应用下方的条带会显示它最后所说内容的前部,被压平
为一行并在 200 个字符处截断。这就是为什么提示词要求先说一句话:
第一句话就是用户读到的东西。在一次写入之后,它要求按名称说出变更,
而不是复述整个面板,因为面板已经显示了这些内容。
如果你通过 containerPrompt 替换模板,请保留那四项职责——身份、
{card}、禁令、一句话收据——无论你用什么语言撰写它。
两个占位符都是可选的:没有 {card} 的模板就是不会得到它。
由仍然将规则作为消息发布的构建所创建的容器会在其历史记录中保留该消息,
并在下一轮获得该部分。不会迁移任何内容:旧消息只是
那段对话中又说过的另一件事,它与该部分一致,而一个容器不值得
为此重写历史。新容器则会得到那一行问题。
它伸入什么,以及如果 dsh 移动会破坏什么
这个插件所做的六件事并不是任何已发布的接缝要求它做的——它读取或
调整 dsh 内部的某些东西。每一项都会退化为更普通的行为,而不是直接损坏,
并且每一项都列在这里,以便升级时有地方可查:
- 主视觉在容器中让位。 一个以插件
盖在 dsh 的 composer 栈上的属性为键的样式表会隐藏主视觉界面元素,并让应用填满该列。
它按结构选择(> :not([data-slot])),因为 dsh 的类名是
内容哈希的。如果该栈的形状发生变化,这些规则就会停止匹配,而空白
容器看起来就会像一个普通的空白会话,只是 composer 上方有一行应用。
只有在开场白没有发出的状态下才可达。
- 应用工作区被从 dsh 自己的投影中过滤掉。 该插件包装了
workspaces.list.getSnapshot,因此侧边栏树、新建会话选择器和 dsh 自己的
“最近工作区”都不再看到它们。有一个应用工作区会幸存于该
过滤器——也就是你正在查看其空白会话的那个——因为 ConversationRoot
会禁用一个空白会话的 composer,如果它找不到该会话的工作区。(实测:
移除该豁免后,输入框会显示“选择一个工作区以开始”,并且发送会被
禁用。)
- 新建会话被重新瞄准。 workspaces.startSession 被包装,因此位于
容器内的隐式新建会话会转到你自己最近的工作区,而不是
应用的工作区。从工作区行启动的新建会话仍然会准确转到该
行所指示的位置。
- 按下的是两个标签页,而不是选中。 视图选择存在于聊天入口所拥有的 store 中,没有服务动词:Apps 标签页通过其标签找到,而 Chat 则是第一个不属于我们的标签页。
- 应用面板是经过测量的,因为 conversation.view 没有可填充的盒子。 其高度是滚动视口减去 composer 座位,从 dsh 放在它们上面的两个属性(data-conversation-scroll、data-composer-seat)读取,并在每一轮重新读取——这些节点属于 dsh,会被替换,而留在已分离节点上的观察者会把高度冻结在一个已不存在的窗口尺寸上。不变式是:应用面板永远不会比空间更高:一个有待滚动内容的列,就是一个聊天会滚动到底部的列,这会把应用滑出视野。如果两个属性都找不到,面板保持其 320px 的起始高度,面板会很小,但不会损坏。
- 代理的那一行位于 dsh 的内容列中。 该条的规则及其警示色调横跨整个面板;其中的文本行被限制在 --dsh-composer-card-max-width(dsh 在会话根上声明的变量——今天是 calc(748px + 32px))并居中,因此代理与 composer 以及每条聊天消息在同一列中说话。如果某个 dsh 不再发布该变量,则回退到 780px;如果某个 dsh 更改了它,我们的行会随其他一切一起移动。
已知限制
- 开场白是每个应用一次模型调用。 见上文;installMessage: '' 会选择退出,代价是空白会话布局以及该条的第一条回执。规则不受影响——它们是一个系统部分,而 containerPrompt: '' 才是让这些退役的方式。
- 一个从未获得开场白的容器没有标签环。 在会话拥有第一条消息之前,dsh 不会渲染任何视图。在此之前,应用会渲染在 composer 上方自己的一行中(conversation.input.dock)——实时且可交互,从不覆盖输入——并在标签环出现的瞬间移入 Apps 标签页。
- 一个应用的会话会从会话树中隐藏,而不仅仅是其工作区。 仅隐藏工作区会将其会话丢进 Ungrouped 桶,因此它们会随工作区一起隐藏(通过 dsh 自己的已归档会话集,仅在此客户端的投影中——主机上没有任何内容被归档)。它们仍然可以按预期方式访问:通过该应用。
- App Store 的“打开”需要一个知道 ?nav=intent 的引擎(v0.5.1+)。较旧的引擎会忽略该参数,商店的链接会导航其自己的框架,而不是进入另一个应用的容器。
- 一个比面板更高的应用需要一个保持其查看器可滚动的引擎。 面板给每个应用相同的高度,而文档高于该高度的应用必须在其内部滚动。七个随附应用声明了 html,body{overflow:hidden}——这是对根据框架尺寸设置框架的主机的正确建议,该框架尺寸来自
应用,而在固定框架中,这意味着滚轮不起作用(实测:habit-streaks 在
1712×537 下,文档 1127px,树中没有任何可滚动内容,590px 无法到达)。引擎的查看器
(v0.5.1+)在独立页面的根元素上设置 overflow-y:auto,当应用适配时这没有任何代价
——在较旧的引擎上,这些应用会在折叠处被裁剪,在浏览器标签页中与在这个面板中完全一样。
- 沙箱化应用的链接不会被拦截。 以 --sandboxed 安装的应用运行在
运行器自己的子文档中;点击拦截位于引擎所服务的文档中。AI 编写的每个应用,
以及来自 App Store 的所有内容,都是本地的,因此都被覆盖。
- Apps 区域位于侧边栏底部,而不是工作区列表上方。
侧边栏外壳只为插件提供一个孔位(sidebar.footer.action,位于
工作区区域下方);该区域本身是一个单一占用槽位,由 dsh 自己的工作区浏览器填充,
在那里注册第二个条目会遮蔽它,而不是与它并排。
- Chat 保持为 dsh 的默认视图。 Apps 标签页排在最前,但没有存储
偏好的会话会在 Chat 上打开——该回退是 ui-conversation 内部的一个常量。
通过应用节点进入会显式选择 Apps 标签页,当容器的第一条消息到达时的
交接也是如此。
- 为会话中途创建的应用提供按应用的 open_ 工具需要重启 dsh——而
插件不再是原因。 键控槽位不接受通配符,因此插件会询问
引擎它发布了哪些 open_ 工具,并注册每一个;它现在会在
目录被打开或卡片显示一个它尚未听说过的应用时重新询问,因此它的键集合
永远不会比旁边的应用列表更旧。不会改变的是 dsh 自己的工具表:
引擎在 save_app 运行时注册新工具(使用 OMA_DYNAMIC_TOOLS=1),并且 dsh 的
MCP 客户端会在 notifications/tools/list_changed 时重新同步,但引擎的 /mcp 接口是
无状态的——createMcpHandler 为每个请求构建一个新的引擎——因此没有活跃
会话可以在其上发送该通知。在测试台上实测:第 1 轮创建的应用
在第 2 轮时仍然不在模型的工具中(“There is no tool named
open_mood_tracker”*),并且会话日志在整个会话中只携带一个 request/header,
因此目录从未改变。这些都不会到达用户,因为
通用的 open_app 在应用存在的那一刻就覆盖了每个应用——它正是模型在
每次测试台运行中所调用的,包括紧接 save_app 之后的步骤。
- 工具结果到达时是扁平化的。 dsh 只保留 MCP 结果的渲染文本,
因此应用名称是从调用的参数(或工具名称)中恢复的,而不是
从 structuredContent 中恢复。
开发
将同一命令指向一个检出目录而不是注册表——pnpm 会链接它,因此编辑会在下次 dsh 重启时
生效:
dsh plugin --profile web add /path/to/dsh-plugin-open-app
链接插件是从其真实路径解析的,因此配置文件自身的 node_modules 不在其解析路径上。这就是为什么 lib/index.js 只导入 Node 内置模块,以及为什么它手动检查其配置而不是声明 schema。
该包由两个文件组成:lib/index.js(宿主端——路由、应用目录、提示词部分)和 lib/client.js(浏览器端——五个插槽)。cordis.patch.yml 是 dsh plugin add 追加到你的配置文件中的 bundle 层。
许可证
MIT——参见 LICENSE。扫码进群