dsh-plugin-vrmpet3d 0.1.2 → 0.1.4

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.
@@ -1081,7 +1081,7 @@
1081
1081
  * it is the same predicate as `loadModelInfo`'s, not "is the picker here".
1082
1082
  */
1083
1083
  if (!needsModelVocabulary()) return Promise.resolve(null);
1084
- return fetch('/vrmpet3d/catalog', { credentials: 'omit' })
1084
+ return fetch('/vrmpet3d/catalog', { credentials: 'same-origin' })
1085
1085
  .then(function (response) { return response.ok ? response.json() : null; })
1086
1086
  .then(function (body) {
1087
1087
  catalog = body && Array.isArray(body.models) ? body.models : [];
@@ -1183,7 +1183,7 @@
1183
1183
  }
1184
1184
 
1185
1185
  function loadImported() {
1186
- return fetch('/vrmpet3d/models', { credentials: 'omit' })
1186
+ return fetch('/vrmpet3d/models', { credentials: 'same-origin' })
1187
1187
  .then(function (response) { return response.ok ? response.json() : null; })
1188
1188
  .then(function (body) {
1189
1189
  imported.models = body && Array.isArray(body.importedModels) ? body.importedModels : [];
@@ -1209,7 +1209,7 @@
1209
1209
  function writeImported(body) {
1210
1210
  return fetch('/vrmpet3d/models', {
1211
1211
  method: 'POST',
1212
- credentials: 'omit',
1212
+ credentials: 'same-origin',
1213
1213
  headers: { 'content-type': 'application/json' },
1214
1214
  body: JSON.stringify(body),
1215
1215
  }).then(function (response) {
@@ -1341,7 +1341,7 @@
1341
1341
  }
1342
1342
 
1343
1343
  function loadImportedMotions() {
1344
- return fetch('/vrmpet3d/motions', { credentials: 'omit' })
1344
+ return fetch('/vrmpet3d/motions', { credentials: 'same-origin' })
1345
1345
  .then(function (response) { return response.ok ? response.json() : null; })
1346
1346
  .then(function (body) {
1347
1347
  importedMotions.imports = body && Array.isArray(body.importedMotions) ? body.importedMotions : [];
@@ -1363,7 +1363,7 @@
1363
1363
  function writeImportedMotion(body) {
1364
1364
  return fetch('/vrmpet3d/motions', {
1365
1365
  method: 'POST',
1366
- credentials: 'omit',
1366
+ credentials: 'same-origin',
1367
1367
  headers: { 'content-type': 'application/json' },
1368
1368
  body: JSON.stringify(body),
1369
1369
  }).then(function (response) {
@@ -1503,7 +1503,7 @@
1503
1503
  var settle = function () {
1504
1504
  if (modelInfoInFlight && modelInfoInFlight.id === id) modelInfoInFlight = null;
1505
1505
  };
1506
- var promise = fetch('/vrmpet3d/model-info?m=' + encodeURIComponent(id), { credentials: 'omit' })
1506
+ var promise = fetch('/vrmpet3d/model-info?m=' + encodeURIComponent(id), { credentials: 'same-origin' })
1507
1507
  .then(function (response) {
1508
1508
  if (response.status === 404) return null;
1509
1509
  return response.ok ? response.json() : null;
@@ -2787,7 +2787,7 @@
2787
2787
  }
2788
2788
 
2789
2789
  function loadMotions() {
2790
- return fetch('/vrmpet3d/motions', { credentials: 'omit' })
2790
+ return fetch('/vrmpet3d/motions', { credentials: 'same-origin' })
2791
2791
  .then(function (response) {
2792
2792
  if (!response.ok) return null;
2793
2793
  return response.json();
@@ -2992,7 +2992,7 @@
2992
2992
  try {
2993
2993
  fetch('/vrmpet3d/preview', {
2994
2994
  method: 'POST',
2995
- credentials: 'omit',
2995
+ credentials: 'same-origin',
2996
2996
  headers: { 'content-type': 'application/json' },
2997
2997
  body: JSON.stringify(preview || {}),
2998
2998
  }).catch(function () { return null; });
@@ -4186,7 +4186,7 @@
4186
4186
  */
4187
4187
  var loadLlm = function (wanted) {
4188
4188
  askedLlmProvider = wanted || '';
4189
- return fetch('/vrmpet3d/llm' + (wanted ? '?provider=' + encodeURIComponent(wanted) : ''), { credentials: 'omit' })
4189
+ return fetch('/vrmpet3d/llm' + (wanted ? '?provider=' + encodeURIComponent(wanted) : ''), { credentials: 'same-origin' })
4190
4190
  .then(function (response) { return response.ok ? response.json() : null; })
4191
4191
  .then(function (body) {
4192
4192
  if (!body) return;
@@ -4391,7 +4391,7 @@
4391
4391
  fetch('/vrmpet3d/say', {
4392
4392
  method: 'POST',
4393
4393
  headers: { 'content-type': 'application/json' },
4394
- credentials: 'omit',
4394
+ credentials: 'same-origin',
4395
4395
  // The HOST answers in this language, so the test line reads like the pet the user is configuring.
4396
4396
  body: JSON.stringify({ state: state, language: language }),
4397
4397
  }).then(function (response) { return response.json(); })
@@ -4631,7 +4631,7 @@
4631
4631
  * about "is anything drawing this model".
4632
4632
  */
4633
4633
  function refreshStatus() {
4634
- return fetch('/vrmpet3d/desktop', { credentials: 'omit' })
4634
+ return fetch('/vrmpet3d/desktop', { credentials: 'same-origin' })
4635
4635
  .then(function (response) { return response.ok ? response.json() : null; })
4636
4636
  .then(function (status) {
4637
4637
  if (!status) {
@@ -5444,7 +5444,7 @@
5444
5444
  };
5445
5445
  fetch('/vrmpet3d/desktop', {
5446
5446
  method: 'POST',
5447
- credentials: 'omit',
5447
+ credentials: 'same-origin',
5448
5448
  headers: { 'content-type': 'application/json' },
5449
5449
  body: JSON.stringify({ action: 'setup' }),
5450
5450
  }).then(function (response) {
package/lib/core.js CHANGED
@@ -5005,7 +5005,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
5005
5005
  function load() {
5006
5006
  if (!supported || stopped) return Promise.resolve(null);
5007
5007
  var request = ++issued;
5008
- return doFetch(url, { credentials: 'omit' })
5008
+ return doFetch(url, { credentials: 'same-origin' })
5009
5009
  .then(function (response) {
5010
5010
  if (response.status === 404 || response.status === 405) {
5011
5011
  supported = false;
@@ -5169,7 +5169,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
5169
5169
  function load() {
5170
5170
  if (!supported || stopped) return Promise.resolve(null);
5171
5171
  var request = ++issued;
5172
- return doFetch(url, { credentials: 'omit' })
5172
+ return doFetch(url, { credentials: 'same-origin' })
5173
5173
  .then(function (response) {
5174
5174
  if (response.status === 404 || response.status === 405) {
5175
5175
  supported = false;
@@ -5213,7 +5213,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
5213
5213
  /**
5214
5214
  * The reporting half of `watchHostState`: the app's pet tells the host what it resolved.
5215
5215
  *
5216
- * Deduplicated on the last value SENT, so the sixty-per-second frame loop can call this on every change
5216
+ * Deduplicated on the latest in-flight value or acknowledged value, so the frame loop can call this on every change
5217
5217
  * without a POST per frame — and a state that flaps back to what the host already has costs nothing.
5218
5218
  *
5219
5219
  * The 碎碎念 LINE travels with the state, and the dedupe key is both together: the pet can say something
@@ -5245,6 +5245,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
5245
5245
  var sentUnread = null;
5246
5246
  /** The last diagnostic sent (see `report`), so the heartbeat re-states what the state it repeats was based on. */
5247
5247
  var sentWhy = null;
5248
+ var reportSequence = 0;
5249
+ var pendingReport = null;
5248
5250
  var beatTimer = null;
5249
5251
  /**
5250
5252
  * What the heartbeat re-states: the last STATE and LINE, never the touch.
@@ -5465,22 +5467,27 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
5465
5467
  return JSON.stringify(value ? [value.waitingSessionIds || [], value.completedSessionIds || [],
5466
5468
  value.statusSessionId || null, value.completedSessionId || null] : [[], [], null, null]);
5467
5469
  };
5468
- if (force !== true && next === sent && nextLine === sentLine && nextTouchKey === sentTouchKey
5469
- && nextClockKey === sentClockKey && nextUnread === sentUnread && navigationKey(nextWhy) === navigationKey(sentWhy)) {
5470
+ // Dedupe against the newest in-flight request, or the last ACKNOWLEDGED report.
5471
+ // A rejected request must remain retryable; an older response cannot commit over a newer report.
5472
+ if (!doFetch || !next) return Promise.resolve(null);
5473
+ var key = JSON.stringify([next, nextLine, nextTouchKey, nextClockKey, nextUnread, navigationKey(nextWhy)]);
5474
+ if (force !== true && ((pendingReport && pendingReport.key === key)
5475
+ || (!pendingReport && next === sent && nextLine === sentLine && nextTouchKey === sentTouchKey
5476
+ && nextClockKey === sentClockKey && nextUnread === sentUnread && navigationKey(nextWhy) === navigationKey(sentWhy)))) {
5470
5477
  return Promise.resolve(null);
5471
5478
  }
5472
- sent = next;
5473
- sentLine = nextLine;
5474
- sentTouch = nextTouch;
5475
- sentClock = nextClock;
5476
- sentUnread = nextUnread;
5477
- sentWhy = nextWhy;
5478
- // No state, no report: the host refuses an unknown name, and a line with no state behind it would only
5479
- // clear the state it is meant to accompany.
5480
- if (!doFetch || !next) return Promise.resolve(null);
5481
- // Recorded for the heartbeat AFTER the send is judged worth making, so a beat always repeats something the
5482
- // host has actually been told.
5483
- lastBeat = { state: next, line: nextLine, why: nextWhy };
5479
+ var request = ++reportSequence;
5480
+ pendingReport = { key: key, request: request };
5481
+ // Heartbeats retry the latest desired STATE, never a one-shot touch event.
5482
+ lastBeat = { state: next, line: nextLine, why: nextWhy, clock: nextClock, unread: nextUnread };
5483
+ function failed() {
5484
+ if (request === reportSequence) {
5485
+ pendingReport = null;
5486
+ sent = null;
5487
+ sentLine = sentTouch = sentClock = sentUnread = sentWhy = null;
5488
+ }
5489
+ return null;
5490
+ }
5484
5491
  try {
5485
5492
  var body = { state: next };
5486
5493
  if (nextLine) body.line = nextLine;
@@ -5488,16 +5495,57 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
5488
5495
  if (nextClock) body.clock = nextClock;
5489
5496
  if (nextUnread !== null) body.unread = nextUnread;
5490
5497
  if (nextWhy) body.why = nextWhy;
5491
- return doFetch(url, {
5492
- method: 'POST',
5493
- credentials: 'omit',
5494
- headers: { 'content-type': 'application/json' },
5495
- body: JSON.stringify(body),
5496
- }).then(function () { return next; }, function () { return null; });
5498
+ return postReport(body).then(function (accepted) {
5499
+ if (!accepted) return failed();
5500
+ if (request === reportSequence) {
5501
+ pendingReport = null;
5502
+ sent = next;
5503
+ sentLine = nextLine;
5504
+ sentTouch = nextTouch;
5505
+ sentClock = nextClock;
5506
+ sentUnread = nextUnread;
5507
+ sentWhy = nextWhy;
5508
+ }
5509
+ return next;
5510
+ }).catch(failed);
5497
5511
  } catch (_error) {
5498
- return Promise.resolve(null);
5512
+ return Promise.resolve(failed());
5499
5513
  }
5500
5514
  }
5515
+ function readAcknowledgement(response) {
5516
+ if (!response || !response.ok || response.redirected) return Promise.resolve(false);
5517
+ return response.json().then(function (payload) {
5518
+ return !!payload && typeof payload.seq === 'number' && Number.isFinite(payload.seq)
5519
+ && payload.seq >= 0 && payload.superseded !== true;
5520
+ });
5521
+ }
5522
+ function postReport(body) {
5523
+ var controller = typeof AbortController === 'function' ? new AbortController() : null;
5524
+ var deadline = null;
5525
+ // A stalled fetch/body must not permanently suppress all subsequent heartbeats.
5526
+ var timeoutMs = Number(settings.requestTimeoutMs) > 0 ? Number(settings.requestTimeoutMs) : 8000;
5527
+ return new Promise(function (resolve, reject) {
5528
+ deadline = setTimeout(function () {
5529
+ if (controller) controller.abort();
5530
+ resolve(false);
5531
+ }, timeoutMs);
5532
+ if (deadline && typeof deadline.unref === 'function') deadline.unref();
5533
+ try {
5534
+ var options = {
5535
+ method: 'POST', credentials: 'same-origin', redirect: 'error',
5536
+ headers: { 'content-type': 'application/json' }, body: JSON.stringify(body),
5537
+ };
5538
+ if (controller) options.signal = controller.signal;
5539
+ Promise.resolve(doFetch(url, options)).then(readAcknowledgement).then(resolve, reject);
5540
+ } catch (error) { reject(error); }
5541
+ }).then(function (accepted) {
5542
+ clearTimeout(deadline);
5543
+ return accepted;
5544
+ }, function (error) {
5545
+ clearTimeout(deadline);
5546
+ throw error;
5547
+ });
5548
+ }
5501
5549
  /**
5502
5550
  * Re-state the last report, unchanged, so that SILENCE can be told from DEATH.
5503
5551
  *
@@ -5514,7 +5562,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
5514
5562
  * all" on every beat — so the floating window lost the turn's 用时 a few seconds into every long turn. `force`
5515
5563
  * is the parameter that exists for this.
5516
5564
  */
5517
- return report(lastBeat.state, lastBeat.line, null, sentClock, true, sentUnread, lastBeat.why);
5565
+ if (pendingReport) return Promise.resolve(null);
5566
+ return report(lastBeat.state, lastBeat.line, null, lastBeat.clock, true, lastBeat.unread, lastBeat.why);
5518
5567
  }
5519
5568
 
5520
5569
  function startHeartbeat(intervalMs) {
@@ -5544,12 +5593,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
5544
5593
  if (!doFetch) return Promise.resolve(null);
5545
5594
  var message = error && (error.stack || error.message) ? String(error.stack || error.message) : String(error);
5546
5595
  try {
5547
- return doFetch(url, {
5548
- method: 'POST',
5549
- credentials: 'omit',
5550
- headers: { 'content-type': 'application/json' },
5551
- body: JSON.stringify({ state: sent || '', error: message.slice(0, 500) }),
5552
- }).then(function () { return message; }, function () { return null; });
5596
+ return postReport({ state: sent || '', error: message.slice(0, 500) })
5597
+ .then(function (accepted) { return accepted ? message : null; }).catch(function () { return null; });
5553
5598
  } catch (_error) {
5554
5599
  return Promise.resolve(null);
5555
5600
  }
@@ -5610,7 +5655,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
5610
5655
  function load() {
5611
5656
  if (!supported || stopped) return Promise.resolve(null);
5612
5657
  var request = ++issued;
5613
- return doFetch(url, { credentials: 'omit' })
5658
+ return doFetch(url, { credentials: 'same-origin' })
5614
5659
  .then(function (response) {
5615
5660
  if (response.status === 404 || response.status === 405) {
5616
5661
  // No store on this host: stop asking, and let the caller keep its own storage.
@@ -5636,7 +5681,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
5636
5681
  var request = ++issued;
5637
5682
  return doFetch(url, {
5638
5683
  method: 'POST',
5639
- credentials: 'omit',
5684
+ credentials: 'same-origin',
5640
5685
  headers: { 'content-type': 'application/json' },
5641
5686
  body: JSON.stringify(body || {}),
5642
5687
  })
@@ -692,6 +692,7 @@
692
692
  if (id === 'globalSettings') { openSettingsWindow('global'); return; }
693
693
  // 切换模型: the model picker's page, the same one the guide's 偏好模型 step is built from.
694
694
  if (id === 'switchModel') { openSettingsWindow('picker'); return; }
695
+ if (id === 'miniGame') { openSettingsWindow('game'); return; }
695
696
  /**
696
697
  * 重载模型 — the same row the app pet's menu grew, with the same meaning (operator, 2026-10-14: 「给内置和外置宠物的右键
697
698
  * 菜单新增“重载模型”的选项,用来刷新模型」): re-read the model FILE and rebuild this window's scene. Nothing about the
@@ -1373,7 +1374,7 @@
1373
1374
  fetch('/vrmpet3d/say', {
1374
1375
  method: 'POST',
1375
1376
  headers: { 'content-type': 'application/json' },
1376
- credentials: 'omit',
1377
+ credentials: 'same-origin',
1377
1378
  body: JSON.stringify({
1378
1379
  state: hostState || null,
1379
1380
  event: stateEventName(),
@@ -1693,7 +1694,7 @@
1693
1694
  */
1694
1695
  function refreshCatalog() {
1695
1696
  if (typeof fetch !== 'function') return Promise.resolve([]);
1696
- return fetch('/vrmpet3d/catalog', { credentials: 'omit' })
1697
+ return fetch('/vrmpet3d/catalog', { credentials: 'same-origin' })
1697
1698
  .then(function (response) {
1698
1699
  if (!response.ok) throw new Error('catalog route returned HTTP ' + response.status);
1699
1700
  return response.json();
@@ -1868,6 +1869,14 @@
1868
1869
  * query parameter this build no longer reads.
1869
1870
  */
1870
1871
  function openSettingsWindow(page) {
1872
+ if (page === 'game') {
1873
+ fetch('/vrmpet3d/desktop', { method: 'POST', credentials: 'same-origin',
1874
+ headers: { 'content-type': 'application/json' },
1875
+ body: JSON.stringify({ action: 'game', modelId: resolvedModelId(preferences.modelId || loadedModel) })
1876
+ }).then(function (response) { if (!response.ok) throw new Error('小游戏窗口打开失败'); })
1877
+ .catch(function (error) { console.warn(error.message); });
1878
+ return;
1879
+ }
1871
1880
  var which = page === 'global' ? 'settings' : (page === 'picker' ? 'picker' : 'config');
1872
1881
  var url = which === 'settings'
1873
1882
  ? '/vrmpet3d/global'
@@ -1881,7 +1890,7 @@
1881
1890
  fetch('/vrmpet3d/desktop', {
1882
1891
  method: 'POST',
1883
1892
  headers: { 'content-type': 'application/json' },
1884
- credentials: 'omit',
1893
+ credentials: 'same-origin',
1885
1894
  // Same reason as the app's pet: name the model that is on screen, so the window opens on it.
1886
1895
  body: JSON.stringify({ action: which, modelId: resolvedModelId(preferences.modelId || loadedModel) }),
1887
1896
  }).then(function (response) { if (!response.ok) fallback(); })
@@ -2663,7 +2672,7 @@
2663
2672
  try {
2664
2673
  fetch('/vrmpet3d/preview', {
2665
2674
  method: 'POST',
2666
- credentials: 'omit',
2675
+ credentials: 'same-origin',
2667
2676
  headers: { 'content-type': 'application/json' },
2668
2677
  body: '{}',
2669
2678
  }).catch(function () { return null; });
@@ -2790,7 +2799,7 @@
2790
2799
  }
2791
2800
 
2792
2801
  function fetchMotionBytes(id) {
2793
- return fetch('/vrmpet3d/motion?m=' + encodeURIComponent(id), { credentials: 'omit' })
2802
+ return fetch('/vrmpet3d/motion?m=' + encodeURIComponent(id), { credentials: 'same-origin' })
2794
2803
  .then(function (response) {
2795
2804
  if (!response.ok) throw new Error('the motion route answered HTTP ' + response.status);
2796
2805
  return response.arrayBuffer();
@@ -2909,7 +2918,7 @@
2909
2918
  */
2910
2919
  function refreshMotions() {
2911
2920
  if (!motionRouteAvailable || typeof fetch !== 'function') return Promise.resolve(null);
2912
- return fetch('/vrmpet3d/motions', { credentials: 'omit' })
2921
+ return fetch('/vrmpet3d/motions', { credentials: 'same-origin' })
2913
2922
  .then(function (response) {
2914
2923
  if (response.status === 404 || response.status === 405) {
2915
2924
  motionRouteAvailable = false;
@@ -3637,7 +3646,7 @@
3637
3646
  scene.setPreview(null);
3638
3647
  scene.setState('resting');
3639
3648
  fetch('/vrmpet3d/preview', {
3640
- method: 'POST', credentials: 'omit',
3649
+ method: 'POST', credentials: 'same-origin',
3641
3650
  headers: { 'content-type': 'application/json' }, body: '{}',
3642
3651
  }).catch(function () { return null; });
3643
3652
  }