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.
- package/GAME_ASSETS.md +26 -0
- package/LICENSE +21 -0
- package/README.en.md +274 -0
- package/README.md +423 -0
- package/THIRD_PARTY_NOTICES.md +61 -0
- package/bin/bake-rhine-details.mjs +34 -0
- package/bin/install.js +273 -0
- package/bin/pack-map-assets.mjs +110 -0
- package/bin/preview-rhine.mjs +71 -0
- package/bin/rhine-detail-baker.ts +84 -0
- package/contracts/cloud_tools_v1.schema.json +243 -0
- package/contracts/corpus_i18n_v1.schema.json +179 -0
- package/contracts/corpus_tools_v1.schema.json +1801 -0
- package/cordis.patch.yml +32 -0
- package/docs/endfield-official-i18n.md +103 -0
- package/lib/client.js +3912 -0
- package/lib/endfield-map/map.js.br +0 -0
- package/lib/endfield-map/map.js.gz +0 -0
- package/lib/endfield-map/resources/map-00b0d0744a1b4404.json.br +0 -0
- package/lib/endfield-map/resources/map-00b0d0744a1b4404.json.gz +0 -0
- package/lib/endfield-map/resources/map-01616f534aaaeca1.png +0 -0
- package/lib/endfield-map/resources/map-1562e0dff4be3d23.png +0 -0
- package/lib/endfield-map/resources/map-160fe040f080f6a3.png +0 -0
- package/lib/endfield-map/resources/map-17b9d8be25dc2b39.json.br +0 -0
- package/lib/endfield-map/resources/map-17b9d8be25dc2b39.json.gz +0 -0
- package/lib/endfield-map/resources/map-1c4245c4e8b80c43.json.br +0 -0
- package/lib/endfield-map/resources/map-1c4245c4e8b80c43.json.gz +0 -0
- package/lib/endfield-map/resources/map-1d30baaaef3e29cc.png +0 -0
- package/lib/endfield-map/resources/map-21a61386af79cd12.json.br +0 -0
- package/lib/endfield-map/resources/map-21a61386af79cd12.json.gz +0 -0
- package/lib/endfield-map/resources/map-24b920b8dab49b44.json.br +0 -0
- package/lib/endfield-map/resources/map-24b920b8dab49b44.json.gz +0 -0
- package/lib/endfield-map/resources/map-26caf8d71247abb7.png +0 -0
- package/lib/endfield-map/resources/map-2729942c89545e73.json.br +0 -0
- package/lib/endfield-map/resources/map-2729942c89545e73.json.gz +0 -0
- package/lib/endfield-map/resources/map-29063a9802f67014.json.br +0 -0
- package/lib/endfield-map/resources/map-29063a9802f67014.json.gz +0 -0
- package/lib/endfield-map/resources/map-2f35d09a3ab60a63.json.br +0 -0
- package/lib/endfield-map/resources/map-2f35d09a3ab60a63.json.gz +0 -0
- package/lib/endfield-map/resources/map-2f98a873da81425e.json.br +0 -0
- package/lib/endfield-map/resources/map-2f98a873da81425e.json.gz +0 -0
- package/lib/endfield-map/resources/map-3ab978ae44190f80.json.br +0 -0
- package/lib/endfield-map/resources/map-3ab978ae44190f80.json.gz +0 -0
- package/lib/endfield-map/resources/map-3b62c80d14208902.json.br +0 -0
- package/lib/endfield-map/resources/map-3b62c80d14208902.json.gz +0 -0
- package/lib/endfield-map/resources/map-3d486e887ae121ac.png +0 -0
- package/lib/endfield-map/resources/map-418dc4865608fce5.png +0 -0
- package/lib/endfield-map/resources/map-4366b84607335f76.json.br +0 -0
- package/lib/endfield-map/resources/map-4366b84607335f76.json.gz +0 -0
- package/lib/endfield-map/resources/map-4371259d3ab7aac7.png +0 -0
- package/lib/endfield-map/resources/map-48dcc0fd55e13f7f.png +0 -0
- package/lib/endfield-map/resources/map-4aabda0e41197570.json.br +0 -0
- package/lib/endfield-map/resources/map-4aabda0e41197570.json.gz +0 -0
- package/lib/endfield-map/resources/map-505f5687b287043c.json.br +0 -0
- package/lib/endfield-map/resources/map-505f5687b287043c.json.gz +0 -0
- package/lib/endfield-map/resources/map-5177e9d1d46f47e2.png +0 -0
- package/lib/endfield-map/resources/map-51baeb59b7f8c28d.png +0 -0
- package/lib/endfield-map/resources/map-549561b46e621874.png +0 -0
- package/lib/endfield-map/resources/map-56c6a0bef799c8dc.json.br +0 -0
- package/lib/endfield-map/resources/map-56c6a0bef799c8dc.json.gz +0 -0
- package/lib/endfield-map/resources/map-5a6963909423b674.png +0 -0
- package/lib/endfield-map/resources/map-5a9c533c0552d7b7.png +0 -0
- package/lib/endfield-map/resources/map-5d20ae9ffa9a02b0.png +0 -0
- package/lib/endfield-map/resources/map-626c8847393f60bf.png +0 -0
- package/lib/endfield-map/resources/map-63d43cbec3bed3b8.png +0 -0
- package/lib/endfield-map/resources/map-661b6ec56e722574.png +0 -0
- package/lib/endfield-map/resources/map-6ddd384eca190828.png +0 -0
- package/lib/endfield-map/resources/map-6fc1aa9144f8156c.json.br +0 -0
- package/lib/endfield-map/resources/map-6fc1aa9144f8156c.json.gz +0 -0
- package/lib/endfield-map/resources/map-738ebaa002c33cfc.png +0 -0
- package/lib/endfield-map/resources/map-758c13be6085ec07.json.br +0 -0
- package/lib/endfield-map/resources/map-758c13be6085ec07.json.gz +0 -0
- package/lib/endfield-map/resources/map-78929f26d6844509.png +0 -0
- package/lib/endfield-map/resources/map-7f09c142950a0f74.png +0 -0
- package/lib/endfield-map/resources/map-8385e11f39e1a51c.json.br +0 -0
- package/lib/endfield-map/resources/map-8385e11f39e1a51c.json.gz +0 -0
- package/lib/endfield-map/resources/map-881343926a568f27.json.br +0 -0
- package/lib/endfield-map/resources/map-881343926a568f27.json.gz +0 -0
- package/lib/endfield-map/resources/map-8d1ade3f01d952b0.png +0 -0
- package/lib/endfield-map/resources/map-8fef3495bb7ed60f.png +0 -0
- package/lib/endfield-map/resources/map-907a7ecf8a0ce352.png +0 -0
- package/lib/endfield-map/resources/map-9113d5951c4db903.json.br +0 -0
- package/lib/endfield-map/resources/map-9113d5951c4db903.json.gz +0 -0
- package/lib/endfield-map/resources/map-94ad6634436224e3.json.br +0 -0
- package/lib/endfield-map/resources/map-94ad6634436224e3.json.gz +0 -0
- package/lib/endfield-map/resources/map-99722e61da5f3e53.png +0 -0
- package/lib/endfield-map/resources/map-9be9596cc059abe3.png +0 -0
- package/lib/endfield-map/resources/map-a4db9a4763e47fbf.png +0 -0
- package/lib/endfield-map/resources/map-a4e8ef07159997e7.json.br +0 -0
- package/lib/endfield-map/resources/map-a4e8ef07159997e7.json.gz +0 -0
- package/lib/endfield-map/resources/map-ab35d06875ee418f.png +0 -0
- package/lib/endfield-map/resources/map-ac51bb3e7623cd8b.png +0 -0
- package/lib/endfield-map/resources/map-ac830a66296bc83a.png +0 -0
- package/lib/endfield-map/resources/map-b3fd04209718e55e.json.br +0 -0
- package/lib/endfield-map/resources/map-b3fd04209718e55e.json.gz +0 -0
- package/lib/endfield-map/resources/map-b8164f1af7c5d895.png +0 -0
- package/lib/endfield-map/resources/map-b8cde93b965384d8.json.br +0 -0
- package/lib/endfield-map/resources/map-b8cde93b965384d8.json.gz +0 -0
- package/lib/endfield-map/resources/map-b8e48511502e0e8d.json.br +0 -0
- package/lib/endfield-map/resources/map-b8e48511502e0e8d.json.gz +0 -0
- package/lib/endfield-map/resources/map-bfc58277d5ba649d.png +0 -0
- package/lib/endfield-map/resources/map-c013f82048c6891a.png +0 -0
- package/lib/endfield-map/resources/map-c49c947841ae4ab5.json.br +0 -0
- package/lib/endfield-map/resources/map-c49c947841ae4ab5.json.gz +0 -0
- package/lib/endfield-map/resources/map-c4d1b1c6d6706c81.json.br +0 -0
- package/lib/endfield-map/resources/map-c4d1b1c6d6706c81.json.gz +0 -0
- package/lib/endfield-map/resources/map-c607f8b4c8ad8967.png +0 -0
- package/lib/endfield-map/resources/map-c6f20276b8781326.json.br +0 -0
- package/lib/endfield-map/resources/map-c6f20276b8781326.json.gz +0 -0
- package/lib/endfield-map/resources/map-d51adf5952af1e8e.png +0 -0
- package/lib/endfield-map/resources/map-d695eface22f8b74.json.br +0 -0
- package/lib/endfield-map/resources/map-d695eface22f8b74.json.gz +0 -0
- package/lib/endfield-map/resources/map-db11d394456a7465.png +0 -0
- package/lib/endfield-map/resources/map-dcc4d218581e3dc6.png +0 -0
- package/lib/endfield-map/resources/map-ddd43dbb8ab8a4e3.json.br +0 -0
- package/lib/endfield-map/resources/map-ddd43dbb8ab8a4e3.json.gz +0 -0
- package/lib/endfield-map/resources/map-defe5db7dabf748a.png +0 -0
- package/lib/endfield-map/resources/map-e1aeb833e22520aa.json.br +0 -0
- package/lib/endfield-map/resources/map-e1aeb833e22520aa.json.gz +0 -0
- package/lib/endfield-map/resources/map-e25ee9af6e493500.png +0 -0
- package/lib/endfield-map/resources/map-e3824082cd6f747d.json.br +0 -0
- package/lib/endfield-map/resources/map-e3824082cd6f747d.json.gz +0 -0
- package/lib/endfield-map/resources/map-e694a91e630b1bd6.png +0 -0
- package/lib/endfield-map/resources/map-eaa93af79d12a87d.png +0 -0
- package/lib/endfield-map/resources/map-ec9575ee65d7766f.json.br +0 -0
- package/lib/endfield-map/resources/map-ec9575ee65d7766f.json.gz +0 -0
- package/lib/endfield-map/resources/map-ede421a329dc78a4.png +0 -0
- package/lib/endfield-map/resources/map-eff3bce8f6888fc5.json.br +0 -0
- package/lib/endfield-map/resources/map-eff3bce8f6888fc5.json.gz +0 -0
- package/lib/endfield-map/resources/map-fb5f4a59a181452c.png +0 -0
- package/lib/endfield-map/resources/map-fdeaa17eeba90d8e.json.br +0 -0
- package/lib/endfield-map/resources/map-fdeaa17eeba90d8e.json.gz +0 -0
- package/lib/rhine/assets/archive-assembly.glb +0 -0
- package/lib/rhine/assets/archive-cassette.glb +0 -0
- package/lib/rhine/fonts/MiSans-Bold.woff2 +0 -0
- package/lib/rhine/fonts/MiSans-Demibold.woff2 +0 -0
- package/lib/rhine/fonts/MiSans-Light.woff2 +0 -0
- package/lib/rhine/fonts/MiSans-Regular.woff2 +0 -0
- package/lib/rhine/fonts/MiSans-license.pdf +0 -0
- package/lib/rhine/fonts/NOTICE.txt +6 -0
- package/lib/rhine/licenses/RhineLabUI-MIT.txt +21 -0
- package/lib/rhine/licenses/entities-BSD-2-Clause.txt +11 -0
- package/lib/rhine/licenses/linkify-it-MIT.txt +22 -0
- package/lib/rhine/licenses/markdown-it-MIT.txt +22 -0
- package/lib/rhine/licenses/mdurl-MIT.txt +45 -0
- package/lib/rhine/licenses/punycode.js-MIT.txt +20 -0
- package/lib/rhine/licenses/rolling-number-MIT.txt +21 -0
- package/lib/rhine/licenses/three-MIT.txt +21 -0
- package/lib/rhine/licenses/uc.micro-MIT.txt +20 -0
- package/lib/rhine/rhine.css +1 -0
- package/lib/rhine/rhine.js +5106 -0
- package/lib/skins/common.css +31 -0
- package/lib/skins/endfield-aic.css +141 -0
- package/lib/skins/prts-agent.css +136 -0
- package/lib/skins/rhine-lab.css +70 -0
- package/package.json +112 -0
- package/presets/definition.js +26 -0
- package/presets/prts/agent.cordis.yml +25 -0
- package/presets/prts/preset.yml +3 -0
- package/presets/register.js +86 -0
- package/resources/entity-routing.json +21 -0
- package/skills/prts-investigation/SKILL.md +121 -0
- package/skills/prts-retrieval/SKILL.md +35 -0
- package/skills/prts-retrieval/references/cloud-tools.md +13 -0
- package/skills/prts-retrieval/references/diagnostics.md +15 -0
- package/skills/prts-retrieval/references/local-tools.md +24 -0
- package/skills/prts-retrieval/references/module-arknights.md +23 -0
- package/skills/prts-retrieval/references/module-dual.md +15 -0
- package/skills/prts-retrieval/references/module-endfield.md +21 -0
- package/skills/prts-retrieval/references/retrieval-process.md +44 -0
- package/skills/prts-retrieval/references/retrieval-recipes-dual.md +34 -0
- package/skills/prts-retrieval/references/retrieval-recipes-endfield.md +37 -0
- package/skills/prts-retrieval/references/retrieval-recipes.md +39 -0
- package/skills/prts-retrieval/references/tools-runtime.md +48 -0
- package/skills/prts-retrieval/references/wiki-schema.md +38 -0
- package/src/archive-presentation.js +287 -0
- package/src/cloud-projection.js +124 -0
- package/src/cloud.js +498 -0
- package/src/entity-recognizer.js +311 -0
- package/src/entity-routing.js +82 -0
- package/src/evidence-state.js +332 -0
- package/src/i18n.js +365 -0
- package/src/index.js +1261 -0
- package/src/installer.js +1362 -0
- package/src/investigation-review.js +87 -0
- package/src/investigation-store.js +579 -0
- package/src/investigation-tools.js +113 -0
- package/src/localization-format.js +23 -0
- package/src/manual-search.js +103 -0
- package/src/read.js +1097 -0
- package/src/release-compatibility.js +73 -0
- package/src/release-watch.js +40 -0
- package/src/search-projection.js +139 -0
- package/src/search.js +1522 -0
- package/src/skill.js +98 -0
- package/src/source-map.js +348 -0
- package/src/state.js +314 -0
- package/src/store.js +1569 -0
- package/src/timeline.js +386 -0
- package/src/ui.js +1353 -0
- package/src/wiki.js +111 -0
- package/tsconfig.rhine.json +15 -0
- package/ui/rhine/archive-navigation-limit.ts +31 -0
- package/ui/rhine/archive-rack.ts +83 -0
- package/ui/rhine/assets/evidence-observatory.webp +0 -0
- package/ui/rhine/board-plane-transform.ts +24 -0
- package/ui/rhine/catalogue.ts +72 -0
- package/ui/rhine/deferred-preparation.ts +146 -0
- package/ui/rhine/evidence-board-controls.css +22 -0
- package/ui/rhine/evidence-board-history.ts +73 -0
- package/ui/rhine/evidence-board-model.ts +189 -0
- package/ui/rhine/evidence-board-panel.ts +762 -0
- package/ui/rhine/evidence-board-scene.ts +548 -0
- package/ui/rhine/evidence-board.css +191 -0
- package/ui/rhine/evidence-card-art.ts +470 -0
- package/ui/rhine/evidence-card-style.ts +17 -0
- package/ui/rhine/evidence-inbox-scene.ts +43 -0
- package/ui/rhine/evidence-inbox.css +29 -0
- package/ui/rhine/evidence-inbox.ts +104 -0
- package/ui/rhine/index.ts +12 -0
- package/ui/rhine/interaction.css +20 -0
- package/ui/rhine/investigation-board-plane.ts +5 -0
- package/ui/rhine/investigation-board.css +34 -0
- package/ui/rhine/investigation-board.ts +348 -0
- package/ui/rhine/investigation-edits.ts +46 -0
- package/ui/rhine/investigation-layout.ts +29 -0
- package/ui/rhine/label-prefetch.ts +34 -0
- package/ui/rhine/looping-title.ts +40 -0
- package/ui/rhine/manual-search.css +41 -0
- package/ui/rhine/manual-search.ts +89 -0
- package/ui/rhine/original/appearance.ts +180 -0
- package/ui/rhine/original/archive-label.ts +84 -0
- package/ui/rhine/original/archive-lighting.ts +39 -0
- package/ui/rhine/original/archive-loop.ts +82 -0
- package/ui/rhine/original/archive-refill.ts +64 -0
- package/ui/rhine/original/array-detail-data.ts +2 -0
- package/ui/rhine/original/array-detail.ts +41 -0
- package/ui/rhine/original/array-interior.ts +411 -0
- package/ui/rhine/original/array-occlusion.ts +166 -0
- package/ui/rhine/original/array-shell.ts +38 -0
- package/ui/rhine/original/array-visibility.ts +309 -0
- package/ui/rhine/original/asset-url.ts +4 -0
- package/ui/rhine/original/brand.ts +12 -0
- package/ui/rhine/original/cassette-lod.ts +81 -0
- package/ui/rhine/original/data.ts +21 -0
- package/ui/rhine/original/decryption.ts +185 -0
- package/ui/rhine/original/detail-signature.ts +18 -0
- package/ui/rhine/original/glass-reveal.ts +45 -0
- package/ui/rhine/original/internal-optics.ts +70 -0
- package/ui/rhine/original/label-renderer.ts +119 -0
- package/ui/rhine/original/label-textures.ts +117 -0
- package/ui/rhine/original/model-viewer.ts +658 -0
- package/ui/rhine/original/motion.ts +116 -0
- package/ui/rhine/original/opaque-backfaces.ts +64 -0
- package/ui/rhine/original/program-preparation.ts +111 -0
- package/ui/rhine/original/quality-renderer.ts +97 -0
- package/ui/rhine/original/quality-settings.ts +111 -0
- package/ui/rhine/original/render-quality.ts +163 -0
- package/ui/rhine/original/scene.ts +2051 -0
- package/ui/rhine/original/shelf-interior.ts +142 -0
- package/ui/rhine/original/ui-transitions.ts +111 -0
- package/ui/rhine/original/viewer-camera.ts +92 -0
- package/ui/rhine/original/viewport-layout.ts +41 -0
- package/ui/rhine/original-ui.css +2334 -0
- package/ui/rhine/readability.css +46 -0
- package/ui/rhine/reading-object.ts +96 -0
- package/ui/rhine/reading-origin.ts +28 -0
- package/ui/rhine/reading-room.css +55 -0
- package/ui/rhine/report-markdown.ts +98 -0
- package/ui/rhine/scene-edge-navigation.ts +84 -0
- package/ui/rhine/scene.ts +1726 -0
- package/ui/rhine/shelf-layout.ts +38 -0
- package/ui/rhine/snapshot-update-gate.ts +22 -0
- package/ui/rhine/source-reading.ts +15 -0
- package/ui/rhine/source-selection.css +164 -0
- package/ui/rhine/temporary-performance-observers.ts +163 -0
- package/ui/rhine/temporary-performance-panel.ts +124 -0
- package/ui/rhine/temporary-performance.css +19 -0
- package/ui/rhine/temporary-performance.ts +552 -0
- package/ui/rhine/temporary-render-diagnostics.ts +105 -0
- package/ui/rhine/tool-activity.ts +69 -0
- package/ui/rhine/types.ts +219 -0
- package/ui/rhine/visible-scene-hit.ts +13 -0
- package/ui/rhine/workbench.css +818 -0
- package/ui/rhine/workbench.ts +2245 -0
- package/ui/rhine/workspace-travel.ts +32 -0
- package/vite.rhine.config.ts +19 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,1261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prts-terrarchive:PRTS.chat 联合语料检索工具的 deepseek-harness 插件。
|
|
3
|
+
*
|
|
4
|
+
* 不经 defineTool(避免与宿主 dsh-tools 版本漂移),直接向
|
|
5
|
+
* ctx.tools 注册原始 ToolDefinition。模型使用扁平参数(title 或关卡代号 + 行动前后
|
|
6
|
+
* corpus_read、anyOf 拆为 execute 内跨字段校验),执行层再落实版本化契约。
|
|
7
|
+
*
|
|
8
|
+
* 工具集:
|
|
9
|
+
* corpus_search grep 风格本地语料搜索/目录(literal/受限 regex)
|
|
10
|
+
* corpus_read 按关卡代号 + 行动前后或完整标题直读原文(不自动夹带伴随资料)
|
|
11
|
+
* corpus_i18n 官方本地化原文查询(目前覆盖终末地)
|
|
12
|
+
* timeline_search 活动时间线检索(别名裂变 / 年份交集 / 出处标记反查)
|
|
13
|
+
* cloud_search 云端组合语义检索(需 cloud.baseUrl 配置)
|
|
14
|
+
* cloud_inspect 云端检索状态复查(request_id 由运行时注入)
|
|
15
|
+
*
|
|
16
|
+
* 加载方式(任选其一):
|
|
17
|
+
* 1) bundle:dsh plugin --profile <name> add /path/to/agent-dsh
|
|
18
|
+
* 2) overlay:dsh web --patch ./agent-dsh/cordis.patch.yml(需可被 Node 解析)
|
|
19
|
+
*
|
|
20
|
+
* 配置(cordis.patch.yml 行内 config):
|
|
21
|
+
* releasesDir 资料包 releases 目录(默认 $DSH_HOME/prts-corpus/releases)
|
|
22
|
+
* cacheShards 分片 LRU 缓存大小(默认 24)
|
|
23
|
+
* download { order, siteBaseUrl } 设置页字节源;可信元数据固定来自 PRTS.chat
|
|
24
|
+
* cloud { baseUrl, game, userId, token, timeoutMs };game 默认 all(双游戏)
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { isAbsolute, join, parse, resolve } from 'node:path'
|
|
28
|
+
import { homedir, tmpdir } from 'node:os'
|
|
29
|
+
import { createHash } from 'node:crypto'
|
|
30
|
+
import { mkdir, realpath } from 'node:fs/promises'
|
|
31
|
+
import { assertCorpusVersion, corpusVersionSnapshot,
|
|
32
|
+
CorpusStore, documentGame, documentUid, naturalDocumentTitle, publicStoryStageCode,
|
|
33
|
+
publicStoryPart } from './store.js'
|
|
34
|
+
import { readCurrentReleasePointer } from './installer.js'
|
|
35
|
+
import { watchCurrentRelease } from './release-watch.js'
|
|
36
|
+
import { END_FIELD_STORY_CONTENT_TYPES, executeRead, readContractFromCursor,
|
|
37
|
+
normalizeReadRequest, projectReadPublic, renderRead } from './read.js'
|
|
38
|
+
import { executeSearch, renderSearch } from './search.js'
|
|
39
|
+
import { executeI18n, I18N_DESCRIPTION, I18N_PARAMETERS, I18N_OUTPUT_SCHEMA, renderI18n } from './i18n.js'
|
|
40
|
+
import { executeTimelineSearch, renderTimeline } from './timeline.js'
|
|
41
|
+
import { AnonymousSessionProvider, CloudRetrievalClient, StaticTokenProvider,
|
|
42
|
+
createAgentCloudClientRegistry,
|
|
43
|
+
cloudErrorResponse, readOrCreateClientId } from './cloud.js'
|
|
44
|
+
import { createSharedState } from './state.js'
|
|
45
|
+
import { applyUi } from './ui.js'
|
|
46
|
+
import { acquireInvestigationStore } from './investigation-store.js'
|
|
47
|
+
import { mountInvestigationTools } from './investigation-tools.js'
|
|
48
|
+
import { attachLocalSourceMappings } from './source-map.js'
|
|
49
|
+
import { projectCloudInspect, projectCloudSearch } from './cloud-projection.js'
|
|
50
|
+
import { coveredRead, createEvidenceStateRegistry,
|
|
51
|
+
planReadCoverage, rememberCloudMappings, rememberRead,
|
|
52
|
+
rememberSearchCandidates, replayCoveredRead, resolveReadWindow,
|
|
53
|
+
reuseReadCoverage, visibleToolResults } from './evidence-state.js'
|
|
54
|
+
import { applyEntityRecognition, prepareEntityRecognition } from './entity-recognizer.js'
|
|
55
|
+
import { attachRetravelerRelations } from './entity-routing.js'
|
|
56
|
+
import { WIKI_SECTION_VALUES } from './wiki.js'
|
|
57
|
+
import { archivePresentationMeta, cloudArchivePresentation, installArchivePtcReceipts, localArchivePresentation,
|
|
58
|
+
rememberArchivePresentation, timelineArchivePresentation } from './archive-presentation.js'
|
|
59
|
+
|
|
60
|
+
/** Cordis 插件名(Loader 诊断用,与 Node 包名 prts-terrarchive 相互独立)。 */
|
|
61
|
+
export const name = 'prts-corpus'
|
|
62
|
+
|
|
63
|
+
/** 同一 Host 进程中,host 常驻实例与会话 preset 共用一份大型索引。 */
|
|
64
|
+
const storesByDirectory = new Map()
|
|
65
|
+
|
|
66
|
+
const LOCAL_CORPUS_MISSING_MESSAGE =
|
|
67
|
+
'本地数据包暂未安装,请提醒用户前往“设置 → 插件 → PRTS 语料”安装。'
|
|
68
|
+
const DATA_VERSION_PATTERN = /^[0-9a-f]{64}$/
|
|
69
|
+
|
|
70
|
+
function sameFilesystemPath(left, right) {
|
|
71
|
+
return process.platform === 'win32'
|
|
72
|
+
? left.toLowerCase() === right.toLowerCase() : left === right
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function filesystemAncestorOrSame(parent, child) {
|
|
76
|
+
const normalizedParent = process.platform === 'win32' ? parent.toLowerCase() : parent
|
|
77
|
+
const normalizedChild = process.platform === 'win32' ? child.toLowerCase() : child
|
|
78
|
+
return normalizedChild === normalizedParent
|
|
79
|
+
|| normalizedChild.startsWith(`${normalizedParent.replace(/[\\/]$/u, '')}${process.platform === 'win32' ? '\\' : '/'}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function resolveSafeReleasesDirectory(path, dshHome) {
|
|
83
|
+
await mkdir(path, { recursive: true, mode: 0o700 })
|
|
84
|
+
const actual = await realpath(path)
|
|
85
|
+
const forbidden = []
|
|
86
|
+
for (const entry of [parse(actual).root, homedir(), dshHome, process.cwd(), tmpdir()]) {
|
|
87
|
+
const resolved = resolve(entry)
|
|
88
|
+
forbidden.push(await realpath(resolved).catch(() => resolved))
|
|
89
|
+
}
|
|
90
|
+
// 不仅拒绝这些目录本身,也拒绝它们的祖先(例如 /home):否则一个合法
|
|
91
|
+
// releaseId 仍可能把宿主家目录变成 UI 递归删除的候选目标。
|
|
92
|
+
if (forbidden.some((entry) => sameFilesystemPath(actual, entry)
|
|
93
|
+
|| filesystemAncestorOrSame(actual, entry))) {
|
|
94
|
+
throw Object.assign(new Error(`releasesDir 不能指向宽目录:${actual}`), {
|
|
95
|
+
code: 'INVALID_CONFIG',
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
return actual
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Convert local release/index failures into one actionable model-facing error. */
|
|
102
|
+
async function requireLocalCorpus(store) {
|
|
103
|
+
try {
|
|
104
|
+
await store.ready()
|
|
105
|
+
} catch (error) {
|
|
106
|
+
throw Object.assign(new Error(LOCAL_CORPUS_MISSING_MESSAGE), {
|
|
107
|
+
code: 'CORPUS_NOT_INSTALLED', retryable: false, cause: error,
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const READ_DESCRIPTION = [
|
|
113
|
+
'读取 PRTS.chat 本地资料。明日方舟关卡用 stage_code,只有多篇时才填 story_part;干员密录用 character_name + record_name + segment;角色资料用 character_name + material。',
|
|
114
|
+
'整个明日方舟活动用 activity_name + mode=activity,终末地任务用 collection_name + mode=collection;合集续页原样提交 page.continuation 的 position。其他资料使用完整 title。',
|
|
115
|
+
'搜索结果若给出 document_uid,说明标题或合集同名:它替代 title,读取时只提交 document_uid,不得同时提交 title;单篇可配 line 或 mode=document,所属合集可配 mode=activity/collection。',
|
|
116
|
+
'line 扩大单篇原文上下文,section 读取 Wiki 字段,mode=document 分页全文。所有续页都原样提交 page.continuation;旧会话里的 cursor 仅用于兼容。',
|
|
117
|
+
'引用原文使用“《篇章名》第 N 行”;同名结果保留工具给出的 document_uid 作为唯一定位器。不要使用内部代号、路径或自造篇章名。',
|
|
118
|
+
].join(' ')
|
|
119
|
+
|
|
120
|
+
const SEARCH_DESCRIPTION = [
|
|
121
|
+
'像 grep 一样搜索 PRTS.chat 本地语料;命中立即返回原行及上下各一行,并按文档归并。',
|
|
122
|
+
'query 使用短实体名、篇章展示名或原句片段;也可省略 query,仅按过滤条件列出资料入口。',
|
|
123
|
+
'角色个人页用 character_wiki;活动/密录整理页用 story_wiki;角色在单个活动中的辅助整理用 character_activity_wiki。wiki_sections 可精确限定相关活动、相关角色、剧情总结、角色剧情概括等标签字段。',
|
|
124
|
+
'literal 是默认连续字面匹配;只有特殊模式才使用受限 regex。新查询的下一页保留原搜索条件,并把返回的 next_after 原样放入 after;锚点由完整资料版本、资料类型与自然标题组成,不再暴露内部 cursor。旧会话的 cursor 链仅按返回的 next_cursor 继续。',
|
|
125
|
+
].join(' ')
|
|
126
|
+
|
|
127
|
+
const TIMELINE_DESCRIPTION = [
|
|
128
|
+
'按活动名、年份及自动展开的实体别名检索活动时间线(PRTS Wiki《泰拉年表》本地投影)。',
|
|
129
|
+
'人物放 entity_names 以自动裂变别名;结果只给时间、事件正文和“年表出处”标记,把标记原样传回 source_marker 可反查完整来源。',
|
|
130
|
+
].join(' ')
|
|
131
|
+
|
|
132
|
+
const CLOUD_SEARCH_DESCRIPTION = [
|
|
133
|
+
'用一次自然语言请求同时查询 PRTS.chat 云端的明日方舟与终末地图谱、档案、原文、自建 Wiki 和时间线组合索引;只有用户明确限定游戏时才使用 games 收窄。',
|
|
134
|
+
'返回末尾的「## 可读取原文」列出已映射到本地篇章的完整自然语言标题与行号;有同名消歧标记时按 document_uid + line 调用,否则按 title + line。',
|
|
135
|
+
].join(' ')
|
|
136
|
+
|
|
137
|
+
const CLOUD_INSPECT_DESCRIPTION = [
|
|
138
|
+
'按 section 和过滤条件读取最近一次云端检索状态(request_id 由运行时自动注入)。',
|
|
139
|
+
'用于定点读取回答材料、候选状态或诊断记录,并支持按 next_cursor 分页。',
|
|
140
|
+
].join(' ')
|
|
141
|
+
|
|
142
|
+
const TOOL_GAME_LABELS = Object.freeze({ arknights: '明日方舟', endfield: '终末地' })
|
|
143
|
+
|
|
144
|
+
function compactToolTitle(value, maximum = 48) {
|
|
145
|
+
const text = String(value ?? '').replace(/[\u0000-\u001f\u007f]+/gu, ' ')
|
|
146
|
+
.replace(/\s+/gu, ' ').trim()
|
|
147
|
+
return text.length > maximum ? `${text.slice(0, maximum - 1)}…` : text
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function presentedGames(args, enabledGames) {
|
|
151
|
+
const requested = Array.isArray(args?.games) ? args.games : enabledGames
|
|
152
|
+
const labels = [...new Set(requested)].map((game) => TOOL_GAME_LABELS[game]).filter(Boolean)
|
|
153
|
+
return labels.join(' + ') || '未指定资料库'
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function searchCallView(location, args, enabledGames) {
|
|
157
|
+
const query = compactToolTitle(args?.query)
|
|
158
|
+
const scope = presentedGames(args, enabledGames)
|
|
159
|
+
return { card: 'generic', title: `搜索 ${location} · ${scope}${query ? ` · ${query}` : ''}`,
|
|
160
|
+
kind: 'search', ...(query ? { rawInput: query } : {}) }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function readCallView(args, enabledGames) {
|
|
164
|
+
let games = enabledGames
|
|
165
|
+
if (args?.stage_code || args?.record_name || args?.material) games = ['arknights']
|
|
166
|
+
else if (args?.collection_name) games = ['endfield']
|
|
167
|
+
const target = compactToolTitle(args?.title || args?.stage_code || args?.record_name
|
|
168
|
+
|| args?.character_name || args?.collection_name || args?.document_uid || '资料')
|
|
169
|
+
return { card: 'generic', title: `读取 PRTS 本地原文 · ${presentedGames({ games }, enabledGames)} · ${target}`,
|
|
170
|
+
kind: 'read', rawInput: target }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** ---- 模型可见参数(DSH ctx.tools 的 JSON Schema 子集:无 anyOf/$defs/pattern/数值边界) ---- */
|
|
174
|
+
|
|
175
|
+
const RESOURCE_TYPES = ['story', 'character_profile', 'character_module', 'character_voice',
|
|
176
|
+
'character_skin', 'operator_record', 'character_bundle', 'character_wiki', 'story_wiki',
|
|
177
|
+
'character_activity_wiki', 'reviewed_wiki',
|
|
178
|
+
'terra_journey', 'entity_profile', 'reference',
|
|
179
|
+
'original_story', 'archive', 'knowledge', 'wiki', 'character_story', 'timeline']
|
|
180
|
+
|
|
181
|
+
const CONTENT_TYPES = ['dialogue', 'cutscene', 'radio', 'remote_comm', 'black_screen',
|
|
182
|
+
'environment_talk', 'sns_topic', 'sns_chat', 'narration', 'archive', 'knowledge']
|
|
183
|
+
|
|
184
|
+
const stringList = (description) => ({ type: 'array', items: { type: 'string' }, description })
|
|
185
|
+
|
|
186
|
+
const SEARCH_PARAMETERS = {
|
|
187
|
+
type: 'object', additionalProperties: false,
|
|
188
|
+
properties: {
|
|
189
|
+
cursor: { type: 'string', description: '仅兼容旧会话:单独提交旧 next_cursor;新搜索使用 after' },
|
|
190
|
+
query: { type: 'string', description: '短搜索词:实体名、篇章展示名、活动名或原句片段;不要直接提交整句研究问题' },
|
|
191
|
+
resource_types: { type: 'array', items: { type: 'string', enum: RESOURCE_TYPES },
|
|
192
|
+
description: '资料类型;character_bundle 可一次查看角色档案、模组、语音和密录' },
|
|
193
|
+
games: { type: 'array', items: { type: 'string', enum: ['arknights', 'endfield'] },
|
|
194
|
+
description: '可选游戏过滤;省略时在同一次调用中同时检索明日方舟与终末地' },
|
|
195
|
+
content_types: { type: 'array', items: { type: 'string', enum: CONTENT_TYPES },
|
|
196
|
+
description: '统一内容形式,例如 dialogue、cutscene、radio、sns_chat;两款游戏使用相同参数' },
|
|
197
|
+
collection_names: stringList('上级资料集合展示名;明日方舟活动与终末地任务都使用此字段'),
|
|
198
|
+
character_names: stringList('角色展示名,如“凯尔希”'),
|
|
199
|
+
story_names: stringList('剧情篇章的展示名,如“晶簇之内”;不要填写内部 story_id 或路径'),
|
|
200
|
+
activity_names: stringList('活动展示名'),
|
|
201
|
+
wiki_sections: { type: 'array', items: { type: 'string', enum: WIKI_SECTION_VALUES },
|
|
202
|
+
description: 'Wiki 标签字段,可与资料类型、角色、活动和 query 组合;角色页常用相关活动/相关角色/剧情高光,活动页常用剧情总结/关键人物/角色剧情概括' },
|
|
203
|
+
entity_names: stringList('只返回出现指定实体的行'),
|
|
204
|
+
speakers: stringList('结构化说话人展示名,只匹配亲口台词;适合查某人亲口说过什么'),
|
|
205
|
+
match_mode: { type: 'string', enum: ['literal', 'regex'],
|
|
206
|
+
description: '默认 literal 连续字面匹配;除非必须,不使用受限 regex' },
|
|
207
|
+
context_terms: { type: 'array', items: { type: 'string' },
|
|
208
|
+
description: '要求命中附近同时出现的语境词(最多 8 个)' },
|
|
209
|
+
after: { type: 'object', additionalProperties: false,
|
|
210
|
+
description: '版本绑定的下一页锚点;与原 query 和过滤条件一起原样提交上次返回的 next_after',
|
|
211
|
+
required: ['data_version', 'resource_type', 'title', 'position'], properties: {
|
|
212
|
+
data_version: { type: 'string',
|
|
213
|
+
description: '上一页所用资料包的完整 SHA-256 版本;版本切换后必须重新搜索' },
|
|
214
|
+
resource_type: { type: 'string', enum: RESOURCE_TYPES },
|
|
215
|
+
title: { type: 'string', description: '上一页扫描到的资料自然标题' },
|
|
216
|
+
position: { type: 'integer', description: '该标题在当前资料版本中的顺序位置(0..10000000)' },
|
|
217
|
+
} },
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const SEARCH_OUTPUT_SCHEMA = {
|
|
222
|
+
type: 'object', additionalProperties: false,
|
|
223
|
+
required: ['result_kind', 'documents', 'page', 'truncated', 'truncation_reasons'],
|
|
224
|
+
properties: {
|
|
225
|
+
presentation: { type: 'object', required: ['kind', 'sources'], properties: {
|
|
226
|
+
kind: { type: 'string', enum: ['prts-archive-sources-v1'] },
|
|
227
|
+
sources: { type: 'array', items: { type: 'object' } },
|
|
228
|
+
data_version: { type: 'string' }, sources_truncated: { type: 'boolean' },
|
|
229
|
+
} },
|
|
230
|
+
result_kind: { type: 'string', enum: ['text_matches', 'structured_matches',
|
|
231
|
+
'complete_sections', 'documents'] },
|
|
232
|
+
documents: { type: 'array', items: { type: 'object', additionalProperties: false,
|
|
233
|
+
required: ['game', 'title', 'resource_type', 'content_type', 'matches', 'matches_truncated'],
|
|
234
|
+
properties: {
|
|
235
|
+
game: { type: 'string', enum: ['arknights', 'endfield'] },
|
|
236
|
+
title: { type: 'string' }, document_uid: { type: 'string' }, resource_type: { type: 'string' },
|
|
237
|
+
content_type: { type: 'string' }, collection_name: { type: 'string' },
|
|
238
|
+
activity_name: { type: 'string' }, character_name: { type: 'string' },
|
|
239
|
+
matches_truncated: { type: 'boolean' },
|
|
240
|
+
matches: { type: 'array', items: { type: 'object', additionalProperties: false,
|
|
241
|
+
required: ['match_kind', 'evidence_kind', 'excerpt', 'citation'],
|
|
242
|
+
properties: {
|
|
243
|
+
line_start: { type: 'integer' }, line_end: { type: 'integer' },
|
|
244
|
+
match_kind: { type: 'string' }, evidence_kind: { type: 'string' },
|
|
245
|
+
citation: { type: 'string' },
|
|
246
|
+
excerpt: { type: 'array', items: { type: 'object', additionalProperties: false,
|
|
247
|
+
required: ['line', 'role', 'line_type', 'speaker', 'text', 'truncated'],
|
|
248
|
+
properties: { line: { type: 'integer' },
|
|
249
|
+
role: { type: 'string', enum: ['match', 'context', 'constraint'] },
|
|
250
|
+
line_type: { type: 'string' }, speaker: { type: 'string' },
|
|
251
|
+
text: { type: 'string' }, truncated: { type: 'boolean' } } } },
|
|
252
|
+
} } },
|
|
253
|
+
section_content: { type: 'object', additionalProperties: false,
|
|
254
|
+
required: ['section', 'completeness', 'blocks', 'citation'],
|
|
255
|
+
properties: { section: { type: 'string' },
|
|
256
|
+
completeness: { type: 'string', enum: ['complete', 'partial'] },
|
|
257
|
+
blocks: { type: 'array', items: { type: 'object', additionalProperties: false,
|
|
258
|
+
required: ['type', 'text'], properties: { type: { type: 'string', enum: ['text'] },
|
|
259
|
+
text: { type: 'string' } } } }, citation: { type: 'string' } } },
|
|
260
|
+
entity_summary: { type: 'object', additionalProperties: false,
|
|
261
|
+
required: ['canonical_name', 'truncated', 'citation'],
|
|
262
|
+
properties: { canonical_name: { type: 'string' }, description: { type: 'string' },
|
|
263
|
+
history_summary: { type: 'string' }, truncated: { type: 'boolean' },
|
|
264
|
+
citation: { type: 'string' } } },
|
|
265
|
+
} } },
|
|
266
|
+
page: { type: 'object', additionalProperties: false,
|
|
267
|
+
required: ['returned_documents', 'total_relation', 'has_more', 'exhausted', 'next_after'],
|
|
268
|
+
properties: { returned_documents: { type: 'integer' }, total_documents: { type: 'integer' },
|
|
269
|
+
next_cursor: { type: 'string', description: '仅旧会话分页链返回;单独作为 cursor 续查' },
|
|
270
|
+
total_relation: { type: 'string', enum: ['eq', 'unknown'] },
|
|
271
|
+
has_more: { type: 'boolean' },
|
|
272
|
+
exhausted: { type: 'boolean' },
|
|
273
|
+
next_after: { oneOf: [{ type: 'object', additionalProperties: false,
|
|
274
|
+
required: ['data_version', 'resource_type', 'title', 'position'], properties: {
|
|
275
|
+
data_version: { type: 'string' },
|
|
276
|
+
resource_type: { type: 'string' }, title: { type: 'string' },
|
|
277
|
+
position: { type: 'integer' },
|
|
278
|
+
} }, { type: 'null' }] } } },
|
|
279
|
+
truncated: { type: 'boolean' },
|
|
280
|
+
truncation_reasons: { type: 'array', items: { type: 'string' } },
|
|
281
|
+
warnings: { type: 'array', items: { type: 'object', additionalProperties: false,
|
|
282
|
+
required: ['code', 'game', 'message'], properties: {
|
|
283
|
+
code: { type: 'string' }, game: { type: 'string', enum: ['arknights', 'endfield'] },
|
|
284
|
+
message: { type: 'string' },
|
|
285
|
+
} } },
|
|
286
|
+
retraveler_relations: { type: 'array', items: { type: 'object', additionalProperties: false,
|
|
287
|
+
required: ['relation_kind', 'endfield_name', 'relation_status', 'not_alias'], properties: {
|
|
288
|
+
relation_kind: { type: 'string', enum: ['endfield_retraveler_memory_prototype'] },
|
|
289
|
+
endfield_name: { type: 'string' }, terra_memory_prototype: { type: 'string' },
|
|
290
|
+
relation_status: { type: 'string' }, not_alias: { type: 'boolean' },
|
|
291
|
+
} } },
|
|
292
|
+
},
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const READ_PARAMETERS = {
|
|
296
|
+
type: 'object',
|
|
297
|
+
properties: {
|
|
298
|
+
title: { type: 'string', description: '未使用其他定位器时,填写资料完整展示标题;不得与 document_uid 同时提交' },
|
|
299
|
+
document_uid: { type: 'string',
|
|
300
|
+
description: '仅在搜索结果提示同名歧义时原样复制 doc_ 开头的稳定定位;它替代 title,不得与 title 同时提交;可配合 mode=activity/collection 选择所属合集' },
|
|
301
|
+
stage_code: { type: 'string', description: '明日方舟游戏内关卡代号,如 15-17、GT-3、TW-ST-1' },
|
|
302
|
+
story_part: { type: 'string', enum: ['before', 'after', 'story'],
|
|
303
|
+
description: '关卡存在多篇剧情时用于消歧:before=行动前,after=行动后,story=纯剧情/幕间;单篇关卡可省略' },
|
|
304
|
+
character_name: { type: 'string', description: '角色展示名;与 record_name 或 material 配合' },
|
|
305
|
+
record_name: { type: 'string', description: '明日方舟干员密录名称;多段密录再提供 segment' },
|
|
306
|
+
segment: { type: 'integer', description: '干员密录段号,如 1、2' },
|
|
307
|
+
material: { type: 'string', enum: ['profile', 'module', 'voice', 'skin', 'recruitment', 'potential'],
|
|
308
|
+
description: '角色资料类别;profile=档案、module=模组、voice=语音、skin=时装' },
|
|
309
|
+
game: { type: 'string', enum: ['arknights', 'endfield'],
|
|
310
|
+
description: '角色资料在双模块同名时用于消歧;其他定位器不要填写' },
|
|
311
|
+
activity_name: { type: 'string', description: '明日方舟活动展示名;按活动连续阅读全部剧情' },
|
|
312
|
+
collection_name: { type: 'string', description: '终末地任务或剧情集合展示名;跨碎片连续阅读' },
|
|
313
|
+
content_types: { type: 'array', items: { type: 'string', enum: END_FIELD_STORY_CONTENT_TYPES },
|
|
314
|
+
description: '终末地集合可选内容形式过滤;续页时原样保留' },
|
|
315
|
+
line: { type: 'integer', description: 'around 的中心官方行号;与 mode=document 同用时表示续读起始行' },
|
|
316
|
+
position: { type: 'integer', description: '活动/任务连续阅读的下一位置;只从 page.continuation 原样复制' },
|
|
317
|
+
mode: { type: 'string', enum: ['document', 'activity', 'collection'],
|
|
318
|
+
description: '单篇全文用 document;活动用 activity;终末地任务集合用 collection。title 不会自动推断,必须配 line、section 或 mode=document;document_uid、关卡、密录和角色资料可自动推断单篇全文' },
|
|
319
|
+
section: { type: 'string', enum: WIKI_SECTION_VALUES, description: '读取 Wiki 标签字段' },
|
|
320
|
+
before: { type: 'integer', description: 'around 前文行数,默认 3,上限 100' },
|
|
321
|
+
after: { type: 'integer', description: 'around 后文行数,默认 3,上限 100' },
|
|
322
|
+
cursor: { type: 'string', description: '仅兼容旧会话中的不透明游标;新调用应原样提交上次结果的 page.continuation' },
|
|
323
|
+
data_version: { type: 'string', description: '续页时原样提交 page.continuation.data_version,防止版本切换后混读' },
|
|
324
|
+
max_lines: { type: 'integer', description: '最多返回行数,默认 100,上限 500;只限制输出量,不能代替 line、section 或 mode' },
|
|
325
|
+
max_chars: { type: 'integer', description: '最多返回字符数,默认 12000,上限 100000;只限制输出量,不能代替 line、section 或 mode' },
|
|
326
|
+
},
|
|
327
|
+
additionalProperties: false,
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const READ_COVERAGE_RANGE_SCHEMA = {
|
|
331
|
+
type: 'object', additionalProperties: false,
|
|
332
|
+
required: ['line_start', 'line_end'],
|
|
333
|
+
properties: { line_start: { type: 'integer' }, line_end: { type: 'integer' } },
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const READ_OUTPUT_SCHEMA = {
|
|
337
|
+
type: 'object', additionalProperties: false,
|
|
338
|
+
required: ['primary', 'page', 'presentation'],
|
|
339
|
+
properties: {
|
|
340
|
+
coverage: { type: 'object', additionalProperties: false,
|
|
341
|
+
required: ['requested_range', 'reused_ranges', 'fetched_ranges', 'complete'],
|
|
342
|
+
properties: { requested_range: READ_COVERAGE_RANGE_SCHEMA,
|
|
343
|
+
reused_ranges: { type: 'array', items: READ_COVERAGE_RANGE_SCHEMA },
|
|
344
|
+
fetched_ranges: { type: 'array', items: READ_COVERAGE_RANGE_SCHEMA },
|
|
345
|
+
complete: { type: 'boolean' } } },
|
|
346
|
+
guidance: { type: 'string' },
|
|
347
|
+
primary: { type: 'object', additionalProperties: false,
|
|
348
|
+
required: ['game', 'title', 'kind', 'selection', 'lines', 'citation'],
|
|
349
|
+
properties: { game: { type: 'string', enum: ['arknights', 'endfield'] },
|
|
350
|
+
title: { type: 'string' }, stage_code: { type: 'string' },
|
|
351
|
+
story_part: { type: 'string', enum: ['before', 'after', 'story'] },
|
|
352
|
+
character_name: { type: 'string' }, record_name: { type: 'string' },
|
|
353
|
+
segment: { type: 'integer' }, material: { type: 'string' }, kind: { type: 'string' },
|
|
354
|
+
selection: { type: 'object', additionalProperties: false,
|
|
355
|
+
required: ['mode', 'line_start', 'line_end', 'truncated'],
|
|
356
|
+
properties: { mode: { type: 'string' }, line_start: { type: 'integer' },
|
|
357
|
+
line_end: { type: 'integer' }, section: { type: 'string' },
|
|
358
|
+
truncated: { type: 'boolean' } } },
|
|
359
|
+
lines: { type: 'array', items: { type: 'object', additionalProperties: false,
|
|
360
|
+
required: ['line', 'line_type', 'speaker', 'text'],
|
|
361
|
+
properties: { line: { type: 'integer' }, source_line_id: { type: 'string' },
|
|
362
|
+
line_type: { type: 'string' }, speaker: { type: 'string' }, speaker_id: { type: 'string' },
|
|
363
|
+
text: { type: 'string' }, document_title: { type: 'string' },
|
|
364
|
+
document_uid: { type: 'string' },
|
|
365
|
+
stream_position: { type: 'integer' }, citation: { type: 'string' },
|
|
366
|
+
audio: { type: 'string' }, hint: { type: 'string' } } } },
|
|
367
|
+
text: { type: 'string' }, ordering: { type: 'string' }, ordering_note: { type: 'string' },
|
|
368
|
+
citation: { type: 'string' } } },
|
|
369
|
+
presentation: { type: 'object', additionalProperties: false,
|
|
370
|
+
required: ['document_id', 'data_version'], properties: {
|
|
371
|
+
document_id: { type: 'string' }, data_version: { type: 'string' },
|
|
372
|
+
sources: { type: 'array', items: { type: 'object', additionalProperties: false,
|
|
373
|
+
required: ['document_id', 'document_uid', 'title', 'line_start', 'line_end'], properties: {
|
|
374
|
+
document_id: { type: 'string' }, document_uid: { type: 'string' },
|
|
375
|
+
title: { type: 'string' }, line_start: { type: 'integer' }, line_end: { type: 'integer' },
|
|
376
|
+
} } },
|
|
377
|
+
sources_truncated: { type: 'boolean' },
|
|
378
|
+
} },
|
|
379
|
+
page: { type: 'object', required: ['returned_lines', 'has_more', 'continuation'],
|
|
380
|
+
properties: { returned_lines: { type: 'integer' }, has_more: { type: 'boolean' },
|
|
381
|
+
continuation: { oneOf: [{ type: 'object', additionalProperties: false,
|
|
382
|
+
required: ['mode', 'data_version'], properties: {
|
|
383
|
+
title: { type: 'string' }, document_uid: { type: 'string' }, stage_code: { type: 'string' },
|
|
384
|
+
story_part: { type: 'string', enum: ['before', 'after', 'story'] },
|
|
385
|
+
character_name: { type: 'string' }, record_name: { type: 'string' },
|
|
386
|
+
segment: { type: 'integer' }, material: { type: 'string' },
|
|
387
|
+
game: { type: 'string', enum: ['arknights', 'endfield'] },
|
|
388
|
+
activity_name: { type: 'string' }, collection_name: { type: 'string' },
|
|
389
|
+
content_types: { type: 'array', items: { type: 'string', enum: END_FIELD_STORY_CONTENT_TYPES } },
|
|
390
|
+
mode: { type: 'string', enum: ['document', 'activity', 'collection'] },
|
|
391
|
+
line: { type: 'integer' }, position: { type: 'integer' }, data_version: { type: 'string' },
|
|
392
|
+
} }, { type: 'null' }] } } },
|
|
393
|
+
},
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** 保留 UI 重放所需的稳定文档定位;renderRead 不把该字段送给模型。 */
|
|
397
|
+
function projectReadToolValue(response, store) {
|
|
398
|
+
const projected = projectReadPublic(response)
|
|
399
|
+
const continuation = projected.page?.continuation
|
|
400
|
+
if (continuation?.mode === 'document') {
|
|
401
|
+
if (!continuation.stage_code && !continuation.record_name && !continuation.material
|
|
402
|
+
&& store.requiresDocumentUid?.(response.document?.document_id)) {
|
|
403
|
+
projected.page.continuation = {
|
|
404
|
+
document_uid: documentUid(response.document.document_id), mode: 'document',
|
|
405
|
+
line: continuation.line, data_version: continuation.data_version,
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const uniqueStage = continuation.stage_code && continuation.story_part
|
|
409
|
+
&& store.hasUniqueStoryStage(response.document?.document_id)
|
|
410
|
+
const uniqueRecord = continuation.record_name
|
|
411
|
+
&& store.hasUniqueOperatorRecord(response.document?.document_id)
|
|
412
|
+
const uniqueMaterial = continuation.material
|
|
413
|
+
&& store.hasUniqueCharacterMaterial(response.document?.document_id)
|
|
414
|
+
if (!projected.page.continuation.document_uid
|
|
415
|
+
&& (continuation.stage_code || continuation.record_name || continuation.material)
|
|
416
|
+
&& !uniqueStage && !uniqueRecord && !uniqueMaterial) {
|
|
417
|
+
projected.page.continuation = {
|
|
418
|
+
title: projected.primary.title, mode: 'document', line: continuation.line,
|
|
419
|
+
data_version: continuation.data_version,
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const sources = new Map()
|
|
424
|
+
for (const source of response.stream?.sources || []) {
|
|
425
|
+
if (source?.document_id) sources.set(String(source.document_id), { ...source })
|
|
426
|
+
}
|
|
427
|
+
for (const line of response.content?.format === 'lines' ? response.content.lines || [] : []) {
|
|
428
|
+
const documentId = String(line.document_id || response.document?.document_id || '')
|
|
429
|
+
if (!documentId) continue
|
|
430
|
+
const location = store.documents.get(documentId)?.document || {}
|
|
431
|
+
const current = sources.get(documentId) || {
|
|
432
|
+
document_id: documentId,
|
|
433
|
+
document_uid: documentUid(documentId),
|
|
434
|
+
title: String(line.document_title || naturalDocumentTitle(location) || projected.primary?.title || ''),
|
|
435
|
+
line_start: Number(line.line_number), line_end: Number(line.line_number),
|
|
436
|
+
}
|
|
437
|
+
current.line_start = Math.min(current.line_start, Number(line.line_number))
|
|
438
|
+
current.line_end = Math.max(current.line_end, Number(line.line_number))
|
|
439
|
+
sources.set(documentId, current)
|
|
440
|
+
}
|
|
441
|
+
if (!sources.size && response.document?.document_id) {
|
|
442
|
+
sources.set(response.document.document_id, {
|
|
443
|
+
document_id: String(response.document.document_id),
|
|
444
|
+
document_uid: documentUid(response.document.document_id),
|
|
445
|
+
title: String(projected.primary?.title || naturalDocumentTitle(response.document)),
|
|
446
|
+
line_start: Number(projected.primary?.selection?.line_start || 1),
|
|
447
|
+
line_end: Number(projected.primary?.selection?.line_end || 1),
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
const allSources = [...sources.values()]
|
|
451
|
+
return { ...projected, presentation: {
|
|
452
|
+
document_id: String(response.document?.document_id ?? ''),
|
|
453
|
+
data_version: String(response.data_version ?? ''),
|
|
454
|
+
sources: allSources.slice(0, 128),
|
|
455
|
+
sources_truncated: allSources.length > 128,
|
|
456
|
+
} }
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** DSH 持久展示元数据:浏览器证据卡只依赖该有界投影。 */
|
|
460
|
+
function readPresentationMeta(_args, value) {
|
|
461
|
+
return {
|
|
462
|
+
kind: 'prts-corpus-read-v1',
|
|
463
|
+
locator: { document_id: value.presentation.document_id },
|
|
464
|
+
data_version: value.presentation.data_version,
|
|
465
|
+
title: value.primary.title,
|
|
466
|
+
line_start: value.primary.selection.line_start,
|
|
467
|
+
line_end: value.primary.selection.line_end,
|
|
468
|
+
sources: value.presentation.sources,
|
|
469
|
+
sources_truncated: value.presentation.sources_truncated,
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const TIMELINE_PARAMETERS = {
|
|
474
|
+
type: 'object', additionalProperties: false,
|
|
475
|
+
properties: {
|
|
476
|
+
query: { type: 'string', description: '可选的事件正文短语(≤200 字符)。人物应优先放入 entity_names 以自动展开别名' },
|
|
477
|
+
activity_names: { type: 'array', items: { type: 'string' }, description: '活动展示名,例如“孤星”(≤20 项)' },
|
|
478
|
+
entity_names: { type: 'array', items: { type: 'string' }, description: '角色或实体展示名;工具会用别名图鉴自动裂变后检索(≤20 项)' },
|
|
479
|
+
year_start: { type: 'integer', description: '起始年份(含);可单独使用' },
|
|
480
|
+
year_end: { type: 'integer', description: '结束年份(含);可单独使用' },
|
|
481
|
+
source_marker: { type: 'string', description: '反查模式:原样复制时间线结果方括号内的年表出处标记(年表出处:tle_ 开头)' },
|
|
482
|
+
max_results: { type: 'integer', description: '最多返回事件数,默认 20,上限 100' },
|
|
483
|
+
},
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const CLOUD_SEARCH_PARAMETERS = {
|
|
487
|
+
type: 'object', additionalProperties: false, required: ['query'],
|
|
488
|
+
properties: {
|
|
489
|
+
query: { type: 'string', description: '改写为语义完整、适合向量检索的自然语言' },
|
|
490
|
+
games: { type: 'array', items: { type: 'string', enum: ['arknights', 'endfield'] },
|
|
491
|
+
description: '可选游戏范围;省略时同一次调用同时检索明日方舟与终末地' },
|
|
492
|
+
depth: { type: 'string', enum: ['fast', 'standard', 'deep'],
|
|
493
|
+
description: '兼容字段;不会改变云端主站的检索路由。回答深度由 Agent 自己的运行模式控制,通常省略' },
|
|
494
|
+
evidence_policy: { type: 'string', enum: ['mixed', 'original_only'],
|
|
495
|
+
description: 'mixed=完整复用 PRTS.chat 主站检索、审核与 Cleaner;original_only=只运行官方剧情原文向量路线' },
|
|
496
|
+
options: {
|
|
497
|
+
type: 'object', additionalProperties: false,
|
|
498
|
+
description: '通常省略。只有用户记得一句原文大意且措辞可能不准时,选择官方剧情单句向量路线',
|
|
499
|
+
properties: {
|
|
500
|
+
search_intent: {
|
|
501
|
+
type: 'string', enum: ['single_sentence_search'],
|
|
502
|
+
description: '只检索官方剧情单句向量表,并执行原有 LLM 验证;其他问题使用默认主站路线',
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const CLOUD_INSPECT_PARAMETERS = {
|
|
510
|
+
type: 'object', additionalProperties: false,
|
|
511
|
+
description: '查看最近一次云端检索的指定区段;request_id 由运行时自动注入',
|
|
512
|
+
properties: {
|
|
513
|
+
games: { type: 'array', items: { type: 'string', enum: ['arknights', 'endfield'] },
|
|
514
|
+
description: '只查看指定游戏的候选、来源或事件;省略时查看联合状态' },
|
|
515
|
+
section: { type: 'string', enum: ['summary', 'candidates', 'selected_sources', 'events', 'trace_steps', 'answer_context'],
|
|
516
|
+
description: '默认 summary' },
|
|
517
|
+
cursor: { type: 'integer', description: '分页游标:原样复制上次返回的 next_cursor' },
|
|
518
|
+
limit: { type: 'integer', description: '本页条数' },
|
|
519
|
+
channels: stringList('按渠道过滤(candidates/events)'),
|
|
520
|
+
query_variants: stringList('按查询变体过滤'),
|
|
521
|
+
retrievers: stringList('按检索器过滤'),
|
|
522
|
+
source_types: { type: 'array', items: { type: 'string' }, description: '按来源类型过滤' },
|
|
523
|
+
statuses: { type: 'array', items: { type: 'string' }, description: '按候选状态过滤' },
|
|
524
|
+
stages: stringList('按处理阶段过滤'),
|
|
525
|
+
candidate_ids: stringList('按候选 ID 精确过滤'),
|
|
526
|
+
evidence_ids: stringList('按证据 ID 精确过滤'),
|
|
527
|
+
event_sequence_from: { type: 'integer', description: '事件序号下界(含)' },
|
|
528
|
+
event_sequence_to: { type: 'integer', description: '事件序号上界(含)' },
|
|
529
|
+
event_time_from: { type: 'string', description: '事件时间下界' },
|
|
530
|
+
event_time_to: { type: 'string', description: '事件时间上界' },
|
|
531
|
+
content_mode: { type: 'string', enum: ['none', 'preview', 'full'],
|
|
532
|
+
description: 'none 只取结构字段;preview 返回受限正文;full 返回该页完整正文' },
|
|
533
|
+
content_max_chars: { type: 'integer', description: '正文字符上限' },
|
|
534
|
+
},
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** ---- 模型扁平参数 → 版本化 wire contract ---- */
|
|
538
|
+
|
|
539
|
+
/** corpus_read:将模型使用的自然定位器转换为版本化读取契约。 */
|
|
540
|
+
async function modelReadToContract(args = {}, store, enabledGames = ['arknights', 'endfield']) {
|
|
541
|
+
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
542
|
+
throw Object.assign(new Error('读取参数必须是对象'), { code: 'INVALID_REQUEST' })
|
|
543
|
+
}
|
|
544
|
+
let expectedDataVersion
|
|
545
|
+
if (args.data_version !== undefined) {
|
|
546
|
+
expectedDataVersion = String(args.data_version)
|
|
547
|
+
if (!DATA_VERSION_PATTERN.test(expectedDataVersion)) {
|
|
548
|
+
throw Object.assign(new Error('data_version 必须是续页结果给出的 64 位小写 SHA-256'),
|
|
549
|
+
{ code: 'INVALID_REQUEST' })
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const requireEnabled = (found) => {
|
|
553
|
+
if (!found) return
|
|
554
|
+
const game = documentGame(found.record.document)
|
|
555
|
+
if (!enabledGames.includes(game)) {
|
|
556
|
+
throw Object.assign(new Error(`当前未启用${game === 'endfield' ? '终末地' : '明日方舟'}资料`),
|
|
557
|
+
{ code: 'INVALID_REQUEST' })
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (args.cursor !== undefined) {
|
|
561
|
+
const allowed = new Set(['cursor', 'title', 'stage_code', 'story_part', 'mode',
|
|
562
|
+
'max_lines', 'max_chars', 'data_version'])
|
|
563
|
+
if (Object.keys(args).some((key) => !allowed.has(key))) {
|
|
564
|
+
throw Object.assign(new Error('旧 cursor 只能附带定位器、mode=document、max_lines/max_chars'),
|
|
565
|
+
{ code: 'INVALID_REQUEST' })
|
|
566
|
+
}
|
|
567
|
+
const restored = readContractFromCursor(args.cursor)
|
|
568
|
+
const restoredRecord = await store.getDocument(restored.locator.document_id)
|
|
569
|
+
if (!restoredRecord) {
|
|
570
|
+
throw Object.assign(new Error('cursor 指向的资料不存在'), { code: 'CURSOR_INVALID' })
|
|
571
|
+
}
|
|
572
|
+
requireEnabled(restoredRecord)
|
|
573
|
+
if (args.mode !== undefined && args.mode !== 'document') {
|
|
574
|
+
throw Object.assign(new Error('旧 cursor 只兼容 mode=document'), { code: 'INVALID_REQUEST' })
|
|
575
|
+
}
|
|
576
|
+
const hasCursorStageLocator = args.stage_code !== undefined || args.story_part !== undefined
|
|
577
|
+
if (args.title !== undefined && hasCursorStageLocator) {
|
|
578
|
+
throw Object.assign(new Error('title 与 stage_code/story_part 是两种定位方式,不能同时提供'),
|
|
579
|
+
{ code: 'INVALID_REQUEST' })
|
|
580
|
+
}
|
|
581
|
+
if (args.title !== undefined) {
|
|
582
|
+
const record = await store.getDocumentByTitle(String(args.title).trim())
|
|
583
|
+
if (!record || record.record.document.document_id !== restored.locator.document_id) {
|
|
584
|
+
throw Object.assign(new Error('title 与 cursor 指向的资料不一致;请使用 cursor 对应的完整标题'),
|
|
585
|
+
{ code: 'INVALID_REQUEST' })
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (hasCursorStageLocator) {
|
|
589
|
+
if (!enabledGames.includes('arknights')) {
|
|
590
|
+
throw Object.assign(new Error('关卡代号定位仅用于明日方舟,但当前未启用明日方舟资料'),
|
|
591
|
+
{ code: 'INVALID_REQUEST' })
|
|
592
|
+
}
|
|
593
|
+
const stageCode = publicStoryStageCode(args.stage_code, { relaxedInput: true })
|
|
594
|
+
const storyPart = args.story_part === undefined ? '' : publicStoryPart(args.story_part)
|
|
595
|
+
if (!stageCode || (args.story_part !== undefined && !storyPart)) {
|
|
596
|
+
throw Object.assign(new Error('旧 cursor 使用关卡定位时必须提供有效的 stage_code/story_part'),
|
|
597
|
+
{ code: 'INVALID_REQUEST' })
|
|
598
|
+
}
|
|
599
|
+
const record = await store.getDocumentByStoryStage(stageCode, storyPart)
|
|
600
|
+
if (!record || record.record.document.document_id !== restored.locator.document_id) {
|
|
601
|
+
throw Object.assign(new Error('stage_code/story_part 与 cursor 指向的资料不一致'),
|
|
602
|
+
{ code: 'INVALID_REQUEST' })
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return { ...restored,
|
|
606
|
+
...(expectedDataVersion ? { expected_data_version: expectedDataVersion } : {}), limits: {
|
|
607
|
+
...restored.limits,
|
|
608
|
+
...(args.max_lines !== undefined ? { max_lines: args.max_lines } : {}),
|
|
609
|
+
...(args.max_chars !== undefined ? { max_chars: args.max_chars } : {}),
|
|
610
|
+
} }
|
|
611
|
+
}
|
|
612
|
+
const allowed = new Set(['title', 'document_uid', 'stage_code', 'story_part', 'character_name', 'record_name',
|
|
613
|
+
'segment', 'material', 'game', 'activity_name', 'collection_name', 'content_types',
|
|
614
|
+
'line', 'position', 'mode', 'section', 'before', 'after', 'max_lines', 'max_chars',
|
|
615
|
+
'data_version'])
|
|
616
|
+
if (Object.keys(args).some((key) => !allowed.has(key))) {
|
|
617
|
+
throw Object.assign(new Error('corpus_read 包含不支持的参数'), { code: 'INVALID_REQUEST' })
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const hasTitle = args.title !== undefined
|
|
621
|
+
const hasDocumentUid = args.document_uid !== undefined
|
|
622
|
+
const hasStageLocator = args.stage_code !== undefined || args.story_part !== undefined
|
|
623
|
+
const hasRecordLocator = args.record_name !== undefined || args.segment !== undefined
|
|
624
|
+
const hasMaterialLocator = args.material !== undefined
|
|
625
|
+
const hasActivityLocator = args.activity_name !== undefined
|
|
626
|
+
const hasCollectionLocator = args.collection_name !== undefined
|
|
627
|
+
const locatorCount = [hasTitle, hasDocumentUid, hasStageLocator, hasRecordLocator, hasMaterialLocator,
|
|
628
|
+
hasActivityLocator, hasCollectionLocator].filter(Boolean).length
|
|
629
|
+
if (locatorCount !== 1) {
|
|
630
|
+
throw Object.assign(new Error(
|
|
631
|
+
'必须且只能提供一种定位方式:title、document_uid、stage_code、角色密录、角色资料、activity_name 或 collection_name;document_uid 会替代 title,不要同时提交二者',
|
|
632
|
+
), { code: 'INVALID_REQUEST' })
|
|
633
|
+
}
|
|
634
|
+
if (args.character_name !== undefined && !hasRecordLocator && !hasMaterialLocator) {
|
|
635
|
+
throw Object.assign(new Error('character_name 必须与 record_name 或 material 配合'),
|
|
636
|
+
{ code: 'INVALID_REQUEST' })
|
|
637
|
+
}
|
|
638
|
+
if (args.game !== undefined && !hasMaterialLocator) {
|
|
639
|
+
throw Object.assign(new Error('game 只用于角色资料定位'), { code: 'INVALID_REQUEST' })
|
|
640
|
+
}
|
|
641
|
+
if (hasMaterialLocator && !['profile', 'module', 'voice', 'skin', 'recruitment', 'potential']
|
|
642
|
+
.includes(args.material)) {
|
|
643
|
+
throw Object.assign(new Error('material 仅支持 profile/module/voice/skin/recruitment/potential'),
|
|
644
|
+
{ code: 'INVALID_REQUEST' })
|
|
645
|
+
}
|
|
646
|
+
if (args.game !== undefined && !['arknights', 'endfield'].includes(args.game)) {
|
|
647
|
+
throw Object.assign(new Error('game 仅支持 arknights 或 endfield'), { code: 'INVALID_REQUEST' })
|
|
648
|
+
}
|
|
649
|
+
const uidStreamMode = hasDocumentUid && (args.mode === 'activity' || args.mode === 'collection')
|
|
650
|
+
if (args.content_types !== undefined && !(hasCollectionLocator || (hasDocumentUid && args.mode === 'collection'))) {
|
|
651
|
+
throw Object.assign(new Error('content_types 只用于终末地 collection_name 连续阅读'),
|
|
652
|
+
{ code: 'INVALID_REQUEST' })
|
|
653
|
+
}
|
|
654
|
+
if (args.position !== undefined && !hasActivityLocator && !hasCollectionLocator && !uidStreamMode) {
|
|
655
|
+
throw Object.assign(new Error('position 只用于活动或任务集合续读'), { code: 'INVALID_REQUEST' })
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (hasActivityLocator || hasCollectionLocator || uidStreamMode) {
|
|
659
|
+
const expectedMode = hasActivityLocator ? 'activity'
|
|
660
|
+
: hasCollectionLocator ? 'collection' : args.mode
|
|
661
|
+
if (args.mode !== undefined && args.mode !== expectedMode) {
|
|
662
|
+
throw Object.assign(new Error(`${hasActivityLocator ? 'activity_name' : 'collection_name'} 必须使用 mode="${expectedMode}"`),
|
|
663
|
+
{ code: 'INVALID_REQUEST' })
|
|
664
|
+
}
|
|
665
|
+
if (args.line !== undefined || args.section !== undefined || args.before !== undefined
|
|
666
|
+
|| args.after !== undefined) {
|
|
667
|
+
throw Object.assign(new Error('活动/任务连续阅读不能与 line、section、before 或 after 同用'),
|
|
668
|
+
{ code: 'INVALID_REQUEST' })
|
|
669
|
+
}
|
|
670
|
+
const requiredGame = expectedMode === 'activity' ? 'arknights' : 'endfield'
|
|
671
|
+
if (!enabledGames.includes(requiredGame)) {
|
|
672
|
+
throw Object.assign(new Error(`当前未启用${requiredGame === 'arknights' ? '明日方舟' : '终末地'}资料`),
|
|
673
|
+
{ code: 'INVALID_REQUEST' })
|
|
674
|
+
}
|
|
675
|
+
const name = String(hasActivityLocator ? args.activity_name : args.collection_name || '').trim()
|
|
676
|
+
let anchorDocumentId = ''
|
|
677
|
+
if (hasDocumentUid) {
|
|
678
|
+
const anchor = await store.getDocumentByUid(String(args.document_uid || '').trim())
|
|
679
|
+
if (!anchor) throw Object.assign(new Error('document_uid 对应的资料不存在'),
|
|
680
|
+
{ code: 'DOCUMENT_NOT_FOUND' })
|
|
681
|
+
requireEnabled(anchor)
|
|
682
|
+
if (documentGame(anchor.record.document) !== requiredGame) {
|
|
683
|
+
throw Object.assign(new Error(`该 document_uid 不属于${requiredGame === 'endfield' ? '终末地任务' : '明日方舟活动'}资料`),
|
|
684
|
+
{ code: 'INVALID_REQUEST' })
|
|
685
|
+
}
|
|
686
|
+
anchorDocumentId = anchor.record.document.document_id
|
|
687
|
+
} else if (!name) {
|
|
688
|
+
throw Object.assign(new Error('活动/任务集合名称不能为空'), { code: 'INVALID_REQUEST' })
|
|
689
|
+
}
|
|
690
|
+
// 在进入执行层前先做一次歧义检查,让工具调用直接给出可操作的错误。
|
|
691
|
+
try {
|
|
692
|
+
const docs = hasActivityLocator
|
|
693
|
+
? store.activityStoryDocuments({ activityName: name })
|
|
694
|
+
: expectedMode === 'activity'
|
|
695
|
+
? store.activityStoryDocuments({ anchorDocumentId })
|
|
696
|
+
: store.endfieldCollectionDocuments({ collectionName: name, anchorDocumentId,
|
|
697
|
+
contentTypes: Array.isArray(args.content_types) ? args.content_types : [] })
|
|
698
|
+
if (!docs.length) throw Object.assign(new Error(
|
|
699
|
+
`本地资料包中找不到${expectedMode === 'activity' ? '活动' : '终末地集合'}${name ? `“${name}”` : ''}的剧情原文`,
|
|
700
|
+
), { code: 'DOCUMENT_NOT_FOUND' })
|
|
701
|
+
} catch (error) {
|
|
702
|
+
throw Object.assign(new Error(error.message), { code: error.code || 'DOCUMENT_AMBIGUOUS' })
|
|
703
|
+
}
|
|
704
|
+
return {
|
|
705
|
+
locator: hasDocumentUid ? { document_uid: String(args.document_uid).trim() }
|
|
706
|
+
: { [hasActivityLocator ? 'activity_name' : 'collection_name']: name },
|
|
707
|
+
selection: { mode: expectedMode, cursor: null,
|
|
708
|
+
...(args.position !== undefined ? { start_position: args.position } : {}),
|
|
709
|
+
...(args.content_types !== undefined ? { content_types: args.content_types } : {}) },
|
|
710
|
+
...(expectedDataVersion ? { expected_data_version: expectedDataVersion } : {}),
|
|
711
|
+
limits: { ...(args.max_lines !== undefined ? { max_lines: args.max_lines } : {}),
|
|
712
|
+
...(args.max_chars !== undefined ? { max_chars: args.max_chars } : {}) },
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
let locator
|
|
717
|
+
if (hasStageLocator) {
|
|
718
|
+
if (!enabledGames.includes('arknights')) {
|
|
719
|
+
throw Object.assign(new Error('关卡代号定位仅用于明日方舟,但当前未启用明日方舟资料'),
|
|
720
|
+
{ code: 'INVALID_REQUEST' })
|
|
721
|
+
}
|
|
722
|
+
const stageCode = publicStoryStageCode(args.stage_code, { relaxedInput: true })
|
|
723
|
+
const storyPart = args.story_part === undefined ? '' : publicStoryPart(args.story_part)
|
|
724
|
+
if (!stageCode) {
|
|
725
|
+
throw Object.assign(new Error('stage_code 必须是有效的明日方舟关卡代号,如 15-17、GT-3 或 BB-7'),
|
|
726
|
+
{ code: 'INVALID_REQUEST' })
|
|
727
|
+
}
|
|
728
|
+
if (args.story_part !== undefined && !storyPart) {
|
|
729
|
+
throw Object.assign(new Error('story_part 仅支持 before(行动前)、after(行动后)或 story(纯剧情/幕间)'),
|
|
730
|
+
{ code: 'INVALID_REQUEST' })
|
|
731
|
+
}
|
|
732
|
+
if (args.section !== undefined) {
|
|
733
|
+
throw Object.assign(new Error('关卡代号定位只读取官方剧情原文,不能与 Wiki section 同用'),
|
|
734
|
+
{ code: 'INVALID_REQUEST' })
|
|
735
|
+
}
|
|
736
|
+
let record
|
|
737
|
+
try {
|
|
738
|
+
record = await store.getDocumentByStoryStage(stageCode, storyPart)
|
|
739
|
+
} catch (error) {
|
|
740
|
+
throw Object.assign(new Error(error.message),
|
|
741
|
+
{ code: error.code || 'DOCUMENT_AMBIGUOUS' })
|
|
742
|
+
}
|
|
743
|
+
if (!record) {
|
|
744
|
+
const partLabel = storyPart
|
|
745
|
+
? `的${{ before: '行动前剧情', after: '行动后剧情', story: '纯剧情/幕间' }[storyPart]}` : '剧情'
|
|
746
|
+
throw Object.assign(new Error(
|
|
747
|
+
`本地资料包中找不到关卡 ${stageCode}${partLabel}`,
|
|
748
|
+
), { code: 'DOCUMENT_NOT_FOUND' })
|
|
749
|
+
}
|
|
750
|
+
requireEnabled(record)
|
|
751
|
+
locator = { document_id: record.record.document.document_id }
|
|
752
|
+
} else if (hasRecordLocator) {
|
|
753
|
+
if (!enabledGames.includes('arknights')) {
|
|
754
|
+
throw Object.assign(new Error('干员密录定位仅用于明日方舟,但当前未启用明日方舟资料'),
|
|
755
|
+
{ code: 'INVALID_REQUEST' })
|
|
756
|
+
}
|
|
757
|
+
const characterName = String(args.character_name ?? '').trim()
|
|
758
|
+
const recordName = String(args.record_name ?? '').trim()
|
|
759
|
+
if (!characterName || !recordName) {
|
|
760
|
+
throw Object.assign(new Error('干员密录定位必须同时提供 character_name 和 record_name'),
|
|
761
|
+
{ code: 'INVALID_REQUEST' })
|
|
762
|
+
}
|
|
763
|
+
if (args.segment !== undefined && (!Number.isInteger(args.segment) || args.segment < 1)) {
|
|
764
|
+
throw Object.assign(new Error('segment 必须是从 1 开始的整数'), { code: 'INVALID_REQUEST' })
|
|
765
|
+
}
|
|
766
|
+
let record
|
|
767
|
+
try {
|
|
768
|
+
record = await store.getOperatorRecord(characterName, recordName, args.segment)
|
|
769
|
+
} catch (error) {
|
|
770
|
+
throw Object.assign(new Error(error.message), { code: error.code || 'DOCUMENT_AMBIGUOUS' })
|
|
771
|
+
}
|
|
772
|
+
if (!record) throw Object.assign(new Error(
|
|
773
|
+
`本地资料包中找不到干员“${characterName}”的密录“${recordName}”${args.segment ? `第 ${args.segment} 段` : ''}`,
|
|
774
|
+
), { code: 'DOCUMENT_NOT_FOUND' })
|
|
775
|
+
locator = { document_id: record.record.document.document_id }
|
|
776
|
+
} else if (hasMaterialLocator) {
|
|
777
|
+
const characterName = String(args.character_name ?? '').trim()
|
|
778
|
+
if (!characterName) throw Object.assign(new Error('角色资料定位必须提供 character_name'),
|
|
779
|
+
{ code: 'INVALID_REQUEST' })
|
|
780
|
+
const requestedGames = args.game ? [args.game] : enabledGames
|
|
781
|
+
if (requestedGames.some((game) => !enabledGames.includes(game))) {
|
|
782
|
+
throw Object.assign(new Error('指定的角色资料模块当前未启用'), { code: 'INVALID_REQUEST' })
|
|
783
|
+
}
|
|
784
|
+
let record
|
|
785
|
+
try {
|
|
786
|
+
record = await store.getCharacterMaterial(characterName, args.material, requestedGames)
|
|
787
|
+
} catch (error) {
|
|
788
|
+
throw Object.assign(new Error(error.message), { code: error.code || 'DOCUMENT_AMBIGUOUS' })
|
|
789
|
+
}
|
|
790
|
+
if (!record) throw Object.assign(new Error(
|
|
791
|
+
`本地资料包中找不到角色“${characterName}”的 ${args.material} 资料`,
|
|
792
|
+
), { code: 'DOCUMENT_NOT_FOUND' })
|
|
793
|
+
requireEnabled(record)
|
|
794
|
+
locator = { document_id: record.record.document.document_id }
|
|
795
|
+
} else if (hasDocumentUid) {
|
|
796
|
+
const uid = String(args.document_uid || '').trim()
|
|
797
|
+
const record = await store.getDocumentByUid(uid)
|
|
798
|
+
if (!record) throw Object.assign(new Error(`本地资料包中找不到 document_uid=${uid}`),
|
|
799
|
+
{ code: 'DOCUMENT_NOT_FOUND' })
|
|
800
|
+
requireEnabled(record)
|
|
801
|
+
locator = { document_uid: uid }
|
|
802
|
+
} else {
|
|
803
|
+
const title = String(args.title ?? '').trim()
|
|
804
|
+
if (!title) throw Object.assign(new Error('title 不能为空'), { code: 'INVALID_REQUEST' })
|
|
805
|
+
const record = await store.getDocumentByTitle(title)
|
|
806
|
+
requireEnabled(record)
|
|
807
|
+
locator = { display_title: title }
|
|
808
|
+
}
|
|
809
|
+
const section = String(args.section ?? '').trim()
|
|
810
|
+
if (section && !hasTitle) {
|
|
811
|
+
throw Object.assign(new Error('section 只能与 title 定位器一起使用'), { code: 'INVALID_REQUEST' })
|
|
812
|
+
}
|
|
813
|
+
const naturalDocumentLocator = hasDocumentUid || hasStageLocator || hasRecordLocator || hasMaterialLocator
|
|
814
|
+
const mode = args.mode || (section ? 'section' : Number.isInteger(args.line) ? 'around'
|
|
815
|
+
: naturalDocumentLocator ? 'document' : '')
|
|
816
|
+
if (!mode) throw Object.assign(new Error(
|
|
817
|
+
'请提供 line、section 或 mode="document";max_lines/max_chars 只限制输出量,不能代替读取方式',
|
|
818
|
+
), { code: 'INVALID_REQUEST' })
|
|
819
|
+
if (!['around', 'section', 'document'].includes(mode)) {
|
|
820
|
+
throw Object.assign(new Error('mode 仅支持 document;line/section 会自动选择模式'),
|
|
821
|
+
{ code: 'INVALID_REQUEST' })
|
|
822
|
+
}
|
|
823
|
+
if (mode === 'around' && !Number.isInteger(args.line)) {
|
|
824
|
+
throw Object.assign(new Error('around 模式必须提供整数 line'), { code: 'INVALID_REQUEST' })
|
|
825
|
+
}
|
|
826
|
+
if (mode === 'section' && !section) {
|
|
827
|
+
throw Object.assign(new Error('section 模式必须提供 section'), { code: 'INVALID_REQUEST' })
|
|
828
|
+
}
|
|
829
|
+
if (section && mode !== 'section') {
|
|
830
|
+
throw Object.assign(new Error('section 只能与 mode=section 一起使用'), { code: 'INVALID_REQUEST' })
|
|
831
|
+
}
|
|
832
|
+
const selection = mode === 'document'
|
|
833
|
+
? { mode, cursor: null, ...(args.line !== undefined ? { start_line: args.line } : {}) }
|
|
834
|
+
: mode === 'section'
|
|
835
|
+
? { mode, section }
|
|
836
|
+
: { mode: 'around', center_line: args.line,
|
|
837
|
+
...(args.before !== undefined ? { before_lines: args.before } : {}),
|
|
838
|
+
...(args.after !== undefined ? { after_lines: args.after } : {}) }
|
|
839
|
+
return {
|
|
840
|
+
locator, selection,
|
|
841
|
+
...(expectedDataVersion ? { expected_data_version: expectedDataVersion } : {}),
|
|
842
|
+
limits: { ...(args.max_lines !== undefined ? { max_lines: args.max_lines } : {}),
|
|
843
|
+
...(args.max_chars !== undefined ? { max_chars: args.max_chars } : {}) },
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/** ---- 云端渲染(output.render 用,压缩为模型可读文本) ---- */
|
|
848
|
+
|
|
849
|
+
function renderCloudSearch(_args, value) {
|
|
850
|
+
if (value.status === 'error') {
|
|
851
|
+
return [{ type: 'text', text: `[cloud_search:error] ${value.error.code}: ${value.error.message}` }]
|
|
852
|
+
}
|
|
853
|
+
return [{ type: 'text', text: projectCloudSearch(value) }]
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function renderCloudInspect(_args, value) {
|
|
857
|
+
if (value.status === 'error') {
|
|
858
|
+
return [{ type: 'text', text: `[cloud_inspect:error] ${value.error.code}: ${value.error.message}` }]
|
|
859
|
+
}
|
|
860
|
+
return [{ type: 'text', text: JSON.stringify(projectCloudInspect(value)) }]
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/** ---- 插件入口 ---- */
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
867
|
+
* @param {{
|
|
868
|
+
* releasesDir?: string,
|
|
869
|
+
* cacheShards?: number,
|
|
870
|
+
* uiSkin?: 'harness' | 'prts-agent',
|
|
871
|
+
* download?: { order?: ('modelscope'|'site')[], siteBaseUrl?: string },
|
|
872
|
+
* cloud?: { baseUrl?: string, game?: 'arknights' | 'endfield', userId?: string, token?: string, timeoutMs?: number, maxResponseBytes?: number },
|
|
873
|
+
* }} [config]
|
|
874
|
+
*/
|
|
875
|
+
export async function apply(ctx, config = {}) {
|
|
876
|
+
// Cordis 传空配置时可能是 null 而非 undefined,默认参数不生效,需兜底
|
|
877
|
+
config = config ?? {}
|
|
878
|
+
// 工具开关:host 常驻时只做资料管理(registerTools:false),PRTS 预设(模式)加载时才注册工具(true)
|
|
879
|
+
const enableTools = config.registerTools !== false
|
|
880
|
+
const configuredHome = process.env.DSH_HOME?.trim()
|
|
881
|
+
const dshHome = resolve(configuredHome || join(homedir(), '.dsh'))
|
|
882
|
+
const portableReleasesDir = process.env.PRTS_CORPUS_RELEASES_DIR?.trim()
|
|
883
|
+
const configuredReleasesDir = config.releasesDir
|
|
884
|
+
? (isAbsolute(config.releasesDir) ? config.releasesDir : resolve(process.cwd(), config.releasesDir))
|
|
885
|
+
: portableReleasesDir ? resolve(portableReleasesDir) : join(dshHome, 'prts-corpus', 'releases')
|
|
886
|
+
const releasesDir = await resolveSafeReleasesDirectory(configuredReleasesDir, dshHome)
|
|
887
|
+
|
|
888
|
+
// 共享状态:三层配置(默认 ← patch ← $DSH_HOME/prts-corpus.json),设置页可运行时改
|
|
889
|
+
const configPath = join(dshHome, 'prts-corpus.json')
|
|
890
|
+
const shared = createSharedState({ patchConfig: config, configPath, releasesDir })
|
|
891
|
+
await shared.loadConfig()
|
|
892
|
+
|
|
893
|
+
// 云端匿名会话的持久 client id:跨重启稳定,服务端据此统计独立 DSH 用户。
|
|
894
|
+
// 读不出来(只读 HOME 等)不致命——退回每次加载随机的旧行为。
|
|
895
|
+
let cloudClientId = null
|
|
896
|
+
try {
|
|
897
|
+
cloudClientId = await readOrCreateClientId(join(dshHome, 'prts-corpus', 'client-id'))
|
|
898
|
+
} catch (error) {
|
|
899
|
+
ctx.logger?.warn?.(`prts-corpus: client-id 持久化失败(独立用户统计将不准确): ${error?.message ?? error}`)
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
shared.cloudClientId = cloudClientId
|
|
903
|
+
|
|
904
|
+
let storeEntry = storesByDirectory.get(releasesDir)
|
|
905
|
+
if (!storeEntry) {
|
|
906
|
+
storeEntry = {
|
|
907
|
+
store: new CorpusStore({ releasesDir, cacheShards: shared.effective().cacheShards,
|
|
908
|
+
cursorSecretPath: join(dshHome, 'prts-corpus', 'cursor-secret.bin') }),
|
|
909
|
+
watcher: null,
|
|
910
|
+
watchRefs: 0,
|
|
911
|
+
releaseTimer: null,
|
|
912
|
+
refs: 0,
|
|
913
|
+
}
|
|
914
|
+
storesByDirectory.set(releasesDir, storeEntry)
|
|
915
|
+
}
|
|
916
|
+
storeEntry.refs += 1
|
|
917
|
+
ctx.effect(() => () => {
|
|
918
|
+
storeEntry.refs -= 1
|
|
919
|
+
if (storeEntry.refs === 0) storesByDirectory.delete(releasesDir)
|
|
920
|
+
}, 'prts-corpus: shared store')
|
|
921
|
+
const store = storeEntry.store
|
|
922
|
+
shared.store = store
|
|
923
|
+
// 若本地已有资料,插件装载后立即在后台建立目录与实体索引。初始化仍然
|
|
924
|
+
// 幂等且不阻塞 Cordis ready;用户第一问通常可以直接复用已完成的预热。
|
|
925
|
+
// 没装资料时保持安静,工具被调用后再返回统一的安装提示。
|
|
926
|
+
let prewarmCancelled = false
|
|
927
|
+
const prewarmHandle = config.registerUi === false ? null : setImmediate(async () => {
|
|
928
|
+
const started = Date.now()
|
|
929
|
+
try {
|
|
930
|
+
await readCurrentReleasePointer(releasesDir)
|
|
931
|
+
if (prewarmCancelled) return
|
|
932
|
+
await prepareEntityRecognition(store)
|
|
933
|
+
if (!prewarmCancelled) {
|
|
934
|
+
ctx.logger?.info?.(`prts-corpus: local indexes prewarmed in ${Date.now() - started}ms`)
|
|
935
|
+
}
|
|
936
|
+
} catch (error) {
|
|
937
|
+
if (!prewarmCancelled && error?.code !== 'ENOENT') {
|
|
938
|
+
ctx.logger?.warn?.(`prts-corpus: 后台索引预热失败: ${error?.message ?? error}`)
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
})
|
|
942
|
+
ctx.effect(() => () => { prewarmCancelled = true; if (prewarmHandle) clearImmediate(prewarmHandle) },
|
|
943
|
+
'prts-corpus: background index prewarm')
|
|
944
|
+
const evidenceStates = createEvidenceStateRegistry()
|
|
945
|
+
// DSH 自身不会以同一 callId 重试工具执行;此缓存只防御第三方 tools/execute
|
|
946
|
+
// 策略在同一 exec 内的重放,避免重复扫描语料。完整跨重启幂等仍由未来共享
|
|
947
|
+
// 调用存储承担,避免把检索正文写入配置目录。
|
|
948
|
+
|
|
949
|
+
const stopWatching = await shared.watchConfig(ctx.logger)
|
|
950
|
+
ctx.effect(() => stopWatching, 'prts-corpus: config watch')
|
|
951
|
+
ctx.effect(() => shared.subscribe((effective) => {
|
|
952
|
+
store.cacheShards = effective.cacheShards
|
|
953
|
+
}), 'prts-corpus: store config')
|
|
954
|
+
|
|
955
|
+
const mountTools = (toolCtx) => {
|
|
956
|
+
const tools = toolCtx.tools
|
|
957
|
+
installArchivePtcReceipts(toolCtx, readPresentationMeta)
|
|
958
|
+
// Host 的 preset 准入已经完成初始化;工具挂载不再触发下载或全量扫描。
|
|
959
|
+
applyEntityRecognition(toolCtx, store, shared)
|
|
960
|
+
tools.register({
|
|
961
|
+
name: 'corpus_search',
|
|
962
|
+
description: SEARCH_DESCRIPTION,
|
|
963
|
+
parameters: SEARCH_PARAMETERS,
|
|
964
|
+
output: { schema: SEARCH_OUTPUT_SCHEMA, render: renderSearch, presentationMeta: archivePresentationMeta },
|
|
965
|
+
timeoutMs: 120_000,
|
|
966
|
+
isConcurrencySafe: () => true,
|
|
967
|
+
presentCall: (args) => searchCallView('PRTS 本地资料', args,
|
|
968
|
+
shared.effective().enabledGames),
|
|
969
|
+
execute: async (args, exec) => {
|
|
970
|
+
await requireLocalCorpus(store)
|
|
971
|
+
const snapshot = corpusVersionSnapshot(store)
|
|
972
|
+
const evidenceState = evidenceStates.forExecution(exec, store.dataVersion)
|
|
973
|
+
const completedSearchCalls = evidenceState.completedSearchCalls
|
|
974
|
+
const callId = String(exec?.callId || '')
|
|
975
|
+
// 先做对象校验再做幂等哈希:JSON.stringify(undefined) 返回 undefined,
|
|
976
|
+
// 直接 update 会抛 TypeError,绕过本工具的 INVALID_REQUEST 错误通道。
|
|
977
|
+
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
978
|
+
throw Object.assign(new Error('搜索参数必须是对象'),
|
|
979
|
+
{ code: 'INVALID_REQUEST', retryable: false })
|
|
980
|
+
}
|
|
981
|
+
const enabledGames = shared.effective().enabledGames
|
|
982
|
+
let scopedArgs = args
|
|
983
|
+
if (args.cursor == null) {
|
|
984
|
+
const requestedGames = Array.isArray(args.games) ? args.games : enabledGames
|
|
985
|
+
const games = requestedGames.filter((game) => enabledGames.includes(game))
|
|
986
|
+
if (!games.length) {
|
|
987
|
+
throw Object.assign(new Error('请求的游戏资料库当前未启用,请先在 PRTS 资料设置中勾选'),
|
|
988
|
+
{ code: 'INVALID_REQUEST', retryable: false })
|
|
989
|
+
}
|
|
990
|
+
scopedArgs = { ...args, games }
|
|
991
|
+
}
|
|
992
|
+
const requestHash = createHash('sha256').update(JSON.stringify(scopedArgs)).digest('hex')
|
|
993
|
+
if (callId && completedSearchCalls.has(callId)) {
|
|
994
|
+
const cached = completedSearchCalls.get(callId)
|
|
995
|
+
if (cached.requestHash !== requestHash) return {
|
|
996
|
+
contract_version: 'prts-corpus-tools-v1', status: 'error', request_id: callId,
|
|
997
|
+
data_version: store.dataVersion ?? null,
|
|
998
|
+
error: { code: 'INVALID_REQUEST', message: 'callId 已绑定到另一个搜索请求', retryable: false },
|
|
999
|
+
}
|
|
1000
|
+
assertCorpusVersion(store, snapshot)
|
|
1001
|
+
const replay = structuredClone(cached.response)
|
|
1002
|
+
return rememberArchivePresentation(replay, localArchivePresentation(store, replay, snapshot.dataVersion))
|
|
1003
|
+
}
|
|
1004
|
+
let response = await executeSearch(store, scopedArgs, { signal: exec?.signal,
|
|
1005
|
+
requestId: callId || undefined, allowedGames: enabledGames })
|
|
1006
|
+
assertCorpusVersion(store, snapshot)
|
|
1007
|
+
if (response?.error) {
|
|
1008
|
+
throw Object.assign(new Error(response.error.message),
|
|
1009
|
+
{ code: response.error.code, retryable: response.error.retryable })
|
|
1010
|
+
}
|
|
1011
|
+
response = await attachRetravelerRelations(store, response, scopedArgs, enabledGames)
|
|
1012
|
+
assertCorpusVersion(store, snapshot)
|
|
1013
|
+
if (callId) {
|
|
1014
|
+
completedSearchCalls.set(callId, { requestHash, response: structuredClone(response) })
|
|
1015
|
+
if (completedSearchCalls.size > 256) completedSearchCalls.delete(completedSearchCalls.keys().next().value)
|
|
1016
|
+
}
|
|
1017
|
+
rememberSearchCandidates(evidenceState, response)
|
|
1018
|
+
return rememberArchivePresentation(response, localArchivePresentation(store, response, snapshot.dataVersion))
|
|
1019
|
+
},
|
|
1020
|
+
})
|
|
1021
|
+
|
|
1022
|
+
tools.register({
|
|
1023
|
+
name: 'corpus_read',
|
|
1024
|
+
description: READ_DESCRIPTION,
|
|
1025
|
+
parameters: READ_PARAMETERS,
|
|
1026
|
+
output: { schema: READ_OUTPUT_SCHEMA, render: renderRead,
|
|
1027
|
+
presentationMeta: readPresentationMeta },
|
|
1028
|
+
timeoutMs: 120_000,
|
|
1029
|
+
// 原文覆盖去重依赖前一个 tool/result 已进入模型可见 surface。并行
|
|
1030
|
+
// 读取会让同一步的每个调用都误判为首次读取,因此必须由 Harness 按
|
|
1031
|
+
// 模型调用顺序独占执行并逐个提交结果。
|
|
1032
|
+
isConcurrencySafe: () => false,
|
|
1033
|
+
presentCall: (args) => readCallView(args, shared.effective().enabledGames),
|
|
1034
|
+
execute: async (args, exec) => {
|
|
1035
|
+
await requireLocalCorpus(store)
|
|
1036
|
+
const snapshot = corpusVersionSnapshot(store)
|
|
1037
|
+
const evidenceState = evidenceStates.forExecution(exec, store.dataVersion)
|
|
1038
|
+
let contract
|
|
1039
|
+
try {
|
|
1040
|
+
contract = await modelReadToContract(args, store, shared.effective().enabledGames)
|
|
1041
|
+
assertCorpusVersion(store, snapshot)
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
assertCorpusVersion(store, snapshot)
|
|
1044
|
+
throw Object.assign(new Error(error.message),
|
|
1045
|
+
{ code: error.code || 'INVALID_REQUEST', retryable: error.retryable ?? false })
|
|
1046
|
+
}
|
|
1047
|
+
if (contract.expected_data_version !== undefined
|
|
1048
|
+
&& contract.expected_data_version !== store.dataVersion) {
|
|
1049
|
+
throw Object.assign(new Error('续页所属资料版本与当前激活版本不一致,请重新读取'),
|
|
1050
|
+
{ code: 'PACKAGE_VERSION_MISMATCH', retryable: true })
|
|
1051
|
+
}
|
|
1052
|
+
// 证据覆盖按 Harness Agent 隔离;模型不感知 intent_id。
|
|
1053
|
+
contract.intent_id = evidenceState.intentId
|
|
1054
|
+
// 缓存复用也必须经过标准参数校验,不能绕过本次预算和行范围约束。
|
|
1055
|
+
normalizeReadRequest(contract)
|
|
1056
|
+
const requested = await resolveReadWindow(store, contract)
|
|
1057
|
+
assertCorpusVersion(store, snapshot)
|
|
1058
|
+
const visibleResults = visibleToolResults(exec?.agent)
|
|
1059
|
+
if (coveredRead(evidenceState, requested, visibleResults)) {
|
|
1060
|
+
const replay = replayCoveredRead(evidenceState, requested, contract)
|
|
1061
|
+
if (replay) {
|
|
1062
|
+
assertCorpusVersion(store, snapshot)
|
|
1063
|
+
return projectReadToolValue(replay, store)
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
const response = await executeRead(store, contract, { signal: exec?.signal })
|
|
1067
|
+
assertCorpusVersion(store, snapshot)
|
|
1068
|
+
if (response.status !== 'ok') throw Object.assign(new Error(response.error.message), response.error)
|
|
1069
|
+
const coveragePlan = requested ? planReadCoverage(evidenceState, {
|
|
1070
|
+
documentId: response.document.document_id,
|
|
1071
|
+
lineStart: response.selection.line_start, lineEnd: response.selection.line_end,
|
|
1072
|
+
}, visibleResults) : null
|
|
1073
|
+
const delivered = reuseReadCoverage(response, coveragePlan)
|
|
1074
|
+
rememberRead(evidenceState, delivered, { callId: String(exec?.callId || ''), store })
|
|
1075
|
+
return projectReadToolValue(delivered, store)
|
|
1076
|
+
},
|
|
1077
|
+
})
|
|
1078
|
+
|
|
1079
|
+
tools.register({
|
|
1080
|
+
name: 'corpus_i18n',
|
|
1081
|
+
description: I18N_DESCRIPTION,
|
|
1082
|
+
parameters: I18N_PARAMETERS,
|
|
1083
|
+
output: { schema: I18N_OUTPUT_SCHEMA, render: renderI18n },
|
|
1084
|
+
timeoutMs: 60_000,
|
|
1085
|
+
isConcurrencySafe: () => true,
|
|
1086
|
+
presentCall: (args) => ({ card: 'generic', kind: 'read',
|
|
1087
|
+
title: `查询终末地官方多语言 · ${compactToolTitle(args?.query || args?.title || '原文')} · ${(Array.isArray(args?.languages) ? args.languages : []).join('/')}` }),
|
|
1088
|
+
execute: async (args, exec) => {
|
|
1089
|
+
await requireLocalCorpus(store)
|
|
1090
|
+
return executeI18n(store, args, { signal: exec?.signal,
|
|
1091
|
+
enabledGames: shared.effective().enabledGames })
|
|
1092
|
+
},
|
|
1093
|
+
})
|
|
1094
|
+
|
|
1095
|
+
tools.register({
|
|
1096
|
+
name: 'timeline_search',
|
|
1097
|
+
description: TIMELINE_DESCRIPTION,
|
|
1098
|
+
parameters: TIMELINE_PARAMETERS,
|
|
1099
|
+
output: { schema: {}, render: renderTimeline, presentationMeta: archivePresentationMeta },
|
|
1100
|
+
timeoutMs: 60_000,
|
|
1101
|
+
isConcurrencySafe: () => true,
|
|
1102
|
+
presentCall: (args) => {
|
|
1103
|
+
const query = compactToolTitle(args?.query || args?.entity_names?.join('、')
|
|
1104
|
+
|| args?.source_marker)
|
|
1105
|
+
return { card: 'generic', title: `搜索 PRTS 本地年表 · 明日方舟${query ? ` · ${query}` : ''}`,
|
|
1106
|
+
kind: 'search', ...(query ? { rawInput: query } : {}) }
|
|
1107
|
+
},
|
|
1108
|
+
execute: async (args, exec) => {
|
|
1109
|
+
await requireLocalCorpus(store)
|
|
1110
|
+
if (!shared.effective().enabledGames.includes('arknights')) {
|
|
1111
|
+
throw Object.assign(new Error('明日方舟资料当前未启用'), { code: 'INVALID_REQUEST' })
|
|
1112
|
+
}
|
|
1113
|
+
const snapshot = corpusVersionSnapshot(store)
|
|
1114
|
+
const response = await executeTimelineSearch(store, args, { signal: exec?.signal })
|
|
1115
|
+
assertCorpusVersion(store, snapshot)
|
|
1116
|
+
return rememberArchivePresentation(response, timelineArchivePresentation(response))
|
|
1117
|
+
},
|
|
1118
|
+
})
|
|
1119
|
+
|
|
1120
|
+
// 云端工具:按生效配置注册,配置变更(设置页)时 dispose + 热重建
|
|
1121
|
+
const cloudDisposers = []
|
|
1122
|
+
const rebuildCloud = () => {
|
|
1123
|
+
for (const dispose of cloudDisposers.splice(0)) {
|
|
1124
|
+
try { dispose() } catch { /* 卸载旧注册 */ }
|
|
1125
|
+
}
|
|
1126
|
+
const c = shared.effective()
|
|
1127
|
+
if (!c.cloudEnabled || !c.cloudBaseUrl) return false
|
|
1128
|
+
const cloudClients = createAgentCloudClientRegistry(() => {
|
|
1129
|
+
const tokenProvider = c.cloudToken
|
|
1130
|
+
? new StaticTokenProvider(c.cloudToken)
|
|
1131
|
+
: new AnonymousSessionProvider({ baseUrl: c.cloudBaseUrl,
|
|
1132
|
+
userId: c.cloudUserId || cloudClientId || undefined,
|
|
1133
|
+
timeoutMs: c.cloudTimeoutMs })
|
|
1134
|
+
const cloudGame = c.enabledGames.length === 2 ? 'all' : c.enabledGames[0]
|
|
1135
|
+
return new CloudRetrievalClient({
|
|
1136
|
+
baseUrl: c.cloudBaseUrl, tokenProvider, game: cloudGame,
|
|
1137
|
+
timeoutMs: c.cloudTimeoutMs, maxResponseBytes: c.cloudMaxResponseBytes,
|
|
1138
|
+
})
|
|
1139
|
+
})
|
|
1140
|
+
const searchDispose = tools.register({
|
|
1141
|
+
name: 'cloud_search',
|
|
1142
|
+
description: CLOUD_SEARCH_DESCRIPTION,
|
|
1143
|
+
parameters: CLOUD_SEARCH_PARAMETERS,
|
|
1144
|
+
output: { schema: {}, render: renderCloudSearch, presentationMeta: archivePresentationMeta },
|
|
1145
|
+
timeoutMs: 180_000,
|
|
1146
|
+
isConcurrencySafe: () => true,
|
|
1147
|
+
presentCall: (args) => searchCallView('PRTS 云端资料', args, c.enabledGames),
|
|
1148
|
+
execute: async (args, exec) => {
|
|
1149
|
+
try {
|
|
1150
|
+
const evidenceState = evidenceStates.forExecution(exec, store.dataVersion)
|
|
1151
|
+
const cloud = cloudClients.forExecution(exec)
|
|
1152
|
+
await cloud.capabilities({ signal: exec?.signal })
|
|
1153
|
+
const requestedGames = Array.isArray(args.games) ? args.games : c.enabledGames
|
|
1154
|
+
const games = requestedGames.filter((game) => c.enabledGames.includes(game))
|
|
1155
|
+
if (!games.length) throw Object.assign(
|
|
1156
|
+
new Error('请求的游戏资料库当前未启用,请先在 PRTS 资料设置中勾选'),
|
|
1157
|
+
{ code: 'INVALID_REQUEST', retryable: false })
|
|
1158
|
+
const payload = { ...args, games, intent_id: evidenceState.cloudIntentId }
|
|
1159
|
+
const response = await cloud.search(payload, { signal: exec?.signal })
|
|
1160
|
+
const mapped = await attachLocalSourceMappings(store, response, { signal: exec?.signal })
|
|
1161
|
+
const dataVersion = store.dataVersion
|
|
1162
|
+
const enriched = await attachRetravelerRelations(store, mapped, args, c.enabledGames)
|
|
1163
|
+
rememberCloudMappings(evidenceState, enriched)
|
|
1164
|
+
return rememberArchivePresentation(enriched, cloudArchivePresentation(enriched, { dataVersion }))
|
|
1165
|
+
} catch (error) {
|
|
1166
|
+
return cloudErrorResponse(error)
|
|
1167
|
+
}
|
|
1168
|
+
},
|
|
1169
|
+
})
|
|
1170
|
+
if (typeof searchDispose === 'function') cloudDisposers.push(searchDispose)
|
|
1171
|
+
|
|
1172
|
+
const inspectDispose = tools.register({
|
|
1173
|
+
name: 'cloud_inspect',
|
|
1174
|
+
description: CLOUD_INSPECT_DESCRIPTION,
|
|
1175
|
+
parameters: CLOUD_INSPECT_PARAMETERS,
|
|
1176
|
+
output: { schema: {}, render: renderCloudInspect, presentationMeta: archivePresentationMeta },
|
|
1177
|
+
timeoutMs: 120_000,
|
|
1178
|
+
isConcurrencySafe: () => true,
|
|
1179
|
+
presentCall: (args) => ({ card: 'generic',
|
|
1180
|
+
title: `查看 PRTS 云端检索详情 · ${presentedGames(args, c.enabledGames)}`,
|
|
1181
|
+
kind: 'read', rawInput: compactToolTitle(args?.section || 'summary') }),
|
|
1182
|
+
execute: async (args, exec) => {
|
|
1183
|
+
try {
|
|
1184
|
+
const evidenceState = evidenceStates.forExecution(exec, store.dataVersion)
|
|
1185
|
+
const cloud = cloudClients.forExecution(exec)
|
|
1186
|
+
await cloud.capabilities({ signal: exec?.signal })
|
|
1187
|
+
const payload = { ...args,
|
|
1188
|
+
...(args.request_id || !evidenceState.lastCloudRequestId
|
|
1189
|
+
? {} : { request_id: evidenceState.lastCloudRequestId }) }
|
|
1190
|
+
const response = await cloud.inspect(payload, { signal: exec?.signal })
|
|
1191
|
+
const mapped = await attachLocalSourceMappings(store, response, { signal: exec?.signal })
|
|
1192
|
+
rememberCloudMappings(evidenceState, mapped)
|
|
1193
|
+
return rememberArchivePresentation(mapped, cloudArchivePresentation(mapped, {
|
|
1194
|
+
inspect: true, dataVersion: store.dataVersion,
|
|
1195
|
+
}))
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
return cloudErrorResponse(error)
|
|
1198
|
+
}
|
|
1199
|
+
},
|
|
1200
|
+
})
|
|
1201
|
+
if (typeof inspectDispose === 'function') cloudDisposers.push(inspectDispose)
|
|
1202
|
+
|
|
1203
|
+
toolCtx.logger?.info?.(`prts-corpus: cloud tools enabled (baseUrl=${c.cloudBaseUrl}, games=${c.enabledGames.join(',')})`)
|
|
1204
|
+
return true
|
|
1205
|
+
}
|
|
1206
|
+
rebuildCloud()
|
|
1207
|
+
toolCtx.effect(() => shared.subscribe(rebuildCloud), 'prts-corpus: cloud config')
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// tools/systemPrompt/connection 都是可选部署能力;ctx.inject 会等待其出现并在其消失时
|
|
1211
|
+
// 自动卸载对应子树,避免“插件 ACTIVE 但永久漏注册”的启动顺序竞态。
|
|
1212
|
+
// Agent preset 的 standing mount 必须等工具子 fiber 完成注册后才能宣告就绪。
|
|
1213
|
+
// 不 await 会形成“Skill 已可见、工具仍缺席”的半挂载会话,且子 fiber 的
|
|
1214
|
+
// schema/注册异常也无法阻止该会话创建。
|
|
1215
|
+
await ctx.inject(['storageDomain'], async investigationCtx => {
|
|
1216
|
+
const handle = acquireInvestigationStore(investigationCtx.storageDomain)
|
|
1217
|
+
shared.investigations = handle.service
|
|
1218
|
+
investigationCtx.effect(() => () => { if (shared.investigations === handle.service) shared.investigations = null; return handle.release() }, 'prts: investigations')
|
|
1219
|
+
if (enableTools) await investigationCtx.inject(['tools', 'systemPrompt'], toolCtx => mountInvestigationTools(toolCtx, handle.service))
|
|
1220
|
+
})
|
|
1221
|
+
if (enableTools) await ctx.inject(['tools', 'systemPrompt'], mountTools)
|
|
1222
|
+
|
|
1223
|
+
if (enableTools) {
|
|
1224
|
+
await mkdir(releasesDir, { recursive: true })
|
|
1225
|
+
if (!storeEntry.watcher) {
|
|
1226
|
+
storeEntry.watcher = watchCurrentRelease(releasesDir, () => {
|
|
1227
|
+
if (storeEntry.releaseTimer) clearTimeout(storeEntry.releaseTimer)
|
|
1228
|
+
storeEntry.releaseTimer = setTimeout(async () => {
|
|
1229
|
+
storeEntry.releaseTimer = null
|
|
1230
|
+
try {
|
|
1231
|
+
const pointer = await readCurrentReleasePointer(releasesDir)
|
|
1232
|
+
if (store.loaded && store.releaseId === pointer.release_id) return
|
|
1233
|
+
} catch { /* 让 ready() 输出真实指针错误 */ }
|
|
1234
|
+
store.reset()
|
|
1235
|
+
prepareEntityRecognition(store).catch((error) => {
|
|
1236
|
+
ctx.logger?.warn?.(`prts-corpus: 版本热切换失败: ${error?.message ?? error}`)
|
|
1237
|
+
})
|
|
1238
|
+
}, 100)
|
|
1239
|
+
}, { onFallback: (error) => {
|
|
1240
|
+
ctx.logger?.warn?.(`prts-corpus: 原生资料监听资源不足(${error.code}),改为每秒检查当前版本`)
|
|
1241
|
+
} })
|
|
1242
|
+
}
|
|
1243
|
+
storeEntry.watchRefs += 1
|
|
1244
|
+
ctx.effect(() => () => {
|
|
1245
|
+
storeEntry.watchRefs -= 1
|
|
1246
|
+
if (storeEntry.watchRefs > 0) return
|
|
1247
|
+
if (storeEntry.releaseTimer) clearTimeout(storeEntry.releaseTimer)
|
|
1248
|
+
storeEntry.releaseTimer = null
|
|
1249
|
+
storeEntry.watcher?.close()
|
|
1250
|
+
storeEntry.watcher = null
|
|
1251
|
+
}, 'prts-corpus: release watch')
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// Web 与 Electron 共用 Connection 的 Fetch API;旧 Web 资源由 applyUi
|
|
1255
|
+
// 单独等待 webServer 挂载,Electron 无 HTTP 服务时也能管理资料和加载地图。
|
|
1256
|
+
// host 常驻(registerUi 缺省 true)注册 /api/prts-corpus + 设置 tab 数据源;
|
|
1257
|
+
// PRTS 预设(registerUi:false)只注册工具,避免与 host 重复注册同名前缀路由。
|
|
1258
|
+
if (config.registerUi !== false) {
|
|
1259
|
+
ctx.inject(['connection'], (webCtx) => { applyUi(webCtx, shared) })
|
|
1260
|
+
}
|
|
1261
|
+
}
|