跳到主要内容

界面包

作者视角见界面包和格式参考。本页面向要修改这个功能本身的人。

不变量​

  • 包的代码不会在 DSH 页面中运行。 脚本只运行在 <iframe sandbox="allow-scripts"> 中,不带 allow-same-origin。页面自身只会放入一层主题覆盖和一份检查过的样式表。
  • 玩家视图是白名单。 它按字段由玩家角色可能知道的内容构建。只有在泄露测试里为该字段的来源放入一个秘密,才可以向 PlayerView 添加字段。
  • 包每次都重新读取。 除一次请求外不缓存关于包的任何内容;客户端轮询,修订号变化时重新加载。
  • 有任何问题的包不会被使用。 世界的包有问题时,开始故事会被拒绝;故事的副本修好之前,不显示任何包。
  • 这里没有任何东西写入故事状态。 页面可以以玩家身份发送文字或填写输入框;模型仍是游戏主持人。
  • 提示词注入以及一切会改变模型所得信息的功能都不在范围内。

各部分位置​

packages/dsh-agentic-roleplay-core/src 中的路径负责
shared/interface-pack.ts清单和视图的类型、限制、页面名和高度
host/interface/pack.ts读取并检查包文件夹、它的修订号、安全的文件查找
host/interface/skin.tsCSS 检查和 url() 重写
host/interface/packs.ts查找包、开始时复制世界的包、分支继承、比较与同步
host/interface/player-view.ts玩家视图
host/interface/frame.ts桥接脚本、页面策略、响应头
host/interface/service.ts、routes.ts各路由的应答、框架密钥
host/interface/storage.ts包保存在会话元数据中的小型存储
host/interface/world-packs.ts为创作工具列出、检查和删除世界的包文件
host/authoring/pack-jail.ts、pack-tools.tsDSH 文件工具上的限制,以及创作对话的包工具
host/interface/preview.ts由某个世界开始的故事会得到的视图,来自用世界文件做出的临时故事
host/interface/first-party.ts、packs/随包发布的界面包,以及把它们安装到根目录 ui 文件夹
client/widgets/pack-frame/框架、桥接、主题读取:故事的界面位置和编辑器的预览共用
client/interface/dock、标签页、侧栏页面、背景、轮询
client/world-editor/PacksPanel.tsx编辑器的“界面”部分:世界的界面包、问题、预览
client/chat-settings/InterfaceSection.tsx对话设置中的状态、选择与同步

预览与随包发布的界面包​

GET /worlds/interface?id= 列出世界的界面包及其内容和问题(InterfaceService.worldPacks);GET /worlds/interface/preview?id=&pack= 返回故事加载时的界面包和会给它的视图(preview)。视图来自 previewViewOf:像开始故事那样加载世界文件,在内存里建一个故事(:memory:,不写入任何东西),第一个可扮演的角色作为玩家,像回合开始时那样环顾四周,再由 playerViewOf 读取,并附三条示例建议。每个世界和界面包生成一个框架密钥,在宿主运行期间保持不变;框架路由以 preview:<密钥> 作为 id 处理这样的密钥,文件直接从世界文件夹读取,不会复制。编辑器给框架的后端没有回合,界面包保存的内容只留在内存里。

packs/ 中的界面包(目前是 scene)在启动时如果文件夹不存在就放入 <root>/ui/(installFirstPartyPacks);带标签页位置的包不能随包发布,因为任何带标签页的包都会让所有故事出现“界面”标签(availability)。

流程​

  1. 开始。 startSession 调用 copyWorldPacks,检查世界的每个包并复制到 <会话>/ui/;有问题则拒绝开始(422)。分支会复制会话的包(inheritPacks)。
  2. 选择。 activePack 读取设置 interface.pack:留空为故事自带的第一个包,none 为关闭,填名称则为故事的包,否则为根目录中的包。
  3. 页面。 客户端请求 GET /sessions/interface(带已检查皮肤和主题的描述),带着上一个修订号轮询 GET /sessions/interface/view,并为每个页面挂载一个框架。一个 PackDock 以 conversation.input.dock 的槽位同时服务 hud、dock 和 opening;hud 那个还会应用皮肤和主题并挂载背景。每个故事共享轮询。
  4. 桥接。 框架发出请求;页面通过 Bridge(权限、限制)和宿主路由(视图、回顾、存储)作答。

