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/state.js ADDED
@@ -0,0 +1,314 @@
1
+ /**
2
+ * 插件共享状态与运行时配置。
3
+ *
4
+ * 配置分三层(与 DSH settings 的解析顺序同构,但不依赖 schemastery,
5
+ * 保持零 npm 依赖):
6
+ * 默认值 ← cordis.patch.yml 行内 config(base 层)← 用户文件($DSH_HOME/prts-corpus.json)
7
+ * 界面(settings tab)改配置 = 写用户层文件并立即生效:
8
+ * 显式下载的字节回退源走 getter 每次调用现读;可信元数据固定来自
9
+ * https://prts.chat;cloud 工具由
10
+ * index.js 的 rebuildCloud() 在配置变化时 dispose + 重注册。
11
+ */
12
+ import { readFile, rename, writeFile, mkdir, unlink } from 'node:fs/promises'
13
+ import { randomBytes } from 'node:crypto'
14
+ import { dirname } from 'node:path'
15
+ import { DEFAULT_SITE_BASE_URL } from './installer.js'
16
+ import { watchFileChanges } from './release-watch.js'
17
+
18
+ /** 运行时可改配置的默认值。 */
19
+ export const CONFIG_DEFAULTS = Object.freeze({
20
+ uiSkin: 'harness',
21
+ cloudEnabled: false,
22
+ cloudBaseUrl: DEFAULT_SITE_BASE_URL,
23
+ cloudToken: '',
24
+ cloudGame: 'all',
25
+ enabledGames: Object.freeze(['arknights', 'endfield']),
26
+ cloudUserId: '',
27
+ downloadSiteBaseUrl: DEFAULT_SITE_BASE_URL,
28
+ downloadOrder: Object.freeze(['modelscope', 'site']),
29
+ cloudTimeoutMs: 90_000,
30
+ cloudMaxResponseBytes: 32 * 1024 * 1024,
31
+ cacheShards: 24,
32
+ })
33
+
34
+ /**
35
+ * 云端/字节回退站点地址校验:必须是 https,或仅指向环回主机的 http(本地开发用)。
36
+ * 明文 http 只允许 localhost / 127.0.0.1 / [::1],避免把 token 或资料
37
+ * 下载暴露给局域网中间人。
38
+ */
39
+ const isServiceBaseUrl = (value) => {
40
+ if (typeof value !== 'string') return false
41
+ const raw = value.trim()
42
+ if (!raw || raw.length >= 512) return false
43
+ try {
44
+ const url = new URL(raw)
45
+ if (url.username || url.password || url.search || url.hash) return false
46
+ if (url.protocol === 'https:') return true
47
+ return url.protocol === 'http:'
48
+ && (url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]')
49
+ } catch {
50
+ return false
51
+ }
52
+ }
53
+
54
+ const serviceOrigin = (value) => new URL(value).origin
55
+
56
+ /** 界面可写的键(白名单 + 类型校验器)。 */
57
+ const WRITABLE = {
58
+ uiSkin: (v) => v === 'harness' || v === 'prts-agent' || v === 'endfield-aic' || v === 'rhine-lab',
59
+ cloudEnabled: (v) => typeof v === 'boolean',
60
+ cloudBaseUrl: isServiceBaseUrl,
61
+ cloudToken: (v) => typeof v === 'string' && v.length < 4096,
62
+ cloudGame: (v) => v === 'all' || v === 'arknights' || v === 'endfield',
63
+ enabledGames: (v) => Array.isArray(v) && v.length > 0 && v.length <= 2
64
+ && new Set(v).size === v.length
65
+ && v.every((entry) => entry === 'arknights' || entry === 'endfield'),
66
+ cloudUserId: (v) => typeof v === 'string' && v.length < 128,
67
+ downloadSiteBaseUrl: isServiceBaseUrl,
68
+ downloadOrder: (v) => Array.isArray(v) && v.length > 0 && v.length <= 2
69
+ && new Set(v).size === v.length && v.every((entry) => entry === 'modelscope' || entry === 'site'),
70
+ cloudTimeoutMs: (v) => Number.isInteger(v) && v >= 1000 && v <= 10 * 60_000,
71
+ cloudMaxResponseBytes: (v) => Number.isInteger(v) && v >= 1024 && v <= 64 * 1024 * 1024,
72
+ cacheShards: (v) => Number.isInteger(v) && v >= 1 && v <= 128,
73
+ }
74
+
75
+ /** 从 cordis.patch.yml 行内 config 提取 base 层。 */
76
+ function baseLayer(patchConfig = {}) {
77
+ const base = {}
78
+ if (patchConfig.uiSkin) base.uiSkin = String(patchConfig.uiSkin)
79
+ const cloud = patchConfig.cloud
80
+ if (cloud?.baseUrl) {
81
+ base.cloudEnabled = true
82
+ base.cloudBaseUrl = String(cloud.baseUrl)
83
+ if (cloud.token) base.cloudToken = String(cloud.token)
84
+ if (cloud.game) base.cloudGame = cloud.game
85
+ if (cloud.userId) base.cloudUserId = String(cloud.userId)
86
+ }
87
+ if (Array.isArray(patchConfig.enabledGames)) base.enabledGames = [...patchConfig.enabledGames]
88
+ const download = patchConfig.download
89
+ if (download?.siteBaseUrl) base.downloadSiteBaseUrl = String(download.siteBaseUrl)
90
+ if (Array.isArray(download?.order)) base.downloadOrder = [...download.order]
91
+ if (Number.isInteger(cloud?.timeoutMs)) base.cloudTimeoutMs = cloud.timeoutMs
92
+ if (Number.isInteger(cloud?.maxResponseBytes)) base.cloudMaxResponseBytes = cloud.maxResponseBytes
93
+ if (Number.isInteger(patchConfig.cacheShards)) base.cacheShards = patchConfig.cacheShards
94
+ return validateUserLayer(base)
95
+ }
96
+
97
+ const cancelledConfigWrite = () => Object.assign(new Error('PRTS 配置写入已取消'), {
98
+ name: 'AbortError', code: 'CANCELLED',
99
+ })
100
+
101
+ const assertConfigWriteActive = (signal) => {
102
+ if (signal?.aborted) throw cancelledConfigWrite()
103
+ }
104
+
105
+ const atomicWriteJson = async (path, value, { signal } = {}) => {
106
+ assertConfigWriteActive(signal)
107
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 })
108
+ assertConfigWriteActive(signal)
109
+ const temp = `${path}.${randomBytes(6).toString('hex')}.tmp`
110
+ try {
111
+ await writeFile(temp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600, signal })
112
+ // rename 是不可取消的原子提交点;进入它之前最后检查一次。进入后即使
113
+ // transport 同时断开,也必须让内存态跟已经替换的磁盘文件保持一致。
114
+ assertConfigWriteActive(signal)
115
+ await rename(temp, path)
116
+ } catch (error) {
117
+ await unlink(temp).catch(() => {})
118
+ if (signal?.aborted || error?.name === 'AbortError' || error?.code === 'ABORT_ERR') {
119
+ throw cancelledConfigWrite()
120
+ }
121
+ throw error
122
+ }
123
+ }
124
+
125
+ function validateUserLayer(value, { persisted = false } = {}) {
126
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
127
+ throw Object.assign(new Error('PRTS 配置文件必须是对象'), { code: 'INVALID_CONFIG' })
128
+ }
129
+ const result = {}
130
+ const deprecated = new Set([
131
+ 'approvalMode', 'softIntentCharacters', 'hardIntentCharacters', 'hardIntentRecords', 'autoDownload',
132
+ // 远程下载已改为必须由 PRTS.chat current 选版;旧 pin 键只做无害迁移。
133
+ 'downloadReleaseId',
134
+ ])
135
+ for (const [key, entry] of Object.entries(value)) {
136
+ // 旧版用户配置可以无痛升级:预算/审批已经完全交给 DSH,这些键不再生效。
137
+ if (deprecated.has(key)) continue
138
+ // 令牌所属源只由 saveConfig 在显式提交令牌时记录,不能由 UI 补丁改绑。
139
+ if (persisted && key === 'cloudTokenOrigin') {
140
+ if (!isServiceBaseUrl(entry) || serviceOrigin(entry) !== entry) {
141
+ throw Object.assign(new Error('配置项 cloudTokenOrigin 的值不合法'), { code: 'INVALID_CONFIG' })
142
+ }
143
+ result.cloudTokenOrigin = entry
144
+ continue
145
+ }
146
+ const check = Object.hasOwn(WRITABLE, key) ? WRITABLE[key] : null
147
+ if (!check) throw Object.assign(new Error(`不可识别或只读的配置项:${key}`), { code: 'INVALID_CONFIG' })
148
+ if (!check(entry)) throw Object.assign(new Error(`配置项 ${key} 的值不合法`), { code: 'INVALID_CONFIG' })
149
+ if (key === 'cloudBaseUrl' || key === 'downloadSiteBaseUrl') result[key] = entry.trim()
150
+ else if (key === 'downloadOrder') result[key] = [...entry]
151
+ else result[key] = entry
152
+ }
153
+ return result
154
+ }
155
+
156
+ /**
157
+ * 创建共享状态单例(每插件实例一个,ui.js 与 index.js 共用)。
158
+ * @param {{ patchConfig?: object, configPath: string, releasesDir: string }} options
159
+ */
160
+ export function createSharedState({ patchConfig, configPath, releasesDir }) {
161
+ const base = baseLayer(patchConfig)
162
+ let user = {}
163
+ let writeChain = Promise.resolve()
164
+ const listeners = new Set()
165
+ const runtimeListeners = new Set()
166
+
167
+ const commitUser = (next) => {
168
+ const previous = JSON.stringify(user)
169
+ user = next
170
+ if (JSON.stringify(next) === previous) return
171
+ for (const listener of listeners) listener(state.effective())
172
+ }
173
+
174
+ const state = {
175
+ configPath,
176
+ releasesDir,
177
+ /** @type {import('./store.js').CorpusStore | null} */
178
+ store: null,
179
+ /** 下载任务状态(供设置页轮询) */
180
+ download: {
181
+ active: false, phase: 'idle', source: null, releaseId: null,
182
+ filesDone: 0, filesTotal: null, bytesDone: 0, error: null, finishedAt: null,
183
+ },
184
+ /** 载入用户层配置文件;缺失视为空,存在但无效则失败。 */
185
+ async loadConfig() {
186
+ try {
187
+ const parsed = JSON.parse(await readFile(configPath, 'utf8'))
188
+ commitUser(validateUserLayer(parsed, { persisted: true }))
189
+ } catch (error) {
190
+ if (error?.code !== 'ENOENT') throw error
191
+ commitUser({})
192
+ }
193
+ return state.effective()
194
+ },
195
+ /** 三层合并后的生效配置 */
196
+ effective() {
197
+ const { cloudTokenOrigin: userTokenOrigin, ...publicUser } = user
198
+ const effective = { ...CONFIG_DEFAULTS, ...base, ...publicUser }
199
+ // 用户令牌必须有保存时记录的源;不能在启动时按可能已改动的 base
200
+ // 猜测旧令牌所属源。旧版无 origin 的值仍保留,重新保存令牌后恢复。
201
+ // base 层令牌与同一份声明中的 baseUrl 绑定,避免用户 URL 覆盖带走它。
202
+ const tokenOrigin = Object.hasOwn(user, 'cloudToken')
203
+ ? userTokenOrigin : base.cloudToken ? serviceOrigin(base.cloudBaseUrl) : null
204
+ if (effective.cloudToken && tokenOrigin !== serviceOrigin(effective.cloudBaseUrl)) {
205
+ effective.cloudToken = ''
206
+ }
207
+ // 旧配置只有 cloudGame。首次打开新版设置页之前仍沿用旧选择,避免
208
+ // 升级后悄悄把单游戏范围放宽为双游戏。
209
+ const explicitlyEnabled = user.enabledGames ?? base.enabledGames
210
+ const enabledGames = explicitlyEnabled
211
+ ? [...explicitlyEnabled]
212
+ : effective.cloudGame === 'all'
213
+ ? ['arknights', 'endfield'] : [effective.cloudGame]
214
+ return { ...effective, enabledGames, downloadOrder: [...effective.downloadOrder] }
215
+ },
216
+ /** 当前用户层(配置界面的编辑起点) */
217
+ userLayer() {
218
+ const { cloudTokenOrigin: _tokenOrigin, ...publicUser } = user
219
+ return { ...publicUser,
220
+ ...(user.enabledGames ? { enabledGames: [...user.enabledGames] } : {}),
221
+ ...(user.downloadOrder ? { downloadOrder: [...user.downloadOrder] } : {}) }
222
+ },
223
+ /** 订阅已验证配置的变化。 */
224
+ subscribe(listener) {
225
+ listeners.add(listener)
226
+ return () => { listeners.delete(listener) }
227
+ },
228
+ /** 订阅下载、激活和索引准备等非配置状态变化。 */
229
+ subscribeRuntime(listener) {
230
+ runtimeListeners.add(listener)
231
+ return () => { runtimeListeners.delete(listener) }
232
+ },
233
+ /** 通知只读取运行时状态的宿主能力重新求值。 */
234
+ notifyRuntime() {
235
+ for (const listener of runtimeListeners) listener()
236
+ },
237
+ /**
238
+ * 监听其他插件实例或人工编辑写入的配置文件。运行期无效内容保留最后一个
239
+ * 可用快照并记录警告,不让一次手误卸载已经工作的工具。
240
+ */
241
+ async watchConfig(logger) {
242
+ await mkdir(dirname(configPath), { recursive: true, mode: 0o700 })
243
+ let timer = null
244
+ let closed = false
245
+ const reload = () => {
246
+ if (closed) return
247
+ if (timer) clearTimeout(timer)
248
+ timer = setTimeout(() => {
249
+ timer = null
250
+ state.loadConfig().catch((error) => {
251
+ logger?.warn?.(`prts-corpus: 忽略无效配置更新:${error?.message ?? error}`)
252
+ })
253
+ }, 100)
254
+ }
255
+ const watcher = watchFileChanges(configPath, reload, {
256
+ onFallback(error) {
257
+ logger?.warn?.(`prts-corpus: 原生配置监听资源不足(${error.code}),改为每秒检查配置`)
258
+ },
259
+ onError(error) {
260
+ logger?.warn?.(`prts-corpus: 配置监听停止:${error?.message ?? error}`)
261
+ },
262
+ })
263
+ return () => {
264
+ closed = true
265
+ if (timer) clearTimeout(timer)
266
+ watcher.close()
267
+ }
268
+ },
269
+ /**
270
+ * 写入用户层补丁并持久化(原子写)。非法键/类型拒绝,不落盘。
271
+ * @returns {Promise<object>} 新的生效配置
272
+ */
273
+ async saveConfig(patch, { signal } = {}) {
274
+ assertConfigWriteActive(signal)
275
+ if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
276
+ throw Object.assign(new Error('配置补丁必须是对象'), { code: 'INVALID_CONFIG' })
277
+ }
278
+ validateUserLayer(patch)
279
+ const operation = writeChain.then(async () => {
280
+ // 被取消的排队写不能在较新的选择之后苏醒并覆盖配置。
281
+ assertConfigWriteActive(signal)
282
+ const previousOrigin = serviceOrigin(state.effective().cloudBaseUrl)
283
+ const next = validateUserLayer({ ...user, ...patch }, { persisted: true })
284
+ const nextOrigin = serviceOrigin({ ...CONFIG_DEFAULTS, ...base, ...next }.cloudBaseUrl)
285
+ // 静态令牌只属于签发它的源。设置页把云端服务切到另一个 origin 时,
286
+ // 未同时提交的新令牌不得被带到新服务;空字符串也能遮住 patch 层令牌。
287
+ if (nextOrigin !== previousOrigin && !Object.hasOwn(patch, 'cloudToken')) {
288
+ next.cloudToken = ''
289
+ delete next.cloudTokenOrigin
290
+ }
291
+ if (Object.hasOwn(patch, 'cloudToken')) {
292
+ if (next.cloudToken) next.cloudTokenOrigin = nextOrigin
293
+ else delete next.cloudTokenOrigin
294
+ }
295
+ await atomicWriteJson(configPath, next, { signal })
296
+ commitUser(next)
297
+ return state.effective()
298
+ })
299
+ writeChain = operation.catch(() => {})
300
+ return operation
301
+ },
302
+ }
303
+ return state
304
+ }
305
+
306
+ /** 配置快照(token 只暴露"是否已设置",不回传明文)。 */
307
+ export function redactConfig(effective, userLayer = {}) {
308
+ return {
309
+ ...effective,
310
+ cloudToken: undefined,
311
+ hasCloudToken: Boolean(effective.cloudToken),
312
+ cloudTokenUserSet: Boolean(userLayer.cloudToken),
313
+ }
314
+ }