This commit is contained in:
19
docs/SGS_MAP_SDK_IFRAME_NOT_ENABLED.md
Normal file
19
docs/SGS_MAP_SDK_IFRAME_NOT_ENABLED.md
Normal file
@@ -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 代码。
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import type {
|
|
||||||
SgsMapSDKConstructor
|
|
||||||
} from '@/types/sgs-map-sdk'
|
|
||||||
|
|
||||||
let loadingScriptUrl = ''
|
|
||||||
let loadRequest: Promise<SgsMapSDKConstructor> | 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<SgsMapSDKConstructor> => {
|
|
||||||
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<SgsMapSDKConstructor>((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
|
|
||||||
}
|
|
||||||
@@ -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<K extends keyof SgsMapEvents> = (payload: SgsMapEvents[K]) => void
|
|
||||||
|
|
||||||
export class SgsMapService {
|
|
||||||
private sdk: SgsMapSDKInstance | null = null
|
|
||||||
private readyPromise: Promise<SgsMapEvents['ready']> | 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<SgsMapEvents['ready']>((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<K extends keyof SgsMapEvents>(event: K, listener: SgsMapListener<K>) {
|
|
||||||
this.externalListeners.push({
|
|
||||||
event,
|
|
||||||
listener: listener as (payload: SgsMapEvents[keyof SgsMapEvents]) => void
|
|
||||||
})
|
|
||||||
|
|
||||||
if (this.sdk) {
|
|
||||||
this.registerSdkListener(event, listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerSdkListener<K extends keyof SgsMapEvents>(event: K, listener: SgsMapListener<K>) {
|
|
||||||
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<SgsRouteResult> {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
# SGS Map SDK Changelog
|
# 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
|
## [2.4.1] - 2026-07-01
|
||||||
|
|
||||||
### Phase 2: 高层导航API (2026-07-01)
|
### Phase 2: 高层导航API (2026-07-01)
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { createHash } from 'node:crypto'
|
||||||
import { existsSync, readFileSync } from 'node:fs'
|
import { existsSync, readFileSync } from 'node:fs'
|
||||||
import { resolve } from 'node:path'
|
import { resolve } from 'node:path'
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
const sdkDirectory = resolve(process.cwd(), 'static/sgs-map-sdk')
|
const sdkDirectory = resolve(process.cwd(), 'static/sgs-map-sdk')
|
||||||
const readSdkFile = (name: string) => readFileSync(resolve(sdkDirectory, name), 'utf8')
|
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', () => {
|
describe('SGS Map SDK 2.5.0 release contract', () => {
|
||||||
it('keeps the local package, bridge self-report, and default Engine path aligned', () => {
|
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)
|
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'
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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<keyof SgsMapEvents, (payload: never) => 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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user