@ioai/robot_viewer 0.0.0-stage → 1.1.1

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.
Files changed (159) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +183 -2
  3. package/bin/cli.js +83 -0
  4. package/dist/assets/css/PlotPanel-3e7d7555.css +1 -0
  5. package/dist/assets/js/ColladaLoader-2a0c4638.js +1 -0
  6. package/dist/assets/js/GLTFLoader-7a7f40d7.js +1 -0
  7. package/dist/assets/js/MTLLoader-b6ce88b7.js +1 -0
  8. package/dist/assets/js/OBJLoader-7667aa01.js +1 -0
  9. package/dist/assets/js/PlotPanel-64b4820e.js +1 -0
  10. package/dist/assets/js/STLLoader-de0915d4.js +1 -0
  11. package/dist/assets/js/URDFLoader-a34b6917.js +1 -0
  12. package/dist/assets/js/USDZLoader-64197f74.js +7 -0
  13. package/dist/assets/js/__vite-browser-external-afb87ea8.js +1 -0
  14. package/dist/assets/js/codemirror-c3cec7b3.js +1 -0
  15. package/dist/assets/js/codemirror-themes-e6995a6a.js +1 -0
  16. package/dist/assets/js/d3-2e308866.js +1 -0
  17. package/dist/assets/js/index-2e5294e6.js +6 -0
  18. package/dist/assets/js/mujoco_wasm-e013cb22.js +1 -0
  19. package/dist/assets/js/three-9c891de0.js +6 -0
  20. package/dist/camera-publish-worker.js +1 -0
  21. package/dist/favicon.svg +2 -0
  22. package/dist/heartbeat-worker.js +1 -0
  23. package/dist/index.html +4373 -0
  24. package/dist/models/mocap/blender_human.urdf +2319 -0
  25. package/dist/models/mocap/blender_human_skeleton_v4.urdf +1432 -0
  26. package/dist/models/mocap/blender_human_skeleton_v5.urdf +2717 -0
  27. package/dist/models/mocap/meshes/v4/L_base_link.STL +0 -0
  28. package/dist/models/mocap/meshes/v4/L_figer1_1.STL +0 -0
  29. package/dist/models/mocap/meshes/v4/L_figer1_2.STL +0 -0
  30. package/dist/models/mocap/meshes/v4/L_figer1_3.STL +0 -0
  31. package/dist/models/mocap/meshes/v4/L_figer1_4.STL +0 -0
  32. package/dist/models/mocap/meshes/v4/L_figer1_5.STL +0 -0
  33. package/dist/models/mocap/meshes/v4/L_figer1base.STL +0 -0
  34. package/dist/models/mocap/meshes/v4/L_figer2_1.STL +0 -0
  35. package/dist/models/mocap/meshes/v4/L_figer2_2.STL +0 -0
  36. package/dist/models/mocap/meshes/v4/L_figer2_3.STL +0 -0
  37. package/dist/models/mocap/meshes/v4/L_figer2_4.STL +0 -0
  38. package/dist/models/mocap/meshes/v4/L_figer2_5.STL +0 -0
  39. package/dist/models/mocap/meshes/v4/L_figer3_1.STL +0 -0
  40. package/dist/models/mocap/meshes/v4/L_figer3_2.STL +0 -0
  41. package/dist/models/mocap/meshes/v4/L_figer3_3.STL +0 -0
  42. package/dist/models/mocap/meshes/v4/L_figer3_4.STL +0 -0
  43. package/dist/models/mocap/meshes/v4/L_figer3_5.STL +0 -0
  44. package/dist/models/mocap/meshes/v4/L_figer4_1.STL +0 -0
  45. package/dist/models/mocap/meshes/v4/L_figer4_2.STL +0 -0
  46. package/dist/models/mocap/meshes/v4/L_figer4_3.STL +0 -0
  47. package/dist/models/mocap/meshes/v4/L_figer4_4.STL +0 -0
  48. package/dist/models/mocap/meshes/v4/L_figer4_5.STL +0 -0
  49. package/dist/models/mocap/meshes/v4/L_figer5_1.STL +0 -0
  50. package/dist/models/mocap/meshes/v4/L_figer5_2.STL +0 -0
  51. package/dist/models/mocap/meshes/v4/L_figer5_3.STL +0 -0
  52. package/dist/models/mocap/meshes/v4/L_figer5_4.STL +0 -0
  53. package/dist/models/mocap/meshes/v4/L_figer5_5.STL +0 -0
  54. package/dist/models/mocap/meshes/v4/L_handle.STL +0 -0
  55. package/dist/models/mocap/meshes/v4/L_handleseat.STL +0 -0
  56. package/dist/models/mocap/meshes/v4/L_mark.STL +0 -0
  57. package/dist/models/mocap/meshes/v4/L_markseat.STL +0 -0
  58. package/dist/models/mocap/meshes/v4/Lgamepadbase.STL +0 -0
  59. package/dist/models/mocap/meshes/v4/Link1.STL +0 -0
  60. package/dist/models/mocap/meshes/v4/Link2.STL +0 -0
  61. package/dist/models/mocap/meshes/v4/R_base_link.STL +0 -0
  62. package/dist/models/mocap/meshes/v4/R_figer1_1.STL +0 -0
  63. package/dist/models/mocap/meshes/v4/R_figer1_2.STL +0 -0
  64. package/dist/models/mocap/meshes/v4/R_figer1_3.STL +0 -0
  65. package/dist/models/mocap/meshes/v4/R_figer1_4.STL +0 -0
  66. package/dist/models/mocap/meshes/v4/R_figer1_5.STL +0 -0
  67. package/dist/models/mocap/meshes/v4/R_figer1base.STL +0 -0
  68. package/dist/models/mocap/meshes/v4/R_figer2_1.STL +0 -0
  69. package/dist/models/mocap/meshes/v4/R_figer2_2.STL +0 -0
  70. package/dist/models/mocap/meshes/v4/R_figer2_3.STL +0 -0
  71. package/dist/models/mocap/meshes/v4/R_figer2_4.STL +0 -0
  72. package/dist/models/mocap/meshes/v4/R_figer2_5.STL +0 -0
  73. package/dist/models/mocap/meshes/v4/R_figer3_1.STL +0 -0
  74. package/dist/models/mocap/meshes/v4/R_figer3_2.STL +0 -0
  75. package/dist/models/mocap/meshes/v4/R_figer3_3.STL +0 -0
  76. package/dist/models/mocap/meshes/v4/R_figer3_4.STL +0 -0
  77. package/dist/models/mocap/meshes/v4/R_figer3_5.STL +0 -0
  78. package/dist/models/mocap/meshes/v4/R_figer4_1.STL +0 -0
  79. package/dist/models/mocap/meshes/v4/R_figer4_2.STL +0 -0
  80. package/dist/models/mocap/meshes/v4/R_figer4_3.STL +0 -0
  81. package/dist/models/mocap/meshes/v4/R_figer4_4.STL +0 -0
  82. package/dist/models/mocap/meshes/v4/R_figer4_5.STL +0 -0
  83. package/dist/models/mocap/meshes/v4/R_figer5_1.STL +0 -0
  84. package/dist/models/mocap/meshes/v4/R_figer5_2.STL +0 -0
  85. package/dist/models/mocap/meshes/v4/R_figer5_3.STL +0 -0
  86. package/dist/models/mocap/meshes/v4/R_figer5_4.STL +0 -0
  87. package/dist/models/mocap/meshes/v4/R_figer5_5.STL +0 -0
  88. package/dist/models/mocap/meshes/v4/R_handle.STL +0 -0
  89. package/dist/models/mocap/meshes/v4/R_handleseat.STL +0 -0
  90. package/dist/models/mocap/meshes/v4/R_mark.STL +0 -0
  91. package/dist/models/mocap/meshes/v4/R_markseat.STL +0 -0
  92. package/dist/models/mocap/meshes/v4/Rgamepadbase.STL +0 -0
  93. package/dist/models/mocap/meshes/v4/base_link.STL +0 -0
  94. package/dist/models/mocap/meshes/v4/r_figer1_1.STL +0 -0
  95. package/dist/models/mocap/meshes/v5/L_handleseat.STL +0 -0
  96. package/dist/models/mocap/meshes/v5/R_handleseat.STL +0 -0
  97. package/dist/models/mocap/meshes/v5/link_LeftSkeletonBase.STL +0 -0
  98. package/dist/models/mocap/meshes/v5/link_LeftSkeletonIndex1.STL +0 -0
  99. package/dist/models/mocap/meshes/v5/link_LeftSkeletonIndex2.STL +0 -0
  100. package/dist/models/mocap/meshes/v5/link_LeftSkeletonIndex3.STL +0 -0
  101. package/dist/models/mocap/meshes/v5/link_LeftSkeletonIndex4.STL +0 -0
  102. package/dist/models/mocap/meshes/v5/link_LeftSkeletonMiddle1.STL +0 -0
  103. package/dist/models/mocap/meshes/v5/link_LeftSkeletonMiddle2.STL +0 -0
  104. package/dist/models/mocap/meshes/v5/link_LeftSkeletonMiddle3.STL +0 -0
  105. package/dist/models/mocap/meshes/v5/link_LeftSkeletonMiddle4.STL +0 -0
  106. package/dist/models/mocap/meshes/v5/link_LeftSkeletonPinky1.STL +0 -0
  107. package/dist/models/mocap/meshes/v5/link_LeftSkeletonPinky2.STL +0 -0
  108. package/dist/models/mocap/meshes/v5/link_LeftSkeletonPinky3.STL +0 -0
  109. package/dist/models/mocap/meshes/v5/link_LeftSkeletonPinky4.STL +0 -0
  110. package/dist/models/mocap/meshes/v5/link_LeftSkeletonRing1.STL +0 -0
  111. package/dist/models/mocap/meshes/v5/link_LeftSkeletonRing2.STL +0 -0
  112. package/dist/models/mocap/meshes/v5/link_LeftSkeletonRing3.STL +0 -0
  113. package/dist/models/mocap/meshes/v5/link_LeftSkeletonRing4.STL +0 -0
  114. package/dist/models/mocap/meshes/v5/link_LeftSkeletonThumb1.STL +0 -0
  115. package/dist/models/mocap/meshes/v5/link_LeftSkeletonThumb2.STL +0 -0
  116. package/dist/models/mocap/meshes/v5/link_LeftSkeletonThumb3.STL +0 -0
  117. package/dist/models/mocap/meshes/v5/link_LeftSkeletonThumb4.STL +0 -0
  118. package/dist/models/mocap/meshes/v5/link_LeftSkeletonThumbBase.STL +0 -0
  119. package/dist/models/mocap/meshes/v5/link_RightSkeletonBase.STL +0 -0
  120. package/dist/models/mocap/meshes/v5/link_RightSkeletonIndex1.STL +0 -0
  121. package/dist/models/mocap/meshes/v5/link_RightSkeletonIndex2.STL +0 -0
  122. package/dist/models/mocap/meshes/v5/link_RightSkeletonIndex3.STL +0 -0
  123. package/dist/models/mocap/meshes/v5/link_RightSkeletonIndex4.STL +0 -0
  124. package/dist/models/mocap/meshes/v5/link_RightSkeletonMiddle1.STL +0 -0
  125. package/dist/models/mocap/meshes/v5/link_RightSkeletonMiddle2.STL +0 -0
  126. package/dist/models/mocap/meshes/v5/link_RightSkeletonMiddle3.STL +0 -0
  127. package/dist/models/mocap/meshes/v5/link_RightSkeletonMiddle4.STL +0 -0
  128. package/dist/models/mocap/meshes/v5/link_RightSkeletonPinky1.STL +0 -0
  129. package/dist/models/mocap/meshes/v5/link_RightSkeletonPinky2.STL +0 -0
  130. package/dist/models/mocap/meshes/v5/link_RightSkeletonPinky3.STL +0 -0
  131. package/dist/models/mocap/meshes/v5/link_RightSkeletonPinky4.STL +0 -0
  132. package/dist/models/mocap/meshes/v5/link_RightSkeletonRing1.STL +0 -0
  133. package/dist/models/mocap/meshes/v5/link_RightSkeletonRing2.STL +0 -0
  134. package/dist/models/mocap/meshes/v5/link_RightSkeletonRing3.STL +0 -0
  135. package/dist/models/mocap/meshes/v5/link_RightSkeletonRing4.STL +0 -0
  136. package/dist/models/mocap/meshes/v5/link_RightSkeletonThumb1.STL +0 -0
  137. package/dist/models/mocap/meshes/v5/link_RightSkeletonThumb2.STL +0 -0
  138. package/dist/models/mocap/meshes/v5/link_RightSkeletonThumb3.STL +0 -0
  139. package/dist/models/mocap/meshes/v5/link_RightSkeletonThumb4.STL +0 -0
  140. package/dist/models/mocap/meshes/v5/link_RightSkeletonThumbBase.STL +0 -0
  141. package/dist/scripts/start-rosbridge.sh +77 -0
  142. package/dist/usd-iframe.html +41 -0
  143. package/dist/usd-viewer/bindings/.gitattributes +2 -0
  144. package/dist/usd-viewer/bindings/emHdBindings.data +19331 -0
  145. package/dist/usd-viewer/bindings/emHdBindings.js +12281 -0
  146. package/dist/usd-viewer/bindings/emHdBindings.wasm +0 -0
  147. package/dist/usd-viewer/bindings/emHdBindings.worker.js +124 -0
  148. package/dist/usd-viewer/environments/neutral.hdr +0 -0
  149. package/dist/usd-viewer/error-handler.js +57 -0
  150. package/dist/usd-viewer/hydra/HydraPrimitives.js +740 -0
  151. package/dist/usd-viewer/hydra/TextureRegistry.js +130 -0
  152. package/dist/usd-viewer/hydra/ThreeJsRenderDelegate.js +65 -0
  153. package/dist/usd-viewer/index.d.ts +1 -0
  154. package/dist/usd-viewer/main.js +1018 -0
  155. package/dist/usd-viewer/usd.html +36 -0
  156. package/dist/usd-viewer/usdUtils.js +67 -0
  157. package/package.json +70 -4
  158. package/server/package.json +14 -0
  159. package/server/static-server.js +88 -0
