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/i18n.js ADDED
@@ -0,0 +1,365 @@
1
+ /** Official localizations: exact source identities, independent of reading coverage. */
2
+ import { createHash } from 'node:crypto'
3
+ import { assertCorpusVersion, corpusVersionSnapshot, documentGame, documentUid,
4
+ naturalDocumentTitle } from './store.js'
5
+ import { LOCALIZATION_LANGUAGES } from './localization-format.js'
6
+
7
+ const PACK = 'endfield_official_game'
8
+ const states = new WeakMap()
9
+ const fields = ['text', 'name', 'title', 'actor', 'hint']
10
+ const sha256 = (value) => createHash('sha256').update(value).digest('hex')
11
+ const normalize = (value) => String(value)
12
+ .replace(/<image[^>]*>[\s\S]*?<\/image>|<image\s*=\s*[^>]*>/giu, '')
13
+ .replace(/<br\s*\/?>/giu, '\n')
14
+ .replace(/<@[^>]*>|<\/>|<\/?(?:color|b|i|size)(?:=[^>]*)?>/giu, '')
15
+ .normalize('NFKC').toLowerCase().replace(/\s+/gu, ' ').trim()
16
+ const aliases = { 'zh-cn': 'CN', 'zh-hans': 'CN', en: 'EN', ja: 'JP', ko: 'KR',
17
+ 'zh-tw': 'TC', 'zh-hant': 'TC', 'es-mx': 'MX', 'pt-br': 'BR', fr: 'FR', de: 'DE',
18
+ ru: 'RU', it: 'IT', id: 'ID', th: 'TH', vi: 'VN' }
19
+ for (const [locale, code] of Object.entries({ 'en-us': 'EN', 'ja-jp': 'JP', 'ko-kr': 'KR',
20
+ 'fr-fr': 'FR', 'de-de': 'DE', 'ru-ru': 'RU', 'it-it': 'IT', 'id-id': 'ID', 'th-th': 'TH', 'vi-vn': 'VN' })) {
21
+ aliases[locale] = code
22
+ }
23
+ const fault = (code, message) => Object.assign(new Error(message), { code,
24
+ retryable: code === 'PACKAGE_VERSION_MISMATCH' })
25
+
26
+ export const I18N_DESCRIPTION = [
27
+ '查询终末地官方本地化文本,返回其他语言原文;不生成翻译。',
28
+ '已有资料时优先使用完整 title 或 document_uid,可加 line;只有原句/名称时用 query 反查,source_language 默认 CN。',
29
+ 'languages 指定目标语言(EN 英语、JP 日语、KR 韩语、TC 繁中等)。query 默认 exact,片段用 match_mode=literal。',
30
+ '相同原文可能对应多个不同译文,按来源消歧。角色档案按整条记录、档案库按整篇内容块对齐,中文行号不是目标语言行号。',
31
+ '默认不显示内部文本 ID;仅核验时 include_ids=true,后续可用 text_ids 精确查询。续页原样提交 page.continuation。',
32
+ ].join(' ')
33
+
34
+ export const I18N_PARAMETERS = {
35
+ type: 'object', additionalProperties: false, required: ['languages'],
36
+ properties: {
37
+ game: { type: 'string', enum: ['endfield'], description: '当前支持终末地官方资料' },
38
+ query: { type: 'string', description: '名称或原句;仅有片段时使用 match_mode=literal' },
39
+ title: { type: 'string', description: '已有检索结果的完整篇章标题,与 query/document_uid/text_ids 四选一' },
40
+ document_uid: { type: 'string', description: '已有检索结果的文档定位,替代 title' },
41
+ line: { type: 'integer', description: '已有中文结果中的行号,只用于定位对应文本/记录' },
42
+ text_ids: { type: 'array', items: { type: 'string' }, description: '显式请求 ID 后可用于精确复查;普通查询不需要' },
43
+ source_language: { type: 'string', description: '原句的语言,默认 CN;也接受 en/ja/ko/zh-CN 等标准代码' },
44
+ languages: { type: 'array', items: { type: 'string' }, description: '1–4 种目标语言:CN/EN/JP/KR/TC/MX/BR/FR/DE/RU/IT/ID/TH/VN' },
45
+ match_mode: { type: 'string', enum: ['exact', 'literal'], description: '原句精确匹配或字面量片段搜索,默认 exact' },
46
+ include_ids: { type: 'boolean', description: '默认 false;仅调试、核验或精确复查时返回内部文本 ID 与来源字段' },
47
+ max_matches: { type: 'integer', description: '每页候选数,默认 5,最多 20' },
48
+ max_chars: { type: 'integer', description: '每页原文和译文的字符预算,默认 16000,范围 1000–48000' },
49
+ data_version: { type: 'string', description: '绑定已有结果的资料版本,避免混用版本' },
50
+ after: { type: 'object', additionalProperties: false,
51
+ required: ['data_version', 'request_hash', 'position', 'text_offset'],
52
+ properties: { data_version: { type: 'string' }, request_hash: { type: 'string' },
53
+ position: { type: 'integer' }, text_offset: { type: 'integer' } },
54
+ description: '仅原样提交工具返回的续页位置;长记录会明确分块返回' },
55
+ },
56
+ }
57
+
58
+ export const I18N_OUTPUT_SCHEMA = {
59
+ type: 'object', properties: {
60
+ status: { type: 'string', enum: ['ok', 'not_found', 'unmapped_source', 'unavailable'] },
61
+ game: { type: 'string' }, data_version: { type: 'string' }, game_version: { type: 'string' },
62
+ source_language: { type: 'string' }, languages: { type: 'array', items: { type: 'string' } },
63
+ matches: { type: 'array', items: { type: 'object', properties: {
64
+ source: { type: 'object' }, translations: { type: 'object' },
65
+ references: { type: 'array', items: { type: 'object' } }, reference_count: { type: 'integer' },
66
+ text_id: { type: 'string' }, source_fields: { type: 'array', items: { type: 'object' } },
67
+ } } },
68
+ page: { type: 'object' }, message: { type: 'string' }, note: { type: 'string' },
69
+ },
70
+ }
71
+
72
+ function language(value) {
73
+ if (typeof value !== 'string') throw fault('INVALID_REQUEST', '语言代码必须是字符串')
74
+ const result = aliases[value.toLowerCase()] || value.toUpperCase()
75
+ if (!LOCALIZATION_LANGUAGES.includes(result)) throw fault('INVALID_REQUEST', `不支持的语言:${value}`)
76
+ return result
77
+ }
78
+
79
+ function integer(value, minimum, maximum, name) {
80
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
81
+ throw fault('INVALID_REQUEST', `${name} 必须在 ${minimum}–${maximum} 之间`)
82
+ }
83
+ return value
84
+ }
85
+
86
+ export function normalizeI18nRequest(args) {
87
+ if (!args || typeof args !== 'object' || Array.isArray(args)
88
+ || Object.keys(args).some((key) => !Object.hasOwn(I18N_PARAMETERS.properties, key))) {
89
+ throw fault('INVALID_REQUEST', 'corpus_i18n 包含不支持的参数')
90
+ }
91
+ if (args.game !== undefined && args.game !== 'endfield') throw fault('INVALID_REQUEST', '当前仅支持终末地官方本地化')
92
+ const locators = ['query', 'title', 'document_uid', 'text_ids'].filter((key) => args[key] !== undefined)
93
+ if (locators.length !== 1) throw fault('INVALID_REQUEST', 'query/title/document_uid/text_ids 必须且只能提供一种')
94
+ const locator = locators[0]
95
+ const request = { game: 'endfield' }
96
+ if (locator === 'text_ids') {
97
+ if (!Array.isArray(args.text_ids) || !args.text_ids.length || args.text_ids.length > 20
98
+ || args.text_ids.some((id) => typeof id !== 'string' || !/^-?[0-9]{1,20}$/u.test(id))) {
99
+ throw fault('INVALID_REQUEST', 'text_ids 必须是 1–20 个十进制字符串 ID,不能使用数字')
100
+ }
101
+ request.text_ids = [...new Set(args.text_ids)]
102
+ } else {
103
+ if (typeof args[locator] !== 'string' || !args[locator].trim() || args[locator].length > 12000) {
104
+ throw fault('INVALID_REQUEST', `${locator} 必须是非空字符串,最多 12000 字符`)
105
+ }
106
+ request[locator] = args[locator].trim()
107
+ if (locator === 'query' && !normalize(request.query)) throw fault('INVALID_REQUEST', 'query 必须包含可查询的文本')
108
+ if (locator === 'document_uid' && !/^doc_[A-Za-z0-9_-]{16}$/u.test(request.document_uid)) {
109
+ throw fault('INVALID_REQUEST', 'document_uid 必须来自检索结果')
110
+ }
111
+ }
112
+ if (args.line !== undefined) {
113
+ if (!['title', 'document_uid'].includes(locator)) throw fault('INVALID_REQUEST', 'line 只能与文档定位一起使用')
114
+ request.line = integer(args.line, 1, 100_000_000, 'line')
115
+ }
116
+ if (!Array.isArray(args.languages) || args.languages.length < 1 || args.languages.length > 4) {
117
+ throw fault('INVALID_REQUEST', 'languages 必须包含 1–4 种目标语言')
118
+ }
119
+ request.source_language = language(args.source_language ?? 'CN')
120
+ request.languages = [...new Set(args.languages.map(language))]
121
+ request.match_mode = args.match_mode ?? 'exact'
122
+ if (!['exact', 'literal'].includes(request.match_mode)) throw fault('INVALID_REQUEST', 'match_mode 只支持 exact/literal')
123
+ if (args.include_ids !== undefined && typeof args.include_ids !== 'boolean') throw fault('INVALID_REQUEST', 'include_ids 必须是布尔值')
124
+ request.include_ids = args.include_ids ?? false
125
+ request.max_matches = integer(args.max_matches ?? 5, 1, 20, 'max_matches')
126
+ request.max_chars = integer(args.max_chars ?? 16000, 1000, 48000, 'max_chars')
127
+ if (args.data_version !== undefined) {
128
+ if (typeof args.data_version !== 'string' || !/^[0-9a-f]{64}$/u.test(args.data_version)) throw fault('INVALID_REQUEST', 'data_version 必须是有效的 SHA-256')
129
+ request.data_version = args.data_version
130
+ }
131
+ if (args.after !== undefined) {
132
+ const after = args.after
133
+ if (!after || typeof after !== 'object' || Array.isArray(after)
134
+ || Object.keys(after).sort().join() !== 'data_version,position,request_hash,text_offset'
135
+ || !/^[0-9a-f]{64}$/u.test(after.data_version) || !/^[0-9a-f]{64}$/u.test(after.request_hash)) {
136
+ throw fault('INVALID_REQUEST', 'after 必须原样使用工具返回的续页位置')
137
+ }
138
+ request.after = { ...after,
139
+ position: integer(after.position, 0, 1_000_000, 'after.position'),
140
+ text_offset: integer(after.text_offset, 0, 10_000_000, 'after.text_offset') }
141
+ }
142
+ return request
143
+ }
144
+
145
+ function checkpoint(store, snapshot, signal) {
146
+ assertCorpusVersion(store, snapshot)
147
+ if (signal?.aborted) throw fault('CANCELLED', '本地化查询已取消')
148
+ }
149
+
150
+ function stateFor(store) {
151
+ let state = states.get(store)
152
+ if (!state || state.generation !== store._generation || state.version !== store.dataVersion) {
153
+ state = { generation: store._generation, version: store.dataVersion,
154
+ catalog: null, languages: new Map(), pending: new Map() }
155
+ states.set(store, state)
156
+ }
157
+ return state
158
+ }
159
+
160
+ async function loadRows(store, snapshot, asset) {
161
+ try {
162
+ const bytes = await store._readPacked(PACK, asset.path, store.releaseId, asset)
163
+ assertCorpusVersion(store, snapshot)
164
+ return bytes.toString('utf8').split('\n').filter(Boolean).map((line) => JSON.parse(line))
165
+ } catch (error) {
166
+ assertCorpusVersion(store, snapshot)
167
+ throw fault('INDEX_CORRUPT', `本地化附件读取失败:${error.message}`)
168
+ }
169
+ }
170
+
171
+ async function catalogFor(store, state, meta, snapshot) {
172
+ if (!state.catalog) {
173
+ state.catalog = (async () => {
174
+ const rows = await loadRows(store, snapshot, meta.catalog)
175
+ const byId = new Map(), byDocument = new Map()
176
+ for (const entry of rows) {
177
+ if (typeof entry.text_id !== 'string' || !/^-?[0-9]{1,20}$/u.test(entry.text_id)
178
+ || byId.has(entry.text_id) || !Array.isArray(entry.references) || !entry.references.length
179
+ || !Array.isArray(entry.sources)) throw fault('INDEX_CORRUPT', '本地化目录记录无效')
180
+ byId.set(entry.text_id, entry)
181
+ for (const ref of entry.references) {
182
+ const doc = store.documents.get(ref.document_id)?.document
183
+ if (!doc || documentGame(doc) !== 'endfield'
184
+ || store.documents.get(ref.document_id)?.packId !== PACK
185
+ || !Number.isSafeInteger(ref.line_start) || !Number.isSafeInteger(ref.line_end)
186
+ || ref.line_start < 1 || ref.line_end < ref.line_start || ref.line_end > doc.line_count
187
+ || (ref.source_order !== undefined && (!Number.isSafeInteger(ref.source_order) || ref.source_order < 1))
188
+ || !fields.includes(ref.field) || !['text', 'record', 'document'].includes(ref.alignment)) {
189
+ throw fault('INDEX_CORRUPT', '本地化目录指向无效的官方来源')
190
+ }
191
+ const ids = byDocument.get(ref.document_id) || new Set()
192
+ ids.add(entry.text_id)
193
+ byDocument.set(ref.document_id, ids)
194
+ }
195
+ }
196
+ if (rows.length !== meta.text_count) throw fault('INDEX_CORRUPT', '本地化目录数量不一致')
197
+ return { rows, byId, byDocument }
198
+ })().catch((error) => { state.catalog = null; throw error })
199
+ }
200
+ return state.catalog
201
+ }
202
+
203
+ async function dictionaryFor(store, state, meta, catalog, lang, snapshot) {
204
+ if (!meta.languages[lang]) return null
205
+ if (state.languages.has(lang)) {
206
+ const cached = state.languages.get(lang)
207
+ state.languages.delete(lang); state.languages.set(lang, cached)
208
+ return cached
209
+ }
210
+ if (!state.pending.has(lang)) {
211
+ const promise = (async () => {
212
+ const rows = await loadRows(store, snapshot, meta.languages[lang])
213
+ const byId = new Map(), exact = new Map(), search = []
214
+ for (const row of rows) {
215
+ if (!Array.isArray(row) || row.length !== 2 || typeof row[0] !== 'string'
216
+ || typeof row[1] !== 'string' || !catalog.byId.has(row[0]) || byId.has(row[0])) {
217
+ throw fault('INDEX_CORRUPT', '本地化字典记录无效')
218
+ }
219
+ const [id, text] = row
220
+ byId.set(id, text)
221
+ const key = normalize(text)
222
+ if (key) {
223
+ const ids = exact.get(key) || []; ids.push(id); exact.set(key, ids)
224
+ search.push([id, key])
225
+ }
226
+ }
227
+ if (byId.size !== meta.text_count) throw fault('INDEX_CORRUPT', '本地化字典数量不一致')
228
+ assertCorpusVersion(store, snapshot)
229
+ const value = { byId, exact, search }
230
+ state.languages.set(lang, value)
231
+ while (state.languages.size > 4) state.languages.delete(state.languages.keys().next().value)
232
+ return value
233
+ })().finally(() => state.pending.delete(lang))
234
+ state.pending.set(lang, promise)
235
+ }
236
+ return state.pending.get(lang)
237
+ }
238
+
239
+ function publicReference(store, ref) {
240
+ const doc = store.documents.get(ref.document_id).document
241
+ const title = naturalDocumentTitle(doc)
242
+ return { title, document_uid: documentUid(doc.document_id), line_start: ref.line_start,
243
+ line_end: ref.line_end, field: ref.field, alignment: ref.alignment,
244
+ citation: `《${title}》中文定位第 ${ref.line_start}${ref.line_end === ref.line_start ? '' : `–${ref.line_end}`} 行` }
245
+ }
246
+
247
+ function chunk(text, offset, width, available) {
248
+ if (!available) return { status: 'unavailable' }
249
+ if (!text?.trim()) return { status: 'missing_localization' }
250
+ const chars = [...text]
251
+ const start = Math.min(offset, chars.length), end = Math.min(offset + width, chars.length)
252
+ return { status: 'available', text: chars.slice(start, end).join(''),
253
+ ...(offset || end < chars.length ? { character_start: start, character_end: end,
254
+ total_characters: chars.length, truncated: end < chars.length } : {}) }
255
+ }
256
+
257
+ export async function executeI18n(store, raw, { signal, enabledGames = ['endfield'] } = {}) {
258
+ const request = normalizeI18nRequest(raw)
259
+ if (!enabledGames.includes('endfield')) throw fault('INVALID_REQUEST', '当前未启用终末地资料')
260
+ await store.ready()
261
+ const snapshot = corpusVersionSnapshot(store)
262
+ checkpoint(store, snapshot, signal)
263
+ if ((request.data_version && request.data_version !== snapshot.dataVersion)
264
+ || (request.after && request.after.data_version !== snapshot.dataVersion)) {
265
+ throw fault('PACKAGE_VERSION_MISMATCH', '本地化查询所属资料版本已变化,请重新查询')
266
+ }
267
+ const { after, data_version: _version, ...identity } = request
268
+ const requestHash = sha256(JSON.stringify(identity))
269
+ if (after && after.request_hash !== requestHash) throw fault('INVALID_REQUEST', '续页参数与原查询不一致')
270
+ const base = { game: 'endfield', data_version: snapshot.dataVersion,
271
+ source_language: request.source_language, languages: request.languages }
272
+ const pack = store.packs.get(PACK), meta = pack?.localization
273
+ if (!meta) return { ...base, status: 'unavailable', matches: [],
274
+ message: '当前终末地官方资料包未包含多语言附件,请安装带本地化数据的版本。',
275
+ page: { has_more: false, continuation: null } }
276
+ base.game_version = meta.game_version
277
+ const state = stateFor(store)
278
+ const catalog = await catalogFor(store, state, meta, snapshot)
279
+ checkpoint(store, snapshot, signal)
280
+ const requestedLanguages = [...new Set([request.source_language, ...request.languages])]
281
+ const dictionaries = new Map(await Promise.all(requestedLanguages.map(async (lang) =>
282
+ [lang, await dictionaryFor(store, state, meta, catalog, lang, snapshot)])))
283
+ checkpoint(store, snapshot, signal)
284
+ const source = dictionaries.get(request.source_language)
285
+ if (!source) return { ...base, status: 'unavailable', matches: [],
286
+ message: '当前附件没有查询所用的来源语言。', page: { has_more: false, continuation: null } }
287
+ let ids = [], documentId = null
288
+ if (request.query) {
289
+ const query = normalize(request.query)
290
+ if (request.match_mode === 'exact') ids = source.exact.get(query) || []
291
+ else {
292
+ for (let i = 0; i < source.search.length; i += 1) {
293
+ if ((i & 2047) === 0) { await new Promise((resolve) => setImmediate(resolve)); checkpoint(store, snapshot, signal) }
294
+ const [id, text] = source.search[i]
295
+ if (text.includes(query)) ids.push(id)
296
+ }
297
+ }
298
+ } else if (request.text_ids) ids = request.text_ids.filter((id) => catalog.byId.has(id))
299
+ else {
300
+ const found = request.document_uid ? await store.getDocumentByUid(request.document_uid)
301
+ : await store.getDocumentByTitle(request.title)
302
+ checkpoint(store, snapshot, signal)
303
+ if (!found) throw fault('DOCUMENT_NOT_FOUND', '找不到指定资料,请使用检索结果中的完整定位')
304
+ if (found.packId !== PACK) throw fault('INVALID_REQUEST', '该资料不是终末地官方原文')
305
+ documentId = found.record.document.document_id
306
+ if (request.line && request.line > found.record.lines.length) throw fault('LINE_RANGE_INVALID', '中文定位行号超出文档范围')
307
+ ids = [...(catalog.byDocument.get(documentId) || [])].filter((id) => {
308
+ const text = source.byId.get(id) || ''
309
+ // An absent source translation still has a valid identity. Omit only
310
+ // image/markup-only blocks, retaining explicit missing_localization.
311
+ return (!text.trim() || normalize(text)) && catalog.byId.get(id).references.some((ref) => ref.document_id === documentId
312
+ && (!request.line || (ref.line_start <= request.line && ref.line_end >= request.line)))
313
+ })
314
+ const sourceOrder = (entry) => {
315
+ const ref = entry.references.find((item) => item.document_id === documentId
316
+ && (!request.line || (item.line_start <= request.line && item.line_end >= request.line)))
317
+ const fieldOrder = ['text', 'title', 'name', 'actor', 'hint'].indexOf(ref.field)
318
+ if (ref.alignment !== 'document') return [ref.line_start, fieldOrder, '']
319
+ // Archive Markdown does not have exact line alignment. Preserve the order
320
+ // of the original rich-content/radio blocks rather than sorting int64 IDs.
321
+ return [ref.source_order ?? 0, 0, '']
322
+ }
323
+ ids.sort((left, right) => {
324
+ const a = sourceOrder(catalog.byId.get(left)), b = sourceOrder(catalog.byId.get(right))
325
+ return a[0] - b[0] || a[1] - b[1] || a[2].localeCompare(b[2], 'en', { numeric: true })
326
+ })
327
+ }
328
+ // Preserve source/catalog order for document reads and deterministic query pages.
329
+ let position = after?.position ?? 0, offset = after?.text_offset ?? 0
330
+ if (position > ids.length || (position === ids.length && offset)) throw fault('INVALID_REQUEST', '续页位置超出查询结果')
331
+ const matches = []
332
+ let budget = request.max_chars
333
+ while (position < ids.length && matches.length < request.max_matches && budget >= 100) {
334
+ checkpoint(store, snapshot, signal)
335
+ const id = ids[position], entry = catalog.byId.get(id)
336
+ const width = Math.floor(budget / (request.languages.length + 1))
337
+ const sourceValue = chunk(source.byId.get(id), offset, width, true)
338
+ const translations = Object.fromEntries(request.languages.map((lang) => [lang,
339
+ chunk(dictionaries.get(lang)?.byId.get(id), offset, width, Boolean(dictionaries.get(lang)))]))
340
+ const values = [sourceValue, ...Object.values(translations)]
341
+ const maximumLength = Math.max(...requestedLanguages.map((lang) => [...(dictionaries.get(lang)?.byId.get(id) || '')].length))
342
+ if (offset && offset >= maximumLength) throw fault('INVALID_REQUEST', '记录续页偏移超出正文')
343
+ const refs = entry.references.filter((ref) => !documentId || ref.document_id === documentId)
344
+ matches.push({ source: { language: request.source_language, ...sourceValue }, translations,
345
+ references: refs.slice(0, 8).map((ref) => publicReference(store, ref)), reference_count: refs.length,
346
+ ...(request.include_ids ? { text_id: id, source_fields: entry.sources.slice(0, 8),
347
+ source_field_count: entry.sources.length } : {}) })
348
+ budget -= values.reduce((sum, value) => sum + [...(value.text || '')].length, 0)
349
+ if (values.some((value) => value.truncated)) { offset += width; break }
350
+ position += 1; offset = 0
351
+ }
352
+ checkpoint(store, snapshot, signal)
353
+ const hasMore = position < ids.length
354
+ return { ...base, status: ids.length ? 'ok' : documentId ? 'unmapped_source' : 'not_found',
355
+ matches, total_matches: ids.length,
356
+ ...(request.text_ids ? { missing_text_ids: request.text_ids.filter((id) => !catalog.byId.has(id)) } : {}),
357
+ note: '返回官方本地化原文;引用请注明语言。中文行号仅用于定位,record/document 对齐不表示跨语言逐行对应。',
358
+ page: { has_more: hasMore, continuation: hasMore ? { ...identity,
359
+ data_version: snapshot.dataVersion, after: { data_version: snapshot.dataVersion,
360
+ request_hash: requestHash, position, text_offset: offset } } : null } }
361
+ }
362
+
363
+ export function renderI18n(_args, value) {
364
+ return [{ type: 'text', text: JSON.stringify(value) }]
365
+ }