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
@@ -0,0 +1,386 @@
1
+ /**
2
+ * dsh 版 timeline_search:活动时间线检索(PRTS Wiki《泰拉年表》的本地投影)。
3
+ *
4
+ * 数据来自资料包 references/activity_timelines.jsonl(每行一个活动:
5
+ * {activity_id, activity_name, events:[{event_id, time, event, sources}], source})。
6
+ * entity_names 先经别名图鉴(entities 包 + references/char_alias.txt)裂变再匹配;
7
+ * source_marker(年表出处:tle_xxx)可反查单条事件的完整来源。
8
+ */
9
+ import { randomBytes } from 'node:crypto'
10
+ import { loadEntityRelationCatalog, relationEndfieldNames } from './entity-routing.js'
11
+
12
+ export const TIMELINE_CONTRACT_VERSION = 'prts-corpus-tools-v1'
13
+ const MARKER_PATTERN = /^年表出处:(tle_[0-9a-f]{24})$/
14
+
15
+ function normalizeText(value) {
16
+ return String(value ?? '').normalize('NFKC').replace(/\s+/gu, ' ').trim()
17
+ }
18
+
19
+ function chineseInteger(value) {
20
+ const digits = '零一二三四五六七八九'
21
+ if (!Number.isInteger(value) || value < 0 || value > 99) return String(value)
22
+ if (value < 10) return digits[value]
23
+ const tens = Math.floor(value / 10)
24
+ const ones = value % 10
25
+ return `${tens === 1 ? '' : digits[tens]}十${ones ? digits[ones] : ''}`
26
+ }
27
+
28
+ /** 活动展示名之外,再接受主线章节号的常见自然写法。 */
29
+ export function activityAliases(value = {}) {
30
+ const aliases = new Set([normalizeText(value.activity_name)].filter(Boolean))
31
+ const source = [value.activity_id, value.collection_id].map((item) => String(item || '')).join('\n')
32
+ const match = /(?:^|event:|\b)main[_:](\d+)(?:\b|$)/u.exec(source)
33
+ if (match) {
34
+ const chapter = Number(match[1])
35
+ aliases.add(`第${chapter}章`)
36
+ aliases.add(`第${chineseInteger(chapter)}章`)
37
+ aliases.add(`${chapter}章`)
38
+ aliases.add(`主线${chapter}章`)
39
+ aliases.add(`主线第${chapter}章`)
40
+ }
41
+ return [...aliases]
42
+ }
43
+
44
+ export function activityMatches(value, requested, { exact = false } = {}) {
45
+ const needle = normalizeText(requested)
46
+ if (!needle) return false
47
+ return activityAliases(value).some((alias) => exact
48
+ ? alias === needle : alias.includes(needle) || needle.includes(alias))
49
+ }
50
+
51
+ function usefulEntityAliases(group) {
52
+ const aliases = [...new Set((group.aliases || []).map(normalizeText).filter(Boolean))]
53
+ const longer = aliases.filter((alias) => [...alias].length > 1)
54
+ return longer.length ? longer : aliases
55
+ }
56
+
57
+ function displayEventTime(time = {}) {
58
+ const label = String(time.text || time.section || '时间不详').trim()
59
+ if (/年|世纪|纪元/u.test(label)) return label
60
+ const start = Number.isInteger(time.year_start) ? time.year_start : null
61
+ const end = Number.isInteger(time.year_end) ? time.year_end : start
62
+ if (start === null || start < 0) return label
63
+ const year = end !== null && end !== start ? `${start}-${end} 年` : `${start} 年`
64
+ return label === '时间不详' ? year : `${year} ${label}`
65
+ }
66
+
67
+ function normalizedTimeBounds(time = {}) {
68
+ const label = normalizeText(time.text || time.section)
69
+ const century = /(\d+)\s*世纪(?:\s*(\d+)\s*年代)?/u.exec(label)
70
+ if (century) {
71
+ const base = (Number(century[1]) - 1) * 100
72
+ const start = base + (century[2] ? Number(century[2]) : 1)
73
+ return { start, end: century[2] ? start + 9 : base + 100 }
74
+ }
75
+ let start = Number.isInteger(time.year_start) ? time.year_start : null
76
+ if (start === null) {
77
+ const explicit = /(?:^|\D)(\d{1,4})\s*年/u.exec(label)
78
+ if (explicit) start = Number(explicit[1])
79
+ }
80
+ return { start, end: Number.isInteger(time.year_end) ? time.year_end : start }
81
+ }
82
+
83
+ function numericTimeOrder(time = {}, sequence = 0) {
84
+ const label = normalizeText(time.text || time.section)
85
+ const bounds = normalizedTimeBounds(time)
86
+ if (bounds.start === null) return [Number.POSITIVE_INFINITY, 13, 32, 24 * 60, sequence]
87
+ const monthMatch = /(\d{1,2})\s*月/u.exec(label)
88
+ const seasonMonth = /春/u.test(label) ? 3 : /夏/u.test(label) ? 6
89
+ : /秋/u.test(label) ? 9 : /冬/u.test(label) ? 12 : 0
90
+ const month = monthMatch ? Number(monthMatch[1]) : seasonMonth
91
+ const dayMatch = /(\d{1,2})\s*日/u.exec(label)
92
+ const day = dayMatch ? Number(dayMatch[1]) : 0
93
+ const clock = /(\d{1,2})\s*[::]\s*(\d{1,2})/u.exec(label)
94
+ let minutes = clock ? Number(clock[1]) * 60 + Number(clock[2]) : 0
95
+ if (!clock) {
96
+ if (/深夜/u.test(label)) minutes = 23 * 60
97
+ else if (/晚上/u.test(label)) minutes = 19 * 60
98
+ else if (/下午/u.test(label)) minutes = 14 * 60
99
+ else if (/中午/u.test(label)) minutes = 12 * 60
100
+ else if (/上午/u.test(label)) minutes = 9 * 60
101
+ else if (/早上/u.test(label)) minutes = 7 * 60
102
+ else if (/清晨/u.test(label)) minutes = 5 * 60
103
+ else if (/凌晨/u.test(label)) minutes = 1 * 60
104
+ }
105
+ return [bounds.start, month, day, minutes, sequence]
106
+ }
107
+
108
+ function compareTimeOrder(left, right) {
109
+ for (let index = 0; index < left.order.length; index += 1) {
110
+ if (left.order[index] !== right.order[index]) return left.order[index] - right.order[index]
111
+ }
112
+ return 0
113
+ }
114
+
115
+ function mergeTimelineMatches(candidates) {
116
+ const merged = []
117
+ const byKey = new Map()
118
+ for (const candidate of [...candidates].sort(compareTimeOrder)) {
119
+ const projected = timelineEvent(candidate.row, candidate.event)
120
+ const fallbackKey = `${normalizeText(projected.time)}\0${normalizeText(projected.event)}`
121
+ const key = String(candidate.event.event_id || '') || fallbackKey
122
+ const existing = byKey.get(key)
123
+ if (existing) {
124
+ if (projected.activity_name && !existing.activity_names.includes(projected.activity_name)) {
125
+ existing.activity_names.push(projected.activity_name)
126
+ }
127
+ continue
128
+ }
129
+ projected.activity_names = projected.activity_name ? [projected.activity_name] : []
130
+ byKey.set(key, projected)
131
+ merged.push(projected)
132
+ }
133
+ return merged
134
+ }
135
+
136
+ class TimelineFault extends Error {
137
+ constructor(code, message, retryable = false) {
138
+ super(message)
139
+ this.code = code
140
+ this.retryable = retryable
141
+ }
142
+ }
143
+
144
+ /** ---- 实体别名图鉴(entities 包投影 + char_alias.txt 人工补充) ---- */
145
+
146
+ /**
147
+ * 构建全量别名组。组来源:
148
+ * 1. entities 包每条实体记录的 canonical_name + aliases(权威源);
149
+ * 2. references/char_alias.txt 的分号分隔行(挂到同名 canonical 组,找不到则自建)。
150
+ * @returns {Promise<Array<{canonical: string, aliases: string[], games: string[]}>>}
151
+ */
152
+ export async function buildAliasGroups(store) {
153
+ const groups = new Map()
154
+ const crossGameNames = relationEndfieldNames(await loadEntityRelationCatalog(store))
155
+ const remember = (canonicalValue, aliasValues = [], gameValue = '') => {
156
+ const canonical = String(canonicalValue ?? '').trim()
157
+ if (!canonical) return
158
+ const group = groups.get(canonical) || { canonical, aliases: new Set([canonical]), games: new Set() }
159
+ for (const value of aliasValues) {
160
+ const alias = String(value ?? '').trim()
161
+ // 旧 PRTS 数据曾把终末地再旅者名塞进泰拉人物 aliases 方便联搜。
162
+ // 关系现在由独立附属字段承载;跨游戏名不再改变实体身份。
163
+ if (alias && (alias === canonical || !crossGameNames.has(alias))) group.aliases.add(alias)
164
+ }
165
+ const game = String(gameValue || '').trim().toLocaleLowerCase()
166
+ if (game === 'arknights' || game === 'endfield') group.games.add(game)
167
+ groups.set(canonical, group)
168
+ }
169
+
170
+ for await (const record of store.iterateDocuments({
171
+ predicate: (document) => document.document_type === 'entity',
172
+ })) {
173
+ const entity = record.entity || {}
174
+ const identity = `${record.document.document_id || ''} ${record.document.source_ref_prefix || ''}`
175
+ .toLocaleLowerCase()
176
+ const game = record.document.game || (identity.includes('endfield:') ? 'endfield' : 'arknights')
177
+ remember(entity.canonical_name || record.document.display_title,
178
+ entity.aliases || record.aliases, game)
179
+ }
180
+
181
+ const aliasReference = await store.getDocumentByPath('char_alias.txt')
182
+ for (const line of aliasReference?.record?.lines || []) {
183
+ const aliases = [...new Set(String(line.text || '').split(';').map((item) => item.trim()).filter(Boolean))]
184
+ if (!aliases.length) continue
185
+ const existing = [...groups.values()].find((group) =>
186
+ group.canonical === aliases[0] || group.aliases.has(aliases[0]))
187
+ remember(existing?.canonical || aliases[0], aliases)
188
+ }
189
+
190
+ return [...groups.values()].map((group) => ({ canonical: group.canonical,
191
+ aliases: [...group.aliases], games: [...group.games] }))
192
+ }
193
+
194
+ /**
195
+ * 把请求中的实体名展开为别名组;未知名字原样返回单别名组。
196
+ * @param {import('./store.js').CorpusStore} store
197
+ * @param {string[]} entityNames
198
+ */
199
+ export async function aliasesFor(store, entityNames) {
200
+ if (!entityNames?.length) return []
201
+ if (!store._aliasGroups) store._aliasGroups = await buildAliasGroups(store)
202
+ return entityNames.map((name) => {
203
+ const group = store._aliasGroups.find((item) => item.aliases.includes(name))
204
+ return group ? { canonical: group.canonical, aliases: [...group.aliases], games: [...group.games] }
205
+ : { canonical: name, aliases: [name], games: [] }
206
+ })
207
+ }
208
+
209
+ /** ---- 时间线数据 ---- */
210
+
211
+ /** 读取并解析 activity_timelines.jsonl(每行一个活动,含 events 数组)。 */
212
+ export async function timelineRows(store) {
213
+ if (store._timelineRows) return store._timelineRows
214
+ const found = await store.getDocumentByPath('activity_timelines.jsonl')
215
+ store._timelineRows = (found?.record?.lines || []).flatMap((line) => {
216
+ try {
217
+ const row = JSON.parse(String(line.text || ''))
218
+ return row && Array.isArray(row.events) ? [row] : []
219
+ } catch {
220
+ return []
221
+ }
222
+ })
223
+ return store._timelineRows
224
+ }
225
+
226
+ function timelineEvent(row, event) {
227
+ return {
228
+ activity_name: row.activity_name ?? '',
229
+ time: displayEventTime(event.time),
230
+ ...(Number.isInteger(event.time?.year_start) ? { year_start: event.time.year_start } : {}),
231
+ ...(Number.isInteger(event.time?.year_end) ? { year_end: event.time.year_end } : {}),
232
+ event: event.event ?? '',
233
+ source_marker: `年表出处:${event.event_id}`,
234
+ }
235
+ }
236
+
237
+ /** ---- 工具执行 ---- */
238
+
239
+ function normalizedTimelineRequest(raw = {}) {
240
+ const query = normalizeText(raw.query)
241
+ if (query.length > 200) throw new TimelineFault('INVALID_REQUEST', 'query 最长 200 字符')
242
+ // schema 之外的第三方调用可能传字符串等:按 INVALID_REQUEST 拒绝,
243
+ // 而不是让 .map 抛 TypeError 被归入 INTERNAL_ERROR。
244
+ const nameFilter = (value, field) => {
245
+ if (value === undefined || value === null) return []
246
+ if (!Array.isArray(value)) {
247
+ throw new TimelineFault('INVALID_REQUEST', `${field} 必须是字符串数组`)
248
+ }
249
+ return value.map(normalizeText).filter(Boolean)
250
+ }
251
+ const activities = nameFilter(raw.activity_names, 'activity_names')
252
+ if (activities.length > 20) throw new TimelineFault('INVALID_REQUEST', 'activity_names 最多 20 项')
253
+ const entities = nameFilter(raw.entity_names, 'entity_names')
254
+ if (entities.length > 20) throw new TimelineFault('INVALID_REQUEST', 'entity_names 最多 20 项')
255
+ const marker = String(raw.source_marker || '').trim()
256
+ if (marker && !MARKER_PATTERN.test(marker)) {
257
+ throw new TimelineFault('INVALID_REQUEST', '时间线出处标记格式无效(应为 年表出处:tle_<24位十六进制>)')
258
+ }
259
+ const yearStart = raw.year_start == null ? null : Number(raw.year_start)
260
+ const yearEnd = raw.year_end == null ? null : Number(raw.year_end)
261
+ if (yearStart !== null && !Number.isInteger(yearStart)) throw new TimelineFault('INVALID_REQUEST', 'year_start 必须是整数')
262
+ if (yearEnd !== null && !Number.isInteger(yearEnd)) throw new TimelineFault('INVALID_REQUEST', 'year_end 必须是整数')
263
+ if (!query && !activities.length && !entities.length && yearStart === null && yearEnd === null && !marker) {
264
+ throw new TimelineFault('INVALID_REQUEST', '请提供 query / activity_names / entity_names / 年份范围 / source_marker 之一')
265
+ }
266
+ const limit = raw.max_results === undefined ? 20 : Number(raw.max_results)
267
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
268
+ throw new TimelineFault('INVALID_REQUEST', 'max_results 必须在 1..100')
269
+ }
270
+ return { query, activities, entities, marker, yearStart, yearEnd, limit }
271
+ }
272
+
273
+ /**
274
+ * 执行 timeline_search。
275
+ * @param {import('./store.js').CorpusStore} store
276
+ * @param {object} raw 模型原始参数
277
+ * @param {{ signal?: AbortSignal }} [options]
278
+ * @returns {Promise<object>} 契约响应(ok / error)
279
+ */
280
+ export async function executeTimelineSearch(store, raw, { signal } = {}) {
281
+ const started = Date.now()
282
+ const requestId = `req-${randomBytes(8).toString('hex')}`
283
+ try {
284
+ await store.ready()
285
+ const request = normalizedTimelineRequest(raw)
286
+ const rows = await timelineRows(store)
287
+ if (!rows.length) {
288
+ throw new TimelineFault('TIMELINE_NOT_INSTALLED', '活动时间线资料尚未安装(资料包缺少 activity_timelines.jsonl)', true)
289
+ }
290
+ if (signal?.aborted) throw new TimelineFault('CANCELLED', '时间线检索已取消')
291
+
292
+ // 反查模式:source_marker → 单条事件完整来源
293
+ if (request.marker) {
294
+ const eventId = MARKER_PATTERN.exec(request.marker)[1]
295
+ for (const row of rows) {
296
+ const event = row.events.find((item) => item.event_id === eventId)
297
+ if (!event) continue
298
+ return {
299
+ contract_version: TIMELINE_CONTRACT_VERSION, status: 'ok', request_id: requestId,
300
+ data_version: store.dataVersion, mode: 'source',
301
+ event: timelineEvent(row, event), provenance: {
302
+ source_title: 'PRTS Wiki《泰拉年表》', source: row.source ?? null,
303
+ ...(event.source_location == null ? {} : { source_location: event.source_location }),
304
+ sources: event.sources || [],
305
+ },
306
+ }
307
+ }
308
+ throw new TimelineFault('TIMELINE_SOURCE_NOT_FOUND', '没有找到该时间线出处标记')
309
+ }
310
+
311
+ // 检索模式:活动 / 实体别名 / 年份 / 正文短语 四维交集
312
+ const aliasGroups = await aliasesFor(store, request.entities)
313
+ const entityAliases = [...new Set(aliasGroups.flatMap(usefulEntityAliases)
314
+ .map((item) => normalizeText(item).toLocaleLowerCase()))]
315
+ const inferredActivity = !request.activities.length && request.query
316
+ ? rows.find((row) => activityMatches(row, request.query, { exact: true })) : null
317
+ const requestedActivities = inferredActivity ? [inferredActivity.activity_name] : request.activities
318
+ const query = inferredActivity ? '' : request.query.toLocaleLowerCase()
319
+ const candidates = []
320
+ let sequence = 0
321
+ for (const row of rows) {
322
+ if (requestedActivities.length
323
+ && !requestedActivities.some((name) => activityMatches(row, name))) continue
324
+ for (const event of row.events) {
325
+ const { start: yearStart, end: yearEnd } = normalizedTimeBounds(event.time)
326
+ if (request.yearStart !== null && (yearEnd == null || yearEnd < request.yearStart)) continue
327
+ if (request.yearEnd !== null && (yearStart == null || yearStart > request.yearEnd)) continue
328
+ const haystack = normalizeText([event.event, event.time?.text,
329
+ ...(event.sources || []).map((item) => item.text)].join('\n')).toLocaleLowerCase()
330
+ if (query && !haystack.includes(query)) continue
331
+ if (entityAliases.length && !entityAliases.some((alias) => haystack.includes(alias))) continue
332
+ candidates.push({ row, event, order: numericTimeOrder(event.time, sequence) })
333
+ sequence += 1
334
+ }
335
+ }
336
+ const matches = mergeTimelineMatches(candidates)
337
+ return {
338
+ contract_version: TIMELINE_CONTRACT_VERSION, status: 'ok', request_id: requestId,
339
+ data_version: store.dataVersion, mode: 'search',
340
+ normalized_filters: { query: inferredActivity ? '' : request.query,
341
+ activity_names: requestedActivities,
342
+ ...(inferredActivity ? { inferred_activity_from_query: request.query } : {}),
343
+ entity_alias_groups: aliasGroups, year_start: request.yearStart, year_end: request.yearEnd },
344
+ events: matches.slice(0, request.limit),
345
+ page: { returned: Math.min(matches.length, request.limit), total: matches.length,
346
+ has_more: matches.length > request.limit },
347
+ stats: { elapsed_ms: Date.now() - started, scanned_activities: rows.length,
348
+ matched_events: matches.length, matched_occurrences: candidates.length },
349
+ guidance: matches.length
350
+ ? '时间线是可采纳的整理性证据;若与原文冲突则以原文为准。方括号内的出处标记可原样传给 source_marker 反查完整来源。'
351
+ : '没有命中;可放宽年份、换用实体展示名,或省略实体只按活动浏览。',
352
+ }
353
+ } catch (error) {
354
+ return {
355
+ contract_version: TIMELINE_CONTRACT_VERSION, status: 'error', request_id: requestId,
356
+ data_version: store.dataVersion ?? null,
357
+ error: { code: error.code || 'INTERNAL_ERROR', message: error.message || String(error), retryable: error.retryable ?? false },
358
+ }
359
+ }
360
+ }
361
+
362
+ /** 模型可见文本渲染(output.render 用)。 */
363
+ export function renderTimeline(_args, value) {
364
+ if (value.status !== 'ok') {
365
+ return [{ type: 'text', text: `[timeline_search:error] ${value.error.code}: ${value.error.message}` }]
366
+ }
367
+ const lines = [`[timeline_search:${value.mode}] data_version=${String(value.data_version ?? '').slice(0, 12)}…`]
368
+ if (value.mode === 'source') {
369
+ const event = value.event
370
+ lines.push(`${event.time}:${event.event}`)
371
+ lines.push(`activity: ${event.activity_name}`)
372
+ const provenance = value.provenance
373
+ lines.push(`source: ${provenance.source_title}${provenance.source ? ` (${provenance.source})` : ''}`)
374
+ for (const source of provenance.sources || []) {
375
+ lines.push(` - ${[source.text, source.story_name, source.activity_name].filter(Boolean).join(' | ')}`)
376
+ }
377
+ return [{ type: 'text', text: lines.join('\n') }]
378
+ }
379
+ for (const [index, event] of value.events.entries()) {
380
+ const activities = event.activity_names?.length ? event.activity_names : [event.activity_name]
381
+ lines.push(`${index + 1}. [${activities.filter(Boolean).join('、')}] ${event.time}:${event.event} [${event.source_marker}]`)
382
+ }
383
+ lines.push(`page: returned=${value.page.returned} total=${value.page.total} has_more=${value.page.has_more}`)
384
+ if (value.guidance) lines.push(value.guidance)
385
+ return [{ type: 'text', text: lines.join('\n') }]
386
+ }