路由​

均在 /roleplay-api 下。

路由用途
GET /sessions/interface?id=正在使用的包;没有时给出问题
GET /sessions/interface/view?id=&revision=玩家视图,或 { unchanged: true }
GET /sessions/interface/history?id=&limit=至今的回合
GET /sessions/interface/status?id=设置、正在使用的包、可用的包、世界的包与副本的对比
POST /sessions/interface/sync?id=带入世界的包
POST /sessions/interface/storage?id=读取、设置或移除包的一个值
GET /interface/availability是否有包带有标签页
GET /sessions/interface/pack/<id>/<file>供页面使用的包文件(皮肤的 url),需要登录
GET /sessions/interface/frame/<key>/<file>供页面框架使用的包文件,无需登录

为什么框架路由不需要登录​

沙盒框架的来源是不透明的,因此它不能通过需要登录的包路由加载文档或子资源。框架的所有文件都通过路径中的密钥以只读方式提供,这同时也让相对地址得以生效。密钥是每个故事 24 个随机字节,在宿主启动时生成,只由需要登录的描述路由给出。它只能打开当前启用的包自己的文件,绝不涉及玩家视图;玩家视图只通过 postMessage 传递。API 处理器只对这个前缀上的 GET 跳过登录检查;tests/host/http/api.spec.ts 检查其他所有路由和方法仍然需要登录。

每个包文件都带有 Content-Security-Policy: sandbox allow-scripts; default-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'; worker-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; media-src 'self' data: blob:; connect-src 'self'; frame-src 'self'; form-action 'none'; base-uri 'self',以及 nosniff 和 no-referrer;框架路由提供的文件还带有 Access-Control-Allow-Origin: *,因为没有来源的框架是跨源读取自己的文件的。

为什么框架内部的策略这么宽:缺失的来源让代码无法接触宿主页面、Cookie 和存储,资源加载指令则把普通资源请求限制在包内。限制 eval、worker 或 WebAssembly 并不能约束界面包的计算能力,反而会让常见的库无法使用。connect-src 'self' 让页面可以 fetch('data.json');框架向需要登录的 DSH API 发出的请求不带登录,其应答也无法读取。已在真实浏览器框架中验证:自己的文件、worker、WebAssembly、eval、WebGL 和 canvas 可用;外部的 fetch、图片和 WebSocket、宿主 API、父页面、存储、Cookie、弹窗和顶层跳转都不可用。自行跳转仍然可用,而且能把数据放进 URL;目前没有正式发布的浏览器支持可阻止它的 CSP 指令。因此界面包属于受信任的输入。 sandbox 指令既在响应头里也在框架上,所以单独打开框架地址时文档限制仍然生效。HTML 页面会先得到桥接脚本。

桥接​

frame.ts 保存页面得到的脚本(纯 ES5,只发送消息)。client/interface/bridge.ts 负责应答:清单中的操作列表、每秒 20 个请求、每回合一次 say(或 30 秒)、2000 个字符的文字、回顾的限制。页面会忽略来源不是该框架窗口、或形状未知的消息。parseFrameMessage 是读取框架来信的唯一入口。

永远不要把注入 dock 的属性命名为 input。 DSH 自己的 dock 属性里就有一个,它遮住了我们的(现场发现:rp.say 报“input.say is not a function”)。我们的叫 frameInput。

侧栏页面​

