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/src/cloud.js ADDED
@@ -0,0 +1,498 @@
1
+ /**
2
+ * dsh 版云端检索适配器:cloud_search / cloud_inspect 的 Node HTTP 客户端。
3
+ *
4
+ * 与浏览器端 agent/browser/src/cloud-client.js 及 frontend/src/agent-demo/prts-auth.js 语义对齐:
5
+ * - 固定同站路径 /api/agent/retrieval/{capabilities,search,inspect},URL 永不受模型控制;
6
+ * - 认证:匿名 PoW 会话(GET /api/init-session → 解工作量证明 → POST /api/login),
7
+ * 或配置静态 token(cloud.token)跳过登录;401 时强制刷新一次重试;
8
+ * - 响应上限 32MiB;code===200 校验;HTTP 状态 → 契约错误码映射(agent-cloud-retrieval.v1)。
9
+ */
10
+ import { createHash, randomUUID } from 'node:crypto'
11
+ import { mkdir, readFile, writeFile } from 'node:fs/promises'
12
+ import { dirname } from 'node:path'
13
+ import { createRequire } from 'node:module'
14
+
15
+ export const CLOUD_CONTRACT_VERSION = 'agent-cloud-retrieval.v1'
16
+
17
+ /** 随包版本走的调用方标识;服务端据此把 DSH 插件流量与 web 前端分开统计。 */
18
+ export const DSH_CLIENT_HEADER = `dsh-plugin/${createRequire(import.meta.url)('../package.json').version}`
19
+
20
+ const CLIENT_ID_PATTERN = /^[A-Za-z0-9_-]{8,64}$/
21
+ const MAX_CLOUD_REQUEST_BYTES = 1024 * 1024
22
+
23
+ function normalizeCloudBaseUrl(value) {
24
+ let url
25
+ try { url = new URL(String(value ?? '')) } catch {
26
+ throw new TypeError('cloud baseUrl 必须是有效 URL')
27
+ }
28
+ const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)
29
+ if (url.username || url.password || url.search || url.hash
30
+ || (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback))) {
31
+ throw new TypeError('cloud baseUrl 必须使用 HTTPS(本地环回可用 HTTP),且不能包含凭证、查询或片段')
32
+ }
33
+ return url.toString().replace(/\/+$/, '')
34
+ }
35
+
36
+ function remoteMessage(value, fallback) {
37
+ if (typeof value !== 'string') return fallback
38
+ const normalized = value.replace(/[\u0000-\u001f\u007f]+/gu, ' ').replace(/\s+/gu, ' ').trim()
39
+ return normalized ? normalized.slice(0, 1000) : fallback
40
+ }
41
+
42
+ /**
43
+ * 匿名会话的持久 client id:同一安装跨重启保持稳定,服务端按它统计
44
+ * 独立用户。文件 0600、目录 0700,并发首次创建以 wx 保证只有一个胜者。
45
+ * @param {string} path 如 $DSH_HOME/prts-corpus/client-id
46
+ * @returns {Promise<string>}
47
+ */
48
+ export async function readOrCreateClientId(path) {
49
+ try {
50
+ const existing = (await readFile(path, 'utf8')).trim()
51
+ if (CLIENT_ID_PATTERN.test(existing)) return existing
52
+ } catch (error) {
53
+ if (error?.code !== 'ENOENT') throw error
54
+ }
55
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 })
56
+ const created = `dsh-${randomUUID().replaceAll('-', '').slice(0, 20)}`
57
+ try {
58
+ await writeFile(path, `${created}\n`, { flag: 'wx', mode: 0o600 })
59
+ return created
60
+ } catch (error) {
61
+ if (error?.code !== 'EEXIST') throw error
62
+ const existing = (await readFile(path, 'utf8')).trim()
63
+ if (!CLIENT_ID_PATTERN.test(existing)) throw new Error('cloud client id 文件损坏')
64
+ return existing
65
+ }
66
+ }
67
+
68
+ const PATHS = {
69
+ capabilities: '/api/agent/retrieval/capabilities',
70
+ cloud_search: '/api/agent/retrieval/search',
71
+ cloud_inspect: '/api/agent/retrieval/inspect',
72
+ }
73
+
74
+ export class CloudFault extends Error {
75
+ /**
76
+ * @param {string} code 契约错误码
77
+ * @param {string} message
78
+ * @param {boolean} [retryable]
79
+ * @param {object} [details]
80
+ */
81
+ constructor(code, message, retryable = false, details = undefined) {
82
+ super(message)
83
+ this.code = code
84
+ this.retryable = retryable
85
+ this.details = details
86
+ }
87
+ }
88
+
89
+ /** ---- 匿名会话(init-session + PoW + login,与浏览器 prts-auth 同构) ---- */
90
+
91
+ const AUTH_RESPONSE_MAX_BYTES = 256 * 1024
92
+ const MAX_POW_SEED_CHARS = 512
93
+ const MAX_POW_DIFFICULTY = 6
94
+
95
+ function powHex(seed, nonce) {
96
+ return createHash('sha256').update(`${seed}${nonce}`, 'utf8').digest('hex')
97
+ }
98
+
99
+ function cancelledFault() {
100
+ return new CloudFault('CANCELLED', '云端请求已取消')
101
+ }
102
+
103
+ function throwIfCancelled(signal) {
104
+ if (signal?.aborted) throw cancelledFault()
105
+ }
106
+
107
+ /** 解工作量证明:sha256(seed+nonce) 十六进制前缀 difficulty 个 0。定期让出事件循环以响应取消。 */
108
+ export async function solvePow(seed, difficulty, { signal } = {}) {
109
+ if (typeof seed !== 'string' || !seed || [...seed].length > MAX_POW_SEED_CHARS
110
+ || !Number.isInteger(difficulty) || difficulty < 0 || difficulty > MAX_POW_DIFFICULTY) {
111
+ throw new CloudFault('AUTH_REQUIRED', 'PRTS 会话返回了非法的工作量证明参数')
112
+ }
113
+ const prefix = '0'.repeat(difficulty)
114
+ const deadline = Date.now() + 8000
115
+ for (let nonce = 0; ; nonce += 1) {
116
+ throwIfCancelled(signal)
117
+ if (Date.now() > deadline) throw new CloudFault('AUTH_REQUIRED', 'PRTS 会话验证计算超时')
118
+ if (powHex(seed, nonce).startsWith(prefix)) return String(nonce)
119
+ if (nonce > 0 && nonce % 2048 === 0) {
120
+ await new Promise((resolve) => { setImmediate(resolve) })
121
+ }
122
+ }
123
+ }
124
+
125
+ async function fetchWithTimeout(fetchImpl, url, init, timeoutMs, signal, consume = (response) => response) {
126
+ throwIfCancelled(signal)
127
+ const controller = new AbortController()
128
+ let timedOut = false
129
+ const timer = setTimeout(() => {
130
+ timedOut = true
131
+ controller.abort()
132
+ }, timeoutMs)
133
+ const onAbort = () => controller.abort()
134
+ signal?.addEventListener('abort', onAbort, { once: true })
135
+ try {
136
+ // 云端 API 没有跨源跳转语义;拒绝 redirect,避免静态 token 或匿名会话
137
+ // 被配置源借 30x 带往另一个网络边界。
138
+ const response = await fetchImpl(url, { ...init, redirect: 'error', signal: controller.signal })
139
+ return await consume(response)
140
+ } catch (error) {
141
+ if (signal?.aborted) throw cancelledFault()
142
+ if (error instanceof CloudFault) throw error
143
+ if (timedOut || error?.name === 'AbortError') {
144
+ throw new CloudFault('CLOUD_TIMEOUT', '云端请求超时', true)
145
+ }
146
+ throw new CloudFault('CLOUD_UNAVAILABLE', `无法连接 ${new URL(url).host}`, true,
147
+ { exception_type: error?.constructor?.name || typeof error })
148
+ } finally {
149
+ clearTimeout(timer)
150
+ signal?.removeEventListener('abort', onAbort)
151
+ }
152
+ }
153
+
154
+ async function readJsonResponse(response, serviceName) {
155
+ const bytes = await readResponseBytes(response, AUTH_RESPONSE_MAX_BYTES)
156
+ const text = new TextDecoder().decode(bytes)
157
+ let payload = null
158
+ if (text.trim()) {
159
+ try {
160
+ payload = JSON.parse(text)
161
+ } catch {
162
+ throw new CloudFault('INVALID_RESPONSE', `${serviceName}返回了无法解析的响应(HTTP ${response.status})`)
163
+ }
164
+ }
165
+ if (!response.ok) {
166
+ const detail = payload?.detail || payload?.error?.message
167
+ throw new CloudFault('CLOUD_ERROR', remoteMessage(detail,
168
+ `${serviceName}不可用(HTTP ${response.status})`),
169
+ response.status >= 500)
170
+ }
171
+ if (!payload) throw new CloudFault('INVALID_RESPONSE', `${serviceName}返回了空响应`)
172
+ return payload
173
+ }
174
+
175
+ /**
176
+ * 匿名会话提供者:token 缓存在单个 Agent 的客户端内存(不落盘)。
177
+ */
178
+ export class AnonymousSessionProvider {
179
+ /**
180
+ * @param {{ baseUrl: string, userId?: string, timeoutMs?: number, fetchImpl?: typeof fetch }} options
181
+ */
182
+ constructor({ baseUrl, userId, timeoutMs = 8000, fetchImpl } = {}) {
183
+ this.baseUrl = normalizeCloudBaseUrl(baseUrl)
184
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 10 * 60_000) {
185
+ throw new TypeError('cloud timeoutMs 超出允许范围')
186
+ }
187
+ if (userId != null && (typeof userId !== 'string' || !userId || userId.length > 128)) {
188
+ throw new TypeError('cloud userId 非法')
189
+ }
190
+ this.userId = userId || `dsh-${randomUUID().slice(0, 12)}`
191
+ this.timeoutMs = timeoutMs
192
+ this.fetchImpl = fetchImpl || fetch
193
+ /** @type {string | null} */
194
+ this.token = null
195
+ /** @type {{ promise: Promise<string>, controller: AbortController, waiters: number } | null} */
196
+ this.inFlight = null
197
+ }
198
+
199
+ /** @param {{ forceRefresh?: boolean, signal?: AbortSignal }} [options] @returns {Promise<string>} Bearer token */
200
+ async getToken({ forceRefresh = false, signal } = {}) {
201
+ throwIfCancelled(signal)
202
+ if (forceRefresh) {
203
+ this.token = null
204
+ // 强刷不复用 refresh 之前已在途的登录:它可能正是产出刚被 401 的令牌的
205
+ // 一次登录,等待它会在"token 未变化"的判定下放弃 401 重试。脱离引用
206
+ // 后旧登录照常完成并让自身的等待者拿到结果,只是不再作为本次的来源。
207
+ this.inFlight = null
208
+ }
209
+ if (this.token) return this.token
210
+ if (!this.inFlight) {
211
+ const controller = new AbortController()
212
+ const record = { controller, waiters: 0, promise: null }
213
+ record.promise = this.login({ signal: controller.signal }).finally(() => {
214
+ if (this.inFlight === record) this.inFlight = null
215
+ })
216
+ this.inFlight = record
217
+ }
218
+ return this.waitForLogin(this.inFlight, signal)
219
+ }
220
+
221
+ async waitForLogin(record, signal) {
222
+ record.waiters += 1
223
+ return new Promise((resolve, reject) => {
224
+ let settled = false
225
+ const finish = (callback, value) => {
226
+ if (settled) return
227
+ settled = true
228
+ signal?.removeEventListener('abort', onAbort)
229
+ record.waiters -= 1
230
+ callback(value)
231
+ }
232
+ const onAbort = () => {
233
+ finish(reject, cancelledFault())
234
+ if (record.waiters === 0) record.controller.abort()
235
+ }
236
+ signal?.addEventListener('abort', onAbort, { once: true })
237
+ if (signal?.aborted) return onAbort()
238
+ record.promise.then(
239
+ (token) => finish(resolve, token),
240
+ (error) => finish(reject, error),
241
+ )
242
+ })
243
+ }
244
+
245
+ async login({ signal } = {}) {
246
+ const sessionPayload = await fetchWithTimeout(this.fetchImpl,
247
+ `${this.baseUrl}/api/init-session`, { headers: { Accept: 'application/json' } }, this.timeoutMs, signal,
248
+ (response) => readJsonResponse(response, 'PRTS 会话服务'))
249
+ const session = sessionPayload?.data
250
+ if (typeof session?.session_token !== 'string' || !session.session_token
251
+ || session.session_token.length > 4096) {
252
+ throw new CloudFault('AUTH_REQUIRED', 'PRTS 会话响应缺少有效的 session_token')
253
+ }
254
+ const nonce = session.pow_seed
255
+ ? await solvePow(session.pow_seed, session.pow_difficulty ?? 3, { signal })
256
+ : null
257
+ const loginPayload = await fetchWithTimeout(this.fetchImpl,
258
+ `${this.baseUrl}/api/login`, {
259
+ method: 'POST',
260
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json',
261
+ 'X-Session-Token': session.session_token },
262
+ body: JSON.stringify({ user_id: this.userId, nonce }),
263
+ }, this.timeoutMs, signal, (response) => readJsonResponse(response, 'PRTS 登录服务'))
264
+ if (typeof loginPayload?.data !== 'string' || !loginPayload.data
265
+ || loginPayload.data.length > 16 * 1024) {
266
+ throw new CloudFault('AUTH_REQUIRED', 'PRTS 登录响应缺少有效的访问令牌')
267
+ }
268
+ const token = loginPayload.data
269
+ this.token = token.startsWith('Bearer ') ? token : `Bearer ${token}`
270
+ return this.token
271
+ }
272
+ }
273
+
274
+ async function readResponseBytes(response, maximum) {
275
+ const declared = Number(response.headers?.get?.('content-length') ?? NaN)
276
+ if (Number.isFinite(declared) && declared > maximum) {
277
+ throw new CloudFault('RESPONSE_TOO_LARGE', '云端检索响应超过接收上限')
278
+ }
279
+ const reader = response.body?.getReader?.()
280
+ if (!reader) {
281
+ const bytes = new Uint8Array(await response.arrayBuffer())
282
+ if (bytes.byteLength > maximum) throw new CloudFault('RESPONSE_TOO_LARGE', '云端检索响应超过接收上限')
283
+ return bytes
284
+ }
285
+ const chunks = []
286
+ let size = 0
287
+ for (;;) {
288
+ const { done, value } = await reader.read()
289
+ if (done) break
290
+ size += value.byteLength
291
+ if (size > maximum) {
292
+ await reader.cancel().catch(() => {})
293
+ throw new CloudFault('RESPONSE_TOO_LARGE', '云端检索响应超过接收上限')
294
+ }
295
+ chunks.push(value)
296
+ }
297
+ const bytes = new Uint8Array(size)
298
+ let offset = 0
299
+ for (const chunk of chunks) {
300
+ bytes.set(chunk, offset)
301
+ offset += chunk.byteLength
302
+ }
303
+ return bytes
304
+ }
305
+
306
+ /** 静态 token 提供者(cloud.token 配置)。 */
307
+ export class StaticTokenProvider {
308
+ constructor(token) {
309
+ if (typeof token !== 'string' || !token || token.length > 16 * 1024) {
310
+ throw new TypeError('cloud token 非法')
311
+ }
312
+ this.token = token.startsWith('Bearer ') ? token : `Bearer ${token}`
313
+ }
314
+
315
+ async getToken({ signal } = {}) {
316
+ throwIfCancelled(signal)
317
+ return this.token
318
+ }
319
+ }
320
+
321
+ /** 为每个 Agent 惰性创建独立客户端;缺少 Agent 上下文时不缓存,避免意外跨执行共享。 */
322
+ export function createAgentCloudClientRegistry(createClient) {
323
+ const clients = new WeakMap()
324
+ return {
325
+ forExecution(exec) {
326
+ const agent = exec?.agent
327
+ if ((typeof agent !== 'object' || agent === null) && typeof agent !== 'function') {
328
+ return createClient()
329
+ }
330
+ let client = clients.get(agent)
331
+ if (!client) {
332
+ client = createClient()
333
+ clients.set(agent, client)
334
+ }
335
+ return client
336
+ },
337
+ }
338
+ }
339
+
340
+ /** ---- 云端检索客户端 ---- */
341
+
342
+ /**
343
+ * @param {{ baseUrl: string, tokenProvider: { getToken(options?: {forceRefresh?: boolean, signal?: AbortSignal}): Promise<string> },
344
+ * game?: 'all' | 'arknights' | 'endfield', games?: ('arknights' | 'endfield')[],
345
+ * timeoutMs?: number, maxResponseBytes?: number, fetchImpl?: typeof fetch }} options
346
+ */
347
+ export class CloudRetrievalClient {
348
+ constructor({
349
+ baseUrl, tokenProvider, game = 'all', games, timeoutMs = 90_000,
350
+ maxResponseBytes = 32 * 1024 * 1024, fetchImpl,
351
+ } = {}) {
352
+ if (!['all', 'arknights', 'endfield'].includes(game)) throw new TypeError('不支持的游戏命名空间')
353
+ const effectiveGames = games || (game === 'all' ? ['arknights', 'endfield'] : [game])
354
+ if (!Array.isArray(effectiveGames) || effectiveGames.length < 1 || effectiveGames.length > 2
355
+ || new Set(effectiveGames).size !== effectiveGames.length
356
+ || effectiveGames.some((item) => !['arknights', 'endfield'].includes(item))) {
357
+ throw new TypeError('games 只能包含不重复的 arknights / endfield')
358
+ }
359
+ this.baseUrl = normalizeCloudBaseUrl(baseUrl)
360
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 10 * 60_000) {
361
+ throw new TypeError('cloud timeoutMs 超出允许范围')
362
+ }
363
+ if (!Number.isInteger(maxResponseBytes) || maxResponseBytes < 1
364
+ || maxResponseBytes > 64 * 1024 * 1024) {
365
+ throw new TypeError('cloud maxResponseBytes 超出允许范围')
366
+ }
367
+ this.tokenProvider = tokenProvider
368
+ this.game = game
369
+ this.games = [...effectiveGames]
370
+ this.timeoutMs = timeoutMs
371
+ this.maxResponseBytes = maxResponseBytes
372
+ this.fetchImpl = fetchImpl || fetch
373
+ /** @type {unknown | null} capabilities 成功结果缓存 */
374
+ this._capabilities = null
375
+ }
376
+
377
+ /** 契约版本自检;成功结果在单个 Agent 的客户端内缓存。并发首检各自响应取消。 */
378
+ async capabilities({ signal } = {}) {
379
+ if (this._capabilities) return this._capabilities
380
+ const response = await this.request('capabilities', 'GET', undefined, { signal })
381
+ if (response.data?.contract_version !== CLOUD_CONTRACT_VERSION) {
382
+ throw new CloudFault('CONTRACT_MISMATCH',
383
+ `云端检索契约版本不兼容(期望 ${CLOUD_CONTRACT_VERSION},得到 ${response.data?.contract_version})`)
384
+ }
385
+ this._capabilities = response
386
+ return response
387
+ }
388
+
389
+ /**
390
+ * cloud_search:注入 request_id / intent 维度字段由调用方(插件层)补齐。
391
+ * @param {object} arguments_ 已展开的模型参数(含 query 等)
392
+ */
393
+ async search(arguments_, { signal } = {}) {
394
+ const payload = structuredClone(arguments_)
395
+ payload.request_id ||= `req-${randomUUID().replaceAll('-', '').slice(0, 24)}`
396
+ payload.games ||= [...this.games]
397
+ return this.request('cloud_search', 'POST', payload, { signal })
398
+ }
399
+
400
+ /** cloud_inspect:request_id 由按 Agent 隔离的插件证据状态注入。 */
401
+ async inspect(arguments_, { signal } = {}) {
402
+ const payload = structuredClone(arguments_)
403
+ return this.request('cloud_inspect', 'POST', payload, { signal })
404
+ }
405
+
406
+ async request(tool, method, body = undefined, { signal } = {}) {
407
+ throwIfCancelled(signal)
408
+ const serializedBody = body === undefined ? undefined : JSON.stringify(body)
409
+ if (serializedBody !== undefined && Buffer.byteLength(serializedBody) > MAX_CLOUD_REQUEST_BYTES) {
410
+ throw new CloudFault('INVALID_REQUEST', '云端检索请求体超过客户端上限')
411
+ }
412
+ let token = await this.tokenProvider.getToken({ forceRefresh: false, signal })
413
+ const perform = async (authorization) => {
414
+ const controller = new AbortController()
415
+ let timedOut = false
416
+ const timer = setTimeout(() => {
417
+ timedOut = true
418
+ controller.abort()
419
+ }, this.timeoutMs)
420
+ const onAbort = () => controller.abort()
421
+ signal?.addEventListener('abort', onAbort, { once: true })
422
+ if (signal?.aborted) controller.abort()
423
+ try {
424
+ const response = await this.fetchImpl(`${this.baseUrl}${PATHS[tool]}`, {
425
+ method,
426
+ headers: { Accept: 'application/json', Authorization: authorization,
427
+ ...(this.games.length === 1 ? { 'X-Game': this.games[0] } : { 'X-Games': this.games.join(',') }),
428
+ 'X-Client': DSH_CLIENT_HEADER,
429
+ ...(body ? { 'Content-Type': 'application/json' } : {}) },
430
+ ...(serializedBody !== undefined ? { body: serializedBody } : {}),
431
+ redirect: 'error',
432
+ signal: controller.signal,
433
+ })
434
+ const bytes = await readResponseBytes(response, this.maxResponseBytes)
435
+ return { response, bytes }
436
+ } catch (error) {
437
+ if (signal?.aborted) throw new CloudFault('CANCELLED', '云端检索已取消')
438
+ if (error instanceof CloudFault) throw error
439
+ if (timedOut || error?.name === 'AbortError') {
440
+ throw new CloudFault('CLOUD_TIMEOUT', '云端检索超过客户端等待期限', true)
441
+ }
442
+ throw new CloudFault('CLOUD_UNAVAILABLE', '无法连接云端检索服务', true,
443
+ { exception_type: error?.constructor?.name || typeof error })
444
+ } finally {
445
+ clearTimeout(timer)
446
+ signal?.removeEventListener('abort', onAbort)
447
+ }
448
+ }
449
+ let result = await perform(token)
450
+ // 本地缓存的 JWT 可能比服务端有效期短:401 时强制刷新一次重试,
451
+ // 避免过期 token 让整轮 agent 的云端工具全部失效。
452
+ if (result.response.status === 401) {
453
+ const refreshed = await this.tokenProvider.getToken({ forceRefresh: true, signal })
454
+ if (refreshed && refreshed !== token) {
455
+ token = refreshed
456
+ result = await perform(token)
457
+ }
458
+ }
459
+ const { response, bytes } = result
460
+ let parsed
461
+ try {
462
+ parsed = JSON.parse(new TextDecoder().decode(bytes))
463
+ } catch {
464
+ throw new CloudFault('INVALID_RESPONSE', '云端检索返回的不是有效 JSON')
465
+ }
466
+ if (!response.ok) throw httpFault(response.status, parsed)
467
+ if (parsed?.code !== 200) throw new CloudFault('INVALID_RESPONSE', '云端检索返回无效响应')
468
+ return parsed
469
+ }
470
+ }
471
+
472
+ export function httpFault(status, body) {
473
+ const mapping = { 401: ['AUTH_REQUIRED', false], 403: ['ACCESS_DENIED', false],
474
+ 404: ['TRACE_NOT_FOUND', false], 409: ['REQUEST_CONFLICT', true],
475
+ 413: ['RESPONSE_TOO_LARGE', false], 422: ['INVALID_REQUEST', false],
476
+ 429: ['CLOUD_BUSY', true], 503: ['CLOUD_UNAVAILABLE', true], 504: ['CLOUD_TIMEOUT', true] }
477
+ const [code, retryable] = mapping[status] || ['CLOUD_ERROR', status >= 500]
478
+ const rawMessage = typeof body?.detail === 'string' ? body.detail
479
+ : Array.isArray(body?.detail)
480
+ ? body.detail.slice(0, 20).map((item) =>
481
+ `${(Array.isArray(item?.loc) ? item.loc : []).slice(0, 12).join('.')}: ${item?.msg || '参数无效'}`).join(';')
482
+ : '云端检索请求失败'
483
+ const message = remoteMessage(rawMessage, '云端检索请求失败')
484
+ return new CloudFault(code, message, retryable, { http_status: status,
485
+ ...(body?.detail ? { validation: body.detail } : {}) })
486
+ }
487
+
488
+ /** ---- 插件层薄封装:错误 → 契约 error 响应 ---- */
489
+
490
+ export function cloudErrorResponse(error) {
491
+ if (error instanceof CloudFault) {
492
+ return { contract_version: CLOUD_CONTRACT_VERSION, status: 'error',
493
+ error: { code: error.code, message: error.message, retryable: error.retryable,
494
+ ...(error.details ? { details: error.details } : {}) } }
495
+ }
496
+ return { contract_version: CLOUD_CONTRACT_VERSION, status: 'error',
497
+ error: { code: 'CLOUD_INTERNAL_ERROR', message: '云端检索执行失败;详情请查看 DSH Host 日志', retryable: false } }
498
+ }