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,34 @@
1
+ # 双模块检索配方
2
+
3
+ 以下流程只在明日方舟和终末地同时启用时使用。
4
+
5
+ 问题已经明确属于一款游戏时,按对应模块的资料类型做单游戏检索,不必先联合搜索。已知短原句或专名用 `corpus_search`,情节、动机或多资料关系用 `cloud_search` 发现候选,精确结论再回读原文。
6
+
7
+ ## 比较两款游戏中的人物、组织或概念
8
+
9
+ 1. 把比较维度写清楚;可先联合调用默认 `cloud_search` 发现候选。
10
+ 2. 分别执行 `games:["arknights"]` 与 `games:["endfield"]` 的窄检索,使用各自模块的资料类型,不能用同一个类型数组硬套两边。
11
+ 3. 各自形成“该游戏资料明示的事实”,再做比较;共同点和差异属于综合结论,不冒充任一侧原文。
12
+
13
+ ## 再旅者与泰拉记忆原型
14
+
15
+ 1. 分别确认两个实体的规范名称和游戏归属。
16
+ 2. 对任一规范名做检索;结果带 `retraveler_relations` 时,可以将其中的对应关系表述为“PRTS 人工审校关系表登记”,无需强求游戏剧情重复证明该登记。
17
+ 3. 分别检索双方资料来回答各自身份、经历与表现;不要从登记关系外推同一人物、记忆继承方式或剧情确认程度。
18
+ 4. 没有 `retraveler_relations`,只有外观、同名或相似经历时,只能说明相似或尚未确认,不能自行建立关系或别名。
19
+
20
+ ## 不确定问题属于哪款游戏
21
+
22
+ 1. 用短专名执行联合 `corpus_search`,或用完整问题执行联合 `cloud_search`。
23
+ 2. 根据结果的 `game`、资料标题和实体路由确定归属。
24
+ 3. 归属明确后切换为单游戏范围继续,不让另一侧噪声进入后续核验。
25
+
26
+ ## 联合检索只返回一侧
27
+
28
+ 1. 检查是否出现 `CORPUS_GAME_NOT_INSTALLED`,以及过滤类型是否只属于其中一款游戏。
29
+ 2. 如果问题要求完整比较,分别使用两边合法类型再检索一次。
30
+ 3. 仍无一侧证据时明确报告缺口;不要用另一侧材料填补,也不要把零命中写成不存在。
31
+
32
+ ## 双侧时间或剧情对照
33
+
34
+ 明日方舟侧使用 `timeline_search` 或 `story`,终末地侧使用 `resource_types:["timeline"]` 或 `original_story`。先各自建立内部顺序,再比较主题或叙事结构;除非资料明确提供共同纪年关系,不把两套时间直接拼成同一绝对时间轴。
@@ -0,0 +1,37 @@
1
+ # 终末地检索配方
2
+
3
+ 以下流程只在终末地模块启用时使用,并始终带 `games:["endfield"]`。
4
+
5
+ ## 任务或剧情过程
6
+
7
+ 1. 已知任务名且要连续阅读时,直接用 `corpus_read({collection_name:任务名, mode:"collection"})`;同名歧义时用 `collection_names` 配合 `resource_types:["original_story"]` 搜索具体篇章,再以返回的 `document_uid + mode:"collection"` 选定合集。要定位某个片段时也先按该集合搜索;只知道大概情节时先用默认 `cloud_search`。
8
+ 2. 用 `content_types` 区分对话、过场、广播、远程通讯、环境对话或 SNS,避免不同表现形式的同词混在一起。
9
+ 3. 对行动、指代、态度和因果,用命中标题与行号读取连续上下文。
10
+
11
+ ## 台词、广播或通讯
12
+
13
+ 1. 确切短句:搜索 `original_story`,并在已知表现形式时增加对应 `content_types`。
14
+ 2. 查询某人亲口表达时使用 `speakers`;只要求出现某实体时使用 `entity_names`。
15
+ 3. 只记得一句话大意时使用 `single_sentence_search`,再回读原文。
16
+
17
+ ## 人物综合资料
18
+
19
+ 1. 用 `entity_profile` 或默认云端检索建立人物和关联资料入口。
20
+ 2. 身份、经历或机构记录优先查 `archive`;角色叙事查 `character_story`;实际剧情表现查 `original_story`。
21
+ 3. 若 `knowledge/wiki` 提供概括,只按其资料等级表述;具体行动和原话回到 `original_story`。
22
+
23
+ ## 设定、地点或组织
24
+
25
+ 1. 正式名称明确时先查 `archive/knowledge/entity_profile`。
26
+ 2. 涉及多个实体关系或事件过程时用默认 `cloud_search` 建立候选,再用本地正式资料确认定义。
27
+ 3. 若结论来自剧情中的展示而不是档案定义,补读 `original_story` 上下文并区分两者。
28
+
29
+ ## 时间与事件顺序
30
+
31
+ 1. 使用 `corpus_search({games:["endfield"], resource_types:["timeline"], query:事件或实体})`;可配合 `collection_names`。
32
+ 2. 时间线只支持整理性顺序;具体事件发生方式仍查 `original_story`。
33
+ 3. 不调用明日方舟专用的 `timeline_search` 来推断终末地没有相关事件。
34
+
35
+ ## 查找某类资料目录
36
+
37
+ 省略 `query`,用一个或少量 `resource_types` 加 `character_names/collection_names` 列出入口。正式标题命中优先打开对应档案,不被普通剧情中的同词提及带走。
@@ -0,0 +1,39 @@
1
+ # 明日方舟检索配方
2
+
3
+ 以下流程只在明日方舟模块启用时使用,并始终带 `games:["arknights"]`。
4
+
5
+ ## 角色与活动参与关系
6
+
7
+ 1. 角色→活动:省略 `query`,调用 `corpus_search({games:["arknights"], resource_types:["character_activity_wiki"], character_names:[角色]})`,从结果的 `activity_name` 整理活动。
8
+ 2. 活动→角色:省略 `query`,调用 `corpus_search({games:["arknights"], resource_types:["character_activity_wiki"], activity_names:[活动]})`,从结果的 `character_name` 整理角色。
9
+ 3. 角色×活动:省略 `query`,调用 `corpus_search({games:["arknights"], resource_types:["character_activity_wiki"], character_names:[角色], activity_names:[活动]})`;不同过滤字段取精确交集,可用于确认这对关系并读取对应辅助页。
10
+ 4. 完整清单或确认零命中时,保留原条件并原样提交 `page.next_after`,直到 `page.exhausted=true`,再按角色名或活动名去重。穷尽后仍无结果只表示当前资料版本没有该角色×活动整理记录;不得移除一个过滤条件后把任一侧的单独命中当作参与关系,也不能据此断言剧情中绝无参与。
11
+
12
+ ## 某角色在某活动中的作用
13
+
14
+ 1. 查 `story_wiki + activity_names + wiki_sections:["角色剧情概括"] + query:角色`。
15
+ 2. 需要更细过程时,查 `character_activity_wiki + character_names + activity_names + wiki_sections:["相关剧情总结"]`。
16
+ 3. 需要具体行动、说话人或因果时,以活动和角色为范围搜索 `story/operator_record`,再读取命中上下文。
17
+
18
+ ## 某活动的整体研究
19
+
20
+ 1. 默认 `cloud_search` 建立事件和人物骨架;若只需现成概括,直接读 `story_wiki` 的 `剧情总结`。
21
+ 2. 读 `关键人物` 建立人物集合,按需读取目标人物的 `角色剧情概括`。
22
+ 3. 用 `timeline_search(activity_names:[活动])` 补时间位置。
23
+ 4. 只对会影响结论的关键事件回到官方原文;用户明确要求完整顺读时才用 `corpus_read({activity_name:活动, mode:"activity"})`。
24
+
25
+ ## 人物关系
26
+
27
+ 1. 默认 `cloud_search` 搜索双方、关系和关键事件。
28
+ 2. 分别读取双方 `character_wiki` 的 `相关角色`,检查是否双向出现及叙述是否一致。
29
+ 3. 对关系起因、变化、冲突或态度,按云端映射或活动线索读取 `story/operator_record` 原文。
30
+
31
+ ## 台词与说话人
32
+
33
+ 1. 确切短句:`corpus_search({games:["arknights"], query:短句, resource_types:["story","operator_record"]})`。
34
+ 2. 查询某人亲口说的话时加 `speakers:[角色]`;查询谈及某人时才用 `entity_names`。
35
+ 3. 只记得大意时走 `single_sentence_search`,随后以返回标题和行号读取上下文。
36
+
37
+ ## 人物综合资料
38
+
39
+ 先用角色 Wiki 的 `简要介绍/详细介绍/相关活动` 建立骨架;身份和身体资料查 `character_profile`,补充经历查 `character_module/operator_record`,原话与称呼查 `character_voice/story`。只核验实际用于回答的结论。
@@ -0,0 +1,48 @@
1
+ # 当前工具契约
2
+
3
+ ## `corpus_search`
4
+
5
+ 它是本地结构化字面检索,不接收完整研究问题。可用参数为 `query`、`games`、`resource_types`、`content_types`、`collection_names`、`character_names`、`story_names`、`activity_names`、`wiki_sections`、`entity_names`、`speakers`、`match_mode`、`context_terms`、`after`。
6
+
7
+ 同一数组内 OR,不同过滤字段间 AND。`query` 默认连续字面匹配;只有确需模式时才用受限 `regex`。省略 `query` 可按归属列目录;省略 `query` 且指定一个 Wiki 字段可返回完整字段。已有证据足以回答时停止;只有需要完整清单、确认零命中或当前页证据不足时才继续分页。续页保留原条件并把 `page.next_after` 原样放进 `after`;要证明搜索范围已穷尽则继续到 `page.exhausted=true`。`next_after` 含完整 `data_version`;资料版本切换后旧锚点会被拒绝,须从首屏重新搜索。
8
+
9
+ ## `corpus_read`
10
+
11
+ - 明日方舟单篇关卡:`{stage_code:"TW-ST-1"}`;同代号存在多篇时加 `story_part:"before"|"after"|"story"`。提供 `line` 时读上下文,否则直接读全文首段。
12
+ - 干员密录:`{character_name:"安洁莉娜", record_name:"没写收件人的包裹", segment:2}`;只有一段时可省略 `segment`。
13
+ - 角色资料:`{character_name:"凯尔希", material:"profile"}`;`material` 支持 `profile/module/voice/skin/recruitment/potential`,双模块同名时加 `game`。
14
+ - 明日方舟活动连续阅读:`{activity_name:"孤星", mode:"activity"}`。
15
+ - 终末地任务连续阅读:`{collection_name:"武陵特厨", mode:"collection"}`;可加 `content_types`。
16
+ - 其他资料定点上下文:`{title, line, before?, after?}`。
17
+ - Wiki 字段:`{title, section}`。
18
+ - 其他资料全文首段:`{title, mode:"document", max_lines?, max_chars?}`。
19
+ - 搜索结果给出 `document_uid` 时表示标题同名:用它替代 `title`,两者不得同时提交;单篇用 `{document_uid, line}` 或 `{document_uid, mode:"document"}`。
20
+ - 所有续读:原样提交结果的 `page.continuation`。单篇使用 `line`,活动/任务合集使用版本绑定的 `position`;可以另加 `max_lines/max_chars`。
21
+
22
+ 每次只选一种主定位方式:`title`、`document_uid`、`stage_code`(可带 `story_part`)、角色密录组合、角色资料组合、`activity_name` 或 `collection_name`。`max_lines/max_chars` 只限制输出量,不会选择读取方式;裸 `title` 不能只配这两个限制字段。
23
+
24
+ 稳定定位字段已经足够时直接读取,不要先搜索标题。合集结果的每行都带所属篇章标题和篇内行号,应按它们引用。工具返回歧义时收窄条件,不能猜第一篇。新调用不要创建 cursor;旧会话已有 cursor 时才使用兼容入口。
25
+
26
+ ## `corpus_i18n`
27
+
28
+ 查询终末地官方本地化文本,不生成翻译。主定位四选一:`query`、`title`、`document_uid`、`text_ids`;必须指定 `languages`(1–4 种)。有原文定位时优先 `{title, line, languages:["EN","JP"]}` 或 `{document_uid, line, languages:["EN"]}`;仅有名称/原句时用 `{query, languages:["EN"]}`。`source_language` 默认 `CN`,外语反查可用 `EN/JP/KR` 或 `en/ja/ko`。`query` 默认精确匹配,短片段加 `match_mode:"literal"`。
29
+
30
+ 默认无需请求底层 ID。需要核验时才设 `include_ids:true`,返回的 `text_id` 必须当作字符串使用,后续可传 `text_ids`。同文不同译保留候选,按 references 中的篇章和中文定位消歧,不能任选第一个。每个结果最多列出 8 个引用;`reference_count` 表示总数。
31
+
32
+ `alignment:"record"` 是整条角色档案,`alignment:"document"` 是档案库内容块;不保证各语言段落一一对应。引用注明工具返回的篇章、语言和中文定位,不能把中文第 N 行说成英文第 N 行。长文本按字符分块,`character_start/end` 是该语言自己的位置,不表示译文逐字符对应;续页原样提交 `page.continuation`。
33
+
34
+ `missing_localization` 表示该语言官方文本为空;`unmapped_source` 表示没有建立来源映射;`unavailable` 表示附件/语言未安装。均不能自行翻译后当成官方文本。旧资料包仍能正常搜索和阅读,但需要安装带本地化附件的版本才能使用此工具。
35
+
36
+ ## `timeline_search`
37
+
38
+ 可用 `query`、`activity_names`、`entity_names`、`year_start`、`year_end`、`source_marker`、`max_results`。`entity_names` 会展开别名;不同维度取交集。`source_marker` 仅用于反查来源,不写入回答。
39
+
40
+ ## `cloud_search`
41
+
42
+ 模型可见参数只有 `query`、`games`、兼容字段 `depth`、`evidence_policy`,以及可选的 `options.search_intent="single_sentence_search"`。不要发送 channels、filters、阈值、候选预算、`quote_search` 或 `scene_search`。
43
+
44
+ 默认省略 `depth/options` 并使用 `evidence_policy="mixed"`。只有一句官方剧情原文的大意且措辞可能不准时使用 `single_sentence_search`;`original_only` 会切到仅官方剧情原文的向量路线。结果末尾的可读取标题与行号可直接交给 `corpus_read`。
45
+
46
+ ## `cloud_inspect`
47
+
48
+ 仅在云端结果截断、来源不清或诊断召回时使用。通常省略 `request_id`,运行时会关联最近一次云端请求;按返回的整数 `next_cursor` 分页。优先查看 `selected_sources` 或 `answer_context`,诊断时再看 `candidates/events/trace_steps`。
@@ -0,0 +1,38 @@
1
+ # Wiki 资料结构
2
+
3
+ 本地 `reviewed_wiki` 不是一种同质资料,而是三类经过整理的语义骨架。优先选择窄类型,不要一开始就搜索全部 Wiki。
4
+
5
+ ## 规范角色页:character_wiki
6
+
7
+ 一名角色通常对应一页,使用 `character_names` 精确定位。
8
+
9
+ - `简要介绍`:身份、阵营与核心经历的短概览,适合“这个人是谁”。
10
+ - `相关角色`:主要人物关系及关系性质,适合关系入口;复杂关系仍需双向页面、图谱或原文交叉检查。
11
+ - `详细介绍`:跨活动的完整经历与阶段变化,适合人物生平和动机分析。
12
+ - `剧情高光`:重要场面及整理出处,适合发现值得回查的剧情叶片。
13
+ - `战斗表现`:能力、战斗与战术表现;可组合官方档案、模组和原文。
14
+ - `相关活动`:角色参加的活动及其作用,是“参加过哪些活动”的首选字段。
15
+ - `trivia`:轶事和细节,属于整理性证据。
16
+ - `角色点评`:综合评价,主观性最高;不得当成原文明示事实。
17
+
18
+ ## 活动/密录页:story_wiki
19
+
20
+ 使用 `activity_names` 按展示名定位;密录也沿用其故事展示名。
21
+
22
+ - `剧情总结`:整部活动、主线章节或密录的结构、因果和结局。
23
+ - `剧情高光`:重要场面和代表性表达;精确台词必须回原文。
24
+ - `关键人物`:人物清单,适合建立后续查询集合。
25
+ - `角色剧情概括`:逐角色说明其在该活动中的行动和作用,适合“某人在某活动做了什么”。
26
+ - `trivia`:活动中的设定细节与轶事。
27
+
28
+ ## 角色×活动辅助页:character_activity_wiki
29
+
30
+ 它保存某一角色在某一活动中的 `相关剧情总结、相关剧情高光、相关trivia、相关角色总结`,可同时使用 `character_names` 和 `activity_names` 过滤。
31
+ 这是生成规范角色页时使用的辅助整理材料,适合补充过程或定位线索;证据地位低于规范角色页和活动页,更不能替代官方原文。
32
+
33
+ ## 范围与证据
34
+
35
+ - Wiki 标签字段是审核或整理资料,可以直接回答概括、清单和导航问题。
36
+ - `相关活动` 表示资料整理认定的剧情参与,不等于角色在每一篇原文中都亲自出场。
37
+ - `剧情总结` 和 `角色剧情概括` 可能覆盖完整活动,不能套用到单一场景。
38
+ - 用户要求逐字台词、说话人、场景细节或行号时,必须沿 Wiki 提供的活动/篇章线索回到 `story` / `operator_record` 原文。
@@ -0,0 +1,287 @@
1
+ /** Bounded, durable UI material records. Never include a raw remote response or host paths. */
2
+ import { createHash } from 'node:crypto'
3
+ import { documentUid } from './store.js'
4
+ import { projectCloudSearch } from './cloud-projection.js'
5
+
6
+ const MAX_SOURCES = 128
7
+ const MAX_EXCERPT = 1600
8
+ const text = (value, max = MAX_EXCERPT) => typeof value === 'string' ? value.slice(0, max) : ''
9
+ const version = (value) => /^[a-f0-9]{64}$/u.test(value || '') ? value : undefined
10
+ const idFor = (origin, dataVersion, identity) => `${origin}:${createHash('sha256')
11
+ .update(JSON.stringify([dataVersion || '', identity])).digest('hex').slice(0, 32)}`
12
+ const localId = (dataVersion, documentId) => `document:${dataVersion}:${documentUid(documentId)}`
13
+ const range = (start, end = start) => Number.isSafeInteger(Number(start)) && Number(start) > 0
14
+ && Number.isSafeInteger(Number(end)) && Number(end) >= Number(start)
15
+ ? { start: Number(start), end: Number(end) } : null
16
+ const rangesFor = (values) => [...new Map(values.filter(Boolean)
17
+ .map((value) => [`${value.start}:${value.end}`, value])).values()].slice(0, 128)
18
+ const locations = (ranges) => ranges.length ? {
19
+ lineStart: ranges[0].start, lineEnd: ranges[0].end, ranges,
20
+ } : {}
21
+
22
+ function envelope(sources, dataVersion) {
23
+ return { kind: 'prts-archive-sources-v1', sources: sources.slice(0, MAX_SOURCES),
24
+ ...(version(dataVersion) ? { data_version: dataVersion } : {}),
25
+ ...(sources.length > MAX_SOURCES ? { sources_truncated: true } : {}) }
26
+ }
27
+
28
+ /** Capture at execution time: later release switches cannot relabel old tool results. */
29
+ export function rememberArchivePresentation(value, metadata) {
30
+ // DSH snapshots the tool value before rendering; a WeakMap/non-enumerable
31
+ // attachment would disappear. Renderers deliberately ignore this UI field.
32
+ return { ...value, presentation: metadata }
33
+ }
34
+
35
+ export function archivePresentationMeta(_args, value) {
36
+ if (value?.status === 'error' || value?.error) return { ...envelope([]),
37
+ error: text(value?.error?.message || value?.error, 600) || '资料检索未完成' }
38
+ return value?.presentation?.kind === 'prts-archive-sources-v1'
39
+ ? value.presentation : envelope([])
40
+ }
41
+
42
+ const PTC_RECEIPT_TYPE = 'prts-archive-receipt'
43
+ const PTC_TOOLS = new Set(['corpus_search', 'corpus_read', 'timeline_search', 'cloud_search', 'cloud_inspect'])
44
+ const MAX_RECEIPT_BYTES = 512 * 1024
45
+ const MAX_PENDING_BYTES = 2 * 1024 * 1024
46
+ const MAX_PENDING_CALLS = 64
47
+ const PENDING_LIFETIME_MS = 5 * 60 * 1000
48
+ const receiptIdentity = (value, max = 1024) => typeof value === 'string' && value.length <= max ? value : ''
49
+
50
+ /** Keep the log extension as bounded as the native presentationMeta contract. */
51
+ function boundedReceiptMeta(value) {
52
+ if (!value || !['prts-archive-sources-v1', 'prts-corpus-read-v1'].includes(value.kind)) return null
53
+ const dataVersion = version(value.data_version)
54
+ if (value.kind === 'prts-corpus-read-v1') {
55
+ const documentId = receiptIdentity(value.locator?.document_id)
56
+ if (!documentId || !dataVersion) return null
57
+ const primaryRange = range(value.line_start, value.line_end)
58
+ return { kind: value.kind, locator: { document_id: documentId }, data_version: dataVersion,
59
+ title: text(value.title, 512),
60
+ ...(primaryRange ? { line_start: primaryRange.start, line_end: primaryRange.end } : {}),
61
+ sources: (Array.isArray(value.sources) ? value.sources : []).slice(0, MAX_SOURCES).flatMap((source) => {
62
+ const id = receiptIdentity(source?.document_id)
63
+ const selected = range(source?.line_start, source?.line_end)
64
+ if (!id || !selected) return []
65
+ return [{ document_id: id, document_uid: documentUid(id), title: text(source.title, 512),
66
+ line_start: selected.start, line_end: selected.end }]
67
+ }), ...(value.sources_truncated ? { sources_truncated: true } : {}) }
68
+ }
69
+ const sources = (Array.isArray(value.sources) ? value.sources : []).slice(0, MAX_SOURCES).flatMap((source) => {
70
+ const id = receiptIdentity(source?.id), title = text(source?.title, 512)
71
+ if (!id || !title) return []
72
+ const selected = rangesFor((Array.isArray(source.ranges) ? source.ranges : []).slice(0, MAX_SOURCES)
73
+ .map((item) => range(item?.start, item?.end)))
74
+ const first = range(source.lineStart, source.lineEnd)
75
+ const sourceVersion = version(source.dataVersion)
76
+ const url = safeUrl(source.url)
77
+ return [{ id, title, kind: text(source.kind, 80),
78
+ origin: ['local', 'cloud', 'web'].includes(source.origin) ? source.origin : 'local',
79
+ state: ['found', 'read', 'cited'].includes(source.state) ? source.state : 'found',
80
+ excerpt: text(source.excerpt), ...(source.agentRead === true ? { agentRead: true } : {}),
81
+ ...(receiptIdentity(source.documentId) ? { documentId: source.documentId } : {}),
82
+ ...(receiptIdentity(source.documentUid, 128) ? { documentUid: source.documentUid } : {}),
83
+ ...(receiptIdentity(source.sourceRef) ? { sourceRef: source.sourceRef } : {}),
84
+ ...(sourceVersion ? { dataVersion: sourceVersion } : {}), ...(url ? { url } : {}),
85
+ ...locations(selected.length ? selected : first ? [first] : []),
86
+ ...(Array.isArray(source.readRanges) ? { readRanges: rangesFor(source.readRanges.slice(0, MAX_SOURCES)
87
+ .map((item) => range(item?.start, item?.end))) } : {}) }]
88
+ })
89
+ return { ...envelope(sources, dataVersion),
90
+ ...(value.sources_truncated ? { sources_truncated: true } : {}),
91
+ ...(typeof value.error === 'string' && value.error ? { error: text(value.error, 600) } : {}) }
92
+ }
93
+
94
+ /**
95
+ * DSH 0.1.5 omits presentationMeta on nested run_code dispatches. Observe the
96
+ * final, frozen value synchronously, then attach a structured ContentBlock only
97
+ * to tools/ptc-dispatch-log. That official seam changes the human log copy;
98
+ * deriveMessages ignores these events, and neither render() nor the value
99
+ * delivered to the running program/model is changed. No identity is inferred
100
+ * from the rendered prose. ContentBlock is a merge-extensible host contract.
101
+ */
102
+ export function installArchivePtcReceipts(ctx, readMetadata) {
103
+ if (typeof ctx.on !== 'function') return false
104
+ let pending = new WeakMap()
105
+ let disposed = false
106
+ const drop = (state, key) => {
107
+ const item = state.items.get(key)
108
+ if (!item) return
109
+ state.bytes -= item.bytes
110
+ state.items.delete(key)
111
+ }
112
+ ctx.on('tools/result', (exec, result) => {
113
+ if (disposed || exec?.parent === undefined || !exec.agent || typeof exec.agent !== 'object'
114
+ || !PTC_TOOLS.has(exec.name)) return
115
+ const callId = String(exec.callId || '')
116
+ if (!callId || callId.length > 512) return
117
+ let state = pending.get(exec.agent)
118
+ if (state) drop(state, callId)
119
+ if (result?.isError || !result?.value) return
120
+ let meta, bytes
121
+ try {
122
+ const failed = result.value.error || result.value.status === 'error'
123
+ const metadata = exec.name === 'corpus_read' && !failed
124
+ ? readMetadata(exec.arguments, result.value)
125
+ : archivePresentationMeta(exec.arguments, result.value)
126
+ meta = boundedReceiptMeta(metadata)
127
+ if (!meta) return
128
+ bytes = Buffer.byteLength(JSON.stringify(meta), 'utf8')
129
+ while (bytes > MAX_RECEIPT_BYTES && meta.sources.length) {
130
+ meta.sources = meta.sources.slice(0, Math.floor(meta.sources.length / 2))
131
+ meta.sources_truncated = true
132
+ bytes = Buffer.byteLength(JSON.stringify(meta), 'utf8')
133
+ }
134
+ } catch {
135
+ // Optional UI receipts must never interfere with tool completion when
136
+ // a failed or replaced tool supplies no valid presentation projection.
137
+ return
138
+ }
139
+ if (bytes > MAX_RECEIPT_BYTES) return
140
+ if (!state) { state = { items: new Map(), bytes: 0 }; pending.set(exec.agent, state) }
141
+ const now = Date.now()
142
+ for (const [key, item] of state.items) if (now - item.time > PENDING_LIFETIME_MS) drop(state, key)
143
+ while (state.items.size >= MAX_PENDING_CALLS || state.bytes + bytes > MAX_PENDING_BYTES) {
144
+ const oldest = state.items.keys().next().value
145
+ if (oldest === undefined) break
146
+ drop(state, oldest)
147
+ }
148
+ state.items.set(callId, { parent: exec.parent, tool: exec.name, meta, bytes, time: now })
149
+ state.bytes += bytes
150
+ })
151
+ ctx.on('tools/ptc-dispatch-log', async (dispatch, next) => {
152
+ const content = await next()
153
+ if (disposed || !dispatch.agent || !PTC_TOOLS.has(dispatch.name)) return content
154
+ const state = pending.get(dispatch.agent)
155
+ const callId = String(dispatch.subCallId || '')
156
+ const item = state?.items.get(callId)
157
+ if (!item || item.parent !== dispatch.exec?.token || item.tool !== dispatch.name) return content
158
+ drop(state, callId)
159
+ if (dispatch.isError || Date.now() - item.time > PENDING_LIFETIME_MS) return content
160
+ return [...content, { type: PTC_RECEIPT_TYPE, version: 1, callId,
161
+ parentCallId: String(dispatch.exec.callId), tool: item.tool, meta: item.meta }]
162
+ })
163
+ ctx.effect?.(() => () => { disposed = true; pending = new WeakMap() }, 'prts-corpus: PTC archive receipts')
164
+ return true
165
+ }
166
+
167
+ function localDocumentId(store, item) {
168
+ if (item.document_uid) return store.getDocumentIdByUid?.(item.document_uid) || null
169
+ const naturalIds = store.naturalTitleIndex?.get(item.title) || []
170
+ const ids = naturalIds.length ? naturalIds : store.titleIndex?.get(item.title) || []
171
+ // Ambiguous titles must remain unlinked; choosing an arbitrary document is unsafe.
172
+ return ids.length === 1 ? ids[0] : null
173
+ }
174
+
175
+ export function localArchivePresentation(store, value, dataVersion = store.dataVersion) {
176
+ if (value?.error || value?.status === 'error') return envelope([], dataVersion)
177
+ const sources = (value?.documents || []).slice(0, MAX_SOURCES + 1).map((item) => {
178
+ const documentId = localDocumentId(store, item)
179
+ const ranges = rangesFor((item.matches || []).map((match) => range(match.line_start, match.line_end)))
180
+ const excerpt = (item.matches || []).flatMap((match) => match.excerpt || [])
181
+ .map((line) => `${line.speaker ? `${line.speaker}:` : ''}${text(line.text)}`).join('\n')
182
+ || (item.section_content?.blocks || []).map((block) => text(block.text)).join('\n')
183
+ || [item.entity_summary?.description, item.entity_summary?.history_summary].filter(Boolean).join('\n')
184
+ return { id: documentId ? localId(dataVersion, documentId)
185
+ : idFor('local', dataVersion, item.document_uid || [item.game, item.resource_type, item.title]),
186
+ title: text(item.title, 512), kind: text(item.resource_type, 80), origin: 'local', state: 'found',
187
+ excerpt: text(excerpt), ...(documentId ? { documentId, documentUid: documentUid(documentId) } : {}),
188
+ ...(version(dataVersion) ? { dataVersion } : {}), ...locations(ranges) }
189
+ })
190
+ return envelope(sources, dataVersion)
191
+ }
192
+
193
+ const PUBLIC_URL_PARAMETERS = new Set(['title', 'oldid', 'diff', 'id', 'page', 'p',
194
+ 'doc', 'document_id', 'lang', 'section'])
195
+
196
+ function safeUrl(value) {
197
+ try {
198
+ const url = new URL(value)
199
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) return undefined
200
+ for (const key of [...url.searchParams.keys()]) {
201
+ if (!PUBLIC_URL_PARAMETERS.has(key)) url.searchParams.delete(key)
202
+ }
203
+ return url.href.length <= 2048 ? url.href : undefined
204
+ } catch { return undefined }
205
+ }
206
+
207
+ /** Only returned anchors and model-visible content belong on the user's desk. */
208
+ export function cloudArchivePresentation(value, { inspect = false, dataVersion } = {}) {
209
+ if (value?.error || value?.status === 'error') return envelope([])
210
+ const delivered = inspect ? '' : projectCloudSearch(value)
211
+ const remoteVersion = version(value?.data?.data_version || value?.data_version)
212
+ const sources = []
213
+ const byId = new Map()
214
+ const mappedDocuments = new Set()
215
+ const add = (source) => {
216
+ const previous = byId.get(source.id)
217
+ if (previous) {
218
+ previous.ranges = rangesFor([...(previous.ranges || []), ...(source.ranges || [])])
219
+ if (!previous.excerpt) previous.excerpt = source.excerpt
220
+ if (source.state === 'read') { previous.state = 'read'; previous.agentRead = true }
221
+ return
222
+ }
223
+ if (sources.length >= MAX_SOURCES + 1) return
224
+ byId.set(source.id, source); sources.push(source)
225
+ }
226
+ // Both cloud renderers explicitly deliver every suggested local anchor. Do not
227
+ // forward mapping excerpts: they may come from candidates hidden by Cleaner.
228
+ for (const mapping of (value?.local_source_mappings || []).slice(0, 200)) {
229
+ if (!mapping?.suggested_source_ref || !mapping.document_id || !version(dataVersion)) continue
230
+ mappedDocuments.add(mapping.document_id)
231
+ const ranges = rangesFor([range(mapping.start_line, mapping.end_line || mapping.start_line)])
232
+ add({ id: localId(dataVersion, mapping.document_id),
233
+ title: text(mapping.title || mapping.display_title, 512), kind: text(mapping.document_kind || 'source', 80),
234
+ origin: 'cloud', state: 'found', excerpt: '', documentId: text(mapping.document_id, 1024),
235
+ documentUid: text(mapping.document_uid, 128) || documentUid(mapping.document_id),
236
+ dataVersion, ...locations(ranges) })
237
+ }
238
+ // The inspect renderer preserves the allowlisted textual fields below. Use
239
+ // each original item for its opaque identity as well, so filtered null rows
240
+ // cannot shift that identity onto a different projected document.
241
+ const items = inspect ? value?.data?.items || []
242
+ : value?.data?.sources || value?.data?.selected_sources || []
243
+ for (const item of items.slice(0, 200)) {
244
+ if (!item || typeof item !== 'object') continue
245
+ const title = text(item.title || item.story_title || item.display_title, 512)
246
+ if (!title || (!inspect && !delivered.includes(title))) continue
247
+ // Titles are display text, not identities; only an exact canonical document
248
+ // match can establish that the mapped anchor already represents this item.
249
+ const excerpt = inspect
250
+ ? [item.content_preview, item.content, item.text, item.summary].filter((entry) => typeof entry === 'string' && entry.trim()).join('\n\n')
251
+ : [item.content_preview, item.content, item.text, item.summary]
252
+ .find((entry) => typeof entry === 'string' && entry && delivered.includes(entry)) || ''
253
+ // A local anchor is a pointer to a future corpus_read, not evidence that the
254
+ // local document was read. Keep an explicitly delivered remote body separate
255
+ // even when an anchor maps it to that same document: line spaces may differ.
256
+ if (item.document_id && mappedDocuments.has(item.document_id) && (!inspect || !excerpt.trim())) continue
257
+ const url = safeUrl(item.url || item.source_url)
258
+ const ranges = rangesFor([range(item.start_line, item.end_line || item.start_line)])
259
+ const identity = item.document_id || item.source_id || item.doc_id || item.story_id
260
+ || item.source_file || url || [item.game, item.source_type, title]
261
+ add({ id: idFor('cloud', remoteVersion, identity),
262
+ title, kind: text(item.source_type || 'cloud_source', 80), origin: 'cloud',
263
+ state: inspect && excerpt.trim() ? 'read' : 'found',
264
+ ...(inspect && excerpt.trim() ? { agentRead: true } : {}),
265
+ excerpt: text(excerpt), ...(url ? { url } : {}), ...locations(ranges) })
266
+ }
267
+ // Cleaner can deliver a synthesis with no resolvable individual source. Keep
268
+ // exactly that delivered text accessible without inventing document locators.
269
+ const context = value?.data?.answer_context
270
+ if (!sources.length && typeof context === 'string' && context.trim()) {
271
+ const excerpt = inspect ? context : delivered
272
+ add({ id: idFor('cloud', remoteVersion, excerpt), title: '云端检索摘要', kind: 'cloud_summary',
273
+ origin: 'cloud', state: 'read', agentRead: true, excerpt: text(excerpt) })
274
+ }
275
+ return envelope(sources, dataVersion)
276
+ }
277
+
278
+ export function timelineArchivePresentation(value) {
279
+ if (value?.status !== 'ok') return envelope([])
280
+ const events = value.mode === 'source' ? [value.event] : value.events || []
281
+ return envelope(events.slice(0, MAX_SOURCES + 1).filter(Boolean).map((event) => ({
282
+ id: idFor('local', value.data_version, event.source_marker || [event.activity_name, event.time, event.event]),
283
+ title: text(`${event.activity_name || event.activity_names?.join('、') || '泰拉年表'} · ${event.time || '事件'}`, 512),
284
+ kind: 'timeline', origin: 'local', state: 'found', excerpt: text(event.event),
285
+ ...(version(value.data_version) ? { dataVersion: value.data_version } : {}),
286
+ })), value.data_version)
287
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * 云端工具的模型可见投影。语义与 agent/browser/src/context-manager.js 保持一致:
3
+ * cloud_search 只交付 Cleaner 的 bounded answer_context、可读本地锚点和警告;
4
+ * cloud_inspect 保留所请求的结构化区段,但移除内部不透明标识和空元数据。
5
+ */
6
+
7
+ export function withoutEmptyMetadata(value) {
8
+ if (Array.isArray(value)) return value.map(withoutEmptyMetadata).filter((item) => item !== undefined)
9
+ if (value && typeof value === 'object') {
10
+ const entries = Object.entries(value).flatMap(([key, child]) => {
11
+ const projected = withoutEmptyMetadata(child)
12
+ if (projected === undefined) return []
13
+ if (Array.isArray(projected) && !projected.length) return []
14
+ if (projected && typeof projected === 'object' && !Object.keys(projected).length) return []
15
+ return [[key, projected]]
16
+ })
17
+ return Object.fromEntries(entries)
18
+ }
19
+ return value === '' || value == null ? undefined : value
20
+ }
21
+
22
+ const MODEL_HIDDEN_IDENTIFIER_FIELDS = new Set([
23
+ 'call_id', 'request_id', 'intent_id', 'round_id', 'trace_ref',
24
+ 'evidence_id', 'candidate_id', 'document_id', 'source_ref',
25
+ 'source_ref_prefix', 'suggested_source_ref', 'read_ref', 'run_id',
26
+ ])
27
+
28
+ const ZERO_VALUE_SCORE_FIELDS = new Set([
29
+ 'content_score', 'summary_score', 'entity_boost_score', 'ranking_score',
30
+ ])
31
+ const DEFAULT_FALSE_FIELDS = new Set(['llm_validated', 'exact_match_privileged'])
32
+
33
+ /** 删除“未执行/没有信号”的默认审计字段;非零评分和真实诊断状态继续保留。 */
34
+ export function withoutNoSignalMetadata(value) {
35
+ if (Array.isArray(value)) return value.map(withoutNoSignalMetadata)
36
+ if (!value || typeof value !== 'object') return value
37
+ return Object.fromEntries(Object.entries(value).flatMap(([key, child]) => {
38
+ if (ZERO_VALUE_SCORE_FIELDS.has(key) && Number(child) === 0) return []
39
+ if (DEFAULT_FALSE_FIELDS.has(key) && child === false) return []
40
+ if (key === 'llm_validation' && child === 'not_checked') return []
41
+ return [[key, withoutNoSignalMetadata(child)]]
42
+ }))
43
+ }
44
+
45
+ export function withoutOpaqueIdentifiers(value) {
46
+ if (Array.isArray(value)) return value.map(withoutOpaqueIdentifiers)
47
+ if (!value || typeof value !== 'object') return value
48
+ return Object.fromEntries(Object.entries(value)
49
+ .filter(([key]) => !MODEL_HIDDEN_IDENTIFIER_FIELDS.has(key))
50
+ .map(([key, child]) => [key, withoutOpaqueIdentifiers(child)]))
51
+ }
52
+
53
+ function anchorIndex(mappings) {
54
+ const found = new Map()
55
+ for (const mapping of mappings) {
56
+ if (!mapping.suggested_source_ref) continue
57
+ if (mapping.evidence_id) found.set(`evidence:${mapping.evidence_id}`, mapping.suggested_source_ref)
58
+ if (mapping.candidate_id) found.set(`candidate:${mapping.candidate_id}`, mapping.suggested_source_ref)
59
+ }
60
+ return found
61
+ }
62
+
63
+ function addSourceAnchors(items, anchors) {
64
+ return (items || []).map((item) => {
65
+ if (!item || typeof item !== 'object') return item
66
+ const copy = structuredClone(item)
67
+ const sourceRef = anchors.get(`evidence:${copy.evidence_id}`)
68
+ || anchors.get(`candidate:${copy.candidate_id}`)
69
+ if (sourceRef) copy.suggested_source_ref = sourceRef
70
+ return copy
71
+ })
72
+ }
73
+
74
+ export function projectedAnchorPoints(mappings) {
75
+ return mappings.filter((mapping) => mapping.suggested_source_ref).map((mapping) => withoutEmptyMetadata({
76
+ game: mapping.game,
77
+ title: mapping.title || mapping.display_title,
78
+ lines: mapping.line_range || (mapping.start_line && mapping.end_line
79
+ ? (Number(mapping.start_line) === Number(mapping.end_line)
80
+ ? `${mapping.start_line}` : `${mapping.start_line}-${mapping.end_line}`) : undefined),
81
+ // DSH 的短篇章 ID 是人类可读标题发生冲突时才需要的消歧信息;前端
82
+ // 没有该字段,保留在锚点不会暴露内部路径或 source_ref。
83
+ document_uid: mapping.title_ambiguous ? mapping.document_uid || undefined : undefined,
84
+ }))
85
+ }
86
+
87
+ export function projectCloudSearch(value) {
88
+ const data = value?.data || {}
89
+ const mappings = value?.local_source_mappings || []
90
+ const sources = addSourceAnchors(data.sources || data.selected_sources || [], anchorIndex(mappings))
91
+ let answerContext = String(data.answer_context || '')
92
+ for (const source of sources) {
93
+ if (source.evidence_id && source.title) {
94
+ answerContext = answerContext.replaceAll(source.evidence_id, `《${source.title}》`)
95
+ }
96
+ }
97
+ answerContext = answerContext
98
+ .replace(/\b(?:evi|ev)_[A-Za-z0-9_-]+\b/gu, '相关资料')
99
+ .replace(/\[(?:E|A|C|H)[1-9][0-9]*\]/gu, '')
100
+ const anchorLines = projectedAnchorPoints(mappings).map((anchor) =>
101
+ `- [${anchor.game === 'endfield' ? '终末地' : '明日方舟'}] 《${anchor.title || '原文'}》${anchor.lines ? `第 ${anchor.lines} 行` : ''}`
102
+ + `${anchor.document_uid ? `(读取时以 ${anchor.document_uid} 替代 title,不要同时提交二者)` : ''}`)
103
+ const relationLines = (value?.retraveler_relations || []).map((item) =>
104
+ `- 终末地角色:${item.endfield_name};泰拉记忆原型:${item.terra_memory_prototype || '未登记'};` +
105
+ `状态:${item.relation_status}。这是跨游戏关系,不是人物别名。`)
106
+ const warnings = [...(data.errors || []),
107
+ ...(value?.local_source_mapping_warning ? [value.local_source_mapping_warning] : [])]
108
+ return [answerContext.trim(), relationLines.length
109
+ ? `## 再旅者对应关系(人工审校附属字段)\n${relationLines.join('\n')}` : '',
110
+ anchorLines.length ? `## 可读取原文\n${anchorLines.join('\n')}` : '',
111
+ warnings.length ? `## 警告\n${warnings.map((item) => item.message || item).join('\n')}` : '']
112
+ .filter(Boolean).join('\n\n')
113
+ }
114
+
115
+ export function projectCloudInspect(value) {
116
+ const data = value?.data || {}
117
+ const mappings = value?.local_source_mappings || []
118
+ const items = addSourceAnchors(data.items || [], anchorIndex(mappings))
119
+ return withoutEmptyMetadata(withoutNoSignalMetadata(withoutOpaqueIdentifiers({ tool_name: 'cloud_inspect',
120
+ status: 'ok', payload: {
121
+ code: value?.code, anchor_points: projectedAnchorPoints(mappings), data: { ...data, items },
122
+ ...(value?.local_source_mapping_warning ? { warnings: [value.local_source_mapping_warning] } : {}),
123
+ } })))
124
+ }