@realsee/vr-cli 0.1.4 → 0.1.5
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/README.md +11 -15
- package/agent-assets/AGENTS.md +5 -4
- package/dist/cli.mjs +4 -4
- package/package.json +1 -1
- package/skills/realsee-vr-app/references/five-runtime.md +2 -2
- package/skills/realsee-vr-app/scripts/verify-project.mjs +10 -0
- package/templates/base/.env.example +4 -2
- package/templates/base/README.md +7 -6
- package/templates/base/package.json +3 -1
- package/templates/base/scripts/setup-config.mjs +179 -0
- package/templates/base/scripts/setup-config.test.mjs +83 -0
- package/templates/base/scripts/setup.mjs +49 -0
- package/templates/base/server/app.ts +3 -1
- package/templates/base/server/config.ts +11 -1
- package/templates/base/server/index.ts +5 -1
- package/templates/base/server/types.ts +2 -1
- package/templates/base/server/work-route.ts +40 -32
- package/templates/base/server/work-service.ts +2 -2
- package/templates/base/src/vr/work/WorkSource.ts +13 -9
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ CI 和自动化生成流程应使用明确版本。当前版本示例:
|
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
43
|
npm exec --yes \
|
|
44
|
-
--package=@realsee/vr-cli@0.1.
|
|
44
|
+
--package=@realsee/vr-cli@0.1.5 \
|
|
45
45
|
-- vr-cli create ./my-vrapp --no-install
|
|
46
46
|
|
|
47
47
|
cd ./my-vrapp
|
|
@@ -52,20 +52,16 @@ npm run build
|
|
|
52
52
|
|
|
53
53
|
## 快速开始
|
|
54
54
|
|
|
55
|
-
`create` 默认在生成目录中执行 `npm ci
|
|
55
|
+
`create` 默认在生成目录中执行 `npm ci`。项目创建完成后进入目录,运行固定的本地配置入口:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
58
|
cd ./my-vrapp
|
|
59
|
-
|
|
59
|
+
npm run setup
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
在 Realsee China Portal 的 onboarding 页面完成组织、开发者应用和空间项目选择,点击“复制本地配置”,将单行 `realsee-setup:v1:...` 配置包粘贴到隐藏输入中。配置包由 UTF-8 JSON 经 Base64URL 编码,兼容 Windows、macOS 和 Linux;Base64URL 仅提供稳定的单行传输格式,不是加密。不要把配置包放入聊天、命令参数、管道、重定向、Shell 历史、源码或日志。
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
REALSEE_GATEWAY_ENDPOINT=https://app-gateway.realsee.cn
|
|
66
|
-
REALSEE_GATEWAY_APP_KEY=<your-app-key>
|
|
67
|
-
REALSEE_GATEWAY_APP_SECRET=<your-app-secret>
|
|
68
|
-
```
|
|
64
|
+
`npm run setup` 校验 AK、SK 和 `resourceCode` 后,原子写入 gitignored 的服务端 `.env.development`。POSIX 系统使用 `0600`,Windows 使用当前用户 ACL。首次接入未成功时可以重新运行同一命令并完整覆盖本地配置;首次运行后的凭据轮换和默认项目切换属于独立管理流程。
|
|
69
65
|
|
|
70
66
|
启动开发环境:
|
|
71
67
|
|
|
@@ -73,10 +69,10 @@ REALSEE_GATEWAY_APP_SECRET=<your-app-secret>
|
|
|
73
69
|
npm run dev
|
|
74
70
|
```
|
|
75
71
|
|
|
76
|
-
|
|
72
|
+
访问以下地址,Starter 会使用 Setup 保存的首次运行项目:
|
|
77
73
|
|
|
78
74
|
```text
|
|
79
|
-
http://127.0.0.1:8087/
|
|
75
|
+
http://127.0.0.1:8087/
|
|
80
76
|
```
|
|
81
77
|
|
|
82
78
|
开发环境由 Vite 提供浏览器应用,并将 `/api` 请求代理至 Fastify BFF。使用 `--no-install` 创建项目时,需要先在生成目录中执行 `npm ci`。
|
|
@@ -189,15 +185,15 @@ node --version
|
|
|
189
185
|
nvm use
|
|
190
186
|
```
|
|
191
187
|
|
|
192
|
-
###
|
|
188
|
+
### 开发服务提示本地配置未完成
|
|
193
189
|
|
|
194
|
-
|
|
190
|
+
运行 `npm run setup`,从 Portal 一次复制并粘贴完整配置。配置完成后重新启动开发服务。
|
|
195
191
|
|
|
196
192
|
### Work 加载失败
|
|
197
193
|
|
|
198
194
|
依次确认:
|
|
199
195
|
|
|
200
|
-
1.
|
|
196
|
+
1. Setup 中选择的 `resourceCode` 正确;
|
|
201
197
|
2. 开放平台应用有权访问对应资源;
|
|
202
198
|
3. `REALSEE_GATEWAY_ENDPOINT` 配置正确;
|
|
203
199
|
4. BFF 日志中的上游 HTTP 状态符合预期;
|
|
@@ -207,7 +203,7 @@ nvm use
|
|
|
207
203
|
|
|
208
204
|
- npm 包:`@realsee/vr-cli`。
|
|
209
205
|
- 可执行命令:`vr-cli`。
|
|
210
|
-
- 当前版本:`0.1.
|
|
206
|
+
- 当前版本:`0.1.5`。
|
|
211
207
|
|
|
212
208
|
`0.x` 版本可能调整模板、检查规则和生成内容。生产环境与 CI 应固定 CLI 版本,并在升级时审查生成差异。
|
|
213
209
|
|
package/agent-assets/AGENTS.md
CHANGED
|
@@ -6,6 +6,7 @@ This project was generated by `@realsee/vr-cli` {{TOOL_VERSION}}.
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npm ci
|
|
9
|
+
npm run setup
|
|
9
10
|
npm run dev
|
|
10
11
|
npm run typecheck
|
|
11
12
|
npm run lint
|
|
@@ -91,9 +92,9 @@ Every direct dependency except `three` must use an open `>=` minimum no lower th
|
|
|
91
92
|
- `Mapview` is the three-dimensional overview; `Topview` is the map/floor structure.
|
|
92
93
|
- Never directly edit `work.json`, downloaded Work, or customer Work. The default Viewer is read-only; persistence needs an authoritative editor/API/BFF/source contract and user confirmation. Otherwise propose a typed patch or keep the feature runtime-only.
|
|
93
94
|
- Use only Work-provided, approved API/CDN or user-authorized media/model assets; never introduce an unrelated third-party URL.
|
|
94
|
-
- Resolve Work
|
|
95
|
-
- The generated Fastify BFF is an example adapter: `realsee-open-api.ts` owns token exchange/cache and upstream calls, `work-service.ts` owns the framework-neutral envelope, and `work-route.ts` owns HTTP. Preserve `GET /api/work
|
|
96
|
-
- The generated project has one `.env.example`; only the server reads `REALSEE_GATEWAY_ENDPOINT`, `REALSEE_GATEWAY_APP_KEY` and `
|
|
95
|
+
- Resolve the onboarding Work from `/` through same-origin `/api/work`, backed by the server-only `REALSEE_DEFAULT_RESOURCE_CODE`. Keep `/{resourceCode}` and `/api/work/{resourceCode}` only as explicit development compatibility routes. Never add `VITE_WORK_URL`, a local `/work.json` fallback or browser-side OpenAPI credentials.
|
|
96
|
+
- The generated Fastify BFF is an example adapter: `realsee-open-api.ts` owns token exchange/cache and upstream calls, `work-service.ts` owns the framework-neutral envelope, and `work-route.ts` owns HTTP. Preserve `GET /api/work` when replacing it with Go or another backend.
|
|
97
|
+
- The generated project has one `.env.example`; `npm run setup` is the only first-onboarding writer for `.env.development`. It accepts one hidden, versioned Portal payload containing AK, SK and `resourceCode`. Only the server reads `REALSEE_GATEWAY_ENDPOINT`, `REALSEE_GATEWAY_APP_KEY`, `REALSEE_GATEWAY_APP_SECRET` and `REALSEE_DEFAULT_RESOURCE_CODE`. `APP_ID` and `REALSEE_SOURCE` are optional deployment metadata.
|
|
97
98
|
- Keep the real `REALSEE_GATEWAY_APP_SECRET`, AK/SK and token in the BFF environment or secret manager. They must not enter `VITE_*`, `src/`, Vite `define/envPrefix`, browser requests, logs or bundles.
|
|
98
99
|
- AK/SK authorize the BFF's upstream access. If an internet-facing deployment requires caller or tenant authorization, enforce it before the Work route; never satisfy that requirement by exposing AK/SK to the browser.
|
|
99
100
|
- Construct and append Five only after its host has positive dimensions; use `five.refresh` for later positive resizes.
|
|
@@ -103,7 +104,7 @@ Every direct dependency except `three` must use an open `>=` minimum no lower th
|
|
|
103
104
|
|
|
104
105
|
## Validation
|
|
105
106
|
|
|
106
|
-
Run type checking and production build. For spatial changes, validate at `http://127.0.0.1:8087
|
|
107
|
+
Run type checking and production build. For spatial changes, validate at `http://127.0.0.1:8087/` in a real browser; the template uses local IPv4 loopback and non-privileged port 8087, not privileged port 517. Inspect Five state/readiness, native surface dimensions, spatial-tag anchors/renderers, Three coordinates/resources, plugin lifecycle, layer rectangles and hit targets rather than relying only on screenshots.
|
|
107
108
|
|
|
108
109
|
## Model floorplan contract
|
|
109
110
|
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{parseArgs as Mr}from"node:util";var Y="@realsee/vr-cli";var F="https://registry.npmjs.org/",N="public";var j="0.1.
|
|
2
|
-
`),n&&await ir(i),{targetRoot:i,packageName:o,installedAgentAssets:f,installed:n}}import{execFile as nr}from"node:child_process";import{readFile as E,readdir as lr,stat as cr}from"node:fs/promises";import l from"node:path";import{promisify as pr}from"node:util";var ur=pr(nr),s=(r,t,e,a)=>({level:r,code:t,message:e,...a?{detail:a}:{}}),_e=r=>/(?:^|_)(?:AK|SK|SECRET|TOKEN|PASSWORD|CREDENTIALS?)(?:_|$)/i.test(r),fr=Object.freeze([/\bAPP_ID\b/,/\bREALSEE_(?:GATEWAY_[A-Z0-9_]+|SOURCE|ACCESS_TOKEN)\b/,/\b(?:VITE_)?(?:AK|SK|ACCESS_TOKEN)\b/]),ee=r=>{for(let t of fr){let e=t.exec(r);if(e)return e[0]}return null},y=r=>r.replace(/\/\*[\s\S]*?\*\//g,"").replace(/(^|[^:])\/\/.*$/gm,"$1"),dr=Object.freeze([{pattern:/three\/addons\//,message:"r117 \u6269\u5C55\u6A21\u5757\u5FC5\u987B\u4ECE three/examples/jsm \u5BFC\u5165\u3002"},{pattern:/three\/tsl\b/,message:"Three r117 \u4E0D\u63D0\u4F9B\u73B0\u4EE3 TSL \u5165\u53E3\u3002"},{pattern:/\b(?:SRGBColorSpace|LinearSRGBColorSpace|NoColorSpace|CapsuleGeometry|WebGPURenderer)\b/,message:"\u6E90\u7801\u4F7F\u7528\u4E86 Three r117 \u4E0D\u5B58\u5728\u7684\u73B0\u4EE3\u7B26\u53F7\u3002"},{pattern:/\b(?:renderer|[\w$]*Renderer)\.outputColorSpace\b/,message:"Three r117 \u4F7F\u7528 renderer.outputEncoding\u3002"},{pattern:/\b(?:texture|[\w$]*(?:map|Map|texture|Texture))\.colorSpace\s*=/,message:"Three r117 \u7EB9\u7406\u4F7F\u7528 encoding\uFF0C\u800C\u4E0D\u662F colorSpace\u3002"}]),mr=Object.freeze(["from '@realsee/five'","from 'three'","FEATURE_OWNED_MESH_PLUGIN_NAME","example.featureOwnedMesh","new Group(","new BoxBufferGeometry(","new MeshBasicMaterial(","new Mesh(","five.scene.add(root)","root.parent?.remove(root)","five.needsRender = true","geometry.dispose()","material.dispose()","plugins.register(FEATURE_OWNED_MESH_PLUGIN_NAME, plugin)","plugin.dispose()"]),vr=Object.freeze(["new Five(","new WebGLRenderer(","renderer.render(","requestAnimationFrame(","five.dispose(","three/addons/"]),gr=Object.freeze(["Image resources use approved Realsee CDN domains only; never use `ljcdn.com`, `lianjia.com` or their subdomains.","Prefer Five `defaultImageURLTransform` over hand-built \u6570\u636E\u4E07\u8C61 query strings when its installed contract satisfies the request.","URL transformation does not grant CORS access.","Ordinary images use the deployment's approved image-4 host.","Feature media from OpenAPI or Work data is displayed through a derived sized variant, never the original: pass ordinary images through the reviewed `defaultImageURLTransform` adapter with a bounded `size`, and do not hand a full-resolution URL to a tag, tour, card or DOM thumbnail."]),hr=Object.freeze(["When the user explicitly requests Vapor components, use matching named exports from the installed `@realsee/vapor` package for every applicable DOM control; importing `Vapor` or `vapor.css` alone does not satisfy the request.","Do not replace an available Vapor component with raw HTML or a locally styled replica; use raw DOM only for semantics or capabilities absent from the installed Vapor declarations, and record that gap.","Before implementing Vapor UI, read `node_modules/@realsee/vapor/AI_CONTEXT.md`, `llms.txt`, `vapor.d.ts`, and the selected component declarations; never invent component names or props.","Use the installed `ai_guides` only for component discovery and composition recipes; the target project's root `vapor.d.ts` and selected component declarations are the API contract, and conflicts resolve in favor of installed declarations.","Keep the Five canvas in its dedicated sibling host outside `Vapor`; Vapor layout components own DOM chrome only, even when an external recipe nests a placeholder VR component inside a stack."]),wr=Object.freeze(["The current generated baseline is `@realsee/vapor@0.2.12`.","The 0.2.12 baseline requires exactly one executable `@realsee/vapor/vapor.css` import, not `vapor.scss`","Choose by product semantics before styling:","`HStack` arranges DOM horizontally, `VStack` vertically and `ZStack` overlays DOM children.","The 0.2.12 match union is exact:","if both represent the same physical inset, de-duplicate rather than adding it twice."]),Er=Object.freeze(["For an explicit Vapor request, executable source must import and render the matching non-provider Vapor component; the provider and stylesheet alone are insufficient.","exactly one executable `@realsee/vapor/vapor.css` import and no `vapor.scss` or copied internal stylesheet;","the `Vapor` provider rectangle matches the DOM overlay host","safe-area behavior uses the declared source/merge rule without double-counting","correct portal order, focus entry/return, Escape/backdrop behavior and close reasons;","no application stylesheet overrides `.vapor-*` internals","typecheck validates every component prop, compound child and symbol"]),Sr=/\bimport\s*['"]@realsee\/vapor\/vapor\.css['"]\s*;?/,Ae=r=>r.match(new RegExp(Sr.source,"g"))?.length??0,z=(r,t)=>new RegExp(`\\bimport\\s*\\{[^}]*\\b${t}\\b[^}]*\\}\\s*from\\s*['"]@realsee/vapor['"]`).test(r),U=(r,t)=>new RegExp(`<${t}(?:\\s|/?>)`).test(r),yr=Object.freeze(["Spatial capability resolution order: installed @realsee/dnalogel preset -> native Five -> Three.js.","Tags, hotspots, and anchors default to PanoTagPlugin with GET /open/v3/hot/list.","Normalize optional OpenAPI configuration fields such as `version`, `globalConfig` and `contentTypeConfig`: treat `null` as absent before validating the installed SDK contract.","Load ordinary PanoTag point/plane tags after `five.load` without waiting for `five.model.loaded`; gate only model-dependent tag content or anchors on model readiness.","Tours use Five State snapshots with GET /open/v3/observerpicture/search.","Model annotations default to ModelMakerPlugin with GET /open/v3/annotation/search.","Routes use GuideLinePlugin for rendering and shape-selected CruisePlugin or MovePlugin playback with GET /open/v3/route/search.","Tour adapters read `data.list`, map each validated record to `Partial<State>` with the active `workCode`, and never cast an observer-picture record directly to `State`.","Model annotation adapters read `data.list`, validate `object_data` for `box`, `triangles` or `prism`, and pass `{ list }` to ModelMaker only for a model-capable Work.","Feature API, schema, or plugin failures disable only the affected feature; keep the base Viewer usable and never runtime-fallback to raw data or a custom Three implementation.","Scope tour, ModelMaker, GuideLine, Cruise and Move work to an active `workCode`/generation token; abort fetches, ignore stale continuations, pause the owned playback controller, remove hooks, clear GuideLine data, wait for pending loads, then let identity-safe unregister dispose each registered controller exactly once before `five.dispose()`.","SDK-shaped custom data requires an explicit developer opt-out from Open Platform data.","VR spatial tag (spatial annotation) is the umbrella concept","Classify content, renderer, anchor, and lifecycle/data ownership","Never infer PanoTag from","Never directly edit `work.json`, downloaded Work, or customer Work"]),R=Object.freeze(["dnalogel 3.81.2 route playback is shape-selected: a validated pano group uses `CruisePlugin` `{ panoIndexList }`, while a validated model-space curve uses `MovePlugin` `{ path }`; `GuideLinePlugin` renders route geometry and is not playback input.","`CruisePlugin` accepts exactly one of `panoIndexList` or `keyframes`; its installed union has no type discriminator.","OpenAPI route records are never passed raw: rename validated style config only for GuideLine `lines`, and never pass GuideLine `PathItem` arrays or style objects to `CruisePlugin` or `MovePlugin`.","If a source record contains both `pano_group` and `path`, the route adapter must choose product semantics explicitly and pass only the selected geometry branch to GuideLine.","`GuideLinePlugin.load` waits for model readiness; `CruisePlugin` waits only for Work and observers; `MovePlugin` enters Model and awaits Five readiness when playback starts.","Reject empty routes, unknown pano indexes, unsupported curves, non-finite coordinates, timing or speed, and ambiguous multi-segment Move paths before plugin load.","Route cleanup pauses the owned playback controller before hook removal and disposal; `clear` belongs to GuideLine, is not a public Cruise operation, and GuideLine disposal does not own an external or embedded Cruise controller automatically.","Do not invent `RoutePlugin`, `PanoRove`, `stop()`, or a universal route controller; use only the exports and methods declared by the installed package."]),Oe=Object.freeze(["Floorplans default to ModelFloorplanPlugin with GET /open/v3/floorplan/plugins.","`data.format_floorplan_url`, not `format_model_url`, resolves to the preferred `{ version, data: FloorplanServerData }` plugin input.","Expose `capabilities.modelFloorplan` only after validating the complete wrapper and nested floorplan shape; missing, empty, malformed or failed floorplan data must leave the base Viewer usable.","The default tabs are `\u6F2B\u6E38` for `Panorama` and `\u4E09\u7EF4\u6A21\u578B` for `Mapview`; do not expose `Topview` as `\u5730\u56FE`.","Add the `\u5E73\u9762\u56FE` tab only after the capability validates and `ModelFloorplanPlugin.load` succeeds.","Configure `ModelFloorplanPlugin` with `autoShowEnable: false`; the application tab owns explicit `show()`/`hide()`, uses `modelOpacity: 0` while the floorplan is visible, and restores the Five model surface on hide, failure, and disposal.","Resolve the Five canvas parent and call `ModelFloorplanPlugin.appendTo(wrapper)` before `load`; `five.addExtraElement` registers input handling but does not mount the plugin DOM.","For multi-floor Work, ModelFloorplanPlugin and Five pano/model floor synchronization are the floor source of truth; do not invent a React-only `floorIndex`.","Floorplan show/hide, hooks, pending load and identity-safe disposal must finish before `five.dispose()`."]),br=Object.freeze(Oe.slice(0,3)),kr="The complete model floorplan contract lives in",_r="The complete route playback contract lives in",Ar=Object.freeze([R[0],R[7],_r]),Te=Object.freeze(["Vapor","Text","TitleBar","ZStack","VStack","Layer"]),Re=Object.freeze(["Button","Font","HStack","VStack"]),je=Object.freeze(["title?: string | null",'className="viewer-vapor"','width="100%"','height="100%"','theme="arc_beauty"',"useSaveAreaInsets()","useDimension()","<Layer vignetting />","labelMaxWidth={titleMaxWidth}",'className="viewer-controls"',"align={{ horizontal: 'center' }}",'className="viewer-status"','appearance="solid"',"role={error ? 'alert' : 'status'}","aria-live={error ? 'assertive' : 'polite'}","env(safe-area-inset-${side}, 0px)"]),Pe=Object.freeze(['role="group"',"useDimension()","width={controlWidth}","height={compact ? '3rem' : '5rem'}","align={{ horizontal: 'space-around' }}",'appearance="shadow"',"weight={selected ? 'bold' : 'regular'}","opacity={selected ? '100' : '80'}",'className="mode-switcher-indicator"',"onClick={() => void changeTab(option.value)}"]),Le=Object.freeze([".dom-overlay",".viewer-controls",".mode-switcher","pointer-events: auto",".mode-switcher-indicator","bottom: -0.5rem","width: 3.375rem","height: 0.125rem","background-image: linear-gradient(",".viewer-status"]),Ne=Object.freeze(["viewer-glass",'className="viewer-topbar"',"<h1"]),xe=Object.freeze(["Segmented","viewer-segmented","viewer-glass",'appearance="horizontal"','maxWidth="100%"']),Me=Object.freeze(["--viewer-glass-","--viewer-control-size",".vapor-segmented","backdrop-filter"]);async function re(r){return JSON.parse(await E(r,"utf8"))}async function Or(){let r=process.platform==="win32"?"npm.cmd":"npm",{stdout:t}=await ur(r,["--version"],{encoding:"utf8"});return t.trim()}async function $e(r,t){let e=[];for(let[n,i,o]of[["packageManager",t.packageManager,se],["engines.node",t.engines?.node,te],["engines.npm",t.engines?.npm,ae]])i!==o&&e.push(s("error","toolchain.version",`package.json ${n} \u5FC5\u987B\u4E3A ${o}\uFF0C\u5F53\u524D\u4E3A ${i??"\u672A\u58F0\u660E"}`));for(let n of[".nvmrc",".node-version"]){let i=l.join(r,n);if(await u(i)){let o=(await E(i,"utf8")).trim();o!==B&&e.push(s("error","toolchain.node-pin",`${n} \u5FC5\u987B\u56FA\u5B9A Node ${B}\uFF0C\u5F53\u524D\u4E3A ${o||"\u7A7A"}`))}}let a=l.join(r,".npmrc");if(await u(a)){let n=new Set((await E(a,"utf8")).split(/\r?\n/).map(i=>i.trim()).filter(Boolean));for(let i of["engine-strict=true","package-lock=true","save-prefix=>="])n.has(i)||e.push(s("error","toolchain.npmrc",`.npmrc \u5FC5\u987B\u5305\u542B ${i}`))}return e}async function Tr(r,t){let e=l.join(r,"package-lock.json");if(!await u(e))return[];try{let a=await re(e),n=a.packages?.[""],i=[];if(a.lockfileVersion!==3||!n)return[s("error","lockfile.invalid","package-lock.json \u5FC5\u987B\u4F7F\u7528 lockfileVersion 3 \u5E76\u5305\u542B\u6839\u5305\u8BB0\u5F55\u3002")];(a.name!==t.name||n.name!==t.name||n.version!==t.version)&&i.push(s("error","lockfile.package","package-lock.json \u6839\u5305\u540D\u79F0\u6216\u7248\u672C\u4E0E package.json \u4E0D\u4E00\u81F4\u3002"));for(let[o,m,f,p]of[["dependency",n.dependencies,t.dependencies,G],["dev-dependency",n.devDependencies,t.devDependencies,q]])for(let[w,b]of Object.entries(f??{})){m?.[w]!==b&&i.push(s("error",`lockfile.${o}`,`package-lock.json \u4E0E package.json \u4E0D\u4E00\u81F4\uFF1A${w}@${b}\uFF1B\u8BF7\u8FD0\u884C npm run deps:relock\u3002`));let k=p[w];if(!k)continue;let c=a.packages?.[`node_modules/${w}`]?.version;if(!c){i.push(s("error","lockfile.resolved",`package-lock.json \u7F3A\u5C11\u76F4\u63A5\u4F9D\u8D56 ${w} \u7684\u89E3\u6790\u7248\u672C\u3002`));continue}if(w in V){c!==k&&i.push(s("error","lockfile.locked",`package-lock.json \u5FC5\u987B\u5C06 ${w} \u89E3\u6790\u4E3A ${k}\uFF0C\u5F53\u524D\u4E3A ${c}\u3002`));continue}let d=H(b);d&&(!P(c,k)||!P(c,d))&&i.push(s("error","lockfile.minimum",`${w} \u5728 package-lock.json \u4E2D\u89E3\u6790\u4E3A ${c}\uFF0C\u5FC5\u987B\u540C\u65F6\u6EE1\u8DB3\u751F\u6210\u5668\u57FA\u7EBF >=${k} \u4E0E\u58F0\u660E ${b}\u3002`))}return i}catch(a){return[s("error","lockfile.invalid","package-lock.json \u65E0\u6CD5\u89E3\u6790\u3002",String(a))]}}async function x(r){let t=[];for(let e of await lr(r,{withFileTypes:!0})){let a=l.join(r,e.name);e.isDirectory()?t.push(...await x(a)):e.isFile()&&/\.(?:[cm]?[jt]sx?)$/.test(e.name)&&t.push(a)}return t}async function Fe(r,t){let e=l.join(r,t);if(!await u(e))return[];let a=y(await E(e,"utf8")),n=[];for(let m of mr)a.includes(m)||n.push(s("error","architecture.custom-plugin-example",`${t} \u7F3A\u5C11\u81EA\u5B9A\u4E49\u63D2\u4EF6\u6240\u6709\u6743\u6807\u8BB0\uFF1A${m}`));for(let m of vr)a.includes(m)&&n.push(s("error","architecture.custom-plugin-example",`${t} \u7981\u6B62\u62E5\u6709 Five \u8FD0\u884C\u65F6\u6216\u7B2C\u4E8C\u6E32\u67D3\u5FAA\u73AF\uFF1A${m}`));let i=t.startsWith("templates/")?"templates/viewer/src/features/tags/PanoTagFeature.ts":"src/features/tags/PanoTagFeature.ts",o=l.join(r,i);if(await u(o)){let m=y(await E(o,"utf8"));for(let f of["from '@realsee/dnalogel/libs/PanoTagPlugin'","const PLUGIN_NAME = 'dnalogel.panoTag'","const candidate = PanoTagPlugin(","plugins.register(PLUGIN_NAME, candidate)","candidate.dispose()","async load(panoTagList: Tags['tagList'])","plugin.load({ tagList: panoTagList })","const disposeError = plugins.unregister(PLUGIN_NAME, plugin)"])m.includes(f)||n.push(s("error","architecture.pano-tag-contract",`${i} \u7F3A\u5C11 PanoTag \u517C\u5BB9\u6027\u3001\u663E\u5F0F\u7C7B\u578B\u6216\u6240\u6709\u6743\u6807\u8BB0\uFF1A${f}`));for(let f of["model_file","as Tags['tagList']","from '../../vr/work/WorkSource'"])m.includes(f)&&n.push(s("error","architecture.pano-tag-contract",`${i} \u542B\u975E\u6807\u51C6 content type \u6216\u672A\u9A8C\u8BC1\u7684 PanoTag \u8F6C\u6362\uFF1A${f}`))}return n}async function Ie(r,t){let e=[];for(let a of t){let n=l.join(r,a);if(!await u(n))continue;let i=await E(n,"utf8");for(let o of gr)i.includes(o)||e.push(s("error","agents.image-resource-guidance",`${a} \u7F3A\u5C11\u56FE\u7247\u8D44\u6E90\u57DF\u540D\u3001Five URL transform\u3001\u6570\u636E\u4E07\u8C61\u6216 CORS \u89C4\u5219\uFF1A${o}`))}return e}async function Ce(r,t){let e=[];for(let a of t){let n=l.join(r,a);if(!await u(n))continue;let i=await E(n,"utf8");for(let o of hr)i.includes(o)||e.push(s("error","agents.vapor-guidance",`${a} \u7F3A\u5C11 Vapor \u771F\u5B9E\u7EC4\u4EF6\u9009\u62E9\u3001\u58F0\u660E\u6838\u5BF9\u6216 raw DOM \u4F8B\u5916\u89C4\u5219\uFF1A${o}`))}return e}async function De(r,t,e){let a=[];for(let[n,i,o,m]of[[t,wr,"agents.vapor-reference","\u7248\u672C\u5316\u8D44\u6599\u8DEF\u7531\u3001\u7EC4\u4EF6\u9009\u578B\u3001\u5E03\u5C40\u6216 safe-area \u89C4\u5219"],[e,Er,"agents.vapor-acceptance","\u8FD0\u884C\u65F6 Vapor \u9A8C\u6536\u89C4\u5219"]]){let f=l.join(r,n);if(!await u(f))continue;let p=await E(f,"utf8");for(let w of i)p.includes(w)||a.push(s("error",o,`${n} \u7F3A\u5C11 ${m}\uFF1A${w}`))}return a}async function Ge(r,t,e){let a=[];for(let i of t){let o=l.join(r,i);if(!await u(o))continue;let m=await E(o,"utf8");for(let f of yr)m.includes(f)||a.push(s("error","agents.spatial-tag-guidance",`${i} \u7F3A\u5C11\u7A7A\u95F4\u6807\u7B7E\u51B3\u7B56\u6216\u53EA\u8BFB\u89C4\u5219\uFF1A${f}`))}let n=l.join(r,e);if(await u(n)){let i=await E(n,"utf8");for(let o of["Spatial capability resolution order: installed @realsee/dnalogel preset -> native Five -> Three.js.","Tags, hotspots, and anchors default to PanoTagPlugin with GET /open/v3/hot/list.","Normalize optional OpenAPI configuration fields such as `version`, `globalConfig` and `contentTypeConfig`: treat `null` as absent before validating the installed SDK contract.","Load ordinary PanoTag point/plane tags after `five.load` without waiting for `five.model.loaded`; gate only model-dependent tag content or anchors on model readiness.","Tours use Five State snapshots with GET /open/v3/observerpicture/search.","Model annotations default to ModelMakerPlugin with GET /open/v3/annotation/search.","Routes use GuideLinePlugin for rendering and shape-selected CruisePlugin or MovePlugin playback with GET /open/v3/route/search.","Tour adapters read `data.list`, map each validated record to `Partial<State>` with the active `workCode`, and never cast an observer-picture record directly to `State`.","Model annotation adapters read `data.list`, validate `object_data` for `box`, `triangles` or `prism`, and pass `{ list }` to ModelMaker only for a model-capable Work.","Feature API, schema, or plugin failures disable only the affected feature; keep the base Viewer usable and never runtime-fallback to raw data or a custom Three implementation.","Scope tour, ModelMaker, GuideLine, Cruise and Move work to an active `workCode`/generation token; abort fetches, ignore stale continuations, pause the owned playback controller, remove hooks, clear GuideLine data, wait for pending loads, then let identity-safe unregister dispose each registered controller exactly once before `five.dispose()`.","SDK-shaped custom data requires an explicit developer opt-out from Open Platform data.","`model_file` is not canonical","`ModelTVVideoPlugin`","`CSS3DRenderPlugin.create3DDomContainer`","`PanoVideoPlugin`","generic `tags`, `tagList` and `tag_list`","`ServerData` with `box`, `triangles` or `prism` items"])i.includes(o)||a.push(s("error","agents.spatial-tag-capabilities",`${e} \u7F3A\u5C11\u7A7A\u95F4\u80FD\u529B\u4F18\u5148\u7EA7\u3001\u6570\u636E\u6E90\u6216\u63D2\u4EF6\u7C7B\u578B\u8DEF\u7531\uFF1A${o}`))}return a}async function Ve(r,t,e){let a=[],n=[...t.map(i=>[i,i.endsWith("SKILL.md")?Ar:R]),[`${e}/references/dnalogel-api-capabilities.md`,R],[`${e}/references/five-runtime.md`,R.slice(4,6)],[`${e}/references/plugin-lifecycle.md`,R.slice(6,8)],[`${e}/references/official-docs.md`,R.slice(2,4)]];for(let[i,o]of n){let m=l.join(r,i);if(!await u(m))continue;let f=await E(m,"utf8");for(let p of o)f.includes(p)||a.push(s("error","agents.route-guidance",`${i} \u7F3A\u5C11\u70B9\u4F4D/\u81EA\u5B9A\u4E49\u8DEF\u7EBF\u5206\u6D41\u3001\u6821\u9A8C\u6216\u751F\u547D\u5468\u671F\u89C4\u5219\uFF1A${p}`))}return a}async function Ke(r,t,e){let a=[],n=[[`${e}/references/dnalogel-api-capabilities.md`,Oe],...t.map(i=>[i,br]),...["five-runtime.md","official-docs.md","plugin-lifecycle.md","ui-and-validation.md"].map(i=>[`${e}/references/${i}`,[kr]])];for(let[i,o]of n){let m=l.join(r,i);if(!await u(m))continue;let f=await E(m,"utf8");for(let p of o)f.includes(p)||a.push(s("error","agents.floorplan-guidance",`${i} \u7F3A\u5C11\u5E73\u9762\u56FE endpoint\u3001\u6821\u9A8C\u3001\u52A8\u6001 tab\u3001\u591A\u697C\u5C42\u6216\u6E05\u7406\u89C4\u5219\uFF1A${p}`))}return a}async function We(r,t){let e=[],a=[[t.validator,["export function asModelFloorplanServerData(","!isInteger(value.version)","value.version < 0","!isFloorplanServerData(value.data)","value.outlines.length > 0","value.floor_datas.length > 0","isArrayOf(value.rooms, isRoom)","isRules(value.rules)","value.entrance === null || isEntrance(value.entrance)","isBounding(value.bounding)","isArrayOf(value.observers, isObserver)","isArrayOf(value.doors, isDoor)"]],[t.types,["WorkCapabilities","modelFloorplan?: ModelFloorplanServerData","ModelFloorplanClient","RealseeSpatialClient","extends WorkDetailClient, ModelFloorplanClient"]],[t.client,["implements RealseeSpatialClient","FLOORPLAN_PLUGINS_PATH = '/open/v3/floorplan/plugins'","asRecord(descriptor.data).format_floorplan_url","floorplanUrl.protocol !== 'https:'","floorplanUrl.username","floorplanUrl.password","redirect: 'error'","asModelFloorplanServerData(payload)"]],[t.service,[".getModelFloorplan(resourceCode, { signal })",".catch((error: unknown) =>","modelFloorplan ? { capabilities: { modelFloorplan } } : {}"]],[t.workSource,["modelFloorplan?: ModelFloorplanServerData","asModelFloorplanServerData(","asRecord(envelope.capabilities).modelFloorplan","modelFloorplan ? { capabilities: { modelFloorplan } } : {}"]],[t.feature,["@realsee/dnalogel/libs/floorplan/ModelFloorplanPlugin","dnalogel.modelFloorplan","ModelFloorplanPlugin(five, { autoShowEnable: false })","plugins.register(PLUGIN_NAME, candidate)","candidate.dispose()","const modelSurface = five.getElement()","const wrapper = modelSurface?.parentElement","plugin.appendTo(wrapper)","plugin.load(serverData)","modelOpacity: 0","plugin.hide({ userAction: true })",".finally(restoreModelSurface)","if (!state.visible) restoreModelSurface()","plugins.unregister(PLUGIN_NAME, plugin)","stopStateChange()","if (disposeRequested) finalize()","if (!loading) finalize()"]],[t.app,["payload.capabilities?.modelFloorplan","if (modelFloorplan)","createModelFloorplanFeature(five, plugins)","await currentFloorplan.load(modelFloorplan)","currentFloorplan.dispose()","setFloorplanFeature(floorplan)","floorplan?.dispose()"]],[t.modeSwitcher,["floorplan?.subscribeVisibility","await floorplan.show()","if (floorplan?.visible) await floorplan.hide()","floorplan?.visible ? 'floorplan' : tabFromFiveMode(five.currentMode)","Five.Mode.Panorama","Five.Mode.Mapview"]]];for(let[o,m]of a){let f=l.join(r,o);if(!await u(f)){e.push(s("error","architecture.floorplan-missing",`\u7F3A\u5C11\u5E73\u9762\u56FE\u5408\u540C\u6587\u4EF6\uFF1A${o}`));continue}let p=y(await E(f,"utf8"));for(let w of m)p.includes(w)||e.push(s("error","architecture.floorplan-contract",`${o} \u7F3A\u5C11\u5E73\u9762\u56FE\u6821\u9A8C\u3001capability \u6216\u751F\u547D\u5468\u671F\u6807\u8BB0\uFF1A${w}`))}let n=l.join(r,t.feature);if(await u(n)){let o=y(await E(n,"utf8"));for(let[v,h]of[[/plugin\.hooks\.on\(\s*['"]show['"]/,"show hook"],[/plugin\.hooks\.on\(\s*['"]hide['"]/,"hide hook"],[/plugin\.hooks\.on\(\s*['"]stateChange['"]/,"stateChange hook"]])v.test(o)||e.push(s("error","architecture.floorplan-lifecycle",`${t.feature} \u7F3A\u5C11 ${h}\u3002`));/autoShowEnable\s*:\s*true/.test(o)&&e.push(s("error","architecture.floorplan-lifecycle",`${t.feature} \u5FC5\u987B\u4FDD\u6301 autoShowEnable: false\u3002`));let m=o.indexOf("plugin.appendTo(wrapper)"),f=o.indexOf("plugin.load(serverData)");(m<0||f<m)&&e.push(s("error","architecture.floorplan-mount",`${t.feature} \u5FC5\u987B\u5148\u628A\u63D2\u4EF6 DOM appendTo Five \u5BBF\u4E3B\uFF0C\u518D\u52A0\u8F7D format_floorplan_url \u6570\u636E\u3002`));let p=o.indexOf("stopShow()"),w=o.indexOf("stopHide()"),b=o.indexOf("stopStateChange()"),k=o.indexOf("if (!loading) finalize()"),c=o.indexOf("const finalize ="),d=o.indexOf("restoreModelSurface()",c),g=o.indexOf("plugins.unregister(PLUGIN_NAME, plugin)",c);(p<0||w<p||b<w||k<b||c<0||d<c||g<d)&&e.push(s("error","architecture.floorplan-cleanup",`${t.feature} \u5FC5\u987B\u5728 hide\u3001\u5931\u8D25\u4E0E\u9500\u6BC1\u65F6\u6062\u590D Five model surface\uFF0C\u5E76\u5148\u79FB\u9664\u5168\u90E8 hooks\uFF0C\u518D\u6309 pending load \u72B6\u6001 identity-safe \u6E05\u7406\u3002`))}let i=l.join(r,t.modeSwitcher);if(await u(i)){let o=y(await E(i,"utf8"));for(let[f,p]of[[/type\s+ViewerTab\s*=\s*['"]tour['"]\s*\|\s*['"]model['"]\s*\|\s*['"]floorplan['"]/,"\u4E09\u4E2A ViewerTab"],[/value\s*:\s*['"]tour['"]\s*,\s*text\s*:\s*['"]漫游['"]/,"Panorama \u6F2B\u6E38 tab"],[/value\s*:\s*['"]model['"]\s*,\s*text\s*:\s*['"]三维模型['"]/,"Mapview \u4E09\u7EF4\u6A21\u578B tab"],[/\.\.\.\(\s*floorplan\s*\?\s*\[\{\s*value\s*:\s*['"]floorplan['"](?:\s+as\s+const)?\s*,\s*text\s*:\s*['"]平面图['"]\s*\}\]\s*:\s*\[\]\s*\)/,"\u6761\u4EF6\u5E73\u9762\u56FE tab"]])f.test(o)||e.push(s("error","architecture.floorplan-tabs",`${t.modeSwitcher} \u7F3A\u5C11 ${p}\u3002`));(o.match(/floorplan\?\.visible\s*\?\s*['"]floorplan['"]\s*:\s*tabFromFiveMode\(five\.currentMode\)/g)?.length??0)<2&&e.push(s("error","architecture.floorplan-tabs",`${t.modeSwitcher} \u5FC5\u987B\u5728\u521D\u59CB\u72B6\u6001\u548C\u5207\u6362\u5931\u8D25\u540E\u90FD\u6309\u63D2\u4EF6\u771F\u5B9E\u53EF\u89C1\u6027\u540C\u6B65 Tab\u3002`)),(o.includes("Five.Mode.Topview")||/text\s*:\s*['"]地图['"]/.test(o))&&e.push(s("error","architecture.floorplan-tabs",`${t.modeSwitcher} \u4E0D\u5F97\u628A Topview \u66B4\u9732\u4E3A\u5730\u56FE tab\u3002`))}for(let[o,m]of[[t.browserTsconfig,'"include": ["src", "shared", "vite.config.ts"]'],[t.serverTsconfig,'"include": ["server", "shared"]']]){let f=l.join(r,o);if(!await u(f))continue;(await E(f,"utf8")).includes(m)||e.push(s("error","tooling.floorplan-include",`${o} \u5FC5\u987B\u5305\u542B shared floorplan validator\uFF1A${m}`))}return e}function Rr(r){let t=new Map;for(let e of r.split(/\r?\n/)){let a=e.trim();if(!a||a.startsWith("#"))continue;let n=/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(a);n&&t.set(n[1],n[2].trim())}return t}async function ze(r,t){let e=[],a=l.join(r,".env.example");if(await u(a)){let f=Rr(await E(a,"utf8"));for(let p of ie)f.has(p)||e.push(s("error","env.required-key",`.env.example \u7F3A\u5C11 ${p}\u3002`));for(let[p,w]of f)p.startsWith("VITE_WORK_")&&e.push(s("error","env.legacy-work-url",`.env.example \u4E0D\u5F97\u58F0\u660E\u8DEF\u7531\u6A21\u5F0F\u4E0D\u9700\u8981\u7684 ${p}\u3002`)),p.startsWith("VITE_")&&_e(p)&&e.push(s("error","env.client-secret",`.env.example \u7981\u6B62\u58F0\u660E\u6D4F\u89C8\u5668\u53EF\u89C1\u7684\u5BC6\u94A5\u53D8\u91CF ${p}\u3002`)),_e(p)&&!le.includes(p)&&w&&w!=="*"&&e.push(s("error","env.secret",`.env.example \u7684 ${p} \u53EA\u80FD\u4E3A\u7A7A\u6216\u4F7F\u7528 * \u5360\u4F4D\uFF0C\u4E0D\u80FD\u5305\u542B\u771F\u5B9E\u5BC6\u94A5\u3002`));for(let p of ne){let w=f.get(p);w!==void 0&&w!==""&&w!=="*"&&e.push(s("error","env.credential",`.env.example \u7684 ${p} \u53EA\u80FD\u4E3A\u7A7A\u6216\u4F7F\u7528 * \u5360\u4F4D\u3002`))}}await u(l.join(r,".env.server.example"))&&e.push(s("error","env.legacy-file","\u73AF\u5883\u914D\u7F6E\u5DF2\u5408\u5E76\u5230 .env.example\uFF0C\u4E0D\u5F97\u4FDD\u7559 .env.server.example\u3002"));let n=l.join(r,"src");if(await u(n))for(let f of await x(n)){let p=y(await E(f,"utf8")),w=ee(p);w&&e.push(s("error","env.client-server-only-reference",`${l.relative(r,f)} \u4E0D\u5F97\u8BFB\u53D6\u6216\u5F15\u7528 server-only \u914D\u7F6E\u6216\u51ED\u636E\uFF1A${w}\u3002`))}let i=l.join(r,"src","vr","work","WorkSource.ts");if(await u(i)){let f=y(await E(i,"utf8"));for(let p of["resourceCodeFromPathname","window.location.pathname","segments.length !== 1","`/api/work/${encodeURIComponent(resourceCode)}`","asExplicitPanoTagList","hasCanonicalPanoTagData(value.contentType, value.data)","hasCanonicalPanoTagAnchor(value)","asRecord(envelope.presetData)","presetData: { panoTagList }","displayName: string","displayName: displayNameFrom(work.name) ?? workCode","new RouteWorkSource()"])f.includes(p)||e.push(s("error","architecture.resource-route",`WorkSource \u7F3A\u5C11 /{resourceCode} \u8DEF\u7531\u3001\u540C\u6E90 API \u6216\u663E\u5F0F preset \u6807\u8BB0\uFF1A${p}`));for(let p of["VITE_WORK_URL","/work.json","envelope.tags","envelope.tagList","envelope.tag_list","payload.tags"])f.includes(p)&&e.push(s("error","architecture.resource-route",`WorkSource \u4E0D\u5F97\u4F9D\u8D56\u65E7 Work URL\u3001\u672C\u5730 Work \u6587\u4EF6\u6216\u628A generic tags \u6620\u5C04\u5230 PanoTag\uFF1A${p}`))}for(let f of["vite.config.ts","vite.config.js","vite.config.mjs"]){let p=l.join(r,f);if(!await u(p))continue;let w=y(await E(p,"utf8")),b=ee(w);(b||/envPrefix\s*:\s*[^\n]*REALSEE_/.test(w))&&e.push(s("error","env.vite-secret",`${f} \u4E0D\u5F97\u628A server-only \u914D\u7F6E\u3001AK/SK/token \u6216 REALSEE_* \u524D\u7F00\u6CE8\u5165\u6D4F\u89C8\u5668${b?`\uFF1A${b}`:"\u3002"}`));let k=w.match(/\bhost\s*:\s*['"]127\.0\.0\.1['"]/g)?.length??0,c=w.match(/\bport\s*:\s*8087\b/g)?.length??0,d=w.match(/\bstrictPort\s*:\s*true\b/g)?.length??0,g=/target\s*:\s*['"]http:\/\/127\.0\.0\.1:8088['"]/.test(w);(k<2||c<2||d<2||!g)&&e.push(s("error","architecture.dev-route",`${f} \u5FC5\u987B\u8BA9 Vite server/preview \u4F7F\u7528 strict IPv4 127.0.0.1:8087\uFF0C\u5E76\u628A /api \u4EE3\u7406\u5230 Fastify 127.0.0.1:8088\u3002`))}let o=[["server/types.ts",["ServerConfig","WorkEnvelope","WorkDetailClient","WorkService"]],["server/config.ts",["loadServerConfig","REALSEE_GATEWAY_APP_KEY","REALSEE_GATEWAY_APP_SECRET","gatewayEndpoint.protocol !== 'https:'","): ServerConfig {"]],["server/check-config.ts",["loadServerConfig()","BFF server configuration is valid."]],["server/realsee-open-api.ts",["RealseeOpenApiClient","implements RealseeSpatialClient","ACCESS_TOKEN_PATH = '/auth/access_token'","'application/x-www-form-urlencoded'","WORK_INFO_PATH = '/open/v3/vr/info'","Authorization: accessToken","resource_code: resourceCode","UNAUTHORIZED_UPSTREAM_CODES","TOKEN_REFRESH_SKEW_MS"]],["server/work-service.ts",["createWorkService","): WorkService {","getWorkByResourceCode","openApiClient.getWorkDetail(resourceCode, { signal })"]],["server/work-route.ts",["'/api/work/:resourceCode'","getWorkByResourceCode(resourceCode, {","signal: requestAbort.signal","WORK_UPSTREAM_FAILED"]],["server/app.ts",["Fastify({ logger })","'/api/health'","fastifyStatic","serveFrontend","reply.sendFile('index.html')","Promise<FastifyInstance>"]],["server/index.ts",["loadServerConfig","RealseeOpenApiClient","createWorkService","createApp","app.listen({ host: config.host, port: config.port })"]]];for(let[f,p]of o){let w=l.join(r,f);if(!await u(w)){e.push(s("error","architecture.bff-missing",`\u7F3A\u5C11 Fastify BFF \u6587\u4EF6\uFF1A${f}`));continue}let b=y(await E(w,"utf8"));for(let k of p)b.includes(k)||e.push(s("error","architecture.bff-contract",`${f} \u7F3A\u5C11 BFF \u89E3\u8026\u6216 OpenAPI \u5951\u7EA6\uFF1A${k}`))}let m=l.join(r,t);if(await u(m)){let f=new Set((await E(m,"utf8")).split(/\r?\n/).map(p=>p.trim()).filter(Boolean));for(let p of[".env*","!.env.example"])f.has(p)||e.push(s("error","env.gitignore",`${t} \u5FC5\u987B\u5305\u542B ${p}\u3002`))}return e}async function jr(r,t){return t.name!==Y?!1:Object.hasOwn(t.bin??{},"vr-cli")||await u(l.join(r,"bin","vr-cli.mjs"))||await u(l.join(r,"templates","base"))}async function Pr(r,t){let e=[];t.private!==!1&&e.push(s("error","tool.private","\u53EF\u53D1\u5E03\u5DE5\u5177\u5FC5\u987B\u663E\u5F0F\u58F0\u660E private: false\u3002"));let a=Object.entries(t.bin??{});(a.length!==1||a[0]?.[0]!=="vr-cli"||a[0]?.[1]!=="bin/vr-cli.mjs")&&e.push(s("error","tool.bin","\u5F00\u53D1\u5DE5\u5177\u5FC5\u987B\u53EA\u66B4\u9732 vr-cli -> bin/vr-cli.mjs\u3002")),((typeof t.repository=="string"?t.repository:t.repository?.url)??null)!==null&&e.push(s("error","tool.repository",null===null?"\u5F53\u524D\u53D1\u5E03 profile \u8981\u6C42 package.json \u7701\u7565 repository\u3002":`package.json repository \u5FC5\u987B\u4E3A ${null}\u3002`));let i=t.publishConfig?.registry??null,o=t.publishConfig?.access??null;(i!==F||o!==N)&&e.push(s("error","tool.publish-profile",`package.json publishConfig \u5FC5\u987B\u5339\u914D\u5F53\u524D\u53D1\u5E03 profile\uFF1Aregistry=${F}\uFF0Caccess=${N??"\u672A\u8BBE\u7F6E"}\u3002`)),e.push(...await $e(r,t));let m=["README.md",...await u(l.join(r,".git"))?[".nvmrc",".node-version",".npmrc"]:[],"agent-assets/AGENTS.md","bin","bin/vr-cli.mjs",...N==="public"?["dist/cli.mjs"]:["src","src/core/spatial-policy.mjs"],"templates/base","templates/base/README.md","templates/base/.editorconfig","templates/base/.env.example","templates/base/.gitignore.template","templates/base/.nvmrc","templates/base/.node-version","templates/base/.npmrc.template","templates/base/.prettierignore","templates/base/eslint.config.mjs","templates/base/package-lock.json","templates/base/prettier.config.mjs","templates/base/tsconfig.server.json","templates/base/server/types.ts","templates/base/server/config.ts","templates/base/server/check-config.ts","templates/base/server/realsee-open-api.ts","templates/base/server/work-service.ts","templates/base/server/work-route.ts","templates/base/server/app.ts","templates/base/server/index.ts","templates/base/shared/model-floorplan.ts","templates/base/src/vr/five/api.ts","templates/base/src/vr/five/fiveConfig.ts","templates/base/src/vr/five/SpatialRuntimeProvider.tsx","templates/base/src/vr/five/initialState.ts","templates/base/src/vr/plugins/dnalogel.ts","templates/base/src/vr/plugins/SpatialPluginRegistry.ts","templates/base/src/vr/plugins/examples/FeatureOwnedMeshPlugin.ts","templates/base/src/vr/work/WorkSource.ts","templates/viewer/src/app/App.tsx","templates/viewer/src/features/floorplan/ModelFloorplanFeature.ts","templates/viewer/src/features/navigation/ModeSwitcher.tsx","templates/viewer/src/features/tags/PanoTagFeature.ts","templates/viewer/src/ui/ViewerOverlay.tsx","skills/realsee-vr-app","skills/realsee-vr-app/references/five-api-capabilities.md","skills/realsee-vr-app/references/dnalogel-api-capabilities.md","skills/realsee-vr-app/references/five-runtime.md","skills/realsee-vr-app/references/official-docs.md","skills/realsee-vr-app/references/plugin-lifecycle.md","skills/realsee-vr-app/references/ui-and-validation.md","skills/realsee-vr-app/references/vapor-dom-layer.md","skills/realsee-image-resources/SKILL.md","skills/realsee-image-resources/references/cors-and-three-textures.md","skills/realsee-image-resources/references/data-wanxiang.md","skills/realsee-image-resources/references/five-image-url-transform.md","skills/realsee-image-resources/references/source-and-domain-policy.md","skills/threejs-r117-five/SKILL.md","skills/threejs-r117-five/references/r117-capabilities.md","skills/threejs-r117-five/references/r117-version-guards.md","skills/threejs-r117-five/references/five-ownership.md"];for(let v of m)await u(l.join(r,v))||e.push(s("error","tool.missing",`\u7F3A\u5C11\u5DE5\u5177\u8DEF\u5F84\uFF1A${v}`));for(let v of["templates/base/README.md","agent-assets/AGENTS.md"]){let h=l.join(r,v);if(!await u(h))continue;let S=await E(h,"utf8");S.includes("{{TOOL_VERSION}}")||e.push(s("error","tool.token",`${v} \u5FC5\u987B\u4F7F\u7528 {{TOOL_VERSION}} \u7248\u672C token\u3002`)),S.includes("__TOOL_VERSION__")&&e.push(s("error","tool.token",`${v} \u4E0D\u5F97\u4F7F\u7528\u4E0E Markdown \u7C97\u4F53\u51B2\u7A81\u7684 __TOOL_VERSION__ token\u3002`))}let f=l.join(r,"templates","base");if(await u(f)){let v=new Set(["package-lock.json","package.json","tsconfig.json","tsconfig.server.json"]);for(let h of await K(f))h.endsWith(".json")&&!v.has(h)&&e.push(s("error","tool.data-asset",`\u6A21\u677F\u4E0D\u5F97\u5206\u53D1\u672A\u7ECF\u660E\u786E\u5BA1\u6838\u7684 JSON/Work \u6570\u636E\uFF1Atemplates/base/${h}`))}e.push(...await Ge(r,["skills/realsee-vr-app/SKILL.md","agent-assets/AGENTS.md"],"skills/realsee-vr-app/references/dnalogel-api-capabilities.md")),e.push(...await Ve(r,["skills/realsee-vr-app/SKILL.md","agent-assets/AGENTS.md"],"skills/realsee-vr-app")),e.push(...await Ke(r,["agent-assets/AGENTS.md","skills/realsee-vr-app/SKILL.md"],"skills/realsee-vr-app")),e.push(...await We(r,{validator:"templates/base/shared/model-floorplan.ts",types:"templates/base/server/types.ts",client:"templates/base/server/realsee-open-api.ts",service:"templates/base/server/work-service.ts",workSource:"templates/base/src/vr/work/WorkSource.ts",feature:"templates/viewer/src/features/floorplan/ModelFloorplanFeature.ts",app:"templates/viewer/src/app/App.tsx",modeSwitcher:"templates/viewer/src/features/navigation/ModeSwitcher.tsx",browserTsconfig:"templates/base/tsconfig.json",serverTsconfig:"templates/base/tsconfig.server.json"})),e.push(...await Ie(r,["agent-assets/AGENTS.md","skills/realsee-vr-app/SKILL.md","skills/realsee-image-resources/SKILL.md"])),e.push(...await Ce(r,["agent-assets/AGENTS.md","skills/realsee-vr-app/SKILL.md","skills/realsee-vr-app/references/vapor-dom-layer.md"])),e.push(...await De(r,"skills/realsee-vr-app/references/vapor-dom-layer.md","skills/realsee-vr-app/references/ui-and-validation.md")),e.push(...await Fe(r,"templates/base/src/vr/plugins/examples/FeatureOwnedMeshPlugin.ts"));let p=l.join(r,"templates","base","src","main.tsx");if(await u(p)){let v=y(await E(p,"utf8"));(Ae(v)!==1||v.includes("@realsee/vapor/vapor.scss"))&&e.push(s("error","tool.vapor-css","\u9ED8\u8BA4\u6A21\u677F\u5FC5\u987B\u6070\u597D\u4E00\u6B21\u4EE5\u53EF\u6267\u884C side-effect import \u52A0\u8F7D @realsee/vapor/vapor.css\uFF0C\u4E14\u4E0D\u5F97\u4F7F\u7528 vapor.scss\u3002"))}let w=l.join(r,"templates","base","src","vr","work","WorkSource.ts");if(await u(w)){let v=y(await E(w,"utf8"));for(let h of["displayName: string","displayNameFrom(work.name) ?? workCode"])v.includes(h)||e.push(s("error","tool.work-name",`\u9ED8\u8BA4 WorkSource \u5FC5\u987B\u4ECE work.name \u5B89\u5168\u6D3E\u751F VR \u5C55\u793A\u540D\u79F0\uFF1A${h}`))}let b=l.join(r,"templates","viewer","src","ui","ViewerOverlay.tsx");if(await u(b)){let v=y(await E(b,"utf8")),h=Te.filter(S=>!z(v,S)||!U(v,S));h.length>0&&e.push(s("error","tool.vapor-provider",`\u9ED8\u8BA4 ViewerOverlay \u5FC5\u987B\u4ECE @realsee/vapor \u5BFC\u5165\u5E76\u5B9E\u9645\u6E32\u67D3\u5B98\u65B9 Demo \u7EC4\u4EF6\uFF1A${h.join(", ")}`));for(let S of je)v.includes(S)||e.push(s("error","tool.viewer-topbar",`\u9ED8\u8BA4 ViewerOverlay \u5FC5\u987B\u4F7F\u7528 Vapor TitleBar\u3001Layer \u4E0E\u54CD\u5E94\u5F0F Stack\uFF0C\u5E76\u6E32\u67D3\u5DE6\u4E0A\u6807\u9898\u3001\u5C45\u4E2D controls \u4E0E\u72B6\u6001\u8BED\u4E49\uFF1A${S}`));for(let S of Ne)v.includes(S)&&e.push(s("error","tool.viewer-native-vapor",`\u9ED8\u8BA4 ViewerOverlay \u4E0D\u5F97\u7528\u539F\u751F\u6807\u9898\u6216\u81EA\u5B9A\u4E49 glass \u6837\u5F0F\u66FF\u4EE3 Vapor \u7EC4\u4EF6\u89C6\u89C9\uFF1A${S}`))}let k=l.join(r,"templates","base","src","styles.css");if(await u(k)){let v=await E(k,"utf8");for(let h of Le)v.includes(h)||e.push(s("error","tool.viewer-layout",`\u9ED8\u8BA4 Viewer \u5FC5\u987B\u4FDD\u6301 safe-area \u53CA\u684C\u9762/\u5E73\u677F/\u624B\u673A/\u77EE\u5C4F\u5916\u5C42\u54CD\u5E94\u5F0F\u5E03\u5C40\uFF1A${h}`));for(let h of Me)v.includes(h)&&e.push(s("error","tool.viewer-native-vapor",`\u9ED8\u8BA4 Viewer \u6837\u5F0F\u4E0D\u5F97\u8986\u76D6 Vapor \u5185\u90E8\u7C7B\u6216\u91CD\u7ED8\u7EC4\u4EF6\u89C6\u89C9\uFF1A${h}`))}let c=l.join(r,"templates","viewer","src","features","navigation","ModeSwitcher.tsx");if(await u(c)){let v=y(await E(c,"utf8")),h=Re.filter(S=>!z(v,S)||!U(v,S));h.length>0&&e.push(s("error","tool.vapor-component",`\u9ED8\u8BA4 ModeSwitcher \u5FC5\u987B\u4ECE @realsee/vapor \u5BFC\u5165\u5E76\u5B9E\u9645\u6E32\u67D3\u5B98\u65B9 Demo \u7684\u6A21\u5F0F\u5207\u6362\u7EC4\u4EF6\uFF1A${h.join(", ")}`));for(let S of Pe)v.includes(S)||e.push(s("error","tool.viewer-control-size",`\u9ED8\u8BA4 ModeSwitcher \u5FC5\u987B\u4F7F\u7528\u5B98\u65B9 Demo \u7684 Font/HStack/VStack \u4E0E shadow Button \u7EC4\u5408\uFF1A${S}`));for(let S of xe)v.includes(S)&&e.push(s("error","tool.viewer-native-vapor",`\u9ED8\u8BA4 ModeSwitcher \u4E0D\u5F97\u9000\u56DE Segmented \u6216\u6DFB\u52A0\u65E7\u7684\u62C9\u4F38/glass \u6837\u5F0F\uFF1A${S}`))}let d=l.join(r,"templates","viewer","src");if(await u(d))for(let v of await x(d)){let h=y(await E(v,"utf8")),S=ee(h);S&&e.push(s("error","env.client-server-only-reference",`${l.relative(r,v)} \u4E0D\u5F97\u8BFB\u53D6\u6216\u5F15\u7528 server-only \u914D\u7F6E\u6216\u51ED\u636E\uFF1A${S}\u3002`))}let g=l.join(r,"templates","viewer","src","app","App.tsx");if(await u(g)){let v=y(await E(g,"utf8"));for(let h of["setWorkName(payload.displayName)","title={workName}"])v.includes(h)||e.push(s("error","tool.work-name",`\u9ED8\u8BA4 App \u5FC5\u987B\u628A WorkSource \u5C55\u793A\u540D\u79F0\u4F20\u7ED9 ViewerOverlay\uFF1A${h}`));for(let h of["FeatureOwnedMeshPlugin","registerFeatureOwnedMeshPlugin","vr/plugins/examples"])v.includes(h)&&e.push(s("error","tool.custom-plugin-enabled",`\u9ED8\u8BA4 Viewer \u4E0D\u5F97\u542F\u7528\u81EA\u5B9A\u4E49\u63D2\u4EF6\u793A\u4F8B\uFF1A${h}`))}return e.push(...await ze(l.join(r,"templates","base"),".gitignore.template")),e}async function Lr(r,t,e){let a=new Set;for(let n of e)await u(l.join(r,n))&&a.add(n);for(let n of t){let i=l.join(r,n);if(!await u(i))continue;if(!(await cr(i)).isDirectory()){a.add(n);continue}for(let m of await K(i))a.add(l.posix.join(n,m))}return a}async function Nr(r){let t=[],e=l.join(r,".vr-cli.json");if(!await u(e)){let o=l.join(r,".agents"),m=await u(l.join(r,"AGENTS.md")),f=await u(o);if(!m&&!f)t.push(s("warning","agents.missing","\u672A\u5B89\u88C5\u8DE8 Agent \u6307\u4EE4\u8D44\u4EA7\u3002"));else for(let p of["skills/realsee-vr-app/SKILL.md","skills/realsee-image-resources/SKILL.md","skills/realsee-image-resources/references/cors-and-three-textures.md","skills/realsee-image-resources/references/data-wanxiang.md","skills/realsee-image-resources/references/five-image-url-transform.md","skills/realsee-image-resources/references/source-and-domain-policy.md","skills/threejs-r117-five/SKILL.md","skills/threejs-r117-five/references/r117-capabilities.md","skills/threejs-r117-five/references/r117-version-guards.md","skills/threejs-r117-five/references/five-ownership.md"])await u(l.join(o,p))||t.push(s("error","agents.skill-missing",`.agents/${p} \u7F3A\u5931\u3002`));return t}let a;try{a=await re(e)}catch(o){return[s("error","manifest.invalid",".vr-cli.json \u65E0\u6CD5\u89E3\u6790\u3002",String(o))]}if(!(a&&typeof a=="object"&&!Array.isArray(a)&&a.schemaVersion===1&&O.includes(a.agentAssets)))return[s("error","manifest.invalid",".vr-cli.json \u7F3A\u5C11\u6709\u6548\u7684 schemaVersion \u6216 agentAssets\u3002")];for(let o of W(a.agentAssets))await u(l.join(r,o))||t.push(s("error","agents.asset-missing",`agentAssets=${a.agentAssets} \u7F3A\u5C11\uFF1A${o}`));let i=a.agentAssets==="portable"?{directoryRoots:Ee,singleFiles:Se}:a.agentAssets==="none"?{directoryRoots:he,singleFiles:we}:null;if(i)for(let o of await Lr(r,i.directoryRoots,i.singleFiles))t.push(s("error","agents.unexpected",`agentAssets=${a.agentAssets} \u4E0D\u5E94\u5305\u542B\uFF1A${o}`));return t}async function xr(r,t){let e=[];t.private!==!0&&e.push(s("error","project.public","\u751F\u6210\u9879\u76EE\u5FC5\u987B\u4FDD\u6301 private\u3002")),t.type!=="module"&&e.push(s("error","project.module","\u751F\u6210\u9879\u76EE\u5FC5\u987B\u4F7F\u7528 ESM\uFF1Atype=module\u3002")),e.push(...await $e(r,t));for(let c of ce)await u(l.join(r,c))||e.push(s("error","project.missing",`\u7F3A\u5C11\u751F\u6210\u6587\u4EF6\uFF1A${c}`));for(let[c,d,g]of[["dependency",t.dependencies,G],["dev-dependency",t.devDependencies,q]])for(let[v,h]of Object.entries(g)){let S=d?.[v];if(S===void 0){e.push(s("error",`${c}.missing`,`${v} \u5FC5\u987B\u58F0\u660E\uFF0C\u751F\u6210\u5668\u57FA\u7EBF\u4E3A ${h}`));continue}if(v in V){S!==h&&e.push(s("error",`${c}.locked`,`${v} \u5FC5\u987B\u9501\u5B9A\u4E3A ${h}\uFF0C\u5F53\u524D\u4E3A ${S}\uFF1BFive 6.8.9 \u4E0E Three \u80FD\u529B\u53C2\u8003\u5747\u9488\u5BF9\u8BE5\u7248\u672C\uFF0C\u5347\u7EA7\u9700\u6574\u4F53\u8BC4\u4F30\u5E76\u540C\u6B65\u751F\u6210\u5668\u3002`));continue}let $=H(S);if(!$){e.push(s("error",`${c}.range`,`${v} \u5FC5\u987B\u4F7F\u7528\u5F00\u653E\u6700\u4F4E\u7248\u672C\u8303\u56F4 >=x.y.z\uFF0C\u751F\u6210\u5668\u57FA\u7EBF\u4E3A >=${h}\uFF0C\u5F53\u524D\u4E3A ${S}\u3002`));continue}if(!P($,h)){e.push(s("error",`${c}.minimum`,`${v} \u7684\u58F0\u660E\u4E0B\u9650 ${$} \u4F4E\u4E8E\u751F\u6210\u5668\u57FA\u7EBF ${h}\u3002`));continue}S!==oe(v,h)&&e.push(s("warning",`${c}.updated`,`${v} \u7684\u6700\u4F4E\u7248\u672C\u5DF2\u4ECE\u751F\u6210\u5668\u57FA\u7EBF ${h} \u63D0\u9AD8\u5230 ${$}\uFF1B\u8BF7\u786E\u8BA4 npm run check \u4E0E npm run build \u901A\u8FC7\uFF0C\u5E76\u5728\u5347\u7EA7 SDK \u65F6\u590D\u6838\u7248\u672C\u5316 fixture\u3002`))}for(let[c,d]of[["dev",["check:env","concurrently"]],["dev:bff",["--env-file-if-exists=.env.development","server/index.ts"]],["dev:web",["vite"]],["check:env",["--env-file-if-exists=.env.development","server/check-config.ts"]],["typecheck",["npm run typecheck:web","npm run typecheck:bff"]],["typecheck:web",["tsc -p tsconfig.json"]],["typecheck:bff",["tsc -p tsconfig.server.json"]],["lint",["eslint . --max-warnings=0"]],["format",["prettier --write ."]],["format:check",["prettier --check ."]],["check",["npm run typecheck","npm run lint","npm run format:check"]],["check:bff",["typecheck:bff"]],["deps:relock",["npm install --package-lock-only"]],["deps:install",["npm ci"]],["start",["--env-file-if-exists=.env.development","server/index.ts","--serve-frontend"]]])for(let g of d)t.scripts?.[c]?.includes(g)||e.push(s("error","script.contract",`package.json scripts.${c} \u5FC5\u987B\u5305\u542B\uFF1A${g}`));e.push(...await Tr(r,t));for(let[c,d]of[["eslint.config.mjs",["from '@eslint/js'","from 'typescript-eslint'","from 'eslint-plugin-react-hooks'","recommendedTypeChecked","tsconfig.server.json","globals.browser","globals.node","'react-hooks/rules-of-hooks'","'react-hooks/exhaustive-deps'","no-floating-promises","disableTypeChecked"]],["prettier.config.mjs",["semi: false","singleQuote: true","trailingComma: 'all'"]],["tsconfig.json",['"strict": true','"noUncheckedIndexedAccess": true','"noEmit": true','"include": ["src", "shared", "vite.config.ts"]']],["tsconfig.server.json",['"strict": true','"erasableSyntaxOnly": true','"verbatimModuleSyntax": true','"exactOptionalPropertyTypes": true','"noUncheckedIndexedAccess": true','"types": ["node"]','"include": ["server", "shared"]']]]){let g=l.join(r,c);if(!await u(g))continue;let v=await E(g,"utf8");for(let h of d)v.includes(h)||e.push(s("error","tooling.config",`${c} \u7F3A\u5C11\u914D\u7F6E\u6807\u8BB0\uFF1A${h}`))}let a=l.join(r,"src","main.tsx");if(await u(a)){let c=await E(a,"utf8"),d=y(c);(Ae(d)!==1||d.includes("@realsee/vapor/vapor.scss"))&&e.push(s("error","vapor.css","src/main.tsx \u5FC5\u987B\u6070\u597D\u4E00\u6B21\u4EE5\u53EF\u6267\u884C side-effect import \u5BFC\u5165 @realsee/vapor/vapor.css\uFF0C\u4E14\u4E0D\u5F97\u4F7F\u7528 vapor.scss\u3002")),d.includes("./app/App")||e.push(s("error","architecture.app","src/main.tsx \u5FC5\u987B\u4ECE app \u5C42\u7EC4\u5408\u7A7A\u95F4\u8FD0\u884C\u65F6\u4E0E DOM UI\u3002"))}let n=l.join(r,"src","vr","five","api.ts");if(await u(n)){let c=await E(n,"utf8"),d=y(c);for(let g of["@realsee/five","@realsee/five/plugins","@realsee/five/line","@realsee/five/sticker","@realsee/five/vfx","@realsee/five/gltf-loader","@realsee/five/shader-lib","@realsee/five/work-downloader"])d.includes(`'${g}'`)||e.push(s("error","architecture.five-api",`Five \u80FD\u529B\u5165\u53E3\u7F3A\u5931\uFF1A${g}`))}let i=l.join(r,"src","vr","plugins","dnalogel.ts");await u(i)&&(y(await E(i,"utf8")).includes("from '@realsee/dnalogel'")||e.push(s("error","architecture.dnalogel-api","dnalogel \u9884\u8BBE\u63D2\u4EF6\u6839\u80FD\u529B\u5165\u53E3\u7F3A\u5931\u3002"))),e.push(...await Fe(r,"src/vr/plugins/examples/FeatureOwnedMeshPlugin.ts"));let o=l.join(r,"src");if(await u(o))for(let c of await x(o)){let d=y(await E(c,"utf8"));d.includes("@realsee/five/react")&&e.push(s("error","architecture.five-react",`${l.relative(r,c)} \u7981\u6B62\u4F7F\u7528\u66F4\u65B0\u6EDE\u540E\u7684 @realsee/five/react\uFF1B\u8BF7\u76F4\u63A5\u4F7F\u7528\u539F\u751F Five API\u3002`));for(let g of dr)g.pattern.test(d)&&e.push(s("error","compatibility.three-r117",`${l.relative(r,c)}\uFF1A${g.message}`))}let m=l.join(r,"src","vr","five","SpatialRuntimeProvider.tsx");if(await u(m)){let c=await E(m,"utf8"),d=y(c);(d.includes("unsafe__useFiveInstance")||d.includes("provider?.five"))&&e.push(s("error","architecture.five-instance","\u7A7A\u95F4\u8FD0\u884C\u65F6\u7981\u6B62\u901A\u8FC7 Five React wrapper \u83B7\u53D6\u5B9E\u4F8B\u3002")),/import\s*\{[^}]*\bFive\b[^}]*\}\s*from\s*['"]@realsee\/five['"]/.test(d)||e.push(s("error","architecture.five-native","SpatialRuntimeProvider \u5FC5\u987B\u4ECE @realsee/five \u5BFC\u5165\u539F\u751F Five\u3002"));for(let h of["new Five(",".appendTo(",".refresh(",".getElement()",".dispose()","new ResizeObserver","new SpatialRuntimeLifetime","runExclusive<T>(","ownedLifetime?.disposeFeatures()","ownedLifetime.afterPending(finalize)","width <= 0 || height <= 0",'className="five-surface"'])d.includes(h)||e.push(s("error","architecture.five-native",`\u539F\u751F Five \u751F\u547D\u5468\u671F\u7F3A\u5C11\uFF1A${h}`));let g=d.indexOf("ownedLifetime?.disposeFeatures()"),v=d.indexOf("ownedLifetime.afterPending(finalize)");(g<0||v<g)&&e.push(s("error","architecture.five-cleanup","\u5FC5\u987B\u5148\u540C\u6B65\u505C\u6B62 feature\uFF0C\u518D\u7B49\u5F85\u539F\u751F\u5F02\u6B65\u4EFB\u52A1\u5B8C\u6210\u540E\u6E05\u7406\u63D2\u4EF6\u548C Five\u3002"))}let f=l.join(r,"src","app","App.tsx");if(await u(f)){let c=await E(f,"utf8"),d=y(c);for(let g of["lifetime.own(","lifetime.runExclusive(","await five.load(","await waitForActiveModel(","const panoTagList = payload.presetData?.panoTagList","if (panoTagList?.length)","await currentTags.load(panoTagList)","setWorkName(payload.displayName)","title={workName}"])d.includes(g)||e.push(s("error","architecture.feature-lifetime",`App \u7684\u539F\u751F\u5F02\u6B65\u751F\u547D\u5468\u671F\u6216\u663E\u5F0F PanoTag preset \u6761\u4EF6\u7F3A\u5C11\uFF1A${g}`));d.includes("payload.tags")&&e.push(s("error","architecture.pano-tag-contract","App \u4E0D\u5F97\u628A generic payload.tags \u76F4\u63A5\u52A0\u8F7D\u4E3A PanoTag\u3002"))}let p=l.join(r,"src","ui"),w=l.join(p,"ViewerOverlay.tsx");if(await u(w)){let c=y(await E(w,"utf8")),d=Te.filter(g=>!z(c,g)||!U(c,g));d.length>0&&e.push(s("error","architecture.vapor-provider",`ViewerOverlay \u5FC5\u987B\u4ECE @realsee/vapor \u5BFC\u5165\u5E76\u5B9E\u9645\u6E32\u67D3\u5B98\u65B9 Demo \u7EC4\u4EF6\uFF1A${d.join(", ")}`));for(let g of je)c.includes(g)||e.push(s("error","architecture.viewer-topbar",`ViewerOverlay \u5FC5\u987B\u4F7F\u7528 Vapor TitleBar\u3001Layer \u4E0E\u54CD\u5E94\u5F0F Stack\uFF0C\u5E76\u6E32\u67D3\u5DE6\u4E0A\u6807\u9898\u3001\u5C45\u4E2D controls \u4E0E\u72B6\u6001\u8BED\u4E49\uFF1A${g}`));for(let g of Ne)c.includes(g)&&e.push(s("error","architecture.viewer-native-vapor",`ViewerOverlay \u4E0D\u5F97\u7528\u539F\u751F\u6807\u9898\u6216\u81EA\u5B9A\u4E49 glass \u6837\u5F0F\u66FF\u4EE3 Vapor \u7EC4\u4EF6\u89C6\u89C9\uFF1A${g}`))}let b=l.join(r,"src","features","navigation","ModeSwitcher.tsx");if(await u(b)){let c=y(await E(b,"utf8")),d=Re.filter(g=>!z(c,g)||!U(c,g));d.length>0&&e.push(s("error","architecture.vapor-component",`ModeSwitcher \u5FC5\u987B\u4ECE @realsee/vapor \u5BFC\u5165\u5E76\u5B9E\u9645\u6E32\u67D3\u5B98\u65B9 Demo \u7684\u6A21\u5F0F\u5207\u6362\u7EC4\u4EF6\uFF1A${d.join(", ")}`));for(let g of Pe)c.includes(g)||e.push(s("error","architecture.viewer-control-size",`ModeSwitcher \u5FC5\u987B\u4F7F\u7528\u5B98\u65B9 Demo \u7684 Font/HStack/VStack \u4E0E shadow Button \u7EC4\u5408\uFF1A${g}`));for(let g of xe)c.includes(g)&&e.push(s("error","architecture.viewer-native-vapor",`ModeSwitcher \u4E0D\u5F97\u9000\u56DE Segmented \u6216\u6DFB\u52A0\u65E7\u7684\u62C9\u4F38/glass \u6837\u5F0F\uFF1A${g}`))}if(await u(p))for(let c of await x(p)){let d=y(await E(c,"utf8")),g=/(?:from|import)\s*(?:\(\s*)?['"][^'"]*\/vr\//.test(d);(d.includes("@realsee/five")||d.includes("@realsee/dnalogel")||g)&&e.push(s("error","architecture.ui-boundary",`${l.relative(r,c)} \u4E0D\u5F97\u5BFC\u5165 src/vr\u3001Five \u6216 dnalogel \u7A7A\u95F4\u8FD0\u884C\u65F6\u3002`))}let k=l.join(r,"src","styles.css");if(await u(k)){let c=await E(k,"utf8");(!c.includes(".five-surface")||!c.includes(".dom-overlay")||!c.includes("pointer-events: none"))&&e.push(s("error","architecture.dom-overlay","\u539F\u751F Five surface \u4E0E DOM overlay \u5FC5\u987B\u4FDD\u6301\u72EC\u7ACB\u5206\u5C42\u53CA pointer-events \u9694\u79BB\u3002"));for(let d of Le)c.includes(d)||e.push(s("error","architecture.viewer-layout",`Viewer \u5FC5\u987B\u4FDD\u6301 safe-area \u53CA\u684C\u9762/\u5E73\u677F/\u624B\u673A/\u77EE\u5C4F\u5916\u5C42\u54CD\u5E94\u5F0F\u5E03\u5C40\uFF1A${d}`));for(let d of Me)c.includes(d)&&e.push(s("error","architecture.viewer-native-vapor",`Viewer \u6837\u5F0F\u4E0D\u5F97\u8986\u76D6 Vapor \u5185\u90E8\u7C7B\u6216\u91CD\u7ED8\u7EC4\u4EF6\u89C6\u89C9\uFF1A${d}`))}return e.push(...await ze(r,".gitignore")),e.push(...await Ge(r,[".agents/skills/realsee-vr-app/SKILL.md","AGENTS.md"],".agents/skills/realsee-vr-app/references/dnalogel-api-capabilities.md")),e.push(...await Ve(r,[".agents/skills/realsee-vr-app/SKILL.md","AGENTS.md"],".agents/skills/realsee-vr-app")),e.push(...await Ke(r,["AGENTS.md",".agents/skills/realsee-vr-app/SKILL.md"],".agents/skills/realsee-vr-app")),e.push(...await We(r,{validator:"shared/model-floorplan.ts",types:"server/types.ts",client:"server/realsee-open-api.ts",service:"server/work-service.ts",workSource:"src/vr/work/WorkSource.ts",feature:"src/features/floorplan/ModelFloorplanFeature.ts",app:"src/app/App.tsx",modeSwitcher:"src/features/navigation/ModeSwitcher.tsx",browserTsconfig:"tsconfig.json",serverTsconfig:"tsconfig.server.json"})),e.push(...await Ie(r,["AGENTS.md",".agents/skills/realsee-vr-app/SKILL.md",".agents/skills/realsee-image-resources/SKILL.md"])),e.push(...await Ce(r,["AGENTS.md",".agents/skills/realsee-vr-app/SKILL.md",".agents/skills/realsee-vr-app/references/vapor-dom-layer.md"])),e.push(...await De(r,".agents/skills/realsee-vr-app/references/vapor-dom-layer.md",".agents/skills/realsee-vr-app/references/ui-and-validation.md")),e.push(...await Nr(r)),e}async function Ue({directory:r=".",json:t=!1}){let e=l.resolve(r),a=l.join(e,"package.json"),n=[];P(process.versions.node,C)||n.push(s("error","node.version",`\u9700\u8981 Node >=${C}\uFF0C\u5F53\u524D ${process.versions.node}`));try{let o=await Or();P(o,D)||n.push(s("error","npm.version",`\u9700\u8981 npm >=${D}\uFF0C\u5F53\u524D ${o}`))}catch(o){n.push(s("error","npm.unavailable","\u65E0\u6CD5\u6267\u884C npm --version\u3002",String(o)))}if(!await u(a))n.push(s("error","package.missing",`\u627E\u4E0D\u5230 package.json\uFF1A${a}`));else try{let o=await re(a);n.push(...await jr(e,o)?await Pr(e,o):await xr(e,o))}catch(o){n.push(s("error","package.invalid","package.json \u65E0\u6CD5\u89E3\u6790\u3002",String(o)))}let i={ok:!n.some(o=>o.level==="error"),root:e,errors:n.filter(o=>o.level==="error").length,warnings:n.filter(o=>o.level==="warning").length,diagnostics:n};if(t)console.log(JSON.stringify(i,null,2));else{for(let o of n)console.log(`${o.level==="error"?"ERROR":"WARN "} [${o.code}] ${o.message}`);console.log(i.ok?`OK ${e}`:`FAILED ${e}`)}return i}var M=Object.freeze({version:1,capabilityOrder:["@realsee/dnalogel","@realsee/five","three"],intentRoutes:Object.freeze([Object.freeze({intent:"tag|hotspot|anchor",defaultCapability:"PanoTagPlugin",source:"GET /open/v3/hot/list"}),Object.freeze({intent:"tour",defaultCapability:"Five State snapshot",source:"GET /open/v3/observerpicture/search"}),Object.freeze({intent:"model-annotation",defaultCapability:"ModelMakerPlugin",source:"GET /open/v3/annotation/search"}),Object.freeze({intent:"floorplan|house-layout",defaultCapability:"ModelFloorplanPlugin",source:"GET /open/v3/floorplan/plugins -> data.format_floorplan_url"}),Object.freeze({intent:"route",defaultCapability:"GuideLinePlugin + CruisePlugin|MovePlugin",source:"GET /open/v3/route/search"})]),invariants:Object.freeze(["OpenAPI data is fetched by the BFF, validated, then adapted to installed SDK types.","SDK-shaped local data requires an explicit Open Platform opt-out.","A fallback records the missing preset capability before using Five or Three.","Only a completely validated { version, data: FloorplanServerData } wrapper from data.format_floorplan_url may expose the modelFloorplan capability.","Missing, empty, malformed or failed floorplan data is feature-local and leaves the base Viewer usable.","Each feature owns cancellation, registrations, hooks, media and Three resources.","Persistent Work or customer-data mutations require an authoritative write contract and confirmation."])});var Ye=`vr-cli ${j}
|
|
1
|
+
import{parseArgs as Mr}from"node:util";var Y="@realsee/vr-cli";var F="https://registry.npmjs.org/",N="public";var j="0.1.5",O=["portable","all","none"],B="22.23.2",C="22.18.0",te=`>=${C}`,D="11.19.0",se=">=11.19.0",ae=`npm@${D}`,G=Object.freeze({"@fastify/static":"10.1.3","@realsee/dnalogel":"3.81.2","@realsee/five":"6.8.9","@realsee/vapor":"0.2.12",fastify:"5.12.1",react:"18.3.1","react-dom":"18.3.1",three:"0.117.1"}),V=Object.freeze({three:G.three}),oe=(r,t)=>r in V?t:`>=${t}`,H=r=>{if(typeof r!="string")return null;let t=/^>=(\d+\.\d+\.\d+(?:[-+][\w.]+)?)$/.exec(r.trim());return t?t[1]:null},P=(r,t)=>{let e=i=>{if(typeof i!="string")return null;let o=/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(i.trim());return o?{core:o.slice(1,4).map(Number),prerelease:o[4]??null}:null},s=e(r),n=e(t);if(!s||!n)return!1;for(let i=0;i<3;i+=1){let o=s.core[i]-n.core[i];if(o>0)return!0;if(o<0)return!1}return s.prerelease===n.prerelease||s.prerelease===null?!0:n.prerelease===null?!1:s.prerelease.localeCompare(n.prerelease,"en",{numeric:!0})>=0},q=Object.freeze({"@eslint/js":"10.0.1","@types/node":"24.13.3","@types/react":"18.3.12","@types/react-dom":"18.3.1","@vitejs/plugin-react":"4.3.1",concurrently:"10.0.5",eslint:"10.9.1","eslint-plugin-react-hooks":"7.1.1",globals:"17.11.0",prettier:"3.9.6",typescript:"5.9.3","typescript-eslint":"8.68.0",vite:"5.4.14"}),ie=Object.freeze(["APP_ID","REALSEE_GATEWAY_ENDPOINT","REALSEE_GATEWAY_APP_KEY","REALSEE_GATEWAY_APP_SECRET","REALSEE_DEFAULT_RESOURCE_CODE","REALSEE_SOURCE"]),ne=Object.freeze(["REALSEE_GATEWAY_APP_KEY","REALSEE_GATEWAY_APP_SECRET"]),le=Object.freeze(["REALSEE_GATEWAY_APP_SECRET"]),ce=Object.freeze(["package.json","package-lock.json","README.md","index.html","tsconfig.json","tsconfig.server.json","vite.config.ts",".editorconfig",".nvmrc",".node-version",".npmrc",".prettierignore","eslint.config.mjs","prettier.config.mjs","scripts/setup-config.mjs","scripts/setup-config.test.mjs","scripts/setup.mjs","server/types.ts","server/config.ts","server/check-config.ts","server/realsee-open-api.ts","server/work-service.ts","server/work-route.ts","server/app.ts","server/index.ts","shared/model-floorplan.ts","src/main.tsx","src/app/App.tsx","src/shared/useElementSize.ts","src/vr/five/api.ts","src/vr/five/fiveConfig.ts","src/vr/five/SpatialRuntimeProvider.tsx","src/vr/five/initialState.ts","src/vr/plugins/dnalogel.ts","src/vr/plugins/SpatialPluginRegistry.ts","src/vr/plugins/examples/FeatureOwnedMeshPlugin.ts","src/vr/work/WorkSource.ts","src/features/floorplan/ModelFloorplanFeature.ts","src/features/navigation/ModeSwitcher.tsx","src/features/tags/PanoTagFeature.ts","src/ui/ViewerOverlay.tsx","src/styles.css",".env.example",".gitignore",".vr-cli.json"]);import{spawn as er}from"node:child_process";import{rename as ye,writeFile as rr}from"node:fs/promises";import A from"node:path";import{fileURLToPath as tr}from"node:url";import _ from"node:path";import{mkdir as qe,readFile as Ze,writeFile as me}from"node:fs/promises";import{mkdir as ue,readFile as Be,readdir as Z,stat as fe,writeFile as pe}from"node:fs/promises";import L from"node:path";var He=new Set(["",".css",".html",".json",".md",".mjs",".ts",".tsx",".txt",".yml",".yaml"]);function Q(r,t){return Object.entries(t).reduce((e,[s,n])=>e.replaceAll(`{{${s}}}`,String(n)).replaceAll(`__${s}__`,String(n)),r)}async function u(r){try{return await fe(r),!0}catch(t){if(t?.code==="ENOENT")return!1;throw t}}async function de(r){if(!await u(r)){await ue(r,{recursive:!0});return}if(!(await fe(r)).isDirectory())throw new Error(`\u76EE\u6807\u8DEF\u5F84\u4E0D\u662F\u76EE\u5F55\uFF1A${r}`);if((await Z(r)).length)throw new Error(`\u76EE\u6807\u76EE\u5F55\u5FC5\u987B\u4E3A\u7A7A\uFF1A${r}`)}async function T(r,t,e={}){if(!await u(r))throw new Error(`\u6A21\u677F\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${r}`);await ue(t,{recursive:!0});for(let s of await Z(r,{withFileTypes:!0})){let n=L.join(r,s.name),i=L.join(t,s.name);if(s.isDirectory()){await T(n,i,e);continue}if(!s.isFile())continue;let o=L.extname(s.name),m=await Be(n);if(!He.has(o)){await pe(i,m);continue}await pe(i,Q(m.toString("utf8"),e))}}async function K(r,t=r){let e=[];for(let s of await Z(t,{withFileTypes:!0})){let n=L.join(t,s.name);s.isDirectory()?e.push(...await K(r,n)):s.isFile()&&e.push(L.relative(r,n).split(L.sep).join("/"))}return e.sort()}var ve=Object.freeze(["realsee-vr-app","realsee-image-resources","threejs-r117-five"]),Qe=Object.freeze(["realsee-vr-app/SKILL.md","realsee-vr-app/references/dnalogel-api-capabilities.md","realsee-vr-app/references/dnalogel-openapi-recipes.md","realsee-vr-app/references/continuous-improvement.md","realsee-vr-app/references/five-api-capabilities.md","realsee-vr-app/references/five-runtime.md","realsee-vr-app/references/official-docs.md","realsee-vr-app/references/plugin-lifecycle.md","realsee-vr-app/references/ui-and-validation.md","realsee-vr-app/references/vapor-dom-layer.md","realsee-vr-app/scripts/verify-project.mjs","realsee-image-resources/SKILL.md","realsee-image-resources/references/cors-and-three-textures.md","realsee-image-resources/references/data-wanxiang.md","realsee-image-resources/references/five-image-url-transform.md","realsee-image-resources/references/source-and-domain-policy.md","threejs-r117-five/SKILL.md","threejs-r117-five/references/five-ownership.md","threejs-r117-five/references/r117-capabilities.md","threejs-r117-five/references/r117-version-guards.md"]),X=r=>Qe.map(t=>_.posix.join(r,t)),ge=Object.freeze(["AGENTS.md",...X(".agents/skills")]),Xe=Object.freeze([".agents/README.md",...X(".claude/skills"),...X(".kiro/skills"),"CLAUDE.md",".cursor/rules/realsee-vr.mdc",".github/copilot-instructions.md",".kiro/steering/realsee-vr.md"]),he=Object.freeze([".agents",".claude/skills",".kiro/skills",".cursor/rules",".kiro/steering"]),we=Object.freeze(["AGENTS.md","CLAUDE.md",".github/copilot-instructions.md"]),Ee=Object.freeze([".claude/skills",".kiro/skills",".cursor/rules",".kiro/steering"]),Se=Object.freeze([".agents/README.md","CLAUDE.md",".github/copilot-instructions.md"]),Je=Object.freeze([...ge,...Xe]);function W(r){if(r==="none")return[];if(r==="portable")return[...ge];if(r==="all")return[...Je];throw new Error(`\u672A\u77E5 agent assets \u6A21\u5F0F\uFF1A${r}`)}async function ke({toolRoot:r,targetRoot:t,mode:e,tokens:s}){if(e==="none")return[];for(let i of ve)await T(_.join(r,"skills",i),_.join(t,".agents","skills",i),s);let n=await Ze(_.join(r,"agent-assets","AGENTS.md"),"utf8");if(await me(_.join(t,"AGENTS.md"),Q(n,s)),e!=="all")return W(e);for(let i of[".claude/skills",".kiro/skills"])for(let o of ve)await T(_.join(r,"skills",o),_.join(t,i,o),s);return await T(_.join(r,"agent-assets","adapters"),t,s),await qe(_.join(t,".agents"),{recursive:!0}),await me(_.join(t,".agents","README.md"),"# Agent assets\n\n`skills/realsee-vr-app`, `skills/realsee-image-resources` and `skills/threejs-r117-five` are the three canonical portable Skills. Vendor-specific copies are generated; edit the canonical copies and regenerate adapters.\n"),W(e)}var sr=!0,J=tr(new URL(sr?"..":"../..",import.meta.url)),ar=process.platform==="win32"?"npm.cmd":"npm";function or(r,t){let e=t||A.basename(r),s=e.trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"-").replace(/^-+|-+$/g,"");if(!s||!/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/.test(s))throw new Error(`\u65E0\u6CD5\u751F\u6210\u5408\u6CD5 npm \u5305\u540D\uFF1A${e}`);return s}function ir(r){return new Promise((t,e)=>{let s=er(ar,["ci"],{cwd:r,stdio:"inherit",shell:!1});s.once("error",e),s.once("exit",(n,i)=>{n===0?t():e(new Error(`npm ci \u5931\u8D25\uFF08code=${n??"null"}, signal=${i??"none"}\uFF09`))})})}async function be({directory:r,preset:t,name:e,agentAssets:s="portable",install:n=!0}){if(!r)throw new Error("\u7F3A\u5C11\u76EE\u6807\u76EE\u5F55\u3002\u7528\u6CD5\uFF1Avr-cli create <directory>");if(t&&t!=="viewer")throw new Error("--preset \u6682\u65F6\u53EA\u652F\u6301 viewer\u3002");if(!O.includes(s))throw new Error(`\u672A\u77E5 agent assets \u6A21\u5F0F\uFF1A${s}\u3002\u53EF\u9009\u503C\uFF1A${O.join(", ")}`);let i=A.resolve(r),o=or(i,e),m={PACKAGE_NAME:o,TOOL_VERSION:j};await de(i),await T(A.join(J,"templates","base"),i,m),await ye(A.join(i,".gitignore.template"),A.join(i,".gitignore")),await ye(A.join(i,".npmrc.template"),A.join(i,".npmrc")),await T(A.join(J,"templates","viewer"),i,m);let f=await ke({toolRoot:J,targetRoot:i,mode:s,tokens:m}),p={schemaVersion:1,toolVersion:j,preset:"viewer",packageName:o,agentAssets:s,generatedAt:new Date().toISOString()};return await rr(A.join(i,".vr-cli.json"),`${JSON.stringify(p,null,2)}
|
|
2
|
+
`),n&&await ir(i),{targetRoot:i,packageName:o,installedAgentAssets:f,installed:n}}import{execFile as nr}from"node:child_process";import{readFile as E,readdir as lr,stat as cr}from"node:fs/promises";import l from"node:path";import{promisify as pr}from"node:util";var ur=pr(nr),a=(r,t,e,s)=>({level:r,code:t,message:e,...s?{detail:s}:{}}),_e=r=>/(?:^|_)(?:AK|SK|SECRET|TOKEN|PASSWORD|CREDENTIALS?)(?:_|$)/i.test(r),fr=Object.freeze([/\bAPP_ID\b/,/\bREALSEE_(?:GATEWAY_[A-Z0-9_]+|SOURCE|ACCESS_TOKEN)\b/,/\b(?:VITE_)?(?:AK|SK|ACCESS_TOKEN)\b/]),ee=r=>{for(let t of fr){let e=t.exec(r);if(e)return e[0]}return null},k=r=>r.replace(/\/\*[\s\S]*?\*\//g,"").replace(/(^|[^:])\/\/.*$/gm,"$1"),dr=Object.freeze([{pattern:/three\/addons\//,message:"r117 \u6269\u5C55\u6A21\u5757\u5FC5\u987B\u4ECE three/examples/jsm \u5BFC\u5165\u3002"},{pattern:/three\/tsl\b/,message:"Three r117 \u4E0D\u63D0\u4F9B\u73B0\u4EE3 TSL \u5165\u53E3\u3002"},{pattern:/\b(?:SRGBColorSpace|LinearSRGBColorSpace|NoColorSpace|CapsuleGeometry|WebGPURenderer)\b/,message:"\u6E90\u7801\u4F7F\u7528\u4E86 Three r117 \u4E0D\u5B58\u5728\u7684\u73B0\u4EE3\u7B26\u53F7\u3002"},{pattern:/\b(?:renderer|[\w$]*Renderer)\.outputColorSpace\b/,message:"Three r117 \u4F7F\u7528 renderer.outputEncoding\u3002"},{pattern:/\b(?:texture|[\w$]*(?:map|Map|texture|Texture))\.colorSpace\s*=/,message:"Three r117 \u7EB9\u7406\u4F7F\u7528 encoding\uFF0C\u800C\u4E0D\u662F colorSpace\u3002"}]),mr=Object.freeze(["from '@realsee/five'","from 'three'","FEATURE_OWNED_MESH_PLUGIN_NAME","example.featureOwnedMesh","new Group(","new BoxBufferGeometry(","new MeshBasicMaterial(","new Mesh(","five.scene.add(root)","root.parent?.remove(root)","five.needsRender = true","geometry.dispose()","material.dispose()","plugins.register(FEATURE_OWNED_MESH_PLUGIN_NAME, plugin)","plugin.dispose()"]),vr=Object.freeze(["new Five(","new WebGLRenderer(","renderer.render(","requestAnimationFrame(","five.dispose(","three/addons/"]),gr=Object.freeze(["Image resources use approved Realsee CDN domains only; never use `ljcdn.com`, `lianjia.com` or their subdomains.","Prefer Five `defaultImageURLTransform` over hand-built \u6570\u636E\u4E07\u8C61 query strings when its installed contract satisfies the request.","URL transformation does not grant CORS access.","Ordinary images use the deployment's approved image-4 host.","Feature media from OpenAPI or Work data is displayed through a derived sized variant, never the original: pass ordinary images through the reviewed `defaultImageURLTransform` adapter with a bounded `size`, and do not hand a full-resolution URL to a tag, tour, card or DOM thumbnail."]),hr=Object.freeze(["When the user explicitly requests Vapor components, use matching named exports from the installed `@realsee/vapor` package for every applicable DOM control; importing `Vapor` or `vapor.css` alone does not satisfy the request.","Do not replace an available Vapor component with raw HTML or a locally styled replica; use raw DOM only for semantics or capabilities absent from the installed Vapor declarations, and record that gap.","Before implementing Vapor UI, read `node_modules/@realsee/vapor/AI_CONTEXT.md`, `llms.txt`, `vapor.d.ts`, and the selected component declarations; never invent component names or props.","Use the installed `ai_guides` only for component discovery and composition recipes; the target project's root `vapor.d.ts` and selected component declarations are the API contract, and conflicts resolve in favor of installed declarations.","Keep the Five canvas in its dedicated sibling host outside `Vapor`; Vapor layout components own DOM chrome only, even when an external recipe nests a placeholder VR component inside a stack."]),wr=Object.freeze(["The current generated baseline is `@realsee/vapor@0.2.12`.","The 0.2.12 baseline requires exactly one executable `@realsee/vapor/vapor.css` import, not `vapor.scss`","Choose by product semantics before styling:","`HStack` arranges DOM horizontally, `VStack` vertically and `ZStack` overlays DOM children.","The 0.2.12 match union is exact:","if both represent the same physical inset, de-duplicate rather than adding it twice."]),Er=Object.freeze(["For an explicit Vapor request, executable source must import and render the matching non-provider Vapor component; the provider and stylesheet alone are insufficient.","exactly one executable `@realsee/vapor/vapor.css` import and no `vapor.scss` or copied internal stylesheet;","the `Vapor` provider rectangle matches the DOM overlay host","safe-area behavior uses the declared source/merge rule without double-counting","correct portal order, focus entry/return, Escape/backdrop behavior and close reasons;","no application stylesheet overrides `.vapor-*` internals","typecheck validates every component prop, compound child and symbol"]),Sr=/\bimport\s*['"]@realsee\/vapor\/vapor\.css['"]\s*;?/,Ae=r=>r.match(new RegExp(Sr.source,"g"))?.length??0,z=(r,t)=>new RegExp(`\\bimport\\s*\\{[^}]*\\b${t}\\b[^}]*\\}\\s*from\\s*['"]@realsee/vapor['"]`).test(r),U=(r,t)=>new RegExp(`<${t}(?:\\s|/?>)`).test(r),kr=Object.freeze(["Spatial capability resolution order: installed @realsee/dnalogel preset -> native Five -> Three.js.","Tags, hotspots, and anchors default to PanoTagPlugin with GET /open/v3/hot/list.","Normalize optional OpenAPI configuration fields such as `version`, `globalConfig` and `contentTypeConfig`: treat `null` as absent before validating the installed SDK contract.","Load ordinary PanoTag point/plane tags after `five.load` without waiting for `five.model.loaded`; gate only model-dependent tag content or anchors on model readiness.","Tours use Five State snapshots with GET /open/v3/observerpicture/search.","Model annotations default to ModelMakerPlugin with GET /open/v3/annotation/search.","Routes use GuideLinePlugin for rendering and shape-selected CruisePlugin or MovePlugin playback with GET /open/v3/route/search.","Tour adapters read `data.list`, map each validated record to `Partial<State>` with the active `workCode`, and never cast an observer-picture record directly to `State`.","Model annotation adapters read `data.list`, validate `object_data` for `box`, `triangles` or `prism`, and pass `{ list }` to ModelMaker only for a model-capable Work.","Feature API, schema, or plugin failures disable only the affected feature; keep the base Viewer usable and never runtime-fallback to raw data or a custom Three implementation.","Scope tour, ModelMaker, GuideLine, Cruise and Move work to an active `workCode`/generation token; abort fetches, ignore stale continuations, pause the owned playback controller, remove hooks, clear GuideLine data, wait for pending loads, then let identity-safe unregister dispose each registered controller exactly once before `five.dispose()`.","SDK-shaped custom data requires an explicit developer opt-out from Open Platform data.","VR spatial tag (spatial annotation) is the umbrella concept","Classify content, renderer, anchor, and lifecycle/data ownership","Never infer PanoTag from","Never directly edit `work.json`, downloaded Work, or customer Work"]),R=Object.freeze(["dnalogel 3.81.2 route playback is shape-selected: a validated pano group uses `CruisePlugin` `{ panoIndexList }`, while a validated model-space curve uses `MovePlugin` `{ path }`; `GuideLinePlugin` renders route geometry and is not playback input.","`CruisePlugin` accepts exactly one of `panoIndexList` or `keyframes`; its installed union has no type discriminator.","OpenAPI route records are never passed raw: rename validated style config only for GuideLine `lines`, and never pass GuideLine `PathItem` arrays or style objects to `CruisePlugin` or `MovePlugin`.","If a source record contains both `pano_group` and `path`, the route adapter must choose product semantics explicitly and pass only the selected geometry branch to GuideLine.","`GuideLinePlugin.load` waits for model readiness; `CruisePlugin` waits only for Work and observers; `MovePlugin` enters Model and awaits Five readiness when playback starts.","Reject empty routes, unknown pano indexes, unsupported curves, non-finite coordinates, timing or speed, and ambiguous multi-segment Move paths before plugin load.","Route cleanup pauses the owned playback controller before hook removal and disposal; `clear` belongs to GuideLine, is not a public Cruise operation, and GuideLine disposal does not own an external or embedded Cruise controller automatically.","Do not invent `RoutePlugin`, `PanoRove`, `stop()`, or a universal route controller; use only the exports and methods declared by the installed package."]),Oe=Object.freeze(["Floorplans default to ModelFloorplanPlugin with GET /open/v3/floorplan/plugins.","`data.format_floorplan_url`, not `format_model_url`, resolves to the preferred `{ version, data: FloorplanServerData }` plugin input.","Expose `capabilities.modelFloorplan` only after validating the complete wrapper and nested floorplan shape; missing, empty, malformed or failed floorplan data must leave the base Viewer usable.","The default tabs are `\u6F2B\u6E38` for `Panorama` and `\u4E09\u7EF4\u6A21\u578B` for `Mapview`; do not expose `Topview` as `\u5730\u56FE`.","Add the `\u5E73\u9762\u56FE` tab only after the capability validates and `ModelFloorplanPlugin.load` succeeds.","Configure `ModelFloorplanPlugin` with `autoShowEnable: false`; the application tab owns explicit `show()`/`hide()`, uses `modelOpacity: 0` while the floorplan is visible, and restores the Five model surface on hide, failure, and disposal.","Resolve the Five canvas parent and call `ModelFloorplanPlugin.appendTo(wrapper)` before `load`; `five.addExtraElement` registers input handling but does not mount the plugin DOM.","For multi-floor Work, ModelFloorplanPlugin and Five pano/model floor synchronization are the floor source of truth; do not invent a React-only `floorIndex`.","Floorplan show/hide, hooks, pending load and identity-safe disposal must finish before `five.dispose()`."]),yr=Object.freeze(Oe.slice(0,3)),br="The complete model floorplan contract lives in",_r="The complete route playback contract lives in",Ar=Object.freeze([R[0],R[7],_r]),Te=Object.freeze(["Vapor","Text","TitleBar","ZStack","VStack","Layer"]),Re=Object.freeze(["Button","Font","HStack","VStack"]),je=Object.freeze(["title?: string | null",'className="viewer-vapor"','width="100%"','height="100%"','theme="arc_beauty"',"useSaveAreaInsets()","useDimension()","<Layer vignetting />","labelMaxWidth={titleMaxWidth}",'className="viewer-controls"',"align={{ horizontal: 'center' }}",'className="viewer-status"','appearance="solid"',"role={error ? 'alert' : 'status'}","aria-live={error ? 'assertive' : 'polite'}","env(safe-area-inset-${side}, 0px)"]),Pe=Object.freeze(['role="group"',"useDimension()","width={controlWidth}","height={compact ? '3rem' : '5rem'}","align={{ horizontal: 'space-around' }}",'appearance="shadow"',"weight={selected ? 'bold' : 'regular'}","opacity={selected ? '100' : '80'}",'className="mode-switcher-indicator"',"onClick={() => void changeTab(option.value)}"]),Le=Object.freeze([".dom-overlay",".viewer-controls",".mode-switcher","pointer-events: auto",".mode-switcher-indicator","bottom: -0.5rem","width: 3.375rem","height: 0.125rem","background-image: linear-gradient(",".viewer-status"]),Ne=Object.freeze(["viewer-glass",'className="viewer-topbar"',"<h1"]),xe=Object.freeze(["Segmented","viewer-segmented","viewer-glass",'appearance="horizontal"','maxWidth="100%"']),Me=Object.freeze(["--viewer-glass-","--viewer-control-size",".vapor-segmented","backdrop-filter"]);async function re(r){return JSON.parse(await E(r,"utf8"))}async function Or(){let r=process.platform==="win32"?"npm.cmd":"npm",{stdout:t}=await ur(r,["--version"],{encoding:"utf8"});return t.trim()}async function $e(r,t){let e=[];for(let[n,i,o]of[["packageManager",t.packageManager,ae],["engines.node",t.engines?.node,te],["engines.npm",t.engines?.npm,se]])i!==o&&e.push(a("error","toolchain.version",`package.json ${n} \u5FC5\u987B\u4E3A ${o}\uFF0C\u5F53\u524D\u4E3A ${i??"\u672A\u58F0\u660E"}`));for(let n of[".nvmrc",".node-version"]){let i=l.join(r,n);if(await u(i)){let o=(await E(i,"utf8")).trim();o!==B&&e.push(a("error","toolchain.node-pin",`${n} \u5FC5\u987B\u56FA\u5B9A Node ${B}\uFF0C\u5F53\u524D\u4E3A ${o||"\u7A7A"}`))}}let s=l.join(r,".npmrc");if(await u(s)){let n=new Set((await E(s,"utf8")).split(/\r?\n/).map(i=>i.trim()).filter(Boolean));for(let i of["engine-strict=true","package-lock=true","save-prefix=>="])n.has(i)||e.push(a("error","toolchain.npmrc",`.npmrc \u5FC5\u987B\u5305\u542B ${i}`))}return e}async function Tr(r,t){let e=l.join(r,"package-lock.json");if(!await u(e))return[];try{let s=await re(e),n=s.packages?.[""],i=[];if(s.lockfileVersion!==3||!n)return[a("error","lockfile.invalid","package-lock.json \u5FC5\u987B\u4F7F\u7528 lockfileVersion 3 \u5E76\u5305\u542B\u6839\u5305\u8BB0\u5F55\u3002")];(s.name!==t.name||n.name!==t.name||n.version!==t.version)&&i.push(a("error","lockfile.package","package-lock.json \u6839\u5305\u540D\u79F0\u6216\u7248\u672C\u4E0E package.json \u4E0D\u4E00\u81F4\u3002"));for(let[o,m,f,p]of[["dependency",n.dependencies,t.dependencies,G],["dev-dependency",n.devDependencies,t.devDependencies,q]])for(let[w,y]of Object.entries(f??{})){m?.[w]!==y&&i.push(a("error",`lockfile.${o}`,`package-lock.json \u4E0E package.json \u4E0D\u4E00\u81F4\uFF1A${w}@${y}\uFF1B\u8BF7\u8FD0\u884C npm run deps:relock\u3002`));let b=p[w];if(!b)continue;let c=s.packages?.[`node_modules/${w}`]?.version;if(!c){i.push(a("error","lockfile.resolved",`package-lock.json \u7F3A\u5C11\u76F4\u63A5\u4F9D\u8D56 ${w} \u7684\u89E3\u6790\u7248\u672C\u3002`));continue}if(w in V){c!==b&&i.push(a("error","lockfile.locked",`package-lock.json \u5FC5\u987B\u5C06 ${w} \u89E3\u6790\u4E3A ${b}\uFF0C\u5F53\u524D\u4E3A ${c}\u3002`));continue}let d=H(y);d&&(!P(c,b)||!P(c,d))&&i.push(a("error","lockfile.minimum",`${w} \u5728 package-lock.json \u4E2D\u89E3\u6790\u4E3A ${c}\uFF0C\u5FC5\u987B\u540C\u65F6\u6EE1\u8DB3\u751F\u6210\u5668\u57FA\u7EBF >=${b} \u4E0E\u58F0\u660E ${y}\u3002`))}return i}catch(s){return[a("error","lockfile.invalid","package-lock.json \u65E0\u6CD5\u89E3\u6790\u3002",String(s))]}}async function x(r){let t=[];for(let e of await lr(r,{withFileTypes:!0})){let s=l.join(r,e.name);e.isDirectory()?t.push(...await x(s)):e.isFile()&&/\.(?:[cm]?[jt]sx?)$/.test(e.name)&&t.push(s)}return t}async function Fe(r,t){let e=l.join(r,t);if(!await u(e))return[];let s=k(await E(e,"utf8")),n=[];for(let m of mr)s.includes(m)||n.push(a("error","architecture.custom-plugin-example",`${t} \u7F3A\u5C11\u81EA\u5B9A\u4E49\u63D2\u4EF6\u6240\u6709\u6743\u6807\u8BB0\uFF1A${m}`));for(let m of vr)s.includes(m)&&n.push(a("error","architecture.custom-plugin-example",`${t} \u7981\u6B62\u62E5\u6709 Five \u8FD0\u884C\u65F6\u6216\u7B2C\u4E8C\u6E32\u67D3\u5FAA\u73AF\uFF1A${m}`));let i=t.startsWith("templates/")?"templates/viewer/src/features/tags/PanoTagFeature.ts":"src/features/tags/PanoTagFeature.ts",o=l.join(r,i);if(await u(o)){let m=k(await E(o,"utf8"));for(let f of["from '@realsee/dnalogel/libs/PanoTagPlugin'","const PLUGIN_NAME = 'dnalogel.panoTag'","const candidate = PanoTagPlugin(","plugins.register(PLUGIN_NAME, candidate)","candidate.dispose()","async load(panoTagList: Tags['tagList'])","plugin.load({ tagList: panoTagList })","const disposeError = plugins.unregister(PLUGIN_NAME, plugin)"])m.includes(f)||n.push(a("error","architecture.pano-tag-contract",`${i} \u7F3A\u5C11 PanoTag \u517C\u5BB9\u6027\u3001\u663E\u5F0F\u7C7B\u578B\u6216\u6240\u6709\u6743\u6807\u8BB0\uFF1A${f}`));for(let f of["model_file","as Tags['tagList']","from '../../vr/work/WorkSource'"])m.includes(f)&&n.push(a("error","architecture.pano-tag-contract",`${i} \u542B\u975E\u6807\u51C6 content type \u6216\u672A\u9A8C\u8BC1\u7684 PanoTag \u8F6C\u6362\uFF1A${f}`))}return n}async function Ie(r,t){let e=[];for(let s of t){let n=l.join(r,s);if(!await u(n))continue;let i=await E(n,"utf8");for(let o of gr)i.includes(o)||e.push(a("error","agents.image-resource-guidance",`${s} \u7F3A\u5C11\u56FE\u7247\u8D44\u6E90\u57DF\u540D\u3001Five URL transform\u3001\u6570\u636E\u4E07\u8C61\u6216 CORS \u89C4\u5219\uFF1A${o}`))}return e}async function Ce(r,t){let e=[];for(let s of t){let n=l.join(r,s);if(!await u(n))continue;let i=await E(n,"utf8");for(let o of hr)i.includes(o)||e.push(a("error","agents.vapor-guidance",`${s} \u7F3A\u5C11 Vapor \u771F\u5B9E\u7EC4\u4EF6\u9009\u62E9\u3001\u58F0\u660E\u6838\u5BF9\u6216 raw DOM \u4F8B\u5916\u89C4\u5219\uFF1A${o}`))}return e}async function De(r,t,e){let s=[];for(let[n,i,o,m]of[[t,wr,"agents.vapor-reference","\u7248\u672C\u5316\u8D44\u6599\u8DEF\u7531\u3001\u7EC4\u4EF6\u9009\u578B\u3001\u5E03\u5C40\u6216 safe-area \u89C4\u5219"],[e,Er,"agents.vapor-acceptance","\u8FD0\u884C\u65F6 Vapor \u9A8C\u6536\u89C4\u5219"]]){let f=l.join(r,n);if(!await u(f))continue;let p=await E(f,"utf8");for(let w of i)p.includes(w)||s.push(a("error",o,`${n} \u7F3A\u5C11 ${m}\uFF1A${w}`))}return s}async function Ge(r,t,e){let s=[];for(let i of t){let o=l.join(r,i);if(!await u(o))continue;let m=await E(o,"utf8");for(let f of kr)m.includes(f)||s.push(a("error","agents.spatial-tag-guidance",`${i} \u7F3A\u5C11\u7A7A\u95F4\u6807\u7B7E\u51B3\u7B56\u6216\u53EA\u8BFB\u89C4\u5219\uFF1A${f}`))}let n=l.join(r,e);if(await u(n)){let i=await E(n,"utf8");for(let o of["Spatial capability resolution order: installed @realsee/dnalogel preset -> native Five -> Three.js.","Tags, hotspots, and anchors default to PanoTagPlugin with GET /open/v3/hot/list.","Normalize optional OpenAPI configuration fields such as `version`, `globalConfig` and `contentTypeConfig`: treat `null` as absent before validating the installed SDK contract.","Load ordinary PanoTag point/plane tags after `five.load` without waiting for `five.model.loaded`; gate only model-dependent tag content or anchors on model readiness.","Tours use Five State snapshots with GET /open/v3/observerpicture/search.","Model annotations default to ModelMakerPlugin with GET /open/v3/annotation/search.","Routes use GuideLinePlugin for rendering and shape-selected CruisePlugin or MovePlugin playback with GET /open/v3/route/search.","Tour adapters read `data.list`, map each validated record to `Partial<State>` with the active `workCode`, and never cast an observer-picture record directly to `State`.","Model annotation adapters read `data.list`, validate `object_data` for `box`, `triangles` or `prism`, and pass `{ list }` to ModelMaker only for a model-capable Work.","Feature API, schema, or plugin failures disable only the affected feature; keep the base Viewer usable and never runtime-fallback to raw data or a custom Three implementation.","Scope tour, ModelMaker, GuideLine, Cruise and Move work to an active `workCode`/generation token; abort fetches, ignore stale continuations, pause the owned playback controller, remove hooks, clear GuideLine data, wait for pending loads, then let identity-safe unregister dispose each registered controller exactly once before `five.dispose()`.","SDK-shaped custom data requires an explicit developer opt-out from Open Platform data.","`model_file` is not canonical","`ModelTVVideoPlugin`","`CSS3DRenderPlugin.create3DDomContainer`","`PanoVideoPlugin`","generic `tags`, `tagList` and `tag_list`","`ServerData` with `box`, `triangles` or `prism` items"])i.includes(o)||s.push(a("error","agents.spatial-tag-capabilities",`${e} \u7F3A\u5C11\u7A7A\u95F4\u80FD\u529B\u4F18\u5148\u7EA7\u3001\u6570\u636E\u6E90\u6216\u63D2\u4EF6\u7C7B\u578B\u8DEF\u7531\uFF1A${o}`))}return s}async function Ve(r,t,e){let s=[],n=[...t.map(i=>[i,i.endsWith("SKILL.md")?Ar:R]),[`${e}/references/dnalogel-api-capabilities.md`,R],[`${e}/references/five-runtime.md`,R.slice(4,6)],[`${e}/references/plugin-lifecycle.md`,R.slice(6,8)],[`${e}/references/official-docs.md`,R.slice(2,4)]];for(let[i,o]of n){let m=l.join(r,i);if(!await u(m))continue;let f=await E(m,"utf8");for(let p of o)f.includes(p)||s.push(a("error","agents.route-guidance",`${i} \u7F3A\u5C11\u70B9\u4F4D/\u81EA\u5B9A\u4E49\u8DEF\u7EBF\u5206\u6D41\u3001\u6821\u9A8C\u6216\u751F\u547D\u5468\u671F\u89C4\u5219\uFF1A${p}`))}return s}async function Ke(r,t,e){let s=[],n=[[`${e}/references/dnalogel-api-capabilities.md`,Oe],...t.map(i=>[i,yr]),...["five-runtime.md","official-docs.md","plugin-lifecycle.md","ui-and-validation.md"].map(i=>[`${e}/references/${i}`,[br]])];for(let[i,o]of n){let m=l.join(r,i);if(!await u(m))continue;let f=await E(m,"utf8");for(let p of o)f.includes(p)||s.push(a("error","agents.floorplan-guidance",`${i} \u7F3A\u5C11\u5E73\u9762\u56FE endpoint\u3001\u6821\u9A8C\u3001\u52A8\u6001 tab\u3001\u591A\u697C\u5C42\u6216\u6E05\u7406\u89C4\u5219\uFF1A${p}`))}return s}async function We(r,t){let e=[],s=[[t.validator,["export function asModelFloorplanServerData(","!isInteger(value.version)","value.version < 0","!isFloorplanServerData(value.data)","value.outlines.length > 0","value.floor_datas.length > 0","isArrayOf(value.rooms, isRoom)","isRules(value.rules)","value.entrance === null || isEntrance(value.entrance)","isBounding(value.bounding)","isArrayOf(value.observers, isObserver)","isArrayOf(value.doors, isDoor)"]],[t.types,["WorkCapabilities","modelFloorplan?: ModelFloorplanServerData","ModelFloorplanClient","RealseeSpatialClient","extends WorkDetailClient, ModelFloorplanClient"]],[t.client,["implements RealseeSpatialClient","FLOORPLAN_PLUGINS_PATH = '/open/v3/floorplan/plugins'","asRecord(descriptor.data).format_floorplan_url","floorplanUrl.protocol !== 'https:'","floorplanUrl.username","floorplanUrl.password","redirect: 'error'","asModelFloorplanServerData(payload)"]],[t.service,[".getModelFloorplan(resourceCode, { signal })",".catch((error: unknown) =>","modelFloorplan ? { capabilities: { modelFloorplan } } : {}"]],[t.workSource,["modelFloorplan?: ModelFloorplanServerData","asModelFloorplanServerData(","asRecord(envelope.capabilities).modelFloorplan","modelFloorplan ? { capabilities: { modelFloorplan } } : {}"]],[t.feature,["@realsee/dnalogel/libs/floorplan/ModelFloorplanPlugin","dnalogel.modelFloorplan","ModelFloorplanPlugin(five, { autoShowEnable: false })","plugins.register(PLUGIN_NAME, candidate)","candidate.dispose()","const modelSurface = five.getElement()","const wrapper = modelSurface?.parentElement","plugin.appendTo(wrapper)","plugin.load(serverData)","modelOpacity: 0","plugin.hide({ userAction: true })",".finally(restoreModelSurface)","if (!state.visible) restoreModelSurface()","plugins.unregister(PLUGIN_NAME, plugin)","stopStateChange()","if (disposeRequested) finalize()","if (!loading) finalize()"]],[t.app,["payload.capabilities?.modelFloorplan","if (modelFloorplan)","createModelFloorplanFeature(five, plugins)","await currentFloorplan.load(modelFloorplan)","currentFloorplan.dispose()","setFloorplanFeature(floorplan)","floorplan?.dispose()"]],[t.modeSwitcher,["floorplan?.subscribeVisibility","await floorplan.show()","if (floorplan?.visible) await floorplan.hide()","floorplan?.visible ? 'floorplan' : tabFromFiveMode(five.currentMode)","Five.Mode.Panorama","Five.Mode.Mapview"]]];for(let[o,m]of s){let f=l.join(r,o);if(!await u(f)){e.push(a("error","architecture.floorplan-missing",`\u7F3A\u5C11\u5E73\u9762\u56FE\u5408\u540C\u6587\u4EF6\uFF1A${o}`));continue}let p=k(await E(f,"utf8"));for(let w of m)p.includes(w)||e.push(a("error","architecture.floorplan-contract",`${o} \u7F3A\u5C11\u5E73\u9762\u56FE\u6821\u9A8C\u3001capability \u6216\u751F\u547D\u5468\u671F\u6807\u8BB0\uFF1A${w}`))}let n=l.join(r,t.feature);if(await u(n)){let o=k(await E(n,"utf8"));for(let[v,h]of[[/plugin\.hooks\.on\(\s*['"]show['"]/,"show hook"],[/plugin\.hooks\.on\(\s*['"]hide['"]/,"hide hook"],[/plugin\.hooks\.on\(\s*['"]stateChange['"]/,"stateChange hook"]])v.test(o)||e.push(a("error","architecture.floorplan-lifecycle",`${t.feature} \u7F3A\u5C11 ${h}\u3002`));/autoShowEnable\s*:\s*true/.test(o)&&e.push(a("error","architecture.floorplan-lifecycle",`${t.feature} \u5FC5\u987B\u4FDD\u6301 autoShowEnable: false\u3002`));let m=o.indexOf("plugin.appendTo(wrapper)"),f=o.indexOf("plugin.load(serverData)");(m<0||f<m)&&e.push(a("error","architecture.floorplan-mount",`${t.feature} \u5FC5\u987B\u5148\u628A\u63D2\u4EF6 DOM appendTo Five \u5BBF\u4E3B\uFF0C\u518D\u52A0\u8F7D format_floorplan_url \u6570\u636E\u3002`));let p=o.indexOf("stopShow()"),w=o.indexOf("stopHide()"),y=o.indexOf("stopStateChange()"),b=o.indexOf("if (!loading) finalize()"),c=o.indexOf("const finalize ="),d=o.indexOf("restoreModelSurface()",c),g=o.indexOf("plugins.unregister(PLUGIN_NAME, plugin)",c);(p<0||w<p||y<w||b<y||c<0||d<c||g<d)&&e.push(a("error","architecture.floorplan-cleanup",`${t.feature} \u5FC5\u987B\u5728 hide\u3001\u5931\u8D25\u4E0E\u9500\u6BC1\u65F6\u6062\u590D Five model surface\uFF0C\u5E76\u5148\u79FB\u9664\u5168\u90E8 hooks\uFF0C\u518D\u6309 pending load \u72B6\u6001 identity-safe \u6E05\u7406\u3002`))}let i=l.join(r,t.modeSwitcher);if(await u(i)){let o=k(await E(i,"utf8"));for(let[f,p]of[[/type\s+ViewerTab\s*=\s*['"]tour['"]\s*\|\s*['"]model['"]\s*\|\s*['"]floorplan['"]/,"\u4E09\u4E2A ViewerTab"],[/value\s*:\s*['"]tour['"]\s*,\s*text\s*:\s*['"]漫游['"]/,"Panorama \u6F2B\u6E38 tab"],[/value\s*:\s*['"]model['"]\s*,\s*text\s*:\s*['"]三维模型['"]/,"Mapview \u4E09\u7EF4\u6A21\u578B tab"],[/\.\.\.\(\s*floorplan\s*\?\s*\[\{\s*value\s*:\s*['"]floorplan['"](?:\s+as\s+const)?\s*,\s*text\s*:\s*['"]平面图['"]\s*\}\]\s*:\s*\[\]\s*\)/,"\u6761\u4EF6\u5E73\u9762\u56FE tab"]])f.test(o)||e.push(a("error","architecture.floorplan-tabs",`${t.modeSwitcher} \u7F3A\u5C11 ${p}\u3002`));(o.match(/floorplan\?\.visible\s*\?\s*['"]floorplan['"]\s*:\s*tabFromFiveMode\(five\.currentMode\)/g)?.length??0)<2&&e.push(a("error","architecture.floorplan-tabs",`${t.modeSwitcher} \u5FC5\u987B\u5728\u521D\u59CB\u72B6\u6001\u548C\u5207\u6362\u5931\u8D25\u540E\u90FD\u6309\u63D2\u4EF6\u771F\u5B9E\u53EF\u89C1\u6027\u540C\u6B65 Tab\u3002`)),(o.includes("Five.Mode.Topview")||/text\s*:\s*['"]地图['"]/.test(o))&&e.push(a("error","architecture.floorplan-tabs",`${t.modeSwitcher} \u4E0D\u5F97\u628A Topview \u66B4\u9732\u4E3A\u5730\u56FE tab\u3002`))}for(let[o,m]of[[t.browserTsconfig,'"include": ["src", "shared", "vite.config.ts"]'],[t.serverTsconfig,'"include": ["server", "shared"]']]){let f=l.join(r,o);if(!await u(f))continue;(await E(f,"utf8")).includes(m)||e.push(a("error","tooling.floorplan-include",`${o} \u5FC5\u987B\u5305\u542B shared floorplan validator\uFF1A${m}`))}return e}function Rr(r){let t=new Map;for(let e of r.split(/\r?\n/)){let s=e.trim();if(!s||s.startsWith("#"))continue;let n=/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(s);n&&t.set(n[1],n[2].trim())}return t}async function ze(r,t){let e=[],s=l.join(r,".env.example");if(await u(s)){let f=Rr(await E(s,"utf8"));for(let p of ie)f.has(p)||e.push(a("error","env.required-key",`.env.example \u7F3A\u5C11 ${p}\u3002`));for(let[p,w]of f)p.startsWith("VITE_WORK_")&&e.push(a("error","env.legacy-work-url",`.env.example \u4E0D\u5F97\u58F0\u660E\u8DEF\u7531\u6A21\u5F0F\u4E0D\u9700\u8981\u7684 ${p}\u3002`)),p.startsWith("VITE_")&&_e(p)&&e.push(a("error","env.client-secret",`.env.example \u7981\u6B62\u58F0\u660E\u6D4F\u89C8\u5668\u53EF\u89C1\u7684\u5BC6\u94A5\u53D8\u91CF ${p}\u3002`)),_e(p)&&!le.includes(p)&&w&&w!=="*"&&e.push(a("error","env.secret",`.env.example \u7684 ${p} \u53EA\u80FD\u4E3A\u7A7A\u6216\u4F7F\u7528 * \u5360\u4F4D\uFF0C\u4E0D\u80FD\u5305\u542B\u771F\u5B9E\u5BC6\u94A5\u3002`));for(let p of ne){let w=f.get(p);w!==void 0&&w!==""&&w!=="*"&&e.push(a("error","env.credential",`.env.example \u7684 ${p} \u53EA\u80FD\u4E3A\u7A7A\u6216\u4F7F\u7528 * \u5360\u4F4D\u3002`))}}await u(l.join(r,".env.server.example"))&&e.push(a("error","env.legacy-file","\u73AF\u5883\u914D\u7F6E\u5DF2\u5408\u5E76\u5230 .env.example\uFF0C\u4E0D\u5F97\u4FDD\u7559 .env.server.example\u3002"));let n=l.join(r,"src");if(await u(n))for(let f of await x(n)){let p=k(await E(f,"utf8")),w=ee(p);w&&e.push(a("error","env.client-server-only-reference",`${l.relative(r,f)} \u4E0D\u5F97\u8BFB\u53D6\u6216\u5F15\u7528 server-only \u914D\u7F6E\u6216\u51ED\u636E\uFF1A${w}\u3002`))}let i=l.join(r,"src","vr","work","WorkSource.ts");if(await u(i)){let f=k(await E(i,"utf8"));for(let p of["resourceCodeFromPathname","window.location.pathname","segments.length === 0","segments.length !== 1",": '/api/work'","`/api/work/${encodeURIComponent(resourceCode)}`","asExplicitPanoTagList","hasCanonicalPanoTagData(value.contentType, value.data)","hasCanonicalPanoTagAnchor(value)","asRecord(envelope.presetData)","presetData: { panoTagList }","displayName: string","displayName: displayNameFrom(work.name) ?? workCode","new RouteWorkSource()"])f.includes(p)||e.push(a("error","architecture.resource-route",`WorkSource \u7F3A\u5C11\u9ED8\u8BA4/\u517C\u5BB9\u8DEF\u7531\u3001\u540C\u6E90 API \u6216\u663E\u5F0F preset \u6807\u8BB0\uFF1A${p}`));for(let p of["VITE_WORK_URL","/work.json","envelope.tags","envelope.tagList","envelope.tag_list","payload.tags"])f.includes(p)&&e.push(a("error","architecture.resource-route",`WorkSource \u4E0D\u5F97\u4F9D\u8D56\u65E7 Work URL\u3001\u672C\u5730 Work \u6587\u4EF6\u6216\u628A generic tags \u6620\u5C04\u5230 PanoTag\uFF1A${p}`))}for(let f of["vite.config.ts","vite.config.js","vite.config.mjs"]){let p=l.join(r,f);if(!await u(p))continue;let w=k(await E(p,"utf8")),y=ee(w);(y||/envPrefix\s*:\s*[^\n]*REALSEE_/.test(w))&&e.push(a("error","env.vite-secret",`${f} \u4E0D\u5F97\u628A server-only \u914D\u7F6E\u3001AK/SK/token \u6216 REALSEE_* \u524D\u7F00\u6CE8\u5165\u6D4F\u89C8\u5668${y?`\uFF1A${y}`:"\u3002"}`));let b=w.match(/\bhost\s*:\s*['"]127\.0\.0\.1['"]/g)?.length??0,c=w.match(/\bport\s*:\s*8087\b/g)?.length??0,d=w.match(/\bstrictPort\s*:\s*true\b/g)?.length??0,g=/target\s*:\s*['"]http:\/\/127\.0\.0\.1:8088['"]/.test(w);(b<2||c<2||d<2||!g)&&e.push(a("error","architecture.dev-route",`${f} \u5FC5\u987B\u8BA9 Vite server/preview \u4F7F\u7528 strict IPv4 127.0.0.1:8087\uFF0C\u5E76\u628A /api \u4EE3\u7406\u5230 Fastify 127.0.0.1:8088\u3002`))}let o=[["server/types.ts",["ServerConfig","WorkEnvelope","WorkDetailClient","WorkService"]],["server/config.ts",["loadServerConfig","REALSEE_GATEWAY_APP_KEY","REALSEE_GATEWAY_APP_SECRET","REALSEE_DEFAULT_RESOURCE_CODE","gatewayEndpoint.protocol !== 'https:'","): ServerConfig {"]],["server/check-config.ts",["loadServerConfig()","BFF server configuration is valid."]],["server/realsee-open-api.ts",["RealseeOpenApiClient","implements RealseeSpatialClient","ACCESS_TOKEN_PATH = '/auth/access_token'","'application/x-www-form-urlencoded'","WORK_INFO_PATH = '/open/v3/vr/info'","Authorization: accessToken","resource_code: resourceCode","UNAUTHORIZED_UPSTREAM_CODES","TOKEN_REFRESH_SKEW_MS"]],["server/work-service.ts",["createWorkService","): WorkService {","getWorkByResourceCode","openApiClient.getWorkDetail(resourceCode, { signal })"]],["server/work-route.ts",["'/api/work'","'/api/work/:resourceCode'","getWorkByResourceCode(resourceCode, {","signal: requestAbort.signal","WORK_UPSTREAM_FAILED"]],["server/app.ts",["Fastify({ logger })","'/api/health'","fastifyStatic","serveFrontend","reply.sendFile('index.html')","Promise<FastifyInstance>"]],["server/index.ts",["loadServerConfig","RealseeOpenApiClient","createWorkService","createApp","app.listen({ host: config.host, port: config.port })"]]];for(let[f,p]of o){let w=l.join(r,f);if(!await u(w)){e.push(a("error","architecture.bff-missing",`\u7F3A\u5C11 Fastify BFF \u6587\u4EF6\uFF1A${f}`));continue}let y=k(await E(w,"utf8"));for(let b of p)y.includes(b)||e.push(a("error","architecture.bff-contract",`${f} \u7F3A\u5C11 BFF \u89E3\u8026\u6216 OpenAPI \u5951\u7EA6\uFF1A${b}`))}let m=l.join(r,t);if(await u(m)){let f=new Set((await E(m,"utf8")).split(/\r?\n/).map(p=>p.trim()).filter(Boolean));for(let p of[".env*","!.env.example"])f.has(p)||e.push(a("error","env.gitignore",`${t} \u5FC5\u987B\u5305\u542B ${p}\u3002`))}return e}async function jr(r,t){return t.name!==Y?!1:Object.hasOwn(t.bin??{},"vr-cli")||await u(l.join(r,"bin","vr-cli.mjs"))||await u(l.join(r,"templates","base"))}async function Pr(r,t){let e=[];t.private!==!1&&e.push(a("error","tool.private","\u53EF\u53D1\u5E03\u5DE5\u5177\u5FC5\u987B\u663E\u5F0F\u58F0\u660E private: false\u3002"));let s=Object.entries(t.bin??{});(s.length!==1||s[0]?.[0]!=="vr-cli"||s[0]?.[1]!=="bin/vr-cli.mjs")&&e.push(a("error","tool.bin","\u5F00\u53D1\u5DE5\u5177\u5FC5\u987B\u53EA\u66B4\u9732 vr-cli -> bin/vr-cli.mjs\u3002")),((typeof t.repository=="string"?t.repository:t.repository?.url)??null)!==null&&e.push(a("error","tool.repository",null===null?"\u5F53\u524D\u53D1\u5E03 profile \u8981\u6C42 package.json \u7701\u7565 repository\u3002":`package.json repository \u5FC5\u987B\u4E3A ${null}\u3002`));let i=t.publishConfig?.registry??null,o=t.publishConfig?.access??null;(i!==F||o!==N)&&e.push(a("error","tool.publish-profile",`package.json publishConfig \u5FC5\u987B\u5339\u914D\u5F53\u524D\u53D1\u5E03 profile\uFF1Aregistry=${F}\uFF0Caccess=${N??"\u672A\u8BBE\u7F6E"}\u3002`)),e.push(...await $e(r,t));let m=["README.md",...await u(l.join(r,".git"))?[".nvmrc",".node-version",".npmrc"]:[],"agent-assets/AGENTS.md","bin","bin/vr-cli.mjs",...N==="public"?["dist/cli.mjs"]:["src","src/core/spatial-policy.mjs"],"templates/base","templates/base/README.md","templates/base/.editorconfig","templates/base/.env.example","templates/base/.gitignore.template","templates/base/.nvmrc","templates/base/.node-version","templates/base/.npmrc.template","templates/base/.prettierignore","templates/base/eslint.config.mjs","templates/base/package-lock.json","templates/base/prettier.config.mjs","templates/base/tsconfig.server.json","templates/base/server/types.ts","templates/base/server/config.ts","templates/base/server/check-config.ts","templates/base/server/realsee-open-api.ts","templates/base/server/work-service.ts","templates/base/server/work-route.ts","templates/base/server/app.ts","templates/base/server/index.ts","templates/base/shared/model-floorplan.ts","templates/base/src/vr/five/api.ts","templates/base/src/vr/five/fiveConfig.ts","templates/base/src/vr/five/SpatialRuntimeProvider.tsx","templates/base/src/vr/five/initialState.ts","templates/base/src/vr/plugins/dnalogel.ts","templates/base/src/vr/plugins/SpatialPluginRegistry.ts","templates/base/src/vr/plugins/examples/FeatureOwnedMeshPlugin.ts","templates/base/src/vr/work/WorkSource.ts","templates/viewer/src/app/App.tsx","templates/viewer/src/features/floorplan/ModelFloorplanFeature.ts","templates/viewer/src/features/navigation/ModeSwitcher.tsx","templates/viewer/src/features/tags/PanoTagFeature.ts","templates/viewer/src/ui/ViewerOverlay.tsx","skills/realsee-vr-app","skills/realsee-vr-app/references/five-api-capabilities.md","skills/realsee-vr-app/references/dnalogel-api-capabilities.md","skills/realsee-vr-app/references/five-runtime.md","skills/realsee-vr-app/references/official-docs.md","skills/realsee-vr-app/references/plugin-lifecycle.md","skills/realsee-vr-app/references/ui-and-validation.md","skills/realsee-vr-app/references/vapor-dom-layer.md","skills/realsee-image-resources/SKILL.md","skills/realsee-image-resources/references/cors-and-three-textures.md","skills/realsee-image-resources/references/data-wanxiang.md","skills/realsee-image-resources/references/five-image-url-transform.md","skills/realsee-image-resources/references/source-and-domain-policy.md","skills/threejs-r117-five/SKILL.md","skills/threejs-r117-five/references/r117-capabilities.md","skills/threejs-r117-five/references/r117-version-guards.md","skills/threejs-r117-five/references/five-ownership.md"];for(let v of m)await u(l.join(r,v))||e.push(a("error","tool.missing",`\u7F3A\u5C11\u5DE5\u5177\u8DEF\u5F84\uFF1A${v}`));for(let v of["templates/base/README.md","agent-assets/AGENTS.md"]){let h=l.join(r,v);if(!await u(h))continue;let S=await E(h,"utf8");S.includes("{{TOOL_VERSION}}")||e.push(a("error","tool.token",`${v} \u5FC5\u987B\u4F7F\u7528 {{TOOL_VERSION}} \u7248\u672C token\u3002`)),S.includes("__TOOL_VERSION__")&&e.push(a("error","tool.token",`${v} \u4E0D\u5F97\u4F7F\u7528\u4E0E Markdown \u7C97\u4F53\u51B2\u7A81\u7684 __TOOL_VERSION__ token\u3002`))}let f=l.join(r,"templates","base");if(await u(f)){let v=new Set(["package-lock.json","package.json","tsconfig.json","tsconfig.server.json"]);for(let h of await K(f))h.endsWith(".json")&&!v.has(h)&&e.push(a("error","tool.data-asset",`\u6A21\u677F\u4E0D\u5F97\u5206\u53D1\u672A\u7ECF\u660E\u786E\u5BA1\u6838\u7684 JSON/Work \u6570\u636E\uFF1Atemplates/base/${h}`))}e.push(...await Ge(r,["skills/realsee-vr-app/SKILL.md","agent-assets/AGENTS.md"],"skills/realsee-vr-app/references/dnalogel-api-capabilities.md")),e.push(...await Ve(r,["skills/realsee-vr-app/SKILL.md","agent-assets/AGENTS.md"],"skills/realsee-vr-app")),e.push(...await Ke(r,["agent-assets/AGENTS.md","skills/realsee-vr-app/SKILL.md"],"skills/realsee-vr-app")),e.push(...await We(r,{validator:"templates/base/shared/model-floorplan.ts",types:"templates/base/server/types.ts",client:"templates/base/server/realsee-open-api.ts",service:"templates/base/server/work-service.ts",workSource:"templates/base/src/vr/work/WorkSource.ts",feature:"templates/viewer/src/features/floorplan/ModelFloorplanFeature.ts",app:"templates/viewer/src/app/App.tsx",modeSwitcher:"templates/viewer/src/features/navigation/ModeSwitcher.tsx",browserTsconfig:"templates/base/tsconfig.json",serverTsconfig:"templates/base/tsconfig.server.json"})),e.push(...await Ie(r,["agent-assets/AGENTS.md","skills/realsee-vr-app/SKILL.md","skills/realsee-image-resources/SKILL.md"])),e.push(...await Ce(r,["agent-assets/AGENTS.md","skills/realsee-vr-app/SKILL.md","skills/realsee-vr-app/references/vapor-dom-layer.md"])),e.push(...await De(r,"skills/realsee-vr-app/references/vapor-dom-layer.md","skills/realsee-vr-app/references/ui-and-validation.md")),e.push(...await Fe(r,"templates/base/src/vr/plugins/examples/FeatureOwnedMeshPlugin.ts"));let p=l.join(r,"templates","base","src","main.tsx");if(await u(p)){let v=k(await E(p,"utf8"));(Ae(v)!==1||v.includes("@realsee/vapor/vapor.scss"))&&e.push(a("error","tool.vapor-css","\u9ED8\u8BA4\u6A21\u677F\u5FC5\u987B\u6070\u597D\u4E00\u6B21\u4EE5\u53EF\u6267\u884C side-effect import \u52A0\u8F7D @realsee/vapor/vapor.css\uFF0C\u4E14\u4E0D\u5F97\u4F7F\u7528 vapor.scss\u3002"))}let w=l.join(r,"templates","base","src","vr","work","WorkSource.ts");if(await u(w)){let v=k(await E(w,"utf8"));for(let h of["displayName: string","displayNameFrom(work.name) ?? workCode"])v.includes(h)||e.push(a("error","tool.work-name",`\u9ED8\u8BA4 WorkSource \u5FC5\u987B\u4ECE work.name \u5B89\u5168\u6D3E\u751F VR \u5C55\u793A\u540D\u79F0\uFF1A${h}`))}let y=l.join(r,"templates","viewer","src","ui","ViewerOverlay.tsx");if(await u(y)){let v=k(await E(y,"utf8")),h=Te.filter(S=>!z(v,S)||!U(v,S));h.length>0&&e.push(a("error","tool.vapor-provider",`\u9ED8\u8BA4 ViewerOverlay \u5FC5\u987B\u4ECE @realsee/vapor \u5BFC\u5165\u5E76\u5B9E\u9645\u6E32\u67D3\u5B98\u65B9 Demo \u7EC4\u4EF6\uFF1A${h.join(", ")}`));for(let S of je)v.includes(S)||e.push(a("error","tool.viewer-topbar",`\u9ED8\u8BA4 ViewerOverlay \u5FC5\u987B\u4F7F\u7528 Vapor TitleBar\u3001Layer \u4E0E\u54CD\u5E94\u5F0F Stack\uFF0C\u5E76\u6E32\u67D3\u5DE6\u4E0A\u6807\u9898\u3001\u5C45\u4E2D controls \u4E0E\u72B6\u6001\u8BED\u4E49\uFF1A${S}`));for(let S of Ne)v.includes(S)&&e.push(a("error","tool.viewer-native-vapor",`\u9ED8\u8BA4 ViewerOverlay \u4E0D\u5F97\u7528\u539F\u751F\u6807\u9898\u6216\u81EA\u5B9A\u4E49 glass \u6837\u5F0F\u66FF\u4EE3 Vapor \u7EC4\u4EF6\u89C6\u89C9\uFF1A${S}`))}let b=l.join(r,"templates","base","src","styles.css");if(await u(b)){let v=await E(b,"utf8");for(let h of Le)v.includes(h)||e.push(a("error","tool.viewer-layout",`\u9ED8\u8BA4 Viewer \u5FC5\u987B\u4FDD\u6301 safe-area \u53CA\u684C\u9762/\u5E73\u677F/\u624B\u673A/\u77EE\u5C4F\u5916\u5C42\u54CD\u5E94\u5F0F\u5E03\u5C40\uFF1A${h}`));for(let h of Me)v.includes(h)&&e.push(a("error","tool.viewer-native-vapor",`\u9ED8\u8BA4 Viewer \u6837\u5F0F\u4E0D\u5F97\u8986\u76D6 Vapor \u5185\u90E8\u7C7B\u6216\u91CD\u7ED8\u7EC4\u4EF6\u89C6\u89C9\uFF1A${h}`))}let c=l.join(r,"templates","viewer","src","features","navigation","ModeSwitcher.tsx");if(await u(c)){let v=k(await E(c,"utf8")),h=Re.filter(S=>!z(v,S)||!U(v,S));h.length>0&&e.push(a("error","tool.vapor-component",`\u9ED8\u8BA4 ModeSwitcher \u5FC5\u987B\u4ECE @realsee/vapor \u5BFC\u5165\u5E76\u5B9E\u9645\u6E32\u67D3\u5B98\u65B9 Demo \u7684\u6A21\u5F0F\u5207\u6362\u7EC4\u4EF6\uFF1A${h.join(", ")}`));for(let S of Pe)v.includes(S)||e.push(a("error","tool.viewer-control-size",`\u9ED8\u8BA4 ModeSwitcher \u5FC5\u987B\u4F7F\u7528\u5B98\u65B9 Demo \u7684 Font/HStack/VStack \u4E0E shadow Button \u7EC4\u5408\uFF1A${S}`));for(let S of xe)v.includes(S)&&e.push(a("error","tool.viewer-native-vapor",`\u9ED8\u8BA4 ModeSwitcher \u4E0D\u5F97\u9000\u56DE Segmented \u6216\u6DFB\u52A0\u65E7\u7684\u62C9\u4F38/glass \u6837\u5F0F\uFF1A${S}`))}let d=l.join(r,"templates","viewer","src");if(await u(d))for(let v of await x(d)){let h=k(await E(v,"utf8")),S=ee(h);S&&e.push(a("error","env.client-server-only-reference",`${l.relative(r,v)} \u4E0D\u5F97\u8BFB\u53D6\u6216\u5F15\u7528 server-only \u914D\u7F6E\u6216\u51ED\u636E\uFF1A${S}\u3002`))}let g=l.join(r,"templates","viewer","src","app","App.tsx");if(await u(g)){let v=k(await E(g,"utf8"));for(let h of["setWorkName(payload.displayName)","title={workName}"])v.includes(h)||e.push(a("error","tool.work-name",`\u9ED8\u8BA4 App \u5FC5\u987B\u628A WorkSource \u5C55\u793A\u540D\u79F0\u4F20\u7ED9 ViewerOverlay\uFF1A${h}`));for(let h of["FeatureOwnedMeshPlugin","registerFeatureOwnedMeshPlugin","vr/plugins/examples"])v.includes(h)&&e.push(a("error","tool.custom-plugin-enabled",`\u9ED8\u8BA4 Viewer \u4E0D\u5F97\u542F\u7528\u81EA\u5B9A\u4E49\u63D2\u4EF6\u793A\u4F8B\uFF1A${h}`))}return e.push(...await ze(l.join(r,"templates","base"),".gitignore.template")),e}async function Lr(r,t,e){let s=new Set;for(let n of e)await u(l.join(r,n))&&s.add(n);for(let n of t){let i=l.join(r,n);if(!await u(i))continue;if(!(await cr(i)).isDirectory()){s.add(n);continue}for(let m of await K(i))s.add(l.posix.join(n,m))}return s}async function Nr(r){let t=[],e=l.join(r,".vr-cli.json");if(!await u(e)){let o=l.join(r,".agents"),m=await u(l.join(r,"AGENTS.md")),f=await u(o);if(!m&&!f)t.push(a("warning","agents.missing","\u672A\u5B89\u88C5\u8DE8 Agent \u6307\u4EE4\u8D44\u4EA7\u3002"));else for(let p of["skills/realsee-vr-app/SKILL.md","skills/realsee-image-resources/SKILL.md","skills/realsee-image-resources/references/cors-and-three-textures.md","skills/realsee-image-resources/references/data-wanxiang.md","skills/realsee-image-resources/references/five-image-url-transform.md","skills/realsee-image-resources/references/source-and-domain-policy.md","skills/threejs-r117-five/SKILL.md","skills/threejs-r117-five/references/r117-capabilities.md","skills/threejs-r117-five/references/r117-version-guards.md","skills/threejs-r117-five/references/five-ownership.md"])await u(l.join(o,p))||t.push(a("error","agents.skill-missing",`.agents/${p} \u7F3A\u5931\u3002`));return t}let s;try{s=await re(e)}catch(o){return[a("error","manifest.invalid",".vr-cli.json \u65E0\u6CD5\u89E3\u6790\u3002",String(o))]}if(!(s&&typeof s=="object"&&!Array.isArray(s)&&s.schemaVersion===1&&O.includes(s.agentAssets)))return[a("error","manifest.invalid",".vr-cli.json \u7F3A\u5C11\u6709\u6548\u7684 schemaVersion \u6216 agentAssets\u3002")];for(let o of W(s.agentAssets))await u(l.join(r,o))||t.push(a("error","agents.asset-missing",`agentAssets=${s.agentAssets} \u7F3A\u5C11\uFF1A${o}`));let i=s.agentAssets==="portable"?{directoryRoots:Ee,singleFiles:Se}:s.agentAssets==="none"?{directoryRoots:he,singleFiles:we}:null;if(i)for(let o of await Lr(r,i.directoryRoots,i.singleFiles))t.push(a("error","agents.unexpected",`agentAssets=${s.agentAssets} \u4E0D\u5E94\u5305\u542B\uFF1A${o}`));return t}async function xr(r,t){let e=[];t.private!==!0&&e.push(a("error","project.public","\u751F\u6210\u9879\u76EE\u5FC5\u987B\u4FDD\u6301 private\u3002")),t.type!=="module"&&e.push(a("error","project.module","\u751F\u6210\u9879\u76EE\u5FC5\u987B\u4F7F\u7528 ESM\uFF1Atype=module\u3002")),e.push(...await $e(r,t));for(let c of ce)await u(l.join(r,c))||e.push(a("error","project.missing",`\u7F3A\u5C11\u751F\u6210\u6587\u4EF6\uFF1A${c}`));for(let[c,d,g]of[["dependency",t.dependencies,G],["dev-dependency",t.devDependencies,q]])for(let[v,h]of Object.entries(g)){let S=d?.[v];if(S===void 0){e.push(a("error",`${c}.missing`,`${v} \u5FC5\u987B\u58F0\u660E\uFF0C\u751F\u6210\u5668\u57FA\u7EBF\u4E3A ${h}`));continue}if(v in V){S!==h&&e.push(a("error",`${c}.locked`,`${v} \u5FC5\u987B\u9501\u5B9A\u4E3A ${h}\uFF0C\u5F53\u524D\u4E3A ${S}\uFF1BFive 6.8.9 \u4E0E Three \u80FD\u529B\u53C2\u8003\u5747\u9488\u5BF9\u8BE5\u7248\u672C\uFF0C\u5347\u7EA7\u9700\u6574\u4F53\u8BC4\u4F30\u5E76\u540C\u6B65\u751F\u6210\u5668\u3002`));continue}let $=H(S);if(!$){e.push(a("error",`${c}.range`,`${v} \u5FC5\u987B\u4F7F\u7528\u5F00\u653E\u6700\u4F4E\u7248\u672C\u8303\u56F4 >=x.y.z\uFF0C\u751F\u6210\u5668\u57FA\u7EBF\u4E3A >=${h}\uFF0C\u5F53\u524D\u4E3A ${S}\u3002`));continue}if(!P($,h)){e.push(a("error",`${c}.minimum`,`${v} \u7684\u58F0\u660E\u4E0B\u9650 ${$} \u4F4E\u4E8E\u751F\u6210\u5668\u57FA\u7EBF ${h}\u3002`));continue}S!==oe(v,h)&&e.push(a("warning",`${c}.updated`,`${v} \u7684\u6700\u4F4E\u7248\u672C\u5DF2\u4ECE\u751F\u6210\u5668\u57FA\u7EBF ${h} \u63D0\u9AD8\u5230 ${$}\uFF1B\u8BF7\u786E\u8BA4 npm run check \u4E0E npm run build \u901A\u8FC7\uFF0C\u5E76\u5728\u5347\u7EA7 SDK \u65F6\u590D\u6838\u7248\u672C\u5316 fixture\u3002`))}for(let[c,d]of[["dev",["check:env","concurrently"]],["setup",["scripts/setup.mjs"]],["test:setup",["--test","scripts/setup-config.test.mjs"]],["dev:bff",["--env-file-if-exists=.env.development","server/index.ts"]],["dev:web",["vite"]],["check:env",["--env-file-if-exists=.env.development","server/check-config.ts"]],["typecheck",["npm run typecheck:web","npm run typecheck:bff"]],["typecheck:web",["tsc -p tsconfig.json"]],["typecheck:bff",["tsc -p tsconfig.server.json"]],["lint",["eslint . --max-warnings=0"]],["format",["prettier --write ."]],["format:check",["prettier --check ."]],["check",["npm run typecheck","npm run lint","npm run format:check"]],["check:bff",["typecheck:bff"]],["deps:relock",["npm install --package-lock-only"]],["deps:install",["npm ci"]],["start",["--env-file-if-exists=.env.development","server/index.ts","--serve-frontend"]]])for(let g of d)t.scripts?.[c]?.includes(g)||e.push(a("error","script.contract",`package.json scripts.${c} \u5FC5\u987B\u5305\u542B\uFF1A${g}`));e.push(...await Tr(r,t));for(let[c,d]of[["eslint.config.mjs",["from '@eslint/js'","from 'typescript-eslint'","from 'eslint-plugin-react-hooks'","recommendedTypeChecked","tsconfig.server.json","globals.browser","globals.node","'react-hooks/rules-of-hooks'","'react-hooks/exhaustive-deps'","no-floating-promises","disableTypeChecked"]],["prettier.config.mjs",["semi: false","singleQuote: true","trailingComma: 'all'"]],["tsconfig.json",['"strict": true','"noUncheckedIndexedAccess": true','"noEmit": true','"include": ["src", "shared", "vite.config.ts"]']],["tsconfig.server.json",['"strict": true','"erasableSyntaxOnly": true','"verbatimModuleSyntax": true','"exactOptionalPropertyTypes": true','"noUncheckedIndexedAccess": true','"types": ["node"]','"include": ["server", "shared"]']]]){let g=l.join(r,c);if(!await u(g))continue;let v=await E(g,"utf8");for(let h of d)v.includes(h)||e.push(a("error","tooling.config",`${c} \u7F3A\u5C11\u914D\u7F6E\u6807\u8BB0\uFF1A${h}`))}let s=l.join(r,"src","main.tsx");if(await u(s)){let c=await E(s,"utf8"),d=k(c);(Ae(d)!==1||d.includes("@realsee/vapor/vapor.scss"))&&e.push(a("error","vapor.css","src/main.tsx \u5FC5\u987B\u6070\u597D\u4E00\u6B21\u4EE5\u53EF\u6267\u884C side-effect import \u5BFC\u5165 @realsee/vapor/vapor.css\uFF0C\u4E14\u4E0D\u5F97\u4F7F\u7528 vapor.scss\u3002")),d.includes("./app/App")||e.push(a("error","architecture.app","src/main.tsx \u5FC5\u987B\u4ECE app \u5C42\u7EC4\u5408\u7A7A\u95F4\u8FD0\u884C\u65F6\u4E0E DOM UI\u3002"))}let n=l.join(r,"src","vr","five","api.ts");if(await u(n)){let c=await E(n,"utf8"),d=k(c);for(let g of["@realsee/five","@realsee/five/plugins","@realsee/five/line","@realsee/five/sticker","@realsee/five/vfx","@realsee/five/gltf-loader","@realsee/five/shader-lib","@realsee/five/work-downloader"])d.includes(`'${g}'`)||e.push(a("error","architecture.five-api",`Five \u80FD\u529B\u5165\u53E3\u7F3A\u5931\uFF1A${g}`))}let i=l.join(r,"src","vr","plugins","dnalogel.ts");await u(i)&&(k(await E(i,"utf8")).includes("from '@realsee/dnalogel'")||e.push(a("error","architecture.dnalogel-api","dnalogel \u9884\u8BBE\u63D2\u4EF6\u6839\u80FD\u529B\u5165\u53E3\u7F3A\u5931\u3002"))),e.push(...await Fe(r,"src/vr/plugins/examples/FeatureOwnedMeshPlugin.ts"));let o=l.join(r,"src");if(await u(o))for(let c of await x(o)){let d=k(await E(c,"utf8"));d.includes("@realsee/five/react")&&e.push(a("error","architecture.five-react",`${l.relative(r,c)} \u7981\u6B62\u4F7F\u7528\u66F4\u65B0\u6EDE\u540E\u7684 @realsee/five/react\uFF1B\u8BF7\u76F4\u63A5\u4F7F\u7528\u539F\u751F Five API\u3002`));for(let g of dr)g.pattern.test(d)&&e.push(a("error","compatibility.three-r117",`${l.relative(r,c)}\uFF1A${g.message}`))}let m=l.join(r,"src","vr","five","SpatialRuntimeProvider.tsx");if(await u(m)){let c=await E(m,"utf8"),d=k(c);(d.includes("unsafe__useFiveInstance")||d.includes("provider?.five"))&&e.push(a("error","architecture.five-instance","\u7A7A\u95F4\u8FD0\u884C\u65F6\u7981\u6B62\u901A\u8FC7 Five React wrapper \u83B7\u53D6\u5B9E\u4F8B\u3002")),/import\s*\{[^}]*\bFive\b[^}]*\}\s*from\s*['"]@realsee\/five['"]/.test(d)||e.push(a("error","architecture.five-native","SpatialRuntimeProvider \u5FC5\u987B\u4ECE @realsee/five \u5BFC\u5165\u539F\u751F Five\u3002"));for(let h of["new Five(",".appendTo(",".refresh(",".getElement()",".dispose()","new ResizeObserver","new SpatialRuntimeLifetime","runExclusive<T>(","ownedLifetime?.disposeFeatures()","ownedLifetime.afterPending(finalize)","width <= 0 || height <= 0",'className="five-surface"'])d.includes(h)||e.push(a("error","architecture.five-native",`\u539F\u751F Five \u751F\u547D\u5468\u671F\u7F3A\u5C11\uFF1A${h}`));let g=d.indexOf("ownedLifetime?.disposeFeatures()"),v=d.indexOf("ownedLifetime.afterPending(finalize)");(g<0||v<g)&&e.push(a("error","architecture.five-cleanup","\u5FC5\u987B\u5148\u540C\u6B65\u505C\u6B62 feature\uFF0C\u518D\u7B49\u5F85\u539F\u751F\u5F02\u6B65\u4EFB\u52A1\u5B8C\u6210\u540E\u6E05\u7406\u63D2\u4EF6\u548C Five\u3002"))}let f=l.join(r,"src","app","App.tsx");if(await u(f)){let c=await E(f,"utf8"),d=k(c);for(let g of["lifetime.own(","lifetime.runExclusive(","await five.load(","await waitForActiveModel(","const panoTagList = payload.presetData?.panoTagList","if (panoTagList?.length)","await currentTags.load(panoTagList)","setWorkName(payload.displayName)","title={workName}"])d.includes(g)||e.push(a("error","architecture.feature-lifetime",`App \u7684\u539F\u751F\u5F02\u6B65\u751F\u547D\u5468\u671F\u6216\u663E\u5F0F PanoTag preset \u6761\u4EF6\u7F3A\u5C11\uFF1A${g}`));d.includes("payload.tags")&&e.push(a("error","architecture.pano-tag-contract","App \u4E0D\u5F97\u628A generic payload.tags \u76F4\u63A5\u52A0\u8F7D\u4E3A PanoTag\u3002"))}let p=l.join(r,"src","ui"),w=l.join(p,"ViewerOverlay.tsx");if(await u(w)){let c=k(await E(w,"utf8")),d=Te.filter(g=>!z(c,g)||!U(c,g));d.length>0&&e.push(a("error","architecture.vapor-provider",`ViewerOverlay \u5FC5\u987B\u4ECE @realsee/vapor \u5BFC\u5165\u5E76\u5B9E\u9645\u6E32\u67D3\u5B98\u65B9 Demo \u7EC4\u4EF6\uFF1A${d.join(", ")}`));for(let g of je)c.includes(g)||e.push(a("error","architecture.viewer-topbar",`ViewerOverlay \u5FC5\u987B\u4F7F\u7528 Vapor TitleBar\u3001Layer \u4E0E\u54CD\u5E94\u5F0F Stack\uFF0C\u5E76\u6E32\u67D3\u5DE6\u4E0A\u6807\u9898\u3001\u5C45\u4E2D controls \u4E0E\u72B6\u6001\u8BED\u4E49\uFF1A${g}`));for(let g of Ne)c.includes(g)&&e.push(a("error","architecture.viewer-native-vapor",`ViewerOverlay \u4E0D\u5F97\u7528\u539F\u751F\u6807\u9898\u6216\u81EA\u5B9A\u4E49 glass \u6837\u5F0F\u66FF\u4EE3 Vapor \u7EC4\u4EF6\u89C6\u89C9\uFF1A${g}`))}let y=l.join(r,"src","features","navigation","ModeSwitcher.tsx");if(await u(y)){let c=k(await E(y,"utf8")),d=Re.filter(g=>!z(c,g)||!U(c,g));d.length>0&&e.push(a("error","architecture.vapor-component",`ModeSwitcher \u5FC5\u987B\u4ECE @realsee/vapor \u5BFC\u5165\u5E76\u5B9E\u9645\u6E32\u67D3\u5B98\u65B9 Demo \u7684\u6A21\u5F0F\u5207\u6362\u7EC4\u4EF6\uFF1A${d.join(", ")}`));for(let g of Pe)c.includes(g)||e.push(a("error","architecture.viewer-control-size",`ModeSwitcher \u5FC5\u987B\u4F7F\u7528\u5B98\u65B9 Demo \u7684 Font/HStack/VStack \u4E0E shadow Button \u7EC4\u5408\uFF1A${g}`));for(let g of xe)c.includes(g)&&e.push(a("error","architecture.viewer-native-vapor",`ModeSwitcher \u4E0D\u5F97\u9000\u56DE Segmented \u6216\u6DFB\u52A0\u65E7\u7684\u62C9\u4F38/glass \u6837\u5F0F\uFF1A${g}`))}if(await u(p))for(let c of await x(p)){let d=k(await E(c,"utf8")),g=/(?:from|import)\s*(?:\(\s*)?['"][^'"]*\/vr\//.test(d);(d.includes("@realsee/five")||d.includes("@realsee/dnalogel")||g)&&e.push(a("error","architecture.ui-boundary",`${l.relative(r,c)} \u4E0D\u5F97\u5BFC\u5165 src/vr\u3001Five \u6216 dnalogel \u7A7A\u95F4\u8FD0\u884C\u65F6\u3002`))}let b=l.join(r,"src","styles.css");if(await u(b)){let c=await E(b,"utf8");(!c.includes(".five-surface")||!c.includes(".dom-overlay")||!c.includes("pointer-events: none"))&&e.push(a("error","architecture.dom-overlay","\u539F\u751F Five surface \u4E0E DOM overlay \u5FC5\u987B\u4FDD\u6301\u72EC\u7ACB\u5206\u5C42\u53CA pointer-events \u9694\u79BB\u3002"));for(let d of Le)c.includes(d)||e.push(a("error","architecture.viewer-layout",`Viewer \u5FC5\u987B\u4FDD\u6301 safe-area \u53CA\u684C\u9762/\u5E73\u677F/\u624B\u673A/\u77EE\u5C4F\u5916\u5C42\u54CD\u5E94\u5F0F\u5E03\u5C40\uFF1A${d}`));for(let d of Me)c.includes(d)&&e.push(a("error","architecture.viewer-native-vapor",`Viewer \u6837\u5F0F\u4E0D\u5F97\u8986\u76D6 Vapor \u5185\u90E8\u7C7B\u6216\u91CD\u7ED8\u7EC4\u4EF6\u89C6\u89C9\uFF1A${d}`))}return e.push(...await ze(r,".gitignore")),e.push(...await Ge(r,[".agents/skills/realsee-vr-app/SKILL.md","AGENTS.md"],".agents/skills/realsee-vr-app/references/dnalogel-api-capabilities.md")),e.push(...await Ve(r,[".agents/skills/realsee-vr-app/SKILL.md","AGENTS.md"],".agents/skills/realsee-vr-app")),e.push(...await Ke(r,["AGENTS.md",".agents/skills/realsee-vr-app/SKILL.md"],".agents/skills/realsee-vr-app")),e.push(...await We(r,{validator:"shared/model-floorplan.ts",types:"server/types.ts",client:"server/realsee-open-api.ts",service:"server/work-service.ts",workSource:"src/vr/work/WorkSource.ts",feature:"src/features/floorplan/ModelFloorplanFeature.ts",app:"src/app/App.tsx",modeSwitcher:"src/features/navigation/ModeSwitcher.tsx",browserTsconfig:"tsconfig.json",serverTsconfig:"tsconfig.server.json"})),e.push(...await Ie(r,["AGENTS.md",".agents/skills/realsee-vr-app/SKILL.md",".agents/skills/realsee-image-resources/SKILL.md"])),e.push(...await Ce(r,["AGENTS.md",".agents/skills/realsee-vr-app/SKILL.md",".agents/skills/realsee-vr-app/references/vapor-dom-layer.md"])),e.push(...await De(r,".agents/skills/realsee-vr-app/references/vapor-dom-layer.md",".agents/skills/realsee-vr-app/references/ui-and-validation.md")),e.push(...await Nr(r)),e}async function Ue({directory:r=".",json:t=!1}){let e=l.resolve(r),s=l.join(e,"package.json"),n=[];P(process.versions.node,C)||n.push(a("error","node.version",`\u9700\u8981 Node >=${C}\uFF0C\u5F53\u524D ${process.versions.node}`));try{let o=await Or();P(o,D)||n.push(a("error","npm.version",`\u9700\u8981 npm >=${D}\uFF0C\u5F53\u524D ${o}`))}catch(o){n.push(a("error","npm.unavailable","\u65E0\u6CD5\u6267\u884C npm --version\u3002",String(o)))}if(!await u(s))n.push(a("error","package.missing",`\u627E\u4E0D\u5230 package.json\uFF1A${s}`));else try{let o=await re(s);n.push(...await jr(e,o)?await Pr(e,o):await xr(e,o))}catch(o){n.push(a("error","package.invalid","package.json \u65E0\u6CD5\u89E3\u6790\u3002",String(o)))}let i={ok:!n.some(o=>o.level==="error"),root:e,errors:n.filter(o=>o.level==="error").length,warnings:n.filter(o=>o.level==="warning").length,diagnostics:n};if(t)console.log(JSON.stringify(i,null,2));else{for(let o of n)console.log(`${o.level==="error"?"ERROR":"WARN "} [${o.code}] ${o.message}`);console.log(i.ok?`OK ${e}`:`FAILED ${e}`)}return i}var M=Object.freeze({version:1,capabilityOrder:["@realsee/dnalogel","@realsee/five","three"],intentRoutes:Object.freeze([Object.freeze({intent:"tag|hotspot|anchor",defaultCapability:"PanoTagPlugin",source:"GET /open/v3/hot/list"}),Object.freeze({intent:"tour",defaultCapability:"Five State snapshot",source:"GET /open/v3/observerpicture/search"}),Object.freeze({intent:"model-annotation",defaultCapability:"ModelMakerPlugin",source:"GET /open/v3/annotation/search"}),Object.freeze({intent:"floorplan|house-layout",defaultCapability:"ModelFloorplanPlugin",source:"GET /open/v3/floorplan/plugins -> data.format_floorplan_url"}),Object.freeze({intent:"route",defaultCapability:"GuideLinePlugin + CruisePlugin|MovePlugin",source:"GET /open/v3/route/search"})]),invariants:Object.freeze(["OpenAPI data is fetched by the BFF, validated, then adapted to installed SDK types.","SDK-shaped local data requires an explicit Open Platform opt-out.","A fallback records the missing preset capability before using Five or Three.","Only a completely validated { version, data: FloorplanServerData } wrapper from data.format_floorplan_url may expose the modelFloorplan capability.","Missing, empty, malformed or failed floorplan data is feature-local and leaves the base Viewer usable.","Each feature owns cancellation, registrations, hooks, media and Three resources.","Persistent Work or customer-data mutations require an authoritative write contract and confirmation."])});var Ye=`vr-cli ${j}
|
|
3
3
|
|
|
4
4
|
Usage:
|
|
5
5
|
vr-cli create <directory> [options]
|
|
@@ -18,7 +18,7 @@ Examples:
|
|
|
18
18
|
vr-cli create ./my-vrapp --preset viewer --agent-assets all --no-install
|
|
19
19
|
vr-cli doctor ./my-vrapp --json
|
|
20
20
|
vr-cli policy --json
|
|
21
|
-
`;function $r(r){return Mr({args:r,allowPositionals:!0,strict:!0,options:{preset:{type:"string"},name:{type:"string"},"agent-assets":{type:"string",default:"portable"},"no-install":{type:"boolean",default:!1},json:{type:"boolean",default:!1},help:{type:"boolean",short:"h",default:!1},version:{type:"boolean",short:"v",default:!1}}})}async function mt(r){try{let{values:t,positionals:e}=$r(r);if(t.version){console.log(j);return}let
|
|
22
|
-
Created viewer project at ${n.targetRoot}`),n.installed?console.log("Next:
|
|
21
|
+
`;function $r(r){return Mr({args:r,allowPositionals:!0,strict:!0,options:{preset:{type:"string"},name:{type:"string"},"agent-assets":{type:"string",default:"portable"},"no-install":{type:"boolean",default:!1},json:{type:"boolean",default:!1},help:{type:"boolean",short:"h",default:!1},version:{type:"boolean",short:"v",default:!1}}})}async function mt(r){try{let{values:t,positionals:e}=$r(r);if(t.version){console.log(j);return}let s=e[0]||(t.help?"help":void 0);if(!s||s==="help"){console.log(Ye);return}if(s==="create"){if(t.preset&&t.preset!=="viewer")throw new Error("--preset \u6682\u65F6\u53EA\u652F\u6301 viewer\u3002");if(!O.includes(t["agent-assets"]))throw new Error(`--agent-assets \u5FC5\u987B\u662F\uFF1A${O.join(", ")}`);let n=await be({directory:e[1],preset:t.preset,name:t.name,agentAssets:t["agent-assets"],install:!t["no-install"]});console.log(`
|
|
22
|
+
Created viewer project at ${n.targetRoot}`),n.installed?console.log("Next: npm run setup"):console.log("Next: npm ci && npm run setup"),console.log("Configure: paste the single-line Portal setup payload into the hidden prompt, then run npm run dev"),console.log("Open: http://127.0.0.1:8087/");return}if(s==="doctor"){(await Ue({directory:e[1]||".",json:t.json})).ok||(process.exitCode=1);return}if(s==="policy"){if(t.json)console.log(JSON.stringify(M,null,2));else{console.log(`Spatial policy v${M.version}`),console.log(`Capability order: ${M.capabilityOrder.join(" -> ")}`);for(let n of M.intentRoutes)console.log(`${n.intent}: ${n.defaultCapability} (${n.source})`)}return}throw new Error(`\u672A\u77E5\u547D\u4EE4\uFF1A${s}
|
|
23
23
|
|
|
24
24
|
${Ye}`)}catch(t){console.error(t instanceof Error?t.message:String(t)),process.exitCode=1}}export{mt as main};
|
package/package.json
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
- Accept Work through a `WorkSource` boundary, not from a hardcoded business endpoint or local `work.json`.
|
|
6
6
|
- The browser route is `/{resourceCode}` and the Viewer calls only the same-origin `/api/work/{resourceCode}` contract. Do not add `VITE_WORK_URL` or embed a business endpoint.
|
|
7
7
|
- The generated Fastify BFF is a replaceable TypeScript example. Keep the shared contracts in `server/types.ts`, Realsee token exchange/cache and upstream calls in `server/realsee-open-api.ts`, the framework-neutral browser envelope in `server/work-service.ts`, and HTTP adaptation in `server/work-route.ts`. Node runs these files directly through type stripping, so `server/` must stay erasable-only and keep explicit `.ts` import extensions.
|
|
8
|
-
- A Go or other backend may replace `server/`, but must preserve `GET /api/work
|
|
9
|
-
- Use the single `.env.example` as the server/deployment schema.
|
|
8
|
+
- A Go or other backend may replace `server/`, but must preserve onboarding `GET /api/work` and return `{ work, workCode, baseURL?, capabilities?: { modelFloorplan } }` without exposing credentials or tokens. The explicit resource-code route is development-only compatibility.
|
|
9
|
+
- Use the single `.env.example` as the server/deployment schema. `npm run setup` writes AK, SK and the default `resourceCode` to `.env.development`; only the BFF reads `REALSEE_GATEWAY_ENDPOINT`, `REALSEE_GATEWAY_APP_KEY`, `REALSEE_GATEWAY_APP_SECRET` and `REALSEE_DEFAULT_RESOURCE_CODE`. Browser code reads none of them.
|
|
10
10
|
- Keep the real `REALSEE_GATEWAY_APP_SECRET`, access token and OpenAPI authorization logic in the BFF environment or secret manager. Never expose them through `VITE_*`, Vite `define/envPrefix`, client source, query strings, browser request headers, logs or returned Work envelopes.
|
|
11
11
|
- Carry `workCode`, optional `baseURL`, raw Work and optional initial State together. Preset feature input must be explicit; only `presetData.panoTagList` or a validated named feature-data contract may feed a plugin.
|
|
12
12
|
- Never infer PanoTag from generic `tags`, `tagList` or `tag_list` fields. `/open/v3/hot/list` is authoritative only after its envelope and typed tag records are validated by a named BFF adapter.
|
|
@@ -44,6 +44,7 @@ const requiredEnvKeys = [
|
|
|
44
44
|
'REALSEE_GATEWAY_ENDPOINT',
|
|
45
45
|
'REALSEE_GATEWAY_APP_KEY',
|
|
46
46
|
'REALSEE_GATEWAY_APP_SECRET',
|
|
47
|
+
'REALSEE_DEFAULT_RESOURCE_CODE',
|
|
47
48
|
'REALSEE_SOURCE',
|
|
48
49
|
]
|
|
49
50
|
const credentialEnvKeys = [
|
|
@@ -67,6 +68,9 @@ const requiredFiles = [
|
|
|
67
68
|
'.vr-cli.json',
|
|
68
69
|
'eslint.config.mjs',
|
|
69
70
|
'prettier.config.mjs',
|
|
71
|
+
'scripts/setup-config.mjs',
|
|
72
|
+
'scripts/setup-config.test.mjs',
|
|
73
|
+
'scripts/setup.mjs',
|
|
70
74
|
'server/types.ts',
|
|
71
75
|
'server/config.ts',
|
|
72
76
|
'server/check-config.ts',
|
|
@@ -477,6 +481,8 @@ try {
|
|
|
477
481
|
}
|
|
478
482
|
for (const [name, markers] of [
|
|
479
483
|
['dev', ['check:env', 'concurrently']],
|
|
484
|
+
['setup', ['scripts/setup.mjs']],
|
|
485
|
+
['test:setup', ['--test', 'scripts/setup-config.test.mjs']],
|
|
480
486
|
['dev:bff', ['--env-file-if-exists=.env.development', 'server/index.ts']],
|
|
481
487
|
['dev:web', ['vite']],
|
|
482
488
|
[
|
|
@@ -727,7 +733,9 @@ if (await exists('src/vr/work/WorkSource.ts')) {
|
|
|
727
733
|
for (const marker of [
|
|
728
734
|
'resourceCodeFromPathname',
|
|
729
735
|
'window.location.pathname',
|
|
736
|
+
'segments.length === 0',
|
|
730
737
|
'segments.length !== 1',
|
|
738
|
+
": '/api/work'",
|
|
731
739
|
'`/api/work/${encodeURIComponent(resourceCode)}`',
|
|
732
740
|
'asExplicitPanoTagList',
|
|
733
741
|
'hasCanonicalPanoTagData(value.contentType, value.data)',
|
|
@@ -1312,6 +1320,7 @@ for (const [relativePath, markers] of [
|
|
|
1312
1320
|
'loadServerConfig',
|
|
1313
1321
|
'REALSEE_GATEWAY_APP_KEY',
|
|
1314
1322
|
'REALSEE_GATEWAY_APP_SECRET',
|
|
1323
|
+
'REALSEE_DEFAULT_RESOURCE_CODE',
|
|
1315
1324
|
"gatewayEndpoint.protocol !== 'https:'",
|
|
1316
1325
|
'): ServerConfig {',
|
|
1317
1326
|
],
|
|
@@ -1346,6 +1355,7 @@ for (const [relativePath, markers] of [
|
|
|
1346
1355
|
[
|
|
1347
1356
|
'server/work-route.ts',
|
|
1348
1357
|
[
|
|
1358
|
+
"'/api/work'",
|
|
1349
1359
|
"'/api/work/:resourceCode'",
|
|
1350
1360
|
'getWorkByResourceCode(resourceCode, {',
|
|
1351
1361
|
'signal: requestAbort.signal',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# Fastify BFF server-only
|
|
2
|
-
# The browser calls only same-origin /api/work
|
|
1
|
+
# Fastify BFF server-only schema. Run npm run setup to create .env.development safely.
|
|
2
|
+
# The browser calls only same-origin /api/work; never rename these values to VITE_*.
|
|
3
3
|
# 标识当前的应用 ID(可选部署元数据)
|
|
4
4
|
APP_ID=next-vr-develpment
|
|
5
5
|
# Realsee 开放平台网关(中文版;海外版为 https://app-gateway.realsee.jp)
|
|
@@ -8,5 +8,7 @@ REALSEE_GATEWAY_ENDPOINT=https://app-gateway.realsee.cn
|
|
|
8
8
|
REALSEE_GATEWAY_APP_KEY=*
|
|
9
9
|
# Realsee 的网关 app secret
|
|
10
10
|
REALSEE_GATEWAY_APP_SECRET=*
|
|
11
|
+
# 首次运行时默认加载的 VR ID(resourceCode)
|
|
12
|
+
REALSEE_DEFAULT_RESOURCE_CODE=*
|
|
11
13
|
# Realsee 的企业账号 ID(可选部署元数据)
|
|
12
14
|
REALSEE_SOURCE=REALSEE_TECH
|
package/templates/base/README.md
CHANGED
|
@@ -11,16 +11,17 @@ Use Node.js `22.23.2` LTS from `.nvmrc` and `.node-version`: it is the current s
|
|
|
11
11
|
## Start
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
cp .env.example .env.development
|
|
15
|
-
# Fill REALSEE_GATEWAY_APP_KEY and REALSEE_GATEWAY_APP_SECRET in .env.development.
|
|
16
14
|
npm ci
|
|
15
|
+
npm run setup
|
|
17
16
|
npm run dev
|
|
18
|
-
# Open http://127.0.0.1:8087
|
|
17
|
+
# Open http://127.0.0.1:8087/.
|
|
19
18
|
```
|
|
20
19
|
|
|
20
|
+
`npm run setup` accepts exactly one versioned single-line payload copied from the Realsee China Portal onboarding page. Paste it only into the hidden interactive prompt. The payload contains AK, SK and the selected `resourceCode`; Base64URL is a cross-platform transport encoding, not encryption. Never put it in chat, command arguments, pipes, redirected input, shell history, source or logs. Setup writes the gitignored server-only `.env.development` atomically with `0600` on POSIX or a current-user ACL on Windows.
|
|
21
|
+
|
|
21
22
|
`REALSEE_GATEWAY_ENDPOINT` must be an HTTPS URL; the BFF refuses HTTP so AK/SK cannot be sent in cleartext. `npm run dev` validates `.env.development` before starting Fastify and Vite. After changing `.env.development`, stop the existing dev command and start it again so Node reloads the environment.
|
|
22
23
|
|
|
23
|
-
Development runs the Fastify BFF on `127.0.0.1:8088` and Vite on `127.0.0.1:8087`; Vite proxies `/api` to Fastify, so
|
|
24
|
+
Development runs the Fastify BFF on `127.0.0.1:8088` and Vite on `127.0.0.1:8087`; Vite proxies `/api` to Fastify, so onboarding uses the same-origin `GET /api/work` contract backed by `REALSEE_DEFAULT_RESOURCE_CODE`. The explicit `GET /api/work/{resourceCode}` and `/{resourceCode}` paths remain development-only compatibility routes. Port 8087 is non-privileged; do not change it to privileged port 517.
|
|
24
25
|
|
|
25
26
|
The BFF posts `REALSEE_GATEWAY_APP_KEY` and `REALSEE_GATEWAY_APP_SECRET` as form fields to `/auth/access_token`, caches the 5-minute token until shortly before `expire_at`, retries once after an upstream 401, then calls `GET /open/v3/vr/info?resource_code={resourceCode}` with the raw token in the `Authorization` header. It returns only the browser-safe `{ work, workCode, baseURL? }` envelope. Generic `tags` are not inferred as PanoTag data. The default gateway is `https://app-gateway.realsee.cn`; overseas deployments use `https://app-gateway.realsee.jp`. See the official [Realsee OpenAPI reference](https://open-platform.realsee.com/developer/openapi/?id=1001).
|
|
26
27
|
|
|
@@ -33,7 +34,7 @@ npm start
|
|
|
33
34
|
|
|
34
35
|
## Replace the example BFF
|
|
35
36
|
|
|
36
|
-
The browser depends
|
|
37
|
+
The browser onboarding path depends on `GET /api/work`; Fastify is an example implementation, not an application architecture requirement:
|
|
37
38
|
|
|
38
39
|
- `server/types.ts` declares the server-side contracts (`ServerConfig`, `WorkEnvelope`, `WorkDetailClient`, `WorkService`);
|
|
39
40
|
- `server/realsee-open-api.ts` owns Realsee token exchange, expiry caching and upstream Work calls;
|
|
@@ -46,7 +47,7 @@ The BFF is TypeScript and runs without a build step: Node executes `.ts` directl
|
|
|
46
47
|
|
|
47
48
|
A Go, Java or other backend may replace the entire `server/` directory as long as it preserves the same-origin endpoint and response envelope. Keep AK/SK and access tokens server-side; never put them in `src/`, `VITE_*`, Vite `define/envPrefix`, URLs, browser headers, logs or bundles. AK/SK authorize the BFF to call the upstream API; if an internet-facing deployment also needs caller or tenant authorization, add that policy before the Work route instead of exposing AK/SK to the browser. The generated project does not include `work.json` or customer Work data.
|
|
48
49
|
|
|
49
|
-
The generated project has one `.env.example`. Only the Fastify BFF reads `REALSEE_GATEWAY_ENDPOINT`, `REALSEE_GATEWAY_APP_KEY` and `
|
|
50
|
+
The generated project has one `.env.example`. Only the Fastify BFF reads `REALSEE_GATEWAY_ENDPOINT`, `REALSEE_GATEWAY_APP_KEY`, `REALSEE_GATEWAY_APP_SECRET` and `REALSEE_DEFAULT_RESOURCE_CODE`; `APP_ID` and `REALSEE_SOURCE` remain optional deployment metadata. Real values belong in `.env.development`, the deployment environment or a secret manager.
|
|
50
51
|
|
|
51
52
|
## Update dependencies
|
|
52
53
|
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@11.19.0",
|
|
7
7
|
"scripts": {
|
|
8
|
+
"setup": "node scripts/setup.mjs",
|
|
9
|
+
"test:setup": "node --test scripts/setup-config.test.mjs",
|
|
8
10
|
"dev": "npm run check:env && concurrently --kill-others-on-fail --names bff,web \"npm:dev:bff\" \"npm:dev:web\"",
|
|
9
11
|
"dev:bff": "node --env-file-if-exists=.env.development --watch server/index.ts",
|
|
10
12
|
"dev:web": "vite",
|
|
@@ -16,7 +18,7 @@
|
|
|
16
18
|
"format": "prettier --write .",
|
|
17
19
|
"format:check": "prettier --check .",
|
|
18
20
|
"check:bff": "npm run typecheck:bff",
|
|
19
|
-
"check": "npm run typecheck && npm run lint && npm run format:check",
|
|
21
|
+
"check": "npm run test:setup && npm run typecheck && npm run lint && npm run format:check",
|
|
20
22
|
"deps:relock": "npm install --package-lock-only",
|
|
21
23
|
"deps:install": "npm ci",
|
|
22
24
|
"build": "npm run check && vite build",
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { chmod, lstat, open, rename, rm } from 'node:fs/promises'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { spawn } from 'node:child_process'
|
|
6
|
+
|
|
7
|
+
export const SETUP_PAYLOAD_PREFIX = 'realsee-setup:v1:'
|
|
8
|
+
export const DEFAULT_GATEWAY_ENDPOINT = 'https://app-gateway.realsee.cn'
|
|
9
|
+
|
|
10
|
+
const RESOURCE_CODE_PATTERN = /^[A-Za-z0-9_-]{1,128}$/
|
|
11
|
+
const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/
|
|
12
|
+
const MAX_PAYLOAD_LENGTH = 8_192
|
|
13
|
+
const MAX_CREDENTIAL_LENGTH = 1_024
|
|
14
|
+
|
|
15
|
+
function setupError(message) {
|
|
16
|
+
return new Error(`配置包无效:${message}`)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function exactString(value, label) {
|
|
20
|
+
if (
|
|
21
|
+
typeof value !== 'string' ||
|
|
22
|
+
value.length === 0 ||
|
|
23
|
+
value.length > MAX_CREDENTIAL_LENGTH ||
|
|
24
|
+
value !== value.trim() ||
|
|
25
|
+
/[\0\r\n]/u.test(value)
|
|
26
|
+
) {
|
|
27
|
+
throw setupError(`${label} 缺失或格式不正确。`)
|
|
28
|
+
}
|
|
29
|
+
return value
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function decodeSetupPayload(input) {
|
|
33
|
+
const value = typeof input === 'string' ? input.trim() : ''
|
|
34
|
+
if (!value.startsWith(SETUP_PAYLOAD_PREFIX)) {
|
|
35
|
+
throw setupError('请从 Portal 复制完整的 v1 本地配置。')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const encoded = value.slice(SETUP_PAYLOAD_PREFIX.length)
|
|
39
|
+
if (
|
|
40
|
+
encoded.length === 0 ||
|
|
41
|
+
encoded.length > MAX_PAYLOAD_LENGTH ||
|
|
42
|
+
!BASE64URL_PATTERN.test(encoded)
|
|
43
|
+
) {
|
|
44
|
+
throw setupError('单行内容损坏或不完整。')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let text
|
|
48
|
+
try {
|
|
49
|
+
const bytes = Buffer.from(encoded, 'base64url')
|
|
50
|
+
if (bytes.toString('base64url') !== encoded) {
|
|
51
|
+
throw setupError('Base64URL 内容不是规范编码。')
|
|
52
|
+
}
|
|
53
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(bytes)
|
|
54
|
+
} catch {
|
|
55
|
+
throw setupError('Base64URL 内容无法解码。')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let parsed
|
|
59
|
+
try {
|
|
60
|
+
parsed = JSON.parse(text)
|
|
61
|
+
} catch {
|
|
62
|
+
throw setupError('JSON 内容无法解析。')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
66
|
+
throw setupError('内容结构不正确。')
|
|
67
|
+
}
|
|
68
|
+
const keys = Object.keys(parsed).sort()
|
|
69
|
+
if (keys.join(',') !== 'appKey,appSecret,resourceCode') {
|
|
70
|
+
throw setupError('字段集合不正确。')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const appKey = exactString(parsed.appKey, 'AK')
|
|
74
|
+
const appSecret = exactString(parsed.appSecret, 'SK')
|
|
75
|
+
const resourceCode = exactString(parsed.resourceCode, 'resourceCode')
|
|
76
|
+
if (!RESOURCE_CODE_PATTERN.test(resourceCode)) {
|
|
77
|
+
throw setupError('resourceCode 格式不正确。')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return Object.freeze({ appKey, appSecret, resourceCode })
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function quoteDotenvValue(value) {
|
|
84
|
+
return JSON.stringify(value)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function renderLocalEnv({ appKey, appSecret, resourceCode }) {
|
|
88
|
+
return [
|
|
89
|
+
'# Generated by npm run setup. Keep this server-only file out of version control.',
|
|
90
|
+
`REALSEE_GATEWAY_ENDPOINT=${DEFAULT_GATEWAY_ENDPOINT}`,
|
|
91
|
+
`REALSEE_GATEWAY_APP_KEY=${quoteDotenvValue(appKey)}`,
|
|
92
|
+
`REALSEE_GATEWAY_APP_SECRET=${quoteDotenvValue(appSecret)}`,
|
|
93
|
+
`REALSEE_DEFAULT_RESOURCE_CODE=${quoteDotenvValue(resourceCode)}`,
|
|
94
|
+
'',
|
|
95
|
+
].join('\n')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function run(command, args) {
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
const child = spawn(command, args, {
|
|
101
|
+
shell: false,
|
|
102
|
+
stdio: 'ignore',
|
|
103
|
+
windowsHide: true,
|
|
104
|
+
})
|
|
105
|
+
child.once('error', reject)
|
|
106
|
+
child.once('exit', (code) => {
|
|
107
|
+
if (code === 0) resolve()
|
|
108
|
+
else reject(new Error(`${command} exited with code ${code ?? 'unknown'}`))
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function windowsIdentity(env) {
|
|
114
|
+
const username = env.USERNAME?.trim() || os.userInfo().username
|
|
115
|
+
const domain = env.USERDOMAIN?.trim()
|
|
116
|
+
return domain ? `${domain}\\${username}` : username
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function protectLocalConfig(
|
|
120
|
+
targetPath,
|
|
121
|
+
{ platform = process.platform, env = process.env, runCommand = run } = {},
|
|
122
|
+
) {
|
|
123
|
+
if (platform === 'win32') {
|
|
124
|
+
const identity = windowsIdentity(env)
|
|
125
|
+
await runCommand('icacls.exe', [
|
|
126
|
+
targetPath,
|
|
127
|
+
'/inheritance:r',
|
|
128
|
+
'/grant:r',
|
|
129
|
+
`${identity}:(F)`,
|
|
130
|
+
])
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
await chmod(targetPath, 0o600)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function assertSafeTarget(targetPath) {
|
|
137
|
+
try {
|
|
138
|
+
const target = await lstat(targetPath)
|
|
139
|
+
if (!target.isFile() || target.isSymbolicLink()) {
|
|
140
|
+
throw new Error('.env.development 必须是普通文件。')
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (error?.code !== 'ENOENT') throw error
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function writeLocalConfig(
|
|
148
|
+
config,
|
|
149
|
+
{
|
|
150
|
+
root = process.cwd(),
|
|
151
|
+
platform = process.platform,
|
|
152
|
+
env = process.env,
|
|
153
|
+
runCommand = run,
|
|
154
|
+
} = {},
|
|
155
|
+
) {
|
|
156
|
+
const targetPath = path.join(path.resolve(root), '.env.development')
|
|
157
|
+
await assertSafeTarget(targetPath)
|
|
158
|
+
const temporaryPath = path.join(
|
|
159
|
+
path.dirname(targetPath),
|
|
160
|
+
`.env.development.${process.pid}.${randomUUID()}.tmp`,
|
|
161
|
+
)
|
|
162
|
+
const previousUmask = platform === 'win32' ? undefined : process.umask(0o077)
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const handle = await open(temporaryPath, 'wx', 0o600)
|
|
166
|
+
try {
|
|
167
|
+
await handle.writeFile(renderLocalEnv(config), 'utf8')
|
|
168
|
+
await handle.sync()
|
|
169
|
+
} finally {
|
|
170
|
+
await handle.close()
|
|
171
|
+
}
|
|
172
|
+
await protectLocalConfig(temporaryPath, { platform, env, runCommand })
|
|
173
|
+
await rename(temporaryPath, targetPath)
|
|
174
|
+
return targetPath
|
|
175
|
+
} finally {
|
|
176
|
+
if (previousUmask !== undefined) process.umask(previousUmask)
|
|
177
|
+
await rm(temporaryPath, { force: true }).catch(() => undefined)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { mkdtemp, readFile, rm, stat } from 'node:fs/promises'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import test from 'node:test'
|
|
6
|
+
import {
|
|
7
|
+
decodeSetupPayload,
|
|
8
|
+
protectLocalConfig,
|
|
9
|
+
renderLocalEnv,
|
|
10
|
+
SETUP_PAYLOAD_PREFIX,
|
|
11
|
+
writeLocalConfig,
|
|
12
|
+
} from './setup-config.mjs'
|
|
13
|
+
|
|
14
|
+
const CONFIG = Object.freeze({
|
|
15
|
+
appKey: 'test-app-key',
|
|
16
|
+
appSecret: '测试-secret_+/=',
|
|
17
|
+
resourceCode: 'resource-code_1',
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const ENCODED =
|
|
21
|
+
'eyJhcHBLZXkiOiJ0ZXN0LWFwcC1rZXkiLCJhcHBTZWNyZXQiOiLmtYvor5Utc2VjcmV0XysvPSIsInJlc291cmNlQ29kZSI6InJlc291cmNlLWNvZGVfMSJ9'
|
|
22
|
+
|
|
23
|
+
test('decodes the versioned UTF-8 Base64URL setup payload', () => {
|
|
24
|
+
assert.deepEqual(
|
|
25
|
+
decodeSetupPayload(`${SETUP_PAYLOAD_PREFIX}${ENCODED}`),
|
|
26
|
+
CONFIG,
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('rejects non-canonical, incomplete and extra-field payloads', () => {
|
|
31
|
+
assert.throws(() => decodeSetupPayload('not-a-setup-payload'))
|
|
32
|
+
assert.throws(() => decodeSetupPayload(`${SETUP_PAYLOAD_PREFIX}${ENCODED}=`))
|
|
33
|
+
const withExtraField = Buffer.from(
|
|
34
|
+
JSON.stringify({ ...CONFIG, unexpected: true }),
|
|
35
|
+
).toString('base64url')
|
|
36
|
+
assert.throws(() =>
|
|
37
|
+
decodeSetupPayload(`${SETUP_PAYLOAD_PREFIX}${withExtraField}`),
|
|
38
|
+
)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('renders one server-only env file without printing the setup payload', () => {
|
|
42
|
+
const rendered = renderLocalEnv(CONFIG)
|
|
43
|
+
assert.match(
|
|
44
|
+
rendered,
|
|
45
|
+
/REALSEE_GATEWAY_ENDPOINT=https:\/\/app-gateway\.realsee\.cn/u,
|
|
46
|
+
)
|
|
47
|
+
assert.match(rendered, /REALSEE_GATEWAY_APP_KEY="test-app-key"/u)
|
|
48
|
+
assert.match(rendered, /REALSEE_GATEWAY_APP_SECRET="测试-secret_\+\/="/u)
|
|
49
|
+
assert.match(rendered, /REALSEE_DEFAULT_RESOURCE_CODE="resource-code_1"/u)
|
|
50
|
+
assert.doesNotMatch(rendered, /realsee-setup:v1:/u)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('writes atomically with restrictive POSIX permissions', async () => {
|
|
54
|
+
if (process.platform === 'win32') return
|
|
55
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'realsee-setup-'))
|
|
56
|
+
try {
|
|
57
|
+
const target = await writeLocalConfig(CONFIG, { root, platform: 'linux' })
|
|
58
|
+
assert.equal(await readFile(target, 'utf8'), renderLocalEnv(CONFIG))
|
|
59
|
+
assert.equal((await stat(target)).mode & 0o777, 0o600)
|
|
60
|
+
} finally {
|
|
61
|
+
await rm(root, { recursive: true, force: true })
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('uses icacls without a shell for Windows ACL protection', async () => {
|
|
66
|
+
const calls = []
|
|
67
|
+
await protectLocalConfig('C:\\project\\.env.development', {
|
|
68
|
+
platform: 'win32',
|
|
69
|
+
env: { USERDOMAIN: 'WORKSTATION', USERNAME: 'developer' },
|
|
70
|
+
runCommand: async (command, args) => calls.push({ command, args }),
|
|
71
|
+
})
|
|
72
|
+
assert.deepEqual(calls, [
|
|
73
|
+
{
|
|
74
|
+
command: 'icacls.exe',
|
|
75
|
+
args: [
|
|
76
|
+
'C:\\project\\.env.development',
|
|
77
|
+
'/inheritance:r',
|
|
78
|
+
'/grant:r',
|
|
79
|
+
'WORKSTATION\\developer:(F)',
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
])
|
|
83
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { pathToFileURL } from 'node:url'
|
|
2
|
+
import { Writable } from 'node:stream'
|
|
3
|
+
import { createInterface } from 'node:readline/promises'
|
|
4
|
+
import { decodeSetupPayload, writeLocalConfig } from './setup-config.mjs'
|
|
5
|
+
|
|
6
|
+
class HiddenOutput extends Writable {
|
|
7
|
+
_write(_chunk, _encoding, callback) {
|
|
8
|
+
callback()
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function readHiddenPayload() {
|
|
13
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
'npm run setup 需要交互终端。请勿通过参数、管道、重定向或聊天传入配置包。',
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
process.stdout.write(
|
|
20
|
+
'请在 Portal 点击“复制本地配置”,粘贴到这里并按 Enter(输入不会显示):',
|
|
21
|
+
)
|
|
22
|
+
const controller = new AbortController()
|
|
23
|
+
const input = process.stdin
|
|
24
|
+
const output = new HiddenOutput()
|
|
25
|
+
const prompt = createInterface({ input, output, terminal: true })
|
|
26
|
+
prompt.once('SIGINT', () => controller.abort())
|
|
27
|
+
try {
|
|
28
|
+
const value = await prompt.question('', { signal: controller.signal })
|
|
29
|
+
process.stdout.write('\n')
|
|
30
|
+
return value
|
|
31
|
+
} finally {
|
|
32
|
+
prompt.close()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function main() {
|
|
37
|
+
const payload = decodeSetupPayload(await readHiddenPayload())
|
|
38
|
+
await writeLocalConfig(payload)
|
|
39
|
+
console.log('配置完成。请回到 AI 助手并回复“已完成”。')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const entrypoint = process.argv[1]
|
|
43
|
+
if (entrypoint && import.meta.url === pathToFileURL(entrypoint).href) {
|
|
44
|
+
main().catch((error) => {
|
|
45
|
+
const message = error instanceof Error ? error.message : '未知错误。'
|
|
46
|
+
console.error(`本地配置失败:${message}`)
|
|
47
|
+
process.exitCode = 1
|
|
48
|
+
})
|
|
49
|
+
}
|
|
@@ -9,6 +9,7 @@ const PROJECT_ROOT = fileURLToPath(new URL('..', import.meta.url))
|
|
|
9
9
|
|
|
10
10
|
export interface CreateAppOptions {
|
|
11
11
|
readonly workService: WorkService
|
|
12
|
+
readonly defaultResourceCode: string
|
|
12
13
|
readonly logger?: boolean
|
|
13
14
|
readonly serveFrontend?: boolean
|
|
14
15
|
readonly frontendRoot?: string
|
|
@@ -16,6 +17,7 @@ export interface CreateAppOptions {
|
|
|
16
17
|
|
|
17
18
|
export async function createApp({
|
|
18
19
|
workService,
|
|
20
|
+
defaultResourceCode,
|
|
19
21
|
logger = true,
|
|
20
22
|
serveFrontend = false,
|
|
21
23
|
frontendRoot = path.join(PROJECT_ROOT, 'dist'),
|
|
@@ -23,7 +25,7 @@ export async function createApp({
|
|
|
23
25
|
const app = Fastify({ logger })
|
|
24
26
|
|
|
25
27
|
app.get('/api/health', () => ({ ok: true }))
|
|
26
|
-
await app.register(workRoutes, { workService })
|
|
28
|
+
await app.register(workRoutes, { workService, defaultResourceCode })
|
|
27
29
|
|
|
28
30
|
if (serveFrontend) {
|
|
29
31
|
await app.register(fastifyStatic, {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ServerConfig } from './types.ts'
|
|
2
2
|
|
|
3
3
|
const DEFAULT_GATEWAY_ENDPOINT = 'https://app-gateway.realsee.cn'
|
|
4
|
+
const RESOURCE_CODE_PATTERN = /^[A-Za-z0-9_-]{1,128}$/
|
|
4
5
|
|
|
5
6
|
type ServerEnv = Record<string, string | undefined>
|
|
6
7
|
|
|
@@ -15,8 +16,16 @@ const nonEmptyString = (value: string | undefined): value is string =>
|
|
|
15
16
|
function requiredCredential(env: ServerEnv, key: string): string {
|
|
16
17
|
const value = env[key]?.trim()
|
|
17
18
|
if (!nonEmptyString(value) || value === '*') {
|
|
19
|
+
throw new Error(`${key} 未配置。请运行 npm run setup 完成本地配置。`)
|
|
20
|
+
}
|
|
21
|
+
return value
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function requiredResourceCode(env: ServerEnv): string {
|
|
25
|
+
const value = env.REALSEE_DEFAULT_RESOURCE_CODE?.trim()
|
|
26
|
+
if (!nonEmptyString(value) || !RESOURCE_CODE_PATTERN.test(value)) {
|
|
18
27
|
throw new Error(
|
|
19
|
-
|
|
28
|
+
'REALSEE_DEFAULT_RESOURCE_CODE 未配置或格式无效。请运行 npm run setup 完成本地配置。',
|
|
20
29
|
)
|
|
21
30
|
}
|
|
22
31
|
return value
|
|
@@ -50,5 +59,6 @@ export function loadServerConfig({
|
|
|
50
59
|
gatewayEndpoint: gatewayEndpoint.href.replace(/\/$/, ''),
|
|
51
60
|
appKey: requiredCredential(env, 'REALSEE_GATEWAY_APP_KEY'),
|
|
52
61
|
appSecret: requiredCredential(env, 'REALSEE_GATEWAY_APP_SECRET'),
|
|
62
|
+
defaultResourceCode: requiredResourceCode(env),
|
|
53
63
|
})
|
|
54
64
|
}
|
|
@@ -7,7 +7,11 @@ const serveFrontend = process.argv.includes('--serve-frontend')
|
|
|
7
7
|
const config = loadServerConfig({ serveFrontend })
|
|
8
8
|
const openApiClient = new RealseeOpenApiClient(config)
|
|
9
9
|
const workService = createWorkService({ openApiClient })
|
|
10
|
-
const app = await createApp({
|
|
10
|
+
const app = await createApp({
|
|
11
|
+
workService,
|
|
12
|
+
defaultResourceCode: config.defaultResourceCode,
|
|
13
|
+
serveFrontend,
|
|
14
|
+
})
|
|
11
15
|
|
|
12
16
|
let closing = false
|
|
13
17
|
const close = async (signal: string): Promise<void> => {
|
|
@@ -11,6 +11,7 @@ export interface ServerConfig {
|
|
|
11
11
|
readonly gatewayEndpoint: string
|
|
12
12
|
readonly appKey: string
|
|
13
13
|
readonly appSecret: string
|
|
14
|
+
readonly defaultResourceCode: string
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export interface RequestOptions {
|
|
@@ -21,7 +22,7 @@ export interface WorkCapabilities {
|
|
|
21
22
|
readonly modelFloorplan?: ModelFloorplanServerData
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
/** Browser-safe response envelope for `GET /api/work
|
|
25
|
+
/** Browser-safe response envelope for `GET /api/work`. */
|
|
25
26
|
export interface WorkEnvelope {
|
|
26
27
|
readonly work: RawRecord
|
|
27
28
|
readonly workCode: string
|
|
@@ -5,6 +5,7 @@ const RESOURCE_CODE_PATTERN = '^[A-Za-z0-9_-]{1,128}$'
|
|
|
5
5
|
|
|
6
6
|
export interface WorkRoutesOptions {
|
|
7
7
|
readonly workService: WorkService
|
|
8
|
+
readonly defaultResourceCode: string
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
interface WorkRouteParams {
|
|
@@ -23,9 +24,45 @@ const upstreamStatusCode = (error: unknown): number => {
|
|
|
23
24
|
|
|
24
25
|
export function workRoutes(
|
|
25
26
|
app: FastifyInstance,
|
|
26
|
-
{ workService }: WorkRoutesOptions,
|
|
27
|
+
{ workService, defaultResourceCode }: WorkRoutesOptions,
|
|
27
28
|
done: (error?: Error) => void,
|
|
28
29
|
): void {
|
|
30
|
+
const getWork = async (
|
|
31
|
+
request: FastifyRequest,
|
|
32
|
+
reply: FastifyReply,
|
|
33
|
+
resourceCode: string,
|
|
34
|
+
): Promise<WorkEnvelope | FastifyReply> => {
|
|
35
|
+
const requestAbort = new AbortController()
|
|
36
|
+
const abortDisconnectedRequest = (): void => {
|
|
37
|
+
if (request.raw.aborted || !request.raw.complete) requestAbort.abort()
|
|
38
|
+
}
|
|
39
|
+
request.raw.once('close', abortDisconnectedRequest)
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
return await workService.getWorkByResourceCode(resourceCode, {
|
|
43
|
+
signal: requestAbort.signal,
|
|
44
|
+
})
|
|
45
|
+
} catch (error) {
|
|
46
|
+
const statusCode = upstreamStatusCode(error)
|
|
47
|
+
request.log.error({ err: error }, 'Work BFF request failed')
|
|
48
|
+
return reply.code(statusCode).send({
|
|
49
|
+
error:
|
|
50
|
+
statusCode === 404
|
|
51
|
+
? 'WORK_NOT_FOUND'
|
|
52
|
+
: statusCode === 504
|
|
53
|
+
? 'WORK_UPSTREAM_TIMEOUT'
|
|
54
|
+
: 'WORK_UPSTREAM_FAILED',
|
|
55
|
+
requestId: request.id,
|
|
56
|
+
})
|
|
57
|
+
} finally {
|
|
58
|
+
request.raw.off('close', abortDisconnectedRequest)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
app.get('/api/work', async (request, reply) =>
|
|
63
|
+
getWork(request, reply, defaultResourceCode),
|
|
64
|
+
)
|
|
65
|
+
|
|
29
66
|
app.get(
|
|
30
67
|
'/api/work/:resourceCode',
|
|
31
68
|
{
|
|
@@ -43,37 +80,8 @@ export function workRoutes(
|
|
|
43
80
|
async (
|
|
44
81
|
request: FastifyRequest<{ Params: WorkRouteParams }>,
|
|
45
82
|
reply: FastifyReply,
|
|
46
|
-
): Promise<WorkEnvelope | FastifyReply> =>
|
|
47
|
-
|
|
48
|
-
const requestAbort = new AbortController()
|
|
49
|
-
const abortDisconnectedRequest = (): void => {
|
|
50
|
-
if (request.raw.aborted || !request.raw.complete) requestAbort.abort()
|
|
51
|
-
}
|
|
52
|
-
request.raw.once('close', abortDisconnectedRequest)
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
return await workService.getWorkByResourceCode(resourceCode, {
|
|
56
|
-
signal: requestAbort.signal,
|
|
57
|
-
})
|
|
58
|
-
} catch (error) {
|
|
59
|
-
const statusCode = upstreamStatusCode(error)
|
|
60
|
-
request.log.error(
|
|
61
|
-
{ err: error, resourceCode },
|
|
62
|
-
'Work BFF request failed',
|
|
63
|
-
)
|
|
64
|
-
return reply.code(statusCode).send({
|
|
65
|
-
error:
|
|
66
|
-
statusCode === 404
|
|
67
|
-
? 'WORK_NOT_FOUND'
|
|
68
|
-
: statusCode === 504
|
|
69
|
-
? 'WORK_UPSTREAM_TIMEOUT'
|
|
70
|
-
: 'WORK_UPSTREAM_FAILED',
|
|
71
|
-
requestId: request.id,
|
|
72
|
-
})
|
|
73
|
-
} finally {
|
|
74
|
-
request.raw.off('close', abortDisconnectedRequest)
|
|
75
|
-
}
|
|
76
|
-
},
|
|
83
|
+
): Promise<WorkEnvelope | FastifyReply> =>
|
|
84
|
+
getWork(request, reply, request.params.resourceCode),
|
|
77
85
|
)
|
|
78
86
|
|
|
79
87
|
done()
|
|
@@ -27,8 +27,8 @@ export function createWorkService({
|
|
|
27
27
|
.catch((error: unknown) => {
|
|
28
28
|
if (!signal?.aborted) {
|
|
29
29
|
console.warn(
|
|
30
|
-
|
|
31
|
-
error,
|
|
30
|
+
'所选空间的平面图 capability 不可用,继续返回基础 Work。',
|
|
31
|
+
error instanceof Error ? error.name : 'UnknownError',
|
|
32
32
|
)
|
|
33
33
|
}
|
|
34
34
|
return undefined
|
|
@@ -177,7 +177,9 @@ const asExplicitPanoTagList = (value: unknown): Tags['tagList'] | undefined => {
|
|
|
177
177
|
|
|
178
178
|
export function resourceCodeFromPathname(pathname: string) {
|
|
179
179
|
const segments = pathname.split('/').filter(Boolean)
|
|
180
|
-
if (segments.length
|
|
180
|
+
if (segments.length === 0) return undefined
|
|
181
|
+
if (segments.length !== 1)
|
|
182
|
+
throw new Error('访问路径必须为 / 或 /{resourceCode}。')
|
|
181
183
|
const [encodedResourceCode = ''] = segments
|
|
182
184
|
|
|
183
185
|
let resourceCode: string
|
|
@@ -199,13 +201,13 @@ export class RouteWorkSource implements WorkSource {
|
|
|
199
201
|
async load(signal?: AbortSignal): Promise<WorkPayload> {
|
|
200
202
|
const resourceCode = resourceCodeFromPathname(window.location.pathname)
|
|
201
203
|
// The generated Fastify BFF implements this contract; another language may replace it without changing the browser.
|
|
202
|
-
const
|
|
203
|
-
`/api/work/${encodeURIComponent(resourceCode)}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
},
|
|
208
|
-
)
|
|
204
|
+
const workUrl = resourceCode
|
|
205
|
+
? `/api/work/${encodeURIComponent(resourceCode)}`
|
|
206
|
+
: '/api/work'
|
|
207
|
+
const response = await fetch(workUrl, {
|
|
208
|
+
signal,
|
|
209
|
+
headers: { Accept: 'application/json' },
|
|
210
|
+
})
|
|
209
211
|
if (!response.ok) throw new Error(`Work 请求失败:HTTP ${response.status}`)
|
|
210
212
|
|
|
211
213
|
const raw = (await response.json()) as unknown
|
|
@@ -221,7 +223,9 @@ export class RouteWorkSource implements WorkSource {
|
|
|
221
223
|
work.workCode,
|
|
222
224
|
work.work_code,
|
|
223
225
|
work.code,
|
|
224
|
-
) ??
|
|
226
|
+
) ??
|
|
227
|
+
resourceCode ??
|
|
228
|
+
'unknown-work'
|
|
225
229
|
const presetData = asRecord(envelope.presetData)
|
|
226
230
|
const panoTagList = asExplicitPanoTagList(presetData.panoTagList)
|
|
227
231
|
const modelFloorplan = asModelFloorplanServerData(
|