prts-terrarchive 0.2.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 (287) hide show
  1. package/GAME_ASSETS.md +26 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +274 -0
  4. package/README.md +423 -0
  5. package/THIRD_PARTY_NOTICES.md +61 -0
  6. package/bin/bake-rhine-details.mjs +34 -0
  7. package/bin/install.js +273 -0
  8. package/bin/pack-map-assets.mjs +110 -0
  9. package/bin/preview-rhine.mjs +71 -0
  10. package/bin/rhine-detail-baker.ts +84 -0
  11. package/contracts/cloud_tools_v1.schema.json +243 -0
  12. package/contracts/corpus_i18n_v1.schema.json +179 -0
  13. package/contracts/corpus_tools_v1.schema.json +1801 -0
  14. package/cordis.patch.yml +32 -0
  15. package/docs/endfield-official-i18n.md +103 -0
  16. package/lib/client.js +3912 -0
  17. package/lib/endfield-map/map.js.br +0 -0
  18. package/lib/endfield-map/map.js.gz +0 -0
  19. package/lib/endfield-map/resources/map-00b0d0744a1b4404.json.br +0 -0
  20. package/lib/endfield-map/resources/map-00b0d0744a1b4404.json.gz +0 -0
  21. package/lib/endfield-map/resources/map-01616f534aaaeca1.png +0 -0
  22. package/lib/endfield-map/resources/map-1562e0dff4be3d23.png +0 -0
  23. package/lib/endfield-map/resources/map-160fe040f080f6a3.png +0 -0
  24. package/lib/endfield-map/resources/map-17b9d8be25dc2b39.json.br +0 -0
  25. package/lib/endfield-map/resources/map-17b9d8be25dc2b39.json.gz +0 -0
  26. package/lib/endfield-map/resources/map-1c4245c4e8b80c43.json.br +0 -0
  27. package/lib/endfield-map/resources/map-1c4245c4e8b80c43.json.gz +0 -0
  28. package/lib/endfield-map/resources/map-1d30baaaef3e29cc.png +0 -0
  29. package/lib/endfield-map/resources/map-21a61386af79cd12.json.br +0 -0
  30. package/lib/endfield-map/resources/map-21a61386af79cd12.json.gz +0 -0
  31. package/lib/endfield-map/resources/map-24b920b8dab49b44.json.br +0 -0
  32. package/lib/endfield-map/resources/map-24b920b8dab49b44.json.gz +0 -0
  33. package/lib/endfield-map/resources/map-26caf8d71247abb7.png +0 -0
  34. package/lib/endfield-map/resources/map-2729942c89545e73.json.br +0 -0
  35. package/lib/endfield-map/resources/map-2729942c89545e73.json.gz +0 -0
  36. package/lib/endfield-map/resources/map-29063a9802f67014.json.br +0 -0
  37. package/lib/endfield-map/resources/map-29063a9802f67014.json.gz +0 -0
  38. package/lib/endfield-map/resources/map-2f35d09a3ab60a63.json.br +0 -0
  39. package/lib/endfield-map/resources/map-2f35d09a3ab60a63.json.gz +0 -0
  40. package/lib/endfield-map/resources/map-2f98a873da81425e.json.br +0 -0
  41. package/lib/endfield-map/resources/map-2f98a873da81425e.json.gz +0 -0
  42. package/lib/endfield-map/resources/map-3ab978ae44190f80.json.br +0 -0
  43. package/lib/endfield-map/resources/map-3ab978ae44190f80.json.gz +0 -0
  44. package/lib/endfield-map/resources/map-3b62c80d14208902.json.br +0 -0
  45. package/lib/endfield-map/resources/map-3b62c80d14208902.json.gz +0 -0
  46. package/lib/endfield-map/resources/map-3d486e887ae121ac.png +0 -0
  47. package/lib/endfield-map/resources/map-418dc4865608fce5.png +0 -0
  48. package/lib/endfield-map/resources/map-4366b84607335f76.json.br +0 -0
  49. package/lib/endfield-map/resources/map-4366b84607335f76.json.gz +0 -0
  50. package/lib/endfield-map/resources/map-4371259d3ab7aac7.png +0 -0
  51. package/lib/endfield-map/resources/map-48dcc0fd55e13f7f.png +0 -0
  52. package/lib/endfield-map/resources/map-4aabda0e41197570.json.br +0 -0
  53. package/lib/endfield-map/resources/map-4aabda0e41197570.json.gz +0 -0
  54. package/lib/endfield-map/resources/map-505f5687b287043c.json.br +0 -0
  55. package/lib/endfield-map/resources/map-505f5687b287043c.json.gz +0 -0
  56. package/lib/endfield-map/resources/map-5177e9d1d46f47e2.png +0 -0
  57. package/lib/endfield-map/resources/map-51baeb59b7f8c28d.png +0 -0
  58. package/lib/endfield-map/resources/map-549561b46e621874.png +0 -0
  59. package/lib/endfield-map/resources/map-56c6a0bef799c8dc.json.br +0 -0
  60. package/lib/endfield-map/resources/map-56c6a0bef799c8dc.json.gz +0 -0
  61. package/lib/endfield-map/resources/map-5a6963909423b674.png +0 -0
  62. package/lib/endfield-map/resources/map-5a9c533c0552d7b7.png +0 -0
  63. package/lib/endfield-map/resources/map-5d20ae9ffa9a02b0.png +0 -0
  64. package/lib/endfield-map/resources/map-626c8847393f60bf.png +0 -0
  65. package/lib/endfield-map/resources/map-63d43cbec3bed3b8.png +0 -0
  66. package/lib/endfield-map/resources/map-661b6ec56e722574.png +0 -0
  67. package/lib/endfield-map/resources/map-6ddd384eca190828.png +0 -0
  68. package/lib/endfield-map/resources/map-6fc1aa9144f8156c.json.br +0 -0
  69. package/lib/endfield-map/resources/map-6fc1aa9144f8156c.json.gz +0 -0
  70. package/lib/endfield-map/resources/map-738ebaa002c33cfc.png +0 -0
  71. package/lib/endfield-map/resources/map-758c13be6085ec07.json.br +0 -0
  72. package/lib/endfield-map/resources/map-758c13be6085ec07.json.gz +0 -0
  73. package/lib/endfield-map/resources/map-78929f26d6844509.png +0 -0
  74. package/lib/endfield-map/resources/map-7f09c142950a0f74.png +0 -0
  75. package/lib/endfield-map/resources/map-8385e11f39e1a51c.json.br +0 -0
  76. package/lib/endfield-map/resources/map-8385e11f39e1a51c.json.gz +0 -0
  77. package/lib/endfield-map/resources/map-881343926a568f27.json.br +0 -0
  78. package/lib/endfield-map/resources/map-881343926a568f27.json.gz +0 -0
  79. package/lib/endfield-map/resources/map-8d1ade3f01d952b0.png +0 -0
  80. package/lib/endfield-map/resources/map-8fef3495bb7ed60f.png +0 -0
  81. package/lib/endfield-map/resources/map-907a7ecf8a0ce352.png +0 -0
  82. package/lib/endfield-map/resources/map-9113d5951c4db903.json.br +0 -0
  83. package/lib/endfield-map/resources/map-9113d5951c4db903.json.gz +0 -0
  84. package/lib/endfield-map/resources/map-94ad6634436224e3.json.br +0 -0
  85. package/lib/endfield-map/resources/map-94ad6634436224e3.json.gz +0 -0
  86. package/lib/endfield-map/resources/map-99722e61da5f3e53.png +0 -0
  87. package/lib/endfield-map/resources/map-9be9596cc059abe3.png +0 -0
  88. package/lib/endfield-map/resources/map-a4db9a4763e47fbf.png +0 -0
  89. package/lib/endfield-map/resources/map-a4e8ef07159997e7.json.br +0 -0
  90. package/lib/endfield-map/resources/map-a4e8ef07159997e7.json.gz +0 -0
  91. package/lib/endfield-map/resources/map-ab35d06875ee418f.png +0 -0
  92. package/lib/endfield-map/resources/map-ac51bb3e7623cd8b.png +0 -0
  93. package/lib/endfield-map/resources/map-ac830a66296bc83a.png +0 -0
  94. package/lib/endfield-map/resources/map-b3fd04209718e55e.json.br +0 -0
  95. package/lib/endfield-map/resources/map-b3fd04209718e55e.json.gz +0 -0
  96. package/lib/endfield-map/resources/map-b8164f1af7c5d895.png +0 -0
  97. package/lib/endfield-map/resources/map-b8cde93b965384d8.json.br +0 -0
  98. package/lib/endfield-map/resources/map-b8cde93b965384d8.json.gz +0 -0
  99. package/lib/endfield-map/resources/map-b8e48511502e0e8d.json.br +0 -0
  100. package/lib/endfield-map/resources/map-b8e48511502e0e8d.json.gz +0 -0
  101. package/lib/endfield-map/resources/map-bfc58277d5ba649d.png +0 -0
  102. package/lib/endfield-map/resources/map-c013f82048c6891a.png +0 -0
  103. package/lib/endfield-map/resources/map-c49c947841ae4ab5.json.br +0 -0
  104. package/lib/endfield-map/resources/map-c49c947841ae4ab5.json.gz +0 -0
  105. package/lib/endfield-map/resources/map-c4d1b1c6d6706c81.json.br +0 -0
  106. package/lib/endfield-map/resources/map-c4d1b1c6d6706c81.json.gz +0 -0
  107. package/lib/endfield-map/resources/map-c607f8b4c8ad8967.png +0 -0
  108. package/lib/endfield-map/resources/map-c6f20276b8781326.json.br +0 -0
  109. package/lib/endfield-map/resources/map-c6f20276b8781326.json.gz +0 -0
  110. package/lib/endfield-map/resources/map-d51adf5952af1e8e.png +0 -0
  111. package/lib/endfield-map/resources/map-d695eface22f8b74.json.br +0 -0
  112. package/lib/endfield-map/resources/map-d695eface22f8b74.json.gz +0 -0
  113. package/lib/endfield-map/resources/map-db11d394456a7465.png +0 -0
  114. package/lib/endfield-map/resources/map-dcc4d218581e3dc6.png +0 -0
  115. package/lib/endfield-map/resources/map-ddd43dbb8ab8a4e3.json.br +0 -0
  116. package/lib/endfield-map/resources/map-ddd43dbb8ab8a4e3.json.gz +0 -0
  117. package/lib/endfield-map/resources/map-defe5db7dabf748a.png +0 -0
  118. package/lib/endfield-map/resources/map-e1aeb833e22520aa.json.br +0 -0
  119. package/lib/endfield-map/resources/map-e1aeb833e22520aa.json.gz +0 -0
  120. package/lib/endfield-map/resources/map-e25ee9af6e493500.png +0 -0
  121. package/lib/endfield-map/resources/map-e3824082cd6f747d.json.br +0 -0
  122. package/lib/endfield-map/resources/map-e3824082cd6f747d.json.gz +0 -0
  123. package/lib/endfield-map/resources/map-e694a91e630b1bd6.png +0 -0
  124. package/lib/endfield-map/resources/map-eaa93af79d12a87d.png +0 -0
  125. package/lib/endfield-map/resources/map-ec9575ee65d7766f.json.br +0 -0
  126. package/lib/endfield-map/resources/map-ec9575ee65d7766f.json.gz +0 -0
  127. package/lib/endfield-map/resources/map-ede421a329dc78a4.png +0 -0
  128. package/lib/endfield-map/resources/map-eff3bce8f6888fc5.json.br +0 -0
  129. package/lib/endfield-map/resources/map-eff3bce8f6888fc5.json.gz +0 -0
  130. package/lib/endfield-map/resources/map-fb5f4a59a181452c.png +0 -0
  131. package/lib/endfield-map/resources/map-fdeaa17eeba90d8e.json.br +0 -0
  132. package/lib/endfield-map/resources/map-fdeaa17eeba90d8e.json.gz +0 -0
  133. package/lib/rhine/assets/archive-assembly.glb +0 -0
  134. package/lib/rhine/assets/archive-cassette.glb +0 -0
  135. package/lib/rhine/fonts/MiSans-Bold.woff2 +0 -0
  136. package/lib/rhine/fonts/MiSans-Demibold.woff2 +0 -0
  137. package/lib/rhine/fonts/MiSans-Light.woff2 +0 -0
  138. package/lib/rhine/fonts/MiSans-Regular.woff2 +0 -0
  139. package/lib/rhine/fonts/MiSans-license.pdf +0 -0
  140. package/lib/rhine/fonts/NOTICE.txt +6 -0
  141. package/lib/rhine/licenses/RhineLabUI-MIT.txt +21 -0
  142. package/lib/rhine/licenses/entities-BSD-2-Clause.txt +11 -0
  143. package/lib/rhine/licenses/linkify-it-MIT.txt +22 -0
  144. package/lib/rhine/licenses/markdown-it-MIT.txt +22 -0
  145. package/lib/rhine/licenses/mdurl-MIT.txt +45 -0
  146. package/lib/rhine/licenses/punycode.js-MIT.txt +20 -0
  147. package/lib/rhine/licenses/rolling-number-MIT.txt +21 -0
  148. package/lib/rhine/licenses/three-MIT.txt +21 -0
  149. package/lib/rhine/licenses/uc.micro-MIT.txt +20 -0
  150. package/lib/rhine/rhine.css +1 -0
  151. package/lib/rhine/rhine.js +5106 -0
  152. package/lib/skins/common.css +31 -0
  153. package/lib/skins/endfield-aic.css +141 -0
  154. package/lib/skins/prts-agent.css +136 -0
  155. package/lib/skins/rhine-lab.css +70 -0
  156. package/package.json +112 -0
  157. package/presets/definition.js +26 -0
  158. package/presets/prts/agent.cordis.yml +25 -0
  159. package/presets/prts/preset.yml +3 -0
  160. package/presets/register.js +86 -0
  161. package/resources/entity-routing.json +21 -0
  162. package/skills/prts-investigation/SKILL.md +121 -0
  163. package/skills/prts-retrieval/SKILL.md +35 -0
  164. package/skills/prts-retrieval/references/cloud-tools.md +13 -0
  165. package/skills/prts-retrieval/references/diagnostics.md +15 -0
  166. package/skills/prts-retrieval/references/local-tools.md +24 -0
  167. package/skills/prts-retrieval/references/module-arknights.md +23 -0
  168. package/skills/prts-retrieval/references/module-dual.md +15 -0
  169. package/skills/prts-retrieval/references/module-endfield.md +21 -0
  170. package/skills/prts-retrieval/references/retrieval-process.md +44 -0
  171. package/skills/prts-retrieval/references/retrieval-recipes-dual.md +34 -0
  172. package/skills/prts-retrieval/references/retrieval-recipes-endfield.md +37 -0
  173. package/skills/prts-retrieval/references/retrieval-recipes.md +39 -0
  174. package/skills/prts-retrieval/references/tools-runtime.md +48 -0
  175. package/skills/prts-retrieval/references/wiki-schema.md +38 -0
  176. package/src/archive-presentation.js +287 -0
  177. package/src/cloud-projection.js +124 -0
  178. package/src/cloud.js +498 -0
  179. package/src/entity-recognizer.js +311 -0
  180. package/src/entity-routing.js +82 -0
  181. package/src/evidence-state.js +332 -0
  182. package/src/i18n.js +365 -0
  183. package/src/index.js +1261 -0
  184. package/src/installer.js +1362 -0
  185. package/src/investigation-review.js +87 -0
  186. package/src/investigation-store.js +579 -0
  187. package/src/investigation-tools.js +113 -0
  188. package/src/localization-format.js +23 -0
  189. package/src/manual-search.js +103 -0
  190. package/src/read.js +1097 -0
  191. package/src/release-compatibility.js +73 -0
  192. package/src/release-watch.js +40 -0
  193. package/src/search-projection.js +139 -0
  194. package/src/search.js +1522 -0
  195. package/src/skill.js +98 -0
  196. package/src/source-map.js +348 -0
  197. package/src/state.js +314 -0
  198. package/src/store.js +1569 -0
  199. package/src/timeline.js +386 -0
  200. package/src/ui.js +1353 -0
  201. package/src/wiki.js +111 -0
  202. package/tsconfig.rhine.json +15 -0
  203. package/ui/rhine/archive-navigation-limit.ts +31 -0
  204. package/ui/rhine/archive-rack.ts +83 -0
  205. package/ui/rhine/assets/evidence-observatory.webp +0 -0
  206. package/ui/rhine/board-plane-transform.ts +24 -0
  207. package/ui/rhine/catalogue.ts +72 -0
  208. package/ui/rhine/deferred-preparation.ts +146 -0
  209. package/ui/rhine/evidence-board-controls.css +22 -0
  210. package/ui/rhine/evidence-board-history.ts +73 -0
  211. package/ui/rhine/evidence-board-model.ts +189 -0
  212. package/ui/rhine/evidence-board-panel.ts +762 -0
  213. package/ui/rhine/evidence-board-scene.ts +548 -0
  214. package/ui/rhine/evidence-board.css +191 -0
  215. package/ui/rhine/evidence-card-art.ts +470 -0
  216. package/ui/rhine/evidence-card-style.ts +17 -0
  217. package/ui/rhine/evidence-inbox-scene.ts +43 -0
  218. package/ui/rhine/evidence-inbox.css +29 -0
  219. package/ui/rhine/evidence-inbox.ts +104 -0
  220. package/ui/rhine/index.ts +12 -0
  221. package/ui/rhine/interaction.css +20 -0
  222. package/ui/rhine/investigation-board-plane.ts +5 -0
  223. package/ui/rhine/investigation-board.css +34 -0
  224. package/ui/rhine/investigation-board.ts +348 -0
  225. package/ui/rhine/investigation-edits.ts +46 -0
  226. package/ui/rhine/investigation-layout.ts +29 -0
  227. package/ui/rhine/label-prefetch.ts +34 -0
  228. package/ui/rhine/looping-title.ts +40 -0
  229. package/ui/rhine/manual-search.css +41 -0
  230. package/ui/rhine/manual-search.ts +89 -0
  231. package/ui/rhine/original/appearance.ts +180 -0
  232. package/ui/rhine/original/archive-label.ts +84 -0
  233. package/ui/rhine/original/archive-lighting.ts +39 -0
  234. package/ui/rhine/original/archive-loop.ts +82 -0
  235. package/ui/rhine/original/archive-refill.ts +64 -0
  236. package/ui/rhine/original/array-detail-data.ts +2 -0
  237. package/ui/rhine/original/array-detail.ts +41 -0
  238. package/ui/rhine/original/array-interior.ts +411 -0
  239. package/ui/rhine/original/array-occlusion.ts +166 -0
  240. package/ui/rhine/original/array-shell.ts +38 -0
  241. package/ui/rhine/original/array-visibility.ts +309 -0
  242. package/ui/rhine/original/asset-url.ts +4 -0
  243. package/ui/rhine/original/brand.ts +12 -0
  244. package/ui/rhine/original/cassette-lod.ts +81 -0
  245. package/ui/rhine/original/data.ts +21 -0
  246. package/ui/rhine/original/decryption.ts +185 -0
  247. package/ui/rhine/original/detail-signature.ts +18 -0
  248. package/ui/rhine/original/glass-reveal.ts +45 -0
  249. package/ui/rhine/original/internal-optics.ts +70 -0
  250. package/ui/rhine/original/label-renderer.ts +119 -0
  251. package/ui/rhine/original/label-textures.ts +117 -0
  252. package/ui/rhine/original/model-viewer.ts +658 -0
  253. package/ui/rhine/original/motion.ts +116 -0
  254. package/ui/rhine/original/opaque-backfaces.ts +64 -0
  255. package/ui/rhine/original/program-preparation.ts +111 -0
  256. package/ui/rhine/original/quality-renderer.ts +97 -0
  257. package/ui/rhine/original/quality-settings.ts +111 -0
  258. package/ui/rhine/original/render-quality.ts +163 -0
  259. package/ui/rhine/original/scene.ts +2051 -0
  260. package/ui/rhine/original/shelf-interior.ts +142 -0
  261. package/ui/rhine/original/ui-transitions.ts +111 -0
  262. package/ui/rhine/original/viewer-camera.ts +92 -0
  263. package/ui/rhine/original/viewport-layout.ts +41 -0
  264. package/ui/rhine/original-ui.css +2334 -0
  265. package/ui/rhine/readability.css +46 -0
  266. package/ui/rhine/reading-object.ts +96 -0
  267. package/ui/rhine/reading-origin.ts +28 -0
  268. package/ui/rhine/reading-room.css +55 -0
  269. package/ui/rhine/report-markdown.ts +98 -0
  270. package/ui/rhine/scene-edge-navigation.ts +84 -0
  271. package/ui/rhine/scene.ts +1726 -0
  272. package/ui/rhine/shelf-layout.ts +38 -0
  273. package/ui/rhine/snapshot-update-gate.ts +22 -0
  274. package/ui/rhine/source-reading.ts +15 -0
  275. package/ui/rhine/source-selection.css +164 -0
  276. package/ui/rhine/temporary-performance-observers.ts +163 -0
  277. package/ui/rhine/temporary-performance-panel.ts +124 -0
  278. package/ui/rhine/temporary-performance.css +19 -0
  279. package/ui/rhine/temporary-performance.ts +552 -0
  280. package/ui/rhine/temporary-render-diagnostics.ts +105 -0
  281. package/ui/rhine/tool-activity.ts +69 -0
  282. package/ui/rhine/types.ts +219 -0
  283. package/ui/rhine/visible-scene-hit.ts +13 -0
  284. package/ui/rhine/workbench.css +818 -0
  285. package/ui/rhine/workbench.ts +2245 -0
  286. package/ui/rhine/workspace-travel.ts +32 -0
  287. package/vite.rhine.config.ts +19 -0
