@realsee/vr-cli 0.1.3
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/NOTICE.md +7 -0
- package/README.md +216 -0
- package/TERMS.txt +54 -0
- package/agent-assets/AGENTS.md +113 -0
- package/agent-assets/adapters/.cursor/rules/realsee-vr.mdc +7 -0
- package/agent-assets/adapters/.github/copilot-instructions.md +1 -0
- package/agent-assets/adapters/.kiro/steering/realsee-vr.md +6 -0
- package/agent-assets/adapters/CLAUDE.md +3 -0
- package/bin/vr-cli.mjs +3 -0
- package/dist/cli.mjs +24 -0
- package/package.json +38 -0
- package/skills/realsee-image-resources/SKILL.md +82 -0
- package/skills/realsee-image-resources/references/cors-and-three-textures.md +50 -0
- package/skills/realsee-image-resources/references/data-wanxiang.md +57 -0
- package/skills/realsee-image-resources/references/five-image-url-transform.md +81 -0
- package/skills/realsee-image-resources/references/source-and-domain-policy.md +63 -0
- package/skills/realsee-vr-app/SKILL.md +157 -0
- package/skills/realsee-vr-app/references/continuous-improvement.md +24 -0
- package/skills/realsee-vr-app/references/dnalogel-api-capabilities.md +241 -0
- package/skills/realsee-vr-app/references/dnalogel-openapi-recipes.md +818 -0
- package/skills/realsee-vr-app/references/five-api-capabilities.md +119 -0
- package/skills/realsee-vr-app/references/five-runtime.md +83 -0
- package/skills/realsee-vr-app/references/official-docs.md +61 -0
- package/skills/realsee-vr-app/references/plugin-lifecycle.md +65 -0
- package/skills/realsee-vr-app/references/ui-and-validation.md +57 -0
- package/skills/realsee-vr-app/references/vapor-dom-layer.md +186 -0
- package/skills/realsee-vr-app/scripts/verify-project.mjs +1566 -0
- package/skills/threejs-r117-five/SKILL.md +71 -0
- package/skills/threejs-r117-five/references/five-ownership.md +70 -0
- package/skills/threejs-r117-five/references/r117-capabilities.md +82 -0
- package/skills/threejs-r117-five/references/r117-version-guards.md +60 -0
- package/templates/base/.editorconfig +12 -0
- package/templates/base/.env.example +12 -0
- package/templates/base/.gitignore.template +6 -0
- package/templates/base/.node-version +1 -0
- package/templates/base/.npmrc.template +3 -0
- package/templates/base/.nvmrc +1 -0
- package/templates/base/.prettierignore +12 -0
- package/templates/base/README.md +160 -0
- package/templates/base/eslint.config.mjs +75 -0
- package/templates/base/index.html +13 -0
- package/templates/base/package-lock.json +6444 -0
- package/templates/base/package.json +55 -0
- package/templates/base/prettier.config.mjs +5 -0
- package/templates/base/server/app.ts +54 -0
- package/templates/base/server/check-config.ts +4 -0
- package/templates/base/server/config.ts +54 -0
- package/templates/base/server/index.ts +31 -0
- package/templates/base/server/realsee-open-api.ts +316 -0
- package/templates/base/server/types.ts +64 -0
- package/templates/base/server/work-route.ts +80 -0
- package/templates/base/server/work-service.ts +54 -0
- package/templates/base/shared/model-floorplan.ts +242 -0
- package/templates/base/src/main.tsx +11 -0
- package/templates/base/src/shared/useElementSize.ts +28 -0
- package/templates/base/src/styles.css +91 -0
- package/templates/base/src/vr/five/SpatialRuntimeProvider.tsx +234 -0
- package/templates/base/src/vr/five/api.ts +10 -0
- package/templates/base/src/vr/five/fiveConfig.ts +42 -0
- package/templates/base/src/vr/five/initialState.ts +48 -0
- package/templates/base/src/vr/plugins/SpatialPluginRegistry.ts +104 -0
- package/templates/base/src/vr/plugins/dnalogel.ts +2 -0
- package/templates/base/src/vr/plugins/examples/FeatureOwnedMeshPlugin.ts +73 -0
- package/templates/base/src/vr/work/WorkSource.ts +247 -0
- package/templates/base/tsconfig.json +27 -0
- package/templates/base/tsconfig.server.json +25 -0
- package/templates/base/vite.config.ts +21 -0
- package/templates/viewer/src/app/App.tsx +182 -0
- package/templates/viewer/src/features/floorplan/ModelFloorplanFeature.ts +141 -0
- package/templates/viewer/src/features/navigation/ModeSwitcher.tsx +111 -0
- package/templates/viewer/src/features/tags/PanoTagFeature.ts +110 -0
- package/templates/viewer/src/ui/ViewerOverlay.tsx +141 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
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.3",O=["portable","all","none"],B="22.23.2",C="22.18.0",te=`>=${C}`,D="11.19.0",ae=">=11.19.0",se=`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},a=e(r),n=e(t);if(!a||!n)return!1;for(let i=0;i<3;i+=1){let o=a.core[i]-n.core[i];if(o>0)return!0;if(o<0)return!1}return a.prerelease===n.prerelease||a.prerelease===null?!0:n.prerelease===null?!1:a.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_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","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 be,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,[a,n])=>e.replaceAll(`{{${a}}}`,String(n)).replaceAll(`__${a}__`,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 a of await Z(r,{withFileTypes:!0})){let n=L.join(r,a.name),i=L.join(t,a.name);if(a.isDirectory()){await T(n,i,e);continue}if(!a.isFile())continue;let o=L.extname(a.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 a of await Z(t,{withFileTypes:!0})){let n=L.join(t,a.name);a.isDirectory()?e.push(...await K(r,n)):a.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 ye({toolRoot:r,targetRoot:t,mode:e,tokens:a}){if(e==="none")return[];for(let i of ve)await T(_.join(r,"skills",i),_.join(t,".agents","skills",i),a);let n=await Ze(_.join(r,"agent-assets","AGENTS.md"),"utf8");if(await me(_.join(t,"AGENTS.md"),Q(n,a)),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),a);return await T(_.join(r,"agent-assets","adapters"),t,a),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 ar=!0,J=tr(new URL(ar?"..":"../..",import.meta.url)),sr=process.platform==="win32"?"npm.cmd":"npm";function or(r,t){let e=t||A.basename(r),a=e.trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"-").replace(/^-+|-+$/g,"");if(!a||!/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/.test(a))throw new Error(`\u65E0\u6CD5\u751F\u6210\u5408\u6CD5 npm \u5305\u540D\uFF1A${e}`);return a}function ir(r){return new Promise((t,e)=>{let a=er(sr,["ci"],{cwd:r,stdio:"inherit",shell:!1});a.once("error",e),a.once("exit",(n,i)=>{n===0?t():e(new Error(`npm ci \u5931\u8D25\uFF08code=${n??"null"}, signal=${i??"none"}\uFF09`))})})}async function ke({directory:r,preset:t,name:e,agentAssets:a="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(a))throw new Error(`\u672A\u77E5 agent assets \u6A21\u5F0F\uFF1A${a}\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 be(A.join(i,".gitignore.template"),A.join(i,".gitignore")),await be(A.join(i,".npmrc.template"),A.join(i,".npmrc")),await T(A.join(J,"templates","viewer"),i,m);let f=await ye({toolRoot:J,targetRoot:i,mode:a,tokens:m}),p={schemaVersion:1,toolVersion:j,preset:"viewer",packageName:o,agentAssets:a,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),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}
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
vr-cli create <directory> [options]
|
|
6
|
+
vr-cli doctor [directory] [--json]
|
|
7
|
+
vr-cli policy [--json]
|
|
8
|
+
vr-cli help
|
|
9
|
+
|
|
10
|
+
Create options:
|
|
11
|
+
--preset <viewer> Optional; Viewer is the only template
|
|
12
|
+
--name <npm-name> Override generated package name
|
|
13
|
+
--agent-assets <mode> portable | all | none (default: portable)
|
|
14
|
+
--no-install Skip npm ci
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
vr-cli create ./my-vrapp
|
|
18
|
+
vr-cli create ./my-vrapp --preset viewer --agent-assets all --no-install
|
|
19
|
+
vr-cli doctor ./my-vrapp --json
|
|
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 a=e[0]||(t.help?"help":void 0);if(!a||a==="help"){console.log(Ye);return}if(a==="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 ke({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: cp .env.example .env.development"):console.log("Next: npm ci && cp .env.example .env.development"),console.log("Configure: set server-only REALSEE_GATEWAY_APP_KEY and REALSEE_GATEWAY_APP_SECRET, then run npm run dev"),console.log("Open: http://127.0.0.1:8087/{resourceCode} (replace {resourceCode})");return}if(a==="doctor"){(await Ue({directory:e[1]||".",json:t.json})).ok||(process.exitCode=1);return}if(a==="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${a}
|
|
23
|
+
|
|
24
|
+
${Ye}`)}catch(t){console.error(t instanceof Error?t.message:String(t)),process.exitCode=1}}export{mt as main};
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@realsee/vr-cli",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Create and validate TypeScript projects for Realsee spatial Viewer applications.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"realsee",
|
|
8
|
+
"spatial",
|
|
9
|
+
"vr",
|
|
10
|
+
"viewer",
|
|
11
|
+
"five",
|
|
12
|
+
"scaffolding"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"bin": {
|
|
16
|
+
"vr-cli": "bin/vr-cli.mjs"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"registry": "https://registry.npmjs.org/",
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"agent-assets",
|
|
24
|
+
"bin",
|
|
25
|
+
"skills",
|
|
26
|
+
"dist",
|
|
27
|
+
"templates",
|
|
28
|
+
"README.md",
|
|
29
|
+
"NOTICE.md",
|
|
30
|
+
"TERMS.txt"
|
|
31
|
+
],
|
|
32
|
+
"packageManager": "npm@11.19.0",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=22.18.0",
|
|
35
|
+
"npm": ">=11.19.0"
|
|
36
|
+
},
|
|
37
|
+
"license": "SEE LICENSE IN TERMS.txt"
|
|
38
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: realsee-image-resources
|
|
3
|
+
description: Use Realsee image assets safely with approved Realsee CDN domains, Five defaultImageURLTransform, Tencent Cloud Infinite (数据万象) imageInfo/imageMogr2 operations, browser CORS, and Three.js r117 textures. Use for image URLs, thumbnails, resize, crop, quality, format conversion, image metadata, Five imageOptions/textureOptions, CDN mappings, texture loading, and image resource provenance.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Realsee image resources
|
|
7
|
+
|
|
8
|
+
Use this Skill whenever a Realsee application loads, transforms, inspects or renders an image. It owns image-source authorization, CDN host selection, Five image URL transformation, 数据万象 boundaries, CORS checks and handoff to Three texture ownership rules.
|
|
9
|
+
|
|
10
|
+
These rules are invariant across image consumers:
|
|
11
|
+
|
|
12
|
+
- Image resources use approved Realsee CDN domains only; never use `ljcdn.com`, `lianjia.com` or their subdomains.
|
|
13
|
+
- Prefer Five `defaultImageURLTransform` over hand-built 数据万象 query strings when its installed contract satisfies the request.
|
|
14
|
+
- URL transformation does not grant CORS access.
|
|
15
|
+
- Ordinary images use the deployment's approved image-4 host.
|
|
16
|
+
- 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.
|
|
17
|
+
|
|
18
|
+
## 1. Classify the image request
|
|
19
|
+
|
|
20
|
+
Identify all of these before changing code:
|
|
21
|
+
|
|
22
|
+
1. **Source owner:** Work/OpenAPI, approved Realsee API/CDN, user-authorized project asset or user upload.
|
|
23
|
+
2. **Resource family:** pano, texture, tile, model-associated image, ordinary image or static application asset.
|
|
24
|
+
3. **Operation:** unchanged load, size/quality optimization, crop, format conversion, metadata lookup or Three texture creation.
|
|
25
|
+
4. **Access model:** public HTTPS URL, private/signed URL or server-mediated resource.
|
|
26
|
+
5. **Consumer:** Five Work/runtime, dnalogel plugin, DOM image, Canvas, or an application-owned Three texture.
|
|
27
|
+
|
|
28
|
+
Unknown ownership or an unapproved host blocks the operation. Do not invent a replacement image URL.
|
|
29
|
+
|
|
30
|
+
## 2. Apply the domain policy
|
|
31
|
+
|
|
32
|
+
Read [references/source-and-domain-policy.md](references/source-and-domain-policy.md).
|
|
33
|
+
|
|
34
|
+
- Use approved Realsee domain families such as `realsee-cdn.cn`, `rscdn.cn` and `realsee-cdn.com` according to the deployment contract.
|
|
35
|
+
- Image resource URLs must not use `ljcdn.com`, `lianjia.com` or their subdomains.
|
|
36
|
+
- Reject a forbidden source instead of silently replacing its hostname. Equal paths are not proof that a resource was synchronized.
|
|
37
|
+
- Ordinary images use the deployment's approved image-4 host. Pano, texture, tile and model resources keep their dedicated pools.
|
|
38
|
+
- Overseas resources require separate synchronization; never infer that a domestic path exists overseas.
|
|
39
|
+
|
|
40
|
+
## 3. Prefer Five's installed URL transform
|
|
41
|
+
|
|
42
|
+
Read [references/five-image-url-transform.md](references/five-image-url-transform.md).
|
|
43
|
+
|
|
44
|
+
For Five 6.8.9, import `defaultImageURLTransform`, `ImageURLOptions` and `ImageURLMappings` from `@realsee/five`. Confirm the installed declarations before using a different version. Prefer this API over a second utility package or a hand-built `imageMogr2` command when its declared options satisfy the request.
|
|
45
|
+
|
|
46
|
+
Treat custom mappings as deployment configuration. The public declaration exposes `type?: string`; it does not publish a cloud-service enum. A value such as `tencentCloud` is a reviewed Realsee deployment convention, not a TypeScript guarantee.
|
|
47
|
+
|
|
48
|
+
Do not mutate caller options, downgrade HTTPS, append credentials, or apply an image transform to an unapproved host.
|
|
49
|
+
|
|
50
|
+
## 4. Use 数据万象 only on a confirmed service host
|
|
51
|
+
|
|
52
|
+
Read [references/data-wanxiang.md](references/data-wanxiang.md).
|
|
53
|
+
|
|
54
|
+
- Use `imageInfo` to avoid downloading an image only to discover basic dimensions, but validate the returned metadata as untrusted input.
|
|
55
|
+
- Use Five's transform for the supported resize, quality, format, crop, sharpen and EXIF-orientation subset.
|
|
56
|
+
- Use raw 数据万象 commands only when the operation is absent from the installed Five contract and the exact command is verified against Tencent Cloud documentation.
|
|
57
|
+
- Do not assume overseas, AWS-backed or Aliyun-backed Realsee domains accept Tencent Cloud query syntax.
|
|
58
|
+
- Public query transforms may run in the browser only for approved public URLs. Private signing and credentials remain server-side.
|
|
59
|
+
|
|
60
|
+
## 5. Preserve CORS and texture ownership
|
|
61
|
+
|
|
62
|
+
Read [references/cors-and-three-textures.md](references/cors-and-three-textures.md) and, for Three work, `../threejs-r117-five/SKILL.md`.
|
|
63
|
+
|
|
64
|
+
A transformed URL is not CORS authorization. Validate the final URL, redirects, response headers and browser behavior. In Three r117 use `texture.encoding = THREE.sRGBEncoding` for color textures when appropriate, not modern `texture.colorSpace`, and dispose only application-owned textures and media.
|
|
65
|
+
|
|
66
|
+
## 6. Validate
|
|
67
|
+
|
|
68
|
+
Run type checking and production build, then test the final transformed URL in a real browser. Verify:
|
|
69
|
+
|
|
70
|
+
- the final hostname is an approved Realsee host and the URL remains HTTPS;
|
|
71
|
+
- no forbidden legacy host, credential or unrelated third-party URL reaches source or output;
|
|
72
|
+
- transform parameters match the requested resource family and remain bounded;
|
|
73
|
+
- `imageInfo` or transformed image failures use an explicit fallback without retry loops;
|
|
74
|
+
- CORS, content type, dimensions, orientation and cache behavior are correct;
|
|
75
|
+
- Five rendering and application-owned Three texture cleanup survive remounts.
|
|
76
|
+
|
|
77
|
+
## Sources
|
|
78
|
+
|
|
79
|
+
- Tencent Cloud Infinite image processing: https://cloud.tencent.com/document/product/460/72916
|
|
80
|
+
- Tencent Cloud `imageInfo`: https://cloud.tencent.com/document/product/460/6927
|
|
81
|
+
|
|
82
|
+
The source material is summarized rather than copied. Installed package declarations remain authoritative for exact SDK signatures.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# CORS and Three.js r117 textures
|
|
2
|
+
|
|
3
|
+
## URL transformation is not CORS
|
|
4
|
+
|
|
5
|
+
Changing an image hostname, size or format does not authorize browser reads. Validate the final response in a real browser:
|
|
6
|
+
|
|
7
|
+
- every redirect remains HTTPS and on the approved host allowlist;
|
|
8
|
+
- `Access-Control-Allow-Origin` matches the deployment origin and credential mode;
|
|
9
|
+
- the response has the expected image content type and is not an HTML/service error body;
|
|
10
|
+
- Canvas readback, texture upload and screenshot/export behavior do not fail from a tainted origin;
|
|
11
|
+
- cache headers and `Vary` behavior are compatible with transformed variants.
|
|
12
|
+
|
|
13
|
+
Do not add `crossOrigin` flags blindly or use a public proxy to bypass a CDN policy. Fix the authoritative CDN/CORS configuration.
|
|
14
|
+
|
|
15
|
+
## Three r117 texture rules
|
|
16
|
+
|
|
17
|
+
Read `../../threejs-r117-five/SKILL.md` before changing texture loaders or materials.
|
|
18
|
+
|
|
19
|
+
- Use the single pinned `three@0.117.1` instance.
|
|
20
|
+
- Color/albedo textures normally use `texture.encoding = THREE.sRGBEncoding` when the installed material/rendering contract expects color data.
|
|
21
|
+
- Data maps remain linear according to their declared use.
|
|
22
|
+
- Do not use modern `texture.colorSpace`, `SRGBColorSpace`, `three/addons`, WebGPU or TSL APIs.
|
|
23
|
+
- Bound decoded dimensions and GPU memory, not just compressed transfer size.
|
|
24
|
+
- Request a Five render after an asynchronously loaded application texture changes visible output.
|
|
25
|
+
|
|
26
|
+
## Ownership and cleanup
|
|
27
|
+
|
|
28
|
+
An application feature disposes only resources it creates:
|
|
29
|
+
|
|
30
|
+
- remove feature-owned Object3D roots;
|
|
31
|
+
- abort or ignore stale image requests;
|
|
32
|
+
- stop owned video/canvas producers;
|
|
33
|
+
- dispose feature-owned textures, materials and render targets exactly once;
|
|
34
|
+
- release object URLs and event listeners;
|
|
35
|
+
- do not dispose Work, Five, dnalogel or shared-cache textures.
|
|
36
|
+
|
|
37
|
+
A transformed URL may be shared by several materials. URL equality alone does not define texture ownership; record the cache/owner explicitly.
|
|
38
|
+
|
|
39
|
+
## Validation checklist
|
|
40
|
+
|
|
41
|
+
Test representative pano, texture and ordinary-image URLs rather than relying on one sample:
|
|
42
|
+
|
|
43
|
+
1. source and transformed host policy;
|
|
44
|
+
2. success and malformed-service-response paths;
|
|
45
|
+
3. CORS with the real application origin;
|
|
46
|
+
4. format fallback for browsers without AVIF/HEIF support;
|
|
47
|
+
5. EXIF orientation and crop behavior;
|
|
48
|
+
6. color encoding under the pinned Five/Three renderer;
|
|
49
|
+
7. remount and Work-change cleanup;
|
|
50
|
+
8. network transfer, decoded size and GPU memory.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# 数据万象 image processing
|
|
2
|
+
|
|
3
|
+
Tencent Cloud Infinite (数据万象/CI) provides image metadata and `imageMogr2` processing. In Realsee applications, use it only when the concrete Realsee image host is configured for that service.
|
|
4
|
+
|
|
5
|
+
## Preferred decision order
|
|
6
|
+
|
|
7
|
+
1. Leave the approved source unchanged when no optimization is required.
|
|
8
|
+
2. Use Five 6.8.9 `defaultImageURLTransform` when `ImageURLOptions` covers the requirement.
|
|
9
|
+
3. Use `imageInfo` for basic metadata when the host contract explicitly supports it.
|
|
10
|
+
4. Use a raw 数据万象 command only for a verified capability absent from Five's installed contract.
|
|
11
|
+
5. Use a server/BFF adapter when signing, private access, tenant authorization or hidden parameters are required.
|
|
12
|
+
|
|
13
|
+
Do not introduce an additional image-transform library merely to duplicate Five's declared transform.
|
|
14
|
+
|
|
15
|
+
## Approved operation surface
|
|
16
|
+
|
|
17
|
+
Five's reviewed subset covers size, quality, output format, rectangular crop, sharpen and EXIF-orientation behavior. Keep values bounded and preserve the resource-family `key`.
|
|
18
|
+
|
|
19
|
+
A raw `imageMogr2` expression is untrusted configuration. Parse/construct it from a typed allowlist; do not concatenate user-provided command fragments. Verify operation order, escaping, input limits, output limits, animation behavior and format support against current Tencent documentation.
|
|
20
|
+
|
|
21
|
+
## `imageInfo`
|
|
22
|
+
|
|
23
|
+
Use `imageInfo` to query basic image information without decoding the full image in the browser. Treat the response as unknown and validate at least:
|
|
24
|
+
|
|
25
|
+
- successful HTTP and service status;
|
|
26
|
+
- expected JSON content type;
|
|
27
|
+
- finite, positive width and height within application limits;
|
|
28
|
+
- supported format and orientation fields;
|
|
29
|
+
- final response URL remains on the approved host.
|
|
30
|
+
|
|
31
|
+
Do not append `?imageInfo` blindly to a URL that already carries a signature or another processing command. Preserve query ordering and signing rules through the approved URL builder or request a server-generated URL.
|
|
32
|
+
|
|
33
|
+
## Service and deployment boundaries
|
|
34
|
+
|
|
35
|
+
- `vr-image-4.realsee-cdn.cn` is the ordinary-image host for the domestic enterprise Realsee CDN policy and is the default 数据万象 example host.
|
|
36
|
+
- `vr-image-4.rscdn.cn` may be selected by the domestic personal/SaaS-C deployment contract.
|
|
37
|
+
- `global-image-4.realsee-cdn.com` is an overseas host and requires separately synchronized resources. Do not infer Tencent query compatibility from the hostname alone.
|
|
38
|
+
- `mars-*.realsee-cdn.cn` is an Aliyun mapping family; use its declared image service rather than Tencent command syntax.
|
|
39
|
+
- Never use `ljcdn.com` or `lianjia.com` image resources.
|
|
40
|
+
|
|
41
|
+
## Security and failure handling
|
|
42
|
+
|
|
43
|
+
- Public image processing needs no browser credential beyond an already approved public URL.
|
|
44
|
+
- COS/CI credentials and private signing remain in a BFF or secret manager; never put them in `VITE_*`, source, logs or generated assets.
|
|
45
|
+
- Signed URLs are sensitive and short-lived. Avoid logging complete queries and do not cache them beyond expiry.
|
|
46
|
+
- Use explicit timeout, abort and fallback behavior. A failed optimization may fall back to the original approved image only when product policy allows it.
|
|
47
|
+
- Rate-limit metadata probes and deduplicate concurrent requests; do not issue `imageInfo` per frame or render.
|
|
48
|
+
|
|
49
|
+
## Authoritative public references
|
|
50
|
+
|
|
51
|
+
- Tencent basic image processing: https://cloud.tencent.com/document/product/460/72916
|
|
52
|
+
- Tencent scaling: https://cloud.tencent.com/document/product/460/36540
|
|
53
|
+
- Tencent cropping: https://cloud.tencent.com/document/product/460/36541
|
|
54
|
+
- Tencent format conversion: https://cloud.tencent.com/document/product/460/36543
|
|
55
|
+
- Tencent image information: https://cloud.tencent.com/document/product/460/6927
|
|
56
|
+
|
|
57
|
+
External documentation is summarized; confirm current limits and syntax at implementation time.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Five 6.8.9 image URL transformation
|
|
2
|
+
|
|
3
|
+
Installed declarations are authoritative. For Five 6.8.9, the root package exports:
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import {
|
|
7
|
+
defaultImageURLTransform,
|
|
8
|
+
type ImageURLOptions,
|
|
9
|
+
type ImageURLMappings,
|
|
10
|
+
} from '@realsee/five'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The precise public function contract is:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
(source: string, options: ImageURLOptions) => string
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Declared options
|
|
20
|
+
|
|
21
|
+
`ImageURLOptions` declares:
|
|
22
|
+
|
|
23
|
+
- required `key: string` resource category;
|
|
24
|
+
- optional `size: number`;
|
|
25
|
+
- optional `quality: number` documented as 0–100;
|
|
26
|
+
- optional `format: 'jpg' | 'jpeg' | 'png' | 'heif' | 'heic' | 'webp' | 'avif'`;
|
|
27
|
+
- optional `cut: [x, y, width, height]`;
|
|
28
|
+
- optional `sharpen: number`, with installed documentation noting an overseas limitation;
|
|
29
|
+
- optional `forceUseExifOrientation: boolean`;
|
|
30
|
+
- optional `mappings: ImageURLMappings`.
|
|
31
|
+
|
|
32
|
+
Validate application input before constructing these options. Use finite positive dimensions, clamp quality to the declared range, reject negative crop values and choose formats supported by the target browsers/runtime.
|
|
33
|
+
|
|
34
|
+
## Mapping contract
|
|
35
|
+
|
|
36
|
+
Each `ImageURLMappings` entry is keyed by a concrete public-data hostname and requires resource pools for `pano`, `tile`, `model`, `texture` and `default`:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const REALSEE_IMAGE_URL_MAPPINGS = {
|
|
40
|
+
'vr-public.realsee-cdn.cn': {
|
|
41
|
+
type: 'tencentCloud',
|
|
42
|
+
pano: ['vr-image-1.realsee-cdn.cn', 'vr-image-2.realsee-cdn.cn'],
|
|
43
|
+
texture: ['vr-image-3.realsee-cdn.cn'],
|
|
44
|
+
tile: ['vr-tile-1.realsee-cdn.cn', 'vr-tile-2.realsee-cdn.cn'],
|
|
45
|
+
model: ['vr-model-1.realsee-cdn.cn', 'vr-model-2.realsee-cdn.cn'],
|
|
46
|
+
default: ['vr-image-4.realsee-cdn.cn'],
|
|
47
|
+
},
|
|
48
|
+
} satisfies ImageURLMappings
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This example intentionally uses only Realsee domains. Add `rscdn.cn` or overseas mappings only when the application's deployment contract requires them. Never add `ljcdn.com` or `lianjia.com` image hosts.
|
|
52
|
+
|
|
53
|
+
The public SDK type of `type` is only `string | undefined`. `tencentCloud` above is a reviewed Realsee deployment convention; it is not a public Five literal union or enum. Recheck installed implementation/declarations when changing Five versions.
|
|
54
|
+
|
|
55
|
+
## Non-mutating adapter
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
export function imageURLTransform(
|
|
59
|
+
source: string,
|
|
60
|
+
options: ImageURLOptions,
|
|
61
|
+
): string {
|
|
62
|
+
assertApprovedRealseeImageURL(source)
|
|
63
|
+
const transformed = defaultImageURLTransform(source, {
|
|
64
|
+
...options,
|
|
65
|
+
mappings: {
|
|
66
|
+
...REALSEE_IMAGE_URL_MAPPINGS,
|
|
67
|
+
...options.mappings,
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
assertApprovedRealseeImageURL(transformed)
|
|
71
|
+
return transformed
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`assertApprovedRealseeImageURL` is an application-owned policy function that parses the URL, requires HTTPS and checks a concrete host allowlist. Do not mutate `options`; do not use `replace('http:', '')` as a security policy.
|
|
76
|
+
|
|
77
|
+
## Five initialization
|
|
78
|
+
|
|
79
|
+
Five also declares `ImageOptions.transform` and `TextureOptions.transform`; both receive the same source/options shape. The application may inject the reviewed adapter through `FiveInitArgs.imageOptions` and `textureOptions` in `src/vr/five/fiveConfig.ts`.
|
|
80
|
+
|
|
81
|
+
Keep the policy injectable. Do not place credentials, signed private URLs or tenant-specific secrets in a module-level mapping. A transform changes a URL; it does not grant CORS access, authorize a source, upload an object or prove that a derived object exists.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Realsee image source and domain policy
|
|
2
|
+
|
|
3
|
+
## Source authorization
|
|
4
|
+
|
|
5
|
+
An image must come from one of these explicit sources:
|
|
6
|
+
|
|
7
|
+
1. the active Work or a validated Realsee OpenAPI response;
|
|
8
|
+
2. an approved Realsee API/CDN contract;
|
|
9
|
+
3. a user-authorized project asset or upload;
|
|
10
|
+
4. an application API that records provenance and authorization.
|
|
11
|
+
|
|
12
|
+
A URL discovered in sample code, a screenshot, arbitrary JSON or an unrelated public website is not authorized input. Preserve the original source/provenance in application data even when a derived CDN URL is produced.
|
|
13
|
+
|
|
14
|
+
## Forbidden legacy image hosts
|
|
15
|
+
|
|
16
|
+
Image resource URLs must not use these hosts or any subdomain:
|
|
17
|
+
|
|
18
|
+
- `ljcdn.com`
|
|
19
|
+
- `lianjia.com`
|
|
20
|
+
|
|
21
|
+
Compare a parsed, lower-cased `URL.hostname` by exact hostname or dot-suffix. Do not use substring matching, and do not apply this image-host rule to repository remotes or documentation links.
|
|
22
|
+
|
|
23
|
+
Reject a forbidden image URL. Do not automatically rewrite it to a Realsee hostname: path equality does not prove that the object was synchronized, and overseas resources explicitly require separate synchronization.
|
|
24
|
+
|
|
25
|
+
## Approved Realsee host families
|
|
26
|
+
|
|
27
|
+
The exact host set is deployment configuration, not a global wildcard allowlist.
|
|
28
|
+
|
|
29
|
+
| Deployment | Business domain | CDN family | Ordinary image host |
|
|
30
|
+
|---|---|---|---|
|
|
31
|
+
| Domestic enterprise | `realsee.cn` | `realsee-cdn.cn` | `vr-image-4.realsee-cdn.cn` |
|
|
32
|
+
| Domestic personal/SaaS-C | `rsvr.cn` | `rscdn.cn` | `vr-image-4.rscdn.cn` |
|
|
33
|
+
| Overseas | `realsee.ai` / legacy `realsee.jp` | `realsee-cdn.com` | `global-image-4.realsee-cdn.com` |
|
|
34
|
+
|
|
35
|
+
Use a concrete allowlist from the product/deployment contract. A hostname merely ending in one of these suffixes is not automatically trusted.
|
|
36
|
+
|
|
37
|
+
## Resource-family routing
|
|
38
|
+
|
|
39
|
+
For the selected deployment prefix:
|
|
40
|
+
|
|
41
|
+
- `public`: backend-delivered data other than Work;
|
|
42
|
+
- `static`: JavaScript, CSS and other frontend static assets;
|
|
43
|
+
- `image-1` and `image-2`: Five panorama pools;
|
|
44
|
+
- `image-3`: Five model textures;
|
|
45
|
+
- `image-4`: ordinary images and the primary image-processing candidate;
|
|
46
|
+
- `tile-1` and `tile-2`: Five panorama tiles;
|
|
47
|
+
- `model-1` and `model-2`: Five models;
|
|
48
|
+
- `video-1` and `video-2`: video resources.
|
|
49
|
+
|
|
50
|
+
Do not route a pano, tile, texture or model URL through image-4 solely to gain processing features. Keep the resource family declared by Work/Five unless an authoritative migration contract says otherwise.
|
|
51
|
+
|
|
52
|
+
## URL handling
|
|
53
|
+
|
|
54
|
+
- Require HTTPS for final browser URLs.
|
|
55
|
+
- Preserve paths, encoded components and existing query parameters through a service-aware builder.
|
|
56
|
+
- Do not strip signatures, append operations after a signature blindly, or log private query strings.
|
|
57
|
+
- Follow redirects only to another approved host and revalidate every hop.
|
|
58
|
+
- Keep overseas and domestic mappings separate.
|
|
59
|
+
- Never introduce an unrelated third-party placeholder when a source is missing.
|
|
60
|
+
|
|
61
|
+
## Upload boundary
|
|
62
|
+
|
|
63
|
+
This portable Skill does not define an upload client. Upload destination, authentication, object key policy, content validation and overwrite behavior require a separate authoritative service contract. Do not depend on internal legacy upload tools or infer that uploading to one CDN family synchronizes every deployment.
|