dsh-plugin-beyond-simulator 2.0.6 → 2.0.8

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.
@@ -42,6 +42,10 @@
42
42
  'use strict'
43
43
  const API = '/qxqy-simulator/api/play'
44
44
  const sessionId = decodeURIComponent(location.hash.slice(1))
45
+ const embedded = window.parent !== window && new URLSearchParams(location.search).get('embedded') === '1'
46
+ let closing = false
47
+ let stopped = false
48
+ let embeddedApi = null
45
49
  const $ = (id) => document.getElementById(id)
46
50
  const stage = $('stage')
47
51
  const cv = $('cv')
@@ -64,11 +68,16 @@
64
68
  if (!showLog) requestAnimationFrame(() => fitStage(true))
65
69
  }
66
70
 
67
- async function api(action, args = {}) {
71
+ function api(action, args = {}) {
72
+ if (closing && action !== 'stop') return Promise.reject(new Error('试玩正在关闭'))
73
+ return embeddedApi ? embeddedApi(action, args) : request(action, args)
74
+ }
75
+ async function request(action, args) {
68
76
  const response = await fetch(API, {
69
77
  method: 'POST',
70
78
  headers: { 'content-type': 'application/json' },
71
79
  body: JSON.stringify({ sessionId, action, args }),
80
+ signal: AbortSignal.timeout(15_000),
72
81
  })
73
82
  const envelope = await response.json().catch(() => null)
74
83
  if (!response.ok || !envelope || envelope.ok !== true) throw new Error(envelope?.error || '试玩请求失败')
@@ -225,6 +234,7 @@
225
234
  }
226
235
 
227
236
  function showError(error) {
237
+ if (closing) return
228
238
  $('loading').classList.add('hidden')
229
239
  $('fatal').classList.remove('hidden')
230
240
  $('fatal').textContent = error?.message || String(error)
@@ -238,6 +248,7 @@
238
248
  onSnapshot: renderChrome,
239
249
  onStatus: applyStatus,
240
250
  onError: showError,
251
+ isActive: () => !closing,
241
252
  })
242
253
  play.bindInput(stage, { keysOn: window })
243
254
 
@@ -300,7 +311,40 @@
300
311
  revealStage()
301
312
  } catch (error) { showError(error) }
302
313
  })
303
- $('close').addEventListener('click', async () => { if (play.running) await play.stop().catch(() => {}); window.close() })
314
+ function quiesce() {
315
+ closing = true
316
+ document.querySelector('.app').inert = true
317
+ play.reset()
318
+ }
319
+ function focusStage() {
320
+ stage.tabIndex = -1
321
+ stage.focus({ preventScroll: true })
322
+ }
323
+ stage.addEventListener('pointerdown', focusStage)
324
+ function requestEmbeddedClose() {
325
+ // Custom desktop schemes can serialize their origin as "null".
326
+ // The parent checks the exact iframe WindowProxy and session id.
327
+ parent.postMessage({ type: 'qxqy-play-close', sessionId }, location.origin === 'null' ? '*' : location.origin)
328
+ }
329
+ addEventListener('keydown', event => {
330
+ if (embedded && event.key === 'Escape') { event.preventDefault(); requestEmbeddedClose() }
331
+ })
332
+ $('close').addEventListener('click', async () => {
333
+ if (embedded) {
334
+ requestEmbeddedClose()
335
+ return
336
+ }
337
+ try {
338
+ quiesce()
339
+ await api('stop')
340
+ stopped = true
341
+ window.close()
342
+ } catch (error) {
343
+ closing = false
344
+ document.querySelector('.app').inert = false
345
+ showError(error)
346
+ }
347
+ })
304
348
  $('theme').addEventListener('click', () => { theme = theme === 'dark' ? 'light' : 'dark'; applyTheme() })
305
349
  let lastCase = null
306
350
  function parseAsserts() {
@@ -327,12 +371,37 @@
327
371
  for (const button of document.querySelectorAll('[data-view]')) button.addEventListener('click', () => setView(button.dataset.view))
328
372
  addEventListener('resize', () => fitStage(true))
329
373
  addEventListener('pagehide', () => {
374
+ closing = true
330
375
  play.destroy()
331
- fetch(API, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ sessionId, action: 'stop', args: {} }), keepalive: true }).catch(() => {})
376
+ // Embedded close/unmount is owned by the parent. A late keepalive stop
377
+ // from the old frame must never terminate the next play session.
378
+ if (!embedded && !stopped) fetch(API, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ sessionId, action: 'stop', args: {} }), keepalive: true }).catch(() => {})
332
379
  })
380
+ if (embedded) {
381
+ // Closing the whole application document has no awaitable teardown.
382
+ // This fallback is separate from normal modal close, which awaits stop.
383
+ const parentPagehide = () => {
384
+ quiesce()
385
+ fetch(API, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ sessionId, action: 'stop', args: {} }), keepalive: true }).catch(() => {})
386
+ }
387
+ parent.addEventListener('pagehide', parentPagehide)
388
+ addEventListener('pagehide', () => parent.removeEventListener('pagehide', parentPagehide))
389
+ }
333
390
 
334
391
  if (!sessionId) showError(new Error('缺少会话 ID'))
335
- else play.start().then(() => setTimeout(revealStage, 0)).catch(showError)
392
+ else if (embedded) {
393
+ // Wait for the parent's load handler: closing a still-loading iframe
394
+ // must not race an automatic start from this document.
395
+ window.qxqyEmbeddedPlay = {
396
+ start: request => {
397
+ if (closing) return Promise.resolve()
398
+ embeddedApi = request
399
+ return play.start().then(() => { if (closing) play.reset(); else { revealStage(); focusStage() } })
400
+ },
401
+ quiesce,
402
+ focus: focusStage,
403
+ }
404
+ } else play.start().then(() => setTimeout(revealStage, 0)).catch(showError)
336
405
  })()
337
406
  </script>
338
407
  </body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-beyond-simulator",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
4
4
  "description": "千星奇域客户端 UI 编辑与试玩模拟器 for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.15.0",
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "peerDependencies": {
52
52
  "@deepseek-ai/cordis": "^4.0.1",
53
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6"
53
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.2.0-rc.1"
54
54
  },
55
55
  "license": "GPL-3.0-only"
56
56
  }