package/bin/install.js ADDED
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * prts-terrarchive 一键安装脚本。
4
+ *
5
+ * 用法:
6
+ * node bin/install.js web # 从当前插件源码目录安装
7
+ * node bin/install.js web /path/to/pkg # 安装指定本地目录或压缩包
8
+ * node bin/install.js web --preset-only # 旧版打包器已放置插件,只生成/迁移预设
9
+ *
10
+ * 环境变量:
11
+ * DSH_HOME 宿主根目录(缺省 ~/.dsh);DSH 插件命令(缺省 dsh)
12
+ * PRTS_DSH_VERSION 仅 --preset-only:可显式指定打包目标的 DSH 版本,且不调用 dsh
13
+ *
14
+ * 做什么:
15
+ * 1) 把插件加入 profile(dsh plugin add)
16
+ * 2) DSH 0.1.7 起由插件注册「PRTS 模式」;旧版创建用户预设
17
+ * ($DSH_HOME/.agent-presets/prts/*),且只有选中该模式才加载语料工具
18
+ * 3) 打印后续指引(如何设为默认模式)
19
+ *
20
+ * 说明:资料管理(设置页 /api + UI)由插件 host 常驻提供;语料三工具由
21
+ * PRTS 预设加载——标准/极简等其它模式不加载 PRTS 工具。
22
+ */
23
+ import { execFileSync } from 'node:child_process'
24
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
25
+ import { homedir } from 'node:os'
26
+ import { dirname, join, resolve } from 'node:path'
27
+ import { fileURLToPath } from 'node:url'
28
+
29
+ const here = dirname(fileURLToPath(import.meta.url))
30
+ const packageDir = resolve(here, '..')
31
+ const packageMetadata = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'))
32
+ const argv = process.argv.slice(2)
33
+ const presetOnly = argv.includes('--preset-only')
34
+ const knownFlags = new Set(['--preset-only'])
35
+ const unknownFlag = argv.find((arg) => arg.startsWith('-') && !knownFlags.has(arg))
36
+ if (unknownFlag) throw new Error(`未知选项:${unknownFlag}`)
37
+ const positionalArgs = argv.filter((arg) => !knownFlags.has(arg))
38
+ if (positionalArgs.length > 2) throw new Error('位置参数过多')
39
+ const profile = positionalArgs[0] || 'web'
40
+ if (profile.toLowerCase() === 'desktop' && !presetOnly) {
41
+ throw new Error('官方 DSH Desktop 的 desktop profile 由应用管理,请在桌面插件管理窗口安装 prts-terrarchive;npm 发布前请使用 Web 本地安装或 Portable。--preset-only 仅供已放置插件的发行版打包器使用。')
42
+ }
43
+ const pkg = positionalArgs[1] ? resolve(positionalArgs[1]) : packageDir
44
+ if (!presetOnly && !existsSync(pkg)) {
45
+ throw new Error(`本地插件目录或压缩包不存在:${pkg}`)
46
+ }
47
+ if (!profile || profile === '.' || profile === '..' || profile === 'node_modules'
48
+ || profile.startsWith('-') || profile.includes('/') || profile.includes('\\')
49
+ || /[\u0000-\u001f\u007f]/u.test(profile)) {
50
+ throw new Error(`DSH profile 名称非法:${JSON.stringify(profile)}`)
51
+ }
52
+ const dshHome = process.env.DSH_HOME || join(homedir(), '.dsh')
53
+ const dshCmd = process.env.DSH || 'dsh'
54
+
55
+ const PRESET_ID = 'prts'
56
+ const presetDir = join(dshHome, '.agent-presets', PRESET_ID)
57
+ const compositionPath = join(presetDir, 'agent.cordis.yml')
58
+ const metadataPath = join(presetDir, 'preset.yml')
59
+
60
+ // 预设组合:以 bare 包名加载本插件(dsh plugin add 安装后即可解析,可移植),
61
+ // 注册语料工具(registerTools:true);registerUi:false 让资料管理 API/设置 UI
62
+ // 归 host 常驻那份,预设只注册工具,避免重复注册 /api/prts-corpus 路由。
63
+ const bundledPreset = join(packageDir, 'presets', PRESET_ID)
64
+ const PRESET_COMPOSITION = readFileSync(join(bundledPreset, 'agent.cordis.yml'), 'utf8')
65
+ const PRESET_METADATA = readFileSync(join(bundledPreset, 'preset.yml'), 'utf8')
66
+
67
+ function run(cmd, args, stdio = 'inherit') {
68
+ if (process.platform === 'win32') {
69
+ // dsh 在 Windows 上通常是 .cmd,必须经 shell 执行;此时 execFileSync 会把
70
+ // 参数拼成一条命令行,含空格的路径用双引号包裹。cmd.exe 的引用规则无法
71
+ // 在双引号内屏蔽 %VAR% 展开与 & | < > ^ 等元字符——与其静默炸裂,不如
72
+ // 预检后明确报错,让用户换路径或用 DSH 环境变量指向 dsh.cmd 绝对路径。
73
+ const unsafe = /[\r\n%!&|<>^"]/
74
+ for (const value of [cmd, ...args]) {
75
+ if (unsafe.test(String(value))) {
76
+ throw new Error(
77
+ `Windows cmd 无法安全传递含特殊字符的参数:「${value}」。` +
78
+ '请把插件放到不含换行及 % ! & | < > ^ " 的路径后重试,或设置环境变量 DSH 指向 dsh.cmd 的绝对路径。')
79
+ }
80
+ }
81
+ const quote = (value) => `"${String(value)}"`
82
+ return execFileSync(quote(cmd), args.map(quote), {
83
+ stdio, encoding: 'utf8', shell: true, env: process.env,
84
+ })
85
+ }
86
+ return execFileSync(cmd, args, { stdio, encoding: 'utf8', env: process.env })
87
+ }
88
+
89
+ function usesDeclarativePresets(version) {
90
+ const match = /^\s*(?:dsh\s+)?(\d+)\.(\d+)\.(\d+)(?:[-+]|\s|$)/u.exec(version)
91
+ // Old launchers (and legacy package fixtures) may print no version at all.
92
+ if (!match && !version.trim()) return false
93
+ if (!match) throw new Error(`无法识别 DSH 版本:${JSON.stringify(version.trim())}`)
94
+ const [, major, minor, patch] = match.map(Number)
95
+ return major > 0 || minor > 1 || (minor === 1 && patch >= 7)
96
+ }
97
+
98
+ const CONFIG_KEY = /^ {2}(?:config|'config'|"config")[ \t]*:/u
99
+ const BLOCK_CONFIG = /^ {2}(?:config|'config'|"config")[ \t]*:[ \t]*(?:#[^\r\n]*)?\r?$/u
100
+ const ENABLED_GAMES_KEY = /^ {4}(?:enabledGames|'enabledGames'|"enabledGames")[ \t]*:/u
101
+
102
+ function presetEntryEnd(lines, start) {
103
+ // YAML mapping key order is not fixed: the next plugin may start with name,
104
+ // config, or an anchor. Any top-level sequence item ends this entry's scope.
105
+ const next = lines.findIndex((line, index) => index > start && /^-(?:\s|$)/u.test(line))
106
+ return next < 0 ? lines.length : next
107
+ }
108
+
109
+ function isStandardBlockConfig(lines, configIndex, end) {
110
+ if (!BLOCK_CONFIG.test(lines[configIndex])) return false
111
+ for (let index = configIndex + 1; index < end; index += 1) {
112
+ if (/^\s*(?:#.*)?$/u.test(lines[index])) continue
113
+ const indentation = /^ */u.exec(lines[index])[0].length
114
+ // 不把自定义缩进中的字段挤到安装器使用的四空格层级。
115
+ return indentation <= 2 || indentation === 4
116
+ }
117
+ return true
118
+ }
119
+
120
+ // 只改安装器使用的块映射;用户写成行内映射、alias 或 anchor 时保留原文。
121
+ // 在没有完整 YAML parser 的安装环境里,不能把不认识的写法当成缺失键。
122
+ /** 在已有 PRTS preset 中启用 DSH 安全 provider 支撑的 web_fetch,不触碰其他 entry。 */
123
+ function enableSafeWebFetch(composition) {
124
+ const lines = composition.split('\n')
125
+ const start = lines.findIndex((line) => /^- id: tool-web\s*$/.test(line))
126
+ if (start < 0) return composition
127
+ const end = presetEntryEnd(lines, start)
128
+ const configIndex = lines.findIndex((line, index) => index > start && index < end
129
+ && CONFIG_KEY.test(line))
130
+ if (configIndex >= 0 && !isStandardBlockConfig(lines, configIndex, end)) return composition
131
+ const fetchIndex = lines.findIndex((line, index) => index > start && index < end
132
+ && /^ {4}(?:fetch|'fetch'|"fetch")[ \t]*:/u.test(line))
133
+ if (fetchIndex >= 0) {
134
+ lines[fetchIndex] = ' fetch: true'
135
+ return lines.join('\n')
136
+ }
137
+ if (configIndex >= 0) lines.splice(configIndex + 1, 0, ' fetch: true')
138
+ else lines.splice(end, 0, ' config:', ' fetch: true')
139
+ return lines.join('\n')
140
+ }
141
+
142
+ /** 为安装器生成的双游戏云端 preset 显式固定本地资料范围。 */
143
+ function enableDualGameModules(composition) {
144
+ const lines = composition.split('\n')
145
+ const start = lines.findIndex((line) => /^- id: prts-corpus\s*$/.test(line))
146
+ if (start < 0) return composition
147
+ const end = presetEntryEnd(lines, start)
148
+ const block = lines.slice(start, end)
149
+ if (block.some((line) => ENABLED_GAMES_KEY.test(line))) return composition
150
+ if (block.some((line) => CONFIG_KEY.test(line) && !BLOCK_CONFIG.test(line))) return composition
151
+ const usesDefaultDualCloud = block.some((line) => /^\s{6}baseUrl:\s*https:\/\/prts\.chat\s*$/u.test(line))
152
+ && block.some((line) => /^\s{6}game:\s*all\s*$/u.test(line))
153
+ if (!usesDefaultDualCloud) return composition
154
+ const registerUi = lines.findIndex((line, index) => index > start && index < end
155
+ && /^\s{4}registerUi:\s*/u.test(line))
156
+ const registerTools = lines.findIndex((line, index) => index > start && index < end
157
+ && /^\s{4}registerTools:\s*/u.test(line))
158
+ const insertAfter = registerUi >= 0 ? registerUi : registerTools
159
+ if (insertAfter < 0) return composition
160
+ lines.splice(insertAfter + 1, 0, ' enabledGames:', ' - arknights', ' - endfield')
161
+ return lines.join('\n')
162
+ }
163
+
164
+ /** 让独立 Skill entry 与安装器生成的双模块工具 entry 使用同一基础范围。 */
165
+ function enableDualSkillModules(composition) {
166
+ const lines = composition.split('\n')
167
+ const start = lines.findIndex((line) => /^- id: prts-retrieval-skill\s*$/u.test(line))
168
+ if (start < 0) return composition
169
+ const end = presetEntryEnd(lines, start)
170
+ if (lines.slice(start, end).some((line) => ENABLED_GAMES_KEY.test(line))) return composition
171
+ const nameIndex = lines.findIndex((line, index) => index > start && index < end
172
+ && /^\s{2}name:\s*prts-terrarchive\/skill\s*$/u.test(line))
173
+ if (nameIndex < 0) return composition
174
+ const configIndex = lines.findIndex((line, index) => index > start && index < end
175
+ && CONFIG_KEY.test(line))
176
+ if (configIndex >= 0) {
177
+ if (!isStandardBlockConfig(lines, configIndex, end)) return composition
178
+ lines.splice(configIndex + 1, 0, ' enabledGames:', ' - arknights', ' - endfield')
179
+ } else {
180
+ lines.splice(nameIndex + 1, 0, ' config:', ' enabledGames:',
181
+ ' - arknights', ' - endfield')
182
+ }
183
+ return lines.join('\n')
184
+ }
185
+
186
+ function migrateLegacyCloudGame(composition) {
187
+ const lines = composition.split('\n')
188
+ const start = lines.findIndex((line) => /^- id: prts-corpus\s*$/u.test(line))
189
+ if (start < 0) return composition
190
+ const end = presetEntryEnd(lines, start)
191
+ const entry = lines.slice(start, end).join('\n').replace(
192
+ /(\s{4}cloud:\r?\n\s{6}baseUrl:\s*https:\/\/prts\.chat\r?\n\s{6}game:\s*)arknights\b/u,
193
+ '$1all',
194
+ )
195
+ return [...lines.slice(0, start), entry, ...lines.slice(end)].join('\n')
196
+ }
197
+
198
+ console.log(`prts-terrarchive 一键安装 → profile「${profile}」`)
199
+
200
+ if (!presetOnly) {
201
+ console.log('\n[1/2] 把插件加入 profile(dsh plugin add)…')
202
+ try {
203
+ run(dshCmd, ['plugin', '--profile', profile, 'add', pkg])
204
+ console.log(' 已加入。')
205
+ } catch (error) {
206
+ console.error(` 安装失败(${String(error?.message ?? error).split('\n')[0]})。`)
207
+ console.error(` 请修复后重试:${dshCmd} plugin --profile ${profile} add ${pkg}`)
208
+ process.exitCode = 1
209
+ throw error
210
+ }
211
+ } else {
212
+ console.log('\n[1/2] 插件实体由发行版管理,跳过 dsh plugin add。')
213
+ }
214
+
215
+ // --preset-only is used by offline legacy packagers: never invoke dsh there.
216
+ // A packager targeting modern DSH can declare the exact version explicitly.
217
+ const dshVersion = presetOnly ? (process.env.PRTS_DSH_VERSION || '') : run(dshCmd, ['--version'], 'pipe')
218
+ if (usesDeclarativePresets(dshVersion)) {
219
+ console.log('\n[2/2] 当前 DSH 使用声明式预设;PRTS 模式由插件自动注册,无需写入旧版用户预设文件。')
220
+ console.log('\n完成。重启 dsh,在新会话中选择「PRTS 模式」。')
221
+ } else {
222
+ console.log('\n[2/2] 创建 PRTS 用户预设…')
223
+ mkdirSync(presetDir, { recursive: true })
224
+ // 各文件独立修复;已存在的组合只迁移本插件旧 guidance,并补齐网页工具和 Skill loader,
225
+ // 不覆盖其它用户改动。
226
+ if (!existsSync(compositionPath)) {
227
+ writeFileSync(compositionPath, PRESET_COMPOSITION)
228
+ } else {
229
+ const existing = readFileSync(compositionPath, 'utf8')
230
+ let migrated = existing.replace(
231
+ /- id: prts-corpus-guidance\r?\n\s+name: prts-terrarchive\/guidance/g,
232
+ '- id: prts-retrieval-skill\n name: prts-terrarchive/skill',
233
+ )
234
+ // 0.1.0-alpha.1 的官方预设曾把基础层锁死为 arknights,使新版
235
+ // enabledGames 在无用户层配置时无法默认双游戏。只迁移本安装器生成的
236
+ // 标准 baseUrl + game 片段;自定义云端地址和其他 preset 不受影响。
237
+ migrated = migrateLegacyCloudGame(migrated)
238
+ migrated = enableDualGameModules(migrated)
239
+ migrated = enableSafeWebFetch(migrated)
240
+ // Web 搜索 provider 留在 DSH Web host;preset 只需挂载稳定的模型工具。
241
+ if (!/^- id: tool-web\s*$/m.test(migrated)) {
242
+ const toolWeb = "- id: tool-web\n name: '@deepseek-ai/dsh-tool-web'\n config:\n fetch: true\n searchTimeoutMs: 60000\n"
243
+ const toolSkillAnchor = /^- id: tool-skill\s*$/m
244
+ const retrievalSkillAnchor = /^- id: prts-retrieval-skill\s*$/m
245
+ migrated = toolSkillAnchor.test(migrated)
246
+ ? migrated.replace(toolSkillAnchor, `${toolWeb}- id: tool-skill`)
247
+ : retrievalSkillAnchor.test(migrated)
248
+ ? migrated.replace(retrievalSkillAnchor, `${toolWeb}- id: prts-retrieval-skill`)
249
+ : `${migrated.trimEnd()}\n${toolWeb}`
250
+ }
251
+ // DSH Web 把宿主层 tool-skill 禁用,由每个 agent preset 自行挂载。
252
+ // 旧 PRTS preset 只有 Skill 注册项,没有 catalog/loader,模型看不到也无法加载 Skill。
253
+ if (!/^- id: tool-skill\s*$/m.test(migrated)) {
254
+ const toolSkill = "- id: tool-skill\n name: '@deepseek-ai/dsh-tool-skill'\n"
255
+ const skillAnchor = /^- id: prts-retrieval-skill\s*$/m
256
+ migrated = skillAnchor.test(migrated)
257
+ ? migrated.replace(skillAnchor, `${toolSkill}- id: prts-retrieval-skill`)
258
+ : `${migrated.trimEnd()}\n${toolSkill}`
259
+ }
260
+ migrated = enableDualSkillModules(migrated)
261
+ if (migrated !== existing) writeFileSync(compositionPath, migrated)
262
+ }
263
+ if (!existsSync(metadataPath)) writeFileSync(metadataPath, PRESET_METADATA)
264
+ console.log(` 已确保预设文件存在(仅自动迁移本插件旧检索指导):${presetDir}`)
265
+
266
+ console.log('\n完成。重启 dsh 后:')
267
+ console.log(' · 设置 → 插件 →「PRTS 语料」= 资料管理(host 常驻,始终可进)')
268
+ console.log(' · 新建会话顶部的模式下拉选「PRTS 模式」→ 加载语料三工具')
269
+ console.log(' · 标准/极简等其它模式不加载 PRTS 工具')
270
+ console.log(` · 以后执行 dsh plugin update 后,可运行本命令加 --preset-only 同步预设迁移`)
271
+ console.log(` · 卸载:${dshCmd} plugin --profile ${profile} remove ${packageMetadata.name}`)
272
+ console.log(` · 想让新会话默认就用 PRTS 模式:设置 → Agent 预设 → 设为默认`)
273
+ }
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 预压缩终末地地图资源(与 endfield.prts.chat 前端 scripts/precompress.mjs
4
+ * 同一方法):对 map.js 与 resources/*.json 生成 brotli(q9, TEXT) 与 gzip(9)
5
+ * 两种压缩副本,并删除明文原件——插件交付包内只携带压缩版,由 ui.js 的静态
6
+ * 路由按 Accept-Encoding 直接回传对应编码。PNG 已是压缩格式,保持原样。
7
+ *
8
+ * 运行:node bin/pack-map-assets.mjs # 压缩(幂等,可重复执行)
9
+ * node bin/pack-map-assets.mjs --restore # 用压缩副本还原明文(调试用)
10
+ */
11
+ import { readdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
12
+ import { promisify } from 'node:util'
13
+ import { brotliCompress, brotliDecompress, constants, gunzip, gzip } from 'node:zlib'
14
+ import { dirname, join, resolve } from 'node:path'
15
+ import { fileURLToPath } from 'node:url'
16
+
17
+ const brotli = promisify(brotliCompress)
18
+ const unbrotli = promisify(brotliDecompress)
19
+ const gzipFile = promisify(gzip)
20
+ const gunzipFile = promisify(gunzip)
21
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'lib', 'endfield-map')
22
+ const restore = process.argv.includes('--restore')
23
+
24
+ const targets = [join(root, 'map.js')]
25
+ const resourcesDir = join(root, 'resources')
26
+ const resourceTargets = new Set()
27
+ for (const entry of await readdir(resourcesDir, { withFileTypes: true })) {
28
+ if (!entry.isFile()) continue
29
+ const match = /^(.*\.json)(?:\.(?:br|gz))?$/.exec(entry.name)
30
+ if (match) resourceTargets.add(match[1])
31
+ }
32
+ targets.push(...[...resourceTargets].sort().map((name) => join(resourcesDir, name)))
33
+
34
+ const megabytes = (bytes) => (bytes / 1_000_000).toFixed(2)
35
+ let rawBytes = 0
36
+ let brotliBytes = 0
37
+ let gzipBytes = 0
38
+ let processed = 0
39
+
40
+ async function readCompressed(filePath) {
41
+ try {
42
+ return await unbrotli(await readFile(`${filePath}.br`))
43
+ } catch (error) {
44
+ if (error?.code !== 'ENOENT') throw error
45
+ }
46
+ try {
47
+ return await gunzipFile(await readFile(`${filePath}.gz`))
48
+ } catch (error) {
49
+ if (error?.code !== 'ENOENT') throw error
50
+ }
51
+ throw Object.assign(new Error(`找不到明文或压缩副本:${filePath}`), { code: 'ENOENT' })
52
+ }
53
+
54
+ for (const filePath of targets) {
55
+ if (restore) {
56
+ // 明文存在说明已是还原态;否则真正解压 .br(缺失时回退 .gz)。
57
+ try {
58
+ await readFile(filePath)
59
+ continue
60
+ } catch { /* 缺明文 → 从压缩副本还原 */ }
61
+ const restored = await readCompressed(filePath)
62
+ const temp = `${filePath}.restore.tmp-${process.pid}`
63
+ await writeFile(temp, restored)
64
+ await rename(temp, filePath)
65
+ await rm(`${filePath}.br`, { force: true })
66
+ await rm(`${filePath}.gz`, { force: true })
67
+ processed += 1
68
+ continue
69
+ }
70
+ let source
71
+ try {
72
+ source = await readFile(filePath)
73
+ } catch (error) {
74
+ if (error?.code !== 'ENOENT') throw error
75
+ // 已同时具备两种发布变体即为打包态;重复执行应是无操作。
76
+ try {
77
+ await Promise.all([readFile(`${filePath}.br`), readFile(`${filePath}.gz`)])
78
+ continue
79
+ } catch { /* 副本不完整:从现有副本恢复明文并补齐 */ }
80
+ source = await readCompressed(filePath)
81
+ }
82
+ const [brotliResult, gzipResult] = await Promise.all([
83
+ brotli(source, {
84
+ params: {
85
+ [constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_TEXT,
86
+ [constants.BROTLI_PARAM_QUALITY]: 9,
87
+ },
88
+ }),
89
+ gzipFile(source, { level: 9 }),
90
+ ])
91
+ // 原子写压缩副本,确认落盘后再删明文。
92
+ const tmpBr = `${filePath}.br.tmp-${process.pid}`
93
+ const tmpGz = `${filePath}.gz.tmp-${process.pid}`
94
+ await writeFile(tmpBr, brotliResult)
95
+ await writeFile(tmpGz, gzipResult)
96
+ await rename(tmpBr, `${filePath}.br`)
97
+ await rename(tmpGz, `${filePath}.gz`)
98
+ await rm(filePath, { force: true })
99
+ rawBytes += source.length
100
+ brotliBytes += brotliResult.length
101
+ gzipBytes += gzipResult.length
102
+ processed += 1
103
+ }
104
+
105
+ if (!restore) {
106
+ console.log(`[pack-map-assets] ${processed}/${targets.length} files: ${megabytes(rawBytes)} MB raw → `
107
+ + `${megabytes(brotliBytes)} MB br + ${megabytes(gzipBytes)} MB gzip`)
108
+ } else {
109
+ console.log(`[pack-map-assets] restored ${processed}/${targets.length} files to plain text`)
110
+ }
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ // A loopback-only, read-only preview using the same corpus API as the plugin.
3
+ import { createServer } from 'node:http'
4
+ import { readFile, mkdtemp } from 'node:fs/promises'
5
+ import { tmpdir } from 'node:os'
6
+ import { join, resolve } from 'node:path'
7
+ import { fileURLToPath } from 'node:url'
8
+ import { CorpusStore } from '../src/store.js'
9
+ import { buildApi } from '../src/ui.js'
10
+
11
+ const root = fileURLToPath(new URL('../', import.meta.url))
12
+ const releasesDir = resolve(process.env.PRTS_RHINE_RELEASES || join(root, 'data/releases'))
13
+ const port = Number(process.env.PORT || 4177)
14
+ const temporary = await mkdtemp(join(tmpdir(), 'prts-rhine-preview-'))
15
+ const store = new CorpusStore({ releasesDir, cursorSecretPath: join(temporary, 'cursor-secret.bin') })
16
+ await store.ready()
17
+ const api = buildApi({ store, effective: () => ({ enabledGames: ['arknights', 'endfield'] }) })
18
+ const files = new Map([
19
+ ['/rhine/rhine.js', ['lib/rhine/rhine.js', 'text/javascript']],
20
+ ['/rhine/rhine.css', ['lib/rhine/rhine.css', 'text/css']],
21
+ ['/rhine/assets/archive-cassette.glb', ['lib/rhine/assets/archive-cassette.glb', 'model/gltf-binary']],
22
+ ['/rhine/assets/archive-assembly.glb', ['lib/rhine/assets/archive-assembly.glb', 'model/gltf-binary']],
23
+ ...['Light', 'Regular', 'Demibold', 'Bold'].map(weight => [`/rhine/fonts/MiSans-${weight}.woff2`, [`lib/rhine/fonts/MiSans-${weight}.woff2`, 'font/woff2']]),
24
+ ['/rhine/fonts/NOTICE.txt', ['lib/rhine/fonts/NOTICE.txt', 'text/plain; charset=utf-8']],
25
+ ['/rhine/fonts/MiSans-license.pdf', ['lib/rhine/fonts/MiSans-license.pdf', 'application/pdf']],
26
+ ])
27
+ const html = `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>莱茵生命 · 资料馆预览</title><link rel="stylesheet" href="/rhine/rhine.css"><style>html,body,#app{margin:0;width:100%;height:100%;overflow:hidden;background:#eeece4}.preview-label{position:fixed;z-index:50;bottom:2px;left:50%;transform:translateX(-50%);font:9px system-ui;color:#77766c;pointer-events:none;max-width:70vw;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}</style><body><main id="app"></main><span class="preview-label">本地预览 · 真实资料库</span><script src="/rhine/rhine.js"></script><script>
28
+ const snapshot={sessionId:'preview-local-v1',running:false,searching:false,query:'',tool:'',sources:[],answer:'',records:[]};
29
+ window.rhineWorkbench=window.__PRTS_RHINE__.mountRhineWorkbench(document.getElementById('app'),{
30
+ assetBase:'/rhine/',snapshot,agentAvailable:false,
31
+ api:async(endpoint,payload,signal)=>{const res=await fetch('/rpc',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({endpoint,payload}),signal});const body=await res.json();if(!res.ok)throw new Error(body.error||'请求失败');return body;},
32
+ askAgent:async()=>{throw new Error('这是资料馆独立预览。请在 Harness 会话中启用「莱茵生命资料馆」,即可将问题和摘录交给 Agent。');},
33
+ close:()=>{document.querySelector('.preview-label').textContent='独立预览没有对话窗口;在 Harness 中此按钮返回当前会话。';}
34
+ });
35
+ </script></body></html>`
36
+ const send = (res, status, body, type = 'application/json') => {
37
+ const data = typeof body === 'string' || Buffer.isBuffer(body) ? body : JSON.stringify(body)
38
+ res.writeHead(status, { 'content-type': type, 'x-content-type-options': 'nosniff',
39
+ 'cache-control': 'no-store', 'cross-origin-resource-policy': 'same-origin' })
40
+ res.end(data)
41
+ }
42
+ const server = createServer(async (req, res) => {
43
+ try {
44
+ if (!['GET', 'HEAD', 'POST'].includes(req.method)) return send(res, 405, { error: 'Method not allowed' })
45
+ const url = new URL(req.url, `http://127.0.0.1:${port}`)
46
+ if (req.method === 'GET' && url.pathname === '/') return send(res, 200, html, 'text/html; charset=utf-8')
47
+ const file = files.get(url.pathname)
48
+ if (file && ['GET', 'HEAD'].includes(req.method)) {
49
+ const bytes = await readFile(join(root, file[0]))
50
+ return send(res, 200, req.method === 'HEAD' ? '' : bytes, file[1])
51
+ }
52
+ if (req.method !== 'POST' || url.pathname !== '/rpc') return send(res, 404, { error: 'Not found' })
53
+ if (req.headers.origin && new URL(req.headers.origin).host !== req.headers.host) return send(res, 403, { error: 'Origin rejected' })
54
+ let body = ''
55
+ req.setEncoding('utf8')
56
+ for await (const chunk of req) {
57
+ body += chunk
58
+ if (Buffer.byteLength(body) > 128 * 1024) return send(res, 413, { error: 'Request too large' })
59
+ }
60
+ const { endpoint, payload } = JSON.parse(body)
61
+ const route = { 'archive.search': 'archive/search', 'archive.options': 'archive/options', 'archive.cloud-search': 'archive/cloud-search', read: 'read' }[endpoint]
62
+ if (!route) return send(res, 404, { error: '此预览只开放搜索与读取' })
63
+ const controller = new AbortController()
64
+ res.on('close', () => { if (!res.writableEnded) controller.abort() })
65
+ const result = await api.call(endpoint === 'archive.options' ? 'GET' : 'POST', '/api/prts-corpus/' + route, payload, { signal: controller.signal })
66
+ if (!res.destroyed) send(res, result.status, result.json)
67
+ } catch (error) {
68
+ if (!res.destroyed) send(res, 500, { error: error.message || 'Preview request failed' })
69
+ }
70
+ })
71
+ server.listen(port, '127.0.0.1', () => console.log(`Rhine preview: http://127.0.0.1:${port} · ${store.documents.size} documents · ${store.dataVersion}`))
@@ -0,0 +1,84 @@
1
+ import * as THREE from "three";
2
+
3
+ /** Offline rasterization of the authored shallow lettering. Runtime uses the
4
+ * generated data only; this module is not imported by the browser bundle. */
5
+ export function createArrayDetail(surface: string, source: THREE.BufferGeometry, material: THREE.Material) {
6
+ if (surface !== "Array_Interior_Lettering") return undefined;
7
+ const position = source.getAttribute("position"), normal = source.getAttribute("normal"), index = source.index;
8
+ if (!index || !normal || !position || !(material instanceof THREE.MeshStandardMaterial)) return undefined;
9
+ if (!source.boundingBox) source.computeBoundingBox();
10
+ const baked: number[] = [], bounds = new THREE.Box3(), point = new THREE.Vector3();
11
+ for (let i = 0; i < index.count; i += 3) {
12
+ const vertices = [index.getX(i), index.getX(i + 1), index.getX(i + 2)];
13
+ baked.push(...vertices);
14
+ for (const v of vertices) bounds.expandByPoint(point.fromBufferAttribute(position, v));
15
+ }
16
+ if (baked.length < 30 || bounds.isEmpty()) return undefined;
17
+ const size = bounds.getSize(new THREE.Vector3());
18
+ // Unsupported relief keeps the real mesh. This is not a general 3D impostor.
19
+ if (size.z > 0.0011 || size.x <= 0 || size.y <= 0) return undefined;
20
+ const width = 512, height = 64;
21
+ const paddingX = size.x * 2 / (width - 4), paddingY = size.y * 2 / (height - 4);
22
+ bounds.min.x -= paddingX; bounds.max.x += paddingX;
23
+ bounds.min.y -= paddingY; bounds.max.y += paddingY;
24
+ bounds.getSize(size);
25
+ const w = width * 2, h = height * 2;
26
+ const depth = new Float32Array(w * h).fill(-Infinity), normals = new Float32Array(w * h * 3);
27
+ for (let i = 0; i < baked.length; i += 3) {
28
+ const vertices = baked.slice(i, i + 3);
29
+ const xs = vertices.map(v => (position.getX(v) - bounds.min.x) / size.x * w);
30
+ const ys = vertices.map(v => (position.getY(v) - bounds.min.y) / size.y * h);
31
+ const denominator = (ys[1] - ys[2]) * (xs[0] - xs[2]) + (xs[2] - xs[1]) * (ys[0] - ys[2]);
32
+ if (Math.abs(denominator) < 1e-10) continue;
33
+ for (let y = Math.max(0, Math.floor(Math.min(...ys))); y < Math.min(h, Math.ceil(Math.max(...ys))); y++) {
34
+ for (let x = Math.max(0, Math.floor(Math.min(...xs))); x < Math.min(w, Math.ceil(Math.max(...xs))); x++) {
35
+ const a = ((ys[1] - ys[2]) * (x + 0.5 - xs[2]) + (xs[2] - xs[1]) * (y + 0.5 - ys[2])) / denominator;
36
+ const b = ((ys[2] - ys[0]) * (x + 0.5 - xs[2]) + (xs[0] - xs[2]) * (y + 0.5 - ys[2])) / denominator;
37
+ const c = 1 - a - b;
38
+ if (Math.min(a, b, c) < -1e-6) continue;
39
+ const z = a * position.getZ(vertices[0]) + b * position.getZ(vertices[1]) + c * position.getZ(vertices[2]);
40
+ const pixel = y * w + x;
41
+ if (z < depth[pixel]) continue;
42
+ depth[pixel] = z;
43
+ for (let channel = 0; channel < 3; channel++) normals[pixel * 3 + channel] =
44
+ a * normal.getComponent(vertices[0], channel) + b * normal.getComponent(vertices[1], channel) + c * normal.getComponent(vertices[2], channel);
45
+ }
46
+ }
47
+ }
48
+ const pixels = new Uint8Array(width * height * 4);
49
+ let occupied = 0;
50
+ for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) {
51
+ point.set(0, 0, 0); let count = 0;
52
+ for (let dy = 0; dy < 2; dy++) for (let dx = 0; dx < 2; dx++) {
53
+ const pixel = (y * 2 + dy) * w + x * 2 + dx;
54
+ if (!Number.isFinite(depth[pixel])) continue;
55
+ point.x += normals[pixel * 3]; point.y += normals[pixel * 3 + 1]; point.z += normals[pixel * 3 + 2]; count++;
56
+ }
57
+ if (count) { point.normalize(); occupied++; } else point.set(0, 0, 1);
58
+ const offset = (y * width + x) * 4;
59
+ pixels[offset] = Math.round((point.x * 0.5 + 0.5) * 255);
60
+ pixels[offset + 1] = Math.round((point.y * 0.5 + 0.5) * 255);
61
+ pixels[offset + 2] = Math.round((point.z * 0.5 + 0.5) * 255);
62
+ pixels[offset + 3] = Math.round(count * 255 / 4);
63
+ }
64
+ if (!occupied) return undefined;
65
+ const texture = new THREE.DataTexture(pixels, width, height, THREE.RGBAFormat);
66
+ texture.name = `Array ${surface} normal and coverage`;
67
+ texture.colorSpace = THREE.NoColorSpace;
68
+ texture.generateMipmaps = true; texture.minFilter = THREE.LinearMipmapLinearFilter;
69
+ texture.magFilter = THREE.LinearFilter; texture.anisotropy = 4; texture.needsUpdate = true;
70
+ const geometry = new THREE.PlaneGeometry(size.x, size.y).translate(
71
+ (bounds.min.x + bounds.max.x) / 2, (bounds.min.y + bounds.max.y) / 2, bounds.max.z + 0.00001);
72
+ const proxyMaterial = material.clone();
73
+ proxyMaterial.normalMap = texture; proxyMaterial.normalMapType = THREE.TangentSpaceNormalMap;
74
+ proxyMaterial.normalScale.set(1, 1); proxyMaterial.transparent = true; proxyMaterial.depthWrite = false;
75
+ proxyMaterial.alphaTest = 0.01;
76
+ proxyMaterial.onBeforeCompile = shader => {
77
+ shader.fragmentShader = shader.fragmentShader.replace("#include <alphatest_fragment>",
78
+ "diffuseColor.a *= texture2D(normalMap, vNormalMapUv).a;\n#include <alphatest_fragment>");
79
+ };
80
+ proxyMaterial.customProgramCacheKey = () => "array-normal-coverage-v1";
81
+ proxyMaterial.userData.arrayDetail = { surface, width, height, bakedTriangles: baked.length / 3 };
82
+ return { geometry, material: proxyMaterial, texture, bakedTriangles: baked.length / 3,
83
+ textureBytes: pixels.byteLength, sourceTriangles: index.count / 3 };
84
+ }