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/lib/client.js
ADDED
|
@@ -0,0 +1,3912 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prts-terrarchive 浏览器宿主适配器(手写);莱茵资料馆单独编译并按需加载。
|
|
3
|
+
*
|
|
4
|
+
* 格式与 @deepseek-ai 官方 client 包的 lib/client.js 一致:
|
|
5
|
+
* window.__ModuleLoader__.load({ id, factory(require) { ... return module.exports } })
|
|
6
|
+
* react 由平台模块表注入。本插件只依赖 connection、slots 与 theme 服务(零 UI 库依赖,
|
|
7
|
+
* 组件用内联样式原生元素)。
|
|
8
|
+
*
|
|
9
|
+
* 注册:Settings → Plugins 里的「PRTS 语料」tab(settings.plugins.tab slot)。
|
|
10
|
+
* 数据:Host Connection 共享 Fetch 通道 /api/prts-corpus/rpc(Web / Electron)。
|
|
11
|
+
*/
|
|
12
|
+
window.__ModuleLoader__.load({
|
|
13
|
+
id: 'prts-terrarchive',
|
|
14
|
+
factory: (require) => {
|
|
15
|
+
const module = { exports: {} }
|
|
16
|
+
const exports = module.exports
|
|
17
|
+
const React = require('react')
|
|
18
|
+
const { useState, useEffect, useCallback, useMemo, useRef, createElement: h, Fragment } = React
|
|
19
|
+
|
|
20
|
+
let callApi = null
|
|
21
|
+
let themeRuntime = null
|
|
22
|
+
let removeSkinTokens = null
|
|
23
|
+
let activeSkin = 'harness'
|
|
24
|
+
let clientContext = null
|
|
25
|
+
let disposeAicLayout = null
|
|
26
|
+
let disposeScene = null
|
|
27
|
+
let syncAicLayout = () => {}
|
|
28
|
+
let sceneSnapshotModel = null
|
|
29
|
+
let notifySceneSnapshot = () => {}
|
|
30
|
+
let applyGeneration = 0
|
|
31
|
+
let skinOperationEpoch = 0
|
|
32
|
+
let skinApplyEpoch = 0
|
|
33
|
+
// Host -> browser 的皮肤快照共享同一代次。启动配置、设置页完整刷新和
|
|
34
|
+
// 状态轮询无论从哪个入口发出,都只能由最后开始的请求提交到本地。
|
|
35
|
+
let hostSkinSyncGeneration = 0
|
|
36
|
+
const pendingSkinOperations = new Set()
|
|
37
|
+
let skinSelectionOwner = null
|
|
38
|
+
let clientAbortController = null
|
|
39
|
+
const NOOP_STYLESHEET_TRANSACTION = Object.freeze({ commit() {}, rollback() {} })
|
|
40
|
+
let prepareSkinStylesheet = async () => NOOP_STYLESHEET_TRANSACTION
|
|
41
|
+
let skinStylesheetOwner = null
|
|
42
|
+
const SKIN_SOURCE = 'prts-terrarchive:prts-agent-skin'
|
|
43
|
+
|
|
44
|
+
function isHostSettingsOpen(doc = document) {
|
|
45
|
+
return Boolean(doc.querySelector('[data-shortcut-modal="settings"], '
|
|
46
|
+
+ '[data-slot="sidebar.settings"] [role="dialog"][aria-modal="true"], '
|
|
47
|
+
+ '[role="dialog"][aria-modal="true"]:has([data-slot="settings.header"])'))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function openHostSettings(ctx = clientContext, doc = document) {
|
|
51
|
+
if (isHostSettingsOpen(doc)) return true
|
|
52
|
+
// DSH 0.1.3/0.1.5 keeps settings state inside its mounted React component.
|
|
53
|
+
// Clicking its own trigger preserves that instance (including hidden sidebars).
|
|
54
|
+
const trigger = doc.querySelector('[data-slot="sidebar.settings"] button[aria-haspopup="dialog"]')
|
|
55
|
+
if (trigger && !trigger.disabled) {
|
|
56
|
+
trigger.click()
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
// DSH 0.1.7 replaces the trigger with settings.launcher (account menu).
|
|
60
|
+
// Its registered shell store's create() returns the SAME instance used by
|
|
61
|
+
// SettingsRoot and settings.open. Read the live winning entry on each click;
|
|
62
|
+
// retaining it would target a dead shell after a host/plugin reload.
|
|
63
|
+
const slots = ctx?.slots
|
|
64
|
+
const entries = slots?.entriesOfSlot?.('sidebar.settings') ?? slots?.entries?.('sidebar.settings') ?? []
|
|
65
|
+
const shell = entries[0]
|
|
66
|
+
if (!shell?.children?.['settings.launcher'] || typeof shell.store?.create !== 'function') return false
|
|
67
|
+
const instance = shell.store.create()
|
|
68
|
+
if (typeof instance?.actions?.open !== 'function') return false
|
|
69
|
+
instance.actions.open()
|
|
70
|
+
return true
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* ---- 终末地「开机场」加载屏:由当前 apply/切肤代次独占,旧回调不得操作新遮罩。 ---- */
|
|
74
|
+
const AIC_BOOT_KEY = 'prts.uiSkin'
|
|
75
|
+
let aicBootEl = null
|
|
76
|
+
let aicBootTimer = null
|
|
77
|
+
let aicBootWatchdog = null
|
|
78
|
+
let aicBootTimeouts = new Set()
|
|
79
|
+
let aicBootPct = 0
|
|
80
|
+
let aicBootEpoch = 0
|
|
81
|
+
let aicBootFinishing = false
|
|
82
|
+
let aicBootStyleOwned = false
|
|
83
|
+
let aicBootPreviousBackground
|
|
84
|
+
function aicBootClearTimers() {
|
|
85
|
+
if (aicBootTimer) { clearInterval(aicBootTimer); aicBootTimer = null }
|
|
86
|
+
if (aicBootWatchdog) { clearTimeout(aicBootWatchdog); aicBootWatchdog = null }
|
|
87
|
+
for (const timer of aicBootTimeouts) clearTimeout(timer)
|
|
88
|
+
aicBootTimeouts.clear()
|
|
89
|
+
}
|
|
90
|
+
function aicBootSchedule(callback, delay, token) {
|
|
91
|
+
const timer = setTimeout(() => {
|
|
92
|
+
aicBootTimeouts.delete(timer)
|
|
93
|
+
if (token === aicBootEpoch) callback()
|
|
94
|
+
}, delay)
|
|
95
|
+
aicBootTimeouts.add(timer)
|
|
96
|
+
return timer
|
|
97
|
+
}
|
|
98
|
+
function aicBootRestoreBackground() {
|
|
99
|
+
if (typeof document === 'undefined') return
|
|
100
|
+
if (aicBootPreviousBackground !== undefined) {
|
|
101
|
+
document.documentElement.style.background = aicBootPreviousBackground
|
|
102
|
+
aicBootPreviousBackground = undefined
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function aicBootRestoreDocument() {
|
|
106
|
+
if (typeof document === 'undefined') return
|
|
107
|
+
aicBootRestoreBackground()
|
|
108
|
+
if (aicBootStyleOwned) document.getElementById('aic-boot-style')?.remove()
|
|
109
|
+
aicBootStyleOwned = false
|
|
110
|
+
}
|
|
111
|
+
function aicBootPaint(percent, message, token = aicBootEpoch) {
|
|
112
|
+
if (!aicBootEl || token !== aicBootEpoch) return
|
|
113
|
+
const failed = percent < 0
|
|
114
|
+
const fill = aicBootEl.querySelector('.aic-boot-fill')
|
|
115
|
+
const label = aicBootEl.querySelector('.aic-boot-msg')
|
|
116
|
+
const pct = aicBootEl.querySelector('.aic-boot-pct')
|
|
117
|
+
if (fill) fill.style.width = `${aicBootPct}%`
|
|
118
|
+
if (label) label.textContent = failed
|
|
119
|
+
? `LOAD FAILED: ${message ?? 'UNKNOWN'}` : `${(message ?? 'LOADING').toUpperCase()}…`
|
|
120
|
+
if (pct) pct.textContent = failed ? 'ERROR' : `${aicBootPct}%`
|
|
121
|
+
}
|
|
122
|
+
function aicBootProgress(percent, message, token = aicBootEpoch) {
|
|
123
|
+
if (!aicBootEl || token !== aicBootEpoch) return
|
|
124
|
+
if (aicBootTimer) { clearInterval(aicBootTimer); aicBootTimer = null }
|
|
125
|
+
if (percent < 0) {
|
|
126
|
+
aicBootPaint(-1, message, token)
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
// 进度只进不退(假爬行与真实事件混跑时避免条子回跳)。
|
|
130
|
+
aicBootPct = Math.max(aicBootPct, Math.min(100, Math.round(percent)))
|
|
131
|
+
aicBootPaint(aicBootPct, message, token)
|
|
132
|
+
}
|
|
133
|
+
function aicBootAbort(forceRestore = activeSkin !== 'endfield-aic') {
|
|
134
|
+
aicBootEpoch += 1
|
|
135
|
+
aicBootClearTimers()
|
|
136
|
+
aicBootEl?.remove()
|
|
137
|
+
aicBootEl = null
|
|
138
|
+
aicBootFinishing = false
|
|
139
|
+
if (forceRestore) aicBootRestoreDocument()
|
|
140
|
+
}
|
|
141
|
+
function aicBootShow(message = 'INITIALIZING', restart = false, skin = 'endfield-aic') {
|
|
142
|
+
if (typeof document === 'undefined') return 0
|
|
143
|
+
if (aicBootEl && !restart && !aicBootFinishing) {
|
|
144
|
+
aicBootProgress(aicBootPct, message)
|
|
145
|
+
return aicBootEpoch
|
|
146
|
+
}
|
|
147
|
+
if (aicBootEl) aicBootAbort(false)
|
|
148
|
+
const token = ++aicBootEpoch
|
|
149
|
+
if (!document.getElementById('aic-boot-style')) {
|
|
150
|
+
const style = document.createElement('style')
|
|
151
|
+
style.id = 'aic-boot-style'
|
|
152
|
+
style.textContent = '.aic-boot{position:fixed;inset:0;z-index:99999;background:#0b0d10;display:flex;align-items:center;justify-content:center;transition:opacity .6s ease .15s,visibility .6s ease .15s}'
|
|
153
|
+
+ '.aic-boot[data-skin="prts-agent"],.aic-boot[data-skin="harness"]{background:#f5f5f1;color:#202327}'
|
|
154
|
+
+ '.aic-boot[data-skin="rhine-lab"]{background:#ebe8e2;color:#30362c}'
|
|
155
|
+
+ '.aic-boot:not([data-skin="endfield-aic"]) .aic-boot-logo{color:inherit;letter-spacing:.12em}'
|
|
156
|
+
+ '.aic-boot:not([data-skin="endfield-aic"]) :is(.aic-boot-sub,.aic-boot-line){color:#676b62}'
|
|
157
|
+
+ '.aic-boot:not([data-skin="endfield-aic"]) .aic-boot-bar{background:#a5a99655}'
|
|
158
|
+
+ '.aic-boot:not([data-skin="endfield-aic"]) .aic-boot-fill{background:currentColor;box-shadow:none}'
|
|
159
|
+
+ '.aic-boot.done{opacity:0;visibility:hidden;pointer-events:none}'
|
|
160
|
+
+ '.aic-boot-box{width:380px;max-width:82vw;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}'
|
|
161
|
+
+ '.aic-boot-logo{font-size:44px;font-weight:900;letter-spacing:10px;color:#faff3f}'
|
|
162
|
+
+ '.aic-boot-sub{font-size:10px;letter-spacing:2px;color:rgba(255,255,255,.4);margin:8px 0 26px}'
|
|
163
|
+
+ '.aic-boot-bar{height:3px;background:rgba(255,255,255,.1);overflow:hidden}'
|
|
164
|
+
+ '.aic-boot-fill{height:100%;width:0;background:#faff3f;box-shadow:0 0 8px rgba(250,255,63,.7);transition:width .3s ease}'
|
|
165
|
+
+ '.aic-boot-line{display:flex;justify-content:space-between;margin-top:8px;font-size:10px;letter-spacing:1px;color:rgba(255,255,255,.5)}'
|
|
166
|
+
+ '@media(prefers-reduced-motion:reduce){.aic-boot,.aic-boot-fill{transition:none!important}}'
|
|
167
|
+
document.head.appendChild(style)
|
|
168
|
+
aicBootStyleOwned = true
|
|
169
|
+
}
|
|
170
|
+
aicBootEl = document.createElement('div')
|
|
171
|
+
aicBootEl.className = 'aic-boot'
|
|
172
|
+
aicBootEl.dataset.skin = skin
|
|
173
|
+
const branding = skin === 'endfield-aic' ? ['AIC', 'ENDFIELD INDUSTRIES // TALOS-II SURVEY SYSTEM']
|
|
174
|
+
: skin === 'rhine-lab' ? ['RHINE LAB', 'SYNTHESIZE INFORMATION // ARCHIVE SYSTEM']
|
|
175
|
+
: skin === 'prts-agent' ? ['PRTS', 'TERRARCHIVE // RETRIEVAL SYSTEM']
|
|
176
|
+
: ['HARNESS', 'PREPARING WORKSPACE']
|
|
177
|
+
aicBootEl.innerHTML = `<div class="aic-boot-box"><div class="aic-boot-logo">${branding[0]}</div>`
|
|
178
|
+
+ `<div class="aic-boot-sub">${branding[1]}</div>`
|
|
179
|
+
+ '<div class="aic-boot-bar"><div class="aic-boot-fill"></div></div>'
|
|
180
|
+
+ '<div class="aic-boot-line"><span class="aic-boot-msg"></span><span class="aic-boot-pct"></span></div></div>'
|
|
181
|
+
document.body.appendChild(aicBootEl)
|
|
182
|
+
if (aicBootPreviousBackground === undefined) {
|
|
183
|
+
aicBootPreviousBackground = document.documentElement.style.background
|
|
184
|
+
}
|
|
185
|
+
document.documentElement.style.background = skin === 'endfield-aic' ? '#0b0d10' : skin === 'rhine-lab' ? '#ebe8e2' : '#f5f5f1'
|
|
186
|
+
aicBootPct = 4
|
|
187
|
+
aicBootFinishing = false
|
|
188
|
+
aicBootPaint(aicBootPct, message, token)
|
|
189
|
+
// 真实进度(地图资源 / 插件装载事件)到来前缓慢爬行,避免停在 0%。
|
|
190
|
+
aicBootTimer = setInterval(() => {
|
|
191
|
+
if (!aicBootEl || token !== aicBootEpoch) return
|
|
192
|
+
aicBootPct = Math.min(90, aicBootPct + Math.random() * 3)
|
|
193
|
+
aicBootPaint(aicBootPct, 'LOADING', token)
|
|
194
|
+
}, 160)
|
|
195
|
+
// 看门狗:任何挂起(配置请求悬挂、地图脚本阻塞、席位未渲染)最多 30s 强制放行,绝不卡死界面。
|
|
196
|
+
aicBootWatchdog = setTimeout(() => {
|
|
197
|
+
if (!aicBootEl || token !== aicBootEpoch) return
|
|
198
|
+
aicBootProgress(-1, 'STARTUP TIMEOUT', token)
|
|
199
|
+
aicBootSchedule(() => aicBootDone(token, false), 2500, token)
|
|
200
|
+
}, 30000)
|
|
201
|
+
return token
|
|
202
|
+
}
|
|
203
|
+
function aicBootDone(token = aicBootEpoch, succeeded = true) {
|
|
204
|
+
if (!aicBootEl || token !== aicBootEpoch || aicBootFinishing) return
|
|
205
|
+
// Failed startup already paints an ERROR state. Do not overwrite it with a
|
|
206
|
+
// misleading success message while the overlay is fading out.
|
|
207
|
+
if (succeeded) aicBootProgress(100, 'SYSTEM ONLINE', token)
|
|
208
|
+
const el = aicBootEl
|
|
209
|
+
aicBootClearTimers()
|
|
210
|
+
aicBootFinishing = true
|
|
211
|
+
aicBootSchedule(() => {
|
|
212
|
+
el.classList.add('done')
|
|
213
|
+
aicBootSchedule(() => {
|
|
214
|
+
el.remove()
|
|
215
|
+
if (aicBootEl === el) aicBootEl = null
|
|
216
|
+
aicBootFinishing = false
|
|
217
|
+
if (aicBootStyleOwned) document.getElementById('aic-boot-style')?.remove()
|
|
218
|
+
aicBootStyleOwned = false
|
|
219
|
+
if (activeSkin !== 'endfield-aic') aicBootRestoreDocument()
|
|
220
|
+
}, 900, token)
|
|
221
|
+
}, 260, token)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
// PRTS Agent 原版是“深色侧栏 + 亮色阅读区”的固定混合亮度,
|
|
226
|
+
// 不是 Harness 主题的一个普通亮/暗配色。因此两种 base scheme 投影到
|
|
227
|
+
// 同一套 canonical token;侧栏的反白文字由局部 CSS token 负责。
|
|
228
|
+
const canonical = (value) => ({ light: value, dark: value })
|
|
229
|
+
const PRTS_AGENT_TOKENS = Object.freeze({
|
|
230
|
+
'--dsw-alias-bg-base': canonical('rgba(255,255,255,.01)'),
|
|
231
|
+
'--dsw-alias-bg-layer-1': canonical('rgba(248,249,246,.9)'),
|
|
232
|
+
'--dsw-alias-bg-layer-2': canonical('rgba(255,255,255,.95)'),
|
|
233
|
+
'--dsw-alias-bg-layer-3': canonical('#ffffff'),
|
|
234
|
+
'--dsw-alias-bg-module-platform': canonical('rgba(239,239,235,.9)'),
|
|
235
|
+
'--dsw-alias-bg-multi-select': canonical('#f1f2ef'),
|
|
236
|
+
'--dsw-alias-bg-overlay': canonical('#f2f3f0'),
|
|
237
|
+
'--dsw-alias-border-l1': canonical('rgba(17,18,20,.075)'),
|
|
238
|
+
'--dsw-alias-border-l2': canonical('rgba(17,18,20,.15)'),
|
|
239
|
+
'--dsw-alias-border-l2-darkmode-thin': canonical('rgba(17,18,20,.11)'),
|
|
240
|
+
'--dsw-alias-border-l3': canonical('rgba(17,18,20,.2)'),
|
|
241
|
+
'--dsw-alias-border-l4': canonical('rgba(17,18,20,.28)'),
|
|
242
|
+
'--dsw-alias-brand-primary': canonical('#111214'),
|
|
243
|
+
'--dsw-alias-brand-text': canonical('#111214'),
|
|
244
|
+
'--dsw-alias-button-elevated-fill': canonical('rgba(255,255,255,.92)'),
|
|
245
|
+
'--dsw-alias-button-floating-fill': canonical('rgba(255,255,255,.9)'),
|
|
246
|
+
'--dsw-alias-button-floating-hover': canonical('#efefeb'),
|
|
247
|
+
'--dsw-alias-button-ghost-active-fill': canonical('rgba(17,18,20,.095)'),
|
|
248
|
+
'--dsw-alias-button-ghost-active-hover': canonical('rgba(17,18,20,.13)'),
|
|
249
|
+
'--dsw-alias-button-info-fill': canonical('#111214'),
|
|
250
|
+
'--dsw-alias-button-info-hover': canonical('#292b2f'),
|
|
251
|
+
'--dsw-alias-button-primary-dimmed': canonical('#e5e6e2'),
|
|
252
|
+
'--dsw-alias-button-primary-hover': canonical('#34373b'),
|
|
253
|
+
'--dsw-alias-interactive-bg-active': canonical('rgba(17,18,20,.095)'),
|
|
254
|
+
'--dsw-alias-interactive-bg-hover': canonical('rgba(17,18,20,.055)'),
|
|
255
|
+
'--dsw-alias-interactive-bg-hover-accent': canonical('rgba(40,105,216,.12)'),
|
|
256
|
+
'--dsw-alias-interactive-bg-hover-solid': canonical('#f1f1ed'),
|
|
257
|
+
'--dsw-alias-label-primary': canonical('#111214'),
|
|
258
|
+
'--dsw-alias-label-primary-dimmed': canonical('#34373b'),
|
|
259
|
+
'--dsw-alias-label-primary-foreground': canonical('#ffffff'),
|
|
260
|
+
'--dsw-alias-label-primary-inverted': canonical('#ffffff'),
|
|
261
|
+
'--dsw-alias-label-secondary': canonical('#34373b'),
|
|
262
|
+
'--dsw-alias-label-tertiary': canonical('#62666c'),
|
|
263
|
+
'--dsw-alias-label-caption': canonical('#858a91'),
|
|
264
|
+
'--dsw-alias-label-dimmed': canonical('#b4b7bb'),
|
|
265
|
+
'--dsw-alias-markdown-citation': canonical('#e4ecfb'),
|
|
266
|
+
'--dsw-alias-markdown-code-block': canonical('rgba(245,246,243,.9)'),
|
|
267
|
+
'--dsw-alias-markdown-code-block-banner': canonical('#efefeb'),
|
|
268
|
+
'--dsw-alias-markdown-inline-code': canonical('#efefeb'),
|
|
269
|
+
'--dsw-alias-scrollbar-bg-l1': canonical('rgba(127,130,132,.25)'),
|
|
270
|
+
'--dsw-alias-scrollbar-bg-l2': canonical('rgba(127,130,132,.32)'),
|
|
271
|
+
'--dsw-alias-scrollbar-hover-l1': canonical('rgba(98,102,108,.45)'),
|
|
272
|
+
'--dsw-alias-scrollbar-hover-l2': canonical('rgba(98,102,108,.55)'),
|
|
273
|
+
'--dsw-alias-state-business-primary': canonical('#2869d8'),
|
|
274
|
+
'--dsw-alias-state-business-tertiary': canonical('#e4ecfb'),
|
|
275
|
+
'--dsw-alias-state-error-primary': canonical('#f25a5a'),
|
|
276
|
+
'--dsw-alias-state-success-primary': canonical('#35ad64'),
|
|
277
|
+
'--dsw-alias-state-warn-primary': canonical('#e89a1c'),
|
|
278
|
+
'--dsw-specific-bubble': canonical('rgba(255,255,255,.82)'),
|
|
279
|
+
'--dsw-specific-bubble-highlight': canonical('#e4ecfb'),
|
|
280
|
+
'--dsw-specific-input-major': canonical('rgba(255,255,255,.9)'),
|
|
281
|
+
'--dsw-specific-menu': canonical('rgba(255,255,255,.97)'),
|
|
282
|
+
'--dsw-specific-selector': canonical('#f5f6f3'),
|
|
283
|
+
'--dsw-specific-sidebar-fill': canonical('transparent'),
|
|
284
|
+
'--dsw-specific-sidebar-nav-item-active': canonical('rgba(255,255,255,.14)'),
|
|
285
|
+
'--dsw-specific-sidebar-nav-item-hover': canonical('rgba(255,255,255,.08)'),
|
|
286
|
+
'--dsw-specific-tip': canonical('rgba(245,246,243,.94)'),
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
// 皮肤接管整套 --dsw-* token:终末地是固定暗色界面,而 Harness 主题仍是
|
|
290
|
+
// light 基线,漏掉的 token(如 bg-module-platform、button-floating-fill)
|
|
291
|
+
// 会保持近白填充,与近白文字叠成"白块"。因此对照 design-platform 全集补齐。
|
|
292
|
+
const ENDFIELD_AIC_TOKENS = Object.freeze({
|
|
293
|
+
'--dsw-alias-bg-base': canonical('#0b0d10'),
|
|
294
|
+
'--dsw-alias-bg-layer-1': canonical('rgba(12,15,17,.88)'),
|
|
295
|
+
'--dsw-alias-bg-layer-2': canonical('rgba(18,21,23,.94)'),
|
|
296
|
+
'--dsw-alias-bg-layer-3': canonical('#15191c'),
|
|
297
|
+
'--dsw-alias-bg-mask-1': canonical('rgba(0,0,0,.55)'),
|
|
298
|
+
'--dsw-alias-bg-mask-2': canonical('rgba(0,0,0,.28)'),
|
|
299
|
+
'--dsw-alias-bg-mask-3': canonical('rgba(0,0,0,.48)'),
|
|
300
|
+
'--dsw-alias-bg-module-platform': canonical('rgba(255,255,255,.07)'),
|
|
301
|
+
'--dsw-alias-bg-multi-select': canonical('rgba(255,255,255,.06)'),
|
|
302
|
+
'--dsw-alias-bg-overlay': canonical('rgba(16,19,22,.92)'),
|
|
303
|
+
'--dsw-alias-bg-skeleton': canonical('rgba(255,255,255,.07)'),
|
|
304
|
+
// 会话思考状态的 shimmer 直接引用品牌蓝 static token,覆写成酸性黄。
|
|
305
|
+
'--dsw-static-deepseek-500': canonical('#faff3f'),
|
|
306
|
+
'--dsw-static-deepseek-200': canonical('#fdffa3'),
|
|
307
|
+
'--dsw-alias-border-l1': canonical('rgba(255,255,255,.1)'),
|
|
308
|
+
'--dsw-alias-border-l2': canonical('rgba(255,255,255,.17)'),
|
|
309
|
+
'--dsw-alias-border-l2-darkmode-thin': canonical('rgba(255,255,255,.1)'),
|
|
310
|
+
'--dsw-alias-border-l3': canonical('rgba(250,255,63,.38)'),
|
|
311
|
+
'--dsw-alias-border-l4': canonical('rgba(255,255,255,.26)'),
|
|
312
|
+
'--dsw-alias-border-inverted': canonical('rgba(255,255,255,.06)'),
|
|
313
|
+
'--dsw-alias-border-inverted2': canonical('rgba(255,255,255,.08)'),
|
|
314
|
+
'--dsw-alias-brand-primary': canonical('#faff3f'),
|
|
315
|
+
'--dsw-alias-brand-primary-invert': canonical('#080a0c'),
|
|
316
|
+
'--dsw-alias-brand-primary-new-colorprimary-new-color': canonical('#faff3f'),
|
|
317
|
+
'--dsw-alias-brand-text': canonical('#faff3f'),
|
|
318
|
+
'--dsw-alias-button-contrast-fill': canonical('rgba(238,240,235,.85)'),
|
|
319
|
+
'--dsw-alias-button-elevated-fill': canonical('rgba(18,21,24,.95)'),
|
|
320
|
+
'--dsw-alias-button-floating-fill': canonical('rgba(14,17,20,.94)'),
|
|
321
|
+
'--dsw-alias-button-floating-hover': canonical('rgba(30,34,38,.96)'),
|
|
322
|
+
'--dsw-alias-button-ghost-active-border': canonical('rgba(250,255,63,.55)'),
|
|
323
|
+
'--dsw-alias-button-ghost-active-fill': canonical('rgba(250,255,63,.12)'),
|
|
324
|
+
'--dsw-alias-button-ghost-active-hover': canonical('rgba(250,255,63,.18)'),
|
|
325
|
+
'--dsw-alias-button-info-fill': canonical('#dce12f'),
|
|
326
|
+
'--dsw-alias-button-info-hover': canonical('#faff3f'),
|
|
327
|
+
'--dsw-alias-button-primary-dimmed': canonical('rgba(250,255,63,.35)'),
|
|
328
|
+
'--dsw-alias-button-primary-fill': canonical('#faff3f'),
|
|
329
|
+
'--dsw-alias-button-primary-hover': canonical('#fdff86'),
|
|
330
|
+
'--dsw-alias-button-tool-bar-fill': canonical('rgba(84,85,87,.5)'),
|
|
331
|
+
'--dsw-alias-button-tool-bar-fill-invisible': canonical('rgba(31,31,31,.36)'),
|
|
332
|
+
'--dsw-alias-button-tool-bar-hover': canonical('rgba(84,85,87,.6)'),
|
|
333
|
+
'--dsw-alias-interactive-bg-active': canonical('rgba(250,255,63,.12)'),
|
|
334
|
+
'--dsw-alias-interactive-bg-hover': canonical('rgba(255,255,255,.07)'),
|
|
335
|
+
'--dsw-alias-interactive-bg-hover-accent': canonical('rgba(250,255,63,.14)'),
|
|
336
|
+
'--dsw-alias-interactive-bg-hover-danger': canonical('rgba(255,103,103,.16)'),
|
|
337
|
+
'--dsw-alias-interactive-bg-hover-solid': canonical('rgba(28,32,36,.95)'),
|
|
338
|
+
'--dsw-alias-label-primary': canonical('#f2f3ef'),
|
|
339
|
+
'--dsw-alias-label-primary-bluish': canonical('#f2f3ef'),
|
|
340
|
+
'--dsw-alias-label-primary-dimmed': canonical('#d8d9d5'),
|
|
341
|
+
'--dsw-alias-label-primary-foreground': canonical('#080a0c'),
|
|
342
|
+
'--dsw-alias-label-primary-inverted': canonical('#101316'),
|
|
343
|
+
'--dsw-alias-label-secondary': canonical('rgba(242,243,239,.74)'),
|
|
344
|
+
'--dsw-alias-label-tertiary': canonical('rgba(242,243,239,.5)'),
|
|
345
|
+
'--dsw-alias-label-caption': canonical('rgba(242,243,239,.35)'),
|
|
346
|
+
'--dsw-alias-label-dimmed': canonical('rgba(242,243,239,.42)'),
|
|
347
|
+
'--dsw-alias-markdown-citation': canonical('rgba(250,255,63,.12)'),
|
|
348
|
+
'--dsw-alias-markdown-code-block': canonical('rgba(5,7,8,.78)'),
|
|
349
|
+
'--dsw-alias-markdown-code-block-banner': canonical('rgba(250,255,63,.08)'),
|
|
350
|
+
'--dsw-alias-markdown-code-segment-selected': canonical('rgba(255,255,255,.1)'),
|
|
351
|
+
'--dsw-alias-markdown-code-segment-unselected': canonical('rgba(255,255,255,.05)'),
|
|
352
|
+
'--dsw-alias-markdown-inline-code': canonical('rgba(250,255,63,.1)'),
|
|
353
|
+
'--dsw-alias-markdown-placeholder': canonical('rgba(255,255,255,.07)'),
|
|
354
|
+
'--dsw-alias-markdown-tag': canonical('rgba(255,255,255,.08)'),
|
|
355
|
+
'--dsw-alias-scrollbar-bg-l1': canonical('rgba(255,255,255,.12)'),
|
|
356
|
+
'--dsw-alias-scrollbar-bg-l2': canonical('rgba(255,255,255,.18)'),
|
|
357
|
+
'--dsw-alias-scrollbar-hover-l1': canonical('rgba(250,255,63,.42)'),
|
|
358
|
+
'--dsw-alias-scrollbar-hover-l2': canonical('rgba(250,255,63,.45)'),
|
|
359
|
+
'--dsw-alias-state-business-primary': canonical('#faff3f'),
|
|
360
|
+
'--dsw-alias-state-business-tertiary': canonical('rgba(250,255,63,.1)'),
|
|
361
|
+
'--dsw-alias-state-error-primary': canonical('#ff6767'),
|
|
362
|
+
'--dsw-alias-state-error-secondary': canonical('#ff8585'),
|
|
363
|
+
'--dsw-alias-state-success-primary': canonical('#b9e85b'),
|
|
364
|
+
'--dsw-alias-state-success-secondary': canonical('#b9e85b'),
|
|
365
|
+
'--dsw-alias-state-success-tertiary': canonical('rgba(185,232,91,.12)'),
|
|
366
|
+
'--dsw-alias-state-warn-label': canonical('#dce12f'),
|
|
367
|
+
'--dsw-alias-state-warn-primary': canonical('#faff3f'),
|
|
368
|
+
'--dsw-alias-state-warn-secondary': canonical('rgba(250,255,63,.75)'),
|
|
369
|
+
'--dsw-alias-state-warn-tertiary': canonical('rgba(250,255,63,.1)'),
|
|
370
|
+
'--dsw-alias-toast-bg': canonical('rgba(17,20,23,.98)'),
|
|
371
|
+
'--dsw-alias-tooltip-bg': canonical('rgba(17,20,23,.98)'),
|
|
372
|
+
'--dsw-specific-bubble': canonical('rgba(10,12,14,.74)'),
|
|
373
|
+
'--dsw-specific-bubble-highlight': canonical('rgba(250,255,63,.08)'),
|
|
374
|
+
'--dsw-specific-input-major': canonical('rgba(13,16,18,.94)'),
|
|
375
|
+
'--dsw-specific-login-input': canonical('rgba(255,255,255,.06)'),
|
|
376
|
+
'--dsw-specific-menu': canonical('rgba(10,12,14,.98)'),
|
|
377
|
+
'--dsw-specific-selector': canonical('rgba(255,255,255,.05)'),
|
|
378
|
+
'--dsw-specific-sidebar-fill': canonical('rgba(8,10,12,.96)'),
|
|
379
|
+
'--dsw-specific-sidebar-nav-item-active': canonical('rgba(250,255,63,.1)'),
|
|
380
|
+
'--dsw-specific-sidebar-nav-item-active-accent': canonical('rgba(250,255,63,.14)'),
|
|
381
|
+
'--dsw-specific-sidebar-nav-item-hover': canonical('rgba(255,255,255,.06)'),
|
|
382
|
+
'--dsw-specific-tip': canonical('rgba(10,12,14,.96)'),
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
// Match the host's settings and menus to the same ivory/olive workspace.
|
|
386
|
+
const RHINE_LAB_TOKENS = Object.freeze({
|
|
387
|
+
...PRTS_AGENT_TOKENS,
|
|
388
|
+
'--dsw-alias-bg-base': canonical('#ebe8e2'),
|
|
389
|
+
'--dsw-alias-bg-layer-1': canonical('#e7e3d9'),
|
|
390
|
+
'--dsw-alias-bg-layer-2': canonical('#f3f0e8'),
|
|
391
|
+
'--dsw-alias-bg-layer-3': canonical('#faf8f1'),
|
|
392
|
+
'--dsw-alias-bg-mask-1': canonical('rgba(39,43,33,.32)'),
|
|
393
|
+
'--dsw-alias-brand-primary': canonical('#59634b'),
|
|
394
|
+
'--dsw-alias-brand-text': canonical('#48533d'),
|
|
395
|
+
'--dsw-alias-label-primary': canonical('#282e24'),
|
|
396
|
+
'--dsw-alias-label-secondary': canonical('#505747'),
|
|
397
|
+
'--dsw-alias-label-tertiary': canonical('#69705e'),
|
|
398
|
+
'--dsw-alias-label-caption': canonical('#717664'),
|
|
399
|
+
'--dsw-alias-button-info-fill': canonical('#59634b'),
|
|
400
|
+
'--dsw-alias-button-info-hover': canonical('#454e39'),
|
|
401
|
+
'--dsw-alias-button-elevated-fill': canonical('#f3f0e8'),
|
|
402
|
+
'--dsw-alias-button-floating-fill': canonical('#f3f0e8'),
|
|
403
|
+
'--dsw-alias-interactive-bg-active': canonical('rgba(99,112,77,.14)'),
|
|
404
|
+
'--dsw-alias-interactive-bg-hover': canonical('rgba(99,112,77,.08)'),
|
|
405
|
+
'--dsw-alias-state-business-primary': canonical('#59634b'),
|
|
406
|
+
'--dsw-alias-state-business-tertiary': canonical('#e3e6d7'),
|
|
407
|
+
'--dsw-specific-menu': canonical('#f3f0e8'),
|
|
408
|
+
'--dsw-specific-sidebar-fill': canonical('#e7e3d9'),
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
const normalizeSkin = (skin) => skin === 'prts-agent' || skin === 'endfield-aic' || skin === 'rhine-lab'
|
|
412
|
+
? skin : 'harness'
|
|
413
|
+
const skinTokens = (skin) => skin === 'prts-agent' ? PRTS_AGENT_TOKENS
|
|
414
|
+
: skin === 'endfield-aic' ? ENDFIELD_AIC_TOKENS : skin === 'rhine-lab' ? RHINE_LAB_TOKENS : null
|
|
415
|
+
const writeSkinDataset = (skin) => {
|
|
416
|
+
if (typeof document !== 'undefined') {
|
|
417
|
+
if (skin === 'prts-agent') document.body.dataset.prtsSkin = 'agent'
|
|
418
|
+
else if (skin === 'endfield-aic') document.body.dataset.prtsSkin = 'endfield-aic'
|
|
419
|
+
else if (skin === 'rhine-lab') document.body.dataset.prtsSkin = 'rhine-lab'
|
|
420
|
+
else delete document.body.dataset.prtsSkin
|
|
421
|
+
}
|
|
422
|
+
for (const listener of rhineSkinListeners) listener(skin)
|
|
423
|
+
}
|
|
424
|
+
const persistSkin = (skin) => {
|
|
425
|
+
try {
|
|
426
|
+
if (typeof localStorage !== 'undefined') localStorage.setItem(AIC_BOOT_KEY, skin)
|
|
427
|
+
} catch { /* 隐私模式等无 localStorage 环境不影响已提交的皮肤 */ }
|
|
428
|
+
}
|
|
429
|
+
const setSkin = async (skin, options = {}) => {
|
|
430
|
+
const next = normalizeSkin(skin)
|
|
431
|
+
const operation = ++skinApplyEpoch
|
|
432
|
+
const assertCurrent = () => {
|
|
433
|
+
if (operation !== skinApplyEpoch) throw makeAbortError('皮肤切换已被新的操作取代')
|
|
434
|
+
options.assertCurrent?.()
|
|
435
|
+
}
|
|
436
|
+
const stylesheet = await prepareSkinStylesheet(next)
|
|
437
|
+
try {
|
|
438
|
+
assertCurrent()
|
|
439
|
+
} catch (error) {
|
|
440
|
+
stylesheet.rollback()
|
|
441
|
+
throw error
|
|
442
|
+
}
|
|
443
|
+
if (next === activeSkin) {
|
|
444
|
+
// 同值也要修复运行时挂载;slot owner HMR 或旧版半切换状态不能被短路。
|
|
445
|
+
try {
|
|
446
|
+
await options.beforeCommit?.({ previous: activeSkin, next })
|
|
447
|
+
assertCurrent()
|
|
448
|
+
syncScene()
|
|
449
|
+
syncAicLayout()
|
|
450
|
+
stylesheet.commit()
|
|
451
|
+
// AIC 的 boot 已完成时会刻意保留根节点黑底。轮询或裸 setSkin
|
|
452
|
+
// 离开 AIC 不会再经过 boot 淡出,因此在成功提交后独立恢复原值。
|
|
453
|
+
if (next !== 'endfield-aic') aicBootRestoreBackground()
|
|
454
|
+
persistSkin(next)
|
|
455
|
+
return next
|
|
456
|
+
} catch (error) {
|
|
457
|
+
stylesheet.rollback()
|
|
458
|
+
throw error
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const previous = activeSkin
|
|
462
|
+
const previousDispose = removeSkinTokens
|
|
463
|
+
const nextTokens = skinTokens(next)
|
|
464
|
+
let nextDispose = null
|
|
465
|
+
let runtimeStarted = false
|
|
466
|
+
try {
|
|
467
|
+
// External persistence is the asynchronous prepare phase. Do it while
|
|
468
|
+
// activeSkin, tokens, scene and the committed stylesheet are untouched.
|
|
469
|
+
await options.beforeCommit?.({ previous, next })
|
|
470
|
+
assertCurrent()
|
|
471
|
+
runtimeStarted = true
|
|
472
|
+
// 同 source 的新层会原子替换旧层,旧 disposer 自动变成 no-op。
|
|
473
|
+
if (nextTokens && themeRuntime) {
|
|
474
|
+
nextDispose = themeRuntime.overrideTokens(SKIN_SOURCE, nextTokens)
|
|
475
|
+
}
|
|
476
|
+
activeSkin = next
|
|
477
|
+
writeSkinDataset(next)
|
|
478
|
+
syncScene()
|
|
479
|
+
syncAicLayout()
|
|
480
|
+
if (!nextDispose) previousDispose?.()
|
|
481
|
+
removeSkinTokens = nextDispose
|
|
482
|
+
// Runtime and CSS commit form one synchronous section, so cleanup/HMR
|
|
483
|
+
// cannot interleave and let an obsolete rollback overwrite a new owner.
|
|
484
|
+
stylesheet.commit()
|
|
485
|
+
if (next !== 'endfield-aic') aicBootRestoreBackground()
|
|
486
|
+
persistSkin(next)
|
|
487
|
+
return next
|
|
488
|
+
} catch (error) {
|
|
489
|
+
stylesheet.rollback()
|
|
490
|
+
if (!runtimeStarted) throw error
|
|
491
|
+
// 清掉新皮肤已经安装的 DOM/slot,再恢复旧 token、dataset 与运行时。
|
|
492
|
+
try {
|
|
493
|
+
activeSkin = 'harness'
|
|
494
|
+
syncScene()
|
|
495
|
+
syncAicLayout()
|
|
496
|
+
} catch { /* 尽最大努力回滚,保留原始异常 */ }
|
|
497
|
+
try { nextDispose?.() } catch (rollbackError) {
|
|
498
|
+
console.error('[prts-terrarchive] failed to dispose rejected skin tokens', rollbackError)
|
|
499
|
+
}
|
|
500
|
+
const previousTokens = skinTokens(previous)
|
|
501
|
+
try {
|
|
502
|
+
removeSkinTokens = previousTokens && themeRuntime
|
|
503
|
+
? themeRuntime.overrideTokens(SKIN_SOURCE, previousTokens)
|
|
504
|
+
: previousDispose
|
|
505
|
+
} catch (rollbackError) {
|
|
506
|
+
removeSkinTokens = previousDispose
|
|
507
|
+
console.error('[prts-terrarchive] failed to restore previous skin tokens', rollbackError)
|
|
508
|
+
}
|
|
509
|
+
activeSkin = previous
|
|
510
|
+
writeSkinDataset(previous)
|
|
511
|
+
try { syncScene(); syncAicLayout() } catch { /* 保留原始异常 */ }
|
|
512
|
+
throw error
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const PRTS_SEARCH_TOOLS = ['cloud_search', 'cloud_inspect', 'corpus_search', 'timeline_search']
|
|
517
|
+
const PRTS_SCENE_TOOLS = [...PRTS_SEARCH_TOOLS, 'corpus_read']
|
|
518
|
+
const emptyToolVisual = () => ({ state: 'standby', calls: 0, latest: null })
|
|
519
|
+
const compactSceneText = (value, fallback, limit = 32) => {
|
|
520
|
+
const text = String(value ?? '').trim().replace(/\s+/gu, ' ')
|
|
521
|
+
if (!text) return fallback
|
|
522
|
+
return text.length > limit ? `${text.slice(0, limit - 1)}…` : text
|
|
523
|
+
}
|
|
524
|
+
const parseToolArguments = (raw) => {
|
|
525
|
+
if (raw && typeof raw === 'object') return raw
|
|
526
|
+
try { return JSON.parse(String(raw || '{}')) } catch { return {} }
|
|
527
|
+
}
|
|
528
|
+
const toolSnapshot = (root) => {
|
|
529
|
+
if (!root) return null
|
|
530
|
+
if ('kind' in root) return {
|
|
531
|
+
name: root.call?.name || '', argsRaw: root.call?.argsRaw || '{}',
|
|
532
|
+
state: root.isError ? 'error' : 'complete', time: root.time || 0,
|
|
533
|
+
}
|
|
534
|
+
return { name: root.name || '', argsRaw: root.argsRaw || '{}', state: 'active', time: root.time || 0 }
|
|
535
|
+
}
|
|
536
|
+
const visibleAssistantText = (blocks) => (Array.isArray(blocks) ? blocks : [])
|
|
537
|
+
.filter((block) => block?.kind === 'text')
|
|
538
|
+
.map((block) => block.text || '').join('').trim()
|
|
539
|
+
|
|
540
|
+
/** DSH Chat snapshot -> PRTS Agent 四阶段视觉模型。 */
|
|
541
|
+
const buildSceneSnapshotModel = (order, nodes) => {
|
|
542
|
+
const visuals = Object.fromEntries(PRTS_SCENE_TOOLS.map((name) => [name, emptyToolVisual()]))
|
|
543
|
+
const assistantSteps = []
|
|
544
|
+
for (let index = 0; index < (Array.isArray(order) ? order.length : 0); index += 1) {
|
|
545
|
+
const node = nodes?.get?.(order[index])
|
|
546
|
+
if (node?.kind === 'tool-call') {
|
|
547
|
+
const call = toolSnapshot(node.data?.root)
|
|
548
|
+
if (!call || !Object.hasOwn(visuals, call.name)) continue
|
|
549
|
+
const visual = visuals[call.name]
|
|
550
|
+
visual.calls += 1
|
|
551
|
+
visual.latest = { ...call, index, arguments: parseToolArguments(call.argsRaw) }
|
|
552
|
+
visual.state = call.state
|
|
553
|
+
} else if (node?.kind === 'assistant-step') {
|
|
554
|
+
assistantSteps.push({
|
|
555
|
+
index, status: node.data?.status || 'settled',
|
|
556
|
+
text: visibleAssistantText(node.data?.blocks),
|
|
557
|
+
})
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
const aggregate = (names) => {
|
|
561
|
+
const states = names.map((name) => visuals[name].state)
|
|
562
|
+
if (states.includes('active')) return 'active'
|
|
563
|
+
if (states.includes('error') && !states.includes('complete')) return 'error'
|
|
564
|
+
if (states.includes('complete')) return 'complete'
|
|
565
|
+
if (states.includes('error')) return 'error'
|
|
566
|
+
return 'standby'
|
|
567
|
+
}
|
|
568
|
+
const searchCalls = PRTS_SEARCH_TOOLS.flatMap((name) => {
|
|
569
|
+
const latest = visuals[name].latest
|
|
570
|
+
return latest ? [{ ...latest, name, calls: visuals[name].calls }] : []
|
|
571
|
+
}).sort((left, right) => left.index - right.index)
|
|
572
|
+
const latestQuery = searchCalls.at(-1) || null
|
|
573
|
+
const latestRead = visuals.corpus_read.latest
|
|
574
|
+
const hasToolCall = PRTS_SCENE_TOOLS.some((name) => visuals[name].calls > 0)
|
|
575
|
+
const assistantRunning = assistantSteps.some((step) => step.status === 'running')
|
|
576
|
+
const finalAnswer = [...assistantSteps].reverse().find((step) => step.text && step.status !== 'running')
|
|
577
|
+
const planState = hasToolCall ? 'complete' : assistantRunning ? 'active' : 'standby'
|
|
578
|
+
const recallState = aggregate(PRTS_SEARCH_TOOLS)
|
|
579
|
+
const readState = visuals.corpus_read.state
|
|
580
|
+
let verifyState = 'standby'
|
|
581
|
+
if (readState === 'error') verifyState = 'error'
|
|
582
|
+
else if (latestRead) {
|
|
583
|
+
const answerAfterRead = finalAnswer && finalAnswer.index > latestRead.index
|
|
584
|
+
verifyState = answerAfterRead ? 'complete' : 'active'
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const queryArgs = latestQuery?.arguments || {}
|
|
588
|
+
const queryValue = queryArgs.query?.text || queryArgs.query || ''
|
|
589
|
+
const queryScopes = queryArgs.options?.channels || queryArgs.resource_types
|
|
590
|
+
|| queryArgs.filters?.resource_types || []
|
|
591
|
+
const queryScope = Array.isArray(queryScopes) && queryScopes.length
|
|
592
|
+
? queryScopes.join(' + ')
|
|
593
|
+
: latestQuery?.name?.startsWith('cloud_') ? `${queryArgs.depth || 'standard'} / cloud` : 'local corpus + timeline'
|
|
594
|
+
const readArgs = latestRead?.arguments || {}
|
|
595
|
+
const selection = readArgs.selection || readArgs
|
|
596
|
+
const sourceRange = selection.mode === 'range'
|
|
597
|
+
? `L${selection.start_line || '?'}—L${selection.end_line || '?'}`
|
|
598
|
+
: readArgs.line || selection.line
|
|
599
|
+
? `L${readArgs.line || selection.line} ± ${selection.before ?? 3}/${selection.after ?? 3}`
|
|
600
|
+
: selection.mode === 'document' ? 'DOCUMENT / PAGED' : '文档 · 行号 · 说话人'
|
|
601
|
+
const localCalls = visuals.corpus_search.calls + visuals.timeline_search.calls
|
|
602
|
+
const recallTitle = visuals.cloud_search.calls ? '云端召回候选'
|
|
603
|
+
: visuals.timeline_search.calls ? '检索事件时间线' : '召回候选片段'
|
|
604
|
+
const recallMeta = visuals.cloud_search.calls ? `云端检索 / ${visuals.cloud_search.calls} 次`
|
|
605
|
+
: localCalls ? `本地索引 / ${localCalls} 次` : '本地索引 / 待命'
|
|
606
|
+
const stateLabel = (state, labels) => labels[state] || labels.standby
|
|
607
|
+
const running = assistantRunning || PRTS_SCENE_TOOLS.some((name) => visuals[name].state === 'active')
|
|
608
|
+
const latestActivity = latestRead?.state === 'active' ? '正在核验原文'
|
|
609
|
+
: latestQuery?.state === 'active' ? '正在检索候选'
|
|
610
|
+
: verifyState === 'active' ? '正在核对来源与主张'
|
|
611
|
+
: hasToolCall ? '本地证据链' : '本地检索'
|
|
612
|
+
|
|
613
|
+
return {
|
|
614
|
+
visuals,
|
|
615
|
+
plan: { state: planState, title: '拆解问题意图', meta: stateLabel(planState, {
|
|
616
|
+
active: '正在解析实体 · 时间 · 因果', complete: '检索计划已生成',
|
|
617
|
+
error: '计划生成异常', standby: '实体 · 时间 · 因果',
|
|
618
|
+
}) },
|
|
619
|
+
recall: { state: recallState, title: recallTitle, meta: recallMeta },
|
|
620
|
+
read: { state: readState, title: '定位原文上下文', meta: sourceRange },
|
|
621
|
+
verify: { state: verifyState, title: '交叉核验证据', meta: stateLabel(verifyState, {
|
|
622
|
+
active: '正在核对来源与主张', complete: '证据核验完成',
|
|
623
|
+
error: '证据核验异常', standby: '等待已读原文',
|
|
624
|
+
}) },
|
|
625
|
+
query: {
|
|
626
|
+
state: latestQuery?.state || planState,
|
|
627
|
+
text: compactSceneText(queryValue, '等待用户问题'),
|
|
628
|
+
scope: compactSceneText(queryScope, 'local corpus'),
|
|
629
|
+
status: stateLabel(latestQuery?.state || planState, {
|
|
630
|
+
active: 'QUERYING RETRIEVAL SERVICE', complete: 'QUERY RESOLVED',
|
|
631
|
+
error: 'QUERY FAILED', standby: 'WAITING FOR AGENT',
|
|
632
|
+
}),
|
|
633
|
+
},
|
|
634
|
+
source: {
|
|
635
|
+
state: readState,
|
|
636
|
+
title: compactSceneText(readArgs.title || readArgs.locator?.display_title, '等待定位原文'),
|
|
637
|
+
range: sourceRange,
|
|
638
|
+
status: stateLabel(readState, {
|
|
639
|
+
active: 'READING SOURCE CONTEXT', complete: 'SOURCE CONTEXT READY',
|
|
640
|
+
error: 'SOURCE READ FAILED', standby: 'WAITING FOR SOURCE',
|
|
641
|
+
}),
|
|
642
|
+
},
|
|
643
|
+
stack: [
|
|
644
|
+
`01 cloud.search ×${visuals.cloud_search.calls}`,
|
|
645
|
+
`02 corpus.search ×${visuals.corpus_search.calls}`,
|
|
646
|
+
`03 source.read ×${visuals.corpus_read.calls}`,
|
|
647
|
+
],
|
|
648
|
+
stackStates: [
|
|
649
|
+
visuals.cloud_search.state,
|
|
650
|
+
visuals.corpus_search.state,
|
|
651
|
+
visuals.corpus_read.state,
|
|
652
|
+
],
|
|
653
|
+
ticker: latestActivity,
|
|
654
|
+
tickerState: running ? 'LIVE' : hasToolCall || finalAnswer ? 'DONE' : 'IDLE',
|
|
655
|
+
running,
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const sceneSnapshotSignature = (snapshot) => (snapshot?.order || []).map((key) => {
|
|
660
|
+
const node = snapshot.nodes?.get?.(key)
|
|
661
|
+
if (node?.kind === 'tool-call') {
|
|
662
|
+
const call = toolSnapshot(node.data?.root)
|
|
663
|
+
return call ? `t:${call.name}:${call.state}:${call.argsRaw}` : 't:'
|
|
664
|
+
}
|
|
665
|
+
if (node?.kind === 'assistant-step') {
|
|
666
|
+
return `a:${node.data?.status}:${visibleAssistantText(node.data?.blocks).length}`
|
|
667
|
+
}
|
|
668
|
+
return node?.kind || ''
|
|
669
|
+
}).join('|')
|
|
670
|
+
|
|
671
|
+
const SCENE_HTML = `
|
|
672
|
+
<div class="hero-system-map">
|
|
673
|
+
<div class="prts-scene-grid"></div>
|
|
674
|
+
<div class="prts-scene-shard prts-shard-dark"></div>
|
|
675
|
+
<div class="prts-scene-shard prts-shard-glass"></div>
|
|
676
|
+
<div class="prts-scene-shard prts-shard-silver"></div>
|
|
677
|
+
<div class="prts-scan-orbit"><i></i><i></i><i></i><i></i><span>SEARCH<br>RADIUS</span></div>
|
|
678
|
+
<div class="prts-crosshair prts-crosshair-a"><i></i></div>
|
|
679
|
+
<div class="prts-crosshair prts-crosshair-b"><i></i></div>
|
|
680
|
+
<div class="prts-ambient-type">RETRIEVAL<br><strong>PROCESS</strong><small>PRTS / 04—16</small></div>
|
|
681
|
+
<div class="prts-trace-window prts-trace-query"><header><span>QUERY_DECOMPOSE</span><i></i></header><p><em>intent</em> <span>等待用户问题</span></p><p><em>scope</em> <span>local corpus</span></p><b><i></i><strong>WAITING FOR AGENT</strong></b></div>
|
|
682
|
+
<div class="prts-trace-window prts-trace-source"><header><span>SOURCE_READ</span><i></i></header><p><em>doc</em> <span>等待定位原文</span></p><p><em>range</em> <span>文档 · 行号 · 说话人</span></p><b><i></i><strong>WAITING FOR SOURCE</strong></b></div>
|
|
683
|
+
<div class="prts-trace-stack"><span data-state="standby">01 cloud.search</span><span data-state="standby">02 corpus.search</span><span data-state="standby">03 source.read</span></div>
|
|
684
|
+
<div class="prts-system-node prts-node-plan"><i></i><small>01 · QUERY PLAN</small><b>拆解问题意图</b><span>等待计划生成</span></div>
|
|
685
|
+
<div class="prts-system-node prts-node-recall"><i></i><small>02 · RETRIEVAL</small><b>云端召回候选</b><span>本地 / 云端检索</span></div>
|
|
686
|
+
<div class="prts-system-node prts-node-read"><i></i><small>03 · SOURCE READ</small><b>定位原文上下文</b><span>DOCUMENT / PAGED</span></div>
|
|
687
|
+
<div class="prts-system-node prts-node-verify"><i></i><small>04 · VERIFY</small><b>交叉核验证据</b><span>证据链完成</span></div>
|
|
688
|
+
<div class="prts-system-caption"><span></span>LOCAL AGENT PROCESSOR <em>STANDBY</em></div>
|
|
689
|
+
<div class="prts-system-ticker"><b>AGENT TRACE</b><span>本地检索</span><i>IDLE</i></div>
|
|
690
|
+
<svg class="prts-cpu-assembly" viewBox="0 0 620 410" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
691
|
+
<defs><linearGradient id="prts-cpu-board" x1="185" y1="185" x2="433" y2="332" gradientUnits="userSpaceOnUse"><stop stop-color="#F8F8F5"/><stop offset=".52" stop-color="#E8E9E5"/><stop offset="1" stop-color="#FDFDFC"/></linearGradient><linearGradient id="prts-cpu-die" x1="257" y1="196" x2="365" y2="276" gradientUnits="userSpaceOnUse"><stop stop-color="#16181B"/><stop offset=".58" stop-color="#3B4047"/><stop offset="1" stop-color="#0E1012"/></linearGradient><linearGradient id="prts-cpu-lid" x1="250" y1="70" x2="404" y2="180" gradientUnits="userSpaceOnUse"><stop stop-color="#FFF"/><stop offset="1" stop-color="#D7D9D5"/></linearGradient><filter id="prts-cpu-shadow" x="120" y="120" width="390" height="250" filterUnits="userSpaceOnUse"><feDropShadow dx="0" dy="18" stdDeviation="18" flood-color="#16181B" flood-opacity=".14"/></filter><filter id="prts-lid-shadow" x="190" y="22" width="280" height="220" filterUnits="userSpaceOnUse"><feDropShadow dx="0" dy="12" stdDeviation="14" flood-color="#16181B" flood-opacity=".13"/></filter></defs>
|
|
692
|
+
<g class="prts-circuit-lines" stroke="#9DA19F"><path d="M20 255H135L178 227M36 284H145L185 260M63 318H165L205 292M600 242H485L446 221M584 277H473L435 253M552 314H452L415 286"/><circle cx="20" cy="255" r="3" fill="#9DA19F"/><circle cx="36" cy="284" r="3" fill="#9DA19F"/><circle cx="600" cy="242" r="3" fill="#9DA19F"/><circle cx="584" cy="277" r="3" fill="#9DA19F"/></g>
|
|
693
|
+
<g filter="url(#prts-cpu-shadow)"><path d="M176 204L305 147L447 215L315 299L176 231V204Z" fill="#CBCDCA"/><path d="M176 204L305 136L447 204L315 288L176 220V204Z" fill="url(#prts-cpu-board)" stroke="#B7BAB7"/><path d="M202 207L306 152L421 207L314 273L202 221V207Z" stroke="#C4C7C4"/><path d="M228 207L307 165L394 207L314 257L228 218V207Z" stroke="#D1D3D0"/><g class="prts-cpu-die-group"><path class="prts-cpu-die" d="M267 204L308 182L354 204L313 230L267 210V204Z" fill="url(#prts-cpu-die)" stroke="#0A0B0D"/><path d="M267 204L313 226L354 202" stroke="#777D83" opacity=".55"/><path class="prts-cpu-die-highlight" d="M278 201L308 186L342 202L312 221L278 207V201Z" fill="#68717B" opacity=".42"/></g></g>
|
|
694
|
+
<g class="prts-cpu-pins" stroke="#B4B7B4" stroke-width="2"><path d="M191 232L174 242M210 241L193 252M231 251L214 262M252 261L235 272M273 271L256 282M296 282L279 293M333 279L350 290M354 266L371 277M375 253L392 264M397 239L414 250M418 226L435 237"/></g>
|
|
695
|
+
<g class="prts-cpu-lid" filter="url(#prts-lid-shadow)"><path d="M228 112L313 68L404 112L317 166L228 122V112Z" fill="#C6C9C6"/><path d="M228 104L313 60L404 104L317 158L228 114V104Z" fill="url(#prts-cpu-lid)" stroke="#AEB2AF"/><path d="M251 104L314 72L381 105L316 145L251 114V104Z" stroke="#C1C4C1"/><path d="M299 100L314 92L330 100L314 110L299 103V100Z" fill="#17191C"/></g><g class="prts-assembly-guides" stroke="#8E9391" stroke-dasharray="4 7"><path d="M242 126L242 186M388 119L388 190M316 166V184"/></g>
|
|
696
|
+
</svg>
|
|
697
|
+
</div><div class="prts-empty-hero"><div class="prts-hero-identity"><div class="prts-hero-wordmark"><svg viewBox="0 0 1567 299" aria-label="PRTS"><g fill="#111214"><path d="M0 0H260C300 0 331 28 331 63V135C331 171 300 200 260 200H58C54 200 52 203 52 207V298H0V127H51V145C51 150 54 153 58 153H247C265 153 279 139 279 121V78C279 62 266 49 251 49H44Z"/><path d="M428 0H696C734 0 765 28 765 62V139C765 172 738 200 707 200H686L762 296L698 298L610 200H483C481 200 480 203 480 207V280L479 298H430V127H478V145C478 150 481 153 484 153H683C700 153 714 139 714 121V88C714 66 701 49 685 49H469Z"/><path d="M867 0H1163V49H1045C1043 49 1042 51 1042 53V262L1041 297L1029 298L991 263V53C991 51 989 49 987 49H867Z"/><path d="M1319 0H1554L1512 49H1333C1319 49 1308 61 1308 75V103C1308 116 1319 127 1332 127H1506C1541 127 1566 155 1566 189V244C1566 274 1542 298 1513 298H1310L1289 275C1289 272 1291 269 1293 267L1305 256C1308 253 1311 252 1315 252H1487C1504 252 1517 239 1517 222V199C1517 185 1507 174 1495 174H1315C1282 174 1256 147 1256 124V59C1256 26 1284 0 1319 0Z"/></g></svg><em>BETA</em></div><small><s>PRIMITIVE RHODES ISLAND TERMINAL SERVICE</s> // PRIES—???</small></div><h2>想从泰拉的故事里了解什么?</h2><p>我会先检索线索,再回到本地资料逐行核验,并把可复查的原文放在回答旁边。</p></div>
|
|
698
|
+
`
|
|
699
|
+
// Endfield AIC skin CSS lives in lib/skins/endfield-aic.css.
|
|
700
|
+
|
|
701
|
+
const installScene = () => {
|
|
702
|
+
if (typeof document === 'undefined') return () => {}
|
|
703
|
+
const scene = document.createElement('div')
|
|
704
|
+
scene.id = 'prts-agent-scene'
|
|
705
|
+
scene.className = 'prts-agent-scene'
|
|
706
|
+
// installScene 比 Conversation slot 更早挂载时先保持隐藏,避免在设置、
|
|
707
|
+
// 工作区等非会话页面闪出开机场景。
|
|
708
|
+
scene.dataset.page = 'hidden'
|
|
709
|
+
scene.setAttribute('aria-hidden', 'true')
|
|
710
|
+
scene.innerHTML = SCENE_HTML
|
|
711
|
+
document.body.prepend(scene)
|
|
712
|
+
const replayCpu = (forcePurple = false) => {
|
|
713
|
+
const cpu = scene.querySelector('.prts-cpu-assembly')
|
|
714
|
+
if (!cpu) return
|
|
715
|
+
const replacement = cpu.cloneNode(true)
|
|
716
|
+
replacement.classList.toggle('is-purple', forcePurple || Math.random() < .05)
|
|
717
|
+
cpu.replaceWith(replacement)
|
|
718
|
+
}
|
|
719
|
+
let cpuDebugBuffer = ''
|
|
720
|
+
const handleCpuDebugKeydown = (event) => {
|
|
721
|
+
const target = event.target
|
|
722
|
+
if (event.isComposing || event.ctrlKey || event.metaKey || event.altKey
|
|
723
|
+
|| (target instanceof Element && target.closest('input, textarea, select, [contenteditable="true"], [contenteditable="plaintext-only"]'))) {
|
|
724
|
+
cpuDebugBuffer = ''
|
|
725
|
+
return
|
|
726
|
+
}
|
|
727
|
+
if (event.key.length !== 1) return
|
|
728
|
+
cpuDebugBuffer = `${cpuDebugBuffer}${event.key.toLowerCase()}`.slice(-9)
|
|
729
|
+
if (cpuDebugBuffer !== 'priestess') return
|
|
730
|
+
cpuDebugBuffer = ''
|
|
731
|
+
replayCpu(true)
|
|
732
|
+
}
|
|
733
|
+
globalThis.addEventListener('keydown', handleCpuDebugKeydown)
|
|
734
|
+
if (Math.random() < .05) scene.querySelector('.prts-cpu-assembly')?.classList.add('is-purple')
|
|
735
|
+
let frame = null
|
|
736
|
+
let heroExitTimer = null
|
|
737
|
+
let hasShownHero = false
|
|
738
|
+
const stateFor = (names) => {
|
|
739
|
+
const rows = names.flatMap((name) => [...document.querySelectorAll(`[data-tool="${name}"]`)])
|
|
740
|
+
if (rows.some((row) => row.dataset.state === 'running')) return 'active'
|
|
741
|
+
if (rows.some((row) => row.dataset.state === 'error')) return 'error'
|
|
742
|
+
if (rows.length) return 'complete'
|
|
743
|
+
return 'standby'
|
|
744
|
+
}
|
|
745
|
+
const setStage = (selector, value) => {
|
|
746
|
+
const element = scene.querySelector(selector)
|
|
747
|
+
if (!element || !value) return
|
|
748
|
+
element.dataset.state = value.state
|
|
749
|
+
const title = element.querySelector(':scope > b')
|
|
750
|
+
const meta = element.querySelector(':scope > span')
|
|
751
|
+
if (title) title.textContent = value.title
|
|
752
|
+
if (meta) meta.textContent = value.meta
|
|
753
|
+
}
|
|
754
|
+
const setTrace = (selector, value) => {
|
|
755
|
+
const element = scene.querySelector(selector)
|
|
756
|
+
if (!element || !value) return
|
|
757
|
+
element.dataset.state = value.state
|
|
758
|
+
const lines = element.querySelectorAll(':scope > p > span')
|
|
759
|
+
if (lines[0]) lines[0].textContent = value.text || value.title
|
|
760
|
+
if (lines[1]) lines[1].textContent = value.scope || value.range
|
|
761
|
+
const status = element.querySelector(':scope > b > strong')
|
|
762
|
+
if (status) status.textContent = value.status
|
|
763
|
+
}
|
|
764
|
+
const applySnapshotModel = (model, search, read) => {
|
|
765
|
+
if (!model) return
|
|
766
|
+
const resolved = {
|
|
767
|
+
...model,
|
|
768
|
+
recall: { ...model.recall, state: search === 'active' ? 'active' : model.recall.state },
|
|
769
|
+
read: { ...model.read, state: read === 'active' ? 'active' : model.read.state },
|
|
770
|
+
query: { ...model.query,
|
|
771
|
+
state: search === 'active' ? 'active' : model.query.state,
|
|
772
|
+
status: search === 'active' ? 'QUERYING RETRIEVAL SERVICE' : model.query.status },
|
|
773
|
+
source: { ...model.source,
|
|
774
|
+
state: read === 'active' ? 'active' : model.source.state,
|
|
775
|
+
status: read === 'active' ? 'READING SOURCE CONTEXT' : model.source.status },
|
|
776
|
+
verify: { ...model.verify,
|
|
777
|
+
state: read === 'active' ? 'active' : model.verify.state,
|
|
778
|
+
meta: read === 'active' ? '正在核对来源与主张' : model.verify.meta },
|
|
779
|
+
}
|
|
780
|
+
setStage('.prts-node-plan', resolved.plan)
|
|
781
|
+
setStage('.prts-node-recall', resolved.recall)
|
|
782
|
+
setStage('.prts-node-read', resolved.read)
|
|
783
|
+
setStage('.prts-node-verify', resolved.verify)
|
|
784
|
+
setTrace('.prts-trace-query', resolved.query)
|
|
785
|
+
setTrace('.prts-trace-source', resolved.source)
|
|
786
|
+
const stack = scene.querySelectorAll('.prts-trace-stack > span')
|
|
787
|
+
resolved.stack.forEach((text, index) => {
|
|
788
|
+
if (!stack[index]) return
|
|
789
|
+
stack[index].textContent = text
|
|
790
|
+
stack[index].dataset.state = resolved.stackStates?.[index] || 'standby'
|
|
791
|
+
})
|
|
792
|
+
const caption = scene.querySelector('.prts-system-caption > em')
|
|
793
|
+
if (caption) caption.textContent = resolved.running ? 'ACTIVE' : resolved.tickerState
|
|
794
|
+
}
|
|
795
|
+
const sync = () => {
|
|
796
|
+
frame = null
|
|
797
|
+
const conversation = document.querySelector('[data-phase]:not(#prts-agent-scene)')
|
|
798
|
+
// 路由切走时 Conversation 会从 DOM 暂时卸载。它不代表新建了空白
|
|
799
|
+
// 会话,不能把 scene 重置成 hero;否则返回已有会话会重放整套退场
|
|
800
|
+
// 动画。这里只隐藏场景并保留阶段、计数与工具状态,等会话重新挂载。
|
|
801
|
+
if (!conversation) {
|
|
802
|
+
scene.dataset.page = 'hidden'
|
|
803
|
+
return
|
|
804
|
+
}
|
|
805
|
+
scene.dataset.page = 'conversation'
|
|
806
|
+
const conversationRect = conversation?.getBoundingClientRect?.()
|
|
807
|
+
if (conversationRect?.width > 0) {
|
|
808
|
+
const shellOverlay = document.querySelector('[data-shell-overlay]')
|
|
809
|
+
const shell = shellOverlay?.parentElement
|
|
810
|
+
const sidebarRect = shell?.querySelector(':scope > div:nth-of-type(1)')?.getBoundingClientRect?.()
|
|
811
|
+
const contentLeft = Math.max(conversationRect.left, sidebarRect?.right ?? 0)
|
|
812
|
+
const contentRightEdge = conversationRect.right
|
|
813
|
+
const contentCenter = contentLeft + (contentRightEdge - contentLeft) / 2
|
|
814
|
+
scene.style.setProperty('--agent-content-left', `${contentLeft}px`)
|
|
815
|
+
scene.style.setProperty('--agent-content-right', `${document.documentElement.clientWidth - contentRightEdge}px`)
|
|
816
|
+
scene.style.setProperty('--agent-content-center', `${contentCenter}px`)
|
|
817
|
+
|
|
818
|
+
// Harness 的 hero composer 在某些 shell 布局下仍以 viewport 为
|
|
819
|
+
// 横轴。测量实际卡片中心并补偿到侧栏右侧内容列中心;把累计值
|
|
820
|
+
// 写在 Conversation 上,后续侧栏拖宽/折叠不会来回振荡。
|
|
821
|
+
const composerSeat = conversation.querySelector('[data-composer-seat]')
|
|
822
|
+
const composerCard = conversation.querySelector('[data-composer-card]')
|
|
823
|
+
const composerSeatRect = composerSeat?.getBoundingClientRect?.()
|
|
824
|
+
const composerRect = composerCard?.getBoundingClientRect?.()
|
|
825
|
+
if (composerRect?.width > 0) {
|
|
826
|
+
const currentShift = Number.parseFloat(conversation.style.getPropertyValue('--prts-agent-composer-shift')) || 0
|
|
827
|
+
const actualCenter = composerRect.left + composerRect.width / 2
|
|
828
|
+
const nextShift = currentShift + contentCenter - actualCenter
|
|
829
|
+
if (Math.abs(nextShift - currentShift) > .5) {
|
|
830
|
+
conversation.style.setProperty('--prts-agent-composer-shift', `${nextShift}px`)
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// 原版 CPU 在 fixed system-map,字标在其下方的 hero 流中。
|
|
835
|
+
// 当前场景是独立 overlay,因此用 SVG 的真实绘图 bbox 恢复两者
|
|
836
|
+
// 至少 34px 的视觉间距,而不是继续猜一个固定 top。
|
|
837
|
+
const cpu = scene.querySelector('.prts-cpu-assembly')
|
|
838
|
+
const wordmark = scene.querySelector('.prts-hero-wordmark')
|
|
839
|
+
const cpuRect = cpu?.getBoundingClientRect?.()
|
|
840
|
+
const cpuBox = typeof cpu?.getBBox === 'function' ? cpu.getBBox() : null
|
|
841
|
+
const wordmarkRect = wordmark?.getBoundingClientRect?.()
|
|
842
|
+
if (cpuRect?.height > 0 && cpuBox && wordmarkRect?.height > 0) {
|
|
843
|
+
const cpuArtBottom = cpuRect.top + ((cpuBox.y + cpuBox.height) / 410) * cpuRect.height
|
|
844
|
+
const currentHeroTop = Number.parseFloat(scene.style.getPropertyValue('--agent-hero-top')) || 371
|
|
845
|
+
const baseWordmarkTop = wordmarkRect.top - currentHeroTop + 371
|
|
846
|
+
const nextHeroTop = 371 + Math.max(0, 34 - (baseWordmarkTop - cpuArtBottom))
|
|
847
|
+
if (Math.abs(nextHeroTop - currentHeroTop) > .5) {
|
|
848
|
+
scene.style.setProperty('--agent-hero-top', `${nextHeroTop}px`)
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// 欢迎文案和 Harness composer 属于两个独立定位层。垂直测量必须
|
|
853
|
+
// 覆盖整个 seat(含工作区/模式工具栏),不能只测白色输入卡。
|
|
854
|
+
// 文案下方至少留 44px,同时给整个 composer 底部保留 24px。
|
|
855
|
+
// 用“去掉当前位移后的基准 top”计算,避免重复同步累加。
|
|
856
|
+
const heroRect = scene.querySelector('.prts-empty-hero')?.getBoundingClientRect?.()
|
|
857
|
+
if (conversation.dataset.phase === 'hero' && composerSeatRect?.height > 0 && heroRect?.height > 0) {
|
|
858
|
+
const currentY = Number.parseFloat(
|
|
859
|
+
conversation.style.getPropertyValue('--prts-agent-composer-y'),
|
|
860
|
+
) || 0
|
|
861
|
+
const baseComposerTop = composerSeatRect.top - currentY
|
|
862
|
+
const contentBottom = Math.min(document.documentElement.clientHeight, conversationRect.bottom)
|
|
863
|
+
const wantedY = Math.max(0, heroRect.bottom + 44 - baseComposerTop)
|
|
864
|
+
const maxY = Math.max(0, contentBottom - 24 - composerSeatRect.height - baseComposerTop)
|
|
865
|
+
const nextY = Math.min(wantedY, maxY)
|
|
866
|
+
if (Math.abs(nextY - currentY) > .5) {
|
|
867
|
+
conversation.style.setProperty('--prts-agent-composer-y', `${nextY}px`)
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
// 场景本身也会写 data-phase;必须只读取 Harness Conversation,
|
|
872
|
+
// 否则第一次写入 hero 后会永远读回自己,已有会话也不会退场。
|
|
873
|
+
const phase = conversation.dataset.phase ?? 'hero'
|
|
874
|
+
const search = stateFor(['cloud_search', 'corpus_search', 'timeline_search'])
|
|
875
|
+
const read = stateFor(['corpus_read'])
|
|
876
|
+
if (phase === 'hero') {
|
|
877
|
+
hasShownHero = true
|
|
878
|
+
if (heroExitTimer !== null) clearTimeout(heroExitTimer)
|
|
879
|
+
heroExitTimer = null
|
|
880
|
+
scene.dataset.phase = 'hero'
|
|
881
|
+
} else if (phase === 'active' && hasShownHero && scene.dataset.phase !== 'active') {
|
|
882
|
+
scene.dataset.phase = 'leaving'
|
|
883
|
+
if (heroExitTimer === null) {
|
|
884
|
+
const reduceMotion = globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
|
885
|
+
heroExitTimer = setTimeout(() => {
|
|
886
|
+
heroExitTimer = null
|
|
887
|
+
scene.dataset.phase = 'active'
|
|
888
|
+
}, reduceMotion ? 0 : 760)
|
|
889
|
+
}
|
|
890
|
+
} else if (scene.dataset.phase !== 'leaving') {
|
|
891
|
+
scene.dataset.phase = phase
|
|
892
|
+
}
|
|
893
|
+
scene.dataset.search = search
|
|
894
|
+
scene.dataset.read = read
|
|
895
|
+
applySnapshotModel(sceneSnapshotModel, search, read)
|
|
896
|
+
const snapshotRunning = sceneSnapshotModel?.running === true
|
|
897
|
+
scene.dataset.running = search === 'active' || read === 'active' || snapshotRunning ? 'true' : 'false'
|
|
898
|
+
const ticker = scene.querySelector('.prts-system-ticker')
|
|
899
|
+
const tickerLabel = ticker?.querySelector('span')
|
|
900
|
+
const tickerState = ticker?.querySelector('i')
|
|
901
|
+
if (tickerLabel) tickerLabel.textContent = read === 'active' ? '正在核验原文'
|
|
902
|
+
: search === 'active' ? '正在检索候选'
|
|
903
|
+
: sceneSnapshotModel?.ticker || (phase === 'active' ? '本地证据链' : '本地检索')
|
|
904
|
+
if (tickerState) tickerState.textContent = scene.dataset.running === 'true'
|
|
905
|
+
? 'LIVE' : sceneSnapshotModel?.tickerState || (phase === 'active' ? 'DONE' : 'IDLE')
|
|
906
|
+
}
|
|
907
|
+
notifySceneSnapshot = () => schedule()
|
|
908
|
+
const schedule = () => { if (frame == null) frame = requestAnimationFrame(sync) }
|
|
909
|
+
globalThis.addEventListener('resize', schedule)
|
|
910
|
+
const observer = new MutationObserver((records) => {
|
|
911
|
+
// 更新场景自身的 dataset / ticker 也会产生 mutation;忽略这些回声,
|
|
912
|
+
// 避免每帧重新同步。侧栏折叠和拖宽仍由外部节点变更触发。
|
|
913
|
+
if (records.every((record) => record.target === scene || scene.contains(record.target))) return
|
|
914
|
+
schedule()
|
|
915
|
+
})
|
|
916
|
+
observer.observe(document.body, { subtree: true, childList: true, attributes: true,
|
|
917
|
+
attributeFilter: ['data-state', 'data-phase', 'data-tool', 'data-sidebar-collapsed', 'style'] })
|
|
918
|
+
sync()
|
|
919
|
+
return () => {
|
|
920
|
+
observer.disconnect()
|
|
921
|
+
globalThis.removeEventListener('keydown', handleCpuDebugKeydown)
|
|
922
|
+
globalThis.removeEventListener('resize', schedule)
|
|
923
|
+
if (heroExitTimer !== null) clearTimeout(heroExitTimer)
|
|
924
|
+
const conversation = document.querySelector('[data-phase]:not(#prts-agent-scene)')
|
|
925
|
+
conversation?.style.removeProperty('--prts-agent-composer-shift')
|
|
926
|
+
conversation?.style.removeProperty('--prts-agent-composer-y')
|
|
927
|
+
if (frame != null) cancelAnimationFrame(frame)
|
|
928
|
+
notifySceneSnapshot = () => {}
|
|
929
|
+
scene.remove()
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// PRTS Agent 开机场(场景动画 + MutationObserver)只在对应皮肤下挂载;
|
|
934
|
+
// harness / 终末地皮肤不承担这份常驻 DOM 与监听开销。
|
|
935
|
+
const syncScene = () => {
|
|
936
|
+
if (activeSkin === 'prts-agent') {
|
|
937
|
+
if (!disposeScene) disposeScene = installScene()
|
|
938
|
+
} else if (disposeScene) {
|
|
939
|
+
disposeScene()
|
|
940
|
+
disposeScene = null
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// 浏览器半边是预构建 classic script,不能直接 import CSS;Host 只暴露这两份
|
|
945
|
+
// 固定白名单资源。候选样式 load 成功后才交给 setSkin 做二阶段提交,旧样式在此
|
|
946
|
+
// 之前始终保留,避免 404、超时和快速切换留下“有皮肤状态、无皮肤 CSS”。
|
|
947
|
+
const SKIN_COMMON_STYLESHEET = Object.freeze({
|
|
948
|
+
id: 'common', href: '/api/prts-corpus/skins/common.css',
|
|
949
|
+
})
|
|
950
|
+
const SKIN_STYLESHEETS = Object.freeze({
|
|
951
|
+
'prts-agent': '/api/prts-corpus/skins/prts-agent.css',
|
|
952
|
+
'endfield-aic': '/api/prts-corpus/skins/endfield-aic.css',
|
|
953
|
+
'rhine-lab': '/api/prts-corpus/skins/rhine-lab.css',
|
|
954
|
+
})
|
|
955
|
+
const SKIN_STYLESHEET_TIMEOUT_MS = 8000
|
|
956
|
+
const createSkinStylesheet = (id, href) => {
|
|
957
|
+
const link = document.createElement('link')
|
|
958
|
+
link.rel = 'stylesheet'
|
|
959
|
+
link.href = href
|
|
960
|
+
link.dataset.plugin = 'prts-terrarchive'
|
|
961
|
+
link.dataset.pluginCss = `${id}-skin`
|
|
962
|
+
return link
|
|
963
|
+
}
|
|
964
|
+
const installSkinStylesheets = () => {
|
|
965
|
+
// Defend against an overlapping/re-entrant apply that installs a replacement
|
|
966
|
+
// effect before the previous cleanup. Retire that owner now so its ready
|
|
967
|
+
// candidate/committed links cannot survive beside the replacement manager.
|
|
968
|
+
skinStylesheetOwner?.dispose?.()
|
|
969
|
+
const owner = {}
|
|
970
|
+
// This tiny fail-closed rule prevents a missing common.css from exposing
|
|
971
|
+
// the always-registered evidence UI as naked browser controls.
|
|
972
|
+
const commonSafetyStyle = document.createElement('style')
|
|
973
|
+
commonSafetyStyle.dataset.plugin = 'prts-terrarchive'
|
|
974
|
+
commonSafetyStyle.dataset.pluginCss = 'common-safety'
|
|
975
|
+
commonSafetyStyle.textContent = '.prts-header-badge,.prts-evidence-layer{display:none}'
|
|
976
|
+
document.head.appendChild(commonSafetyStyle)
|
|
977
|
+
let commonLink = null
|
|
978
|
+
let commonRequest = null
|
|
979
|
+
let committedLink = null
|
|
980
|
+
let committedSkin = 'harness'
|
|
981
|
+
let currentRequest = null
|
|
982
|
+
let disposed = false
|
|
983
|
+
|
|
984
|
+
const ensureCommon = () => {
|
|
985
|
+
if (disposed) return Promise.reject(makeAbortError('皮肤样式管理器已卸载'))
|
|
986
|
+
if (commonLink) return Promise.resolve(commonLink)
|
|
987
|
+
if (commonRequest) return commonRequest.promise
|
|
988
|
+
const candidate = createSkinStylesheet(
|
|
989
|
+
SKIN_COMMON_STYLESHEET.id, SKIN_COMMON_STYLESHEET.href)
|
|
990
|
+
let resolveReady
|
|
991
|
+
let rejectReady
|
|
992
|
+
const request = {
|
|
993
|
+
candidate, timer: null, finished: false,
|
|
994
|
+
promise: new Promise((resolve, reject) => {
|
|
995
|
+
resolveReady = resolve
|
|
996
|
+
rejectReady = reject
|
|
997
|
+
}),
|
|
998
|
+
finish(error) {
|
|
999
|
+
if (request.finished) return
|
|
1000
|
+
request.finished = true
|
|
1001
|
+
clearTimeout(request.timer)
|
|
1002
|
+
candidate.removeEventListener('load', onLoad)
|
|
1003
|
+
candidate.removeEventListener('error', onError)
|
|
1004
|
+
if (commonRequest === request) commonRequest = null
|
|
1005
|
+
if (error) {
|
|
1006
|
+
candidate.remove()
|
|
1007
|
+
rejectReady(error)
|
|
1008
|
+
return
|
|
1009
|
+
}
|
|
1010
|
+
commonLink = candidate
|
|
1011
|
+
commonSafetyStyle.remove()
|
|
1012
|
+
resolveReady(candidate)
|
|
1013
|
+
},
|
|
1014
|
+
}
|
|
1015
|
+
const onLoad = () => request.finish()
|
|
1016
|
+
const onError = () => {
|
|
1017
|
+
console.error('[prts-terrarchive] failed to load common stylesheet')
|
|
1018
|
+
request.finish(new Error('公共皮肤样式加载失败'))
|
|
1019
|
+
}
|
|
1020
|
+
candidate.addEventListener('load', onLoad, { once: true })
|
|
1021
|
+
candidate.addEventListener('error', onError, { once: true })
|
|
1022
|
+
request.timer = setTimeout(() => {
|
|
1023
|
+
request.finish(new Error('公共皮肤样式加载超时'))
|
|
1024
|
+
}, SKIN_STYLESHEET_TIMEOUT_MS)
|
|
1025
|
+
commonRequest = request
|
|
1026
|
+
document.head.appendChild(candidate)
|
|
1027
|
+
return request.promise
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const invalidateCurrent = (error = makeAbortError('皮肤样式加载已被取代')) => {
|
|
1031
|
+
const request = currentRequest
|
|
1032
|
+
if (!request) return
|
|
1033
|
+
currentRequest = null
|
|
1034
|
+
request.rollback(error)
|
|
1035
|
+
}
|
|
1036
|
+
const prepare = async (skin) => {
|
|
1037
|
+
const id = normalizeSkin(skin)
|
|
1038
|
+
invalidateCurrent()
|
|
1039
|
+
if (disposed) return Promise.reject(makeAbortError('皮肤样式管理器已卸载'))
|
|
1040
|
+
await ensureCommon()
|
|
1041
|
+
if (disposed) throw makeAbortError('皮肤样式管理器已卸载')
|
|
1042
|
+
// Even an already-resolved common promise yields a microtask. A newer
|
|
1043
|
+
// back-to-back switch may have installed its request in that gap.
|
|
1044
|
+
invalidateCurrent()
|
|
1045
|
+
const href = SKIN_STYLESHEETS[id]
|
|
1046
|
+
if (committedSkin === id) return Promise.resolve(NOOP_STYLESHEET_TRANSACTION)
|
|
1047
|
+
if (!href) {
|
|
1048
|
+
let finished = false
|
|
1049
|
+
return Promise.resolve({
|
|
1050
|
+
commit() {
|
|
1051
|
+
if (finished || disposed) return
|
|
1052
|
+
finished = true
|
|
1053
|
+
committedLink?.remove()
|
|
1054
|
+
committedLink = null
|
|
1055
|
+
committedSkin = 'harness'
|
|
1056
|
+
},
|
|
1057
|
+
rollback() { finished = true },
|
|
1058
|
+
})
|
|
1059
|
+
}
|
|
1060
|
+
const candidate = createSkinStylesheet(id, href)
|
|
1061
|
+
let resolveReady
|
|
1062
|
+
let rejectReady
|
|
1063
|
+
const request = {
|
|
1064
|
+
id, candidate, state: 'loading', finished: false, timer: null,
|
|
1065
|
+
promise: new Promise((resolve, reject) => {
|
|
1066
|
+
resolveReady = resolve
|
|
1067
|
+
rejectReady = reject
|
|
1068
|
+
}),
|
|
1069
|
+
cleanup() {
|
|
1070
|
+
clearTimeout(request.timer)
|
|
1071
|
+
candidate.removeEventListener('load', onLoad)
|
|
1072
|
+
candidate.removeEventListener('error', onError)
|
|
1073
|
+
},
|
|
1074
|
+
commit() {
|
|
1075
|
+
if (request.finished || request.state !== 'ready' || disposed
|
|
1076
|
+
|| currentRequest !== request) {
|
|
1077
|
+
throw makeAbortError('皮肤样式事务已失效')
|
|
1078
|
+
}
|
|
1079
|
+
request.finished = true
|
|
1080
|
+
request.cleanup()
|
|
1081
|
+
if (currentRequest === request) currentRequest = null
|
|
1082
|
+
committedLink?.remove()
|
|
1083
|
+
committedLink = candidate
|
|
1084
|
+
committedSkin = id
|
|
1085
|
+
},
|
|
1086
|
+
rollback(error) {
|
|
1087
|
+
if (request.finished) return
|
|
1088
|
+
request.finished = true
|
|
1089
|
+
request.cleanup()
|
|
1090
|
+
candidate.remove()
|
|
1091
|
+
if (currentRequest === request) currentRequest = null
|
|
1092
|
+
if (request.state === 'loading') rejectReady(error)
|
|
1093
|
+
},
|
|
1094
|
+
}
|
|
1095
|
+
const onLoad = () => {
|
|
1096
|
+
if (currentRequest !== request || request.finished) return
|
|
1097
|
+
request.state = 'ready'
|
|
1098
|
+
request.cleanup()
|
|
1099
|
+
resolveReady(request)
|
|
1100
|
+
}
|
|
1101
|
+
const onError = () => {
|
|
1102
|
+
console.error(`[prts-terrarchive] failed to load ${id} skin stylesheet`)
|
|
1103
|
+
request.rollback(new Error(`${id} 皮肤样式加载失败`))
|
|
1104
|
+
}
|
|
1105
|
+
candidate.addEventListener('load', onLoad, { once: true })
|
|
1106
|
+
candidate.addEventListener('error', onError, { once: true })
|
|
1107
|
+
request.timer = setTimeout(() => {
|
|
1108
|
+
request.rollback(new Error(`${id} 皮肤样式加载超时`))
|
|
1109
|
+
}, SKIN_STYLESHEET_TIMEOUT_MS)
|
|
1110
|
+
currentRequest = request
|
|
1111
|
+
document.head.appendChild(candidate)
|
|
1112
|
+
return request.promise
|
|
1113
|
+
}
|
|
1114
|
+
const dispose = () => {
|
|
1115
|
+
if (disposed) return
|
|
1116
|
+
disposed = true
|
|
1117
|
+
invalidateCurrent(makeAbortError('皮肤样式管理器已卸载'))
|
|
1118
|
+
commonRequest?.finish(makeAbortError('皮肤样式管理器已卸载'))
|
|
1119
|
+
commonRequest = null
|
|
1120
|
+
commonLink?.remove()
|
|
1121
|
+
commonSafetyStyle.remove()
|
|
1122
|
+
committedLink?.remove()
|
|
1123
|
+
if (skinStylesheetOwner === owner) {
|
|
1124
|
+
skinApplyEpoch += 1
|
|
1125
|
+
skinStylesheetOwner = null
|
|
1126
|
+
prepareSkinStylesheet = async () => NOOP_STYLESHEET_TRANSACTION
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
owner.prepare = prepare
|
|
1130
|
+
owner.dispose = dispose
|
|
1131
|
+
skinStylesheetOwner = owner
|
|
1132
|
+
prepareSkinStylesheet = prepare
|
|
1133
|
+
// Start fetching immediately; prepare() joins this request and treats any
|
|
1134
|
+
// failure as a transaction failure. A later switch retries with a new link.
|
|
1135
|
+
void ensureCommon().catch(() => {})
|
|
1136
|
+
return dispose
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
const ENDPOINTS = Object.freeze({
|
|
1140
|
+
'/status': 'status', '/releases': 'releases', '/check-update': 'check-update',
|
|
1141
|
+
'/config:GET': 'config.get', '/config:PUT': 'config.update',
|
|
1142
|
+
'/download': 'download', '/activate': 'activate', '/delete': 'delete',
|
|
1143
|
+
})
|
|
1144
|
+
const jsonFetch = async (path, init = {}) => {
|
|
1145
|
+
if (!callApi) throw new Error('PRTS Connection 尚未就绪')
|
|
1146
|
+
const method = init.method ?? 'GET'
|
|
1147
|
+
const endpoint = ENDPOINTS[`${path}:${method}`] ?? ENDPOINTS[path]
|
|
1148
|
+
if (!endpoint) throw new Error(`未知 PRTS 操作 ${method} ${path}`)
|
|
1149
|
+
const payload = init.body ? JSON.parse(init.body) : {}
|
|
1150
|
+
return callApi(endpoint, payload, init.signal)
|
|
1151
|
+
}
|
|
1152
|
+
const makeAbortError = (message = '操作已取消') => {
|
|
1153
|
+
const error = new Error(message)
|
|
1154
|
+
error.name = 'AbortError'
|
|
1155
|
+
return error
|
|
1156
|
+
}
|
|
1157
|
+
const beginHostSkinSync = () => {
|
|
1158
|
+
hostSkinSyncGeneration += 1
|
|
1159
|
+
return hostSkinSyncGeneration
|
|
1160
|
+
}
|
|
1161
|
+
const assertHostSkinSyncActive = (generation) => {
|
|
1162
|
+
if (generation !== hostSkinSyncGeneration) {
|
|
1163
|
+
throw makeAbortError('Host 皮肤快照已被更新的请求取代')
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
const assertApplyActive = (generation, signal) => {
|
|
1167
|
+
if (signal?.aborted || generation !== applyGeneration || !clientContext) throw makeAbortError()
|
|
1168
|
+
}
|
|
1169
|
+
const assertConfiguredSkinActive = (generation, signal, skinEpoch, hostSyncGeneration) => {
|
|
1170
|
+
assertApplyActive(generation, signal)
|
|
1171
|
+
if (skinEpoch !== skinOperationEpoch) throw makeAbortError('皮肤配置已被新的用户操作取代')
|
|
1172
|
+
assertHostSkinSyncActive(hostSyncGeneration)
|
|
1173
|
+
}
|
|
1174
|
+
const waitWithDeadline = (promise, { signal, timeout = 10000, label = '请求' } = {}) =>
|
|
1175
|
+
new Promise((resolve, reject) => {
|
|
1176
|
+
let settled = false
|
|
1177
|
+
const finish = (callback, value) => {
|
|
1178
|
+
if (settled) return
|
|
1179
|
+
settled = true
|
|
1180
|
+
clearTimeout(timer)
|
|
1181
|
+
signal?.removeEventListener('abort', onAbort)
|
|
1182
|
+
callback(value)
|
|
1183
|
+
}
|
|
1184
|
+
const onAbort = () => finish(reject, makeAbortError())
|
|
1185
|
+
const timer = setTimeout(() => finish(reject, new Error(`${label}超时`)), timeout)
|
|
1186
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
1187
|
+
if (signal?.aborted) { onAbort(); return }
|
|
1188
|
+
Promise.resolve(promise).then(
|
|
1189
|
+
(value) => finish(resolve, value),
|
|
1190
|
+
(error) => finish(reject, error),
|
|
1191
|
+
)
|
|
1192
|
+
})
|
|
1193
|
+
const writeSkinConfig = async (skin, {
|
|
1194
|
+
signal = clientAbortController?.signal, timeout = 10000,
|
|
1195
|
+
} = {}) => {
|
|
1196
|
+
if (signal?.aborted) throw makeAbortError('皮肤配置写入已取消')
|
|
1197
|
+
const controller = new AbortController()
|
|
1198
|
+
let timer = null
|
|
1199
|
+
let rejectControl
|
|
1200
|
+
const control = new Promise((_resolve, reject) => { rejectControl = reject })
|
|
1201
|
+
const cancel = (error) => {
|
|
1202
|
+
if (controller.signal.aborted) return
|
|
1203
|
+
controller.abort(error)
|
|
1204
|
+
rejectControl(error)
|
|
1205
|
+
}
|
|
1206
|
+
const onAbort = () => cancel(makeAbortError('皮肤配置写入已取消'))
|
|
1207
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
1208
|
+
timer = setTimeout(() => cancel(makeAbortError('皮肤配置写入超时')), timeout)
|
|
1209
|
+
try {
|
|
1210
|
+
return await Promise.race([
|
|
1211
|
+
jsonFetch('/config', {
|
|
1212
|
+
method: 'PUT', body: JSON.stringify({ patch: { uiSkin: normalizeSkin(skin) } }),
|
|
1213
|
+
signal: controller.signal,
|
|
1214
|
+
}),
|
|
1215
|
+
control,
|
|
1216
|
+
])
|
|
1217
|
+
} finally {
|
|
1218
|
+
clearTimeout(timer)
|
|
1219
|
+
signal?.removeEventListener('abort', onAbort)
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
const fetchWithDeadline = async (url, init, { signal, timeout = 8000 } = {}) => {
|
|
1223
|
+
const controller = new AbortController()
|
|
1224
|
+
const abort = () => controller.abort(signal?.reason)
|
|
1225
|
+
signal?.addEventListener('abort', abort, { once: true })
|
|
1226
|
+
if (signal?.aborted) abort()
|
|
1227
|
+
const timer = setTimeout(() => controller.abort(makeAbortError('配置请求超时')), timeout)
|
|
1228
|
+
try {
|
|
1229
|
+
return await fetch(url, { ...init, signal: controller.signal })
|
|
1230
|
+
} finally {
|
|
1231
|
+
clearTimeout(timer)
|
|
1232
|
+
signal?.removeEventListener('abort', abort)
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
const activateConfiguredSkin = (skin, options = {}) => {
|
|
1236
|
+
const next = normalizeSkin(skin)
|
|
1237
|
+
if (next === 'endfield-aic' && !aicBootEl) aicBootShow('CONNECTING TERMINAL')
|
|
1238
|
+
return setSkin(next, options)
|
|
1239
|
+
}
|
|
1240
|
+
const loadConfiguredSkin = async ({ signal, generation, skinEpoch, hostSyncGeneration }) => {
|
|
1241
|
+
const assertCurrent = () => {
|
|
1242
|
+
assertConfiguredSkinActive(generation, signal, skinEpoch, hostSyncGeneration)
|
|
1243
|
+
}
|
|
1244
|
+
let resolved = null
|
|
1245
|
+
if (typeof fetch === 'function') {
|
|
1246
|
+
try {
|
|
1247
|
+
const response = await fetchWithDeadline('/api/prts-corpus/ui-skin.json', { cache: 'no-store' }, { signal })
|
|
1248
|
+
assertCurrent()
|
|
1249
|
+
if (response.ok) {
|
|
1250
|
+
const payload = await waitWithDeadline(response.json(), {
|
|
1251
|
+
signal, timeout: 3000, label: '皮肤配置解析',
|
|
1252
|
+
})
|
|
1253
|
+
assertCurrent()
|
|
1254
|
+
resolved = await activateConfiguredSkin(payload?.uiSkin, { assertCurrent })
|
|
1255
|
+
}
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
if (signal?.aborted || generation !== applyGeneration
|
|
1258
|
+
|| skinEpoch !== skinOperationEpoch
|
|
1259
|
+
|| hostSyncGeneration !== hostSkinSyncGeneration) throw makeAbortError()
|
|
1260
|
+
console.error('[prts-terrarchive] skin endpoint activation failed', error)
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
if (resolved === null) {
|
|
1264
|
+
try {
|
|
1265
|
+
const status = await waitWithDeadline(jsonFetch('/status'), {
|
|
1266
|
+
signal, timeout: 10000, label: '皮肤配置 RPC',
|
|
1267
|
+
})
|
|
1268
|
+
assertCurrent()
|
|
1269
|
+
resolved = await activateConfiguredSkin(status?.config?.uiSkin, { assertCurrent })
|
|
1270
|
+
} catch (error) {
|
|
1271
|
+
if (error?.name === 'AbortError') throw error
|
|
1272
|
+
// 配置通道失败时立即撤下开机场,回退到当前界面,绝不能把 boot 屏留在原地。
|
|
1273
|
+
aicBootAbort()
|
|
1274
|
+
throw error
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
assertCurrent()
|
|
1278
|
+
// setSkin 已在 stylesheet 提交后写入;这里用最终解析值再次校准启动缓存。
|
|
1279
|
+
try { if (resolved) localStorage.setItem(AIC_BOOT_KEY, resolved) } catch { /* 忽略 */ }
|
|
1280
|
+
if (resolved !== 'endfield-aic') aicBootAbort()
|
|
1281
|
+
return resolved
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/* ---- 证据抽取:优先走会话快照(不依赖视图 DOM),DOM 抓取仅作回退 ---- */
|
|
1285
|
+
|
|
1286
|
+
const rowText = (row) => String(row.textContent ?? '').replace(/\s+/g, ' ').trim()
|
|
1287
|
+
const DATA_VERSION_PATTERN = /^[0-9a-f]{64}$/
|
|
1288
|
+
const validDataVersion = (value) => typeof value === 'string'
|
|
1289
|
+
&& DATA_VERSION_PATTERN.test(value)
|
|
1290
|
+
|
|
1291
|
+
/** 一条 corpus_read 模型投影 → 人类可读标题与引用范围。 */
|
|
1292
|
+
const readPresentation = (value) => {
|
|
1293
|
+
if (!value || value.kind !== 'prts-corpus-read-v1' || !value.locator
|
|
1294
|
+
|| typeof value.locator !== 'object') return null
|
|
1295
|
+
const documentId = typeof value.locator.document_id === 'string'
|
|
1296
|
+
&& value.locator.document_id.length <= 512 ? value.locator.document_id : ''
|
|
1297
|
+
const sourceRef = typeof value.locator.source_ref === 'string'
|
|
1298
|
+
&& value.locator.source_ref.length <= 1024 ? value.locator.source_ref : ''
|
|
1299
|
+
const documentUid = typeof value.locator.document_uid === 'string'
|
|
1300
|
+
&& /^doc_[A-Za-z0-9_-]{16}$/.test(value.locator.document_uid)
|
|
1301
|
+
? value.locator.document_uid : ''
|
|
1302
|
+
if (!documentId && !sourceRef) return null
|
|
1303
|
+
const primary = {
|
|
1304
|
+
documentId, documentUid, sourceRef,
|
|
1305
|
+
// 只有契约中的 sha256 才能将原文绑定到确定版本。旧会话里的
|
|
1306
|
+
// 人类版本名或截断 hash 仅供展示,不能标记 VERIFIED。
|
|
1307
|
+
dataVersion: validDataVersion(value.data_version) ? value.data_version : '',
|
|
1308
|
+
title: typeof value.title === 'string' && value.title.length <= 512 ? value.title : '',
|
|
1309
|
+
lineStart: Number.isInteger(value.line_start) && value.line_start > 0 ? value.line_start : null,
|
|
1310
|
+
lineEnd: Number.isInteger(value.line_end) && value.line_end > 0 ? value.line_end : null,
|
|
1311
|
+
}
|
|
1312
|
+
const sources = Array.isArray(value.sources) ? value.sources.slice(0, 128).map((source) => {
|
|
1313
|
+
const sourceDocumentId = typeof source?.document_id === 'string'
|
|
1314
|
+
&& source.document_id.length <= 512 ? source.document_id : ''
|
|
1315
|
+
if (!sourceDocumentId) return null
|
|
1316
|
+
const documentUid = typeof source.document_uid === 'string'
|
|
1317
|
+
&& /^doc_[A-Za-z0-9_-]{16}$/.test(source.document_uid) ? source.document_uid : ''
|
|
1318
|
+
return { documentId: sourceDocumentId, documentUid, sourceRef: '', dataVersion: primary.dataVersion,
|
|
1319
|
+
title: typeof source.title === 'string' && source.title.length <= 512 ? source.title : '',
|
|
1320
|
+
lineStart: Number.isInteger(source.line_start) && source.line_start > 0 ? source.line_start : null,
|
|
1321
|
+
lineEnd: Number.isInteger(source.line_end) && source.line_end > 0 ? source.line_end : null }
|
|
1322
|
+
}).filter(Boolean) : []
|
|
1323
|
+
return { ...primary, sources }
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
const rememberReadBlock = (byTitle, full, rawPresentation) => {
|
|
1327
|
+
const presentation = readPresentation(rawPresentation)
|
|
1328
|
+
const titleMatch = full.match(/#\s+(.+?)\s+(?:范围:|字段:)/u)
|
|
1329
|
+
const citationMatch = full.match(/引用:《([^》]+)》(?:(document_uid=(doc_[A-Za-z0-9_-]{16})))?(?:第\s*(\d+)(?:[-–](\d+))?\s*行|Wiki)/u)
|
|
1330
|
+
const projectedSources = presentation?.sources?.length ? presentation.sources : [presentation || {}]
|
|
1331
|
+
for (const source of projectedSources) {
|
|
1332
|
+
const title = String(source.title || presentation?.title || citationMatch?.[1]
|
|
1333
|
+
|| titleMatch?.[1] || '').replace(/\s+/g, ' ').trim()
|
|
1334
|
+
if (!title) continue
|
|
1335
|
+
const lineStart = source.lineStart ?? (citationMatch?.[3] ? Number(citationMatch[3]) : null)
|
|
1336
|
+
const lineEnd = source.lineEnd ?? (citationMatch?.[4] ? Number(citationMatch[4]) : lineStart)
|
|
1337
|
+
const excerpt = presentation?.sources?.length > 1
|
|
1338
|
+
? `《${title}》${lineStart ? `第 ${lineStart}${lineEnd && lineEnd !== lineStart ? `-${lineEnd}` : ''} 行` : '资料片段'}`
|
|
1339
|
+
: full.slice(0, 260)
|
|
1340
|
+
// 同一轮内可能因重试/热更新重读同名文档;按会话顺序保留最后一次,
|
|
1341
|
+
// 避免 first-write-wins 把旧 data_version 固化到证据卡。
|
|
1342
|
+
const documentUid = source.documentUid || citationMatch?.[2] || ''
|
|
1343
|
+
const key = documentUid ? `${title}\u0000${documentUid}` : title
|
|
1344
|
+
byTitle.set(key, { title, lineStart, lineEnd, excerpt, documentUid,
|
|
1345
|
+
documentId: source.documentId || '', sourceRef: source.sourceRef || '',
|
|
1346
|
+
dataVersion: source.dataVersion || presentation?.dataVersion || '' })
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
/**
|
|
1351
|
+
* 会话快照版(首选):tool-call 节点的 tool-result 带 renderRead 全文,
|
|
1352
|
+
* 最后一条含正文的 assistant-step 是引用解析对象。与当前挂载的是哪个
|
|
1353
|
+
* 会话视图(chat / trajectory)无关。
|
|
1354
|
+
*/
|
|
1355
|
+
const collectSnapshotEvidence = (order, nodes) => {
|
|
1356
|
+
const byTitle = new Map()
|
|
1357
|
+
let lastAnswer = ''
|
|
1358
|
+
if (!Array.isArray(order) || !nodes) return { byTitle, lastAnswer }
|
|
1359
|
+
// Header 标注的是“本轮证据”:从最后一条 user 节点开始投影,不把
|
|
1360
|
+
// 上一轮的 corpus_read 在当前回答无引用时回退进来。无 user 的旧快照
|
|
1361
|
+
// 保持兼容,仍按完整 order 处理。
|
|
1362
|
+
let turnStart = 0
|
|
1363
|
+
for (let index = 0; index < order.length; index += 1) {
|
|
1364
|
+
const node = typeof nodes.get === 'function' ? nodes.get(order[index]) : null
|
|
1365
|
+
if (node?.kind === 'user') turnStart = index
|
|
1366
|
+
}
|
|
1367
|
+
for (const key of order.slice(turnStart)) {
|
|
1368
|
+
const node = typeof nodes.get === 'function' ? nodes.get(key) : null
|
|
1369
|
+
if (!node) continue
|
|
1370
|
+
if (node.kind === 'tool-call') {
|
|
1371
|
+
const root = node.data?.root
|
|
1372
|
+
if (root?.kind !== 'tool-result' || root.call?.name !== 'corpus_read') continue
|
|
1373
|
+
const full = contentText(root.content).replace(/\s+/g, ' ').trim()
|
|
1374
|
+
if (full.includes('引用:《')) rememberReadBlock(byTitle, full, root.meta)
|
|
1375
|
+
} else if (node.kind === 'assistant-step') {
|
|
1376
|
+
const text = assistantParts(node.data?.blocks).text
|
|
1377
|
+
if (text) lastAnswer = String(text).replace(/\s+/g, ' ').trim()
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
return { byTitle, lastAnswer }
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// ChatNodeStore and order retain their identities for same-key updates. Use
|
|
1384
|
+
// the evidence inputs themselves as the subscription value, including text
|
|
1385
|
+
// and metadata changes that do not alter the scene's status/text length.
|
|
1386
|
+
const evidenceSnapshotSignature = (snapshot) => {
|
|
1387
|
+
const { byTitle, lastAnswer } = collectSnapshotEvidence(snapshot?.order, snapshot?.nodes)
|
|
1388
|
+
return JSON.stringify({ reads: [...byTitle], lastAnswer })
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
/** DOM 版(回退):轨迹视图 tr[data-kind="tool"] 与默认会话视图 [data-tool="corpus_read"] 都要覆盖。 */
|
|
1392
|
+
const collectDomEvidence = () => {
|
|
1393
|
+
const byTitle = new Map()
|
|
1394
|
+
if (typeof document === 'undefined') return { byTitle, lastAnswer: '' }
|
|
1395
|
+
let lastAnswer = ''
|
|
1396
|
+
const flowRows = [...document.querySelectorAll('[data-chat-flow-kind]')]
|
|
1397
|
+
if (flowRows.length) {
|
|
1398
|
+
const turnStart = flowRows.findLastIndex((row) => row.dataset?.chatFlowKind === 'user')
|
|
1399
|
+
const currentRows = flowRows.slice(Math.max(0, turnStart))
|
|
1400
|
+
for (const row of currentRows) {
|
|
1401
|
+
const full = rowText(row)
|
|
1402
|
+
const isCorpusRead = row.matches?.('[data-tool="corpus_read"]')
|
|
1403
|
+
|| row.querySelector?.('[data-tool="corpus_read"]') || full.includes('corpus_read')
|
|
1404
|
+
if (row.dataset?.chatFlowKind === 'tool-call' && isCorpusRead
|
|
1405
|
+
&& full.includes('引用:《')) rememberReadBlock(byTitle, full)
|
|
1406
|
+
if (row.dataset?.chatFlowKind === 'assistant-step' && full
|
|
1407
|
+
&& !full.includes('引用:《')) lastAnswer = full
|
|
1408
|
+
}
|
|
1409
|
+
return { byTitle, lastAnswer }
|
|
1410
|
+
}
|
|
1411
|
+
// 轨迹表回退:同样从最后一条 USER/用户消息开始。
|
|
1412
|
+
const rows = [...document.querySelectorAll('tr[data-kind]')]
|
|
1413
|
+
const turnStart = rows.findLastIndex((row) => row.dataset?.kind === 'message'
|
|
1414
|
+
&& /USER|用户/i.test(row.querySelector('td[class*="event"]')?.textContent ?? ''))
|
|
1415
|
+
for (const row of rows.slice(Math.max(0, turnStart))) {
|
|
1416
|
+
const event = row.querySelector('td[class*="event"]')?.textContent ?? ''
|
|
1417
|
+
const full = rowText(row)
|
|
1418
|
+
if (row.dataset?.kind === 'tool' && full.includes('corpus_read')
|
|
1419
|
+
&& full.includes('引用:《')) rememberReadBlock(byTitle, full)
|
|
1420
|
+
if (row.dataset?.kind === 'message' && /ASSISTANT/i.test(event)) lastAnswer = full
|
|
1421
|
+
}
|
|
1422
|
+
return { byTitle, lastAnswer }
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/** 由读取映射 + 最后一条回答计算证据卡:只取回答真正引用的主要参考资料。 */
|
|
1426
|
+
const buildEvidence = ({ byTitle, lastAnswer }) => {
|
|
1427
|
+
// 「标题 → 文档」解析:优先精确匹配完整展示标题;回答可能用简写(如《黄金三角》),
|
|
1428
|
+
// 仅在唯一命中且非泛指词时做包含匹配,避免《行动前》这类短词误命中多篇。
|
|
1429
|
+
const resolveTitle = (raw, uid = '') => {
|
|
1430
|
+
const title = String(raw ?? '').replace(/\s+/g, ' ').trim()
|
|
1431
|
+
const values = [...byTitle.values()]
|
|
1432
|
+
const exact = values.filter((item) => item.title === title
|
|
1433
|
+
&& (!uid || item.documentUid === uid))
|
|
1434
|
+
if (exact.length === 1) return exact[0]
|
|
1435
|
+
if (title.length < 3) return null
|
|
1436
|
+
const candidates = values.filter((item) => item.title.length >= title.length
|
|
1437
|
+
&& item.title.includes(title) && (!uid || item.documentUid === uid))
|
|
1438
|
+
if (candidates.length !== 1) return null
|
|
1439
|
+
return candidates[0]
|
|
1440
|
+
}
|
|
1441
|
+
const cited = new Map()
|
|
1442
|
+
// 行内可溯源引用:《标题》…第 N(-M)? 行。
|
|
1443
|
+
for (const m of lastAnswer.matchAll(/《([^》]+)》(?:(document_uid=(doc_[A-Za-z0-9_-]{16})))?[^。;\n]{0,40}?第\s*(\d+)(?:\s*[-—–~]\s*(\d+))?\s*行/g)) {
|
|
1444
|
+
const hit = resolveTitle(m[1], m[2] || '')
|
|
1445
|
+
if (!hit) continue
|
|
1446
|
+
const lineStart = Number(m[3])
|
|
1447
|
+
const lineEnd = m[4] ? Number(m[4]) : lineStart
|
|
1448
|
+
cited.set(`${hit.title}\u0000${hit.documentUid || hit.documentId || ''}`,
|
|
1449
|
+
{ ...hit, lineStart, lineEnd })
|
|
1450
|
+
}
|
|
1451
|
+
// 其余《标题》引用(如「主要参考资料」清单),一并纳入。
|
|
1452
|
+
for (const m of lastAnswer.matchAll(/《([^》]+)》(?:(document_uid=(doc_[A-Za-z0-9_-]{16})))?/g)) {
|
|
1453
|
+
const hit = resolveTitle(m[1], m[2] || '')
|
|
1454
|
+
const key = hit ? `${hit.title}\u0000${hit.documentUid || hit.documentId || ''}` : ''
|
|
1455
|
+
if (hit && !cited.has(key)) cited.set(key, { ...hit, lineStart: hit.lineStart, lineEnd: hit.lineEnd })
|
|
1456
|
+
}
|
|
1457
|
+
// 回答里没有可映射引用时,仅回退到 collect* 已限定的本轮读取。
|
|
1458
|
+
const used = cited.size ? cited : new Map(byTitle)
|
|
1459
|
+
const seen = new Set()
|
|
1460
|
+
let index = 0
|
|
1461
|
+
return [...used.values()].map((item) => {
|
|
1462
|
+
index += 1
|
|
1463
|
+
const signature = [item.documentId, item.lineStart, item.title].join('|') || `source-${++index}`
|
|
1464
|
+
if (seen.has(signature)) return null
|
|
1465
|
+
seen.add(signature)
|
|
1466
|
+
return { id: `${index}-${signature.slice(0, 24)}`, title: item.title, kind: 'story',
|
|
1467
|
+
documentId: item.documentId || '', sourceRef: item.sourceRef || '', dataVersion: item.dataVersion || '',
|
|
1468
|
+
lineStart: item.lineStart, lineEnd: item.lineEnd,
|
|
1469
|
+
excerpt: item.excerpt || '已在回答中引用该资料,点开查看原文。' }
|
|
1470
|
+
}).filter(Boolean)
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
const readEvidenceFromPage = () => buildEvidence(collectDomEvidence())
|
|
1474
|
+
|
|
1475
|
+
// 点开证据卡 → 读全文:复用与 corpus_read 工具一致的 read RPC。
|
|
1476
|
+
async function readEvidenceSource(item, cursor = '') {
|
|
1477
|
+
const locator = item.sourceRef ? { source_ref: item.sourceRef }
|
|
1478
|
+
: item.documentId ? { document_id: item.documentId } : null
|
|
1479
|
+
if (!locator) throw new Error('该证据缺少稳定的原文定位,请从当前会话重新读取')
|
|
1480
|
+
if (!validDataVersion(item.dataVersion)) {
|
|
1481
|
+
throw new Error('该证据没有完整的 64 位资料版本,为避免打开错版原文,请重新读取')
|
|
1482
|
+
}
|
|
1483
|
+
if (cursor && (typeof cursor !== 'string' || cursor.length > 65536)) {
|
|
1484
|
+
throw new Error('原文续页定位无效,请重新打开证据')
|
|
1485
|
+
}
|
|
1486
|
+
const result = await callApi('read', { locator,
|
|
1487
|
+
selection: { mode: 'document', ...(cursor ? { cursor } : {}) },
|
|
1488
|
+
data_version: item.dataVersion,
|
|
1489
|
+
max_lines: 500, max_chars: 100000 })
|
|
1490
|
+
if (!result?.ok || result?.error) throw new Error(result?.error?.message || '无法读取原文')
|
|
1491
|
+
if (result.response?.data_version !== item.dataVersion) {
|
|
1492
|
+
throw new Error('返回的原文版本与证据不匹配,已拒绝展示')
|
|
1493
|
+
}
|
|
1494
|
+
return result.response
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
const evidencePage = (response) => {
|
|
1498
|
+
if (response?.status !== 'ok') throw new Error(response?.error?.message || '读取失败')
|
|
1499
|
+
const lines = Array.isArray(response?.content?.lines) ? response.content.lines.map((line) => ({
|
|
1500
|
+
line_number: line.line_number, speaker_raw: line.speaker_raw,
|
|
1501
|
+
text: line.text, source_ref: line.source_ref,
|
|
1502
|
+
})) : []
|
|
1503
|
+
const hasMore = response?.page?.has_more === true
|
|
1504
|
+
const rawCursor = response?.page?.next_cursor
|
|
1505
|
+
const nextCursor = hasMore && typeof rawCursor === 'string' && rawCursor.length <= 65536
|
|
1506
|
+
? rawCursor : ''
|
|
1507
|
+
const total = Number.isInteger(response?.page?.total) && response.page.total >= 0
|
|
1508
|
+
? response.page.total : null
|
|
1509
|
+
return { lines, hasMore, nextCursor, total }
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
const mergeEvidenceLines = (current, incoming) => {
|
|
1513
|
+
const seen = new Set(current.map((line) => `${line.line_number}|${line.source_ref || ''}`))
|
|
1514
|
+
return current.concat(incoming.filter((line) => {
|
|
1515
|
+
const key = `${line.line_number}|${line.source_ref || ''}`
|
|
1516
|
+
if (seen.has(key)) return false
|
|
1517
|
+
seen.add(key)
|
|
1518
|
+
return true
|
|
1519
|
+
}))
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
const MODAL_FOCUSABLE_SELECTOR = [
|
|
1523
|
+
'button:not([disabled])', '[href]', 'input:not([disabled])', 'select:not([disabled])',
|
|
1524
|
+
'textarea:not([disabled])', '[contenteditable="true"]', '[tabindex]:not([tabindex="-1"])',
|
|
1525
|
+
].join(',')
|
|
1526
|
+
const resolveFocusTarget = (target) => {
|
|
1527
|
+
const value = typeof target === 'function' ? target() : target
|
|
1528
|
+
return value?.current ?? value
|
|
1529
|
+
}
|
|
1530
|
+
function installDialogFocusTrap(dialog, options = {}) {
|
|
1531
|
+
if (!dialog) return () => {}
|
|
1532
|
+
const ownerDocument = dialog.ownerDocument
|
|
1533
|
+
|| (typeof document !== 'undefined' ? document : null)
|
|
1534
|
+
if (!ownerDocument) return () => {}
|
|
1535
|
+
const focusables = () => [...dialog.querySelectorAll(MODAL_FOCUSABLE_SELECTOR)]
|
|
1536
|
+
.filter((element) => element.isConnected !== false
|
|
1537
|
+
&& element.getAttribute?.('aria-hidden') !== 'true'
|
|
1538
|
+
&& !element.closest?.('[hidden],[inert],[aria-hidden="true"]')
|
|
1539
|
+
&& (typeof element.getClientRects !== 'function' || element.getClientRects().length > 0)
|
|
1540
|
+
&& ownerDocument.defaultView?.getComputedStyle?.(element)?.visibility !== 'hidden')
|
|
1541
|
+
const focus = (target) => {
|
|
1542
|
+
if (typeof target?.focus !== 'function' || target.isConnected === false) return false
|
|
1543
|
+
target.focus()
|
|
1544
|
+
return true
|
|
1545
|
+
}
|
|
1546
|
+
const initial = resolveFocusTarget(options.initialFocus)
|
|
1547
|
+
if (!focus(initial)) {
|
|
1548
|
+
const first = focusables()[0]
|
|
1549
|
+
if (!focus(first)) focus(dialog)
|
|
1550
|
+
}
|
|
1551
|
+
const onKeyDown = (event) => {
|
|
1552
|
+
if (event.defaultPrevented || options.isSuspended?.()) return
|
|
1553
|
+
if (event.key === 'Escape') {
|
|
1554
|
+
if (!options.onEscape) return
|
|
1555
|
+
event.preventDefault()
|
|
1556
|
+
options.onEscape()
|
|
1557
|
+
return
|
|
1558
|
+
}
|
|
1559
|
+
if (event.key !== 'Tab') return
|
|
1560
|
+
const items = focusables()
|
|
1561
|
+
if (!items.length) {
|
|
1562
|
+
event.preventDefault()
|
|
1563
|
+
focus(dialog)
|
|
1564
|
+
return
|
|
1565
|
+
}
|
|
1566
|
+
const first = items[0]
|
|
1567
|
+
const last = items[items.length - 1]
|
|
1568
|
+
if (!dialog.contains(ownerDocument.activeElement)) {
|
|
1569
|
+
event.preventDefault()
|
|
1570
|
+
focus(event.shiftKey ? last : first)
|
|
1571
|
+
} else if (event.shiftKey
|
|
1572
|
+
&& (ownerDocument.activeElement === first || ownerDocument.activeElement === dialog)) {
|
|
1573
|
+
event.preventDefault()
|
|
1574
|
+
focus(last)
|
|
1575
|
+
} else if (!event.shiftKey && ownerDocument.activeElement === last) {
|
|
1576
|
+
event.preventDefault()
|
|
1577
|
+
focus(first)
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
ownerDocument.addEventListener('keydown', onKeyDown)
|
|
1581
|
+
let active = true
|
|
1582
|
+
return () => {
|
|
1583
|
+
if (!active) return
|
|
1584
|
+
active = false
|
|
1585
|
+
ownerDocument.removeEventListener('keydown', onKeyDown)
|
|
1586
|
+
focus(resolveFocusTarget(options.returnFocus))
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
function SourceReader({ item, onClose, returnFocus }) {
|
|
1591
|
+
const [loading, setLoading] = useState(true)
|
|
1592
|
+
const [loadingMore, setLoadingMore] = useState(false)
|
|
1593
|
+
const [error, setError] = useState('')
|
|
1594
|
+
const [lines, setLines] = useState([])
|
|
1595
|
+
const [page, setPage] = useState({ hasMore: false, nextCursor: '', total: null })
|
|
1596
|
+
const generation = useRef(0)
|
|
1597
|
+
const sourceDialogRef = useRef(null)
|
|
1598
|
+
const onCloseRef = useRef(onClose)
|
|
1599
|
+
const returnFocusRef = useRef(returnFocus)
|
|
1600
|
+
onCloseRef.current = onClose
|
|
1601
|
+
returnFocusRef.current = returnFocus
|
|
1602
|
+
useEffect(() => {
|
|
1603
|
+
const currentGeneration = ++generation.current
|
|
1604
|
+
setLoading(true); setLoadingMore(false); setError(''); setLines([])
|
|
1605
|
+
setPage({ hasMore: false, nextCursor: '', total: null })
|
|
1606
|
+
readEvidenceSource(item).then((response) => {
|
|
1607
|
+
if (generation.current !== currentGeneration) return
|
|
1608
|
+
const first = evidencePage(response)
|
|
1609
|
+
setLines(first.lines)
|
|
1610
|
+
setPage({ hasMore: first.hasMore, nextCursor: first.nextCursor, total: first.total })
|
|
1611
|
+
if (first.hasMore && !first.nextCursor) {
|
|
1612
|
+
setError('原文仍有后续内容,但服务器未提供可用的续页定位。')
|
|
1613
|
+
}
|
|
1614
|
+
}).catch((err) => {
|
|
1615
|
+
if (generation.current === currentGeneration) setError(err.message || '读取失败')
|
|
1616
|
+
}).finally(() => {
|
|
1617
|
+
if (generation.current === currentGeneration) setLoading(false)
|
|
1618
|
+
})
|
|
1619
|
+
return () => {
|
|
1620
|
+
if (generation.current === currentGeneration) generation.current += 1
|
|
1621
|
+
}
|
|
1622
|
+
}, [item])
|
|
1623
|
+
useEffect(() => installDialogFocusTrap(sourceDialogRef.current, {
|
|
1624
|
+
onEscape: () => onCloseRef.current?.(),
|
|
1625
|
+
returnFocus: () => resolveFocusTarget(returnFocusRef.current),
|
|
1626
|
+
}), [item.id])
|
|
1627
|
+
const loadMore = async () => {
|
|
1628
|
+
if (loading || loadingMore || !page.hasMore || !page.nextCursor) return
|
|
1629
|
+
const cursor = page.nextCursor
|
|
1630
|
+
const currentGeneration = generation.current
|
|
1631
|
+
setLoadingMore(true); setError('')
|
|
1632
|
+
try {
|
|
1633
|
+
const next = evidencePage(await readEvidenceSource(item, cursor))
|
|
1634
|
+
if (generation.current !== currentGeneration) return
|
|
1635
|
+
setLines((current) => mergeEvidenceLines(current, next.lines))
|
|
1636
|
+
setPage({ hasMore: next.hasMore,
|
|
1637
|
+
// 服务器若误返原游标,立即停止,避免用户进入无限续页。
|
|
1638
|
+
nextCursor: next.nextCursor === cursor ? '' : next.nextCursor,
|
|
1639
|
+
total: next.total ?? page.total })
|
|
1640
|
+
if (next.hasMore && (!next.nextCursor || next.nextCursor === cursor)) {
|
|
1641
|
+
setError('原文仍有后续内容,但服务器未提供可用的续页定位。')
|
|
1642
|
+
}
|
|
1643
|
+
} catch (err) {
|
|
1644
|
+
if (generation.current === currentGeneration) setError(err.message || '续页读取失败')
|
|
1645
|
+
} finally {
|
|
1646
|
+
if (generation.current === currentGeneration) setLoadingMore(false)
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
return h('div', { className: 'prts-evidence-layer' },
|
|
1650
|
+
h('button', { type: 'button', className: 'prts-evidence-scrim', onClick: onClose,
|
|
1651
|
+
'aria-label': '关闭原文查看器' }),
|
|
1652
|
+
h('aside', { ref: sourceDialogRef, className: 'prts-source-reader', role: 'dialog',
|
|
1653
|
+
tabIndex: -1, 'aria-modal': 'true',
|
|
1654
|
+
'aria-label': `原文:${item.title}` },
|
|
1655
|
+
h('header', null,
|
|
1656
|
+
h('div', null, h('h3', null, item.title),
|
|
1657
|
+
h('p', null, 'LOCAL CORPUS / SOURCE READER')),
|
|
1658
|
+
h('button', { type: 'button', className: 'prts-evidence-close', onClick: onClose,
|
|
1659
|
+
'aria-label': '关闭' }, '×')),
|
|
1660
|
+
h('div', { className: 'prts-source-body' },
|
|
1661
|
+
loading && h('div', { className: 'prts-source-state' }, '正在读取原文…'),
|
|
1662
|
+
error && h('div', { className: 'prts-source-state prts-source-error' }, error),
|
|
1663
|
+
!loading && !error && lines.length === 0 && h('div', { className: 'prts-source-state' },
|
|
1664
|
+
'该资料没有可展示的正文。'),
|
|
1665
|
+
!loading && lines.length > 0 && h('div', { className: 'prts-source-page',
|
|
1666
|
+
role: 'status', 'aria-live': 'polite' }, page.hasMore
|
|
1667
|
+
? `已加载 ${lines.length}${page.total != null ? ` / ${page.total}` : ''} 行,原文尚未完整`
|
|
1668
|
+
: `已加载全部 ${lines.length} 行`),
|
|
1669
|
+
!loading && lines.map((line) => h('p', {
|
|
1670
|
+
className: [
|
|
1671
|
+
'prts-source-line', line.speaker_raw ? '' : 'narration',
|
|
1672
|
+
item.lineStart && line.line_number === item.lineStart ? 'active' : '',
|
|
1673
|
+
].filter(Boolean).join(' '),
|
|
1674
|
+
key: `${line.line_number}-${line.source_ref || ''}`,
|
|
1675
|
+
},
|
|
1676
|
+
h('span', { className: 'prts-source-no' }, String(line.line_number)),
|
|
1677
|
+
line.speaker_raw && h('span', { className: 'prts-source-speaker' }, line.speaker_raw),
|
|
1678
|
+
line.text)),
|
|
1679
|
+
!loading && page.hasMore && page.nextCursor && h('button', { type: 'button',
|
|
1680
|
+
className: 'prts-source-more', onClick: loadMore, disabled: loadingMore },
|
|
1681
|
+
loadingMore ? '正在加载…' : '继续加载原文'))))
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
function EvidenceControl({ useChat }) {
|
|
1685
|
+
const [open, setOpen] = useState(false)
|
|
1686
|
+
const [reader, setReader] = useState(null)
|
|
1687
|
+
const evidenceTriggerRef = useRef(null)
|
|
1688
|
+
const evidenceDrawerRef = useRef(null)
|
|
1689
|
+
const readerReturnFocusRef = useRef(null)
|
|
1690
|
+
const readerReturnIdRef = useRef('')
|
|
1691
|
+
// 首选会话快照(useChat 是 session 槽位的标准 prop,ui-chat 组合必给);
|
|
1692
|
+
// 拿不到时回退 DOM 抓取 + MutationObserver。
|
|
1693
|
+
const snapshotOrder = useChat ? useChat((snapshot) => snapshot?.order ?? []) : null
|
|
1694
|
+
const snapshotNodes = useChat ? useChat((snapshot) => snapshot?.nodes ?? null) : null
|
|
1695
|
+
const sceneRevision = useChat ? useChat(sceneSnapshotSignature) : ''
|
|
1696
|
+
const evidenceRevision = useChat ? useChat(evidenceSnapshotSignature) : ''
|
|
1697
|
+
const sceneModel = useMemo(() => (useChat
|
|
1698
|
+
? buildSceneSnapshotModel(snapshotOrder, snapshotNodes)
|
|
1699
|
+
: null), [snapshotOrder, snapshotNodes, sceneRevision])
|
|
1700
|
+
const [domEvidence, setDomEvidence] = useState([])
|
|
1701
|
+
useEffect(() => {
|
|
1702
|
+
sceneSnapshotModel = sceneModel
|
|
1703
|
+
notifySceneSnapshot()
|
|
1704
|
+
return () => {
|
|
1705
|
+
if (sceneSnapshotModel !== sceneModel) return
|
|
1706
|
+
sceneSnapshotModel = null
|
|
1707
|
+
notifySceneSnapshot()
|
|
1708
|
+
}
|
|
1709
|
+
}, [sceneModel])
|
|
1710
|
+
useEffect(() => {
|
|
1711
|
+
if (useChat || typeof document === 'undefined') return undefined
|
|
1712
|
+
let timer = null
|
|
1713
|
+
const sync = () => {
|
|
1714
|
+
timer = null
|
|
1715
|
+
const next = readEvidenceFromPage()
|
|
1716
|
+
const signature = JSON.stringify(next)
|
|
1717
|
+
setDomEvidence((previous) => JSON.stringify(previous) === signature ? previous : next)
|
|
1718
|
+
}
|
|
1719
|
+
const schedule = () => {
|
|
1720
|
+
if (timer == null) timer = setTimeout(sync, 80)
|
|
1721
|
+
}
|
|
1722
|
+
const observer = new MutationObserver(schedule)
|
|
1723
|
+
observer.observe(document.body, { childList: true, subtree: true, characterData: true,
|
|
1724
|
+
attributes: true, attributeFilter: ['data-state', 'data-tool'] })
|
|
1725
|
+
sync()
|
|
1726
|
+
return () => { observer.disconnect(); if (timer != null) clearTimeout(timer) }
|
|
1727
|
+
}, [])
|
|
1728
|
+
const evidence = useMemo(() => (useChat
|
|
1729
|
+
? buildEvidence(collectSnapshotEvidence(snapshotOrder, snapshotNodes))
|
|
1730
|
+
: domEvidence), [snapshotOrder, snapshotNodes, evidenceRevision, domEvidence])
|
|
1731
|
+
useEffect(() => {
|
|
1732
|
+
if (!open || reader || typeof document === 'undefined') return undefined
|
|
1733
|
+
return installDialogFocusTrap(evidenceDrawerRef.current, {
|
|
1734
|
+
onEscape: () => setOpen(false),
|
|
1735
|
+
initialFocus: () => readerReturnFocusRef.current,
|
|
1736
|
+
returnFocus: () => evidenceTriggerRef.current,
|
|
1737
|
+
})
|
|
1738
|
+
}, [open, reader])
|
|
1739
|
+
return h(Fragment, null,
|
|
1740
|
+
h('button', { ref: evidenceTriggerRef, type: 'button', className: 'prts-header-badge',
|
|
1741
|
+
onClick: () => {
|
|
1742
|
+
readerReturnIdRef.current = ''
|
|
1743
|
+
readerReturnFocusRef.current = null
|
|
1744
|
+
setOpen(true)
|
|
1745
|
+
}, title: '查看本轮本地资料读取记录', 'aria-haspopup': 'dialog',
|
|
1746
|
+
'aria-expanded': open },
|
|
1747
|
+
h('i'), '证据', h('b', null, String(evidence.length))),
|
|
1748
|
+
reader && h('div', { className: 'prts-evidence-layer' },
|
|
1749
|
+
h(SourceReader, { item: reader, onClose: () => setReader(null),
|
|
1750
|
+
returnFocus: () => readerReturnFocusRef.current })),
|
|
1751
|
+
open && !reader && h('div', { className: 'prts-evidence-layer' },
|
|
1752
|
+
h('button', { type: 'button', className: 'prts-evidence-scrim', onClick: () => setOpen(false),
|
|
1753
|
+
'aria-label': '关闭证据面板' }),
|
|
1754
|
+
h('aside', { ref: evidenceDrawerRef, className: 'prts-evidence-drawer', role: 'dialog',
|
|
1755
|
+
tabIndex: -1, 'aria-modal': 'true',
|
|
1756
|
+
'aria-label': '本轮证据' },
|
|
1757
|
+
h('header', null,
|
|
1758
|
+
h('div', null, h('h3', null, '本轮证据'), h('p', null, 'LOCAL CORPUS / SOURCE READ TRACE')),
|
|
1759
|
+
h('button', { type: 'button', className: 'prts-evidence-close', onClick: () => setOpen(false),
|
|
1760
|
+
'aria-label': '关闭' }, '×')),
|
|
1761
|
+
h('div', { className: 'prts-evidence-list' }, evidence.length
|
|
1762
|
+
? evidence.map((item, index) => {
|
|
1763
|
+
const hasLocator = Boolean(item.sourceRef || item.documentId)
|
|
1764
|
+
const readable = hasLocator && validDataVersion(item.dataVersion)
|
|
1765
|
+
return h('button', {
|
|
1766
|
+
type: 'button', className: 'prts-evidence-card', key: item.id,
|
|
1767
|
+
ref: (node) => {
|
|
1768
|
+
if (node && readerReturnIdRef.current === item.id) readerReturnFocusRef.current = node
|
|
1769
|
+
},
|
|
1770
|
+
'data-index': String(index + 1).padStart(2, '0'), disabled: !readable,
|
|
1771
|
+
onClick: readable ? (event) => {
|
|
1772
|
+
readerReturnIdRef.current = item.id
|
|
1773
|
+
readerReturnFocusRef.current = event.currentTarget
|
|
1774
|
+
setReader(item)
|
|
1775
|
+
} : undefined,
|
|
1776
|
+
title: readable ? `点击查看原文(${item.documentId || item.sourceRef})`
|
|
1777
|
+
: hasLocator ? '旧会话未保存完整的 64 位资料版本,不能打开可能已换版的原文'
|
|
1778
|
+
: '旧会话未保存稳定定位,只能查看引用摘要',
|
|
1779
|
+
}, h('b', null, readable ? 'CORPUS_READ · VERIFIED' : 'CORPUS_READ · UNVERIFIED'),
|
|
1780
|
+
h('p', null, item.excerpt))
|
|
1781
|
+
})
|
|
1782
|
+
: h('div', { className: 'prts-evidence-empty' }, '本轮还没有读取本地原文。\n检索并读取资料后,证据卡会出现在这里。')))))
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
const fmtBytes = (bytes) => {
|
|
1786
|
+
if (!Number.isFinite(bytes)) return '—'
|
|
1787
|
+
if (bytes < 1024) return `${bytes} B`
|
|
1788
|
+
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`
|
|
1789
|
+
if (bytes < 1073741824) return `${(bytes / 1048576).toFixed(1)} MB`
|
|
1790
|
+
return `${(bytes / 1073741824).toFixed(2)} GB`
|
|
1791
|
+
}
|
|
1792
|
+
const shortVersion = (value) => (value ? `${String(value).slice(0, 12)}…` : '—')
|
|
1793
|
+
const fmtDate = (value) => {
|
|
1794
|
+
if (!value) return '—'
|
|
1795
|
+
const date = new Date(value)
|
|
1796
|
+
if (Number.isNaN(date.getTime())) return String(value)
|
|
1797
|
+
const pad = (n) => String(n).padStart(2, '0')
|
|
1798
|
+
return `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
/* ---- 内联样式 ---- */
|
|
1802
|
+
const S = {
|
|
1803
|
+
wrap: { display: 'flex', flexDirection: 'column', gap: '16px', fontSize: '13px' },
|
|
1804
|
+
card: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: '10px',
|
|
1805
|
+
padding: '14px 16px', background: 'var(--dsw-alias-bg-layer-1)' },
|
|
1806
|
+
cardTitle: { fontWeight: 600, margin: '0 0 10px', fontSize: '13px' },
|
|
1807
|
+
row: { display: 'flex', alignItems: 'center', gap: '10px', flexWrap: 'wrap', margin: '4px 0' },
|
|
1808
|
+
key: { opacity: 0.65, minWidth: '96px', flex: '0 0 auto' },
|
|
1809
|
+
val: { fontFamily: 'ui-monospace, monospace', wordBreak: 'break-all' },
|
|
1810
|
+
badge: { padding: '1px 8px', borderRadius: '999px', fontSize: '11px', flex: '0 0 auto',
|
|
1811
|
+
background: 'var(--dsw-alias-state-business-tertiary)', color: 'var(--dsw-alias-state-business-primary)' },
|
|
1812
|
+
badgeGray: { padding: '1px 8px', borderRadius: '999px', fontSize: '11px', flex: '0 0 auto',
|
|
1813
|
+
background: 'rgba(128,128,128,.18)', color: '#999' },
|
|
1814
|
+
badgeGreen: { padding: '1px 8px', borderRadius: '999px', fontSize: '11px', flex: '0 0 auto',
|
|
1815
|
+
background: 'var(--dsw-alias-state-success-tertiary)', color: 'var(--dsw-alias-state-success-primary)' },
|
|
1816
|
+
badgeRed: { padding: '1px 8px', borderRadius: '999px', fontSize: '11px', flex: '0 0 auto',
|
|
1817
|
+
background: 'var(--dsw-alias-interactive-bg-hover-danger)', color: 'var(--dsw-alias-state-error-primary)' },
|
|
1818
|
+
button: { padding: '5px 12px', borderRadius: '7px', border: '1px solid var(--dsw-alias-border-l2)',
|
|
1819
|
+
background: 'transparent', color: 'inherit', cursor: 'pointer', fontSize: '12px', flex: '0 0 auto' },
|
|
1820
|
+
buttonPrimary: { padding: '5px 12px', borderRadius: '7px', border: 'none',
|
|
1821
|
+
background: 'var(--dsw-alias-button-info-fill)', color: '#fff', cursor: 'pointer', fontSize: '12px', flex: '0 0 auto' },
|
|
1822
|
+
input: { boxSizing: 'border-box', padding: '4px 8px', borderRadius: '6px',
|
|
1823
|
+
border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-base)', color: 'inherit',
|
|
1824
|
+
fontSize: '12px', flex: '1 1 160px', minWidth: '120px', maxWidth: '100%' },
|
|
1825
|
+
select: { boxSizing: 'border-box', padding: '4px 8px', borderRadius: '6px',
|
|
1826
|
+
border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-base)', color: 'inherit',
|
|
1827
|
+
fontSize: '12px', flex: '0 1 auto', minWidth: '120px', maxWidth: '100%' },
|
|
1828
|
+
table: { width: '100%', borderCollapse: 'collapse', fontSize: '12.5px', tableLayout: 'fixed' },
|
|
1829
|
+
th: { textAlign: 'left', padding: '6px 8px', opacity: 0.6, fontWeight: 500,
|
|
1830
|
+
borderBottom: '1px solid rgba(128,128,128,.3)', whiteSpace: 'nowrap' },
|
|
1831
|
+
td: { padding: '6px 8px', borderBottom: '1px solid rgba(128,128,128,.16)', verticalAlign: 'top' },
|
|
1832
|
+
progressTrack: { height: '8px', borderRadius: '4px', background: 'rgba(128,128,128,.22)',
|
|
1833
|
+
overflow: 'hidden', flex: 1, minWidth: '180px' },
|
|
1834
|
+
progressFill: { height: '100%', background: 'var(--dsw-alias-state-business-primary)', transition: 'width .3s' },
|
|
1835
|
+
hint: { opacity: 0.6, fontSize: '12px' },
|
|
1836
|
+
datasetGrid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
|
|
1837
|
+
gap: '10px', marginTop: '14px' },
|
|
1838
|
+
datasetCard: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: '11px',
|
|
1839
|
+
padding: '13px', background: 'var(--dsw-alias-bg-base)', transition: 'border-color .15s, box-shadow .15s' },
|
|
1840
|
+
datasetCardEnabled: { borderColor: 'var(--dsw-alias-state-business-primary)',
|
|
1841
|
+
boxShadow: '0 0 0 1px var(--dsw-alias-state-business-primary)' },
|
|
1842
|
+
buttonLink: { padding: '3px 0', border: 'none', background: 'transparent', color: 'inherit',
|
|
1843
|
+
opacity: 0.72, cursor: 'pointer', fontSize: '12px' },
|
|
1844
|
+
versionPanel: { marginTop: '12px', padding: '12px', borderRadius: '9px',
|
|
1845
|
+
background: 'rgba(128,128,128,.07)', border: '1px solid rgba(128,128,128,.18)' },
|
|
1846
|
+
versionRow: { display: 'flex', alignItems: 'center', gap: '8px', padding: '10px 0',
|
|
1847
|
+
borderBottom: '1px solid rgba(128,128,128,.16)' },
|
|
1848
|
+
grid2: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: '6px 12px' },
|
|
1849
|
+
field: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap', margin: '6px 0' },
|
|
1850
|
+
fieldKey: { minWidth: '140px', opacity: 0.75, flex: '0 0 auto' },
|
|
1851
|
+
error: { color: 'var(--dsw-alias-state-error-primary)' },
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
function SkinCard({ skin, onChanged }) {
|
|
1855
|
+
const [saving, setSaving] = useState(false)
|
|
1856
|
+
const [message, setMessage] = useState(null)
|
|
1857
|
+
const choose = async (next) => {
|
|
1858
|
+
// React props/state may still describe the previous render. The module
|
|
1859
|
+
// owner is the synchronous de-duplication lock; even clicking the item
|
|
1860
|
+
// that appears selected must be allowed to confirm Host config and
|
|
1861
|
+
// invalidate an unresolved startup/status snapshot.
|
|
1862
|
+
if (skinSelectionOwner) return
|
|
1863
|
+
const selectionOwner = {}
|
|
1864
|
+
skinSelectionOwner = selectionOwner
|
|
1865
|
+
beginHostSkinSync()
|
|
1866
|
+
const operationEpoch = ++skinOperationEpoch
|
|
1867
|
+
pendingSkinOperations.add(selectionOwner)
|
|
1868
|
+
setSaving(true)
|
|
1869
|
+
setMessage(null)
|
|
1870
|
+
const previous = activeSkin
|
|
1871
|
+
const normalizedNext = normalizeSkin(next)
|
|
1872
|
+
const changesRuntime = normalizedNext !== activeSkin
|
|
1873
|
+
// 切肤开机场:终末地等地图就绪后由 aicBootDone 撤下,其它皮肤短暂过渡后立即收尾。
|
|
1874
|
+
// 重申当前 Host 值不重启开机场;AIC runtime 已就绪时不会产生新的
|
|
1875
|
+
// map-ready 事件,若重启遮罩只能等待 watchdog。
|
|
1876
|
+
const bootToken = changesRuntime ? aicBootShow(normalizedNext === 'endfield-aic'
|
|
1877
|
+
? 'REBUILDING INTERFACE' : 'SWITCHING SKIN', true, normalizedNext) : 0
|
|
1878
|
+
let hostCommitted = false
|
|
1879
|
+
try {
|
|
1880
|
+
// CSS 先 ready,Host 确认后再同步提交本地运行时、移除旧 CSS 与写缓存。
|
|
1881
|
+
await setSkin(next, { beforeCommit: async () => {
|
|
1882
|
+
await writeSkinConfig(next)
|
|
1883
|
+
hostCommitted = true
|
|
1884
|
+
} })
|
|
1885
|
+
if (operationEpoch !== skinOperationEpoch) return
|
|
1886
|
+
onChanged(normalizedNext)
|
|
1887
|
+
if (normalizedNext !== 'endfield-aic') {
|
|
1888
|
+
if (changesRuntime) aicBootDone(bootToken)
|
|
1889
|
+
// activeSkin 仍是 Harness 但迟到的启动快照可能已经展示 AIC
|
|
1890
|
+
// 遮罩。此时没有本次过渡可淡出,必须立即撤下旧 owner。
|
|
1891
|
+
else aicBootAbort(true)
|
|
1892
|
+
}
|
|
1893
|
+
} catch (error) {
|
|
1894
|
+
let reportedError = error
|
|
1895
|
+
// 只有 Host 已接受而本地最终提交失败时才需要补偿写;常见的 PUT
|
|
1896
|
+
// 失败已经由 setSkin 在旧 CSS 仍在时完成纯本地回滚。
|
|
1897
|
+
if (hostCommitted && operationEpoch === skinOperationEpoch) {
|
|
1898
|
+
try {
|
|
1899
|
+
await writeSkinConfig(previous)
|
|
1900
|
+
} catch (rollbackError) {
|
|
1901
|
+
console.error('[prts-terrarchive] failed to roll back host skin config', rollbackError)
|
|
1902
|
+
reportedError = new Error(`${error?.message ?? String(error)};Host 配置回滚失败:${rollbackError?.message ?? String(rollbackError)}`)
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
if (operationEpoch === skinOperationEpoch) {
|
|
1906
|
+
aicBootAbort()
|
|
1907
|
+
setMessage(reportedError?.message ?? String(reportedError))
|
|
1908
|
+
}
|
|
1909
|
+
} finally {
|
|
1910
|
+
// 让选择期间发出的旧 status 请求失效;成功与回滚都以此处状态为准。
|
|
1911
|
+
pendingSkinOperations.delete(selectionOwner)
|
|
1912
|
+
if (skinOperationEpoch === operationEpoch) {
|
|
1913
|
+
skinOperationEpoch += 1
|
|
1914
|
+
setSaving(false)
|
|
1915
|
+
}
|
|
1916
|
+
if (skinSelectionOwner === selectionOwner) skinSelectionOwner = null
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
const option = (id, title, description, swatchClass) => h('button', {
|
|
1920
|
+
type: 'button', className: 'prts-skin-option', 'aria-pressed': skin === id,
|
|
1921
|
+
disabled: saving, onClick: () => { void choose(id) },
|
|
1922
|
+
}, h('strong', null, title), h('small', null, description),
|
|
1923
|
+
h('span', { className: `prts-skin-swatch ${swatchClass}` }))
|
|
1924
|
+
return h('div', { style: S.card },
|
|
1925
|
+
h('h3', { style: S.cardTitle }, '界面皮肤'),
|
|
1926
|
+
h('div', { className: 'prts-skin-options' },
|
|
1927
|
+
option('harness', 'Harness 默认', '跟随 DeepSeek Harness 原生视觉', ''),
|
|
1928
|
+
option('prts-agent', 'PRTS Agent', '迁移自 PRTS.chat Agent 的冷白、石墨与蓝色强调', 'prts'),
|
|
1929
|
+
option('endfield-aic', 'AIC 终末地', '完整武陵地图终端布局与酸性黄工业界面', 'endfield'),
|
|
1930
|
+
option('rhine-lab', '莱茵生命资料馆', '三维档案阵列、检索过程与可摘录的资料台', 'rhine')),
|
|
1931
|
+
h('p', { className: 'prts-skin-note', style: message ? S.error : null },
|
|
1932
|
+
message || (saving ? '正在应用…' : '选择后立即生效,并保留当前的明亮 / 暗色偏好。')))
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
/* ---- 资料库选择与按游戏展开的版本管理 ---- */
|
|
1936
|
+
function DatasetLibraryCard({ status, releases, activeRelease, enabledGames, savingGames,
|
|
1937
|
+
onEnabledGames, onRefresh, onCheckUpdate, checking, updateInfo,
|
|
1938
|
+
onActivate, onDelete, onDownload, downloading }) {
|
|
1939
|
+
const store = status?.store ?? {}
|
|
1940
|
+
const download = status?.download ?? {}
|
|
1941
|
+
const percent = download.filesTotal
|
|
1942
|
+
? Math.round((download.filesDone / download.filesTotal) * 100) : null
|
|
1943
|
+
const [expandedGame, setExpandedGame] = useState(null)
|
|
1944
|
+
const games = [
|
|
1945
|
+
{ id: 'arknights', title: '明日方舟', subtitle: '剧情、档案、Wiki 与实体资料' },
|
|
1946
|
+
{ id: 'endfield', title: '明日方舟:终末地', subtitle: '剧情原文、档案库与自建 Wiki' },
|
|
1947
|
+
]
|
|
1948
|
+
const activeDatasets = activeRelease?.datasets ?? {}
|
|
1949
|
+
const shared = activeDatasets.sharedPacks ?? []
|
|
1950
|
+
const sharedDocuments = shared.reduce((sum, item) => sum + (Number(item.documentCount) || 0), 0)
|
|
1951
|
+
const sharedCompressedSize = shared.reduce((sum, item) =>
|
|
1952
|
+
sum + (Number(item.compressedSize) || 0), 0)
|
|
1953
|
+
const toggle = (game) => {
|
|
1954
|
+
const selected = enabledGames.includes(game)
|
|
1955
|
+
? enabledGames.filter((item) => item !== game)
|
|
1956
|
+
: [...enabledGames, game]
|
|
1957
|
+
if (!selected.length || savingGames) return
|
|
1958
|
+
void onEnabledGames(selected)
|
|
1959
|
+
}
|
|
1960
|
+
const versionTitle = (dataset, release) => {
|
|
1961
|
+
const knownTitles = { 'xuesong-youmeng': '雪凇幽梦' }
|
|
1962
|
+
const releaseName = dataset?.releaseTitle
|
|
1963
|
+
|| knownTitles[dataset?.releaseName] || dataset?.releaseName
|
|
1964
|
+
const names = [releaseName, dataset?.gameVersion].filter(Boolean)
|
|
1965
|
+
return names.length ? names.join(' · ') : release.releaseId
|
|
1966
|
+
}
|
|
1967
|
+
const versionRows = expandedGame == null ? [] : (releases ?? [])
|
|
1968
|
+
.filter((release) => release.datasets?.[expandedGame]?.present)
|
|
1969
|
+
return h('div', { style: S.card },
|
|
1970
|
+
h('div', { style: { ...S.row, justifyContent: 'space-between' } },
|
|
1971
|
+
h('div', null,
|
|
1972
|
+
h('h3', { style: { ...S.cardTitle, marginBottom: '3px' } }, '资料库'),
|
|
1973
|
+
h('span', { style: S.hint }, '选择 Agent 可以检索的资料;至少启用一个。')),
|
|
1974
|
+
h('div', { style: S.row },
|
|
1975
|
+
h('button', { style: S.button, onClick: onRefresh }, '刷新'),
|
|
1976
|
+
h('button', { style: S.button, disabled: checking, onClick: onCheckUpdate },
|
|
1977
|
+
checking ? '检查中…' : '检查更新'),
|
|
1978
|
+
h('button', { style: S.buttonPrimary, disabled: downloading, onClick: () => onDownload() },
|
|
1979
|
+
downloading ? '下载中…' : '下载最新'))),
|
|
1980
|
+
store.installed === false && h('div', { style: { ...S.error, marginTop: '8px' } },
|
|
1981
|
+
`本地资料尚未就绪:${store.installationIssue || '未找到有效版本'}。便携版用户还应确认 ZIP 已完整解压。`),
|
|
1982
|
+
store.installed === true && !store.loaded && h('div', { style: { ...S.hint, marginTop: '8px' } },
|
|
1983
|
+
'已检测到语料,正在加载索引;首次启动可能需要一些时间。'),
|
|
1984
|
+
h('div', { style: S.datasetGrid }, ...games.map((game) => {
|
|
1985
|
+
const dataset = activeDatasets[game.id]
|
|
1986
|
+
const enabled = enabledGames.includes(game.id)
|
|
1987
|
+
return h('section', { key: game.id, style: {
|
|
1988
|
+
...S.datasetCard,
|
|
1989
|
+
...(enabled ? S.datasetCardEnabled : {}),
|
|
1990
|
+
...(!dataset?.present ? { opacity: 0.72 } : {}),
|
|
1991
|
+
} },
|
|
1992
|
+
h('div', { style: { ...S.row, flexWrap: 'nowrap', alignItems: 'flex-start' } },
|
|
1993
|
+
h('input', { type: 'checkbox', checked: enabled, disabled: savingGames,
|
|
1994
|
+
'aria-label': `启用${game.title}资料`, onChange: () => toggle(game.id),
|
|
1995
|
+
style: { width: '17px', height: '17px', marginTop: '2px', accentColor: 'var(--dsw-alias-state-business-primary)' } }),
|
|
1996
|
+
h('label', { style: { flex: 1, cursor: 'pointer' }, onClick: () => toggle(game.id) },
|
|
1997
|
+
h('strong', { style: { display: 'block', fontSize: '14px', marginBottom: '4px' } }, game.title),
|
|
1998
|
+
h('span', { style: S.hint }, game.subtitle))),
|
|
1999
|
+
h('div', { style: { ...S.row, marginTop: '12px' } },
|
|
2000
|
+
dataset?.present
|
|
2001
|
+
? h(Fragment, null,
|
|
2002
|
+
h('span', { style: store.loaded ? S.badgeGreen : S.badgeGray }, store.loaded ? '已就绪' : '加载中'),
|
|
2003
|
+
h('span', null, `${dataset.documentCount.toLocaleString()} 篇`),
|
|
2004
|
+
h('span', { style: S.hint }, `${fmtBytes(dataset.compressedSize)} 专属包`))
|
|
2005
|
+
: h('span', { style: S.badgeGray }, '未安装')),
|
|
2006
|
+
h('div', { style: { ...S.row, justifyContent: 'space-between', marginTop: '8px' } },
|
|
2007
|
+
h('span', { style: S.hint }, dataset?.present
|
|
2008
|
+
? versionTitle(dataset, activeRelease) : '没有可用版本'),
|
|
2009
|
+
h('button', { style: S.buttonLink, disabled: !dataset?.present,
|
|
2010
|
+
onClick: () => setExpandedGame(expandedGame === game.id ? null : game.id) },
|
|
2011
|
+
expandedGame === game.id ? '收起版本' : '查看版本')))
|
|
2012
|
+
})),
|
|
2013
|
+
sharedDocuments > 0 && h('div', { style: { ...S.hint, marginTop: '9px' } },
|
|
2014
|
+
`另含 ${sharedDocuments.toLocaleString()} 篇共享资料(${fmtBytes(sharedCompressedSize)}),两款游戏共同使用。`),
|
|
2015
|
+
updateInfo?.text && h('div', { style: { ...S.row, marginTop: '8px' } },
|
|
2016
|
+
h('span', { style: updateInfo.kind === 'error' ? S.error : updateInfo.kind === 'update' ? S.badgeGreen : S.badgeGray },
|
|
2017
|
+
updateInfo.text),
|
|
2018
|
+
updateInfo.kind === 'update' && updateInfo.remoteReleaseId
|
|
2019
|
+
&& h('button', { style: S.buttonPrimary, onClick: updateInfo.onDownload }, '下载更新')),
|
|
2020
|
+
download.phase && download.phase !== 'idle'
|
|
2021
|
+
? h('div', { style: { ...S.row, marginTop: '8px' } },
|
|
2022
|
+
h('span', { style: S.key }, download.active ? '下载中' : '下载状态'),
|
|
2023
|
+
h('span', { style: S.badge }, `${download.source ?? ''} · ${download.phase}`),
|
|
2024
|
+
percent !== null && h('div', { style: S.progressTrack },
|
|
2025
|
+
h('div', { style: { ...S.progressFill, width: `${percent}%` } })),
|
|
2026
|
+
h('span', { style: S.hint },
|
|
2027
|
+
`${download.filesDone}/${download.filesTotal ?? '?'} 文件 · ${fmtBytes(download.bytesDone)}`),
|
|
2028
|
+
download.error && h('span', { style: S.error }, download.error))
|
|
2029
|
+
: null,
|
|
2030
|
+
expandedGame && h('div', { style: S.versionPanel },
|
|
2031
|
+
h('div', { style: { ...S.row, justifyContent: 'space-between' } },
|
|
2032
|
+
h('strong', null, `${games.find((item) => item.id === expandedGame)?.title} · 已安装版本`),
|
|
2033
|
+
h('span', { style: S.hint }, '版本切换会同时切换联合资料包')),
|
|
2034
|
+
...versionRows.map((release) => {
|
|
2035
|
+
const dataset = release.datasets[expandedGame]
|
|
2036
|
+
return h('div', { key: release.releaseId, style: S.versionRow },
|
|
2037
|
+
h('div', { style: { minWidth: 0, flex: 1 } },
|
|
2038
|
+
h('strong', { style: { display: 'block' } }, versionTitle(dataset, release)),
|
|
2039
|
+
h('span', { style: S.hint },
|
|
2040
|
+
`${release.releaseId} · ${dataset.documentCount.toLocaleString()} 篇 · ${fmtBytes(dataset.compressedSize)}`),
|
|
2041
|
+
h('span', { style: { ...S.hint, display: 'block' } },
|
|
2042
|
+
`数据 ${shortVersion(dataset.dataVersion)}${release.createdAt ? ` · ${fmtDate(release.createdAt)}` : ''}`)),
|
|
2043
|
+
release.active
|
|
2044
|
+
? h('span', { style: S.badgeGreen }, '当前')
|
|
2045
|
+
: h(Fragment, null,
|
|
2046
|
+
h('button', { style: S.button, disabled: !release.complete,
|
|
2047
|
+
onClick: () => onActivate(release.releaseId) }, '切换'),
|
|
2048
|
+
h('button', { style: S.button, onClick: () => onDelete(release.releaseId) }, '删除')))
|
|
2049
|
+
}),
|
|
2050
|
+
versionRows.length === 0 && h('span', { style: S.hint }, '本地没有这个资料库的版本。')),
|
|
2051
|
+
h('div', { style: { ...S.hint, marginTop: '9px' } },
|
|
2052
|
+
'版本与哈希先由 PRTS.chat current 确认,文件再按镜像顺序下载;切换后索引自动重建。'))
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
/* ---- 配置编辑 ---- */
|
|
2056
|
+
const NUMBER_FIELDS = [
|
|
2057
|
+
['cacheShards', '分片缓存数'],
|
|
2058
|
+
['cloudTimeoutMs', '云端超时(ms)'],
|
|
2059
|
+
['cloudMaxResponseBytes', '云端响应上限(B)'],
|
|
2060
|
+
]
|
|
2061
|
+
|
|
2062
|
+
function ConfigCard({ config, onSaved }) {
|
|
2063
|
+
const [draft, setDraft] = useState(null)
|
|
2064
|
+
const [saving, setSaving] = useState(false)
|
|
2065
|
+
const [message, setMessage] = useState(null)
|
|
2066
|
+
if (!config) return null
|
|
2067
|
+
const value = draft ?? {}
|
|
2068
|
+
const current = (key, fallback) => Object.hasOwn(value, key) ? value[key] : (config[key] ?? fallback)
|
|
2069
|
+
const set = (key, cast) => (event) => {
|
|
2070
|
+
const raw = event.target.type === 'checkbox' ? event.target.checked : event.target.value
|
|
2071
|
+
setDraft((previous) => ({ ...(previous ?? {}), [key]: cast ? cast(raw) : raw }))
|
|
2072
|
+
}
|
|
2073
|
+
const save = async () => {
|
|
2074
|
+
setSaving(true)
|
|
2075
|
+
setMessage(null)
|
|
2076
|
+
try {
|
|
2077
|
+
await jsonFetch('/config', { method: 'PUT', body: JSON.stringify({ patch: value }) })
|
|
2078
|
+
setDraft(null)
|
|
2079
|
+
setMessage('已保存并生效')
|
|
2080
|
+
onSaved()
|
|
2081
|
+
} catch (error) {
|
|
2082
|
+
setMessage(error.message)
|
|
2083
|
+
} finally {
|
|
2084
|
+
setSaving(false)
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
return h('div', { style: S.card },
|
|
2088
|
+
h('h3', { style: S.cardTitle }, '插件配置'),
|
|
2089
|
+
h('div', { style: S.field },
|
|
2090
|
+
h('span', { style: S.fieldKey }, '云端工具'),
|
|
2091
|
+
h('input', { type: 'checkbox', checked: Boolean(current('cloudEnabled', false)),
|
|
2092
|
+
onChange: set('cloudEnabled') }),
|
|
2093
|
+
h('span', { style: S.hint }, 'cloud_search / cloud_inspect(保存后即时注册/注销)')),
|
|
2094
|
+
h('div', { style: S.field },
|
|
2095
|
+
h('span', { style: S.fieldKey }, '云端检索地址'),
|
|
2096
|
+
h('input', { style: S.input, value: String(current('cloudBaseUrl', '')),
|
|
2097
|
+
onChange: set('cloudBaseUrl') }),
|
|
2098
|
+
h('span', { style: { ...S.hint, flexBasis: '100%', marginLeft: '148px' } }, 'cloud_search / cloud_inspect 服务')),
|
|
2099
|
+
h('div', { style: S.field },
|
|
2100
|
+
h('span', { style: S.fieldKey }, '访问令牌'),
|
|
2101
|
+
h('input', { style: S.input, type: 'password',
|
|
2102
|
+
placeholder: config.hasCloudToken ? '已设置(不修改即保持)' : '留空使用匿名会话',
|
|
2103
|
+
value: String(current('cloudToken', '')), onChange: set('cloudToken') })),
|
|
2104
|
+
h('div', { style: S.grid2 },
|
|
2105
|
+
...NUMBER_FIELDS.map(([key, label]) => h('div', { key, style: S.field },
|
|
2106
|
+
h('span', { style: S.fieldKey }, label),
|
|
2107
|
+
h('input', { style: { ...S.input, flex: '0 0 96px', width: '96px' }, type: 'number',
|
|
2108
|
+
value: String(current(key, '')), onChange: set(key, Number) })))),
|
|
2109
|
+
h('div', { style: S.field },
|
|
2110
|
+
h('span', { style: S.fieldKey }, '资料站点地址'),
|
|
2111
|
+
h('input', { style: S.input, value: String(current('downloadSiteBaseUrl', '')),
|
|
2112
|
+
onChange: set('downloadSiteBaseUrl') })),
|
|
2113
|
+
h('div', { style: S.field },
|
|
2114
|
+
h('span', { style: S.fieldKey }, '下载源顺序'),
|
|
2115
|
+
h('select', { style: S.select, value: current('downloadOrder', ['modelscope', 'site']).join(','),
|
|
2116
|
+
onChange: set('downloadOrder', (raw) => raw.split(',')) },
|
|
2117
|
+
h('option', { value: 'modelscope,site' }, 'ModelScope → PRTS.chat'),
|
|
2118
|
+
h('option', { value: 'site,modelscope' }, 'PRTS.chat → ModelScope'),
|
|
2119
|
+
h('option', { value: 'modelscope' }, '仅 ModelScope'),
|
|
2120
|
+
h('option', { value: 'site' }, '仅 PRTS.chat'))),
|
|
2121
|
+
h('div', { style: { ...S.row, marginTop: '8px' } },
|
|
2122
|
+
h('button', { style: S.buttonPrimary, disabled: !draft || saving, onClick: save },
|
|
2123
|
+
saving ? '保存中…' : '保存'),
|
|
2124
|
+
draft && h('button', { style: S.button, onClick: () => { setDraft(null) } }, '放弃'),
|
|
2125
|
+
message && h('span', { style: message.includes('已保存') ? S.badgeGreen : S.error }, message),
|
|
2126
|
+
h('span', { style: S.hint }, '持久化于 $DSH_HOME/prts-corpus.json,覆盖 patch 配置')))
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
/* ---- 设置页主组件 ---- */
|
|
2130
|
+
const releaseStatusKey = (status) => JSON.stringify([
|
|
2131
|
+
status?.store?.releaseId ?? null, status?.store?.dataVersion ?? null,
|
|
2132
|
+
status?.store?.installed ?? null, status?.download?.finishedAt ?? null,
|
|
2133
|
+
])
|
|
2134
|
+
|
|
2135
|
+
function PrtsSection() {
|
|
2136
|
+
const [status, setStatus] = useState(null)
|
|
2137
|
+
const [releaseList, setReleaseList] = useState(null)
|
|
2138
|
+
const [busy, setBusy] = useState(false)
|
|
2139
|
+
const [error, setError] = useState(null)
|
|
2140
|
+
const [checking, setChecking] = useState(false)
|
|
2141
|
+
const [savingGames, setSavingGames] = useState(false)
|
|
2142
|
+
const [updateInfo, setUpdateInfo] = useState(null)
|
|
2143
|
+
const [skin, setSkinState] = useState(() => activeSkin)
|
|
2144
|
+
const aliveRef = useRef(true)
|
|
2145
|
+
const statusInFlightRef = useRef(false)
|
|
2146
|
+
const hostSyncRef = useRef(0)
|
|
2147
|
+
const releaseStatusRef = useRef(null)
|
|
2148
|
+
|
|
2149
|
+
const beginStatusSync = useCallback(() => {
|
|
2150
|
+
const generation = beginHostSkinSync()
|
|
2151
|
+
hostSyncRef.current = generation
|
|
2152
|
+
return generation
|
|
2153
|
+
}, [])
|
|
2154
|
+
|
|
2155
|
+
const commitStatusSkin = useCallback(async (nextStatus, syncGeneration,
|
|
2156
|
+
observedSkinEpoch, repairSame = false) => {
|
|
2157
|
+
const assertCurrent = () => {
|
|
2158
|
+
if (!aliveRef.current || pendingSkinOperations.size !== 0
|
|
2159
|
+
|| observedSkinEpoch !== skinOperationEpoch) throw makeAbortError()
|
|
2160
|
+
assertHostSkinSyncActive(syncGeneration)
|
|
2161
|
+
}
|
|
2162
|
+
assertCurrent()
|
|
2163
|
+
setStatus(nextStatus)
|
|
2164
|
+
const nextSkin = normalizeSkin(nextStatus?.config?.uiSkin)
|
|
2165
|
+
const committedSkin = nextSkin === activeSkin && !repairSame
|
|
2166
|
+
? nextSkin : await setSkin(nextSkin, { assertCurrent })
|
|
2167
|
+
assertCurrent()
|
|
2168
|
+
setSkinState(committedSkin)
|
|
2169
|
+
// A stale startup AIC request may already have mounted its boot screen.
|
|
2170
|
+
// Host-driven non-AIC snapshots have no transition to finish it, so
|
|
2171
|
+
// retire that overlay only after this newest snapshot is committed.
|
|
2172
|
+
if (committedSkin !== 'endfield-aic') aicBootAbort()
|
|
2173
|
+
return committedSkin
|
|
2174
|
+
}, [])
|
|
2175
|
+
|
|
2176
|
+
const refreshStatus = useCallback(async () => {
|
|
2177
|
+
if (statusInFlightRef.current || pendingSkinOperations.size !== 0
|
|
2178
|
+
|| (typeof document !== 'undefined' && document.hidden)) return
|
|
2179
|
+
const observedSkinEpoch = skinOperationEpoch
|
|
2180
|
+
const syncGeneration = beginStatusSync()
|
|
2181
|
+
statusInFlightRef.current = true
|
|
2182
|
+
try {
|
|
2183
|
+
const nextStatus = await jsonFetch('/status')
|
|
2184
|
+
await commitStatusSkin(nextStatus, syncGeneration, observedSkinEpoch, false)
|
|
2185
|
+
assertHostSkinSyncActive(syncGeneration)
|
|
2186
|
+
const nextReleaseStatus = releaseStatusKey(nextStatus)
|
|
2187
|
+
// Download completion and external activation invalidate the library
|
|
2188
|
+
// list. Ordinary status polls keep the last listing without rescanning
|
|
2189
|
+
// installed releases; a failed or superseded listing retries next poll.
|
|
2190
|
+
if (releaseStatusRef.current !== nextReleaseStatus) {
|
|
2191
|
+
const nextReleases = await jsonFetch('/releases')
|
|
2192
|
+
if (!aliveRef.current) throw makeAbortError()
|
|
2193
|
+
assertHostSkinSyncActive(syncGeneration)
|
|
2194
|
+
setReleaseList(nextReleases)
|
|
2195
|
+
releaseStatusRef.current = nextReleaseStatus
|
|
2196
|
+
}
|
|
2197
|
+
setError(null)
|
|
2198
|
+
} catch (caught) {
|
|
2199
|
+
if (aliveRef.current && syncGeneration === hostSkinSyncGeneration
|
|
2200
|
+
&& caught?.name !== 'AbortError') setError(caught.message)
|
|
2201
|
+
} finally {
|
|
2202
|
+
statusInFlightRef.current = false
|
|
2203
|
+
}
|
|
2204
|
+
}, [beginStatusSync, commitStatusSkin])
|
|
2205
|
+
|
|
2206
|
+
const refresh = useCallback(async () => {
|
|
2207
|
+
const observedSkinEpoch = skinOperationEpoch
|
|
2208
|
+
const syncGeneration = beginStatusSync()
|
|
2209
|
+
try {
|
|
2210
|
+
const [nextStatus, nextReleases] = await Promise.all([
|
|
2211
|
+
jsonFetch('/status'), jsonFetch('/releases'),
|
|
2212
|
+
])
|
|
2213
|
+
await commitStatusSkin(nextStatus, syncGeneration, observedSkinEpoch, true)
|
|
2214
|
+
assertHostSkinSyncActive(syncGeneration)
|
|
2215
|
+
setReleaseList(nextReleases)
|
|
2216
|
+
releaseStatusRef.current = releaseStatusKey(nextStatus)
|
|
2217
|
+
setError(null)
|
|
2218
|
+
} catch (caught) {
|
|
2219
|
+
if (aliveRef.current && syncGeneration === hostSkinSyncGeneration
|
|
2220
|
+
&& caught?.name !== 'AbortError') setError(caught.message)
|
|
2221
|
+
}
|
|
2222
|
+
}, [beginStatusSync, commitStatusSkin])
|
|
2223
|
+
|
|
2224
|
+
useEffect(() => {
|
|
2225
|
+
aliveRef.current = true
|
|
2226
|
+
void refresh()
|
|
2227
|
+
const onVisibility = () => { if (!document.hidden) void refreshStatus() }
|
|
2228
|
+
const timer = setInterval(() => { void refreshStatus() }, 2000)
|
|
2229
|
+
document.addEventListener('visibilitychange', onVisibility)
|
|
2230
|
+
return () => {
|
|
2231
|
+
aliveRef.current = false
|
|
2232
|
+
if (hostSyncRef.current === hostSkinSyncGeneration) beginHostSkinSync()
|
|
2233
|
+
hostSyncRef.current = 0
|
|
2234
|
+
clearInterval(timer)
|
|
2235
|
+
document.removeEventListener('visibilitychange', onVisibility)
|
|
2236
|
+
}
|
|
2237
|
+
}, [refresh, refreshStatus])
|
|
2238
|
+
|
|
2239
|
+
const activeRelease = (releaseList?.releases ?? []).find((item) => item.active) ?? null
|
|
2240
|
+
const enabledGames = Array.isArray(status?.config?.enabledGames)
|
|
2241
|
+
? status.config.enabledGames : ['arknights', 'endfield']
|
|
2242
|
+
|
|
2243
|
+
const updateEnabledGames = async (games) => {
|
|
2244
|
+
const ordered = ['arknights', 'endfield'].filter((game) => games.includes(game))
|
|
2245
|
+
if (!ordered.length) return
|
|
2246
|
+
setSavingGames(true)
|
|
2247
|
+
try {
|
|
2248
|
+
await jsonFetch('/config', { method: 'PUT', body: JSON.stringify({ patch: {
|
|
2249
|
+
enabledGames: ordered,
|
|
2250
|
+
// 同步旧版配置键,使降级到旧插件时仍保持用户选择。
|
|
2251
|
+
cloudGame: ordered.length === 2 ? 'all' : ordered[0],
|
|
2252
|
+
} }) })
|
|
2253
|
+
await refresh()
|
|
2254
|
+
} catch (caught) {
|
|
2255
|
+
setError(caught.message)
|
|
2256
|
+
} finally {
|
|
2257
|
+
setSavingGames(false)
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
const checkUpdate = async () => {
|
|
2262
|
+
setChecking(true)
|
|
2263
|
+
setUpdateInfo(null)
|
|
2264
|
+
try {
|
|
2265
|
+
const result = await jsonFetch('/check-update')
|
|
2266
|
+
if (result.updateAvailable && result.remote) {
|
|
2267
|
+
setUpdateInfo({ kind: 'update', remoteReleaseId: result.remote.releaseId,
|
|
2268
|
+
text: `发现新版本 ${result.remote.releaseId}(${result.remote.documentCount ?? '?'} 篇)` })
|
|
2269
|
+
} else if (!result.error && result.remote) {
|
|
2270
|
+
setUpdateInfo({ kind: 'latest', text: `已是最新版本(${result.remote.releaseId})` })
|
|
2271
|
+
} else {
|
|
2272
|
+
setUpdateInfo({ kind: 'error', text: result.error || '检查失败' })
|
|
2273
|
+
}
|
|
2274
|
+
} catch (caught) {
|
|
2275
|
+
setUpdateInfo({ kind: 'error', text: caught.message })
|
|
2276
|
+
} finally {
|
|
2277
|
+
setChecking(false)
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
const download = async () => {
|
|
2282
|
+
setBusy(true)
|
|
2283
|
+
try {
|
|
2284
|
+
await jsonFetch('/download', { method: 'POST', body: JSON.stringify({}) })
|
|
2285
|
+
if (updateInfo?.kind === 'update') setUpdateInfo(null)
|
|
2286
|
+
void refresh()
|
|
2287
|
+
} catch (caught) {
|
|
2288
|
+
setError(caught.message)
|
|
2289
|
+
} finally {
|
|
2290
|
+
setBusy(false)
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
const activate = async (releaseId) => {
|
|
2294
|
+
if (!window.confirm(`切换激活版本到 ${releaseId}?索引将重建。`)) return
|
|
2295
|
+
try {
|
|
2296
|
+
await jsonFetch('/activate', { method: 'POST', body: JSON.stringify({ releaseId }) })
|
|
2297
|
+
void refresh()
|
|
2298
|
+
} catch (caught) { setError(caught.message) }
|
|
2299
|
+
}
|
|
2300
|
+
const remove = async (releaseId) => {
|
|
2301
|
+
if (!window.confirm(`删除本地版本 ${releaseId}?不可恢复。`)) return
|
|
2302
|
+
try {
|
|
2303
|
+
await jsonFetch('/delete', { method: 'POST', body: JSON.stringify({ releaseId }) })
|
|
2304
|
+
void refresh()
|
|
2305
|
+
} catch (caught) { setError(caught.message) }
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
const statusUpdateInfo = updateInfo?.kind === 'update'
|
|
2309
|
+
? { ...updateInfo, onDownload: download }
|
|
2310
|
+
: updateInfo
|
|
2311
|
+
|
|
2312
|
+
const onSkinChanged = useCallback((next) => {
|
|
2313
|
+
setSkinState(next)
|
|
2314
|
+
setStatus((previous) => previous ? { ...previous,
|
|
2315
|
+
config: { ...(previous.config ?? {}), uiSkin: next } } : previous)
|
|
2316
|
+
}, [])
|
|
2317
|
+
|
|
2318
|
+
return h('div', { style: S.wrap, className: 'prts-settings-wrap' },
|
|
2319
|
+
error && h('div', { style: { ...S.card, ...S.error } }, error),
|
|
2320
|
+
h(SkinCard, { skin, onChanged: onSkinChanged }),
|
|
2321
|
+
h(DatasetLibraryCard, { status, activeRelease,
|
|
2322
|
+
releases: releaseList?.releases ?? [], enabledGames, savingGames,
|
|
2323
|
+
onEnabledGames: updateEnabledGames, onRefresh: refresh,
|
|
2324
|
+
onCheckUpdate: checkUpdate, checking, updateInfo: statusUpdateInfo,
|
|
2325
|
+
onActivate: activate, onDelete: remove, onDownload: download,
|
|
2326
|
+
downloading: busy || Boolean(status?.download?.active),
|
|
2327
|
+
}),
|
|
2328
|
+
h(ConfigCard, { config: status?.config, onSaved: refresh }))
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
const contentText = (content) => (Array.isArray(content) ? content : []).map((block) => {
|
|
2332
|
+
if (typeof block === 'string') return block
|
|
2333
|
+
if (typeof block?.text === 'string') return block.text
|
|
2334
|
+
if (block?.type === 'image' || block?.kind === 'image') return '[IMAGE_ATTACHMENT]'
|
|
2335
|
+
return ''
|
|
2336
|
+
}).filter(Boolean).join('\n')
|
|
2337
|
+
const assistantParts = (blocks) => {
|
|
2338
|
+
const parts = { text: [], reasoning: [] }
|
|
2339
|
+
for (const block of Array.isArray(blocks) ? blocks : []) {
|
|
2340
|
+
if (block?.kind === 'text' && block.text) parts.text.push(block.text)
|
|
2341
|
+
else if (block?.kind === 'reasoning' && block.text) parts.reasoning.push(block.text)
|
|
2342
|
+
}
|
|
2343
|
+
return { text: parts.text.join('\n'), reasoning: parts.reasoning.join('\n') }
|
|
2344
|
+
}
|
|
2345
|
+
const MAP_RUNTIME_ABI = 3
|
|
2346
|
+
let mapBundlePromise = null
|
|
2347
|
+
let cancelMapBundleLoad = null
|
|
2348
|
+
let loadedMapRuntime = null
|
|
2349
|
+
const validMapRuntime = (runtime) => runtime?.RUNTIME_ABI === MAP_RUNTIME_ABI
|
|
2350
|
+
&& typeof runtime.createRegionMap === 'function'
|
|
2351
|
+
const ensureMapBundle = () => {
|
|
2352
|
+
if (validMapRuntime(globalThis.__PRTS_ENDFIELD_MAP__)) {
|
|
2353
|
+
loadedMapRuntime = globalThis.__PRTS_ENDFIELD_MAP__
|
|
2354
|
+
return Promise.resolve(loadedMapRuntime)
|
|
2355
|
+
}
|
|
2356
|
+
// 未标 ABI 的旧 bundle 不得跨 HMR 复用。
|
|
2357
|
+
if (globalThis.__PRTS_ENDFIELD_MAP__) delete globalThis.__PRTS_ENDFIELD_MAP__
|
|
2358
|
+
if (mapBundlePromise) return mapBundlePromise
|
|
2359
|
+
let currentPromise
|
|
2360
|
+
let cancelCurrentLoad = null
|
|
2361
|
+
const pending = new Promise((resolve, reject) => {
|
|
2362
|
+
const script = document.createElement('script')
|
|
2363
|
+
script.src = `/api/prts-corpus/endfield-map/map.js?abi=${MAP_RUNTIME_ABI}`
|
|
2364
|
+
script.async = true
|
|
2365
|
+
let settled = false
|
|
2366
|
+
let loadTimer = null
|
|
2367
|
+
const finish = (callback, value) => {
|
|
2368
|
+
if (settled) return
|
|
2369
|
+
settled = true
|
|
2370
|
+
if (loadTimer !== null) clearTimeout(loadTimer)
|
|
2371
|
+
script.onload = null
|
|
2372
|
+
script.onerror = null
|
|
2373
|
+
script.remove()
|
|
2374
|
+
callback(value)
|
|
2375
|
+
}
|
|
2376
|
+
cancelCurrentLoad = (error = makeAbortError('地图模块加载已取消')) => finish(reject, error)
|
|
2377
|
+
cancelMapBundleLoad = cancelCurrentLoad
|
|
2378
|
+
script.onload = () => {
|
|
2379
|
+
const runtime = globalThis.__PRTS_ENDFIELD_MAP__
|
|
2380
|
+
if (!validMapRuntime(runtime)) {
|
|
2381
|
+
if (globalThis.__PRTS_ENDFIELD_MAP__ === runtime) {
|
|
2382
|
+
delete globalThis.__PRTS_ENDFIELD_MAP__
|
|
2383
|
+
}
|
|
2384
|
+
loadedMapRuntime = null
|
|
2385
|
+
finish(reject, new Error(`地图模块 ABI 不兼容(需要 ${MAP_RUNTIME_ABI})`))
|
|
2386
|
+
return
|
|
2387
|
+
}
|
|
2388
|
+
loadedMapRuntime = runtime
|
|
2389
|
+
finish(resolve, runtime)
|
|
2390
|
+
}
|
|
2391
|
+
script.onerror = () => finish(reject, new Error('地图模块加载失败'))
|
|
2392
|
+
document.head.appendChild(script)
|
|
2393
|
+
// 订阅者超时只取消自己的等待;底层也必须有 watchdog,否则一次既不
|
|
2394
|
+
// load 也不 error 的 script 会把后续所有 AIC 重挂永久钉在同一 pending。
|
|
2395
|
+
loadTimer = setTimeout(() => {
|
|
2396
|
+
cancelCurrentLoad(new Error('地图模块加载超时'))
|
|
2397
|
+
}, 45000)
|
|
2398
|
+
})
|
|
2399
|
+
currentPromise = pending.finally(() => {
|
|
2400
|
+
if (mapBundlePromise === currentPromise) mapBundlePromise = null
|
|
2401
|
+
if (cancelMapBundleLoad === cancelCurrentLoad) cancelMapBundleLoad = null
|
|
2402
|
+
})
|
|
2403
|
+
mapBundlePromise = currentPromise
|
|
2404
|
+
return currentPromise
|
|
2405
|
+
}
|
|
2406
|
+
const loadMapBundle = (signal) => {
|
|
2407
|
+
if (signal?.aborted) return Promise.reject(makeAbortError('地图加载已取消'))
|
|
2408
|
+
// script 是同一插件实例内的共享加载任务。组件卸载只取消自己的等待,
|
|
2409
|
+
// 不移除共享 script;否则快速切肤/slot 重挂会让旧实例误伤新实例。
|
|
2410
|
+
return waitWithDeadline(ensureMapBundle(), {
|
|
2411
|
+
signal, timeout: 50000, label: '地图模块加载',
|
|
2412
|
+
})
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
function AicMap({ focusPoint = .66 }) {
|
|
2416
|
+
const host = useRef(null)
|
|
2417
|
+
const mapRef = useRef(null)
|
|
2418
|
+
const focusPointRef = useRef(focusPoint)
|
|
2419
|
+
focusPointRef.current = focusPoint
|
|
2420
|
+
const [status, setStatus] = useState('MAP_ASSET_SYNC 0%')
|
|
2421
|
+
const [stats, setStats] = useState({ cx: 0, cy: 0, size: 0, fps: 0, meshes: 0, tris: 0 })
|
|
2422
|
+
const [positions, setPositions] = useState([])
|
|
2423
|
+
const [selected, setSelected] = useState(null)
|
|
2424
|
+
const [regions, setRegions] = useState([])
|
|
2425
|
+
useEffect(() => {
|
|
2426
|
+
let disposed = false
|
|
2427
|
+
const controller = new AbortController()
|
|
2428
|
+
const bootToken = aicBootEpoch
|
|
2429
|
+
// WebView2 进入托盘后 document.visibilityState 不一定立刻变化,桌面壳
|
|
2430
|
+
// 会额外发送 prts-shell-visibility。两条信号统一控制 Three.js 的 RAF,
|
|
2431
|
+
// 避免隐藏窗口仍持续渲染并让 renderer/GPU working set 不断增长。
|
|
2432
|
+
const pageIsActive = () => document.visibilityState !== 'hidden'
|
|
2433
|
+
&& globalThis.__PRTS_SHELL_BACKGROUND__ !== true
|
|
2434
|
+
&& !document.querySelector('[aria-modal="true"][role="dialog"]')
|
|
2435
|
+
const syncMapActivity = () => {
|
|
2436
|
+
const map = mapRef.current
|
|
2437
|
+
if (!map) return
|
|
2438
|
+
if (pageIsActive()) map.resume?.()
|
|
2439
|
+
else map.pause?.()
|
|
2440
|
+
}
|
|
2441
|
+
const syncModalState = () => {
|
|
2442
|
+
document.body.classList.toggle('aic-modal-open', isHostSettingsOpen())
|
|
2443
|
+
syncMapActivity()
|
|
2444
|
+
}
|
|
2445
|
+
document.addEventListener('visibilitychange', syncMapActivity)
|
|
2446
|
+
globalThis.addEventListener('prts-shell-visibility', syncMapActivity)
|
|
2447
|
+
// 设置弹窗覆盖 WebGL 时暂停 RAF。尤其在 macOS WebKit 中,全屏蒙层与
|
|
2448
|
+
// 持续运行的 Three.js 合成会显著放大 GPU 压力,表现为发光后卡死。
|
|
2449
|
+
const modalObserver = new MutationObserver(syncModalState)
|
|
2450
|
+
modalObserver.observe(document.body, { childList: true, subtree: true, attributes: true,
|
|
2451
|
+
attributeFilter: ['aria-modal', 'role'] })
|
|
2452
|
+
syncModalState()
|
|
2453
|
+
void loadMapBundle(controller.signal).then(async (runtime) => {
|
|
2454
|
+
if (disposed || !host.current) return
|
|
2455
|
+
setRegions(runtime.REGION_LIST ?? [])
|
|
2456
|
+
const map = await runtime.createRegionMap(host.current, {
|
|
2457
|
+
signal: controller.signal,
|
|
2458
|
+
focusPoint: focusPointRef.current,
|
|
2459
|
+
onProgress: (progress, message) => {
|
|
2460
|
+
if (disposed) return
|
|
2461
|
+
setStatus(progress < 0 ? `ERROR // ${message}` : `MAP_ASSET_SYNC ${progress}% // ${message}`)
|
|
2462
|
+
// 开机场加载屏由地图真实进度驱动;失败时停留片刻再撤下,露出页面内的错误状态。
|
|
2463
|
+
if (progress < 0) {
|
|
2464
|
+
aicBootProgress(-1, message, bootToken)
|
|
2465
|
+
aicBootSchedule(() => aicBootDone(bootToken, false), 2500, bootToken)
|
|
2466
|
+
} else aicBootProgress(progress, message, bootToken)
|
|
2467
|
+
},
|
|
2468
|
+
onStats: (value) => { if (!disposed) setStats(value) },
|
|
2469
|
+
onSelectLv: (value) => { if (!disposed) setSelected(value) },
|
|
2470
|
+
onRegionPositions: (value) => { if (!disposed) setPositions(value) },
|
|
2471
|
+
})
|
|
2472
|
+
if (disposed) map.dispose()
|
|
2473
|
+
else {
|
|
2474
|
+
// 装载期间响应式布局可能已变宽;应用最新值,不能丢掉那次更新。
|
|
2475
|
+
map.setFocusPoint?.(focusPointRef.current)
|
|
2476
|
+
mapRef.current = map
|
|
2477
|
+
syncMapActivity()
|
|
2478
|
+
setStatus('')
|
|
2479
|
+
aicBootDone(bootToken)
|
|
2480
|
+
}
|
|
2481
|
+
}).catch((error) => {
|
|
2482
|
+
if (disposed || error?.name === 'AbortError') return
|
|
2483
|
+
const message = error?.message || '地图加载失败'
|
|
2484
|
+
setStatus(`ERROR // ${message}`)
|
|
2485
|
+
aicBootProgress(-1, message, bootToken)
|
|
2486
|
+
aicBootSchedule(() => aicBootDone(bootToken, false), 2500, bootToken)
|
|
2487
|
+
})
|
|
2488
|
+
return () => {
|
|
2489
|
+
disposed = true
|
|
2490
|
+
controller.abort(makeAbortError('AIC 地图已卸载'))
|
|
2491
|
+
document.removeEventListener('visibilitychange', syncMapActivity)
|
|
2492
|
+
globalThis.removeEventListener('prts-shell-visibility', syncMapActivity)
|
|
2493
|
+
modalObserver.disconnect()
|
|
2494
|
+
document.body.classList.remove('aic-modal-open')
|
|
2495
|
+
mapRef.current?.dispose()
|
|
2496
|
+
mapRef.current = null
|
|
2497
|
+
}
|
|
2498
|
+
}, [])
|
|
2499
|
+
useEffect(() => {
|
|
2500
|
+
focusPointRef.current = focusPoint
|
|
2501
|
+
mapRef.current?.setFocusPoint?.(focusPoint)
|
|
2502
|
+
}, [focusPoint])
|
|
2503
|
+
const positionByLv = Object.fromEntries(positions.map((item) => [item.lv, item]))
|
|
2504
|
+
return h(Fragment, null,
|
|
2505
|
+
h('div', { ref: host, className: 'aic-map' }),
|
|
2506
|
+
status && h('div', { className: `aic-map-status${status.startsWith('ERROR') ? ' error' : ''}` }, status),
|
|
2507
|
+
...regions.map((region) => {
|
|
2508
|
+
const point = positionByLv[region.lv]
|
|
2509
|
+
if (!point?.visible) return null
|
|
2510
|
+
return h('button', { key: region.lv, type: 'button',
|
|
2511
|
+
className: `aic-region-label${selected === region.lv ? ' selected' : ''}`,
|
|
2512
|
+
'aria-pressed': selected === region.lv,
|
|
2513
|
+
'aria-label': `查看区域:${region.name}(${region.lv.toUpperCase()})`,
|
|
2514
|
+
onClick: () => {
|
|
2515
|
+
mapRef.current?.setSelected?.(region.lv)
|
|
2516
|
+
mapRef.current?.focusLv?.(region.lv)
|
|
2517
|
+
setSelected(region.lv)
|
|
2518
|
+
},
|
|
2519
|
+
style: { left: `${point.x}px`,
|
|
2520
|
+
top: `${point.y}px`, transform: 'translate(-50%,-50%)' } },
|
|
2521
|
+
h('i'), region.name, h('small', null, region.lv.toUpperCase()))
|
|
2522
|
+
}),
|
|
2523
|
+
selected && (() => {
|
|
2524
|
+
const region = regions.find((item) => item.lv === selected)
|
|
2525
|
+
return region ? h('aside', { className: 'aic-region-card' },
|
|
2526
|
+
h('small', null, `REGION_INTEL // ${region.lv.toUpperCase()}`),
|
|
2527
|
+
h('h3', null, region.name), h('em', null, region.nameEn),
|
|
2528
|
+
h('p', null, region.description), h('b', null, region.sourceLabel)) : null
|
|
2529
|
+
})(),
|
|
2530
|
+
h('div', { className: 'aic-hud aic-hud-tl' },
|
|
2531
|
+
h('div', { className: 'aic-brand' }, h('i', { className: 'aic-brand-mark' }),
|
|
2532
|
+
h('b', { className: 'aic-brand-name' }, 'AIC'),
|
|
2533
|
+
h('span', { className: 'aic-brand-sub' }, 'ENDFIELD_INDUSTRIES // TALOS-II SURVEY')),
|
|
2534
|
+
h('div', { className: 'aic-coords' }, h('span', null, `VIEW ${stats.cx},${stats.cy}`),
|
|
2535
|
+
h('span', null, `ZOOM ${stats.size}`))),
|
|
2536
|
+
h('div', { className: 'aic-hud aic-hud-br' },
|
|
2537
|
+
'DRAG/方向键 平移 // WHEEL/+/- 缩放 // TAB 区域按钮'),
|
|
2538
|
+
h('button', { type: 'button', className: 'aic-map-reset', onClick: () => mapRef.current?.resetView(),
|
|
2539
|
+
title: '重置地图视角' }, 'RESET_VIEW'))
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
const aicViewportWidth = () => Math.max(1,
|
|
2543
|
+
globalThis.document?.documentElement?.clientWidth || globalThis.innerWidth || 1)
|
|
2544
|
+
const aicBandRange = (viewportWidth) => {
|
|
2545
|
+
if (viewportWidth <= 980) {
|
|
2546
|
+
const fixed = Math.min(720, viewportWidth)
|
|
2547
|
+
return { min: fixed, max: fixed, preferred: fixed }
|
|
2548
|
+
}
|
|
2549
|
+
const max = Math.max(320, Math.min(1200, viewportWidth - 320))
|
|
2550
|
+
const min = Math.min(520, max)
|
|
2551
|
+
const preferred = Math.max(min, Math.min(max, 960, Math.max(760, viewportWidth * .46),
|
|
2552
|
+
viewportWidth - 360))
|
|
2553
|
+
return { min, max, preferred }
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
function AicRoot({ useSessions }) {
|
|
2557
|
+
// useSessions 由 ui-session 的 provideRoot 提供给 root 槽位;不含该包的
|
|
2558
|
+
// 组合下为 undefined——历史抽屉退化为空列表,新建对话仍走 sessions 服务。
|
|
2559
|
+
const sessions = useSessions?.((snapshot) => snapshot)
|
|
2560
|
+
?? { ids: [], byId: {}, current: undefined }
|
|
2561
|
+
const [historyOpen, setHistoryOpen] = useState(false)
|
|
2562
|
+
const [clock, setClock] = useState(() => new Date())
|
|
2563
|
+
const [viewportWidth, setViewportWidth] = useState(aicViewportWidth)
|
|
2564
|
+
const bandRange = aicBandRange(viewportWidth)
|
|
2565
|
+
const defaultBandWidth = bandRange.preferred
|
|
2566
|
+
const [chatBandWidth, setChatBandWidth] = useState(defaultBandWidth)
|
|
2567
|
+
const [chatResizeActive, setChatResizeActive] = useState(false)
|
|
2568
|
+
const stopChatResizeRef = useRef(null)
|
|
2569
|
+
const historyButtonRef = useRef(null)
|
|
2570
|
+
const historyDrawerRef = useRef(null)
|
|
2571
|
+
useEffect(() => {
|
|
2572
|
+
let timer = null
|
|
2573
|
+
const syncClock = () => {
|
|
2574
|
+
if (timer !== null) clearInterval(timer)
|
|
2575
|
+
timer = null
|
|
2576
|
+
if (!document.hidden) {
|
|
2577
|
+
setClock(new Date())
|
|
2578
|
+
timer = setInterval(() => setClock(new Date()), 1000)
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
document.addEventListener('visibilitychange', syncClock)
|
|
2582
|
+
syncClock()
|
|
2583
|
+
return () => {
|
|
2584
|
+
document.removeEventListener('visibilitychange', syncClock)
|
|
2585
|
+
if (timer !== null) clearInterval(timer)
|
|
2586
|
+
}
|
|
2587
|
+
}, [])
|
|
2588
|
+
useEffect(() => {
|
|
2589
|
+
const resize = () => {
|
|
2590
|
+
const width = aicViewportWidth()
|
|
2591
|
+
const range = aicBandRange(width)
|
|
2592
|
+
setViewportWidth(width)
|
|
2593
|
+
setChatBandWidth((currentWidth) => Math.max(range.min, Math.min(range.max, currentWidth)))
|
|
2594
|
+
}
|
|
2595
|
+
globalThis.addEventListener('resize', resize)
|
|
2596
|
+
globalThis.visualViewport?.addEventListener('resize', resize)
|
|
2597
|
+
resize()
|
|
2598
|
+
return () => {
|
|
2599
|
+
globalThis.removeEventListener('resize', resize)
|
|
2600
|
+
globalThis.visualViewport?.removeEventListener('resize', resize)
|
|
2601
|
+
}
|
|
2602
|
+
}, [])
|
|
2603
|
+
useEffect(() => {
|
|
2604
|
+
document.body.style.setProperty('--prts-aic-band-width', `${chatBandWidth}px`)
|
|
2605
|
+
return () => { document.body.style.removeProperty('--prts-aic-band-width') }
|
|
2606
|
+
}, [chatBandWidth])
|
|
2607
|
+
useEffect(() => () => { stopChatResizeRef.current?.() }, [])
|
|
2608
|
+
const current = sessions.current
|
|
2609
|
+
const currentSummary = current === undefined ? undefined : sessions.byId[current]
|
|
2610
|
+
const currentTitle = currentSummary?.title || (currentSummary?.blank ? '新会话' : 'AIC_TERMINAL')
|
|
2611
|
+
const sessionRows = useMemo(() => sessions.ids.map((id) => sessions.byId[id]).filter(Boolean),
|
|
2612
|
+
[sessions.ids, sessions.byId])
|
|
2613
|
+
useEffect(() => {
|
|
2614
|
+
if (!historyOpen) return undefined
|
|
2615
|
+
const drawer = historyDrawerRef.current
|
|
2616
|
+
const previousFocus = document.activeElement
|
|
2617
|
+
const focusables = () => [...(drawer?.querySelectorAll(
|
|
2618
|
+
'button:not([disabled]),[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])',
|
|
2619
|
+
) ?? [])]
|
|
2620
|
+
focusables()[0]?.focus()
|
|
2621
|
+
const onKeyDown = (event) => {
|
|
2622
|
+
if (event.key === 'Escape') {
|
|
2623
|
+
event.preventDefault()
|
|
2624
|
+
setHistoryOpen(false)
|
|
2625
|
+
return
|
|
2626
|
+
}
|
|
2627
|
+
if (event.key !== 'Tab') return
|
|
2628
|
+
const items = focusables()
|
|
2629
|
+
if (!items.length) { event.preventDefault(); return }
|
|
2630
|
+
const first = items[0]
|
|
2631
|
+
const last = items[items.length - 1]
|
|
2632
|
+
if (!drawer?.contains(document.activeElement)) {
|
|
2633
|
+
event.preventDefault()
|
|
2634
|
+
;(event.shiftKey ? last : first).focus()
|
|
2635
|
+
} else if (event.shiftKey && document.activeElement === first) {
|
|
2636
|
+
event.preventDefault(); last.focus()
|
|
2637
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
2638
|
+
event.preventDefault(); first.focus()
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
document.addEventListener('keydown', onKeyDown)
|
|
2642
|
+
return () => {
|
|
2643
|
+
document.removeEventListener('keydown', onKeyDown)
|
|
2644
|
+
if (typeof previousFocus?.focus === 'function' && previousFocus.isConnected) previousFocus.focus()
|
|
2645
|
+
else historyButtonRef.current?.focus()
|
|
2646
|
+
}
|
|
2647
|
+
}, [historyOpen])
|
|
2648
|
+
const newSession = () => {
|
|
2649
|
+
const options = currentSummary?.cwd ? { cwd: currentSummary.cwd } : {}
|
|
2650
|
+
void clientContext?.sessions?.create(options)
|
|
2651
|
+
.then((sessionId) => { clientContext?.sessions?.open(sessionId) })
|
|
2652
|
+
.catch((error) => console.error('[prts-terrarchive] failed to create session', error))
|
|
2653
|
+
}
|
|
2654
|
+
const openSettings = () => { openHostSettings() }
|
|
2655
|
+
const startChatResize = (event) => {
|
|
2656
|
+
if (viewportWidth <= 980) return
|
|
2657
|
+
event.preventDefault()
|
|
2658
|
+
stopChatResizeRef.current?.()
|
|
2659
|
+
setChatResizeActive(true)
|
|
2660
|
+
document.body.classList.add('aic-chat-resizing')
|
|
2661
|
+
const resize = (moveEvent) => {
|
|
2662
|
+
const range = aicBandRange(aicViewportWidth())
|
|
2663
|
+
setChatBandWidth(Math.max(range.min, Math.min(range.max, moveEvent.clientX)))
|
|
2664
|
+
}
|
|
2665
|
+
const stop = () => {
|
|
2666
|
+
setChatResizeActive(false)
|
|
2667
|
+
globalThis.removeEventListener('pointermove', resize)
|
|
2668
|
+
globalThis.removeEventListener('pointerup', stop)
|
|
2669
|
+
globalThis.removeEventListener('pointercancel', stop)
|
|
2670
|
+
document.body.classList.remove('aic-chat-resizing')
|
|
2671
|
+
stopChatResizeRef.current = null
|
|
2672
|
+
}
|
|
2673
|
+
stopChatResizeRef.current = stop
|
|
2674
|
+
globalThis.addEventListener('pointermove', resize)
|
|
2675
|
+
globalThis.addEventListener('pointerup', stop, { once: true })
|
|
2676
|
+
globalThis.addEventListener('pointercancel', stop, { once: true })
|
|
2677
|
+
}
|
|
2678
|
+
const resizeChatWithKeyboard = (event) => {
|
|
2679
|
+
if (viewportWidth <= 980) return
|
|
2680
|
+
let next = null
|
|
2681
|
+
if (event.key === 'ArrowLeft') next = chatBandWidth - 24
|
|
2682
|
+
else if (event.key === 'ArrowRight') next = chatBandWidth + 24
|
|
2683
|
+
else if (event.key === 'Home') next = bandRange.min
|
|
2684
|
+
else if (event.key === 'End') next = bandRange.max
|
|
2685
|
+
if (next === null) return
|
|
2686
|
+
event.preventDefault()
|
|
2687
|
+
setChatBandWidth(Math.max(bandRange.min, Math.min(bandRange.max, next)))
|
|
2688
|
+
}
|
|
2689
|
+
const focusPoint = Math.max(.55, Math.min(.86,
|
|
2690
|
+
.66 + (chatBandWidth - defaultBandWidth) / (Math.max(1, viewportWidth) * 2)))
|
|
2691
|
+
return h('main', { className: 'aic-root', style: {
|
|
2692
|
+
'--aic-band-width': `${chatBandWidth}px`,
|
|
2693
|
+
'--prts-aic-band-width': `${chatBandWidth}px`,
|
|
2694
|
+
} },
|
|
2695
|
+
h(AicMap, { focusPoint }),
|
|
2696
|
+
h('section', { className: 'aic-chat-band' },
|
|
2697
|
+
h('div', { className: 'aic-band-backdrop', 'aria-hidden': 'true' }),
|
|
2698
|
+
h('div', { className: 'aic-chat-resize', role: 'separator', tabIndex: 0,
|
|
2699
|
+
title: '拖动或使用左右方向键调整聊天区域宽度',
|
|
2700
|
+
'aria-label': '调整聊天区域宽度', 'aria-orientation': 'vertical',
|
|
2701
|
+
'aria-valuemin': Math.round(bandRange.min), 'aria-valuemax': Math.round(bandRange.max),
|
|
2702
|
+
'aria-valuenow': Math.round(chatBandWidth), onKeyDown: resizeChatWithKeyboard,
|
|
2703
|
+
onPointerDown: startChatResize }),
|
|
2704
|
+
h('section', { className: 'aic-start-brand', 'aria-hidden': 'true' },
|
|
2705
|
+
h('div', { className: 'aic-start-wordmark' }, 'PRTS'),
|
|
2706
|
+
h('div', { className: 'aic-start-submark' },
|
|
2707
|
+
h('s', null, 'PRIMITIVE RHODES ISLAND TERMINAL SERVICE'), ' // PRIES—???'),
|
|
2708
|
+
h('h2', null, '想从泰拉的故事里了解什么?'),
|
|
2709
|
+
h('p', null, '我会先检索线索,再回到本地资料逐行核验,并把可复查的原文放在回答旁边。')),
|
|
2710
|
+
h('div', { className: 'aic-terminal-overlay' },
|
|
2711
|
+
h('div', { className: 'aic-terminal-name' }, h('i', { className: 'aic-terminal-dot' }),
|
|
2712
|
+
h('span', { title: currentTitle }, currentTitle),
|
|
2713
|
+
h('small', { className: 'aic-terminal-ver' }, 'V9.2 // WULING_NODE')),
|
|
2714
|
+
h('button', { type: 'button', className: 'aic-band-btn aic-settings-trigger',
|
|
2715
|
+
onClick: openSettings }, '设置'),
|
|
2716
|
+
h('button', { ref: historyButtonRef, type: 'button',
|
|
2717
|
+
className: `aic-band-btn${historyOpen ? ' on' : ''}`,
|
|
2718
|
+
'aria-expanded': historyOpen, 'aria-controls': 'aic-session-archive',
|
|
2719
|
+
onClick: () => setHistoryOpen((value) => !value) }, '◷ 历史记录'),
|
|
2720
|
+
h('button', { type: 'button', className: 'aic-band-btn', onClick: newSession }, '+ 新建对话'))),
|
|
2721
|
+
chatResizeActive && h('div', { className: 'aic-chat-resize-line',
|
|
2722
|
+
style: { left: `${chatBandWidth}px` } }),
|
|
2723
|
+
h('div', { className: 'aic-hud aic-hud-tr' },
|
|
2724
|
+
h('span', { className: 'aic-clock' }, clock.toLocaleTimeString('zh-CN', { hour12: false }))),
|
|
2725
|
+
historyOpen && h(Fragment, null,
|
|
2726
|
+
h('div', { className: 'aic-drawer-scrim', 'aria-hidden': 'true',
|
|
2727
|
+
onPointerDown: (event) => { event.preventDefault(); setHistoryOpen(false) } }),
|
|
2728
|
+
h('aside', { ref: historyDrawerRef, id: 'aic-session-archive',
|
|
2729
|
+
className: 'aic-drawer', role: 'dialog', 'aria-modal': 'true', 'aria-label': '历史会话' },
|
|
2730
|
+
h('header', { className: 'aic-drawer-head' }, h('span', null, 'SESSION_ARCHIVE'),
|
|
2731
|
+
h('button', { type: 'button', className: 'aic-close', 'aria-label': '关闭历史会话',
|
|
2732
|
+
onClick: () => setHistoryOpen(false) }, '×')),
|
|
2733
|
+
h('button', { type: 'button', className: 'aic-band-btn', onClick: newSession }, '+ 新建对话'),
|
|
2734
|
+
h('div', { className: 'aic-session-list' }, sessionRows.length
|
|
2735
|
+
? sessionRows.map((session) => h('button', { type: 'button', key: session.id,
|
|
2736
|
+
className: `aic-session${session.id === current ? ' active' : ''}`,
|
|
2737
|
+
'aria-current': session.id === current ? 'page' : undefined,
|
|
2738
|
+
onClick: () => { clientContext?.sessions?.open(session.id); setHistoryOpen(false) } },
|
|
2739
|
+
h('b', null, session.title || (session.blank ? '新会话' : `会话_${String(session.id).slice(0, 6)}`)),
|
|
2740
|
+
h('small', null, `${session.blank ? 'STANDBY' : 'ARCHIVED'} // ${new Date(session.updatedAt).toLocaleString('zh-CN')}`)))
|
|
2741
|
+
: h('div', { className: 'aic-empty' }, 'NO SESSION RECORDS'))))
|
|
2742
|
+
)
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
syncAicLayout = () => {
|
|
2746
|
+
if (!clientContext) return
|
|
2747
|
+
if (activeSkin !== 'endfield-aic') {
|
|
2748
|
+
if (disposeAicLayout) disposeAicLayout()
|
|
2749
|
+
disposeAicLayout = null
|
|
2750
|
+
return
|
|
2751
|
+
}
|
|
2752
|
+
if (disposeAicLayout) return
|
|
2753
|
+
const owner = clientContext
|
|
2754
|
+
// shell.overlay 由 ui-layout 的 root entry 动态声明。inject 会等待声明,
|
|
2755
|
+
// declaration collapse 时卸载,并在 owner HMR 重挂后自动重新注册。
|
|
2756
|
+
const disposeInjection = owner.slots.inject('shell.overlay', () => {
|
|
2757
|
+
if (activeSkin !== 'endfield-aic' || clientContext !== owner) return () => {}
|
|
2758
|
+
return owner.slots.register({
|
|
2759
|
+
name: 'shell.overlay', id: 'prts-aic-shell', order: -100,
|
|
2760
|
+
}, AicRoot)
|
|
2761
|
+
})
|
|
2762
|
+
disposeAicLayout = () => { disposeInjection() }
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
/* Rhine archive: durable returned tool material, independent from camera state. */
|
|
2766
|
+
const rhineSkinListeners = new Set()
|
|
2767
|
+
const rhineCleanups = new Set()
|
|
2768
|
+
let rhineBundlePromise = null
|
|
2769
|
+
let rhineBundleOwner = null
|
|
2770
|
+
const RHINE_BASE = '/api/prts-corpus/rhine/'
|
|
2771
|
+
const rhineNode = (nodes, key) => typeof nodes?.get === 'function' ? nodes.get(key) : nodes?.[key]
|
|
2772
|
+
const rhineToolPresentation = (root) => {
|
|
2773
|
+
const supported = (meta) => meta?.kind === 'prts-archive-sources-v1' || meta?.kind === 'prts-corpus-read-v1'
|
|
2774
|
+
if (supported(root?.meta)) return root.meta
|
|
2775
|
+
if (root?.kind !== 'tool-result' || typeof root.parentCallId !== 'string'
|
|
2776
|
+
|| !Array.isArray(root.content)) return null
|
|
2777
|
+
// PTC receipts are their own log-only block, never text to parse or show.
|
|
2778
|
+
// Bind all three identities before accepting a durable source projection.
|
|
2779
|
+
const receipt = root.content.find((block) => block?.type === 'prts-archive-receipt'
|
|
2780
|
+
&& block.version === 1 && block.callId === root.callId
|
|
2781
|
+
&& block.parentCallId === root.parentCallId && block.tool === root.call?.name
|
|
2782
|
+
&& supported(block.meta) && Array.isArray(block.meta.sources)
|
|
2783
|
+
&& block.meta.sources.length <= 128)
|
|
2784
|
+
return receipt?.meta || null
|
|
2785
|
+
}
|
|
2786
|
+
const mergeArchiveSources = (sources) => {
|
|
2787
|
+
const result = new Map()
|
|
2788
|
+
const rank = { found: 0, read: 1, cited: 2 }
|
|
2789
|
+
for (const item of sources) {
|
|
2790
|
+
if (!item || typeof item.title !== 'string' || !item.title.trim()) continue
|
|
2791
|
+
const version = validDataVersion(item.dataVersion) ? item.dataVersion : ''
|
|
2792
|
+
const identity = version && (item.documentId || item.documentUid)
|
|
2793
|
+
? `${version}:${item.documentId || item.documentUid}` : item.id
|
|
2794
|
+
if (typeof identity !== 'string' || !identity) continue
|
|
2795
|
+
const incoming = { ...item, title: item.title.slice(0, 512),
|
|
2796
|
+
id: version && item.documentUid ? `document:${version}:${item.documentUid}` : item.id || identity,
|
|
2797
|
+
origin: ['cloud', 'web'].includes(item.origin) ? item.origin : 'local',
|
|
2798
|
+
state: Object.hasOwn(rank, item.state) ? item.state : 'found',
|
|
2799
|
+
excerpt: typeof item.excerpt === 'string' ? item.excerpt : '',
|
|
2800
|
+
dataVersion: version || undefined }
|
|
2801
|
+
const previous = result.get(identity)
|
|
2802
|
+
const ranges = [...(previous?.ranges || []), ...(incoming.ranges || []),
|
|
2803
|
+
...(Number.isInteger(item.lineStart) && item.lineStart > 0
|
|
2804
|
+
? [{ start: item.lineStart, end: item.lineEnd || item.lineStart }] : [])]
|
|
2805
|
+
.filter((r) => Number.isSafeInteger(r?.start) && Number.isSafeInteger(r?.end)
|
|
2806
|
+
&& r.start > 0 && r.end >= r.start).sort((a, b) => a.start - b.start)
|
|
2807
|
+
const mergedRanges = []
|
|
2808
|
+
for (const range of ranges) {
|
|
2809
|
+
const last = mergedRanges.at(-1)
|
|
2810
|
+
if (last && range.start <= last.end + 1) last.end = Math.max(last.end, range.end)
|
|
2811
|
+
else mergedRanges.push({ start: range.start, end: range.end })
|
|
2812
|
+
}
|
|
2813
|
+
const readRanges = [...new Map([...(previous?.readRanges || []), ...(incoming.readRanges || [])]
|
|
2814
|
+
.filter((r) => Number.isSafeInteger(r?.start) && Number.isSafeInteger(r?.end) && r.start > 0 && r.end >= r.start)
|
|
2815
|
+
.map((r) => [`${r.start}:${r.end}`, { start: r.start, end: r.end }])).values()]
|
|
2816
|
+
result.set(identity, { ...previous, ...incoming,
|
|
2817
|
+
id: version && (incoming.documentUid || previous?.documentUid)
|
|
2818
|
+
? `document:${version}:${incoming.documentUid || previous.documentUid}` : previous?.id || incoming.id,
|
|
2819
|
+
origin: previous?.origin === 'cloud' ? 'cloud' : incoming.origin,
|
|
2820
|
+
documentId: incoming.documentId || previous?.documentId,
|
|
2821
|
+
documentUid: incoming.documentUid || previous?.documentUid,
|
|
2822
|
+
sourceRef: incoming.sourceRef || previous?.sourceRef,
|
|
2823
|
+
kind: previous?.kind && ['corpus_read', 'reference', 'source'].includes(incoming.kind)
|
|
2824
|
+
? previous.kind : incoming.kind || previous?.kind,
|
|
2825
|
+
state: previous && rank[previous.state] > rank[incoming.state] ? previous.state : incoming.state,
|
|
2826
|
+
agentRead: previous?.agentRead === true || incoming.agentRead === true
|
|
2827
|
+
|| previous?.state === 'read' || incoming.state === 'read' || readRanges.length > 0,
|
|
2828
|
+
excerpt: incoming.excerpt || previous?.excerpt || '', ranges: mergedRanges,
|
|
2829
|
+
readRanges })
|
|
2830
|
+
}
|
|
2831
|
+
return [...result.values()]
|
|
2832
|
+
}
|
|
2833
|
+
// A title mention alone is not evidence of citation. Only explicit corpus
|
|
2834
|
+
// citation syntax or an exact source_ref can mark a returned source as cited.
|
|
2835
|
+
const rhineCitationIndex = sources => {
|
|
2836
|
+
const candidates = mergeArchiveSources(sources), titles = new Map(), refs = new Map()
|
|
2837
|
+
for (const source of candidates) {
|
|
2838
|
+
if (!titles.has(source.title)) titles.set(source.title, [])
|
|
2839
|
+
titles.get(source.title).push(source)
|
|
2840
|
+
if (source.sourceRef && typeof source.sourceRef === 'string') {
|
|
2841
|
+
if (!refs.has(source.sourceRef)) refs.set(source.sourceRef, [])
|
|
2842
|
+
refs.get(source.sourceRef).push(source)
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
const exactRefs = []
|
|
2846
|
+
for (const [ref, hits] of refs) {
|
|
2847
|
+
if (hits.length !== 1) continue
|
|
2848
|
+
const escaped = ref.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
2849
|
+
exactRefs.push([hits[0].id, new RegExp(`(?:^|[\\s\\x60("'=:])${escaped}(?=$|[\\s\\x60)"'\\],.;,。;])`, 'u')])
|
|
2850
|
+
}
|
|
2851
|
+
return { titles, exactRefs }
|
|
2852
|
+
}
|
|
2853
|
+
const citedArchiveIds = (sources, answer, wikiCitations, index = rhineCitationIndex(sources)) => {
|
|
2854
|
+
const cited = new Set()
|
|
2855
|
+
const matchesRange = (source, start, end) => (source.ranges || []).some((range) =>
|
|
2856
|
+
range.start <= start && range.end >= end)
|
|
2857
|
+
for (const match of answer.matchAll(/《([^》\n]+)》(?:(document_uid=(doc_[A-Za-z0-9_-]{16})))?\s*(?:第\s*(\d+)(?:\s*[-—–~]\s*(\d+))?\s*行|Wiki·([^\s。;,,;\n]+))/gu)) {
|
|
2858
|
+
const start = Number(match[3]), end = Number(match[4] || match[3])
|
|
2859
|
+
const hits = (index.titles.get(match[1]) || []).filter(source => !match[2] || source.documentUid === match[2])
|
|
2860
|
+
// UID alone cannot disambiguate two revisions of the same document.
|
|
2861
|
+
if (hits.length !== 1) continue
|
|
2862
|
+
const source = hits[0]
|
|
2863
|
+
if (match[5] ? wikiCitations?.get(source.id)?.has(match[5])
|
|
2864
|
+
: Number.isSafeInteger(start) && start > 0 && end >= start && matchesRange(source, start, end)) cited.add(source.id)
|
|
2865
|
+
}
|
|
2866
|
+
for (const [id, pattern] of index.exactRefs) if (pattern.test(answer)) cited.add(id)
|
|
2867
|
+
return cited
|
|
2868
|
+
}
|
|
2869
|
+
const isRhineWebTool = name => name === 'web_search' || name === 'web_fetch'
|
|
2870
|
+
const isRhineReadTool = name => name === 'corpus_read' || name === 'cloud_inspect' || name === 'web_fetch'
|
|
2871
|
+
const rhineWebUrl = value => {
|
|
2872
|
+
if (typeof value !== 'string') return ''
|
|
2873
|
+
try {
|
|
2874
|
+
const url = new URL(value)
|
|
2875
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) return ''
|
|
2876
|
+
url.hash = ''
|
|
2877
|
+
return url.href
|
|
2878
|
+
} catch { return '' }
|
|
2879
|
+
}
|
|
2880
|
+
// Consume the web tools' durable structured metadata, never links guessed
|
|
2881
|
+
// from arbitrary tool output. Fetch bodies remain the exact returned text.
|
|
2882
|
+
const rhineWebSources = (name, args, meta, text, callId, prior) => {
|
|
2883
|
+
if (name === 'web_search') return (Array.isArray(meta?.sources) ? meta.sources : []).slice(0, 128).flatMap(item => {
|
|
2884
|
+
const url = rhineWebUrl(item?.url)
|
|
2885
|
+
if (!url) return []
|
|
2886
|
+
return [{ id: `web:${url}`, url, title: typeof item.title === 'string' && item.title.trim() ? item.title : url,
|
|
2887
|
+
kind: 'web_page', origin: 'web', state: 'found', callId,
|
|
2888
|
+
excerpt: typeof item.snippet === 'string' ? item.snippet : '' }]
|
|
2889
|
+
})
|
|
2890
|
+
const requested = rhineWebUrl(args.url), url = rhineWebUrl(meta?.url) || requested
|
|
2891
|
+
if (!url || !text.trim() || Number(meta?.statusCode) >= 400) return []
|
|
2892
|
+
const id = `web:${requested || url}`
|
|
2893
|
+
const previous = [...prior].reverse().find(source => source.id === id || source.origin === 'web' && rhineWebUrl(source.url) === url)
|
|
2894
|
+
const heading = text.match(/^#{1,2}\s+([^\n]+)$/mu)?.[1]?.trim()
|
|
2895
|
+
return [{ id: previous?.id || id, url, title: previous?.title || heading || url,
|
|
2896
|
+
kind: 'web_page', origin: 'web', state: 'read', agentRead: true, callId,
|
|
2897
|
+
excerpt: text.slice(0, 1600), content: text, contentTruncated: meta?.truncated === true }]
|
|
2898
|
+
}
|
|
2899
|
+
const rhinePhase = (snapshot) => {
|
|
2900
|
+
if (snapshot.searching) return isRhineReadTool(snapshot.tool) ? 'reading' : 'searching'
|
|
2901
|
+
if (snapshot.running) return 'synthesizing'
|
|
2902
|
+
if (snapshot.error) return 'error'
|
|
2903
|
+
return snapshot.answer ? 'complete' : 'idle'
|
|
2904
|
+
}
|
|
2905
|
+
// Detached input values detect same-length and in-place edits. Equal
|
|
2906
|
+
// subtrees stay shared without serializing the entire transcript per tick.
|
|
2907
|
+
const retainRhineValue = (value, previous) => {
|
|
2908
|
+
if (Object.is(value, previous)) return previous
|
|
2909
|
+
if (!value || typeof value !== 'object') return value
|
|
2910
|
+
const array = Array.isArray(value)
|
|
2911
|
+
const compatible = previous && typeof previous === 'object' && Array.isArray(previous) === array
|
|
2912
|
+
const keys = Object.keys(value)
|
|
2913
|
+
const previousKeys = compatible ? Object.keys(previous) : []
|
|
2914
|
+
let result = compatible ? undefined : array ? [] : {}
|
|
2915
|
+
const copy = () => result ||= array ? previous.slice() : { ...previous }
|
|
2916
|
+
for (const key of keys) {
|
|
2917
|
+
const next = retainRhineValue(value[key], compatible ? previous[key] : undefined)
|
|
2918
|
+
if (!compatible || !Object.hasOwn(previous, key) || !Object.is(next, previous[key])) {
|
|
2919
|
+
copy()
|
|
2920
|
+
if (key === '__proto__') Object.defineProperty(result, key, { value: next, enumerable: true, writable: true, configurable: true })
|
|
2921
|
+
else result[key] = next
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
for (const key of previousKeys) if (!Object.hasOwn(value, key)) { copy(); delete result[key] }
|
|
2925
|
+
if (array && (!compatible || value.length !== previous.length)) { copy(); result.length = value.length }
|
|
2926
|
+
return result || previous
|
|
2927
|
+
}
|
|
2928
|
+
const rhineSnapshotEntries = (order, nodes) => {
|
|
2929
|
+
const entries = []
|
|
2930
|
+
for (const key of order || []) {
|
|
2931
|
+
const node = rhineNode(nodes, key)
|
|
2932
|
+
entries.push([key, node])
|
|
2933
|
+
if (node?.kind !== 'tool-call') continue
|
|
2934
|
+
const seen = new Set([node.data?.root])
|
|
2935
|
+
const queue = [...(node.data?.root?.subCalls || [])].reverse()
|
|
2936
|
+
let count = 0
|
|
2937
|
+
while (queue.length && count++ < 16384) {
|
|
2938
|
+
const child = queue.pop()
|
|
2939
|
+
if (!child || seen.has(child)) continue
|
|
2940
|
+
seen.add(child)
|
|
2941
|
+
entries.push([`${key}:${child.callId || count}`, { kind: 'tool-call', data: { root: child },
|
|
2942
|
+
location: node.location, anchorSeq: node.anchorSeq }])
|
|
2943
|
+
queue.push(...(child.subCalls || []).slice().reverse())
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
return entries
|
|
2947
|
+
}
|
|
2948
|
+
const rhineTurnInput = turn => turn ? { turn: turn.turn, start: turn.start, end: turn.end } : undefined
|
|
2949
|
+
const rhineSnapshotInput = snapshot => {
|
|
2950
|
+
const entries = rhineSnapshotEntries(snapshot?.order, snapshot?.nodes).map(([key, node]) => {
|
|
2951
|
+
if (!node) return [key, node]
|
|
2952
|
+
const raw = node.data, root = raw?.root
|
|
2953
|
+
const data = { seq: raw?.seq, turn: raw?.turn }
|
|
2954
|
+
if (node.kind === 'assistant-step') {
|
|
2955
|
+
data.status = raw?.status
|
|
2956
|
+
data.blocks = [{ kind: 'text', text: visibleAssistantText(raw?.blocks) }]
|
|
2957
|
+
} else if (node.kind === 'user' || node.kind === 'steering') {
|
|
2958
|
+
data.content = (raw?.content || []).filter(block => block?.type === 'text')
|
|
2959
|
+
.map(block => ({ type: 'text', text: block.text }))
|
|
2960
|
+
} else if (node.kind === 'tool-call' && root) {
|
|
2961
|
+
data.root = { name: root.name, argsRaw: root.argsRaw,
|
|
2962
|
+
...('kind' in root ? { kind: root.kind } : {}),
|
|
2963
|
+
call: root.call ? { name: root.call.name, argsRaw: root.call.argsRaw } : undefined,
|
|
2964
|
+
meta: root.meta, content: root.content, isError: root.isError, time: root.time,
|
|
2965
|
+
callTime: root.callTime, callId: root.callId, parentCallId: root.parentCallId,
|
|
2966
|
+
turn: root.turn, seq: root.seq }
|
|
2967
|
+
} else if (node.kind === 'turn-error' || node.kind === 'turn-max-tokens') data.message = raw?.message
|
|
2968
|
+
return [key, { kind: node.kind, data, anchorSeq: node.anchorSeq,
|
|
2969
|
+
location: { turn: rhineTurnInput(node.location?.turn) } }]
|
|
2970
|
+
})
|
|
2971
|
+
const turnNumber = snapshot?.timeline?.turnOrder?.at(-1)
|
|
2972
|
+
const turn = turnNumber === undefined ? undefined : snapshot?.timeline?.turns?.get(turnNumber)
|
|
2973
|
+
return { entries, turnNumber, turn: rhineTurnInput(turn) }
|
|
2974
|
+
}
|
|
2975
|
+
function createRhineProjectionCache(diagnostics) {
|
|
2976
|
+
const slots = new Map()
|
|
2977
|
+
let used = new Set()
|
|
2978
|
+
return {
|
|
2979
|
+
begin() { used = new Set() },
|
|
2980
|
+
end() { for (const key of slots.keys()) if (!used.has(key)) slots.delete(key) },
|
|
2981
|
+
array(key, value) {
|
|
2982
|
+
used.add(key)
|
|
2983
|
+
const previous = slots.get(key)?.value
|
|
2984
|
+
if (previous?.length === value.length && value.every((item, i) => Object.is(item, previous[i]))) return previous
|
|
2985
|
+
const next = value.slice()
|
|
2986
|
+
slots.set(key, { value: next })
|
|
2987
|
+
return next
|
|
2988
|
+
},
|
|
2989
|
+
retain(key, value) {
|
|
2990
|
+
used.add(key)
|
|
2991
|
+
const next = retainRhineValue(value, slots.get(key)?.value)
|
|
2992
|
+
slots.set(key, { value: next })
|
|
2993
|
+
return next
|
|
2994
|
+
},
|
|
2995
|
+
memo(key, deps, task, category) {
|
|
2996
|
+
used.add(key)
|
|
2997
|
+
const previous = slots.get(key)
|
|
2998
|
+
if (previous?.deps?.length === deps.length && deps.every((value, i) => Object.is(value, previous.deps[i]))) {
|
|
2999
|
+
diagnostics?.cacheEvent(category, true)
|
|
3000
|
+
return previous.value
|
|
3001
|
+
}
|
|
3002
|
+
diagnostics?.cacheEvent(category, false)
|
|
3003
|
+
const value = task()
|
|
3004
|
+
slots.set(key, { deps, value })
|
|
3005
|
+
return value
|
|
3006
|
+
},
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
const buildRhineSnapshot = (order = [], nodes, sessionId = '', sessionRunning = false, timeline, cache) => {
|
|
3010
|
+
const memo = (key, deps, task, category) => cache ? cache.memo(key, deps, task, category) : task()
|
|
3011
|
+
const retain = (key, value) => cache ? cache.retain(key, value) : value
|
|
3012
|
+
const retainArray = (key, value) => cache ? cache.array(key, value) : value
|
|
3013
|
+
const sources = []
|
|
3014
|
+
const records = []
|
|
3015
|
+
const operations = []
|
|
3016
|
+
const toolCalls = []
|
|
3017
|
+
const operationSources = new Map()
|
|
3018
|
+
const entries = cache?.entries || rhineSnapshotEntries(order, nodes)
|
|
3019
|
+
const userEntries = entries.filter(([, node]) => node?.kind === 'user' || node?.kind === 'steering')
|
|
3020
|
+
const lastInput = userEntries.at(-1)
|
|
3021
|
+
const inputSeq = lastInput?.[1]?.data?.seq ?? lastInput?.[1]?.anchorSeq
|
|
3022
|
+
const turnNumber = timeline?.turnOrder?.at(-1)
|
|
3023
|
+
let currentTurn = turnNumber === undefined ? undefined : timeline?.turns?.get(turnNumber)
|
|
3024
|
+
if (!currentTurn) {
|
|
3025
|
+
currentTurn = [...entries].reverse().find(([, node]) => node?.location?.turn)?.[1]?.location?.turn
|
|
3026
|
+
}
|
|
3027
|
+
// A newly admitted message can precede its turn/start publication. Never
|
|
3028
|
+
// reuse the previous turn's successful end for that pending question.
|
|
3029
|
+
if (currentTurn?.end && Number.isFinite(inputSeq) && inputSeq > currentTurn.end.seq) currentTurn = undefined
|
|
3030
|
+
const investigationId = currentTurn ? `turn:${currentTurn.turn}`
|
|
3031
|
+
: lastInput ? `message:${lastInput[0]}` : entries.length ? `node:${entries.at(-1)[0]}` : ''
|
|
3032
|
+
const belongsToCurrentTurn = (node) => {
|
|
3033
|
+
if (!currentTurn) return true
|
|
3034
|
+
const owner = node?.location?.turn?.turn ?? node?.data?.turn ?? node?.data?.root?.turn
|
|
3035
|
+
if (owner !== undefined) return owner === currentTurn.turn
|
|
3036
|
+
const seq = node?.anchorSeq ?? node?.data?.seq ?? node?.data?.root?.seq
|
|
3037
|
+
return Number.isFinite(seq) && seq >= (currentTurn.start?.seq ?? Infinity)
|
|
3038
|
+
&& seq <= (currentTurn.end?.seq ?? Infinity)
|
|
3039
|
+
}
|
|
3040
|
+
let turnSourceStart = 0
|
|
3041
|
+
const turnSources = []
|
|
3042
|
+
let searching = false
|
|
3043
|
+
let question = ''
|
|
3044
|
+
let query = ''
|
|
3045
|
+
let tool = ''
|
|
3046
|
+
let answer = ''
|
|
3047
|
+
let error = ''
|
|
3048
|
+
let interrupted = false
|
|
3049
|
+
let terminalError = false
|
|
3050
|
+
let assistantRunning = false
|
|
3051
|
+
let activeArgs = null
|
|
3052
|
+
const cited = new Set()
|
|
3053
|
+
const wikiCitations = new Map()
|
|
3054
|
+
const toolErrors = new Map()
|
|
3055
|
+
const citations = (key, material, text) => {
|
|
3056
|
+
if (!cache) return citedArchiveIds(material, text, wikiCitations)
|
|
3057
|
+
const input = retainArray('citation-input:' + key, material)
|
|
3058
|
+
const wiki = retain('wiki-input:' + key, [...wikiCitations].map(([id, values]) => [id, [...values]]))
|
|
3059
|
+
const candidates = memo('citation-index:' + key, [input], () => rhineCitationIndex(input), 'citation')
|
|
3060
|
+
return memo('citations:' + key, [candidates, wiki, text], () => citedArchiveIds(input, text, wikiCitations, candidates), 'citation')
|
|
3061
|
+
}
|
|
3062
|
+
for (const [key, node] of entries) {
|
|
3063
|
+
if (node?.kind === 'user' || node?.kind === 'steering') {
|
|
3064
|
+
const text = (node.data?.content || []).filter((block) => block?.type === 'text')
|
|
3065
|
+
.map((block) => block.text || '').join('\n').trim()
|
|
3066
|
+
question = text
|
|
3067
|
+
// New user input starts a new answer; keep the durable material rack.
|
|
3068
|
+
answer = ''; error = ''; assistantRunning = false; toolErrors.clear()
|
|
3069
|
+
interrupted = false; terminalError = false
|
|
3070
|
+
// With a real Turn location, steering belongs to the same ongoing
|
|
3071
|
+
// investigation. Older renderers fall back to the last input boundary.
|
|
3072
|
+
if (!currentTurn) { turnSourceStart = sources.length; operations.length = 0; toolCalls.length = 0; operationSources.clear() }
|
|
3073
|
+
if (node.kind === 'user' || !currentTurn) { searching = false; activeArgs = null }
|
|
3074
|
+
if (!searching) { query = ''; tool = '' }
|
|
3075
|
+
continue
|
|
3076
|
+
}
|
|
3077
|
+
if (node?.kind === 'assistant-step') {
|
|
3078
|
+
const text = visibleAssistantText(node.data?.blocks)
|
|
3079
|
+
const current = belongsToCurrentTurn(node)
|
|
3080
|
+
if (current) assistantRunning = node.data?.status === 'running'
|
|
3081
|
+
if (text) {
|
|
3082
|
+
if (current) answer = text
|
|
3083
|
+
for (const id of citations('assistant:' + key, sources, text)) cited.add(id)
|
|
3084
|
+
}
|
|
3085
|
+
if (current && node.data?.status === 'interrupted') { error = '调查已停止'; interrupted = true }
|
|
3086
|
+
continue
|
|
3087
|
+
}
|
|
3088
|
+
if (node?.kind === 'turn-error' || node?.kind === 'turn-max-tokens') {
|
|
3089
|
+
if (!belongsToCurrentTurn(node)) continue
|
|
3090
|
+
error = node.data?.message || (node.kind === 'turn-max-tokens' ? '回答达到输出上限,调查未完成' : '调查未完成')
|
|
3091
|
+
terminalError = true
|
|
3092
|
+
assistantRunning = false
|
|
3093
|
+
continue
|
|
3094
|
+
}
|
|
3095
|
+
if (node?.kind !== 'tool-call') continue
|
|
3096
|
+
const root = node.data?.root
|
|
3097
|
+
const projection = memo('tool:' + key, [root], () => {
|
|
3098
|
+
const call = toolSnapshot(root)
|
|
3099
|
+
const meta = rhineToolPresentation(root)
|
|
3100
|
+
if ((typeof meta?.error === 'string' && meta.error || call?.name === 'web_fetch' && Number(root?.meta?.statusCode) >= 400) && call) call.state = 'error'
|
|
3101
|
+
return { call, meta, args: parseToolArguments(call?.argsRaw),
|
|
3102
|
+
text: root?.kind === 'tool-result' ? contentText(root.content) : '' }
|
|
3103
|
+
}, 'tool')
|
|
3104
|
+
const { call, meta, args, text } = projection
|
|
3105
|
+
if (!call?.name) continue
|
|
3106
|
+
const locator = args.locator && typeof args.locator === 'object' ? args.locator : args
|
|
3107
|
+
const currentQuery = String(args.query || args.title || locator.display_title || locator.document_id
|
|
3108
|
+
|| locator.document_uid || locator.source_ref || args.url
|
|
3109
|
+
|| (Array.isArray(args.queries) ? args.queries.filter(item => typeof item === 'string').join(' · ') : '')
|
|
3110
|
+
|| args.activity_name || args.collection_name || args.section || '')
|
|
3111
|
+
const current = belongsToCurrentTurn(node)
|
|
3112
|
+
records.push(memo('record:' + key, [call, currentQuery, text], () =>
|
|
3113
|
+
({ id: String(key), tool: call.name, state: call.state, query: currentQuery, text }), 'tool'))
|
|
3114
|
+
if (current) {
|
|
3115
|
+
const startedAt = root.kind === 'tool-result' ? root.callTime : root.time
|
|
3116
|
+
const completedAt = root.kind === 'tool-result' ? root.time : undefined
|
|
3117
|
+
toolCalls.push({ id: String(key), tool: call.name, state: call.state,
|
|
3118
|
+
query: currentQuery.slice(0, 240),
|
|
3119
|
+
...(Number.isFinite(startedAt) ? { startedAt } : {}),
|
|
3120
|
+
...(Number.isFinite(completedAt) ? { completedAt } : {}) })
|
|
3121
|
+
}
|
|
3122
|
+
// Corpus and web material both enter the archive; unrelated tools remain in the HUD.
|
|
3123
|
+
if (!PRTS_SCENE_TOOLS.includes(call.name) && !isRhineWebTool(call.name)) continue
|
|
3124
|
+
if (current) {
|
|
3125
|
+
const requestedVersion = args.data_version || args.dataVersion || locator.data_version || locator.dataVersion
|
|
3126
|
+
const startedAt = root.kind === 'tool-result' ? root.callTime : root.time
|
|
3127
|
+
const completedAt = root.kind === 'tool-result' ? root.time : undefined
|
|
3128
|
+
operations.push({ id: String(key), tool: call.name, kind: isRhineReadTool(call.name) ? 'read' : 'search',
|
|
3129
|
+
state: call.state, sourceIds: [], ...(currentQuery ? { query: currentQuery } : {}),
|
|
3130
|
+
...(call.name === 'web_fetch' && rhineWebUrl(args.url) ? { url: rhineWebUrl(args.url) } : {}),
|
|
3131
|
+
...(typeof locator.document_id === 'string' && locator.document_id ? { documentId: locator.document_id } : {}),
|
|
3132
|
+
...(typeof locator.document_uid === 'string' && locator.document_uid ? { documentUid: locator.document_uid } : {}),
|
|
3133
|
+
...(typeof locator.source_ref === 'string' && locator.source_ref ? { sourceRef: locator.source_ref } : {}),
|
|
3134
|
+
...(validDataVersion(requestedVersion) ? { dataVersion: requestedVersion } : {}),
|
|
3135
|
+
...(Number.isFinite(startedAt) ? { startedAt } : {}),
|
|
3136
|
+
...(Number.isFinite(completedAt) ? { completedAt } : {}) })
|
|
3137
|
+
}
|
|
3138
|
+
if (current && call.state === 'active' && !node.location?.turn?.end) {
|
|
3139
|
+
searching = true
|
|
3140
|
+
// PTC can run reads and searches together. Keep the reading operation
|
|
3141
|
+
// as the visual focus instead of letting a later search in tree order
|
|
3142
|
+
// hide its cassette animation or overwrite its document locator.
|
|
3143
|
+
if (!activeArgs || isRhineReadTool(call.name) || !isRhineReadTool(tool)) {
|
|
3144
|
+
query = currentQuery
|
|
3145
|
+
tool = call.name
|
|
3146
|
+
activeArgs = args
|
|
3147
|
+
}
|
|
3148
|
+
} else if (current && !searching) { query = currentQuery || query; tool = call.name }
|
|
3149
|
+
if (call.state === 'error') {
|
|
3150
|
+
if (current) toolErrors.set(call.name, meta?.error || text.slice(0, 600) || `${call.name} 执行失败`)
|
|
3151
|
+
continue
|
|
3152
|
+
}
|
|
3153
|
+
if (call.state !== 'complete') continue
|
|
3154
|
+
if (current) toolErrors.delete(call.name)
|
|
3155
|
+
const sourceStart = sources.length
|
|
3156
|
+
if (isRhineWebTool(call.name)) {
|
|
3157
|
+
const prior = call.name === 'web_fetch' ? retainArray('web-prior:' + key, sources.filter(source => source.origin === 'web')) : undefined
|
|
3158
|
+
sources.push(...memo('web-sources:' + key, [call.name, args, root.meta, text, prior],
|
|
3159
|
+
() => rhineWebSources(call.name, args, root.meta, text, String(key), prior || []), 'material'))
|
|
3160
|
+
}
|
|
3161
|
+
if (meta?.kind === 'prts-archive-sources-v1' && Array.isArray(meta.sources)) {
|
|
3162
|
+
sources.push(...memo('delivered:' + key, [meta.sources], () => meta.sources.slice(0, 128)
|
|
3163
|
+
.map(source => ({ ...source, callId: String(key) })), 'material'))
|
|
3164
|
+
}
|
|
3165
|
+
if (call.name === 'corpus_read') {
|
|
3166
|
+
const delivered = memo('read-sources:' + key, [meta, text], () => {
|
|
3167
|
+
const read = readPresentation(meta)
|
|
3168
|
+
const readSources = read ? read.sources.length ? read.sources : [read] : []
|
|
3169
|
+
const material = [], wiki = []
|
|
3170
|
+
for (const source of readSources) {
|
|
3171
|
+
const id = `document:${source.dataVersion}:${source.documentUid || source.documentId || source.sourceRef}`
|
|
3172
|
+
for (const citation of text.matchAll(/^引用:《([^》\n]+)》Wiki·([^\s\n]+)$/gmu)) {
|
|
3173
|
+
if (citation[1] === (source.title || read.title)) wiki.push([id, citation[2]])
|
|
3174
|
+
}
|
|
3175
|
+
material.push({ ...source, id,
|
|
3176
|
+
title: source.title || read.title || '原文资料', kind: 'corpus_read',
|
|
3177
|
+
origin: 'local', state: 'read', callId: String(key),
|
|
3178
|
+
sourceRef: source.sourceRef || (read.sources.length <= 1 ? read.sourceRef : ''),
|
|
3179
|
+
readRanges: source.lineStart && source.lineEnd ? [{ start: source.lineStart, end: source.lineEnd }] : [],
|
|
3180
|
+
excerpt: read.sources.length > 1
|
|
3181
|
+
? `Agent 已读取《${source.title}》第 ${source.lineStart}–${source.lineEnd} 行;完整返回内容见检索记录。`
|
|
3182
|
+
: text.slice(0, 1600) })
|
|
3183
|
+
}
|
|
3184
|
+
return { material, wiki }
|
|
3185
|
+
}, 'material')
|
|
3186
|
+
sources.push(...delivered.material)
|
|
3187
|
+
for (const [id, field] of delivered.wiki) {
|
|
3188
|
+
if (!wikiCitations.has(id)) wikiCitations.set(id, new Set())
|
|
3189
|
+
wikiCitations.get(id).add(field)
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
if (current) {
|
|
3193
|
+
const delivered = sources.slice(sourceStart)
|
|
3194
|
+
operationSources.set(String(key), delivered)
|
|
3195
|
+
if (currentTurn) turnSources.push(...delivered)
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
const sourceInput = retainArray('source-input', sources)
|
|
3199
|
+
const mergedMaterials = memo('merge-materials', [sourceInput], () => mergeArchiveSources(sourceInput), 'material')
|
|
3200
|
+
const citedInput = retain('cited-input', [...cited])
|
|
3201
|
+
const materials = memo('materials', [mergedMaterials, citedInput], () => mergedMaterials.map(source => cited.has(source.id)
|
|
3202
|
+
? { ...source, state: 'cited' } : source), 'material')
|
|
3203
|
+
error = error || [...toolErrors.values()].at(-1) || ''
|
|
3204
|
+
const end = currentTurn?.end
|
|
3205
|
+
const endKind = end?.type === 'turn/end' ? end.data?.reason?.kind : undefined
|
|
3206
|
+
if (interrupted || terminalError) { searching = false; assistantRunning = false }
|
|
3207
|
+
let outcome = terminalError ? 'error' : interrupted ? 'interrupted'
|
|
3208
|
+
: sessionRunning || searching || assistantRunning ? 'running' : 'unknown'
|
|
3209
|
+
if (endKind) {
|
|
3210
|
+
// A finished turn closes its tools too. A stale streamed node must not
|
|
3211
|
+
// keep animating a cancelled or completed investigation after its end.
|
|
3212
|
+
searching = false; assistantRunning = false
|
|
3213
|
+
if (endKind === 'completed') { outcome = 'completed'; error = '' }
|
|
3214
|
+
else if (endKind === 'aborted' || endKind === 'interrupted') { outcome = 'interrupted'; error = '调查已停止' }
|
|
3215
|
+
else if (endKind === 'error' || endKind === 'max-tokens' || endKind === 'blocked') {
|
|
3216
|
+
outcome = 'error'
|
|
3217
|
+
error = end.data.reason.error?.message || (endKind === 'max-tokens'
|
|
3218
|
+
? '回答达到输出上限,调查未完成' : endKind === 'blocked' ? '调查被阻止,尚未完成' : error || '调查未完成')
|
|
3219
|
+
} else { outcome = 'unknown' }
|
|
3220
|
+
}
|
|
3221
|
+
// Keep one operation per invocation instead of inferring activity from a
|
|
3222
|
+
// source's highest-ever state. A second read of the same ranges is still
|
|
3223
|
+
// a new operation, even though it makes no change to the material rack.
|
|
3224
|
+
const sourceAliases = (source, includeRef = true) => {
|
|
3225
|
+
const version = validDataVersion(source.dataVersion) ? source.dataVersion : ''
|
|
3226
|
+
return [['id', source.id], ['uid', source.documentUid], ['document', source.documentId],
|
|
3227
|
+
...(includeRef ? [['ref', source.sourceRef]] : [])]
|
|
3228
|
+
.flatMap(([kind, value]) => value ? [JSON.stringify([version, kind, value])] : [])
|
|
3229
|
+
}
|
|
3230
|
+
const materialIds = memo('material-aliases', [mergedMaterials], () => {
|
|
3231
|
+
const result = new Map()
|
|
3232
|
+
for (const source of mergedMaterials) {
|
|
3233
|
+
for (const alias of sourceAliases(source)) {
|
|
3234
|
+
if (!result.has(alias)) result.set(alias, new Set())
|
|
3235
|
+
result.get(alias).add(source.id)
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
return result
|
|
3239
|
+
}, 'material')
|
|
3240
|
+
const deliveredMaterialId = (source) => {
|
|
3241
|
+
// A shared source_ref can cover several documents. Prefer the first
|
|
3242
|
+
// matching exact identity, never union a weaker alias into a receipt.
|
|
3243
|
+
const precise = Boolean(source.documentUid || source.documentId)
|
|
3244
|
+
for (const alias of sourceAliases(source, !precise)) {
|
|
3245
|
+
const matches = materialIds.get(alias)
|
|
3246
|
+
if (matches?.size) return matches.size === 1 ? [...matches] : []
|
|
3247
|
+
}
|
|
3248
|
+
return []
|
|
3249
|
+
}
|
|
3250
|
+
for (const call of toolCalls) {
|
|
3251
|
+
if (call.state === 'active' && (endKind || interrupted || terminalError)) {
|
|
3252
|
+
call.state = 'error'
|
|
3253
|
+
const index = records.findIndex(record => record.id === call.id)
|
|
3254
|
+
if (index >= 0) records[index] = { ...records[index], state: 'error' }
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
for (const operation of operations) {
|
|
3258
|
+
if (operation.state === 'active' && (endKind || interrupted || terminalError)) operation.state = 'error'
|
|
3259
|
+
const delivered = (operationSources.get(operation.id) || []).filter(source => operation.kind !== 'read'
|
|
3260
|
+
|| source.agentRead === true || source.state === 'read' || source.readRanges?.length > 0)
|
|
3261
|
+
operation.sourceIds = [...new Set(delivered.flatMap(deliveredMaterialId))]
|
|
3262
|
+
}
|
|
3263
|
+
// Report citations are recomputed from this answer only. Historical
|
|
3264
|
+
// citations still mark the cumulative rack, but never enter a new report.
|
|
3265
|
+
const reportIds = citations('report', materials, answer)
|
|
3266
|
+
const reportInput = retainArray('report-ids', [...reportIds])
|
|
3267
|
+
const reportSources = memo('report-sources', [materials, reportInput], () => materials.filter(source => reportIds.has(source.id))
|
|
3268
|
+
.map(source => ({ ...source, state: 'cited' })), 'material')
|
|
3269
|
+
const turnInput = retainArray('turn-input', currentTurn ? turnSources : sources.slice(turnSourceStart))
|
|
3270
|
+
const mergedTurn = memo('merge-turn', [turnInput], () => mergeArchiveSources(turnInput), 'material')
|
|
3271
|
+
const currentSources = memo('current-sources', [mergedTurn, reportInput], () => mergedTurn
|
|
3272
|
+
.map(source => reportIds.has(source.id) ? { ...source, state: 'cited' } : source), 'material')
|
|
3273
|
+
const activeLocator = activeArgs?.locator || activeArgs || {}
|
|
3274
|
+
let activeDocumentId = typeof activeLocator.document_id === 'string' ? activeLocator.document_id : ''
|
|
3275
|
+
const activeSourceRef = typeof activeLocator.source_ref === 'string' ? activeLocator.source_ref : ''
|
|
3276
|
+
const requestedVersion = activeArgs?.data_version || activeArgs?.dataVersion
|
|
3277
|
+
|| activeLocator.data_version || activeLocator.dataVersion
|
|
3278
|
+
const activeDataVersion = validDataVersion(requestedVersion) ? requestedVersion : ''
|
|
3279
|
+
if (!activeDocumentId && isRhineReadTool(tool)) {
|
|
3280
|
+
const hits = materials.filter((source) => activeLocator.document_uid
|
|
3281
|
+
? source.documentUid === activeLocator.document_uid
|
|
3282
|
+
: activeSourceRef ? source.sourceRef === activeSourceRef
|
|
3283
|
+
: (activeLocator.title || activeLocator.display_title)
|
|
3284
|
+
&& source.title === (activeLocator.title || activeLocator.display_title))
|
|
3285
|
+
.filter((source) => !activeDataVersion || source.dataVersion === activeDataVersion)
|
|
3286
|
+
if (hits.length === 1) activeDocumentId = hits[0].documentId || ''
|
|
3287
|
+
}
|
|
3288
|
+
const snapshot = { sessionId, running: !endKind && !interrupted && !terminalError && Boolean(sessionRunning || searching || assistantRunning || toolCalls.some(call => call.state === 'active')), searching, question, query, tool,
|
|
3289
|
+
investigationId, outcome, turnSources: currentSources, reportSources,
|
|
3290
|
+
...(outcome === 'completed' && Number.isFinite(end?.time) ? { completedAt: end.time } : {}),
|
|
3291
|
+
sources: materials, answer, records, operations, toolCalls, ...(error ? { error } : {}),
|
|
3292
|
+
...(searching && activeDocumentId ? { activeDocumentId } : {}),
|
|
3293
|
+
...(searching && activeSourceRef ? { activeSourceRef } : {}),
|
|
3294
|
+
...(searching && activeDataVersion ? { activeDataVersion } : {}) }
|
|
3295
|
+
return retain('output', { ...snapshot, phase: rhinePhase(snapshot) })
|
|
3296
|
+
}
|
|
3297
|
+
// Each mounted controller owns bounded counters; no snapshot data is exported.
|
|
3298
|
+
function createRhineSnapshotDiagnostics() {
|
|
3299
|
+
let monitor
|
|
3300
|
+
const counts = Object.fromEntries(['host-snapshot-selector', 'host-snapshot-build',
|
|
3301
|
+
'host-snapshot-serialize', 'host-snapshot-parse', 'host-snapshot-merge',
|
|
3302
|
+
'host-snapshot-derive', 'host-snapshot-input'].map(kind => [kind, 0]))
|
|
3303
|
+
const cache = Object.fromEntries(['input', 'tool', 'material', 'citation', 'serialize', 'sourceMerge']
|
|
3304
|
+
.flatMap(kind => [[kind + 'Hits', 0], [kind + 'Misses', 0]]))
|
|
3305
|
+
return {
|
|
3306
|
+
cacheEvent(kind, hit) {
|
|
3307
|
+
const key = kind + (hit ? 'Hits' : 'Misses')
|
|
3308
|
+
if (Object.hasOwn(cache, key)) cache[key]++
|
|
3309
|
+
},
|
|
3310
|
+
measure(kind, task) {
|
|
3311
|
+
if (!Object.prototype.hasOwnProperty.call(counts, kind)) return task()
|
|
3312
|
+
counts[kind]++
|
|
3313
|
+
const done = monitor?.(kind)
|
|
3314
|
+
try { return task() } finally { done?.() }
|
|
3315
|
+
},
|
|
3316
|
+
setPerformanceMonitor(value) { monitor = value },
|
|
3317
|
+
performanceStats() { return { counts: { ...counts }, cache: { ...cache } } },
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
const rhineSnapshotSignature = (snapshot, diagnostics) => {
|
|
3321
|
+
const measure = (kind, task) => diagnostics ? diagnostics.measure(kind, task) : task()
|
|
3322
|
+
return measure('host-snapshot-selector', () => {
|
|
3323
|
+
const value = measure('host-snapshot-build', () => buildRhineSnapshot(snapshot?.order, snapshot?.nodes, '', false, snapshot?.timeline))
|
|
3324
|
+
return measure('host-snapshot-serialize', () => JSON.stringify(value))
|
|
3325
|
+
})
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
// Preserve the host's string selector contract, but serialize only changed
|
|
3329
|
+
// fields and let React consume the already-built detached value. A stale
|
|
3330
|
+
// concurrent render can still parse its own string, never a newer snapshot.
|
|
3331
|
+
function createRhineSnapshotSelector(diagnostics) {
|
|
3332
|
+
const measure = (kind, task) => diagnostics ? diagnostics.measure(kind, task) : task()
|
|
3333
|
+
const cache = createRhineProjectionCache(diagnostics)
|
|
3334
|
+
const fields = new Map()
|
|
3335
|
+
let input, value, serialized
|
|
3336
|
+
const select = snapshot => measure('host-snapshot-selector', () => {
|
|
3337
|
+
const nextInput = measure('host-snapshot-input', () => retainRhineValue(rhineSnapshotInput(snapshot), input))
|
|
3338
|
+
if (nextInput === input) {
|
|
3339
|
+
diagnostics?.cacheEvent('input', true)
|
|
3340
|
+
return serialized
|
|
3341
|
+
}
|
|
3342
|
+
diagnostics?.cacheEvent('input', false)
|
|
3343
|
+
cache.begin()
|
|
3344
|
+
cache.entries = nextInput.entries
|
|
3345
|
+
let next
|
|
3346
|
+
try {
|
|
3347
|
+
const timeline = { turnOrder: [nextInput.turnNumber], turns: new Map([[nextInput.turnNumber, nextInput.turn]]) }
|
|
3348
|
+
next = measure('host-snapshot-build', () => buildRhineSnapshot([], null, '', false, timeline, cache))
|
|
3349
|
+
} finally { cache.end(); cache.entries = undefined }
|
|
3350
|
+
const signature = measure('host-snapshot-serialize', () => {
|
|
3351
|
+
const parts = [], keys = new Set(Object.keys(next))
|
|
3352
|
+
for (const key of fields.keys()) if (!keys.has(key)) fields.delete(key)
|
|
3353
|
+
for (const [key, item] of Object.entries(next)) {
|
|
3354
|
+
let field = fields.get(key)
|
|
3355
|
+
const hit = Boolean(field && Object.is(field.value, item))
|
|
3356
|
+
diagnostics?.cacheEvent('serialize', hit)
|
|
3357
|
+
if (!hit) { field = { value: item, text: JSON.stringify(item) }; fields.set(key, field) }
|
|
3358
|
+
if (field.text !== undefined) parts.push(JSON.stringify(key) + ':' + field.text)
|
|
3359
|
+
}
|
|
3360
|
+
return '{' + parts.join(',') + '}'
|
|
3361
|
+
})
|
|
3362
|
+
input = nextInput; value = next; serialized = signature
|
|
3363
|
+
return serialized
|
|
3364
|
+
})
|
|
3365
|
+
select.read = signature => signature === serialized && value ? value
|
|
3366
|
+
: measure('host-snapshot-parse', () => signature ? JSON.parse(signature) : buildRhineSnapshot([], null))
|
|
3367
|
+
return select
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
// Both resources are staged together and retired on HMR. Failed loads are retryable.
|
|
3371
|
+
const loadRhineBundle = () => {
|
|
3372
|
+
if (rhineBundlePromise) return rhineBundlePromise
|
|
3373
|
+
const owner = { elements: [], cancel: null }
|
|
3374
|
+
rhineBundleOwner = owner
|
|
3375
|
+
const load = (kind, path) => new Promise((resolve, reject) => {
|
|
3376
|
+
const element = document.createElement(kind === 'script' ? 'script' : 'link')
|
|
3377
|
+
if (kind === 'script') { element.src = RHINE_BASE + path; element.async = true }
|
|
3378
|
+
else { element.rel = 'stylesheet'; element.href = RHINE_BASE + path }
|
|
3379
|
+
element.dataset.plugin = 'prts-terrarchive'
|
|
3380
|
+
element.dataset.rhineResource = path
|
|
3381
|
+
owner.elements.push(element)
|
|
3382
|
+
const abort = () => finish(makeAbortError('资料馆资源加载已取消'))
|
|
3383
|
+
const timer = setTimeout(() => finish(new Error('资料馆资源加载超时,请重试')), 15000)
|
|
3384
|
+
const signal = clientAbortController?.signal
|
|
3385
|
+
const finish = (error) => {
|
|
3386
|
+
clearTimeout(timer)
|
|
3387
|
+
element.onload = null; element.onerror = null
|
|
3388
|
+
signal?.removeEventListener('abort', abort)
|
|
3389
|
+
if (error) { element.remove(); reject(error) } else resolve()
|
|
3390
|
+
}
|
|
3391
|
+
signal?.addEventListener('abort', abort, { once: true })
|
|
3392
|
+
element.onload = () => finish()
|
|
3393
|
+
element.onerror = () => finish(new Error(`资料馆资源加载失败:${path}`))
|
|
3394
|
+
if (signal?.aborted) abort()
|
|
3395
|
+
else document.head.appendChild(element)
|
|
3396
|
+
})
|
|
3397
|
+
rhineBundlePromise = Promise.all([load('style', 'rhine.css'), load('script', 'rhine.js')])
|
|
3398
|
+
.then(() => {
|
|
3399
|
+
const runtime = globalThis.__PRTS_RHINE__
|
|
3400
|
+
if (rhineBundleOwner !== owner) throw makeAbortError('资料馆已被新实例替换')
|
|
3401
|
+
if (runtime?.RUNTIME_ABI !== 1 || typeof runtime.mountRhineWorkbench !== 'function') {
|
|
3402
|
+
throw new Error('资料馆资源版本不兼容,请刷新页面')
|
|
3403
|
+
}
|
|
3404
|
+
return runtime
|
|
3405
|
+
}).catch((error) => {
|
|
3406
|
+
for (const element of owner.elements) element.remove()
|
|
3407
|
+
if (rhineBundleOwner === owner) { rhineBundleOwner = null; rhineBundlePromise = null }
|
|
3408
|
+
throw error
|
|
3409
|
+
})
|
|
3410
|
+
return rhineBundlePromise
|
|
3411
|
+
}
|
|
3412
|
+
const disposeRhine = () => {
|
|
3413
|
+
for (const cleanup of [...rhineCleanups]) cleanup()
|
|
3414
|
+
for (const element of rhineBundleOwner?.elements || []) element.remove()
|
|
3415
|
+
rhineBundleOwner = null
|
|
3416
|
+
rhineBundlePromise = null
|
|
3417
|
+
}
|
|
3418
|
+
async function submitRhineQuestion(ctx, sessionId, text, signal) {
|
|
3419
|
+
if (!text?.trim()) return
|
|
3420
|
+
const session = ctx?.sessions?.binding?.(sessionId)?.session
|
|
3421
|
+
if (!session?.prompt || !session?.beginSubmission) throw new Error('当前会话尚未连接,请返回对话后重试')
|
|
3422
|
+
const submission = session.beginSubmission({ mode: 'queue', text, attachments: [] })
|
|
3423
|
+
try {
|
|
3424
|
+
const result = await session.prompt([{ type: 'text', text }], 'queue', signal, submission.requestId)
|
|
3425
|
+
if (!result?.ok) throw new Error(result?.error?.message || 'Agent 未接受问题,请重试')
|
|
3426
|
+
} catch (error) { submission.abandon(); throw error }
|
|
3427
|
+
}
|
|
3428
|
+
// The workbench uses the same public session and model services as DSH's
|
|
3429
|
+
// sidebar/composer. Each controller is owned by one open archive session.
|
|
3430
|
+
function createRhineHostControls(ctx, sessionId) {
|
|
3431
|
+
const sessions = ctx.sessions
|
|
3432
|
+
const session = sessions.binding(sessionId)?.session
|
|
3433
|
+
const directory = ctx.modelDirectories?.directoryFor(sessionId)
|
|
3434
|
+
const listeners = new Set()
|
|
3435
|
+
let disposed = false
|
|
3436
|
+
let busy = false
|
|
3437
|
+
let loading = false
|
|
3438
|
+
let error = ''
|
|
3439
|
+
let modes = []
|
|
3440
|
+
let refreshTask = null
|
|
3441
|
+
let pendingCreated = null
|
|
3442
|
+
const cleanups = []
|
|
3443
|
+
let monitor
|
|
3444
|
+
let frame = null, timer = null
|
|
3445
|
+
const metrics = { notifications: 0, coalesced: 0, flushes: 0, stateReads: 0, emitted: 0, deduplicated: 0,
|
|
3446
|
+
sources: { sessions: 0, workspaces: 0, session: 0, models: 0 } }
|
|
3447
|
+
const cancelPending = () => {
|
|
3448
|
+
if (frame !== null) window.cancelAnimationFrame(frame)
|
|
3449
|
+
if (timer !== null) clearTimeout(timer)
|
|
3450
|
+
frame = timer = null
|
|
3451
|
+
}
|
|
3452
|
+
const modelKey = (selection) => selection ? JSON.stringify([selection.provider, selection.model]) : ''
|
|
3453
|
+
const workspaceState = () => ctx.workspaces?.list.getSnapshot() || { items: [], archivedSessionIds: [] }
|
|
3454
|
+
const workspaceOf = (id) => workspaceState().items.find(item => item.sessionIds?.includes(id))
|
|
3455
|
+
const summaryOf = () => sessions.list.getSnapshot().byId[sessionId]
|
|
3456
|
+
const modelOptions = () => (directory?.store.getSnapshot().groups || []).flatMap(group =>
|
|
3457
|
+
(group.models || []).map(model => ({
|
|
3458
|
+
id: modelKey({ provider: group.id, model: model.id }),
|
|
3459
|
+
name: `${model.name || model.id} · ${group.name || group.id}`,
|
|
3460
|
+
provider: group.id, model: model.id, reasoning: model.reasoning,
|
|
3461
|
+
})))
|
|
3462
|
+
const projectState = () => {
|
|
3463
|
+
const list = sessions.list.getSnapshot()
|
|
3464
|
+
const current = list.byId[sessionId]
|
|
3465
|
+
const lifecycle = session?.getSnapshot?.()
|
|
3466
|
+
const workspaces = workspaceState()
|
|
3467
|
+
const archived = new Set(workspaces.archivedSessionIds || [])
|
|
3468
|
+
const modelState = directory?.store.getSnapshot()
|
|
3469
|
+
const currentModel = modelState?.current
|
|
3470
|
+
const choices = modelOptions().map(({ id, name }) => ({ id, name }))
|
|
3471
|
+
// Catalog membership is advisory; an existing routable model may no
|
|
3472
|
+
// longer be advertised. Preserve its actual selection in the picker.
|
|
3473
|
+
if (currentModel && !choices.some(item => item.id === modelKey(currentModel))) {
|
|
3474
|
+
choices.unshift({ id: modelKey(currentModel), name: `${currentModel.model} · ${currentModel.provider}` })
|
|
3475
|
+
}
|
|
3476
|
+
const workspace = workspaceOf(sessionId)
|
|
3477
|
+
return {
|
|
3478
|
+
sessionId, title: current?.title || (current?.blank ? '新建对话' : current?.displayTitle) || '当前对话',
|
|
3479
|
+
workspace: workspace?.title || workspace?.path || current?.cwd || '',
|
|
3480
|
+
blank: Boolean(lifecycle?.blank ?? current?.blank),
|
|
3481
|
+
running: Boolean(lifecycle?.running ?? current?.running),
|
|
3482
|
+
busy: busy || modelState?.status === 'selecting', loading,
|
|
3483
|
+
canSubmit: modelState?.routable !== false && !lifecycle?.removed,
|
|
3484
|
+
error: error || (modelState?.routable === false ? '当前模型没有可用连接,请选择其他模型。' : modelState?.error || ''),
|
|
3485
|
+
mode: typeof current?.projectionValues?.agentPreset === 'string' ? current.projectionValues.agentPreset : '',
|
|
3486
|
+
modes, model: modelKey(currentModel), models: choices,
|
|
3487
|
+
sessions: list.ids.map(id => list.byId[id]).filter(item => item && !archived.has(item.id) && item.origin !== 'subagent')
|
|
3488
|
+
.map(item => {
|
|
3489
|
+
const owner = workspaces.items.find(workspace => workspace.sessionIds?.includes(item.id))
|
|
3490
|
+
return { id: item.id, title: item.title || (item.blank ? '新建对话' : item.displayTitle) || '未命名对话',
|
|
3491
|
+
workspace: owner?.title || owner?.path || item.cwd || '',
|
|
3492
|
+
running: Boolean(item.running), updatedAt: item.updatedAt || 0 }
|
|
3493
|
+
}),
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
const getState = () => {
|
|
3497
|
+
metrics.stateReads++
|
|
3498
|
+
const done = monitor?.('host-state-project')
|
|
3499
|
+
try { return projectState() } finally { done?.() }
|
|
3500
|
+
}
|
|
3501
|
+
// The history UI displays a date, not per-token timestamp changes. Preserve
|
|
3502
|
+
// ordering, names and all actionable fields; never compare only references.
|
|
3503
|
+
const displaySignature = state => JSON.stringify({ ...state, sessions: state.sessions.map(item => ({
|
|
3504
|
+
...item, updatedAt: Number.isFinite(item.updatedAt) && item.updatedAt > 0 ? new Date(item.updatedAt).toLocaleDateString('zh-CN') : '',
|
|
3505
|
+
})) })
|
|
3506
|
+
let publishedSignature = displaySignature(getState())
|
|
3507
|
+
const emit = () => {
|
|
3508
|
+
cancelPending()
|
|
3509
|
+
if (disposed) return
|
|
3510
|
+
metrics.flushes++
|
|
3511
|
+
const done = monitor?.('host-notify')
|
|
3512
|
+
try {
|
|
3513
|
+
const state = getState(), signature = displaySignature(state)
|
|
3514
|
+
if (signature === publishedSignature) { metrics.deduplicated++; return }
|
|
3515
|
+
publishedSignature = signature; metrics.emitted++
|
|
3516
|
+
for (const listener of listeners) listener(state)
|
|
3517
|
+
} finally { done?.() }
|
|
3518
|
+
}
|
|
3519
|
+
const requestEmit = source => {
|
|
3520
|
+
if (disposed) return
|
|
3521
|
+
metrics.notifications++; metrics.sources[source]++
|
|
3522
|
+
if (!listeners.size) return
|
|
3523
|
+
if (frame !== null || timer !== null) { metrics.coalesced++; return }
|
|
3524
|
+
if (typeof window.requestAnimationFrame === 'function') frame = window.requestAnimationFrame(emit)
|
|
3525
|
+
// RAF may be suspended in a background tab; current state still arrives.
|
|
3526
|
+
timer = setTimeout(emit, frame === null ? 16 : 100)
|
|
3527
|
+
}
|
|
3528
|
+
const assertActive = () => {
|
|
3529
|
+
if (disposed || clientContext !== ctx || sessions.list.getSnapshot().current !== sessionId) {
|
|
3530
|
+
throw makeAbortError('会话已切换,请在当前会话重试')
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
const unwrap = (result, label) => {
|
|
3534
|
+
if (!result?.ok) throw new Error(result?.error?.message || `${label}失败`)
|
|
3535
|
+
return result.value
|
|
3536
|
+
}
|
|
3537
|
+
const run = async (action) => {
|
|
3538
|
+
assertActive()
|
|
3539
|
+
if (busy || directory?.store.getSnapshot().status === 'selecting') throw new Error('上一项设置正在保存,请稍候')
|
|
3540
|
+
busy = true; error = ''; emit()
|
|
3541
|
+
try { await action() }
|
|
3542
|
+
catch (caught) {
|
|
3543
|
+
if (!disposed && caught?.name !== 'AbortError') error = caught?.message || '操作失败,请重试'
|
|
3544
|
+
throw caught
|
|
3545
|
+
} finally { busy = false; emit() }
|
|
3546
|
+
}
|
|
3547
|
+
const refresh = () => {
|
|
3548
|
+
if (disposed) return Promise.resolve()
|
|
3549
|
+
if (refreshTask) return refreshTask
|
|
3550
|
+
loading = true; error = ''; emit()
|
|
3551
|
+
refreshTask = (async () => {
|
|
3552
|
+
const results = await Promise.allSettled([
|
|
3553
|
+
waitWithDeadline(ctx.remote.agentPresets.list(), { timeout: 12000, label: '模式列表' }),
|
|
3554
|
+
waitWithDeadline(sessions.refresh(), { timeout: 12000, label: '会话列表' }),
|
|
3555
|
+
directory ? waitWithDeadline(directory.load(), { timeout: 12000, label: '模型列表' }) : Promise.resolve(),
|
|
3556
|
+
])
|
|
3557
|
+
if (disposed) return
|
|
3558
|
+
const roster = results[0]
|
|
3559
|
+
if (roster.status === 'fulfilled') {
|
|
3560
|
+
try {
|
|
3561
|
+
const value = unwrap(roster.value, '读取模式')
|
|
3562
|
+
modes = (value.presets || []).filter(item => !item.broken)
|
|
3563
|
+
.map(item => ({ id: item.id, name: item.name || item.id, description: item.description || '' }))
|
|
3564
|
+
} catch (caught) { error = caught.message }
|
|
3565
|
+
}
|
|
3566
|
+
const failures = results.filter(result => result.status === 'rejected')
|
|
3567
|
+
.map(result => result.reason?.message || '读取会话设置失败')
|
|
3568
|
+
if (failures.length) error = [error, ...failures].filter(Boolean).join(';')
|
|
3569
|
+
})().finally(() => { loading = false; refreshTask = null; emit() })
|
|
3570
|
+
return refreshTask
|
|
3571
|
+
}
|
|
3572
|
+
const requireMode = (id) => {
|
|
3573
|
+
if (!id || !modes.some(mode => mode.id === id)) throw new Error('该模式暂不可用,请刷新模式列表')
|
|
3574
|
+
}
|
|
3575
|
+
const createSession = (requestedMode) => run(async () => {
|
|
3576
|
+
const original = summaryOf()
|
|
3577
|
+
const mode = requestedMode || original?.projectionValues?.agentPreset
|
|
3578
|
+
if (requestedMode) requireMode(requestedMode)
|
|
3579
|
+
// Inherit the effective selection without making a new conversation
|
|
3580
|
+
// depend on the advisory model catalog being reachable. With no explicit
|
|
3581
|
+
// selection, the new session uses the same Host default.
|
|
3582
|
+
const model = directory?.store.getSnapshot().current || original?.projectionValues?.modelSelection?.next
|
|
3583
|
+
const workspace = workspaceOf(sessionId)
|
|
3584
|
+
const target = workspace ? { workspaceId: workspace.workspaceId }
|
|
3585
|
+
: original?.cwd ? { cwd: original.cwd } : {}
|
|
3586
|
+
const selection = model ? { provider: model.provider, model: model.model,
|
|
3587
|
+
...(model.reasoningEffort === undefined ? {} : { reasoningEffort: model.reasoningEffort }) } : null
|
|
3588
|
+
const targetKey = JSON.stringify([target, mode || null, selection])
|
|
3589
|
+
if (pendingCreated) {
|
|
3590
|
+
const pendingSummary = sessions.list.getSnapshot().byId[pendingCreated.id]
|
|
3591
|
+
const pendingState = pendingSummary ? sessions.binding(pendingCreated.id)?.session.getSnapshot() : null
|
|
3592
|
+
if (pendingCreated.targetKey !== targetKey || !pendingSummary || !pendingSummary.blank
|
|
3593
|
+
|| pendingSummary.running || pendingState?.blank === false || pendingState?.running
|
|
3594
|
+
|| pendingState?.removed || workspaceState().archivedSessionIds.includes(pendingCreated.id)) {
|
|
3595
|
+
// This entry may now be somebody's active conversation. Forget the
|
|
3596
|
+
// retry handle without deleting or rewriting any existing session.
|
|
3597
|
+
pendingCreated = null
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
if (!pendingCreated) {
|
|
3601
|
+
const id = await sessions.create(target)
|
|
3602
|
+
pendingCreated = { id, targetKey, mode, model: selection }
|
|
3603
|
+
}
|
|
3604
|
+
const pending = pendingCreated
|
|
3605
|
+
const id = pending.id
|
|
3606
|
+
try {
|
|
3607
|
+
assertActive()
|
|
3608
|
+
if (pending.mode) unwrap(await ctx.remote.agentPresets.select(id, pending.mode), '设置新对话模式')
|
|
3609
|
+
assertActive()
|
|
3610
|
+
if (pending.model) await ctx.modelDirectories.directoryFor(id).select({ ...pending.model })
|
|
3611
|
+
assertActive()
|
|
3612
|
+
sessions.open(id)
|
|
3613
|
+
if (pendingCreated === pending) pendingCreated = null
|
|
3614
|
+
} catch (caught) {
|
|
3615
|
+
// Keep the old conversation selected until the new one is fully
|
|
3616
|
+
// configured. Retrying the same selection reuses this blank entry.
|
|
3617
|
+
if (caught?.name !== 'AbortError') throw new Error(`新对话已创建,但尚未打开:${caught?.message || '配置失败'}。再次执行相同操作将重试此对话。`)
|
|
3618
|
+
throw caught
|
|
3619
|
+
}
|
|
3620
|
+
})
|
|
3621
|
+
for (const [source, store] of [['sessions', sessions.list], ['workspaces', ctx.workspaces?.list], ['session', session], ['models', directory?.store]]) {
|
|
3622
|
+
if (store?.subscribe) cleanups.push(store.subscribe(() => requestEmit(source)))
|
|
3623
|
+
}
|
|
3624
|
+
const controls = {
|
|
3625
|
+
getState, refresh, createSession,
|
|
3626
|
+
setPerformanceMonitor(value) { monitor = value },
|
|
3627
|
+
performanceStats() { return { ...metrics, sources: { ...metrics.sources }, pending: frame !== null || timer !== null } },
|
|
3628
|
+
subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener) },
|
|
3629
|
+
openSession: (id) => run(async () => {
|
|
3630
|
+
if (!getState().sessions.some(item => item.id === id)) throw new Error('该对话已不可用,请刷新会话列表')
|
|
3631
|
+
sessions.open(id)
|
|
3632
|
+
}),
|
|
3633
|
+
selectMode: (id) => run(async () => {
|
|
3634
|
+
requireMode(id)
|
|
3635
|
+
const current = getState()
|
|
3636
|
+
if (!current.blank || current.running) throw new Error('当前对话已经开始,请使用所选模式新建对话')
|
|
3637
|
+
unwrap(await ctx.remote.agentPresets.select(sessionId, id), '切换模式')
|
|
3638
|
+
assertActive()
|
|
3639
|
+
await sessions.refresh()
|
|
3640
|
+
}),
|
|
3641
|
+
selectModel: (id) => run(async () => {
|
|
3642
|
+
if (!directory) throw new Error('模型目录尚未就绪')
|
|
3643
|
+
const choice = modelOptions().find(item => item.id === id)
|
|
3644
|
+
if (!choice) throw new Error('该模型暂不可用,请刷新模型列表')
|
|
3645
|
+
const current = directory.store.getSnapshot().current
|
|
3646
|
+
const effort = current?.reasoningEffort
|
|
3647
|
+
const reasoningEffort = choice.reasoning?.efforts?.some(item => item.id === effort) ? effort : choice.reasoning?.defaultEffort
|
|
3648
|
+
await directory.select({ provider: choice.provider, model: choice.model,
|
|
3649
|
+
...(reasoningEffort ? { reasoningEffort } : {}) })
|
|
3650
|
+
}),
|
|
3651
|
+
cancel: () => run(async () => {
|
|
3652
|
+
if (!session?.cancel) throw new Error('当前会话尚未连接')
|
|
3653
|
+
unwrap(await session.cancel(), '停止当前任务')
|
|
3654
|
+
}),
|
|
3655
|
+
dispose() { disposed = true; cancelPending(); monitor = undefined; pendingCreated = null; for (const cleanup of cleanups) cleanup(); listeners.clear() },
|
|
3656
|
+
}
|
|
3657
|
+
return controls
|
|
3658
|
+
}
|
|
3659
|
+
function RhineControl({ useChat, useSession, sessionId }) {
|
|
3660
|
+
const [skin, setSkinValue] = useState(activeSkin)
|
|
3661
|
+
const [open, setOpen] = useState(activeSkin === 'rhine-lab')
|
|
3662
|
+
const trigger = useRef(null)
|
|
3663
|
+
const workbench = useRef(null)
|
|
3664
|
+
const previousSources = useRef({ sessionId, sources: [] })
|
|
3665
|
+
const snapshotDiagnostics = useMemo(() => createRhineSnapshotDiagnostics(), [sessionId])
|
|
3666
|
+
const snapshotSelector = useMemo(() => createRhineSnapshotSelector(snapshotDiagnostics), [snapshotDiagnostics])
|
|
3667
|
+
const serialized = useChat ? useChat(snapshotSelector) : ''
|
|
3668
|
+
const sessionState = useSession ? useSession((snapshot) => JSON.stringify({
|
|
3669
|
+
running: Boolean(snapshot.running), hasMoreHistory: Boolean(snapshot.hasMore),
|
|
3670
|
+
loadingHistory: Boolean(snapshot.loadingOlder), error: snapshot.lastAgentError
|
|
3671
|
+
|| (snapshot.promptError?.op === 'send' ? snapshot.promptError.error?.message : '') || '',
|
|
3672
|
+
awaitingFirstTurn: Boolean(snapshot.awaitingFirstTurn || snapshot.pendingSubmissions?.some(item => item.placement === 'transcript')),
|
|
3673
|
+
})) : '{}'
|
|
3674
|
+
const snapshot = useMemo(() => snapshotDiagnostics.measure('host-snapshot-derive', () => {
|
|
3675
|
+
const next = snapshotSelector.read(serialized)
|
|
3676
|
+
const previous = previousSources.current
|
|
3677
|
+
const reuseSources = previous.sessionId === sessionId && previous.input === next.sources
|
|
3678
|
+
snapshotDiagnostics.cacheEvent('sourceMerge', reuseSources)
|
|
3679
|
+
const sources = reuseSources ? previous.sources : snapshotDiagnostics.measure('host-snapshot-merge',
|
|
3680
|
+
() => mergeArchiveSources([...(previous.sessionId === sessionId ? previous.sources : []), ...next.sources]))
|
|
3681
|
+
previousSources.current = { sessionId, sources, input: next.sources }
|
|
3682
|
+
const lifecycle = JSON.parse(sessionState)
|
|
3683
|
+
// Chat's durable turn/end outranks the coarser live running bit, whose
|
|
3684
|
+
// control frame can arrive later. A newly submitted first turn still
|
|
3685
|
+
// takes precedence while its durable user/turn nodes are on the way.
|
|
3686
|
+
const ended = ['completed', 'interrupted', 'error'].includes(next.outcome) && !next.running
|
|
3687
|
+
const running = Boolean(lifecycle.awaitingFirstTurn || !ended && (lifecycle.running || next.running))
|
|
3688
|
+
const error = ended ? next.error : lifecycle.error || next.error
|
|
3689
|
+
const outcome = running ? 'running' : ended ? next.outcome : lifecycle.error ? 'error' : next.outcome || 'unknown'
|
|
3690
|
+
const combined = { ...next, ...lifecycle, error,
|
|
3691
|
+
sessionId: sessionId || '', sources, running, outcome }
|
|
3692
|
+
return { ...combined, phase: rhinePhase(combined) }
|
|
3693
|
+
}), [serialized, sessionId, sessionState, snapshotDiagnostics, snapshotSelector])
|
|
3694
|
+
const latestSnapshot = useRef(snapshot)
|
|
3695
|
+
latestSnapshot.current = snapshot
|
|
3696
|
+
useEffect(() => {
|
|
3697
|
+
const listener = (value) => {
|
|
3698
|
+
setSkinValue(value)
|
|
3699
|
+
setOpen(value === 'rhine-lab')
|
|
3700
|
+
}
|
|
3701
|
+
rhineSkinListeners.add(listener)
|
|
3702
|
+
return () => rhineSkinListeners.delete(listener)
|
|
3703
|
+
}, [])
|
|
3704
|
+
useEffect(() => { workbench.current?.update(snapshot) }, [snapshot])
|
|
3705
|
+
useEffect(() => {
|
|
3706
|
+
if (!open || skin !== 'rhine-lab' || !sessionId) return undefined
|
|
3707
|
+
const ctx = clientContext
|
|
3708
|
+
const api = callApi
|
|
3709
|
+
const signal = clientAbortController?.signal
|
|
3710
|
+
const overlay = document.createElement('div')
|
|
3711
|
+
overlay.className = 'prts-rhine-overlay'
|
|
3712
|
+
overlay.setAttribute('role', 'dialog')
|
|
3713
|
+
overlay.setAttribute('aria-modal', 'true')
|
|
3714
|
+
overlay.setAttribute('aria-label', '莱茵生命资料馆')
|
|
3715
|
+
overlay.tabIndex = -1
|
|
3716
|
+
const status = document.createElement('div')
|
|
3717
|
+
status.className = 'prts-rhine-loading'
|
|
3718
|
+
const message = document.createElement('p')
|
|
3719
|
+
message.textContent = 'RHINE LAB / 正在打开资料馆'
|
|
3720
|
+
const close = document.createElement('button')
|
|
3721
|
+
close.textContent = '返回对话'
|
|
3722
|
+
close.onclick = () => setOpen(false)
|
|
3723
|
+
status.append(message, close); overlay.append(status)
|
|
3724
|
+
document.body.append(overlay)
|
|
3725
|
+
let disposed = false
|
|
3726
|
+
let instance = null
|
|
3727
|
+
let hostControls = null
|
|
3728
|
+
let removeFocusTrap = installDialogFocusTrap(overlay, {
|
|
3729
|
+
returnFocus: trigger, isSuspended: isHostSettingsOpen,
|
|
3730
|
+
})
|
|
3731
|
+
const visible = () => instance?.setActive(!document.hidden)
|
|
3732
|
+
document.addEventListener('visibilitychange', visible)
|
|
3733
|
+
const cleanup = () => {
|
|
3734
|
+
if (disposed) return
|
|
3735
|
+
disposed = true
|
|
3736
|
+
document.removeEventListener('visibilitychange', visible)
|
|
3737
|
+
removeFocusTrap()
|
|
3738
|
+
hostControls?.dispose()
|
|
3739
|
+
instance?.dispose()
|
|
3740
|
+
if (workbench.current === instance) workbench.current = null
|
|
3741
|
+
if (window.rhineWorkbench === instance) window.rhineWorkbench = null
|
|
3742
|
+
overlay.remove()
|
|
3743
|
+
rhineCleanups.delete(cleanup)
|
|
3744
|
+
}
|
|
3745
|
+
rhineCleanups.add(cleanup)
|
|
3746
|
+
void loadRhineBundle().then((runtime) => {
|
|
3747
|
+
if (disposed || clientContext !== ctx || signal?.aborted) return
|
|
3748
|
+
status.remove()
|
|
3749
|
+
hostControls = ctx.sessions?.list?.getSnapshot ? createRhineHostControls(ctx, sessionId) : null
|
|
3750
|
+
instance = runtime.mountRhineWorkbench(overlay, {
|
|
3751
|
+
assetBase: RHINE_BASE, snapshot: latestSnapshot.current, snapshotDiagnostics,
|
|
3752
|
+
...(hostControls ? { host: hostControls } : {}),
|
|
3753
|
+
api: (endpoint, payload, requestSignal) => api(endpoint, payload, requestSignal || signal),
|
|
3754
|
+
askAgent: (text) => submitRhineQuestion(ctx, sessionId, text, signal),
|
|
3755
|
+
loadHistory: async () => {
|
|
3756
|
+
const session = ctx?.sessions?.binding?.(sessionId)?.session
|
|
3757
|
+
if (!session?.loadOlder) throw new Error('当前会话无法加载更早记录')
|
|
3758
|
+
await session.loadOlder()
|
|
3759
|
+
},
|
|
3760
|
+
close: () => setOpen(false),
|
|
3761
|
+
openSettings: () => openHostSettings(ctx),
|
|
3762
|
+
isSettingsOpen: isHostSettingsOpen,
|
|
3763
|
+
})
|
|
3764
|
+
workbench.current = instance
|
|
3765
|
+
// Expose the live workbench for diagnostics (bin/measure-rhine-console.js).
|
|
3766
|
+
window.rhineWorkbench = instance
|
|
3767
|
+
visible()
|
|
3768
|
+
removeFocusTrap()
|
|
3769
|
+
removeFocusTrap = installDialogFocusTrap(overlay, {
|
|
3770
|
+
returnFocus: trigger, isSuspended: isHostSettingsOpen,
|
|
3771
|
+
})
|
|
3772
|
+
}).catch((error) => {
|
|
3773
|
+
if (disposed) return
|
|
3774
|
+
if (!status.isConnected) overlay.append(status)
|
|
3775
|
+
message.textContent = error?.message || '资料馆加载失败,请关闭后重试'
|
|
3776
|
+
})
|
|
3777
|
+
return cleanup
|
|
3778
|
+
}, [open, skin, sessionId])
|
|
3779
|
+
if (skin !== 'rhine-lab') return null
|
|
3780
|
+
return h('button', { type: 'button', className: 'prts-rhine-trigger', ref: trigger,
|
|
3781
|
+
'aria-label': '打开莱茵生命资料馆', onClick: () => setOpen(true) },
|
|
3782
|
+
'▥ 资料馆', snapshot.sources.length ? ` · ${snapshot.sources.length}` : '')
|
|
3783
|
+
}
|
|
3784
|
+
|
|
3785
|
+
|
|
3786
|
+
/* ---- 插件入口:Settings → Plugins 的「PRTS 语料」tab ---- */
|
|
3787
|
+
exports.__sceneStateForTest = { buildSceneSnapshotModel, sceneSnapshotSignature,
|
|
3788
|
+
collectSnapshotEvidence, buildEvidence, readEvidenceSource, installDialogFocusTrap }
|
|
3789
|
+
exports.__rhineStateForTest = { buildRhineSnapshot, mergeArchiveSources, rhineSnapshotSignature, submitRhineQuestion, RhineControl, createRhineHostControls, createRhineSnapshotDiagnostics, createRhineSnapshotSelector }
|
|
3790
|
+
exports.__skinStateForTest = {
|
|
3791
|
+
setSkin, writeSkinConfig, SkinCard, PrtsSection, openHostSettings, isHostSettingsOpen,
|
|
3792
|
+
finishAicBoot: () => aicBootDone(),
|
|
3793
|
+
getActiveSkin: () => activeSkin,
|
|
3794
|
+
getHostSkinSyncGeneration: () => hostSkinSyncGeneration,
|
|
3795
|
+
}
|
|
3796
|
+
// Remote namespaces are separate Cordis services. A new model directory
|
|
3797
|
+
// also resolves remote.session through this caller's tracked context.
|
|
3798
|
+
exports.inject = ['slots', 'connection', 'theme', 'sessions', 'remote', 'remote.agentPresets', 'remote.session', 'workspaces', 'modelDirectories']
|
|
3799
|
+
|
|
3800
|
+
exports.apply = (ctx) => {
|
|
3801
|
+
// A replacement apply must invalidate and cancel the previous owner's
|
|
3802
|
+
// asynchronous skin transaction before installing its own stylesheet owner.
|
|
3803
|
+
clientAbortController?.abort(makeAbortError('PRTS 浏览器插件已被新实例替换'))
|
|
3804
|
+
disposeRhine()
|
|
3805
|
+
skinApplyEpoch += 1
|
|
3806
|
+
skinOperationEpoch += 1
|
|
3807
|
+
beginHostSkinSync()
|
|
3808
|
+
pendingSkinOperations.clear()
|
|
3809
|
+
skinSelectionOwner = null
|
|
3810
|
+
// Retire runtime handles while they still point at the previous context.
|
|
3811
|
+
// Otherwise a truthy old AIC disposer makes syncAicLayout() skip mounting
|
|
3812
|
+
// the shell into the replacement slot owner, and old theme tokens can leak
|
|
3813
|
+
// when the new context exposes a different theme runtime.
|
|
3814
|
+
if (disposeAicLayout) disposeAicLayout()
|
|
3815
|
+
disposeAicLayout = null
|
|
3816
|
+
if (disposeScene) disposeScene()
|
|
3817
|
+
disposeScene = null
|
|
3818
|
+
if (removeSkinTokens) removeSkinTokens()
|
|
3819
|
+
removeSkinTokens = null
|
|
3820
|
+
activeSkin = 'harness'
|
|
3821
|
+
writeSkinDataset('harness')
|
|
3822
|
+
aicBootAbort(true)
|
|
3823
|
+
const generation = ++applyGeneration
|
|
3824
|
+
const controller = new AbortController()
|
|
3825
|
+
clientAbortController = controller
|
|
3826
|
+
clientContext = ctx
|
|
3827
|
+
themeRuntime = ctx.theme
|
|
3828
|
+
if (typeof document !== 'undefined') {
|
|
3829
|
+
ctx.effect(installSkinStylesheets, 'prts-corpus: skin stylesheets')
|
|
3830
|
+
}
|
|
3831
|
+
const instanceCallApi = async (endpoint, payload, signal = controller.signal) => {
|
|
3832
|
+
const response = await fetch('/api/prts-corpus/rpc', {
|
|
3833
|
+
method: 'POST',
|
|
3834
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3835
|
+
credentials: 'same-origin',
|
|
3836
|
+
body: JSON.stringify({ endpoint, payload }),
|
|
3837
|
+
signal,
|
|
3838
|
+
})
|
|
3839
|
+
if (!response.ok) throw new Error(`PRTS 服务请求失败(HTTP ${response.status})`)
|
|
3840
|
+
const result = await response.json()
|
|
3841
|
+
if (typeof result?.ok !== 'boolean') throw new Error('PRTS 服务返回了无效响应')
|
|
3842
|
+
if (!result.ok) throw new Error(result.error?.message || 'PRTS 服务请求失败')
|
|
3843
|
+
return result.value
|
|
3844
|
+
}
|
|
3845
|
+
callApi = instanceCallApi
|
|
3846
|
+
ctx.effect(() => () => {
|
|
3847
|
+
controller.abort(makeAbortError('PRTS 浏览器插件已卸载'))
|
|
3848
|
+
if (generation !== applyGeneration) return
|
|
3849
|
+
disposeRhine()
|
|
3850
|
+
applyGeneration += 1
|
|
3851
|
+
skinOperationEpoch += 1
|
|
3852
|
+
skinApplyEpoch += 1
|
|
3853
|
+
beginHostSkinSync()
|
|
3854
|
+
if (disposeAicLayout) disposeAicLayout()
|
|
3855
|
+
disposeAicLayout = null
|
|
3856
|
+
if (disposeScene) disposeScene()
|
|
3857
|
+
disposeScene = null
|
|
3858
|
+
clientContext = null
|
|
3859
|
+
themeRuntime = null
|
|
3860
|
+
if (clientAbortController === controller) clientAbortController = null
|
|
3861
|
+
if (callApi === instanceCallApi) callApi = null
|
|
3862
|
+
if (removeSkinTokens) removeSkinTokens()
|
|
3863
|
+
removeSkinTokens = null
|
|
3864
|
+
activeSkin = 'harness'
|
|
3865
|
+
pendingSkinOperations.clear()
|
|
3866
|
+
skinSelectionOwner = null
|
|
3867
|
+
writeSkinDataset('harness')
|
|
3868
|
+
aicBootAbort(true)
|
|
3869
|
+
if (loadedMapRuntime && globalThis.__PRTS_ENDFIELD_MAP__ === loadedMapRuntime) {
|
|
3870
|
+
delete globalThis.__PRTS_ENDFIELD_MAP__
|
|
3871
|
+
}
|
|
3872
|
+
loadedMapRuntime = null
|
|
3873
|
+
cancelMapBundleLoad?.()
|
|
3874
|
+
cancelMapBundleLoad = null
|
|
3875
|
+
mapBundlePromise = null
|
|
3876
|
+
}, 'prts-corpus: skin cleanup')
|
|
3877
|
+
ctx.effect(() => ctx.slots.inject('settings.plugins.tab',
|
|
3878
|
+
() => ctx.slots.register(
|
|
3879
|
+
{ name: 'settings.plugins.tab', id: 'prts-corpus', label: 'PRTS 语料', order: 60 },
|
|
3880
|
+
PrtsSection)), 'prts-corpus: settings plugins tab')
|
|
3881
|
+
ctx.effect(() => ctx.slots.inject('conversation.session.header.utilities',
|
|
3882
|
+
() => ctx.slots.register(
|
|
3883
|
+
{ name: 'conversation.session.header.utilities', id: 'prts-evidence', order: 18 },
|
|
3884
|
+
EvidenceControl)), 'prts-corpus: evidence header control')
|
|
3885
|
+
// DSH omits the header utilities in a blank session. Keep the workbench
|
|
3886
|
+
// on the resident composer so it opens before the first prompt and keeps
|
|
3887
|
+
// the same instance when that session gains conversation content.
|
|
3888
|
+
ctx.effect(() => ctx.slots.inject('conversation.input.left',
|
|
3889
|
+
() => ctx.slots.register(
|
|
3890
|
+
{ name: 'conversation.input.left', id: 'prts-rhine', order: 17 },
|
|
3891
|
+
RhineControl)), 'prts-corpus: rhine archive composer control')
|
|
3892
|
+
// 从 apply 内接管启动屏,使 DOM、计时器和背景都归当前 Cordis fiber 所有。
|
|
3893
|
+
try {
|
|
3894
|
+
if (typeof localStorage !== 'undefined' && localStorage.getItem(AIC_BOOT_KEY) === 'endfield-aic') {
|
|
3895
|
+
console.info('[prts-terrarchive] boot takeover: cached endfield skin, showing AIC loading screen')
|
|
3896
|
+
aicBootShow('CONNECTING TERMINAL')
|
|
3897
|
+
}
|
|
3898
|
+
} catch { /* 隐私模式等无 localStorage 环境:退回默认启动屏 */ }
|
|
3899
|
+
const configuredSkinEpoch = skinOperationEpoch
|
|
3900
|
+
const configuredHostSyncGeneration = beginHostSkinSync()
|
|
3901
|
+
void loadConfiguredSkin({ signal: controller.signal, generation,
|
|
3902
|
+
skinEpoch: configuredSkinEpoch,
|
|
3903
|
+
hostSyncGeneration: configuredHostSyncGeneration }).catch((error) => {
|
|
3904
|
+
if (error?.name === 'AbortError' || generation !== applyGeneration) return
|
|
3905
|
+
console.error('[prts-terrarchive] failed to activate configured skin', error)
|
|
3906
|
+
aicBootAbort(true)
|
|
3907
|
+
})
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3910
|
+
return module.exports
|
|
3911
|
+
},
|
|
3912
|
+
})
|