@world-engines/project-host 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/LICENSE +46 -0
  2. package/assets/gui/WELogo-title.svg +11 -0
  3. package/assets/gui/app.js +1326 -0
  4. package/assets/gui/index.html +118 -0
  5. package/assets/gui/local-preview-asset-urls.js +61 -0
  6. package/assets/gui/local-preview-bridge.js +393 -0
  7. package/assets/gui/spatial-template-panel.js +233 -0
  8. package/assets/gui/styles.css +115 -0
  9. package/assets/gui/terrain-render.js +120 -0
  10. package/assets/gui/workspace-status.css +17 -0
  11. package/assets/gui/workspace-status.js +255 -0
  12. package/assets/gui/world-map-globe.js +187 -0
  13. package/assets/gui/world-map-worker.js +62 -0
  14. package/assets/gui/world-map.css +30 -0
  15. package/assets/gui/world-map.js +2466 -0
  16. package/dist/account-model-proxy.d.ts +21 -0
  17. package/dist/account-model-proxy.js +470 -0
  18. package/dist/authoring-browser-runtime.d.ts +7 -0
  19. package/dist/authoring-browser-runtime.js +110 -0
  20. package/dist/authoring-ingestion.d.ts +38 -0
  21. package/dist/authoring-ingestion.js +410 -0
  22. package/dist/authoring-model-connection.d.ts +166 -0
  23. package/dist/authoring-model-connection.js +757 -0
  24. package/dist/chinese-lexical.d.ts +152 -0
  25. package/dist/chinese-lexical.js +555 -0
  26. package/dist/chromium-preview-browser-driver.d.ts +43 -0
  27. package/dist/chromium-preview-browser-driver.js +575 -0
  28. package/dist/cli/download.d.ts +12 -0
  29. package/dist/cli/download.js +50 -0
  30. package/dist/cli/dump.d.ts +32 -0
  31. package/dist/cli/dump.js +91 -0
  32. package/dist/cli/ingest.d.ts +49 -0
  33. package/dist/cli/ingest.js +225 -0
  34. package/dist/cli/review.d.ts +10 -0
  35. package/dist/cli/review.js +44 -0
  36. package/dist/cli/upload.d.ts +57 -0
  37. package/dist/cli/upload.js +112 -0
  38. package/dist/cli-command.d.ts +60 -0
  39. package/dist/cli-command.js +613 -0
  40. package/dist/cli.d.ts +2 -0
  41. package/dist/cli.js +182 -0
  42. package/dist/desktop-authoring-runtime.d.ts +88 -0
  43. package/dist/desktop-authoring-runtime.js +1602 -0
  44. package/dist/desktop-sidecar.d.ts +11 -0
  45. package/dist/desktop-sidecar.js +143 -0
  46. package/dist/document-dump.d.ts +113 -0
  47. package/dist/document-dump.js +431 -0
  48. package/dist/document-graph-extractor.d.ts +125 -0
  49. package/dist/document-graph-extractor.js +588 -0
  50. package/dist/document-ingestion-journal.d.ts +68 -0
  51. package/dist/document-ingestion-journal.js +471 -0
  52. package/dist/document-ingestion.d.ts +183 -0
  53. package/dist/document-ingestion.js +976 -0
  54. package/dist/document-query-path-proof.d.ts +30 -0
  55. package/dist/document-query-path-proof.js +34 -0
  56. package/dist/document-query-path-renderer.d.ts +50 -0
  57. package/dist/document-query-path-renderer.js +75 -0
  58. package/dist/document-query-proof-paths.d.ts +22 -0
  59. package/dist/document-query-proof-paths.js +71 -0
  60. package/dist/document-query.d.ts +97 -0
  61. package/dist/document-query.js +1106 -0
  62. package/dist/document-relation-role.d.ts +14 -0
  63. package/dist/document-relation-role.js +86 -0
  64. package/dist/document-source.d.ts +107 -0
  65. package/dist/document-source.js +255 -0
  66. package/dist/embedding-cache.d.ts +35 -0
  67. package/dist/embedding-cache.js +317 -0
  68. package/dist/frozen-review-local-pipeline.d.ts +88 -0
  69. package/dist/frozen-review-local-pipeline.js +609 -0
  70. package/dist/frozen-review-source-capture.d.ts +15 -0
  71. package/dist/frozen-review-source-capture.js +551 -0
  72. package/dist/frozen-review-wasm-encryptor.d.ts +16 -0
  73. package/dist/frozen-review-wasm-encryptor.js +72 -0
  74. package/dist/frozen-review.d.ts +131 -0
  75. package/dist/frozen-review.js +802 -0
  76. package/dist/graph-traversal.d.ts +23 -0
  77. package/dist/graph-traversal.js +100 -0
  78. package/dist/gui/graph-layout-worker.js +1 -0
  79. package/dist/gui/graph-layout.wasm +0 -0
  80. package/dist/gui/graph.css +1 -0
  81. package/dist/gui/graph.js +76 -0
  82. package/dist/gui/plane-generation.js +112 -0
  83. package/dist/gui/plane-generator.js +310 -0
  84. package/dist/gui/plane-generator_bg.wasm +0 -0
  85. package/dist/gui/procedural-city.js +595 -0
  86. package/dist/gui/road-generation.js +129 -0
  87. package/dist/gui/road-parcels.js +1584 -0
  88. package/dist/gui/score.css +1 -0
  89. package/dist/gui/score.js +54 -0
  90. package/dist/gui/spatial-runtime.js +1231 -0
  91. package/dist/gui/spatial-templates.js +200 -0
  92. package/dist/gui/trigger.css +1 -0
  93. package/dist/gui/trigger.js +142 -0
  94. package/dist/harness-callback-transport.d.ts +17 -0
  95. package/dist/harness-callback-transport.js +128 -0
  96. package/dist/harness-document-ingestion.d.ts +77 -0
  97. package/dist/harness-document-ingestion.js +343 -0
  98. package/dist/index.d.ts +43 -0
  99. package/dist/index.js +43 -0
  100. package/dist/ingestion-source-reconciliation.d.ts +88 -0
  101. package/dist/ingestion-source-reconciliation.js +253 -0
  102. package/dist/ladybug-runtime.d.ts +117 -0
  103. package/dist/ladybug-runtime.js +886 -0
  104. package/dist/local-account-embedding.d.ts +34 -0
  105. package/dist/local-account-embedding.js +294 -0
  106. package/dist/local-gallery.d.ts +35 -0
  107. package/dist/local-gallery.js +227 -0
  108. package/dist/local-gui-auth.d.ts +96 -0
  109. package/dist/local-gui-auth.js +703 -0
  110. package/dist/local-gui-contract.d.ts +53 -0
  111. package/dist/local-gui-contract.js +5 -0
  112. package/dist/local-gui-image-assets.d.ts +17 -0
  113. package/dist/local-gui-image-assets.js +96 -0
  114. package/dist/local-gui-preview-client.d.ts +3 -0
  115. package/dist/local-gui-preview-client.js +111 -0
  116. package/dist/local-gui-score.d.ts +72 -0
  117. package/dist/local-gui-score.js +184 -0
  118. package/dist/local-gui-server.d.ts +10 -0
  119. package/dist/local-gui-server.js +987 -0
  120. package/dist/local-gui-version.d.ts +17 -0
  121. package/dist/local-gui-version.js +147 -0
  122. package/dist/local-gui-workspace.d.ts +14 -0
  123. package/dist/local-gui-workspace.js +171 -0
  124. package/dist/local-model-settings.d.ts +28 -0
  125. package/dist/local-model-settings.js +124 -0
  126. package/dist/local-preview-assets.d.ts +28 -0
  127. package/dist/local-preview-assets.js +124 -0
  128. package/dist/local-preview-bound-resources.d.ts +12 -0
  129. package/dist/local-preview-bound-resources.js +80 -0
  130. package/dist/local-preview-message-store.d.ts +63 -0
  131. package/dist/local-preview-message-store.js +214 -0
  132. package/dist/local-preview-player-read-models.d.ts +47 -0
  133. package/dist/local-preview-player-read-models.js +90 -0
  134. package/dist/local-preview-projections.d.ts +61 -0
  135. package/dist/local-preview-projections.js +117 -0
  136. package/dist/local-preview-runtime.d.ts +105 -0
  137. package/dist/local-preview-runtime.js +553 -0
  138. package/dist/local-preview-session.d.ts +73 -0
  139. package/dist/local-preview-session.js +395 -0
  140. package/dist/local-preview-source.d.ts +40 -0
  141. package/dist/local-preview-source.js +192 -0
  142. package/dist/local-preview-view-events.d.ts +101 -0
  143. package/dist/local-preview-view-events.js +179 -0
  144. package/dist/local-preview-view-state-sqlite.d.ts +24 -0
  145. package/dist/local-preview-view-state-sqlite.js +263 -0
  146. package/dist/local-preview-view-state.d.ts +93 -0
  147. package/dist/local-preview-view-state.js +108 -0
  148. package/dist/local-preview-vite-config.d.ts +6 -0
  149. package/dist/local-preview-vite-config.js +122 -0
  150. package/dist/local-preview-world-effects.d.ts +9 -0
  151. package/dist/local-preview-world-effects.js +55 -0
  152. package/dist/local-preview-worldline-store.d.ts +47 -0
  153. package/dist/local-preview-worldline-store.js +282 -0
  154. package/dist/local-preview-worldlines.d.ts +95 -0
  155. package/dist/local-preview-worldlines.js +136 -0
  156. package/dist/mcp-async-dispatcher.d.ts +3 -0
  157. package/dist/mcp-async-dispatcher.js +129 -0
  158. package/dist/mcp-line-queue.d.ts +14 -0
  159. package/dist/mcp-line-queue.js +184 -0
  160. package/dist/mcp-pipe-internal.d.ts +6 -0
  161. package/dist/mcp-pipe-internal.js +151 -0
  162. package/dist/mcp-pipe-response.d.ts +14 -0
  163. package/dist/mcp-pipe-response.js +50 -0
  164. package/dist/mcp-stdio-response.d.ts +12 -0
  165. package/dist/mcp-stdio-response.js +27 -0
  166. package/dist/mcp.d.ts +27 -0
  167. package/dist/mcp.js +194 -0
  168. package/dist/prepared-embedding-cache.d.ts +38 -0
  169. package/dist/prepared-embedding-cache.js +238 -0
  170. package/dist/preview-browser-adapter.d.ts +53 -0
  171. package/dist/preview-browser-adapter.js +171 -0
  172. package/dist/preview-runtime.d.ts +67 -0
  173. package/dist/preview-runtime.js +460 -0
  174. package/dist/project-auth-transport.d.ts +27 -0
  175. package/dist/project-auth-transport.js +121 -0
  176. package/dist/project-auth.d.ts +51 -0
  177. package/dist/project-auth.js +108 -0
  178. package/dist/project-git-capture-recovery.d.ts +16 -0
  179. package/dist/project-git-capture-recovery.js +347 -0
  180. package/dist/project-git-cli.d.ts +5 -0
  181. package/dist/project-git-cli.js +40 -0
  182. package/dist/project-git-scenario-source.d.ts +10 -0
  183. package/dist/project-git-scenario-source.js +240 -0
  184. package/dist/project-git-view-source.d.ts +13 -0
  185. package/dist/project-git-view-source.js +134 -0
  186. package/dist/project-git.d.ts +97 -0
  187. package/dist/project-git.js +656 -0
  188. package/dist/project-host.d.ts +59 -0
  189. package/dist/project-host.js +471 -0
  190. package/dist/project-writer-lease.d.ts +18 -0
  191. package/dist/project-writer-lease.js +430 -0
  192. package/dist/public-error.d.ts +7 -0
  193. package/dist/public-error.js +61 -0
  194. package/dist/retrieval-benchmark.d.ts +46 -0
  195. package/dist/retrieval-benchmark.js +58 -0
  196. package/dist/safe-project-path.d.ts +8 -0
  197. package/dist/safe-project-path.js +93 -0
  198. package/dist/scenario-writer.d.ts +32 -0
  199. package/dist/scenario-writer.js +191 -0
  200. package/dist/transfer/download.d.ts +159 -0
  201. package/dist/transfer/download.js +598 -0
  202. package/dist/transfer/transport.d.ts +26 -0
  203. package/dist/transfer/transport.js +1 -0
  204. package/dist/transfer/upload.d.ts +162 -0
  205. package/dist/transfer/upload.js +354 -0
  206. package/dist/transfer-types.d.ts +95 -0
  207. package/dist/transfer-types.js +1 -0
  208. package/dist/transfer.d.ts +18 -0
  209. package/dist/transfer.js +273 -0
  210. package/dist/trigger-runtime.d.ts +121 -0
  211. package/dist/trigger-runtime.js +153 -0
  212. package/dist/vendor/wasm-core/asset-hashes.json +5 -0
  213. package/dist/vendor/wasm-core/wasm_core.d.ts +490 -0
  214. package/dist/vendor/wasm-core/wasm_core.js +1 -0
  215. package/dist/vendor/wasm-core/wasm_core_bg.wasm +0 -0
  216. package/dist/view-watcher.d.ts +9 -0
  217. package/dist/view-watcher.js +34 -0
  218. package/package.json +110 -0
