diff --git a/docs/SGS_MAP_SDK_IFRAME_NOT_ENABLED.md b/docs/SGS_MAP_SDK_IFRAME_NOT_ENABLED.md new file mode 100644 index 0000000..2748ab9 --- /dev/null +++ b/docs/SGS_MAP_SDK_IFRAME_NOT_ENABLED.md @@ -0,0 +1,19 @@ +# SGS Map SDK 2.5.0 iframe 状态说明 + +## 当前应用边界 + +`frontend-miniapp` 不加载 `SGSMapSDK`,不创建 iframe,也不发送 SDK Bridge `postMessage` 命令。H5 馆内渲染继续使用 `ThreeMap`。 + +`VITE_GUIDE_DATA_SOURCE_MODE=sdk` 仅表示导览楼层和 POI 通过 SGS REST Repository 获取;它不是 SDK Bridge 或渲染器开关。 + +## 2.5.0 发布记录 + +本地静态包版本为 2.5.0。其公开 TypeScript 方法面相较 2.4.1 没有变化。业务 POI、精品路线/流线、POI 间导航、诊断和漫游均是更早已有能力,不会因升级到 2.5.0 而自动被本应用采用。 + +2.5.0 的运行层变化是渲染架构:旧的 `/h5-sdk` 路由被独立部署的 `/engine/index.html` 运行时取代。未来若接入 iframe,必须使用匹配的 2.5.x Bridge 与 Engine,采用 HTTPS 安全部署、显式 `targetOrigin`,并完成 `HELLO -> ENGINE_READY` 握手。 + +## 保持禁用的原因 + +当前同源 `/engine/index.html` 返回的是博物馆应用 SPA 回退页面,而不是 `SGS Map SDK Engine` 文档。仅有 HTTP 200 不能代表健康。已知的 HTTP Engine 参考地址不能嵌入 HTTPS 生产 H5,否则会产生 mixed content。 + +在未来实施前,运维必须在 HTTPS 源站部署或反向代理完整的 Engine 资源,并验证文档标识、资源与握手。本项目届时需要新增专用 renderer 边界和生命周期/浏览器测试;当前不保留闲置的 iframe 代码。 diff --git a/src/services/sgs/SgsMapScriptLoader.ts b/src/services/sgs/SgsMapScriptLoader.ts deleted file mode 100644 index bada769..0000000 --- a/src/services/sgs/SgsMapScriptLoader.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { - SgsMapSDKConstructor -} from '@/types/sgs-map-sdk' - -let loadingScriptUrl = '' -let loadRequest: Promise | null = null - -const findExistingScript = (scriptUrl: string) => ( - Array.from(document.scripts).find((script) => script.src.endsWith(scriptUrl) || script.getAttribute('src') === scriptUrl) -) - -export const loadSgsMapScript = (scriptUrl: string): Promise => { - if (typeof window === 'undefined' || typeof document === 'undefined') { - return Promise.reject(new Error('SGS Map SDK 只能在 H5 浏览器环境中加载')) - } - - if (window.SGSMapSDK) { - return Promise.resolve(window.SGSMapSDK) - } - - if (loadRequest && loadingScriptUrl === scriptUrl) { - return loadRequest - } - - loadingScriptUrl = scriptUrl - const request = new Promise((resolve, reject) => { - const existingScript = findExistingScript(scriptUrl) - const script = existingScript || document.createElement('script') - - const resolveLoadedSdk = () => { - if (!window.SGSMapSDK) { - reject(new Error('SGS Map SDK 脚本已加载,但未发现 window.SGSMapSDK')) - return - } - - resolve(window.SGSMapSDK) - } - - script.addEventListener('load', resolveLoadedSdk, { once: true }) - script.addEventListener('error', () => { - reject(new Error(`SGS Map SDK 脚本加载失败: ${scriptUrl}`)) - }, { once: true }) - - if (!existingScript) { - script.src = scriptUrl - script.async = true - document.head.appendChild(script) - return - } - - if (window.SGSMapSDK) { - resolveLoadedSdk() - } - }).finally(() => { - loadRequest = null - }) - - loadRequest = request - return request -} diff --git a/src/services/sgs/SgsMapService.ts b/src/services/sgs/SgsMapService.ts deleted file mode 100644 index 0a61dbd..0000000 --- a/src/services/sgs/SgsMapService.ts +++ /dev/null @@ -1,165 +0,0 @@ -import type { - SgsMapEvents, - SgsMapSDKInstance, - SgsMarkerConfig, - SgsMapNode, - SgsRouteOptions, - SgsRouteResult -} from '@/types/sgs-map-sdk' -import { - loadSgsMapScript -} from './SgsMapScriptLoader' - -interface SgsMapServiceOptions { - container: HTMLElement - scriptUrl: string - sdkUrl: string - targetOrigin: string - floorId: string | number - timeout: number -} - -type SgsMapListener = (payload: SgsMapEvents[K]) => void - -export class SgsMapService { - private sdk: SgsMapSDKInstance | null = null - private readyPromise: Promise | null = null - private resolveReady: ((payload: SgsMapEvents['ready']) => void) | null = null - private rejectReady: ((error: Error) => void) | null = null - private listeners: Array<() => void> = [] - private externalListeners: Array<{ - event: keyof SgsMapEvents - listener: (payload: SgsMapEvents[keyof SgsMapEvents]) => void - }> = [] - private disposed = false - - constructor(private readonly options: SgsMapServiceOptions) {} - - async init() { - if (this.readyPromise) return this.readyPromise - - this.readyPromise = new Promise((resolve, reject) => { - this.resolveReady = resolve - this.rejectReady = reject - }) - - loadSgsMapScript(this.options.scriptUrl) - .then((SDKConstructor) => { - if (this.disposed) { - this.rejectReady?.(new Error('SGS Map SDK 初始化已取消')) - return - } - - this.sdk = new SDKConstructor({ - container: this.options.container, - sdkUrl: this.options.sdkUrl, - targetOrigin: this.options.targetOrigin, - floorId: this.options.floorId, - timeout: this.options.timeout - }) - - this.externalListeners.forEach(({ event, listener }) => { - this.registerSdkListener(event, listener) - }) - - this.registerSdkListener('ready', (payload) => { - this.resolveReady?.(payload) - }) - - this.registerSdkListener('loadError', (payload) => { - this.rejectReady?.(new Error(payload.detail || 'SGS Map SDK 地图基座加载失败')) - }) - - this.registerSdkListener('error', (payload) => { - if (!this.sdk?.getIsReady()) { - this.rejectReady?.(new Error(payload.message || payload.code || 'SGS Map SDK 初始化失败')) - } - }) - }) - .catch((error) => { - this.rejectReady?.(error instanceof Error ? error : new Error('SGS Map SDK 初始化失败')) - }) - - return this.readyPromise - } - - on(event: K, listener: SgsMapListener) { - this.externalListeners.push({ - event, - listener: listener as (payload: SgsMapEvents[keyof SgsMapEvents]) => void - }) - - if (this.sdk) { - this.registerSdkListener(event, listener) - } - } - - private registerSdkListener(event: K, listener: SgsMapListener) { - if (!this.sdk) return - - this.sdk.on(event, listener) - this.listeners.push(() => { - this.sdk?.off(event, listener) - }) - } - - private async getReadySdk() { - if (!this.sdk) { - await this.init() - } - - await this.readyPromise - - if (!this.sdk || this.disposed) { - throw new Error('SGS Map SDK 已销毁') - } - - return this.sdk - } - - async changeFloor(floorId: string | number, options?: { forceReload?: boolean }) { - const sdk = await this.getReadySdk() - return sdk.changeFloor(floorId, this.options.timeout, options) - } - - async focusTo(node: SgsMapNode | string) { - const sdk = await this.getReadySdk() - sdk.focusTo(node) - } - - async addMarker(config: SgsMarkerConfig) { - const sdk = await this.getReadySdk() - sdk.addMarker(config) - } - - async clearMarkers() { - const sdk = await this.getReadySdk() - sdk.clearMarkers() - } - - async resetView() { - const sdk = await this.getReadySdk() - return sdk.resetView(this.options.timeout) - } - - async setViewMode(mode: '2d' | '3d') { - const sdk = await this.getReadySdk() - return sdk.setViewMode(mode, this.options.timeout) - } - - async planRoute(from: SgsMapNode | string, to: SgsMapNode | string, options?: SgsRouteOptions): Promise { - const sdk = await this.getReadySdk() - return sdk.planRoute(from, to, options, this.options.timeout) - } - - destroy() { - this.disposed = true - this.listeners.splice(0).forEach((cleanup) => cleanup()) - this.sdk?.destroy() - this.sdk = null - this.rejectReady?.(new Error('SGS Map SDK 已销毁')) - this.resolveReady = null - this.rejectReady = null - this.readyPromise = null - } -} diff --git a/static/sgs-map-sdk/CHANGELOG.md b/static/sgs-map-sdk/CHANGELOG.md index 51db54f..e8896ae 100644 --- a/static/sgs-map-sdk/CHANGELOG.md +++ b/static/sgs-map-sdk/CHANGELOG.md @@ -1,5 +1,9 @@ # SGS Map SDK Changelog +## 本项目接入说明:2.5.0 - 2026-07-16 + +本应用仅消费 SGS REST 数据,H5 渲染器保持使用本地 ThreeMap。SDK Bridge/iframe/Engine 运行时明确未启用,运行边界见 `docs/SGS_MAP_SDK_IFRAME_NOT_ENABLED.md`。本节不是上游 SGS changelog 条目。 + ## [2.4.1] - 2026-07-01 ### Phase 2: 高层导航API (2026-07-01) diff --git a/tests/unit/SgsMapSdkReleaseContract.spec.ts b/tests/unit/SgsMapSdkReleaseContract.spec.ts index 41a89e1..f279dbb 100644 --- a/tests/unit/SgsMapSdkReleaseContract.spec.ts +++ b/tests/unit/SgsMapSdkReleaseContract.spec.ts @@ -1,9 +1,11 @@ +import { createHash } from 'node:crypto' import { existsSync, readFileSync } from 'node:fs' import { resolve } from 'node:path' import { describe, expect, it } from 'vitest' const sdkDirectory = resolve(process.cwd(), 'static/sgs-map-sdk') const readSdkFile = (name: string) => readFileSync(resolve(sdkDirectory, name), 'utf8') +const sha256 = (name: string) => createHash('sha256').update(readFileSync(resolve(sdkDirectory, name))).digest('hex').toUpperCase() describe('SGS Map SDK 2.5.0 release contract', () => { it('keeps the local package, bridge self-report, and default Engine path aligned', () => { @@ -30,4 +32,22 @@ describe('SGS Map SDK 2.5.0 release contract', () => { expect(existsSync(resolve(sdkDirectory, file))).toBe(true) }) }) + + it('pins the complete 2.5.0 artifact set to prevent a partial stale package', () => { + expect({ + global: sha256('index.global.js'), + globalMap: sha256('index.global.js.map'), + module: sha256('index.mjs'), + moduleMap: sha256('index.mjs.map'), + types: sha256('index.d.ts'), + moduleTypes: sha256('index.d.mts') + }).toEqual({ + global: '3C93DEE217615FD9454A4A8EE4282114471B36DA2B3C7A5F2B2C0FDB18BEF5D1', + globalMap: 'EB0C1470AED546194B48386DD092E6D5813CA41E4C3B561838CFC5AE290677CF', + module: 'D3C885464B887276BCB420A45BBB5AF2BECA0779482571013789EF61A0E29D59', + moduleMap: 'A074A886F5EFE400ACD8F7A5591B7043BA2FF10500B429AFC6490BB7EEAA1430', + types: '2E5875CABC6464F2A60625F7400B34B048BD706411FDEA271DBD9D3D4A9BDDD4', + moduleTypes: '2E5875CABC6464F2A60625F7400B34B048BD706411FDEA271DBD9D3D4A9BDDD4' + }) + }) }) diff --git a/tests/unit/SgsMapService.spec.ts b/tests/unit/SgsMapService.spec.ts deleted file mode 100644 index d9fa3f4..0000000 --- a/tests/unit/SgsMapService.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { - SgsMapEvents, - SgsMapSDKConstructor, - SgsMapSDKInstance -} from '@/types/sgs-map-sdk' - -const loadSgsMapScript = vi.hoisted(() => vi.fn()) - -vi.mock('@/services/sgs/SgsMapScriptLoader', () => ({ - loadSgsMapScript -})) - -import { SgsMapService } from '@/services/sgs/SgsMapService' - -const createHarness = async () => { - const listeners = new Map void>() - const sdk = { - on: vi.fn((event: keyof SgsMapEvents, listener: (payload: never) => void) => { - listeners.set(event, listener) - }), - off: vi.fn(), - getIsReady: vi.fn(() => true), - changeFloor: vi.fn().mockResolvedValue({ success: true, floorId: 'L2' }), - resetView: vi.fn().mockResolvedValue({ success: true }), - setViewMode: vi.fn().mockResolvedValue({ success: true, mode: '2d' }), - destroy: vi.fn() - } as unknown as SgsMapSDKInstance - const SDKConstructor = vi.fn(() => sdk) as unknown as SgsMapSDKConstructor - - loadSgsMapScript.mockResolvedValue(SDKConstructor) - - const service = new SgsMapService({ - container: {} as HTMLElement, - scriptUrl: '/static/sgs-map-sdk/index.global.js?v=2.5.0', - sdkUrl: '/engine/index.html', - targetOrigin: 'https://map.example.com', - floorId: 'L1', - timeout: 7000 - }) - const ready = service.init() - - await vi.waitFor(() => expect(SDKConstructor).toHaveBeenCalledOnce()) - listeners.get('ready')?.({ - engineVersion: '2.5.0', - protocolVersion: 2 - } as never) - await ready - - return { sdk, service, SDKConstructor } -} - -beforeEach(() => { - loadSgsMapScript.mockReset() -}) - -describe('SGS Map SDK 2.5.0 service adapter', () => { - it('向 SDK 构造器传入完整的 Engine 桥接配置', async () => { - const { SDKConstructor } = await createHarness() - - expect(SDKConstructor).toHaveBeenCalledWith({ - container: expect.any(Object), - sdkUrl: '/engine/index.html', - targetOrigin: 'https://map.example.com', - floorId: 'L1', - timeout: 7000 - }) - }) - - it('透传楼层强制刷新选项和统一命令超时', async () => { - const { sdk, service } = await createHarness() - - await expect(service.changeFloor('L2', { forceReload: true })).resolves.toEqual({ - success: true, - floorId: 'L2' - }) - expect(sdk.changeFloor).toHaveBeenCalledWith('L2', 7000, { forceReload: true }) - }) - - it('返回异步视图命令结果并向上传递 SDK 错误', async () => { - const { sdk, service } = await createHarness() - - await expect(service.setViewMode('2d')).resolves.toEqual({ success: true, mode: '2d' }) - expect(sdk.setViewMode).toHaveBeenCalledWith('2d', 7000) - - vi.mocked(sdk.resetView).mockRejectedValueOnce(new Error('RESET_VIEW failed')) - await expect(service.resetView()).rejects.toThrow('RESET_VIEW failed') - expect(sdk.resetView).toHaveBeenCalledWith(7000) - }) -})