prts-terrarchive 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GAME_ASSETS.md +26 -0
- package/LICENSE +21 -0
- package/README.en.md +274 -0
- package/README.md +423 -0
- package/THIRD_PARTY_NOTICES.md +61 -0
- package/bin/bake-rhine-details.mjs +34 -0
- package/bin/install.js +273 -0
- package/bin/pack-map-assets.mjs +110 -0
- package/bin/preview-rhine.mjs +71 -0
- package/bin/rhine-detail-baker.ts +84 -0
- package/contracts/cloud_tools_v1.schema.json +243 -0
- package/contracts/corpus_i18n_v1.schema.json +179 -0
- package/contracts/corpus_tools_v1.schema.json +1801 -0
- package/cordis.patch.yml +32 -0
- package/docs/endfield-official-i18n.md +103 -0
- package/lib/client.js +3912 -0
- package/lib/endfield-map/map.js.br +0 -0
- package/lib/endfield-map/map.js.gz +0 -0
- package/lib/endfield-map/resources/map-00b0d0744a1b4404.json.br +0 -0
- package/lib/endfield-map/resources/map-00b0d0744a1b4404.json.gz +0 -0
- package/lib/endfield-map/resources/map-01616f534aaaeca1.png +0 -0
- package/lib/endfield-map/resources/map-1562e0dff4be3d23.png +0 -0
- package/lib/endfield-map/resources/map-160fe040f080f6a3.png +0 -0
- package/lib/endfield-map/resources/map-17b9d8be25dc2b39.json.br +0 -0
- package/lib/endfield-map/resources/map-17b9d8be25dc2b39.json.gz +0 -0
- package/lib/endfield-map/resources/map-1c4245c4e8b80c43.json.br +0 -0
- package/lib/endfield-map/resources/map-1c4245c4e8b80c43.json.gz +0 -0
- package/lib/endfield-map/resources/map-1d30baaaef3e29cc.png +0 -0
- package/lib/endfield-map/resources/map-21a61386af79cd12.json.br +0 -0
- package/lib/endfield-map/resources/map-21a61386af79cd12.json.gz +0 -0
- package/lib/endfield-map/resources/map-24b920b8dab49b44.json.br +0 -0
- package/lib/endfield-map/resources/map-24b920b8dab49b44.json.gz +0 -0
- package/lib/endfield-map/resources/map-26caf8d71247abb7.png +0 -0
- package/lib/endfield-map/resources/map-2729942c89545e73.json.br +0 -0
- package/lib/endfield-map/resources/map-2729942c89545e73.json.gz +0 -0
- package/lib/endfield-map/resources/map-29063a9802f67014.json.br +0 -0
- package/lib/endfield-map/resources/map-29063a9802f67014.json.gz +0 -0
- package/lib/endfield-map/resources/map-2f35d09a3ab60a63.json.br +0 -0
- package/lib/endfield-map/resources/map-2f35d09a3ab60a63.json.gz +0 -0
- package/lib/endfield-map/resources/map-2f98a873da81425e.json.br +0 -0
- package/lib/endfield-map/resources/map-2f98a873da81425e.json.gz +0 -0
- package/lib/endfield-map/resources/map-3ab978ae44190f80.json.br +0 -0
- package/lib/endfield-map/resources/map-3ab978ae44190f80.json.gz +0 -0
- package/lib/endfield-map/resources/map-3b62c80d14208902.json.br +0 -0
- package/lib/endfield-map/resources/map-3b62c80d14208902.json.gz +0 -0
- package/lib/endfield-map/resources/map-3d486e887ae121ac.png +0 -0
- package/lib/endfield-map/resources/map-418dc4865608fce5.png +0 -0
- package/lib/endfield-map/resources/map-4366b84607335f76.json.br +0 -0
- package/lib/endfield-map/resources/map-4366b84607335f76.json.gz +0 -0
- package/lib/endfield-map/resources/map-4371259d3ab7aac7.png +0 -0
- package/lib/endfield-map/resources/map-48dcc0fd55e13f7f.png +0 -0
- package/lib/endfield-map/resources/map-4aabda0e41197570.json.br +0 -0
- package/lib/endfield-map/resources/map-4aabda0e41197570.json.gz +0 -0
- package/lib/endfield-map/resources/map-505f5687b287043c.json.br +0 -0
- package/lib/endfield-map/resources/map-505f5687b287043c.json.gz +0 -0
- package/lib/endfield-map/resources/map-5177e9d1d46f47e2.png +0 -0
- package/lib/endfield-map/resources/map-51baeb59b7f8c28d.png +0 -0
- package/lib/endfield-map/resources/map-549561b46e621874.png +0 -0
- package/lib/endfield-map/resources/map-56c6a0bef799c8dc.json.br +0 -0
- package/lib/endfield-map/resources/map-56c6a0bef799c8dc.json.gz +0 -0
- package/lib/endfield-map/resources/map-5a6963909423b674.png +0 -0
- package/lib/endfield-map/resources/map-5a9c533c0552d7b7.png +0 -0
- package/lib/endfield-map/resources/map-5d20ae9ffa9a02b0.png +0 -0
- package/lib/endfield-map/resources/map-626c8847393f60bf.png +0 -0
- package/lib/endfield-map/resources/map-63d43cbec3bed3b8.png +0 -0
- package/lib/endfield-map/resources/map-661b6ec56e722574.png +0 -0
- package/lib/endfield-map/resources/map-6ddd384eca190828.png +0 -0
- package/lib/endfield-map/resources/map-6fc1aa9144f8156c.json.br +0 -0
- package/lib/endfield-map/resources/map-6fc1aa9144f8156c.json.gz +0 -0
- package/lib/endfield-map/resources/map-738ebaa002c33cfc.png +0 -0
- package/lib/endfield-map/resources/map-758c13be6085ec07.json.br +0 -0
- package/lib/endfield-map/resources/map-758c13be6085ec07.json.gz +0 -0
- package/lib/endfield-map/resources/map-78929f26d6844509.png +0 -0
- package/lib/endfield-map/resources/map-7f09c142950a0f74.png +0 -0
- package/lib/endfield-map/resources/map-8385e11f39e1a51c.json.br +0 -0
- package/lib/endfield-map/resources/map-8385e11f39e1a51c.json.gz +0 -0
- package/lib/endfield-map/resources/map-881343926a568f27.json.br +0 -0
- package/lib/endfield-map/resources/map-881343926a568f27.json.gz +0 -0
- package/lib/endfield-map/resources/map-8d1ade3f01d952b0.png +0 -0
- package/lib/endfield-map/resources/map-8fef3495bb7ed60f.png +0 -0
- package/lib/endfield-map/resources/map-907a7ecf8a0ce352.png +0 -0
- package/lib/endfield-map/resources/map-9113d5951c4db903.json.br +0 -0
- package/lib/endfield-map/resources/map-9113d5951c4db903.json.gz +0 -0
- package/lib/endfield-map/resources/map-94ad6634436224e3.json.br +0 -0
- package/lib/endfield-map/resources/map-94ad6634436224e3.json.gz +0 -0
- package/lib/endfield-map/resources/map-99722e61da5f3e53.png +0 -0
- package/lib/endfield-map/resources/map-9be9596cc059abe3.png +0 -0
- package/lib/endfield-map/resources/map-a4db9a4763e47fbf.png +0 -0
- package/lib/endfield-map/resources/map-a4e8ef07159997e7.json.br +0 -0
- package/lib/endfield-map/resources/map-a4e8ef07159997e7.json.gz +0 -0
- package/lib/endfield-map/resources/map-ab35d06875ee418f.png +0 -0
- package/lib/endfield-map/resources/map-ac51bb3e7623cd8b.png +0 -0
- package/lib/endfield-map/resources/map-ac830a66296bc83a.png +0 -0
- package/lib/endfield-map/resources/map-b3fd04209718e55e.json.br +0 -0
- package/lib/endfield-map/resources/map-b3fd04209718e55e.json.gz +0 -0
- package/lib/endfield-map/resources/map-b8164f1af7c5d895.png +0 -0
- package/lib/endfield-map/resources/map-b8cde93b965384d8.json.br +0 -0
- package/lib/endfield-map/resources/map-b8cde93b965384d8.json.gz +0 -0
- package/lib/endfield-map/resources/map-b8e48511502e0e8d.json.br +0 -0
- package/lib/endfield-map/resources/map-b8e48511502e0e8d.json.gz +0 -0
- package/lib/endfield-map/resources/map-bfc58277d5ba649d.png +0 -0
- package/lib/endfield-map/resources/map-c013f82048c6891a.png +0 -0
- package/lib/endfield-map/resources/map-c49c947841ae4ab5.json.br +0 -0
- package/lib/endfield-map/resources/map-c49c947841ae4ab5.json.gz +0 -0
- package/lib/endfield-map/resources/map-c4d1b1c6d6706c81.json.br +0 -0
- package/lib/endfield-map/resources/map-c4d1b1c6d6706c81.json.gz +0 -0
- package/lib/endfield-map/resources/map-c607f8b4c8ad8967.png +0 -0
- package/lib/endfield-map/resources/map-c6f20276b8781326.json.br +0 -0
- package/lib/endfield-map/resources/map-c6f20276b8781326.json.gz +0 -0
- package/lib/endfield-map/resources/map-d51adf5952af1e8e.png +0 -0
- package/lib/endfield-map/resources/map-d695eface22f8b74.json.br +0 -0
- package/lib/endfield-map/resources/map-d695eface22f8b74.json.gz +0 -0
- package/lib/endfield-map/resources/map-db11d394456a7465.png +0 -0
- package/lib/endfield-map/resources/map-dcc4d218581e3dc6.png +0 -0
- package/lib/endfield-map/resources/map-ddd43dbb8ab8a4e3.json.br +0 -0
- package/lib/endfield-map/resources/map-ddd43dbb8ab8a4e3.json.gz +0 -0
- package/lib/endfield-map/resources/map-defe5db7dabf748a.png +0 -0
- package/lib/endfield-map/resources/map-e1aeb833e22520aa.json.br +0 -0
- package/lib/endfield-map/resources/map-e1aeb833e22520aa.json.gz +0 -0
- package/lib/endfield-map/resources/map-e25ee9af6e493500.png +0 -0
- package/lib/endfield-map/resources/map-e3824082cd6f747d.json.br +0 -0
- package/lib/endfield-map/resources/map-e3824082cd6f747d.json.gz +0 -0
- package/lib/endfield-map/resources/map-e694a91e630b1bd6.png +0 -0
- package/lib/endfield-map/resources/map-eaa93af79d12a87d.png +0 -0
- package/lib/endfield-map/resources/map-ec9575ee65d7766f.json.br +0 -0
- package/lib/endfield-map/resources/map-ec9575ee65d7766f.json.gz +0 -0
- package/lib/endfield-map/resources/map-ede421a329dc78a4.png +0 -0
- package/lib/endfield-map/resources/map-eff3bce8f6888fc5.json.br +0 -0
- package/lib/endfield-map/resources/map-eff3bce8f6888fc5.json.gz +0 -0
- package/lib/endfield-map/resources/map-fb5f4a59a181452c.png +0 -0
- package/lib/endfield-map/resources/map-fdeaa17eeba90d8e.json.br +0 -0
- package/lib/endfield-map/resources/map-fdeaa17eeba90d8e.json.gz +0 -0
- package/lib/rhine/assets/archive-assembly.glb +0 -0
- package/lib/rhine/assets/archive-cassette.glb +0 -0
- package/lib/rhine/fonts/MiSans-Bold.woff2 +0 -0
- package/lib/rhine/fonts/MiSans-Demibold.woff2 +0 -0
- package/lib/rhine/fonts/MiSans-Light.woff2 +0 -0
- package/lib/rhine/fonts/MiSans-Regular.woff2 +0 -0
- package/lib/rhine/fonts/MiSans-license.pdf +0 -0
- package/lib/rhine/fonts/NOTICE.txt +6 -0
- package/lib/rhine/licenses/RhineLabUI-MIT.txt +21 -0
- package/lib/rhine/licenses/entities-BSD-2-Clause.txt +11 -0
- package/lib/rhine/licenses/linkify-it-MIT.txt +22 -0
- package/lib/rhine/licenses/markdown-it-MIT.txt +22 -0
- package/lib/rhine/licenses/mdurl-MIT.txt +45 -0
- package/lib/rhine/licenses/punycode.js-MIT.txt +20 -0
- package/lib/rhine/licenses/rolling-number-MIT.txt +21 -0
- package/lib/rhine/licenses/three-MIT.txt +21 -0
- package/lib/rhine/licenses/uc.micro-MIT.txt +20 -0
- package/lib/rhine/rhine.css +1 -0
- package/lib/rhine/rhine.js +5106 -0
- package/lib/skins/common.css +31 -0
- package/lib/skins/endfield-aic.css +141 -0
- package/lib/skins/prts-agent.css +136 -0
- package/lib/skins/rhine-lab.css +70 -0
- package/package.json +112 -0
- package/presets/definition.js +26 -0
- package/presets/prts/agent.cordis.yml +25 -0
- package/presets/prts/preset.yml +3 -0
- package/presets/register.js +86 -0
- package/resources/entity-routing.json +21 -0
- package/skills/prts-investigation/SKILL.md +121 -0
- package/skills/prts-retrieval/SKILL.md +35 -0
- package/skills/prts-retrieval/references/cloud-tools.md +13 -0
- package/skills/prts-retrieval/references/diagnostics.md +15 -0
- package/skills/prts-retrieval/references/local-tools.md +24 -0
- package/skills/prts-retrieval/references/module-arknights.md +23 -0
- package/skills/prts-retrieval/references/module-dual.md +15 -0
- package/skills/prts-retrieval/references/module-endfield.md +21 -0
- package/skills/prts-retrieval/references/retrieval-process.md +44 -0
- package/skills/prts-retrieval/references/retrieval-recipes-dual.md +34 -0
- package/skills/prts-retrieval/references/retrieval-recipes-endfield.md +37 -0
- package/skills/prts-retrieval/references/retrieval-recipes.md +39 -0
- package/skills/prts-retrieval/references/tools-runtime.md +48 -0
- package/skills/prts-retrieval/references/wiki-schema.md +38 -0
- package/src/archive-presentation.js +287 -0
- package/src/cloud-projection.js +124 -0
- package/src/cloud.js +498 -0
- package/src/entity-recognizer.js +311 -0
- package/src/entity-routing.js +82 -0
- package/src/evidence-state.js +332 -0
- package/src/i18n.js +365 -0
- package/src/index.js +1261 -0
- package/src/installer.js +1362 -0
- package/src/investigation-review.js +87 -0
- package/src/investigation-store.js +579 -0
- package/src/investigation-tools.js +113 -0
- package/src/localization-format.js +23 -0
- package/src/manual-search.js +103 -0
- package/src/read.js +1097 -0
- package/src/release-compatibility.js +73 -0
- package/src/release-watch.js +40 -0
- package/src/search-projection.js +139 -0
- package/src/search.js +1522 -0
- package/src/skill.js +98 -0
- package/src/source-map.js +348 -0
- package/src/state.js +314 -0
- package/src/store.js +1569 -0
- package/src/timeline.js +386 -0
- package/src/ui.js +1353 -0
- package/src/wiki.js +111 -0
- package/tsconfig.rhine.json +15 -0
- package/ui/rhine/archive-navigation-limit.ts +31 -0
- package/ui/rhine/archive-rack.ts +83 -0
- package/ui/rhine/assets/evidence-observatory.webp +0 -0
- package/ui/rhine/board-plane-transform.ts +24 -0
- package/ui/rhine/catalogue.ts +72 -0
- package/ui/rhine/deferred-preparation.ts +146 -0
- package/ui/rhine/evidence-board-controls.css +22 -0
- package/ui/rhine/evidence-board-history.ts +73 -0
- package/ui/rhine/evidence-board-model.ts +189 -0
- package/ui/rhine/evidence-board-panel.ts +762 -0
- package/ui/rhine/evidence-board-scene.ts +548 -0
- package/ui/rhine/evidence-board.css +191 -0
- package/ui/rhine/evidence-card-art.ts +470 -0
- package/ui/rhine/evidence-card-style.ts +17 -0
- package/ui/rhine/evidence-inbox-scene.ts +43 -0
- package/ui/rhine/evidence-inbox.css +29 -0
- package/ui/rhine/evidence-inbox.ts +104 -0
- package/ui/rhine/index.ts +12 -0
- package/ui/rhine/interaction.css +20 -0
- package/ui/rhine/investigation-board-plane.ts +5 -0
- package/ui/rhine/investigation-board.css +34 -0
- package/ui/rhine/investigation-board.ts +348 -0
- package/ui/rhine/investigation-edits.ts +46 -0
- package/ui/rhine/investigation-layout.ts +29 -0
- package/ui/rhine/label-prefetch.ts +34 -0
- package/ui/rhine/looping-title.ts +40 -0
- package/ui/rhine/manual-search.css +41 -0
- package/ui/rhine/manual-search.ts +89 -0
- package/ui/rhine/original/appearance.ts +180 -0
- package/ui/rhine/original/archive-label.ts +84 -0
- package/ui/rhine/original/archive-lighting.ts +39 -0
- package/ui/rhine/original/archive-loop.ts +82 -0
- package/ui/rhine/original/archive-refill.ts +64 -0
- package/ui/rhine/original/array-detail-data.ts +2 -0
- package/ui/rhine/original/array-detail.ts +41 -0
- package/ui/rhine/original/array-interior.ts +411 -0
- package/ui/rhine/original/array-occlusion.ts +166 -0
- package/ui/rhine/original/array-shell.ts +38 -0
- package/ui/rhine/original/array-visibility.ts +309 -0
- package/ui/rhine/original/asset-url.ts +4 -0
- package/ui/rhine/original/brand.ts +12 -0
- package/ui/rhine/original/cassette-lod.ts +81 -0
- package/ui/rhine/original/data.ts +21 -0
- package/ui/rhine/original/decryption.ts +185 -0
- package/ui/rhine/original/detail-signature.ts +18 -0
- package/ui/rhine/original/glass-reveal.ts +45 -0
- package/ui/rhine/original/internal-optics.ts +70 -0
- package/ui/rhine/original/label-renderer.ts +119 -0
- package/ui/rhine/original/label-textures.ts +117 -0
- package/ui/rhine/original/model-viewer.ts +658 -0
- package/ui/rhine/original/motion.ts +116 -0
- package/ui/rhine/original/opaque-backfaces.ts +64 -0
- package/ui/rhine/original/program-preparation.ts +111 -0
- package/ui/rhine/original/quality-renderer.ts +97 -0
- package/ui/rhine/original/quality-settings.ts +111 -0
- package/ui/rhine/original/render-quality.ts +163 -0
- package/ui/rhine/original/scene.ts +2051 -0
- package/ui/rhine/original/shelf-interior.ts +142 -0
- package/ui/rhine/original/ui-transitions.ts +111 -0
- package/ui/rhine/original/viewer-camera.ts +92 -0
- package/ui/rhine/original/viewport-layout.ts +41 -0
- package/ui/rhine/original-ui.css +2334 -0
- package/ui/rhine/readability.css +46 -0
- package/ui/rhine/reading-object.ts +96 -0
- package/ui/rhine/reading-origin.ts +28 -0
- package/ui/rhine/reading-room.css +55 -0
- package/ui/rhine/report-markdown.ts +98 -0
- package/ui/rhine/scene-edge-navigation.ts +84 -0
- package/ui/rhine/scene.ts +1726 -0
- package/ui/rhine/shelf-layout.ts +38 -0
- package/ui/rhine/snapshot-update-gate.ts +22 -0
- package/ui/rhine/source-reading.ts +15 -0
- package/ui/rhine/source-selection.css +164 -0
- package/ui/rhine/temporary-performance-observers.ts +163 -0
- package/ui/rhine/temporary-performance-panel.ts +124 -0
- package/ui/rhine/temporary-performance.css +19 -0
- package/ui/rhine/temporary-performance.ts +552 -0
- package/ui/rhine/temporary-render-diagnostics.ts +105 -0
- package/ui/rhine/tool-activity.ts +69 -0
- package/ui/rhine/types.ts +219 -0
- package/ui/rhine/visible-scene-hit.ts +13 -0
- package/ui/rhine/workbench.css +818 -0
- package/ui/rhine/workbench.ts +2245 -0
- package/ui/rhine/workspace-travel.ts +32 -0
- package/vite.rhine.config.ts +19 -0
package/src/cloud.js
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh 版云端检索适配器:cloud_search / cloud_inspect 的 Node HTTP 客户端。
|
|
3
|
+
*
|
|
4
|
+
* 与浏览器端 agent/browser/src/cloud-client.js 及 frontend/src/agent-demo/prts-auth.js 语义对齐:
|
|
5
|
+
* - 固定同站路径 /api/agent/retrieval/{capabilities,search,inspect},URL 永不受模型控制;
|
|
6
|
+
* - 认证:匿名 PoW 会话(GET /api/init-session → 解工作量证明 → POST /api/login),
|
|
7
|
+
* 或配置静态 token(cloud.token)跳过登录;401 时强制刷新一次重试;
|
|
8
|
+
* - 响应上限 32MiB;code===200 校验;HTTP 状态 → 契约错误码映射(agent-cloud-retrieval.v1)。
|
|
9
|
+
*/
|
|
10
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
11
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
12
|
+
import { dirname } from 'node:path'
|
|
13
|
+
import { createRequire } from 'node:module'
|
|
14
|
+
|
|
15
|
+
export const CLOUD_CONTRACT_VERSION = 'agent-cloud-retrieval.v1'
|
|
16
|
+
|
|
17
|
+
/** 随包版本走的调用方标识;服务端据此把 DSH 插件流量与 web 前端分开统计。 */
|
|
18
|
+
export const DSH_CLIENT_HEADER = `dsh-plugin/${createRequire(import.meta.url)('../package.json').version}`
|
|
19
|
+
|
|
20
|
+
const CLIENT_ID_PATTERN = /^[A-Za-z0-9_-]{8,64}$/
|
|
21
|
+
const MAX_CLOUD_REQUEST_BYTES = 1024 * 1024
|
|
22
|
+
|
|
23
|
+
function normalizeCloudBaseUrl(value) {
|
|
24
|
+
let url
|
|
25
|
+
try { url = new URL(String(value ?? '')) } catch {
|
|
26
|
+
throw new TypeError('cloud baseUrl 必须是有效 URL')
|
|
27
|
+
}
|
|
28
|
+
const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)
|
|
29
|
+
if (url.username || url.password || url.search || url.hash
|
|
30
|
+
|| (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback))) {
|
|
31
|
+
throw new TypeError('cloud baseUrl 必须使用 HTTPS(本地环回可用 HTTP),且不能包含凭证、查询或片段')
|
|
32
|
+
}
|
|
33
|
+
return url.toString().replace(/\/+$/, '')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function remoteMessage(value, fallback) {
|
|
37
|
+
if (typeof value !== 'string') return fallback
|
|
38
|
+
const normalized = value.replace(/[\u0000-\u001f\u007f]+/gu, ' ').replace(/\s+/gu, ' ').trim()
|
|
39
|
+
return normalized ? normalized.slice(0, 1000) : fallback
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 匿名会话的持久 client id:同一安装跨重启保持稳定,服务端按它统计
|
|
44
|
+
* 独立用户。文件 0600、目录 0700,并发首次创建以 wx 保证只有一个胜者。
|
|
45
|
+
* @param {string} path 如 $DSH_HOME/prts-corpus/client-id
|
|
46
|
+
* @returns {Promise<string>}
|
|
47
|
+
*/
|
|
48
|
+
export async function readOrCreateClientId(path) {
|
|
49
|
+
try {
|
|
50
|
+
const existing = (await readFile(path, 'utf8')).trim()
|
|
51
|
+
if (CLIENT_ID_PATTERN.test(existing)) return existing
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error?.code !== 'ENOENT') throw error
|
|
54
|
+
}
|
|
55
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 })
|
|
56
|
+
const created = `dsh-${randomUUID().replaceAll('-', '').slice(0, 20)}`
|
|
57
|
+
try {
|
|
58
|
+
await writeFile(path, `${created}\n`, { flag: 'wx', mode: 0o600 })
|
|
59
|
+
return created
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error?.code !== 'EEXIST') throw error
|
|
62
|
+
const existing = (await readFile(path, 'utf8')).trim()
|
|
63
|
+
if (!CLIENT_ID_PATTERN.test(existing)) throw new Error('cloud client id 文件损坏')
|
|
64
|
+
return existing
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const PATHS = {
|
|
69
|
+
capabilities: '/api/agent/retrieval/capabilities',
|
|
70
|
+
cloud_search: '/api/agent/retrieval/search',
|
|
71
|
+
cloud_inspect: '/api/agent/retrieval/inspect',
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class CloudFault extends Error {
|
|
75
|
+
/**
|
|
76
|
+
* @param {string} code 契约错误码
|
|
77
|
+
* @param {string} message
|
|
78
|
+
* @param {boolean} [retryable]
|
|
79
|
+
* @param {object} [details]
|
|
80
|
+
*/
|
|
81
|
+
constructor(code, message, retryable = false, details = undefined) {
|
|
82
|
+
super(message)
|
|
83
|
+
this.code = code
|
|
84
|
+
this.retryable = retryable
|
|
85
|
+
this.details = details
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** ---- 匿名会话(init-session + PoW + login,与浏览器 prts-auth 同构) ---- */
|
|
90
|
+
|
|
91
|
+
const AUTH_RESPONSE_MAX_BYTES = 256 * 1024
|
|
92
|
+
const MAX_POW_SEED_CHARS = 512
|
|
93
|
+
const MAX_POW_DIFFICULTY = 6
|
|
94
|
+
|
|
95
|
+
function powHex(seed, nonce) {
|
|
96
|
+
return createHash('sha256').update(`${seed}${nonce}`, 'utf8').digest('hex')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function cancelledFault() {
|
|
100
|
+
return new CloudFault('CANCELLED', '云端请求已取消')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function throwIfCancelled(signal) {
|
|
104
|
+
if (signal?.aborted) throw cancelledFault()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 解工作量证明:sha256(seed+nonce) 十六进制前缀 difficulty 个 0。定期让出事件循环以响应取消。 */
|
|
108
|
+
export async function solvePow(seed, difficulty, { signal } = {}) {
|
|
109
|
+
if (typeof seed !== 'string' || !seed || [...seed].length > MAX_POW_SEED_CHARS
|
|
110
|
+
|| !Number.isInteger(difficulty) || difficulty < 0 || difficulty > MAX_POW_DIFFICULTY) {
|
|
111
|
+
throw new CloudFault('AUTH_REQUIRED', 'PRTS 会话返回了非法的工作量证明参数')
|
|
112
|
+
}
|
|
113
|
+
const prefix = '0'.repeat(difficulty)
|
|
114
|
+
const deadline = Date.now() + 8000
|
|
115
|
+
for (let nonce = 0; ; nonce += 1) {
|
|
116
|
+
throwIfCancelled(signal)
|
|
117
|
+
if (Date.now() > deadline) throw new CloudFault('AUTH_REQUIRED', 'PRTS 会话验证计算超时')
|
|
118
|
+
if (powHex(seed, nonce).startsWith(prefix)) return String(nonce)
|
|
119
|
+
if (nonce > 0 && nonce % 2048 === 0) {
|
|
120
|
+
await new Promise((resolve) => { setImmediate(resolve) })
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function fetchWithTimeout(fetchImpl, url, init, timeoutMs, signal, consume = (response) => response) {
|
|
126
|
+
throwIfCancelled(signal)
|
|
127
|
+
const controller = new AbortController()
|
|
128
|
+
let timedOut = false
|
|
129
|
+
const timer = setTimeout(() => {
|
|
130
|
+
timedOut = true
|
|
131
|
+
controller.abort()
|
|
132
|
+
}, timeoutMs)
|
|
133
|
+
const onAbort = () => controller.abort()
|
|
134
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
135
|
+
try {
|
|
136
|
+
// 云端 API 没有跨源跳转语义;拒绝 redirect,避免静态 token 或匿名会话
|
|
137
|
+
// 被配置源借 30x 带往另一个网络边界。
|
|
138
|
+
const response = await fetchImpl(url, { ...init, redirect: 'error', signal: controller.signal })
|
|
139
|
+
return await consume(response)
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (signal?.aborted) throw cancelledFault()
|
|
142
|
+
if (error instanceof CloudFault) throw error
|
|
143
|
+
if (timedOut || error?.name === 'AbortError') {
|
|
144
|
+
throw new CloudFault('CLOUD_TIMEOUT', '云端请求超时', true)
|
|
145
|
+
}
|
|
146
|
+
throw new CloudFault('CLOUD_UNAVAILABLE', `无法连接 ${new URL(url).host}`, true,
|
|
147
|
+
{ exception_type: error?.constructor?.name || typeof error })
|
|
148
|
+
} finally {
|
|
149
|
+
clearTimeout(timer)
|
|
150
|
+
signal?.removeEventListener('abort', onAbort)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function readJsonResponse(response, serviceName) {
|
|
155
|
+
const bytes = await readResponseBytes(response, AUTH_RESPONSE_MAX_BYTES)
|
|
156
|
+
const text = new TextDecoder().decode(bytes)
|
|
157
|
+
let payload = null
|
|
158
|
+
if (text.trim()) {
|
|
159
|
+
try {
|
|
160
|
+
payload = JSON.parse(text)
|
|
161
|
+
} catch {
|
|
162
|
+
throw new CloudFault('INVALID_RESPONSE', `${serviceName}返回了无法解析的响应(HTTP ${response.status})`)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
const detail = payload?.detail || payload?.error?.message
|
|
167
|
+
throw new CloudFault('CLOUD_ERROR', remoteMessage(detail,
|
|
168
|
+
`${serviceName}不可用(HTTP ${response.status})`),
|
|
169
|
+
response.status >= 500)
|
|
170
|
+
}
|
|
171
|
+
if (!payload) throw new CloudFault('INVALID_RESPONSE', `${serviceName}返回了空响应`)
|
|
172
|
+
return payload
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 匿名会话提供者:token 缓存在单个 Agent 的客户端内存(不落盘)。
|
|
177
|
+
*/
|
|
178
|
+
export class AnonymousSessionProvider {
|
|
179
|
+
/**
|
|
180
|
+
* @param {{ baseUrl: string, userId?: string, timeoutMs?: number, fetchImpl?: typeof fetch }} options
|
|
181
|
+
*/
|
|
182
|
+
constructor({ baseUrl, userId, timeoutMs = 8000, fetchImpl } = {}) {
|
|
183
|
+
this.baseUrl = normalizeCloudBaseUrl(baseUrl)
|
|
184
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 10 * 60_000) {
|
|
185
|
+
throw new TypeError('cloud timeoutMs 超出允许范围')
|
|
186
|
+
}
|
|
187
|
+
if (userId != null && (typeof userId !== 'string' || !userId || userId.length > 128)) {
|
|
188
|
+
throw new TypeError('cloud userId 非法')
|
|
189
|
+
}
|
|
190
|
+
this.userId = userId || `dsh-${randomUUID().slice(0, 12)}`
|
|
191
|
+
this.timeoutMs = timeoutMs
|
|
192
|
+
this.fetchImpl = fetchImpl || fetch
|
|
193
|
+
/** @type {string | null} */
|
|
194
|
+
this.token = null
|
|
195
|
+
/** @type {{ promise: Promise<string>, controller: AbortController, waiters: number } | null} */
|
|
196
|
+
this.inFlight = null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** @param {{ forceRefresh?: boolean, signal?: AbortSignal }} [options] @returns {Promise<string>} Bearer token */
|
|
200
|
+
async getToken({ forceRefresh = false, signal } = {}) {
|
|
201
|
+
throwIfCancelled(signal)
|
|
202
|
+
if (forceRefresh) {
|
|
203
|
+
this.token = null
|
|
204
|
+
// 强刷不复用 refresh 之前已在途的登录:它可能正是产出刚被 401 的令牌的
|
|
205
|
+
// 一次登录,等待它会在"token 未变化"的判定下放弃 401 重试。脱离引用
|
|
206
|
+
// 后旧登录照常完成并让自身的等待者拿到结果,只是不再作为本次的来源。
|
|
207
|
+
this.inFlight = null
|
|
208
|
+
}
|
|
209
|
+
if (this.token) return this.token
|
|
210
|
+
if (!this.inFlight) {
|
|
211
|
+
const controller = new AbortController()
|
|
212
|
+
const record = { controller, waiters: 0, promise: null }
|
|
213
|
+
record.promise = this.login({ signal: controller.signal }).finally(() => {
|
|
214
|
+
if (this.inFlight === record) this.inFlight = null
|
|
215
|
+
})
|
|
216
|
+
this.inFlight = record
|
|
217
|
+
}
|
|
218
|
+
return this.waitForLogin(this.inFlight, signal)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async waitForLogin(record, signal) {
|
|
222
|
+
record.waiters += 1
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
let settled = false
|
|
225
|
+
const finish = (callback, value) => {
|
|
226
|
+
if (settled) return
|
|
227
|
+
settled = true
|
|
228
|
+
signal?.removeEventListener('abort', onAbort)
|
|
229
|
+
record.waiters -= 1
|
|
230
|
+
callback(value)
|
|
231
|
+
}
|
|
232
|
+
const onAbort = () => {
|
|
233
|
+
finish(reject, cancelledFault())
|
|
234
|
+
if (record.waiters === 0) record.controller.abort()
|
|
235
|
+
}
|
|
236
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
237
|
+
if (signal?.aborted) return onAbort()
|
|
238
|
+
record.promise.then(
|
|
239
|
+
(token) => finish(resolve, token),
|
|
240
|
+
(error) => finish(reject, error),
|
|
241
|
+
)
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async login({ signal } = {}) {
|
|
246
|
+
const sessionPayload = await fetchWithTimeout(this.fetchImpl,
|
|
247
|
+
`${this.baseUrl}/api/init-session`, { headers: { Accept: 'application/json' } }, this.timeoutMs, signal,
|
|
248
|
+
(response) => readJsonResponse(response, 'PRTS 会话服务'))
|
|
249
|
+
const session = sessionPayload?.data
|
|
250
|
+
if (typeof session?.session_token !== 'string' || !session.session_token
|
|
251
|
+
|| session.session_token.length > 4096) {
|
|
252
|
+
throw new CloudFault('AUTH_REQUIRED', 'PRTS 会话响应缺少有效的 session_token')
|
|
253
|
+
}
|
|
254
|
+
const nonce = session.pow_seed
|
|
255
|
+
? await solvePow(session.pow_seed, session.pow_difficulty ?? 3, { signal })
|
|
256
|
+
: null
|
|
257
|
+
const loginPayload = await fetchWithTimeout(this.fetchImpl,
|
|
258
|
+
`${this.baseUrl}/api/login`, {
|
|
259
|
+
method: 'POST',
|
|
260
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json',
|
|
261
|
+
'X-Session-Token': session.session_token },
|
|
262
|
+
body: JSON.stringify({ user_id: this.userId, nonce }),
|
|
263
|
+
}, this.timeoutMs, signal, (response) => readJsonResponse(response, 'PRTS 登录服务'))
|
|
264
|
+
if (typeof loginPayload?.data !== 'string' || !loginPayload.data
|
|
265
|
+
|| loginPayload.data.length > 16 * 1024) {
|
|
266
|
+
throw new CloudFault('AUTH_REQUIRED', 'PRTS 登录响应缺少有效的访问令牌')
|
|
267
|
+
}
|
|
268
|
+
const token = loginPayload.data
|
|
269
|
+
this.token = token.startsWith('Bearer ') ? token : `Bearer ${token}`
|
|
270
|
+
return this.token
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function readResponseBytes(response, maximum) {
|
|
275
|
+
const declared = Number(response.headers?.get?.('content-length') ?? NaN)
|
|
276
|
+
if (Number.isFinite(declared) && declared > maximum) {
|
|
277
|
+
throw new CloudFault('RESPONSE_TOO_LARGE', '云端检索响应超过接收上限')
|
|
278
|
+
}
|
|
279
|
+
const reader = response.body?.getReader?.()
|
|
280
|
+
if (!reader) {
|
|
281
|
+
const bytes = new Uint8Array(await response.arrayBuffer())
|
|
282
|
+
if (bytes.byteLength > maximum) throw new CloudFault('RESPONSE_TOO_LARGE', '云端检索响应超过接收上限')
|
|
283
|
+
return bytes
|
|
284
|
+
}
|
|
285
|
+
const chunks = []
|
|
286
|
+
let size = 0
|
|
287
|
+
for (;;) {
|
|
288
|
+
const { done, value } = await reader.read()
|
|
289
|
+
if (done) break
|
|
290
|
+
size += value.byteLength
|
|
291
|
+
if (size > maximum) {
|
|
292
|
+
await reader.cancel().catch(() => {})
|
|
293
|
+
throw new CloudFault('RESPONSE_TOO_LARGE', '云端检索响应超过接收上限')
|
|
294
|
+
}
|
|
295
|
+
chunks.push(value)
|
|
296
|
+
}
|
|
297
|
+
const bytes = new Uint8Array(size)
|
|
298
|
+
let offset = 0
|
|
299
|
+
for (const chunk of chunks) {
|
|
300
|
+
bytes.set(chunk, offset)
|
|
301
|
+
offset += chunk.byteLength
|
|
302
|
+
}
|
|
303
|
+
return bytes
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** 静态 token 提供者(cloud.token 配置)。 */
|
|
307
|
+
export class StaticTokenProvider {
|
|
308
|
+
constructor(token) {
|
|
309
|
+
if (typeof token !== 'string' || !token || token.length > 16 * 1024) {
|
|
310
|
+
throw new TypeError('cloud token 非法')
|
|
311
|
+
}
|
|
312
|
+
this.token = token.startsWith('Bearer ') ? token : `Bearer ${token}`
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async getToken({ signal } = {}) {
|
|
316
|
+
throwIfCancelled(signal)
|
|
317
|
+
return this.token
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** 为每个 Agent 惰性创建独立客户端;缺少 Agent 上下文时不缓存,避免意外跨执行共享。 */
|
|
322
|
+
export function createAgentCloudClientRegistry(createClient) {
|
|
323
|
+
const clients = new WeakMap()
|
|
324
|
+
return {
|
|
325
|
+
forExecution(exec) {
|
|
326
|
+
const agent = exec?.agent
|
|
327
|
+
if ((typeof agent !== 'object' || agent === null) && typeof agent !== 'function') {
|
|
328
|
+
return createClient()
|
|
329
|
+
}
|
|
330
|
+
let client = clients.get(agent)
|
|
331
|
+
if (!client) {
|
|
332
|
+
client = createClient()
|
|
333
|
+
clients.set(agent, client)
|
|
334
|
+
}
|
|
335
|
+
return client
|
|
336
|
+
},
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** ---- 云端检索客户端 ---- */
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* @param {{ baseUrl: string, tokenProvider: { getToken(options?: {forceRefresh?: boolean, signal?: AbortSignal}): Promise<string> },
|
|
344
|
+
* game?: 'all' | 'arknights' | 'endfield', games?: ('arknights' | 'endfield')[],
|
|
345
|
+
* timeoutMs?: number, maxResponseBytes?: number, fetchImpl?: typeof fetch }} options
|
|
346
|
+
*/
|
|
347
|
+
export class CloudRetrievalClient {
|
|
348
|
+
constructor({
|
|
349
|
+
baseUrl, tokenProvider, game = 'all', games, timeoutMs = 90_000,
|
|
350
|
+
maxResponseBytes = 32 * 1024 * 1024, fetchImpl,
|
|
351
|
+
} = {}) {
|
|
352
|
+
if (!['all', 'arknights', 'endfield'].includes(game)) throw new TypeError('不支持的游戏命名空间')
|
|
353
|
+
const effectiveGames = games || (game === 'all' ? ['arknights', 'endfield'] : [game])
|
|
354
|
+
if (!Array.isArray(effectiveGames) || effectiveGames.length < 1 || effectiveGames.length > 2
|
|
355
|
+
|| new Set(effectiveGames).size !== effectiveGames.length
|
|
356
|
+
|| effectiveGames.some((item) => !['arknights', 'endfield'].includes(item))) {
|
|
357
|
+
throw new TypeError('games 只能包含不重复的 arknights / endfield')
|
|
358
|
+
}
|
|
359
|
+
this.baseUrl = normalizeCloudBaseUrl(baseUrl)
|
|
360
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 10 * 60_000) {
|
|
361
|
+
throw new TypeError('cloud timeoutMs 超出允许范围')
|
|
362
|
+
}
|
|
363
|
+
if (!Number.isInteger(maxResponseBytes) || maxResponseBytes < 1
|
|
364
|
+
|| maxResponseBytes > 64 * 1024 * 1024) {
|
|
365
|
+
throw new TypeError('cloud maxResponseBytes 超出允许范围')
|
|
366
|
+
}
|
|
367
|
+
this.tokenProvider = tokenProvider
|
|
368
|
+
this.game = game
|
|
369
|
+
this.games = [...effectiveGames]
|
|
370
|
+
this.timeoutMs = timeoutMs
|
|
371
|
+
this.maxResponseBytes = maxResponseBytes
|
|
372
|
+
this.fetchImpl = fetchImpl || fetch
|
|
373
|
+
/** @type {unknown | null} capabilities 成功结果缓存 */
|
|
374
|
+
this._capabilities = null
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** 契约版本自检;成功结果在单个 Agent 的客户端内缓存。并发首检各自响应取消。 */
|
|
378
|
+
async capabilities({ signal } = {}) {
|
|
379
|
+
if (this._capabilities) return this._capabilities
|
|
380
|
+
const response = await this.request('capabilities', 'GET', undefined, { signal })
|
|
381
|
+
if (response.data?.contract_version !== CLOUD_CONTRACT_VERSION) {
|
|
382
|
+
throw new CloudFault('CONTRACT_MISMATCH',
|
|
383
|
+
`云端检索契约版本不兼容(期望 ${CLOUD_CONTRACT_VERSION},得到 ${response.data?.contract_version})`)
|
|
384
|
+
}
|
|
385
|
+
this._capabilities = response
|
|
386
|
+
return response
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* cloud_search:注入 request_id / intent 维度字段由调用方(插件层)补齐。
|
|
391
|
+
* @param {object} arguments_ 已展开的模型参数(含 query 等)
|
|
392
|
+
*/
|
|
393
|
+
async search(arguments_, { signal } = {}) {
|
|
394
|
+
const payload = structuredClone(arguments_)
|
|
395
|
+
payload.request_id ||= `req-${randomUUID().replaceAll('-', '').slice(0, 24)}`
|
|
396
|
+
payload.games ||= [...this.games]
|
|
397
|
+
return this.request('cloud_search', 'POST', payload, { signal })
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** cloud_inspect:request_id 由按 Agent 隔离的插件证据状态注入。 */
|
|
401
|
+
async inspect(arguments_, { signal } = {}) {
|
|
402
|
+
const payload = structuredClone(arguments_)
|
|
403
|
+
return this.request('cloud_inspect', 'POST', payload, { signal })
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async request(tool, method, body = undefined, { signal } = {}) {
|
|
407
|
+
throwIfCancelled(signal)
|
|
408
|
+
const serializedBody = body === undefined ? undefined : JSON.stringify(body)
|
|
409
|
+
if (serializedBody !== undefined && Buffer.byteLength(serializedBody) > MAX_CLOUD_REQUEST_BYTES) {
|
|
410
|
+
throw new CloudFault('INVALID_REQUEST', '云端检索请求体超过客户端上限')
|
|
411
|
+
}
|
|
412
|
+
let token = await this.tokenProvider.getToken({ forceRefresh: false, signal })
|
|
413
|
+
const perform = async (authorization) => {
|
|
414
|
+
const controller = new AbortController()
|
|
415
|
+
let timedOut = false
|
|
416
|
+
const timer = setTimeout(() => {
|
|
417
|
+
timedOut = true
|
|
418
|
+
controller.abort()
|
|
419
|
+
}, this.timeoutMs)
|
|
420
|
+
const onAbort = () => controller.abort()
|
|
421
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
422
|
+
if (signal?.aborted) controller.abort()
|
|
423
|
+
try {
|
|
424
|
+
const response = await this.fetchImpl(`${this.baseUrl}${PATHS[tool]}`, {
|
|
425
|
+
method,
|
|
426
|
+
headers: { Accept: 'application/json', Authorization: authorization,
|
|
427
|
+
...(this.games.length === 1 ? { 'X-Game': this.games[0] } : { 'X-Games': this.games.join(',') }),
|
|
428
|
+
'X-Client': DSH_CLIENT_HEADER,
|
|
429
|
+
...(body ? { 'Content-Type': 'application/json' } : {}) },
|
|
430
|
+
...(serializedBody !== undefined ? { body: serializedBody } : {}),
|
|
431
|
+
redirect: 'error',
|
|
432
|
+
signal: controller.signal,
|
|
433
|
+
})
|
|
434
|
+
const bytes = await readResponseBytes(response, this.maxResponseBytes)
|
|
435
|
+
return { response, bytes }
|
|
436
|
+
} catch (error) {
|
|
437
|
+
if (signal?.aborted) throw new CloudFault('CANCELLED', '云端检索已取消')
|
|
438
|
+
if (error instanceof CloudFault) throw error
|
|
439
|
+
if (timedOut || error?.name === 'AbortError') {
|
|
440
|
+
throw new CloudFault('CLOUD_TIMEOUT', '云端检索超过客户端等待期限', true)
|
|
441
|
+
}
|
|
442
|
+
throw new CloudFault('CLOUD_UNAVAILABLE', '无法连接云端检索服务', true,
|
|
443
|
+
{ exception_type: error?.constructor?.name || typeof error })
|
|
444
|
+
} finally {
|
|
445
|
+
clearTimeout(timer)
|
|
446
|
+
signal?.removeEventListener('abort', onAbort)
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
let result = await perform(token)
|
|
450
|
+
// 本地缓存的 JWT 可能比服务端有效期短:401 时强制刷新一次重试,
|
|
451
|
+
// 避免过期 token 让整轮 agent 的云端工具全部失效。
|
|
452
|
+
if (result.response.status === 401) {
|
|
453
|
+
const refreshed = await this.tokenProvider.getToken({ forceRefresh: true, signal })
|
|
454
|
+
if (refreshed && refreshed !== token) {
|
|
455
|
+
token = refreshed
|
|
456
|
+
result = await perform(token)
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const { response, bytes } = result
|
|
460
|
+
let parsed
|
|
461
|
+
try {
|
|
462
|
+
parsed = JSON.parse(new TextDecoder().decode(bytes))
|
|
463
|
+
} catch {
|
|
464
|
+
throw new CloudFault('INVALID_RESPONSE', '云端检索返回的不是有效 JSON')
|
|
465
|
+
}
|
|
466
|
+
if (!response.ok) throw httpFault(response.status, parsed)
|
|
467
|
+
if (parsed?.code !== 200) throw new CloudFault('INVALID_RESPONSE', '云端检索返回无效响应')
|
|
468
|
+
return parsed
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export function httpFault(status, body) {
|
|
473
|
+
const mapping = { 401: ['AUTH_REQUIRED', false], 403: ['ACCESS_DENIED', false],
|
|
474
|
+
404: ['TRACE_NOT_FOUND', false], 409: ['REQUEST_CONFLICT', true],
|
|
475
|
+
413: ['RESPONSE_TOO_LARGE', false], 422: ['INVALID_REQUEST', false],
|
|
476
|
+
429: ['CLOUD_BUSY', true], 503: ['CLOUD_UNAVAILABLE', true], 504: ['CLOUD_TIMEOUT', true] }
|
|
477
|
+
const [code, retryable] = mapping[status] || ['CLOUD_ERROR', status >= 500]
|
|
478
|
+
const rawMessage = typeof body?.detail === 'string' ? body.detail
|
|
479
|
+
: Array.isArray(body?.detail)
|
|
480
|
+
? body.detail.slice(0, 20).map((item) =>
|
|
481
|
+
`${(Array.isArray(item?.loc) ? item.loc : []).slice(0, 12).join('.')}: ${item?.msg || '参数无效'}`).join(';')
|
|
482
|
+
: '云端检索请求失败'
|
|
483
|
+
const message = remoteMessage(rawMessage, '云端检索请求失败')
|
|
484
|
+
return new CloudFault(code, message, retryable, { http_status: status,
|
|
485
|
+
...(body?.detail ? { validation: body.detail } : {}) })
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** ---- 插件层薄封装:错误 → 契约 error 响应 ---- */
|
|
489
|
+
|
|
490
|
+
export function cloudErrorResponse(error) {
|
|
491
|
+
if (error instanceof CloudFault) {
|
|
492
|
+
return { contract_version: CLOUD_CONTRACT_VERSION, status: 'error',
|
|
493
|
+
error: { code: error.code, message: error.message, retryable: error.retryable,
|
|
494
|
+
...(error.details ? { details: error.details } : {}) } }
|
|
495
|
+
}
|
|
496
|
+
return { contract_version: CLOUD_CONTRACT_VERSION, status: 'error',
|
|
497
|
+
error: { code: 'CLOUD_INTERNAL_ERROR', message: '云端检索执行失败;详情请查看 DSH Host 日志', retryable: false } }
|
|
498
|
+
}
|