client/interface/SidebarPage.tsx 和 index.ts 中的注册,通过 sidebarRightTabs 为 DSH 的右侧栏添加一个标签类型(roleplay-pack,id 为 @dsh-agentic-roleplay/core/interface-sidebar,keepMounted),在 sidebar.right.tab.guide.entry 下添加一张引导卡片,在 sidebar.right.pane.tab 下添加主体。卡片是我们自己按胶囊外观写的,对于界面包没有 sidebar 页面的故事它什么也不渲染,所以普通聊天的引导页不受影响。主体是一条细栏(返回按钮调用 openTab('guide', { replaceTab: true })),下面是占满其余空间的 PackFrame。rp.open 经由 PackFrame 的 FrameInput.open,它调用 sidebarRight.openTab 或激活包的标签视图。

皮肤检查​

checkSkin 是一个小型的词法分析器加规则遍历,不是 CSS 引擎。它保留 @media、@supports、@keyframes、@font-face,要求每个复合选择器中都有独立(不在函数内)的 [data-rp...],拒绝嵌套和字符串之外的转义,并把每个 url() 改写为文件路由。扩展它时,先写拒绝的测试,再放宽。

背景​

对话会绘制一个不透明的根和位于其上的滚动主体,所以仅靠 z-index 无法把一层放到文字后面。client/interface/backdrop.ts 找到主体(从 dock 向上,类名包含 _scrollBody 的元素的父元素),把一个图层插为它的第一个子元素,并用属性 data-rp-lifted(position: relative; z-index: 1)把滚动主体提到它上面。DSH 在输入框上方自带的渐隐会遮住下部。找不到这个结构时,就没有背景,并在控制台给出警告。

创作​

世界创作预设挂载 DSH 的 tool-fs(捆绑补丁中的 tool-fs 行)。registerPackJail 在 tools/pre-execute 中最先注册一个监听器,把创作对话中的 read、write 和 edit 限制在 <世界>/ui/<包>/ 的普通文本文件上(每次调用最多 200 KB),拒绝路径中的任何符号链接,在导入审阅期间拒绝写入,调用链上的其余部分,并且只把单纯的放行改为本插件自己的询问;之后的监听器的拒绝或询问仍然有效。tests/host/authoring/pack-jail-runtime.spec.ts 在 DSH 真实的工具运行时中运行这一切,若注册时没有 prepend 它就会失败。插件自己的工具是 world_pack_guide、list_world_packs、check_world_pack 和 delete_world_pack_file(DSH 的文件工具无法删除)。

给模型的路径是相对于对话所在文件夹的;第一次实测中模型把一条很长的绝对路径写错了。拒绝信息会让它停下来并报告,否则它会反复尝试其他路径和 sandbox_permissions。

DSH 变化时要检查什么​

接触点检查
槽位 conversation.input.dock 及其属性我们注入的名称不冲突;顺序值 1、3、20 仍在预期位置
槽位 conversation.view 与 uiConversation.views标签页在较晚的 availability 应答后仍能注册
sidebarRightTabs.register、槽位 sidebar.right.tab.guide.entry 与 sidebar.right.pane.tab、openTab(kind, { replaceTab })入口出现在其他入口旁,页面能打开,返回能回到引导页
主题服务 overrideTokens(用 scope.get('theme') 查找)仍在,仍接受 { light, dark } 对
body 属性 data-ds-dark-theme仍表示深色;框架的配色方案跟随它
类名片段 _scrollBody、_composerSeat背景和任何测量仍能找到它们(tests/support/dsh-contract.spec.ts 会在已安装的 conversation 包里检查 _scrollBody)
带 file_path 的工具 read、write、edit限制所读取的名称和参数名;exec.agent.session.header.cwd
带 { prepend: true } 的 tools/pre-execute运行时测试仍通过
作为外部模块的 react-dom由 src/client/react-dom.d.ts 提供类型;外壳必须仍然提供它

测试​

单元和集成测试覆盖:包检查、皮肤检查、玩家视图(每个来源都放入一个秘密)、路由(包括无需登录的框架路由和同步)、桥接、背景的查找、限制和创作工具。没有测试覆盖的部分要在浏览器里看:用示例包加载一个故事,在浅色和深色下检查状态条、dock、开场卡片、背景和标签页,编辑文件并看它跟随变化,写坏皮肤并在对话设置中看到报告,修改世界的包并更新故事,让助手写一个包并完成批准流程。