@@ -0,0 +1,118 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="color-scheme" content="dark">
7
+ <meta name="theme-color" content="#161516">
8
+ <meta name="description" content="WorldEngine 本地作者项目工作台">
9
+ <title>WorldEngine 项目工作台</title>
10
+ <link rel="stylesheet" href="/styles.css">
11
+ <link rel="stylesheet" href="/graph.css">
12
+ <link rel="stylesheet" href="/trigger.css">
13
+ <link rel="stylesheet" href="/score.css">
14
+ <link rel="stylesheet" href="/world-map.css">
15
+ </head>
16
+ <body>
17
+ <div class="app-shell">
18
+ <aside class="sidebar" aria-label="工作台导航">
19
+ <div class="brand"><img class="brand-logo" src="/WELogo-title.svg" alt="WorldEngine"></div>
20
+ <nav id="primary-nav">
21
+ <button class="nav-item active" data-view="project"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 11.5 12 4l9 7.5"/><path d="M5.5 10v9.5h13V10M9.5 19.5v-6h5v6"/></svg><span>项目</span></button>
22
+ <button class="nav-item" data-view="graph"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="6" r="2.5"/><circle cx="18" cy="7" r="2.5"/><circle cx="12" cy="18" r="2.5"/><path d="m8.4 6.2 7.1.5M7.4 8l3.4 7.7m5.7-6.5-3.2 6.5"/></svg><span>世界图</span></button>
23
+ <div class="nav-group" data-nav-group="world-map">
24
+ <button class="nav-item nav-group-toggle" type="button" aria-expanded="false" aria-controls="world-map-subnav"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m3 6 5-2 8 3 5-2v13l-5 2-8-3-5 2zM8 4v13m8-10v13"/><circle cx="12" cy="11" r="1.5"/></svg><span>世界地图</span><svg class="nav-chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m9 5 7 7-7 7"/></svg></button>
25
+ <div class="nav-subitems" id="world-map-subnav" hidden>
26
+ <button class="nav-subitem" type="button" data-view="world-map">地图总览</button>
27
+ <button class="nav-subitem" type="button" data-template-kind="city">城市模板</button>
28
+ <button class="nav-subitem" type="button" data-template-kind="parcel">地块模板</button>
29
+ <button class="nav-subitem" type="button" data-template-kind="building">建筑模板</button>
30
+ <button class="nav-subitem" type="button" data-template-kind="interior">室内模板</button>
31
+ </div>
32
+ </div>
33
+ <button class="nav-item" data-view="trigger"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M13.5 2.5 5.8 13h5.6l-.9 8.5L18.2 11h-5.6z"/></svg><span>触发器</span></button>
34
+ <button class="nav-item" data-view="view"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 9h18M8 9v11"/></svg><span>视图</span></button>
35
+ </nav>
36
+ <div class="sidebar-foot"><section class="sidebar-account" aria-label="云端账户" aria-live="polite"><form id="auth-start-form" action="/api/auth/start" method="post"><button class="button ghost compact" id="auth-button" type="submit" disabled>检查登录状态…</button></form><div id="account-details" hidden><strong id="account-name"></strong><span id="account-balance"></span></div><button class="button ghost compact" id="logout-button" hidden>退出登录</button><button class="button ghost compact" id="account-retry" hidden>重试读取</button></section><div class="sidebar-connection"><span class="pulse-dot" id="side-dot"></span><span id="side-status">正在连接 NodeHost…</span></div><button class="version-status" id="version-update" type="button" disabled title="正在检查版本"><span>版本</span><strong id="version-current">检查中…</strong><small id="version-update-note" hidden>可更新</small></button></div>
37
+ </aside>
38
+
39
+ <section class="workspace">
40
+ <header class="topbar">
41
+ <div class="identity">
42
+ <strong id="project-name">读取项目中…</strong>
43
+ <code id="project-id">—</code>
44
+ <span class="revision">REV <b id="project-revision">—</b></span>
45
+ </div>
46
+ <button class="score-badge" id="score-badge" type="button" aria-haspopup="dialog" aria-controls="score-dialog"><span>结构预估</span><strong id="score-badge-value">读取中</strong></button>
47
+ <div class="top-actions">
48
+ <span class="connection" id="connection-state"><i></i>连接中</span>
49
+ <button class="button ghost compact" id="refresh-button">刷新</button>
50
+ </div>
51
+ </header>
52
+
53
+ <main id="main" tabindex="-1">
54
+ <section class="view active" data-view-panel="project" aria-labelledby="project-title">
55
+ <div class="view-heading"><div><p class="eyebrow">YOUR WORK</p><h1 id="project-title">我的作品</h1><p>从世界设定到可玩的画面,都在这一个本地作品中完成。</p></div><button class="button primary" data-view-jump="view">打开作品预览</button></div>
56
+ <div class="metric-grid" id="project-metrics"></div>
57
+ <div class="two-column">
58
+ <article class="panel project-health"><div class="panel-title"><div><h2>作品状态</h2><p>内容始终保存在本地</p></div><span class="tag" id="runtime-tag">正在检查</span></div><p id="project-health-copy">正在读取作品状态…</p><dl class="detail-list" id="status-details" hidden></dl></article>
59
+ <article class="panel"><div class="panel-title"><div><h2>开始创作</h2><p>选择一个部分继续完善作品</p></div></div><div class="action-list"><button class="action-card" data-view-jump="graph"><span class="action-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="6" r="2.5"/><circle cx="18" cy="7" r="2.5"/><circle cx="12" cy="18" r="2.5"/><path d="m8.4 6.2 7.1.5M7.4 8l3.4 7.7m5.7-6.5-3.2 6.5"/></svg></span><span><strong>搭建世界</strong><small>整理角色、地点与它们的关系</small></span><svg class="action-arrow" viewBox="0 0 24 24" aria-hidden="true"><path d="m9 5 7 7-7 7"/></svg></button><button class="action-card" data-view-jump="trigger"><span class="action-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M13.5 2.5 5.8 13h5.6l-.9 8.5L18.2 11h-5.6z"/></svg></span><span><strong>设计触发器</strong><small>决定故事在什么时候发生变化</small></span><svg class="action-arrow" viewBox="0 0 24 24" aria-hidden="true"><path d="m9 5 7 7-7 7"/></svg></button><button class="action-card" data-view-jump="view"><span class="action-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 9h18M8 9v11"/></svg></span><span><strong>查看作品</strong><small>启动实时预览并查看当前画面</small></span><svg class="action-arrow" viewBox="0 0 24 24" aria-hidden="true"><path d="m9 5 7 7-7 7"/></svg></button></div></article>
60
+ </div>
61
+ <button id="snapshot-button" hidden>创建快照</button>
62
+ <article class="panel event-panel" hidden aria-hidden="true"><button id="clear-events">清空</button><div id="event-log"><p>还没有操作。</p></div></article>
63
+ </section>
64
+
65
+ <section class="view graph-workspace-view" data-view-panel="graph" aria-labelledby="graph-title">
66
+ <div class="graph-toolbar">
67
+ <div class="graph-title"><h1 id="graph-title">世界图</h1><span id="graph-summary">—</span></div>
68
+ <div class="graph-actions"><button class="graph-fab save" id="new-graph-node" type="button" aria-label="新建节点" title="新建节点"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button><button class="graph-fab" id="reload-graph" type="button" aria-label="重新读取世界图" title="重新读取世界图"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M19 7v5h-5M5 17v-5h5M7.4 8.2A6 6 0 0 1 18 10m-1.4 5.8A6 6 0 0 1 6 14"/></svg></button></div>
69
+ </div>
70
+ <div class="graph-stage"><div id="graph-canvas" class="graph-canvas" role="region" aria-label="世界实体关系图"></div><div id="spatial-canvas" class="graph-canvas" hidden role="img" aria-label="空间世界投影"></div></div>
71
+
72
+ <aside class="graph-side-panel" id="graph-side-panel" hidden aria-label="世界图检查面板">
73
+ <header><div><p class="graph-panel-kicker" id="graph-panel-kicker">节点</p><h2 id="graph-panel-title">详情</h2></div><div class="graph-fab-row"><button class="graph-fab danger" id="delete-graph-item" data-graph-fab="delete" type="button" aria-label="删除选中项" title="删除选中项"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16M9 7V4h6v3m-9 0 1 13h10l1-13M10 11v5m4-5v5"/></svg></button><button class="graph-fab save" id="save-graph-item" data-graph-fab="save" type="button" aria-label="保存选中项" title="保存选中项"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4h12l2 2v14H5zM8 4v6h8V4M8 20v-6h8v6"/></svg></button><button class="graph-fab graph-close" type="button" data-graph-fab="close" data-close-graph-panel aria-label="关闭侧栏" title="关闭"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg></button></div></header>
74
+ <nav class="graph-panel-tabs" aria-label="检查面板"><button class="active" data-graph-panel-tab="metadata">元数据</button><button data-graph-panel-tab="attributes">属性</button><button data-graph-panel-tab="incoming">指入关系</button><button data-graph-panel-tab="outgoing">指出关系</button></nav>
75
+ <section id="graph-detail-view"><div id="graph-detail-content" class="graph-detail-content"></div></section>
76
+ <form id="graph-editor" class="graph-friendly-form" hidden>
77
+ <div id="entity-fields">
78
+ <label><span>基础类型</span><select name="entity_type"><option value="character">人物</option><option value="location">地点</option><option value="faction">势力</option><option value="item">物品</option><option value="event">事件</option><option value="__worldengine_ui_custom_kind__">其他 / 自定义</option></select></label>
79
+ <label><span>名称</span><input name="name" required placeholder="输入名称"></label>
80
+ <div class="graph-alias-field wide"><span>别名</span><div id="graph-alias-list"></div><button class="graph-fab" id="add-graph-alias" type="button" aria-label="新增别名" title="新增别名"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button></div>
81
+ <label id="custom-kind-field" hidden><span>自定义类型</span><input name="custom_kind" placeholder="输入稳定的底层类型"></label>
82
+ <label><span>访问策略</span><select name="entity_view_access"><option value="">继承</option><option value="deny">不可见</option><option value="read">只读</option><option value="read_write">可读写</option></select></label>
83
+ <label class="graph-switch-field graph-switch-unavailable"><span>初始隐藏<small>当前运行时暂不支持节点初始隐藏</small></span><input name="entity_initial_hidden" type="checkbox" role="switch" aria-label="初始隐藏" aria-checked="false" disabled></label>
84
+ <label class="graph-switch-field"><span>联网搜索</span><input name="online_supplement" type="checkbox" role="switch" aria-label="联网搜索" aria-checked="false"></label>
85
+ <label id="prototype-name-field" class="wide" hidden><span>检索名称</span><input name="prototype_name" placeholder="输入联网检索名称"></label>
86
+ <textarea name="description" hidden></textarea>
87
+ </div>
88
+ <div id="relation-fields" hidden><label><span>起点</span><input name="src" required></label><label><span>终点</span><input name="dst" required></label><label><span>关系类型</span><input name="kind" required placeholder="例如:belongs_to"></label><label><span>显示标题</span><input name="relation_title" placeholder="例如:隶属于"></label><label><span>访问策略</span><select name="relation_view_access"><option value="">继承</option><option value="deny">不可见</option><option value="read">只读</option><option value="read_write">可读写</option></select></label><label><span><input name="relation_initial_available" type="checkbox" checked> 初始可见</span></label><label class="wide"><span>关系内容</span><textarea name="relation_description" placeholder="补充这段关系的说明"></textarea><small id="relation-description-help" hidden>复杂内容由属性页安全保留。</small></label></div>
89
+ <input name="id" type="hidden"><textarea name="props" hidden>{}</textarea>
90
+ </form>
91
+ <div id="graph-editor-cg" class="graph-detail-content" hidden></div>
92
+ </aside>
93
+ </section>
94
+
95
+ <section class="view world-map-view" data-view-panel="world-map" aria-label="世界地图"><div id="world-map-root"><p role="status">正在准备地图工作区…</p></div></section>
96
+ <section class="view spatial-template-view" data-view-panel="spatial-templates" aria-label="空间模板"><div id="spatial-template-root"></div></section>
97
+
98
+ <section class="view" data-view-panel="trigger" aria-labelledby="trigger-title">
99
+ <div id="trigger-blueprint-root" aria-live="polite"><div class="trigger-loading-shell"><div class="trigger-loading-overlay" role="status"><svg class="trigger-loading-mark" viewBox="0 0 64 64" aria-hidden="true"><rect x="10" y="10" width="44" height="44" rx="4"/><path d="M20 38 31 27l7 7 8-10"/><circle cx="20" cy="38" r="2"/><circle cx="31" cy="27" r="2"/><circle cx="38" cy="34" r="2"/><circle cx="46" cy="24" r="2"/></svg><strong>正在加载触发器</strong><span>正在读取蓝图和项目能力</span></div></div></div>
100
+ </section>
101
+
102
+ <section class="view" data-view-panel="view" aria-labelledby="view-title"><div class="view-heading"><div><p class="eyebrow">LIVE PREVIEW</p><h1 id="view-title">作品预览</h1><p>进入这里会启动项目已有的 Vite 开发环境;源文件更新会实时显示。</p></div><div class="button-row"><button class="button" id="preview-start">无缓存刷新</button><button class="button ghost" id="preview-stop">停止预览</button><a class="button ghost" id="preview-open" target="_blank" rel="noopener noreferrer" hidden>新窗口打开</a></div></div><article class="panel preview-panel"><iframe id="preview-frame" title="作品实时预览" sandbox="allow-scripts" referrerpolicy="origin"></iframe><span class="preview-status" id="preview-status">点击左侧“视图”即可启动实时预览。</span></article><div hidden aria-hidden="true"><button data-files-refresh="view">刷新</button><div id="view-files"></div><span id="view-file-name"></span><span id="view-file-sha"></span><button id="save-view-file" disabled>保存</button><textarea id="view-file-content" disabled></textarea></div></section>
103
+
104
+ <section hidden aria-hidden="true"><button data-files-refresh="docs">刷新</button><div id="docs-files"></div><span id="doc-file-name"></span><pre id="doc-content"></pre></section>
105
+
106
+ <section hidden aria-hidden="true"><button id="reload-tools">重新读取</button><div id="tool-list"></div><span id="tool-name"></span><span id="tool-description"></span><span id="tool-effect"></span><pre id="tool-schema"></pre><textarea id="tool-input">{}</textarea><div id="effect-fields" hidden><input id="operation-id"><input id="expected-revision" type="number"></div><button id="run-tool" disabled>执行工具</button><pre id="tool-result"></pre></section>
107
+
108
+ <section hidden aria-hidden="true"><button id="reload-jobs">刷新任务</button><form id="recall-form"><input name="query"><input name="limit" type="number" value="10"></form><form id="ingest-form"><input name="path"><input name="scope_id"><input name="source_id"><input name="source_revision" type="number" value="1"></form><div id="jobs-table"></div><span id="job-result-caption"></span><pre id="job-result"></pre></section>
109
+ </main>
110
+ </section>
111
+ </div>
112
+ <dialog class="score-dialog" id="score-dialog" aria-labelledby="score-dialog-title"><div class="score-dialog-shell"><header><div><p>本地结构与正式画像</p><h2 id="score-dialog-title">作品评分</h2></div><button class="graph-fab" id="score-dialog-close" type="button" aria-label="关闭评分" title="关闭评分"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg></button></header><div id="score-panel-root"><p class="score-panel-state">正在读取评分…</p></div></div></dialog>
113
+ <dialog class="model-settings-dialog" id="model-settings-dialog" aria-labelledby="model-settings-title"><div class="model-settings-shell"><header><div><p>本地项目预览</p><h2 id="model-settings-title">模型设置</h2></div><button class="graph-fab" id="model-settings-close" type="button" aria-label="关闭模型设置" title="关闭"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg></button></header><div class="model-settings-body"><p>当前项目模型:<code id="model-settings-current">读取中…</code></p><label><span>账户可用模型</span><select id="model-settings-select" aria-label="账户可用模型" disabled></select></label><input id="model-settings-sha" type="hidden"><input id="model-settings-catalog-token" type="hidden"><p id="model-settings-note" role="status">正在读取当前模型设置与账户目录…</p></div><footer><button class="button ghost" id="model-settings-cancel" type="button">取消</button><button class="button primary" id="model-settings-save" type="button" disabled>保存</button></footer></div></dialog>
114
+ <div class="toast-region" id="toasts" role="status" aria-live="polite"></div>
115
+ <script type="module" src="/app.js"></script>
116
+ <script type="module" src="/score.js"></script>
117
+ </body>
118
+ </html>
@@ -0,0 +1,61 @@
1
+ const SHA256 = /^[a-f0-9]{64}$/u;
2
+ const ID = /^local-preview-asset:v1:[a-f0-9]{64}:[a-f0-9]{32}$/u;
3
+ const SAFE_MEDIA_TYPES = new Set([
4
+ "image/avif", "image/gif", "image/jpeg", "image/png", "image/webp",
5
+ "audio/aac", "audio/flac", "audio/mpeg", "audio/mp4", "audio/ogg", "audio/wav", "audio/webm",
6
+ "video/mp4", "video/ogg", "video/webm",
7
+ ]);
8
+
9
+ function invalid(message) { throw new Error(`local_preview_asset_url_invalid:${message}`); }
10
+
11
+ function assertAsset(asset) {
12
+ if (!asset || typeof asset !== "object" || Array.isArray(asset)
13
+ || typeof asset.id !== "string" || !ID.test(asset.id)
14
+ || typeof asset.mime !== "string" || !SAFE_MEDIA_TYPES.has(asset.mime)
15
+ || typeof asset.size !== "number" || !Number.isSafeInteger(asset.size) || asset.size < 0
16
+ || typeof asset.createdTs !== "number" || !Number.isSafeInteger(asset.createdTs) || asset.createdTs < 0
17
+ || typeof asset.sha256 !== "string" || !SHA256.test(asset.sha256)) {
18
+ invalid("asset");
19
+ }
20
+ }
21
+
22
+ /**
23
+ * 仅运行于 top-window 的同源 GUI。opaque iframe 不能消费父窗口 storage key 下的 blob URL,
24
+ * 因而只取得一条短期、单资源、capture/hash 绑定的无 cookie 媒体 capability URL。
25
+ */
26
+ export function createLocalPreviewAssetUrlStore({ fetchCapability, revokeCapabilities, locationOrigin = globalThis.location?.origin }) {
27
+ if (typeof fetchCapability !== "function" || typeof locationOrigin !== "string" || locationOrigin.length === 0) invalid("ports");
28
+ const urls = new Map();
29
+ let disposed = false;
30
+ const clear = () => {
31
+ const tokens = [...urls.values()].map(({ url }) => new URL(url).pathname.split("/").at(-1)).filter((token) => typeof token === "string");
32
+ urls.clear();
33
+ if (tokens.length > 0 && typeof revokeCapabilities === "function") void Promise.resolve(revokeCapabilities(tokens)).catch(() => undefined);
34
+ };
35
+ return Object.freeze({
36
+ async getUrl(asset) {
37
+ if (disposed) invalid("disposed");
38
+ assertAsset(asset);
39
+ const cached = urls.get(asset.id);
40
+ if (cached) return cached.url;
41
+ const response = await fetchCapability(asset);
42
+ if (!(response instanceof Response) || !response.ok) invalid("fetch_failed");
43
+ const responseMime = (response.headers.get("content-type") || "").split(";", 1)[0].trim();
44
+ if (responseMime !== "application/json") invalid("capability_content_type");
45
+ const body = await response.json();
46
+ const capability = body && typeof body === "object" && body.ok === true && body.result && typeof body.result === "object" ? body.result : null;
47
+ if (!capability || typeof capability.url !== "string" || capability.mime !== asset.mime || capability.size !== asset.size || capability.sha256 !== asset.sha256) invalid("capability_fence");
48
+ let url;
49
+ try { url = new URL(capability.url); } catch { invalid("capability_url"); }
50
+ if (url.origin !== locationOrigin || !/^\/preview-media\/[A-Za-z0-9_-]{43}$/u.test(url.pathname) || url.search !== "" || url.hash !== "" || url.username !== "" || url.password !== "") invalid("capability_url");
51
+ urls.set(asset.id, Object.freeze({ url: url.href }));
52
+ return url.href;
53
+ },
54
+ clear,
55
+ dispose() {
56
+ if (disposed) return;
57
+ disposed = true;
58
+ clear();
59
+ },
60
+ });
61
+ }
@@ -0,0 +1,393 @@
1
+ import { createLocalPreviewAssetUrlStore } from "./local-preview-asset-urls.js";
2
+
3
+ const PROTOCOL_VERSION = 3;
4
+ const ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
5
+ const MAX_MESSAGE_CHARS = 64 * 1024;
6
+ const MAX_LIST_LIMIT = 200;
7
+
8
+ // 互动结果仅由 Host 返回的 durable receipt 判定;不透传 Host 内部错误。
9
+ function interactionIdentity(envelope) {
10
+ if (!envelope || typeof envelope !== "object") return null;
11
+ const { interaction_id: interactionId, nonce } = envelope;
12
+ return typeof interactionId === "string" && ID_PATTERN.test(interactionId)
13
+ && typeof nonce === "string" && ID_PATTERN.test(nonce)
14
+ ? Object.freeze({ interactionId, nonce })
15
+ : null;
16
+ }
17
+
18
+ /** 将本地 Host 的实际结果收敛为 SDK 可消费的 committed / failed / uncertain 语义。 */
19
+ export function classifyLocalPreviewInteractionResult(envelope, result, uncertaintyCode = "activation_unconfirmed") {
20
+ const identity = interactionIdentity(envelope);
21
+ if (identity === null) return null;
22
+ if (result && typeof result === "object") {
23
+ if (result.status === "committed" || result.status === "already_committed" || result.committed === true) {
24
+ return Object.freeze({ ...identity, status: "committed" });
25
+ }
26
+ if (result.ok === false && result.committed === false) {
27
+ return Object.freeze({ ...identity, status: "failed", code: "activation_failed" });
28
+ }
29
+ }
30
+ return Object.freeze({ ...identity, status: "uncertain", code: uncertaintyCode === "commit_sink_unavailable" ? "commit_sink_unavailable" : "activation_unconfirmed" });
31
+ }
32
+
33
+ function failure(id, code, message) {
34
+ return { v: PROTOCOL_VERSION, kind: "res", id, error: { code, message } };
35
+ }
36
+
37
+ function readyContext(status, theme, previewSession, projectionCapabilities) {
38
+ const project = status?.project && typeof status.project === "object" ? status.project : status;
39
+ const scriptId = project?.project_id;
40
+ if (typeof scriptId !== "string" || scriptId.length === 0) return null;
41
+ return Object.freeze({
42
+ scriptId,
43
+ saveId: previewSession?.worldline_save_id ?? previewSession?.save_id,
44
+ locale: document.documentElement.lang || navigator.language || "zh-CN",
45
+ theme,
46
+ capabilities: Object.freeze({
47
+ schemaVersion: 3,
48
+ capabilitiesShapeVersion: 3,
49
+ runtimeMajor: 3,
50
+ runtimePatch: 1,
51
+ protocolVersion: PROTOCOL_VERSION,
52
+ customStateMaxBytes: projectionCapabilities?.customStateMaxBytes ?? 0,
53
+ assetMaxBytes: projectionCapabilities?.assetMaxBytes ?? 0,
54
+ supportsAssets: projectionCapabilities?.supportsAssets === true,
55
+ supportsEdit: projectionCapabilities?.supportsEdit === true,
56
+ supportsDelete: projectionCapabilities?.supportsDelete === true,
57
+ supportsWorldlines: typeof previewSession?.worldline_save_id === "string",
58
+ }),
59
+ });
60
+ }
61
+
62
+ function localTheme(media) {
63
+ const choice = document.documentElement?.dataset?.theme;
64
+ if (choice === "light" || choice === "dark") return Object.freeze({ choice, resolved: choice });
65
+ return Object.freeze({ choice: "system", resolved: media?.matches === true ? "dark" : "light" });
66
+ }
67
+
68
+ function sameTheme(left, right) {
69
+ return left?.choice === right?.choice && left?.resolved === right?.resolved;
70
+ }
71
+
72
+ function messageId(prefix, sequence) {
73
+ const random = globalThis.crypto?.randomUUID?.();
74
+ return `${prefix}-${random || `${Date.now().toString(36)}-${sequence.toString(36)}`}`;
75
+ }
76
+
77
+ function listMessages(messages, params) {
78
+ const after = typeof params.after === "string" ? messages.findIndex((item) => item.id === params.after) : -1;
79
+ const before = typeof params.before === "string" ? messages.findIndex((item) => item.id === params.before) : messages.length;
80
+ const start = params.after === undefined ? 0 : after < 0 ? messages.length : after + 1;
81
+ const end = params.before === undefined ? messages.length : before < 0 ? 0 : before;
82
+ const limit = Number.isSafeInteger(params.limit) ? Math.min(Math.max(params.limit, 1), MAX_LIST_LIMIT) : MAX_LIST_LIMIT;
83
+ return messages.slice(start, Math.max(start, end)).slice(-limit);
84
+ }
85
+
86
+ /**
87
+ * 正式 Runtime 3.1 的本地 Host controller:只提供 SDK 消息/只读能力。
88
+ * user 消息会真实进入本地队列并发出 notification;没有 11450 LLM route 时绝不伪造 assistant 完成。
89
+ */
90
+ export function attachLocalPreviewBridge(parentWindow, readIframe, readStatus, lifecycle = {}) {
91
+ let generation = 0;
92
+ let sequence = 0;
93
+ let disposed = false;
94
+ let active = false;
95
+ let publishedTheme = null;
96
+ let previewSession = null;
97
+ let projectionCapabilities = null;
98
+ let committedProjectionSnapshot = null;
99
+ const host = lifecycle.host;
100
+ const assetUrls = typeof host?.fetchAssetCapability === "function"
101
+ ? createLocalPreviewAssetUrlStore({ fetchCapability: (asset) => host.fetchAssetCapability(asset.id), revokeCapabilities: (tokens) => host.revokeAssetCapabilities?.(tokens) })
102
+ : null;
103
+ const observedMessageIds = new Set();
104
+ const observedFailedTurns = new Set();
105
+ let stopUpdates = false;
106
+ function currentFrame(iframe, expectedGeneration) {
107
+ return !disposed && active && !stopUpdates && generation === expectedGeneration && readIframe() === iframe;
108
+ }
109
+ function backgroundFailure(iframe, expectedGeneration, streamId, message) {
110
+ if (currentFrame(iframe, expectedGeneration) && iframe?.dataset.bridgeReady === "true") {
111
+ iframe.contentWindow?.postMessage({ v: PROTOCOL_VERSION, kind: "notify", topic: "stream.error", data: { streamId, code: "E_OPERATION_UNCERTAIN", message } }, "*");
112
+ }
113
+ }
114
+ async function refreshSession() {
115
+ if (!host) throw { code: "ERECON", message: "本地 Preview Host 未连接" };
116
+ [previewSession, projectionCapabilities] = await Promise.all([host.inspect(), host.request("preview.capabilities", {})]);
117
+ return previewSession;
118
+ }
119
+ async function assetUrl(id) {
120
+ if (assetUrls === null) throw { code: "E_CAPABILITY_UNAVAILABLE", message: "本地 Preview asset reader 不可用" };
121
+ const records = await host.request("asset.list", {});
122
+ if (!Array.isArray(records)) throw { code: "E_OPERATION_UNCERTAIN", message: "本地 Preview asset catalog 无效" };
123
+ const asset = records.find((candidate) => candidate && typeof candidate === "object" && candidate.id === id);
124
+ if (!asset) throw { code: "ENOTFOUND", message: "本地 Preview asset 不存在" };
125
+ return await assetUrls.getUrl(asset);
126
+ }
127
+ function projectionSnapshot(value) {
128
+ const source = value?.notification_snapshot;
129
+ const state = source?.state;
130
+ if (!state || !Number.isSafeInteger(state.generation) || state.generation < 0 || !Array.isArray(state.entries) || !Array.isArray(source.owned_resources)) return null;
131
+ const entries = new Map();
132
+ for (const entry of state.entries) { if (!entry || typeof entry !== "object" || typeof entry.key !== "string" || !Number.isSafeInteger(entry.revision) || entry.revision < 0) return null; entries.set(entry.key, entry.revision); }
133
+ const resources = new Map();
134
+ for (const group of source.owned_resources) { if (!group || typeof group !== "object" || typeof group.node_id !== "string" || typeof group.attr_key !== "string" || !Array.isArray(group.resources)) return null; resources.set(`${group.node_id}\u0000${group.attr_key}`, group); }
135
+ return { generation: state.generation, entries, resources };
136
+ }
137
+ function publishProjectionDiff(iframe, value) {
138
+ const next = projectionSnapshot(value); if (next === null) return;
139
+ const previous = committedProjectionSnapshot; committedProjectionSnapshot = next;
140
+ if (previous === null) return;
141
+ if (previous.generation !== next.generation) {
142
+ const keys = new Set([...previous.entries.keys(), ...next.entries.keys()]);
143
+ const changedKeys = [...keys].filter((key) => previous.entries.get(key) !== next.entries.get(key)).sort();
144
+ iframe.contentWindow?.postMessage({ v: PROTOCOL_VERSION, kind: "notify", topic: "state.changed", data: { generation: next.generation, changedKeys } }, "*");
145
+ }
146
+ for (const [key, group] of next.resources) if (JSON.stringify(previous.resources.get(key)) !== JSON.stringify(group)) iframe.contentWindow?.postMessage({ v: PROTOCOL_VERSION, kind: "notify", topic: "resource.unlocked", data: group }, "*");
147
+ }
148
+ async function publishViewDeliveries(iframe, entries, expectedGeneration = generation) {
149
+ if (!Array.isArray(entries)) return;
150
+ for (const entry of entries) {
151
+ if (!entry || typeof entry !== "object" || typeof entry.operation_id !== "string" || !Array.isArray(entry.deliveries)) continue;
152
+ if (!currentFrame(iframe, expectedGeneration)) return;
153
+ try {
154
+ for (const delivery of entry.deliveries) {
155
+ if (!currentFrame(iframe, expectedGeneration)) return;
156
+ iframe.contentWindow?.postMessage({ v: PROTOCOL_VERSION, kind: "notify", topic: "tmw.delivery", data: delivery }, "*");
157
+ }
158
+ if (!currentFrame(iframe, expectedGeneration)) return;
159
+ await host?.resolveViewDeliveryFailure?.(entry.operation_id);
160
+ } catch {
161
+ try { await host?.recordViewDeliveryFailure?.(entry.operation_id); }
162
+ catch { backgroundFailure(iframe, expectedGeneration, "tmw.delivery", "界面更新失败,恢复记录未能写入,请刷新后重试"); }
163
+ }
164
+ }
165
+ }
166
+ async function followUpdates(loopGeneration) {
167
+ while (!disposed && active && !stopUpdates && generation === loopGeneration && previewSession && host) {
168
+ const next = await host.wait(previewSession.revision, 15000).catch(() => null);
169
+ if (!next || disposed || !active || stopUpdates || generation !== loopGeneration) return;
170
+ if (next.revision === previewSession.revision) { await new Promise((resolve) => setTimeout(resolve, 50)); continue; }
171
+ previewSession = next;
172
+ const iframe = readIframe();
173
+ if (iframe?.dataset.bridgeReady === "true") {
174
+ publishProjectionDiff(iframe, next);
175
+ await publishViewDeliveries(iframe, next.view_event_deliveries, loopGeneration);
176
+ const messages = await host.request("chat.listMessages", { limit: MAX_LIST_LIMIT }).catch(() => []);
177
+ if (disposed || !active || stopUpdates || generation !== loopGeneration || readIframe() !== iframe) return;
178
+ if (Array.isArray(messages)) for (const message of messages) {
179
+ if (!message || typeof message !== "object" || typeof message.id !== "string" || observedMessageIds.has(message.id)) continue;
180
+ observedMessageIds.add(message.id);
181
+ iframe.contentWindow?.postMessage({ v: PROTOCOL_VERSION, kind: "notify", topic: "message.added", data: message }, "*");
182
+ }
183
+ if (Array.isArray(next.turns)) for (const turn of next.turns) {
184
+ if (!turn || typeof turn !== "object" || turn.state !== "failed" || typeof turn.turn_id !== "string" || observedFailedTurns.has(turn.turn_id)) continue;
185
+ observedFailedTurns.add(turn.turn_id);
186
+ iframe.contentWindow?.postMessage({ v: PROTOCOL_VERSION, kind: "notify", topic: "stream.error", data: { streamId: turn.turn_id, code: typeof turn.error_code === "string" ? turn.error_code : "E_OPERATION_UNCERTAIN", message: "本地 Preview 模型回合未完成" } }, "*");
187
+ }
188
+ }
189
+ }
190
+ }
191
+ const themeMedia = globalThis.matchMedia?.("(prefers-color-scheme: dark)");
192
+ const notifyTheme = () => {
193
+ const theme = localTheme(themeMedia);
194
+ if (sameTheme(theme, publishedTheme)) return;
195
+ publishedTheme = theme;
196
+ const iframe = readIframe();
197
+ if (!disposed && active && iframe?.dataset.bridgeReady === "true") {
198
+ iframe.contentWindow?.postMessage({ v: PROTOCOL_VERSION, kind: "notify", topic: "theme.changed", data: theme }, "*");
199
+ }
200
+ };
201
+ const onThemeMediaChange = () => notifyTheme();
202
+ themeMedia?.addEventListener?.("change", onThemeMediaChange);
203
+ const themeObserver = typeof globalThis.MutationObserver === "function"
204
+ ? new globalThis.MutationObserver(notifyTheme)
205
+ : null;
206
+ themeObserver?.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
207
+ const onMessage = async (event) => {
208
+ const iframe = readIframe();
209
+ if (disposed || !active || iframe === null || event.source !== iframe.contentWindow || event.origin !== "null") return;
210
+ const envelope = event.data;
211
+ if (envelope && typeof envelope === "object" && envelope.schema === "view-interaction-event.v3") {
212
+ const currentGeneration = generation;
213
+ let bytes;
214
+ try { bytes = new TextEncoder().encode(JSON.stringify(envelope)).byteLength; }
215
+ catch { return; }
216
+ if (bytes > MAX_MESSAGE_CHARS + 4096) {
217
+ event.source.postMessage({ v: PROTOCOL_VERSION, kind: "notify", topic: "stream.error", data: { streamId: "view.interaction", code: "view_event_envelope_invalid", message: "互动请求无效或过大" } }, "*");
218
+ return;
219
+ }
220
+ let result;
221
+ let uncertaintyCode = "activation_unconfirmed";
222
+ try {
223
+ if (typeof host?.emitViewInteraction !== "function") {
224
+ uncertaintyCode = "commit_sink_unavailable";
225
+ throw new Error("local_preview_interaction_sink_unavailable");
226
+ }
227
+ result = await host.emitViewInteraction(envelope);
228
+ } catch {
229
+ // 传输层中断不能说明 executeEvent 是否已持久化;不能把异常当作明确拒绝。
230
+ result = null;
231
+ }
232
+ if (disposed || !active || currentGeneration !== generation || readIframe() !== iframe || event.source !== iframe.contentWindow) return;
233
+ const interactionResult = classifyLocalPreviewInteractionResult(envelope, result, uncertaintyCode);
234
+ if (interactionResult !== null) {
235
+ iframe.contentWindow?.postMessage({ v: PROTOCOL_VERSION, kind: "notify", topic: "interaction.result", data: interactionResult }, "*");
236
+ }
237
+ if (interactionResult?.status === "committed") {
238
+ if (result?.ok === true && Array.isArray(result.deliveries)) {
239
+ await publishViewDeliveries(iframe, Array.isArray(result.view_event_deliveries)
240
+ ? result.view_event_deliveries
241
+ : result.deliveries.length === 0 ? [] : [{ operation_id: result.operation_id, deliveries: result.deliveries }], currentGeneration);
242
+ }
243
+ const refreshed = await refreshSession().catch(() => null);
244
+ if (refreshed !== null && !disposed && active && currentGeneration === generation && readIframe() === iframe) publishProjectionDiff(iframe, refreshed);
245
+ } else if (interactionResult !== null && interactionResult.status === "failed") {
246
+ iframe.contentWindow?.postMessage({ v: PROTOCOL_VERSION, kind: "notify", topic: "stream.error", data: { streamId: "view.interaction", code: typeof result?.code === "string" ? result.code : "E_OPERATION_UNCERTAIN", message: typeof result?.player_message === "string" ? result.player_message : "互动操作未提交,请刷新后重试" } }, "*");
247
+ }
248
+ return;
249
+ }
250
+ if (!envelope || typeof envelope !== "object" || envelope.v !== PROTOCOL_VERSION || envelope.kind !== "req") return;
251
+ if (typeof envelope.id !== "string" || !ID_PATTERN.test(envelope.id) || typeof envelope.method !== "string" || envelope.method.length > 128) return;
252
+ if (!envelope.params || typeof envelope.params !== "object" || Array.isArray(envelope.params)) return;
253
+ let paramsBytes;
254
+ try { paramsBytes = new TextEncoder().encode(JSON.stringify(envelope.params)).byteLength; }
255
+ catch { return; }
256
+ if (paramsBytes > MAX_MESSAGE_CHARS + 4096) return;
257
+ const currentGeneration = generation;
258
+ const readyTheme = envelope.method === "system.ready" ? localTheme(themeMedia) : null;
259
+ let response;
260
+ if (envelope.method === "system.ready") {
261
+ try { await refreshSession(); } catch (error) { response = failure(envelope.id, error?.code || "ERECON", "本地 Preview Host 未连接"); }
262
+ }
263
+ const context = readyContext(readStatus(), readyTheme ?? localTheme(themeMedia), previewSession, projectionCapabilities);
264
+ if (envelope.method === "system.ready") {
265
+ response ??= context === null || typeof context.saveId !== "string"
266
+ ? failure(envelope.id, "ERECON", "项目 identity 尚未就绪")
267
+ : { v: PROTOCOL_VERSION, kind: "res", id: envelope.id, result: context };
268
+ } else if (context === null) {
269
+ response = failure(envelope.id, "ERECON", "项目 identity 尚未就绪");
270
+ } else if (envelope.method === "chat.listMessages") {
271
+ try { response = { v: PROTOCOL_VERSION, kind: "res", id: envelope.id, result: await host.request("chat.listMessages", envelope.params) }; }
272
+ catch (error) { response = failure(envelope.id, error?.code || "E_OPERATION_UNCERTAIN", "读取 Preview 消息失败"); }
273
+ } else if (envelope.method === "chat.sendUserMessage") {
274
+ const text = envelope.params.text;
275
+ if (typeof text !== "string" || text.trim().length === 0 || text.length > MAX_MESSAGE_CHARS) {
276
+ response = failure(envelope.id, "EINVAL", "消息必须是 1..65536 字符的非空文本");
277
+ } else {
278
+ sequence += 1;
279
+ try { response = { v: PROTOCOL_VERSION, kind: "res", id: envelope.id, result: await host.request("chat.sendUserMessage", { text, requestId: messageId("local-request", sequence) }) }; }
280
+ catch (error) { response = failure(envelope.id, error?.code || "E_OPERATION_UNCERTAIN", "Preview 消息未排队"); }
281
+ }
282
+ } else if (["worldline.list", "worldline.fork", "worldline.activate"].includes(envelope.method)) {
283
+ try { response = { v: PROTOCOL_VERSION, kind: "res", id: envelope.id, result: await host.request(envelope.method, envelope.params, envelope.id) }; }
284
+ catch (error) { response = failure(envelope.id, error?.code || "E_OPERATION_UNCERTAIN", "本地 Preview worldline 操作失败"); }
285
+ } else if (envelope.method === "system.getLocale") {
286
+ response = { v: PROTOCOL_VERSION, kind: "res", id: envelope.id, result: context.locale };
287
+ } else if (envelope.method === "system.getTheme") {
288
+ response = { v: PROTOCOL_VERSION, kind: "res", id: envelope.id, result: context.theme };
289
+ } else if (envelope.method === "asset.list") {
290
+ try {
291
+ if (assetUrls === null) throw { code: "E_CAPABILITY_UNAVAILABLE" };
292
+ const records = await host.request("asset.list", envelope.params);
293
+ if (!Array.isArray(records)) throw { code: "E_OPERATION_UNCERTAIN" };
294
+ response = { v: PROTOCOL_VERSION, kind: "res", id: envelope.id, result: await Promise.all(records.map(async (asset) => {
295
+ if (!asset || typeof asset !== "object" || typeof asset.id !== "string") throw { code: "E_OPERATION_UNCERTAIN" };
296
+ const { sha256: _sha256, ...publicAsset } = asset;
297
+ return { ...publicAsset, url: await assetUrls.getUrl(asset) };
298
+ })) };
299
+ } catch (error) { response = failure(envelope.id, error?.code || "E_OPERATION_UNCERTAIN", "读取本地 Preview 资源失败"); }
300
+ } else if (envelope.method === "asset.getUrl") {
301
+ try { await host.request("asset.getUrl", envelope.params); response = { v: PROTOCOL_VERSION, kind: "res", id: envelope.id, result: await assetUrl(envelope.params.id) }; }
302
+ catch (error) { response = failure(envelope.id, error?.code || "E_OPERATION_UNCERTAIN", "读取本地 Preview 资源失败"); }
303
+ } else if (["scenario.getMeta", "scenario.getProgress", "scenario.getVariable", "scenario.getUnlock", "scenario.getOwnedResources", "state.get", "state.getEntry", "state.keys"].includes(envelope.method)) {
304
+ try { response = { v: PROTOCOL_VERSION, kind: "res", id: envelope.id, result: await host.request(envelope.method, envelope.params) }; }
305
+ catch (error) { response = failure(envelope.id, error?.code || "E_OPERATION_UNCERTAIN", "读取本地 Preview 状态失败"); }
306
+ } else if (envelope.method === "ui.requestSettings") {
307
+ if (Object.keys(envelope.params).length !== 0) response = failure(envelope.id, "EINVAL", "ui.requestSettings 不接受参数");
308
+ else if (typeof lifecycle.onRequestSettings !== "function") response = failure(envelope.id, "E_CAPABILITY_UNAVAILABLE", "父窗口未提供模型设置");
309
+ else {
310
+ try { await lifecycle.onRequestSettings(); response = { v: PROTOCOL_VERSION, kind: "res", id: envelope.id, result: { ok: true } }; }
311
+ catch (error) { response = failure(envelope.id, error?.code || "E_CAPABILITY_UNAVAILABLE", "模型设置未能打开"); }
312
+ }
313
+ } else {
314
+ response = failure(envelope.id, "EPERM", "本地 Preview 未授予该 ChatPlay SDK 能力");
315
+ }
316
+ if (!disposed && currentGeneration === generation && event.source === iframe.contentWindow) {
317
+ event.source.postMessage(response, "*");
318
+ if (envelope.method === "system.ready" && response.result) {
319
+ iframe.dataset.bridgeReady = "true";
320
+ publishedTheme = context.theme;
321
+ committedProjectionSnapshot = projectionSnapshot(previewSession);
322
+ lifecycle.onReady?.();
323
+ void (typeof host?.viewInteractionConfig === "function" ? host.viewInteractionConfig() : Promise.resolve(null)).then((config) => {
324
+ if (!currentFrame(iframe, currentGeneration) || iframe.dataset.bridgeReady !== "true") return;
325
+ if (config && typeof config === "object" && config.schema === "view-interaction-config.v3") {
326
+ iframe.contentWindow?.postMessage(config, "*");
327
+ }
328
+ }).catch(() => backgroundFailure(iframe, currentGeneration, "view.config", "互动配置读取失败,请刷新后重试"));
329
+ void (typeof host?.recoverViewDeliveries === "function" ? host.recoverViewDeliveries() : Promise.resolve([])).then((entries) => {
330
+ if (currentFrame(iframe, currentGeneration)) return publishViewDeliveries(iframe, entries, currentGeneration);
331
+ }).catch(() => backgroundFailure(iframe, currentGeneration, "tmw.delivery", "恢复界面更新失败,请刷新后重试"));
332
+ void host.request("chat.listMessages", { limit: MAX_LIST_LIMIT }).then((items) => { if (currentFrame(iframe, currentGeneration) && Array.isArray(items)) for (const item of items) if (item?.id) observedMessageIds.add(item.id); }).catch(() => backgroundFailure(iframe, currentGeneration, "messages", "消息读取失败,请刷新后重试"));
333
+ stopUpdates = false; void followUpdates(currentGeneration);
334
+ }
335
+ if (envelope.method === "chat.sendUserMessage" && response.result) {
336
+ void refreshSession().catch(() => undefined);
337
+ }
338
+ if (envelope.method === "worldline.fork" && response.result) {
339
+ event.source.postMessage({ v: PROTOCOL_VERSION, kind: "notify", topic: "worldline.forked", data: response.result }, "*");
340
+ void refreshSession().then((next) => { if (!disposed && active && currentGeneration === generation && readIframe() === iframe) publishProjectionDiff(iframe, next); }).catch(() => undefined);
341
+ }
342
+ if (envelope.method === "worldline.activate" && response.result) {
343
+ event.source.postMessage({ v: PROTOCOL_VERSION, kind: "notify", topic: "worldline.activated", data: response.result }, "*");
344
+ void refreshSession().then((next) => { if (!disposed && active && currentGeneration === generation && readIframe() === iframe) publishProjectionDiff(iframe, next); }).catch(() => undefined);
345
+ }
346
+ }
347
+ };
348
+ parentWindow.addEventListener("message", onMessage);
349
+ return Object.freeze({
350
+ load(url) {
351
+ generation += 1;
352
+ active = true;
353
+ stopUpdates = false;
354
+ assetUrls?.clear();
355
+ committedProjectionSnapshot = null;
356
+ const previous = readIframe();
357
+ if (previous === null) throw new Error("local_preview_iframe_missing");
358
+ const iframe = previous.cloneNode(false);
359
+ previous.replaceWith(iframe);
360
+ delete iframe.dataset.bridgeReady;
361
+ const loadGeneration = generation;
362
+ iframe.addEventListener("load", () => {
363
+ if (!active || generation !== loadGeneration || readIframe() !== iframe || iframe.dataset.bridgeReady === "true") return;
364
+ lifecycle.onLoad?.();
365
+ }, { once: true });
366
+ const next = new URL(url);
367
+ next.searchParams.set("worldengine_preview_generation", String(generation));
368
+ iframe.src = next.toString();
369
+ },
370
+ clear() {
371
+ generation += 1;
372
+ active = false;
373
+ stopUpdates = true;
374
+ assetUrls?.clear();
375
+ committedProjectionSnapshot = null;
376
+ const iframe = readIframe();
377
+ if (iframe) { delete iframe.dataset.bridgeReady; iframe.removeAttribute("src"); }
378
+ },
379
+ dispose() {
380
+ if (disposed) return;
381
+ disposed = true;
382
+ generation += 1;
383
+ active = false;
384
+ stopUpdates = true;
385
+ assetUrls?.dispose();
386
+ committedProjectionSnapshot = null;
387
+ parentWindow.removeEventListener("message", onMessage);
388
+ themeMedia?.removeEventListener?.("change", onThemeMediaChange);
389
+ themeObserver?.disconnect();
390
+ readIframe()?.removeAttribute("src");
391
+ },
392
+ });
393
+ }