@@ -0,0 +1,36 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>Mechaverse-USD</title>
5
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
6
+ <meta name="application-name" content="mechaverse-usd" />
7
+ <meta name="description" content="Mechaverse USD Viewer" />
8
+ <meta name="keywords" content="USD, Hydra, Three.js" />
9
+ <meta name="author" content="jurmy24" />
10
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, viewport-fit=cover, user-scalable=no, shrink-to-fit=0" />
11
+ <link rel="icon" type="image/x-icon" href="/favicon.png" />
12
+ </head>
13
+
14
+ <body style="margin:0px; padding:0px; width:100%; height:100vh; overflow:hidden;
15
+ touch-action: none;
16
+ -webkit-touch-callout: none;
17
+ -webkit-user-select: none;
18
+ -khtml-user-select: none;
19
+ -moz-user-select: none;
20
+ -ms-user-select: none;
21
+ user-select: none;
22
+ background: transparent;">
23
+
24
+ <!-- Import maps for three.js - use local version to avoid COEP issues -->
25
+ <script type="importmap">
26
+ {"imports": {
27
+ "three": "/node_modules/three/build/three.module.js",
28
+ "three/addons/": "/node_modules/three/examples/jsm/"
29
+ }}
30
+ </script>
31
+ <!-- Error handler - must be loaded before main.js -->
32
+ <script src="error-handler.js"></script>
33
+ <script type="module" src="main.js"></script>
34
+ </body>
35
+ </html>
36
+
@@ -0,0 +1,67 @@
1
+ // Common utilities for the USD viewer
2
+
3
+ export const USD_EXTENSIONS = ["usd", "usdc", "usda", "usdz"];
4
+
5
+ export const ASSET_EXTENSIONS = [
6
+ "png",
7
+ "jpg",
8
+ "jpeg",
9
+ "exr",
10
+ "hdr",
11
+ "tiff",
12
+ "tga",
13
+ "bmp",
14
+ "webp",
15
+ "obj",
16
+ "fbx",
17
+ "dae",
18
+ "stl",
19
+ "ply",
20
+ "gltf",
21
+ "glb",
22
+ "wav",
23
+ "mp3",
24
+ "ogg",
25
+ "json",
26
+ "xml",
27
+ ];
28
+
29
+ export function shouldInclude(name) {
30
+ const ext = name.split(".").pop()?.toLowerCase();
31
+ return USD_EXTENSIONS.includes(ext) || ASSET_EXTENSIONS.includes(ext);
32
+ }
33
+
34
+ export function isUsdFile(filename) {
35
+ return USD_EXTENSIONS.includes(filename.split(".").pop()?.toLowerCase());
36
+ }
37
+
38
+ export async function getFileFromHandle(fileOrHandle) {
39
+ return fileOrHandle.getFile ? await fileOrHandle.getFile() : fileOrHandle;
40
+ }
41
+
42
+ export function parseFilePath(fullPath, defaultName) {
43
+ if (!fullPath) return { fileName: defaultName, directory: "/" };
44
+ const fileName = fullPath.split("/").pop();
45
+ const directory = fullPath.substring(0, fullPath.length - fileName.length);
46
+ return { fileName, directory };
47
+ }
48
+
49
+ export function safeCall(obj, method, ...args) {
50
+ try {
51
+ if (obj && typeof obj[method] === "function") {
52
+ return obj[method](...args);
53
+ }
54
+ } catch {}
55
+ return null;
56
+ }
57
+
58
+ export async function getEntryFile(fileEntry) {
59
+ try {
60
+ return await new Promise((resolve, reject) =>
61
+ fileEntry.file(resolve, reject)
62
+ );
63
+ } catch (err) {
64
+ console.warn("Error getting file:", err);
65
+ return null;
66
+ }
67
+ }
package/package.json CHANGED
@@ -1,6 +1,72 @@
1
1
  {
2
2
  "name": "@ioai/robot_viewer",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.1",
4
+ "description": "Lightweight browser-based robot model viewer supporting URDF, MJCF, and USD formats",
5
+ "main": "bin/cli.js",
6
+ "bin": {
7
+ "robot-viewer": "./bin/cli.js",
8
+ "io-robot-viewer": "./bin/cli.js"
9
+ },
10
+ "files": [
11
+ "bin/cli.js",
12
+ "dist/",
13
+ "server/static-server.js",
14
+ "server/package.json"
15
+ ],
16
+ "packageManager": "pnpm@9.0.0",
17
+ "scripts": {
18
+ "start": "node bin/cli.js",
19
+ "dev": "vite",
20
+ "build": "vite build",
21
+ "build:obfuscated": "NODE_ENV=production vite build --config vite.config.obfuscated.js && node scripts/obfuscate-workers.cjs",
22
+ "preview": "vite preview",
23
+ "prepublishOnly": "npm run build:obfuscated",
24
+ "pack:check": "npm pack --dry-run",
25
+ "publish:safe": "npm run build:obfuscated && npm run pack:check && npm publish --access public"
26
+ },
27
+ "engines": {
28
+ "node": ">=18.0.0"
29
+ },
30
+ "keywords": [
31
+ "robot",
32
+ "urdf",
33
+ "xacro",
34
+ "mjcf",
35
+ "usd",
36
+ "three.js",
37
+ "viewer"
38
+ ],
39
+ "author": "IOAI",
40
+ "license": "Apache-2.0",
41
+ "private": false,
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "dependencies": {
46
+ "@babel/runtime": "^7.23.0",
47
+ "@codemirror/autocomplete": "^6.12.0",
48
+ "@codemirror/commands": "^6.3.3",
49
+ "@codemirror/lang-xml": "^6.0.2",
50
+ "@codemirror/search": "^6.5.5",
51
+ "@codemirror/state": "^6.4.0",
52
+ "@codemirror/view": "^6.23.0",
53
+ "@uiw/codemirror-theme-dracula": "^4.21.21",
54
+ "@uiw/codemirror-theme-github": "^4.21.21",
55
+ "@uiw/codemirror-theme-vscode": "^4.21.21",
56
+ "codemirror": "^6.0.1",
57
+ "d3": "^7.9.0",
58
+ "express": "^4.18.2",
59
+ "fflate": "^0.8.2",
60
+ "mujoco-js": "^0.0.7",
61
+ "roslib": "^1.3.0",
62
+ "three": "^0.163.0",
63
+ "uplot": "^1.6.32",
64
+ "urdf-loader": "^0.12.5",
65
+ "xacro-parser": "^0.3.11"
66
+ },
67
+ "devDependencies": {
68
+ "javascript-obfuscator": "^5.6.0",
69
+ "terser": "^5.26.0",
70
+ "vite": "^4.5.0"
71
+ }
72
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "io-robot-viewer-server",
3
+ "version": "2.0.0",
4
+ "description": "Static file server for io_robot_viewer production builds (dist/), with COEP/COOP headers.",
5
+ "main": "static-server.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "start": "node static-server.js",
9
+ "dev": "node --watch static-server.js"
10
+ },
11
+ "dependencies": {
12
+ "express": "^4.18.2"
13
+ }
14
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * io_robot_viewer 生产静态服务器
3
+ *
4
+ * 托管 `vite build` 的产物(dist/),并补齐 vite.config.js 中只配在
5
+ * `configureServer`(dev 专用)里的跨域隔离响应头 —— 少了它们,
6
+ * USD viewer 的 SharedArrayBuffer 和 MuJoCo WASM 会失效。
7
+ *
8
+ * 相对开发服务器(vite dev)的关键差别:产物里没有 Vite HMR 客户端,
9
+ * 因此不存在「后台 WebSocket 断开 → 轮询到服务器可达 → location.reload()」
10
+ * 这条链路。标签页在后台放久了不会被强制刷新,模型也就不会莫名消失。
11
+ *
12
+ * 环境变量:
13
+ * PORT 监听端口(默认 3000)
14
+ * HOST 监听地址(默认 0.0.0.0)
15
+ * DIST_DIR 静态产物目录(默认 ../dist)
16
+ */
17
+
18
+ import express from 'express';
19
+ import path from 'path';
20
+ import fs from 'fs';
21
+ import { fileURLToPath } from 'url';
22
+
23
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
+
25
+ const PORT = Number(process.env.PORT || 3000);
26
+ const HOST = process.env.HOST || '0.0.0.0';
27
+ const DIST_DIR = path.resolve(process.env.DIST_DIR || path.join(__dirname, '..', 'dist'));
28
+
29
+ if (!fs.existsSync(path.join(DIST_DIR, 'index.html'))) {
30
+ console.error(`✗ 找不到构建产物: ${DIST_DIR}/index.html`);
31
+ console.error(' 请先执行 pnpm run build,或用 deploy_on_server.sh 一键部署。');
32
+ process.exit(1);
33
+ }
34
+
35
+ const app = express();
36
+
37
+ // ── 跨域隔离响应头 ──────────────────────────────────────────────────────────
38
+ // 与 vite.config.js 的 configureServer 中间件保持一致:
39
+ // USD iframe 需要 require-corp 才能拿到 SharedArrayBuffer;
40
+ // 其余页面用 credentialless,避免第三方资源被 CORP 拦掉。
41
+ app.use((req, res, next) => {
42
+ const url = req.url || '';
43
+ if (url.includes('usd-iframe.html') ||
44
+ url.includes('usd-viewer/') ||
45
+ url.includes('usd.html')) {
46
+ res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
47
+ res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
48
+ res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
49
+ return next();
50
+ }
51
+ res.setHeader('Cross-Origin-Embedder-Policy', 'credentialless');
52
+ res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
53
+ next();
54
+ });
55
+
56
+ // ── 静态资源 ────────────────────────────────────────────────────────────────
57
+ // 带 hash 的产物可以长缓存;index.html 与 public/ 下的散文件不带 hash,
58
+ // 必须每次校验,否则前端更新后用户拿到的还是旧壳子。
59
+ app.use(express.static(DIST_DIR, {
60
+ setHeaders: (res, filePath) => {
61
+ if (/[.-][0-9a-f]{8,}\.[a-z0-9]+$/i.test(path.basename(filePath))) {
62
+ res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
63
+ } else {
64
+ res.setHeader('Cache-Control', 'no-cache');
65
+ }
66
+ }
67
+ }));
68
+
69
+ // 单页应用兜底:非文件请求都回 index.html,
70
+ // 保证 ?plot=1 / ?mocap=... 这类带查询参数的入口直接访问也能打开。
71
+ app.get('*', (req, res, next) => {
72
+ if (path.extname(req.path)) return next();
73
+ res.setHeader('Cache-Control', 'no-cache');
74
+ res.sendFile(path.join(DIST_DIR, 'index.html'));
75
+ });
76
+
77
+ const server = app.listen(PORT, HOST, () => {
78
+ console.log(`io_robot_viewer 生产服务运行在 http://${HOST}:${PORT}`);
79
+ console.log(` 静态目录: ${DIST_DIR}`);
80
+ });
81
+
82
+ // 容器/systemd 里用 SIGTERM 停服,需要优雅关闭,否则连接会被硬断
83
+ for (const sig of ['SIGTERM', 'SIGINT']) {
84
+ process.on(sig, () => {
85
+ console.log(`\n收到 ${sig},正在关闭…`);
86
+ server.close(() => process.exit(0));
87
+ });
88
+ }