dsh-plugin-vrmpet3d 0.1.3 → 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.
- package/desktop/game-visibility.cjs +31 -16
- package/lib/client.core.js +78 -33
- package/lib/client.js +11 -11
- package/lib/client.scene.js +3 -2
- package/lib/config-page.js +12 -12
- package/lib/core.js +78 -33
- package/lib/desktop-page.js +8 -8
- package/lib/index.js +135 -35
- package/lib/mini-game-page.js +6 -6
- package/lib/model-picker.js +4 -4
- package/lib/scene.js +3 -2
- package/lib/setup-page.js +3 -3
- package/package.json +1 -1
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
// A focused game temporarily hides only a pet window that was visible beforehand.
|
|
2
|
-
const http = require('node:http');
|
|
3
|
-
const https = require('node:https');
|
|
4
2
|
// A transparent window can paint and report pixel hits after showInactive while
|
|
5
3
|
// Chromium still uses the pre-hide input surface. A real bounds transition (the
|
|
6
4
|
// same transition wheel zoom causes) rebuilds that surface without changing the pet.
|
|
@@ -27,24 +25,41 @@ function createVisibilityController(win, hooks = {}) {
|
|
|
27
25
|
function watchGameVisibility(win, pageURL, interval = 500, hooks = {}) {
|
|
28
26
|
const apply = createVisibilityController(win, hooks);
|
|
29
27
|
const url = new URL('/vrmpet3d/desktop', pageURL);
|
|
30
|
-
|
|
28
|
+
// The page and this poll must use the SAME Chromium session (cookies/proxy/TLS).
|
|
29
|
+
// Node's http.get never inherited the window's authentication cookies.
|
|
30
|
+
const session = win.webContents.session;
|
|
31
31
|
let request = null, stopped = false;
|
|
32
|
-
function poll() {
|
|
32
|
+
async function poll() {
|
|
33
33
|
if (stopped || request || win.isDestroyed()) return;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
request = controller;
|
|
36
|
+
const deadline = setTimeout(() => controller.abort(), 1500);
|
|
37
|
+
deadline.unref?.();
|
|
38
|
+
try {
|
|
39
|
+
const response = await session.fetch(url.href, {
|
|
40
|
+
credentials: 'include', redirect: 'error', cache: 'no-store', signal: controller.signal,
|
|
40
41
|
});
|
|
41
|
-
response.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
if (!response.ok || response.redirected || !/^application\/json\b/i.test(response.headers.get('content-type') || '')) {
|
|
43
|
+
await response.body?.cancel();
|
|
44
|
+
throw new Error('game status is not authenticated JSON');
|
|
45
|
+
}
|
|
46
|
+
const reader = response.body.getReader();
|
|
47
|
+
let size = 0;
|
|
48
|
+
const chunks = [];
|
|
49
|
+
try {
|
|
50
|
+
for (;;) {
|
|
51
|
+
const { value, done } = await reader.read();
|
|
52
|
+
if (done) break;
|
|
53
|
+
size += value.byteLength;
|
|
54
|
+
if (size > 16384) { await reader.cancel(); throw new Error('game status exceeds limit'); }
|
|
55
|
+
chunks.push(Buffer.from(value));
|
|
56
|
+
}
|
|
57
|
+
} finally { reader.releaseLock(); }
|
|
58
|
+
if (!stopped) apply(JSON.parse(Buffer.concat(chunks).toString('utf8')).gameFocused === true);
|
|
59
|
+
} catch (_) { if (!stopped) apply(false); }
|
|
60
|
+
finally { clearTimeout(deadline); if (request === controller) request = null; }
|
|
46
61
|
}
|
|
47
62
|
const timer = setInterval(poll, interval); timer.unref(); poll();
|
|
48
|
-
return function stop() { stopped = true; clearInterval(timer); if (request) request.
|
|
63
|
+
return function stop() { stopped = true; clearInterval(timer); if (request) request.abort(); apply(false); };
|
|
49
64
|
}
|
|
50
65
|
module.exports = { createVisibilityController, watchGameVisibility, refreshInputRegion };
|
package/lib/client.core.js
CHANGED
|
@@ -5013,7 +5013,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5013
5013
|
function load() {
|
|
5014
5014
|
if (!supported || stopped) return Promise.resolve(null);
|
|
5015
5015
|
var request = ++issued;
|
|
5016
|
-
return doFetch(url, { credentials: '
|
|
5016
|
+
return doFetch(url, { credentials: 'same-origin' })
|
|
5017
5017
|
.then(function (response) {
|
|
5018
5018
|
if (response.status === 404 || response.status === 405) {
|
|
5019
5019
|
supported = false;
|
|
@@ -5177,7 +5177,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5177
5177
|
function load() {
|
|
5178
5178
|
if (!supported || stopped) return Promise.resolve(null);
|
|
5179
5179
|
var request = ++issued;
|
|
5180
|
-
return doFetch(url, { credentials: '
|
|
5180
|
+
return doFetch(url, { credentials: 'same-origin' })
|
|
5181
5181
|
.then(function (response) {
|
|
5182
5182
|
if (response.status === 404 || response.status === 405) {
|
|
5183
5183
|
supported = false;
|
|
@@ -5221,7 +5221,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5221
5221
|
/**
|
|
5222
5222
|
* The reporting half of `watchHostState`: the app's pet tells the host what it resolved.
|
|
5223
5223
|
*
|
|
5224
|
-
* Deduplicated on the
|
|
5224
|
+
* Deduplicated on the latest in-flight value or acknowledged value, so the frame loop can call this on every change
|
|
5225
5225
|
* without a POST per frame — and a state that flaps back to what the host already has costs nothing.
|
|
5226
5226
|
*
|
|
5227
5227
|
* The 碎碎念 LINE travels with the state, and the dedupe key is both together: the pet can say something
|
|
@@ -5253,6 +5253,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5253
5253
|
var sentUnread = null;
|
|
5254
5254
|
/** The last diagnostic sent (see `report`), so the heartbeat re-states what the state it repeats was based on. */
|
|
5255
5255
|
var sentWhy = null;
|
|
5256
|
+
var reportSequence = 0;
|
|
5257
|
+
var pendingReport = null;
|
|
5256
5258
|
var beatTimer = null;
|
|
5257
5259
|
/**
|
|
5258
5260
|
* What the heartbeat re-states: the last STATE and LINE, never the touch.
|
|
@@ -5473,22 +5475,27 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5473
5475
|
return JSON.stringify(value ? [value.waitingSessionIds || [], value.completedSessionIds || [],
|
|
5474
5476
|
value.statusSessionId || null, value.completedSessionId || null] : [[], [], null, null]);
|
|
5475
5477
|
};
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
+
// Dedupe against the newest in-flight request, or the last ACKNOWLEDGED report.
|
|
5479
|
+
// A rejected request must remain retryable; an older response cannot commit over a newer report.
|
|
5480
|
+
if (!doFetch || !next) return Promise.resolve(null);
|
|
5481
|
+
var key = JSON.stringify([next, nextLine, nextTouchKey, nextClockKey, nextUnread, navigationKey(nextWhy)]);
|
|
5482
|
+
if (force !== true && ((pendingReport && pendingReport.key === key)
|
|
5483
|
+
|| (!pendingReport && next === sent && nextLine === sentLine && nextTouchKey === sentTouchKey
|
|
5484
|
+
&& nextClockKey === sentClockKey && nextUnread === sentUnread && navigationKey(nextWhy) === navigationKey(sentWhy)))) {
|
|
5478
5485
|
return Promise.resolve(null);
|
|
5479
5486
|
}
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5487
|
+
var request = ++reportSequence;
|
|
5488
|
+
pendingReport = { key: key, request: request };
|
|
5489
|
+
// Heartbeats retry the latest desired STATE, never a one-shot touch event.
|
|
5490
|
+
lastBeat = { state: next, line: nextLine, why: nextWhy, clock: nextClock, unread: nextUnread };
|
|
5491
|
+
function failed() {
|
|
5492
|
+
if (request === reportSequence) {
|
|
5493
|
+
pendingReport = null;
|
|
5494
|
+
sent = null;
|
|
5495
|
+
sentLine = sentTouch = sentClock = sentUnread = sentWhy = null;
|
|
5496
|
+
}
|
|
5497
|
+
return null;
|
|
5498
|
+
}
|
|
5492
5499
|
try {
|
|
5493
5500
|
var body = { state: next };
|
|
5494
5501
|
if (nextLine) body.line = nextLine;
|
|
@@ -5496,16 +5503,57 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5496
5503
|
if (nextClock) body.clock = nextClock;
|
|
5497
5504
|
if (nextUnread !== null) body.unread = nextUnread;
|
|
5498
5505
|
if (nextWhy) body.why = nextWhy;
|
|
5499
|
-
return
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5506
|
+
return postReport(body).then(function (accepted) {
|
|
5507
|
+
if (!accepted) return failed();
|
|
5508
|
+
if (request === reportSequence) {
|
|
5509
|
+
pendingReport = null;
|
|
5510
|
+
sent = next;
|
|
5511
|
+
sentLine = nextLine;
|
|
5512
|
+
sentTouch = nextTouch;
|
|
5513
|
+
sentClock = nextClock;
|
|
5514
|
+
sentUnread = nextUnread;
|
|
5515
|
+
sentWhy = nextWhy;
|
|
5516
|
+
}
|
|
5517
|
+
return next;
|
|
5518
|
+
}).catch(failed);
|
|
5505
5519
|
} catch (_error) {
|
|
5506
|
-
return Promise.resolve(
|
|
5520
|
+
return Promise.resolve(failed());
|
|
5507
5521
|
}
|
|
5508
5522
|
}
|
|
5523
|
+
function readAcknowledgement(response) {
|
|
5524
|
+
if (!response || !response.ok || response.redirected) return Promise.resolve(false);
|
|
5525
|
+
return response.json().then(function (payload) {
|
|
5526
|
+
return !!payload && typeof payload.seq === 'number' && Number.isFinite(payload.seq)
|
|
5527
|
+
&& payload.seq >= 0 && payload.superseded !== true;
|
|
5528
|
+
});
|
|
5529
|
+
}
|
|
5530
|
+
function postReport(body) {
|
|
5531
|
+
var controller = typeof AbortController === 'function' ? new AbortController() : null;
|
|
5532
|
+
var deadline = null;
|
|
5533
|
+
// A stalled fetch/body must not permanently suppress all subsequent heartbeats.
|
|
5534
|
+
var timeoutMs = Number(settings.requestTimeoutMs) > 0 ? Number(settings.requestTimeoutMs) : 8000;
|
|
5535
|
+
return new Promise(function (resolve, reject) {
|
|
5536
|
+
deadline = setTimeout(function () {
|
|
5537
|
+
if (controller) controller.abort();
|
|
5538
|
+
resolve(false);
|
|
5539
|
+
}, timeoutMs);
|
|
5540
|
+
if (deadline && typeof deadline.unref === 'function') deadline.unref();
|
|
5541
|
+
try {
|
|
5542
|
+
var options = {
|
|
5543
|
+
method: 'POST', credentials: 'same-origin', redirect: 'error',
|
|
5544
|
+
headers: { 'content-type': 'application/json' }, body: JSON.stringify(body),
|
|
5545
|
+
};
|
|
5546
|
+
if (controller) options.signal = controller.signal;
|
|
5547
|
+
Promise.resolve(doFetch(url, options)).then(readAcknowledgement).then(resolve, reject);
|
|
5548
|
+
} catch (error) { reject(error); }
|
|
5549
|
+
}).then(function (accepted) {
|
|
5550
|
+
clearTimeout(deadline);
|
|
5551
|
+
return accepted;
|
|
5552
|
+
}, function (error) {
|
|
5553
|
+
clearTimeout(deadline);
|
|
5554
|
+
throw error;
|
|
5555
|
+
});
|
|
5556
|
+
}
|
|
5509
5557
|
/**
|
|
5510
5558
|
* Re-state the last report, unchanged, so that SILENCE can be told from DEATH.
|
|
5511
5559
|
*
|
|
@@ -5522,7 +5570,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5522
5570
|
* all" on every beat — so the floating window lost the turn's 用时 a few seconds into every long turn. `force`
|
|
5523
5571
|
* is the parameter that exists for this.
|
|
5524
5572
|
*/
|
|
5525
|
-
|
|
5573
|
+
if (pendingReport) return Promise.resolve(null);
|
|
5574
|
+
return report(lastBeat.state, lastBeat.line, null, lastBeat.clock, true, lastBeat.unread, lastBeat.why);
|
|
5526
5575
|
}
|
|
5527
5576
|
|
|
5528
5577
|
function startHeartbeat(intervalMs) {
|
|
@@ -5552,12 +5601,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5552
5601
|
if (!doFetch) return Promise.resolve(null);
|
|
5553
5602
|
var message = error && (error.stack || error.message) ? String(error.stack || error.message) : String(error);
|
|
5554
5603
|
try {
|
|
5555
|
-
return
|
|
5556
|
-
|
|
5557
|
-
credentials: 'omit',
|
|
5558
|
-
headers: { 'content-type': 'application/json' },
|
|
5559
|
-
body: JSON.stringify({ state: sent || '', error: message.slice(0, 500) }),
|
|
5560
|
-
}).then(function () { return message; }, function () { return null; });
|
|
5604
|
+
return postReport({ state: sent || '', error: message.slice(0, 500) })
|
|
5605
|
+
.then(function (accepted) { return accepted ? message : null; }).catch(function () { return null; });
|
|
5561
5606
|
} catch (_error) {
|
|
5562
5607
|
return Promise.resolve(null);
|
|
5563
5608
|
}
|
|
@@ -5618,7 +5663,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5618
5663
|
function load() {
|
|
5619
5664
|
if (!supported || stopped) return Promise.resolve(null);
|
|
5620
5665
|
var request = ++issued;
|
|
5621
|
-
return doFetch(url, { credentials: '
|
|
5666
|
+
return doFetch(url, { credentials: 'same-origin' })
|
|
5622
5667
|
.then(function (response) {
|
|
5623
5668
|
if (response.status === 404 || response.status === 405) {
|
|
5624
5669
|
// No store on this host: stop asking, and let the caller keep its own storage.
|
|
@@ -5644,7 +5689,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5644
5689
|
var request = ++issued;
|
|
5645
5690
|
return doFetch(url, {
|
|
5646
5691
|
method: 'POST',
|
|
5647
|
-
credentials: '
|
|
5692
|
+
credentials: 'same-origin',
|
|
5648
5693
|
headers: { 'content-type': 'application/json' },
|
|
5649
5694
|
body: JSON.stringify(body || {}),
|
|
5650
5695
|
})
|
package/lib/client.js
CHANGED
|
@@ -1672,7 +1672,7 @@
|
|
|
1672
1672
|
if ((!globalThis.dshDesktop && !/Electron\//i.test(globalThis.navigator.userAgent)) || !self.canUseIndependentPet()) {
|
|
1673
1673
|
globalThis.open(gameUrl, '_blank', 'noopener'); return;
|
|
1674
1674
|
}
|
|
1675
|
-
fetch('/vrmpet3d/desktop', { method: 'POST', credentials: '
|
|
1675
|
+
fetch('/vrmpet3d/desktop', { method: 'POST', credentials: 'same-origin',
|
|
1676
1676
|
headers: { 'content-type': 'application/json' },
|
|
1677
1677
|
body: JSON.stringify({ action: 'game', modelId: self.currentModelId() })
|
|
1678
1678
|
}).then(function (response) {
|
|
@@ -1705,7 +1705,7 @@
|
|
|
1705
1705
|
fetch('/vrmpet3d/desktop', {
|
|
1706
1706
|
method: 'POST',
|
|
1707
1707
|
headers: { 'content-type': 'application/json' },
|
|
1708
|
-
credentials: '
|
|
1708
|
+
credentials: 'same-origin',
|
|
1709
1709
|
// The model goes with the request: the model window must configure the model that is ON SCREEN, and this
|
|
1710
1710
|
// surface is the only one that knows it at that moment. Without it the host can only fall back to the
|
|
1711
1711
|
// launcher's model (or nothing), which is how the settings window ended up describing the built-in
|
|
@@ -1741,12 +1741,12 @@
|
|
|
1741
1741
|
var send = function (body) {
|
|
1742
1742
|
return fetch('/vrmpet3d/setup-state', {
|
|
1743
1743
|
method: 'POST',
|
|
1744
|
-
credentials: '
|
|
1744
|
+
credentials: 'same-origin',
|
|
1745
1745
|
headers: { 'content-type': 'application/json' },
|
|
1746
1746
|
body: JSON.stringify(body),
|
|
1747
1747
|
});
|
|
1748
1748
|
};
|
|
1749
|
-
return fetch('/vrmpet3d/setup-state', { credentials: '
|
|
1749
|
+
return fetch('/vrmpet3d/setup-state', { credentials: 'same-origin' })
|
|
1750
1750
|
.then(function (response) { return response.ok ? response.json() : null; })
|
|
1751
1751
|
.then(function (state) {
|
|
1752
1752
|
if (!state || state.completed === true || state.prompted === true) return null;
|
|
@@ -1754,7 +1754,7 @@
|
|
|
1754
1754
|
.then(function () {
|
|
1755
1755
|
return fetch('/vrmpet3d/desktop', {
|
|
1756
1756
|
method: 'POST',
|
|
1757
|
-
credentials: '
|
|
1757
|
+
credentials: 'same-origin',
|
|
1758
1758
|
headers: { 'content-type': 'application/json' },
|
|
1759
1759
|
body: JSON.stringify({ action: 'setup' }),
|
|
1760
1760
|
});
|
|
@@ -2273,7 +2273,7 @@
|
|
|
2273
2273
|
this.syncDesktop(null, strings.desktopStarting);
|
|
2274
2274
|
return fetch('/vrmpet3d/desktop', {
|
|
2275
2275
|
method: 'POST',
|
|
2276
|
-
credentials: '
|
|
2276
|
+
credentials: 'same-origin',
|
|
2277
2277
|
headers: { 'content-type': 'application/json' },
|
|
2278
2278
|
body: JSON.stringify({
|
|
2279
2279
|
action: enabled ? 'enable' : 'disable',
|
|
@@ -2296,7 +2296,7 @@
|
|
|
2296
2296
|
/** Ask the host what the window is doing, and show it. */
|
|
2297
2297
|
VrmPet3DWidget.prototype.refreshDesktopStatus = function () {
|
|
2298
2298
|
var self = this;
|
|
2299
|
-
return fetch('/vrmpet3d/desktop', { credentials: '
|
|
2299
|
+
return fetch('/vrmpet3d/desktop', { credentials: 'same-origin' })
|
|
2300
2300
|
.then(function (response) { return response.ok ? response.json() : null; })
|
|
2301
2301
|
.then(function (status) { self.syncDesktop(status, null); return status; })
|
|
2302
2302
|
.catch(function () {
|
|
@@ -3004,7 +3004,7 @@
|
|
|
3004
3004
|
*/
|
|
3005
3005
|
VrmPet3DWidget.prototype.refreshCatalog = function () {
|
|
3006
3006
|
var self = this;
|
|
3007
|
-
return fetch('/vrmpet3d/catalog', { credentials: '
|
|
3007
|
+
return fetch('/vrmpet3d/catalog', { credentials: 'same-origin' })
|
|
3008
3008
|
.then(function (response) {
|
|
3009
3009
|
if (!response.ok) throw new Error(word(self.language, 'catalogRoute', { status: response.status }));
|
|
3010
3010
|
return response.json();
|
|
@@ -3222,7 +3222,7 @@
|
|
|
3222
3222
|
*/
|
|
3223
3223
|
VrmPet3DWidget.prototype.refreshMotions = function () {
|
|
3224
3224
|
var self = this;
|
|
3225
|
-
return fetch('/vrmpet3d/motions', { credentials: '
|
|
3225
|
+
return fetch('/vrmpet3d/motions', { credentials: 'same-origin' })
|
|
3226
3226
|
.then(function (response) {
|
|
3227
3227
|
if (!response.ok) throw new Error('motion catalog route returned HTTP ' + response.status);
|
|
3228
3228
|
return response.json();
|
|
@@ -3689,7 +3689,7 @@
|
|
|
3689
3689
|
/** Fetch one motion's bytes from the host's byte route. */
|
|
3690
3690
|
VrmPet3DWidget.prototype.fetchMotionBytes = function (motionId) {
|
|
3691
3691
|
var url = '/vrmpet3d/motion?m=' + encodeURIComponent(motionId);
|
|
3692
|
-
return fetch(url, { credentials: '
|
|
3692
|
+
return fetch(url, { credentials: 'same-origin' }).then(function (response) {
|
|
3693
3693
|
if (!response.ok) throw new Error(word(this.language, 'motionRoute', { status: response.status }));
|
|
3694
3694
|
return response.arrayBuffer();
|
|
3695
3695
|
});
|
|
@@ -4298,7 +4298,7 @@
|
|
|
4298
4298
|
return fetch('/vrmpet3d/say', {
|
|
4299
4299
|
method: 'POST',
|
|
4300
4300
|
headers: { 'content-type': 'application/json' },
|
|
4301
|
-
credentials: '
|
|
4301
|
+
credentials: 'same-origin',
|
|
4302
4302
|
/**
|
|
4303
4303
|
* The request carries five things, and each is a different answer to "what is this line about".
|
|
4304
4304
|
*
|
package/lib/client.scene.js
CHANGED
|
@@ -2060,11 +2060,12 @@
|
|
|
2060
2060
|
* repeated GET of the same URL without touching the disk — which would make 重载模型 look like it did nothing. The
|
|
2061
2061
|
* one-shot buster from `bustModelCache` rides the query string, and is consumed here so an ordinary load stays clean.
|
|
2062
2062
|
*/
|
|
2063
|
-
|
|
2063
|
+
var reload = Boolean(this.modelBust);
|
|
2064
|
+
if (reload) {
|
|
2064
2065
|
url += (url.indexOf('?') === -1 ? '?' : '&') + 'v=' + this.modelBust;
|
|
2065
2066
|
this.modelBust = null;
|
|
2066
2067
|
}
|
|
2067
|
-
var promise = fetch(url, { credentials: '
|
|
2068
|
+
var promise = fetch(url, { credentials: 'same-origin', cache: reload ? 'reload' : 'default' })
|
|
2068
2069
|
.then(function (response) {
|
|
2069
2070
|
if (!response.ok) throw new Error('asset route returned HTTP ' + response.status);
|
|
2070
2071
|
return response.arrayBuffer();
|
package/lib/config-page.js
CHANGED
|
@@ -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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
|
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
|
-
|
|
5469
|
-
|
|
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
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
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
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
5548
|
-
|
|
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: '
|
|
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: '
|
|
5684
|
+
credentials: 'same-origin',
|
|
5640
5685
|
headers: { 'content-type': 'application/json' },
|
|
5641
5686
|
body: JSON.stringify(body || {}),
|
|
5642
5687
|
})
|
package/lib/desktop-page.js
CHANGED
|
@@ -1374,7 +1374,7 @@
|
|
|
1374
1374
|
fetch('/vrmpet3d/say', {
|
|
1375
1375
|
method: 'POST',
|
|
1376
1376
|
headers: { 'content-type': 'application/json' },
|
|
1377
|
-
credentials: '
|
|
1377
|
+
credentials: 'same-origin',
|
|
1378
1378
|
body: JSON.stringify({
|
|
1379
1379
|
state: hostState || null,
|
|
1380
1380
|
event: stateEventName(),
|
|
@@ -1694,7 +1694,7 @@
|
|
|
1694
1694
|
*/
|
|
1695
1695
|
function refreshCatalog() {
|
|
1696
1696
|
if (typeof fetch !== 'function') return Promise.resolve([]);
|
|
1697
|
-
return fetch('/vrmpet3d/catalog', { credentials: '
|
|
1697
|
+
return fetch('/vrmpet3d/catalog', { credentials: 'same-origin' })
|
|
1698
1698
|
.then(function (response) {
|
|
1699
1699
|
if (!response.ok) throw new Error('catalog route returned HTTP ' + response.status);
|
|
1700
1700
|
return response.json();
|
|
@@ -1870,7 +1870,7 @@
|
|
|
1870
1870
|
*/
|
|
1871
1871
|
function openSettingsWindow(page) {
|
|
1872
1872
|
if (page === 'game') {
|
|
1873
|
-
fetch('/vrmpet3d/desktop', { method: 'POST', credentials: '
|
|
1873
|
+
fetch('/vrmpet3d/desktop', { method: 'POST', credentials: 'same-origin',
|
|
1874
1874
|
headers: { 'content-type': 'application/json' },
|
|
1875
1875
|
body: JSON.stringify({ action: 'game', modelId: resolvedModelId(preferences.modelId || loadedModel) })
|
|
1876
1876
|
}).then(function (response) { if (!response.ok) throw new Error('小游戏窗口打开失败'); })
|
|
@@ -1890,7 +1890,7 @@
|
|
|
1890
1890
|
fetch('/vrmpet3d/desktop', {
|
|
1891
1891
|
method: 'POST',
|
|
1892
1892
|
headers: { 'content-type': 'application/json' },
|
|
1893
|
-
credentials: '
|
|
1893
|
+
credentials: 'same-origin',
|
|
1894
1894
|
// Same reason as the app's pet: name the model that is on screen, so the window opens on it.
|
|
1895
1895
|
body: JSON.stringify({ action: which, modelId: resolvedModelId(preferences.modelId || loadedModel) }),
|
|
1896
1896
|
}).then(function (response) { if (!response.ok) fallback(); })
|
|
@@ -2672,7 +2672,7 @@
|
|
|
2672
2672
|
try {
|
|
2673
2673
|
fetch('/vrmpet3d/preview', {
|
|
2674
2674
|
method: 'POST',
|
|
2675
|
-
credentials: '
|
|
2675
|
+
credentials: 'same-origin',
|
|
2676
2676
|
headers: { 'content-type': 'application/json' },
|
|
2677
2677
|
body: '{}',
|
|
2678
2678
|
}).catch(function () { return null; });
|
|
@@ -2799,7 +2799,7 @@
|
|
|
2799
2799
|
}
|
|
2800
2800
|
|
|
2801
2801
|
function fetchMotionBytes(id) {
|
|
2802
|
-
return fetch('/vrmpet3d/motion?m=' + encodeURIComponent(id), { credentials: '
|
|
2802
|
+
return fetch('/vrmpet3d/motion?m=' + encodeURIComponent(id), { credentials: 'same-origin' })
|
|
2803
2803
|
.then(function (response) {
|
|
2804
2804
|
if (!response.ok) throw new Error('the motion route answered HTTP ' + response.status);
|
|
2805
2805
|
return response.arrayBuffer();
|
|
@@ -2918,7 +2918,7 @@
|
|
|
2918
2918
|
*/
|
|
2919
2919
|
function refreshMotions() {
|
|
2920
2920
|
if (!motionRouteAvailable || typeof fetch !== 'function') return Promise.resolve(null);
|
|
2921
|
-
return fetch('/vrmpet3d/motions', { credentials: '
|
|
2921
|
+
return fetch('/vrmpet3d/motions', { credentials: 'same-origin' })
|
|
2922
2922
|
.then(function (response) {
|
|
2923
2923
|
if (response.status === 404 || response.status === 405) {
|
|
2924
2924
|
motionRouteAvailable = false;
|
|
@@ -3646,7 +3646,7 @@
|
|
|
3646
3646
|
scene.setPreview(null);
|
|
3647
3647
|
scene.setState('resting');
|
|
3648
3648
|
fetch('/vrmpet3d/preview', {
|
|
3649
|
-
method: 'POST', credentials: '
|
|
3649
|
+
method: 'POST', credentials: 'same-origin',
|
|
3650
3650
|
headers: { 'content-type': 'application/json' }, body: '{}',
|
|
3651
3651
|
}).catch(function () { return null; });
|
|
3652
3652
|
}
|
package/lib/index.js
CHANGED
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
// boundary hub still provides a baseline; with no `webServer` service the pet
|
|
18
18
|
// still runs from its built-in placeholder avatar and reports why.
|
|
19
19
|
|
|
20
|
-
import { copyFileSync, createReadStream, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { copyFileSync, createReadStream, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
import { createHash } from 'node:crypto';
|
|
21
23
|
import { homedir } from 'node:os';
|
|
22
24
|
import { basename, dirname, extname, isAbsolute, join, normalize, resolve, sep } from 'node:path';
|
|
23
25
|
import { fileURLToPath } from 'node:url';
|
|
@@ -55,6 +57,21 @@ import { COMPRESSED_SUFFIX, decompressBrotli, isCompressedMotionName } from './b
|
|
|
55
57
|
/** The shared pure logic, published by the import above. */
|
|
56
58
|
const core = globalThis.__VRMPET3D_CORE__;
|
|
57
59
|
|
|
60
|
+
/** Resolve from the running CLI, not the plugin's dependencies or the Node executable.
|
|
61
|
+
* package.json is exported by the chat package; lib/client.js is intentionally not.
|
|
62
|
+
* Resolve the entry's symlink first so globally installed CLI shims work too.
|
|
63
|
+
*/
|
|
64
|
+
export function installRootFromDshEntry(entry = process.argv[1]) {
|
|
65
|
+
if (!entry) return null;
|
|
66
|
+
try {
|
|
67
|
+
const fromDsh = createRequire(realpathSync(entry));
|
|
68
|
+
const chatPackage = fromDsh.resolve('@deepseek-ai/dsh-client-ui-chat/package.json');
|
|
69
|
+
return dirname(dirname(dirname(dirname(chatPackage))));
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
58
75
|
export const name = 'vrmpet3d';
|
|
59
76
|
|
|
60
77
|
/**
|
|
@@ -1811,7 +1828,43 @@ function sendJson(req, res, payload) {
|
|
|
1811
1828
|
* copies of this drifted before — one capped at 64 KB and one not — and the route without the
|
|
1812
1829
|
* cap is the one that reads an arbitrary upload into memory.
|
|
1813
1830
|
*/
|
|
1831
|
+
function allowWriteRequest(req, res) {
|
|
1832
|
+
const headers = req.headers || {};
|
|
1833
|
+
const site = headers['sec-fetch-site'];
|
|
1834
|
+
if (site === 'cross-site') {
|
|
1835
|
+
sendStatus(res, 403, 'cross-site writes are not allowed');
|
|
1836
|
+
return false;
|
|
1837
|
+
}
|
|
1838
|
+
// This forbidden browser header describes the PUBLIC request, even when a proxy
|
|
1839
|
+
// rewrites Host internally. A foreign web page cannot forge same-origin metadata.
|
|
1840
|
+
if (site === 'same-origin' && headers.origin !== 'null') return true;
|
|
1841
|
+
// Compare authority using the browser's scheme: TLS may terminate at a reverse proxy.
|
|
1842
|
+
// Do not trust arbitrary X-Forwarded-* headers to authorize a different public host.
|
|
1843
|
+
const source = headers.origin || headers.referer;
|
|
1844
|
+
if (source) {
|
|
1845
|
+
try {
|
|
1846
|
+
const origin = new URL(source);
|
|
1847
|
+
const target = new URL(`${origin.protocol}//${headers.host}`);
|
|
1848
|
+
if (!/^https?:$/.test(origin.protocol) || origin.host !== target.host || target.username || target.password) throw new Error('origin');
|
|
1849
|
+
} catch {
|
|
1850
|
+
sendStatus(res, 403, 'a same-origin request is required');
|
|
1851
|
+
return false;
|
|
1852
|
+
}
|
|
1853
|
+
} else if (site && site !== 'same-origin' && site !== 'none') {
|
|
1854
|
+
sendStatus(res, 403, 'a same-origin request is required');
|
|
1855
|
+
return false;
|
|
1856
|
+
}
|
|
1857
|
+
// Native/local clients have no browser Origin. Browser JSON writes below still require
|
|
1858
|
+
// a non-safelisted content type, so text/plain cannot bypass the CORS preflight.
|
|
1859
|
+
return true;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1814
1862
|
function readJsonBody(req, res, limit, onBody) {
|
|
1863
|
+
if (!allowWriteRequest(req, res)) return;
|
|
1864
|
+
if (!/^application\/json\s*(?:;|$)/i.test(String(req.headers?.['content-type'] || ''))) {
|
|
1865
|
+
sendStatus(res, 415, 'application/json is required');
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1815
1868
|
const chunks = [];
|
|
1816
1869
|
let size = 0;
|
|
1817
1870
|
let refused = false;
|
|
@@ -1855,6 +1908,12 @@ const MODEL_UPLOAD_LIMIT = 256 * 1024 * 1024;
|
|
|
1855
1908
|
* and no callback at all once the response has been sent.
|
|
1856
1909
|
*/
|
|
1857
1910
|
function readRawBody(req, res, limit, onBody) {
|
|
1911
|
+
if (!allowWriteRequest(req, res)) return;
|
|
1912
|
+
const mediaType = String(req.headers?.['content-type'] || '').split(';')[0].trim().toLowerCase();
|
|
1913
|
+
if (!mediaType || ['text/plain', 'application/x-www-form-urlencoded', 'multipart/form-data'].includes(mediaType)) {
|
|
1914
|
+
sendStatus(res, 415, 'a binary content type is required');
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1858
1917
|
const chunks = [];
|
|
1859
1918
|
let size = 0;
|
|
1860
1919
|
let refused = false;
|
|
@@ -1891,18 +1950,32 @@ function methodAllowed(req, res) {
|
|
|
1891
1950
|
return false;
|
|
1892
1951
|
}
|
|
1893
1952
|
|
|
1894
|
-
/**
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
*
|
|
1904
|
-
|
|
1905
|
-
|
|
1953
|
+
/** Private browser validators for a model or its sibling texture. */
|
|
1954
|
+
function modelCacheHeaders(target, stats, reload) {
|
|
1955
|
+
// Include file identity as well as size/times: model IDs are positional and can
|
|
1956
|
+
// later address a different file. Hash the identity so no server path is exposed.
|
|
1957
|
+
const fingerprint = [realpathSync(target), stats.dev, stats.ino, stats.size, stats.mtimeMs, stats.ctimeMs].join('|');
|
|
1958
|
+
return {
|
|
1959
|
+
etag: `W/"${createHash('sha256').update(fingerprint).digest('hex')}"`,
|
|
1960
|
+
// Store only in the user's browser. Revalidation still passes through proxy auth.
|
|
1961
|
+
'cache-control': reload ? 'no-store' : 'private, no-cache',
|
|
1962
|
+
'access-control-allow-origin': '*',
|
|
1963
|
+
'x-content-type-options': 'nosniff',
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
function modelNotModified(req, res, headers, reload) {
|
|
1968
|
+
if (reload) return false;
|
|
1969
|
+
const requested = String(req.headers['if-none-match'] || '').split(',').map(value => value.trim());
|
|
1970
|
+
const tag = headers.etag.replace(/^W\//, '');
|
|
1971
|
+
if (!requested.some(value => value === '*' || value.replace(/^W\//, '') === tag)) return false;
|
|
1972
|
+
res.writeHead(304, headers);
|
|
1973
|
+
res.end();
|
|
1974
|
+
return true;
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
/** Stream an allowlisted model/texture or motion; only model callers enable caching. */
|
|
1978
|
+
function sendFile(req, res, target, root, modelCache = null) {
|
|
1906
1979
|
if (!isInside(root, target)) {
|
|
1907
1980
|
sendStatus(res, 404, 'file not found');
|
|
1908
1981
|
return;
|
|
@@ -1924,6 +1997,12 @@ function sendFile(req, res, target, root) {
|
|
|
1924
1997
|
return;
|
|
1925
1998
|
}
|
|
1926
1999
|
|
|
2000
|
+
let cacheHeaders = {};
|
|
2001
|
+
if (modelCache) {
|
|
2002
|
+
try { cacheHeaders = modelCacheHeaders(target, stats, modelCache.reload); }
|
|
2003
|
+
catch { sendStatus(res, 404, 'file not found'); return; }
|
|
2004
|
+
if (modelNotModified(req, res, cacheHeaders, modelCache.reload)) return;
|
|
2005
|
+
}
|
|
1927
2006
|
const extension = extname(target).toLowerCase();
|
|
1928
2007
|
const headers = {
|
|
1929
2008
|
'content-type': MIME_TYPES[extension] ?? 'application/octet-stream',
|
|
@@ -1931,10 +2010,11 @@ function sendFile(req, res, target, root) {
|
|
|
1931
2010
|
// The page is a different origin from this route in some shells, and the
|
|
1932
2011
|
// bytes are the user's own local files, so a permissive read is correct.
|
|
1933
2012
|
'access-control-allow-origin': '*',
|
|
1934
|
-
//
|
|
1935
|
-
//
|
|
2013
|
+
// Motion callers retain no-store; model/texture callers override this with
|
|
2014
|
+
// private storage and ETag validation so edited files still replace old bytes.
|
|
1936
2015
|
'cache-control': 'no-store',
|
|
1937
2016
|
'x-content-type-options': 'nosniff',
|
|
2017
|
+
...cacheHeaders,
|
|
1938
2018
|
};
|
|
1939
2019
|
|
|
1940
2020
|
try {
|
|
@@ -2026,7 +2106,7 @@ export function createModelInfoHandler(store) {
|
|
|
2026
2106
|
* through the server-held id map, so a crafted id cannot name a file outside it.
|
|
2027
2107
|
*/
|
|
2028
2108
|
/**
|
|
2029
|
-
* Unpacked model bytes, keyed by
|
|
2109
|
+
* Unpacked model bytes, keyed by file identity and metadata — see `modelCacheHeaders`.
|
|
2030
2110
|
*
|
|
2031
2111
|
* Bounded like the thumbnail cache, and for the same reason: a model is megabytes, so an unbounded map of them is a leak with
|
|
2032
2112
|
* a friendly name. Two or three entries cover switching between the models someone actually uses.
|
|
@@ -2059,6 +2139,8 @@ export function createAssetHandler(store) {
|
|
|
2059
2139
|
return;
|
|
2060
2140
|
}
|
|
2061
2141
|
}
|
|
2142
|
+
const reload = url.searchParams.has('v');
|
|
2143
|
+
if (!isInside(entry.root, target)) { sendStatus(res, 404, 'file not found'); return; }
|
|
2062
2144
|
/**
|
|
2063
2145
|
* A COMPRESSED model is unpacked HERE, the way a compressed clip is on the motion route: the renderer is always handed a
|
|
2064
2146
|
* plain VRM/GLB, so nothing downstream — the scene, the loaders, the bytes' own header check — has to know about brotli.
|
|
@@ -2075,8 +2157,15 @@ export function createAssetHandler(store) {
|
|
|
2075
2157
|
sendStatus(res, 404, `cannot read the model: ${error.message}`);
|
|
2076
2158
|
return;
|
|
2077
2159
|
}
|
|
2078
|
-
|
|
2079
|
-
|
|
2160
|
+
if (!stats.isFile()) { sendStatus(res, 404, 'not a file'); return; }
|
|
2161
|
+
if (stats.size > MAX_ASSET_BYTES) { sendStatus(res, 413, 'file exceeds the asset byte limit'); return; }
|
|
2162
|
+
let headers;
|
|
2163
|
+
try { headers = modelCacheHeaders(target, stats, reload); }
|
|
2164
|
+
catch { sendStatus(res, 404, 'file not found'); return; }
|
|
2165
|
+
// Revalidation needs neither a disk read nor Brotli decompression.
|
|
2166
|
+
if (modelNotModified(req, res, headers, reload)) return;
|
|
2167
|
+
const key = headers.etag;
|
|
2168
|
+
let plain = reload ? null : modelBytesCache.get(key);
|
|
2080
2169
|
if (!plain) {
|
|
2081
2170
|
try {
|
|
2082
2171
|
plain = decompressBrotli(readFileSync(target));
|
|
@@ -2087,11 +2176,12 @@ export function createAssetHandler(store) {
|
|
|
2087
2176
|
modelBytesCache.set(key, plain);
|
|
2088
2177
|
while (modelBytesCache.size > MODEL_BYTES_CACHE_LIMIT) modelBytesCache.delete(modelBytesCache.keys().next().value);
|
|
2089
2178
|
}
|
|
2090
|
-
res.writeHead(200, { 'content-type': 'model/gltf-binary', 'content-length': String(plain.byteLength) });
|
|
2091
|
-
res.end(
|
|
2179
|
+
res.writeHead(200, { ...headers, 'content-type': 'model/gltf-binary', 'content-length': String(plain.byteLength) });
|
|
2180
|
+
if (req.method === 'HEAD') { res.end(); return; }
|
|
2181
|
+
res.end(plain);
|
|
2092
2182
|
return;
|
|
2093
2183
|
}
|
|
2094
|
-
sendFile(req, res, target, entry.root);
|
|
2184
|
+
sendFile(req, res, target, entry.root, { reload });
|
|
2095
2185
|
};
|
|
2096
2186
|
}
|
|
2097
2187
|
|
|
@@ -2301,7 +2391,7 @@ export function apply(ctx, config = {}) {
|
|
|
2301
2391
|
const whale = resolveWhaleMark({
|
|
2302
2392
|
asar: envVar('DSH_ASAR') || undefined,
|
|
2303
2393
|
resourcesPath: process.resourcesPath,
|
|
2304
|
-
installRoot: findInstallRoot(dirname(process.execPath)),
|
|
2394
|
+
installRoot: findInstallRoot(dirname(process.execPath)) || installRootFromDshEntry(),
|
|
2305
2395
|
localFile: envVar('WHALE_FILE') || join(PLUGIN_ROOT, 'assets', 'whale-tail.apng'),
|
|
2306
2396
|
stateFile: join(importRootDir(), 'whale-tail.apng'),
|
|
2307
2397
|
log: (message) => ctx.logger?.warn?.(message),
|
|
@@ -3688,6 +3778,8 @@ export function apply(ctx, config = {}) {
|
|
|
3688
3778
|
* the note where it is read below.
|
|
3689
3779
|
*/
|
|
3690
3780
|
const sayHandler = (req, res) => {
|
|
3781
|
+
// GET is retained for older clients, but it can invoke a paid AI provider too.
|
|
3782
|
+
if (!allowWriteRequest(req, res)) return;
|
|
3691
3783
|
const run = (event, state, language, part, structure, chain, clipPrompt) => {
|
|
3692
3784
|
const box = speechBoxFor(event, state);
|
|
3693
3785
|
if (!box) {
|
|
@@ -3833,14 +3925,21 @@ export function apply(ctx, config = {}) {
|
|
|
3833
3925
|
};
|
|
3834
3926
|
|
|
3835
3927
|
/**
|
|
3836
|
-
*
|
|
3837
|
-
*
|
|
3838
|
-
*
|
|
3839
|
-
* header — the one the caller actually reached the plugin on.
|
|
3928
|
+
* Native windows run on THIS host. Reach the actual listener directly so they do not
|
|
3929
|
+
* depend on the remote browser's proxy login cookies or its externally terminated TLS.
|
|
3930
|
+
* Forwarded headers are deliberately not used as a native navigation target.
|
|
3840
3931
|
*/
|
|
3841
3932
|
const pageUrlFor = (req, route) => {
|
|
3842
|
-
const
|
|
3843
|
-
|
|
3933
|
+
const address = req?.socket?.localAddress;
|
|
3934
|
+
const port = req?.socket?.localPort;
|
|
3935
|
+
if (address && Number.isInteger(port) && port > 0 && port <= 65535) {
|
|
3936
|
+
const ip = address.startsWith('::ffff:') && /^\d+\.\d+\.\d+\.\d+$/.test(address.slice(7)) ? address.slice(7) : address;
|
|
3937
|
+
const host = ip.includes(':') ? `[${ip}]` : ip;
|
|
3938
|
+
try { return new URL(route || ROUTES.window, `${req.socket.encrypted ? 'https' : 'http'}://${host}:${port}`).href; }
|
|
3939
|
+
catch { return null; }
|
|
3940
|
+
}
|
|
3941
|
+
// A native window cannot infer a trusted listener from a public Host header.
|
|
3942
|
+
return null;
|
|
3844
3943
|
};
|
|
3845
3944
|
|
|
3846
3945
|
const desktopPageHandler = (req, res) => {
|
|
@@ -4101,7 +4200,7 @@ export function apply(ctx, config = {}) {
|
|
|
4101
4200
|
}
|
|
4102
4201
|
if (action === 'game') {
|
|
4103
4202
|
const gameUrl = pageUrlFor(req, ROUTES.game);
|
|
4104
|
-
if (!gameUrl) { sendStatus(res, 400, '
|
|
4203
|
+
if (!gameUrl) { sendStatus(res, 400, 'local listener address is unavailable'); return; }
|
|
4105
4204
|
const url = new URL(gameUrl);
|
|
4106
4205
|
if (typeof body.modelId === 'string') url.searchParams.set('model', body.modelId.slice(0, 256));
|
|
4107
4206
|
const opened = desktopWindow.openConfig({ url: url.href });
|
|
@@ -4157,7 +4256,7 @@ export function apply(ctx, config = {}) {
|
|
|
4157
4256
|
if (action === 'setup') {
|
|
4158
4257
|
const setupUrl = pageUrlFor(req, ROUTES.setup);
|
|
4159
4258
|
if (!setupUrl) {
|
|
4160
|
-
sendStatus(res, 400, '
|
|
4259
|
+
sendStatus(res, 400, 'local listener address is unavailable');
|
|
4161
4260
|
return;
|
|
4162
4261
|
}
|
|
4163
4262
|
const opened = desktopWindow.openConfig({ url: setupUrl });
|
|
@@ -4178,7 +4277,7 @@ export function apply(ctx, config = {}) {
|
|
|
4178
4277
|
*/
|
|
4179
4278
|
const pickerUrl = pageUrlFor(req, ROUTES.picker);
|
|
4180
4279
|
if (!pickerUrl) {
|
|
4181
|
-
sendStatus(res, 400, '
|
|
4280
|
+
sendStatus(res, 400, 'local listener address is unavailable');
|
|
4182
4281
|
return;
|
|
4183
4282
|
}
|
|
4184
4283
|
const opened = desktopWindow.openConfig({ url: pickerUrl });
|
|
@@ -4207,11 +4306,12 @@ export function apply(ctx, config = {}) {
|
|
|
4207
4306
|
// A model parameter window is ABOUT a model, so it carries one; the 通用设置 window has no model in its URL.
|
|
4208
4307
|
if (page === 'model' && wanted) query.push('model=' + encodeURIComponent(wanted));
|
|
4209
4308
|
const route = page === 'global' ? ROUTES.global : ROUTES.config;
|
|
4210
|
-
const
|
|
4211
|
-
if (!
|
|
4212
|
-
sendStatus(res, 400, '
|
|
4309
|
+
const settingsBase = pageUrlFor(req, route);
|
|
4310
|
+
if (!settingsBase) {
|
|
4311
|
+
sendStatus(res, 400, 'local listener address is unavailable');
|
|
4213
4312
|
return;
|
|
4214
4313
|
}
|
|
4314
|
+
const settingsUrl = settingsBase + (query.length ? '?' + query.join('&') : '');
|
|
4215
4315
|
const opened = desktopWindow.openConfig({ url: settingsUrl });
|
|
4216
4316
|
if (!opened.ok) {
|
|
4217
4317
|
sendStatus(res, 500, opened.error || 'the settings window could not be opened');
|
|
@@ -4227,7 +4327,7 @@ export function apply(ctx, config = {}) {
|
|
|
4227
4327
|
if (action === 'restart') desktopWindow.stop();
|
|
4228
4328
|
const url = pageUrlFor(req);
|
|
4229
4329
|
if (!url) {
|
|
4230
|
-
sendStatus(res, 400, '
|
|
4330
|
+
sendStatus(res, 400, 'local listener address is unavailable');
|
|
4231
4331
|
return;
|
|
4232
4332
|
}
|
|
4233
4333
|
/**
|
package/lib/mini-game-page.js
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
var focusClient = globalThis.crypto && crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(36) + Math.random().toString(36).slice(2);
|
|
40
40
|
function reportGameFocus(active, keepalive) {
|
|
41
41
|
if (testMode || stopped && active) return;
|
|
42
|
-
return fetch('/vrmpet3d/desktop', { method: 'POST', credentials: '
|
|
42
|
+
return fetch('/vrmpet3d/desktop', { method: 'POST', credentials: 'same-origin', keepalive: keepalive === true,
|
|
43
43
|
headers: { 'content-type': 'application/json' }, body: JSON.stringify({ action: 'game-focus', clientId: focusClient, active: active }) }).catch(function () {});
|
|
44
44
|
}
|
|
45
45
|
function fitLifeIcons() {
|
|
@@ -242,7 +242,7 @@
|
|
|
242
242
|
return opened;
|
|
243
243
|
}
|
|
244
244
|
fetch('/vrmpet3d/desktop', { method: 'POST', headers: { 'content-type': 'application/json' },
|
|
245
|
-
credentials: '
|
|
245
|
+
credentials: 'same-origin', body: JSON.stringify({ action: 'game-session', sessionId: target.id }) })
|
|
246
246
|
.then(function (r) { if (!r.ok) throw new Error('会话切换失败'); return r.json(); })
|
|
247
247
|
.then(function (body) { if (!body.ok) throw new Error('该会话已不再等待处理'); bridge.focusHost(); })
|
|
248
248
|
.catch(function (error) { $('connection').textContent = error.message + ',请重试。'; });
|
|
@@ -250,7 +250,7 @@
|
|
|
250
250
|
function poll() {
|
|
251
251
|
statusRequest = new AbortController();
|
|
252
252
|
var timeout = setTimeout(function () { statusRequest.abort(); }, 5000);
|
|
253
|
-
fetch('/vrmpet3d/state', { credentials: '
|
|
253
|
+
fetch('/vrmpet3d/state', { credentials: 'same-origin', cache: 'no-store', signal: statusRequest.signal }).then(function (r) {
|
|
254
254
|
if (!r.ok) throw new Error('status unavailable'); return r.json();
|
|
255
255
|
}).then(function (state) {
|
|
256
256
|
if (stopped) return;
|
|
@@ -557,9 +557,9 @@
|
|
|
557
557
|
if (restored) pause(true); frameId = requestAnimationFrame(frame);
|
|
558
558
|
// Snapshot the selected model; the game never writes model preferences or moves the pet's scene.
|
|
559
559
|
Promise.all([
|
|
560
|
-
fetch('/vrmpet3d/settings', { credentials: '
|
|
561
|
-
fetch('/vrmpet3d/catalog', { credentials: '
|
|
562
|
-
fetch('/vrmpet3d/motions', { credentials: '
|
|
560
|
+
fetch('/vrmpet3d/settings', { credentials: 'same-origin' }).then(function (r) { if (!r.ok) throw new Error('无法读取模型设置'); return r.json(); }),
|
|
561
|
+
fetch('/vrmpet3d/catalog', { credentials: 'same-origin' }).then(function (r) { if (!r.ok) throw new Error('无法读取模型列表'); return r.json(); }),
|
|
562
|
+
fetch('/vrmpet3d/motions', { credentials: 'same-origin' }).then(function (r) { if (!r.ok) throw new Error('无法读取分发的弹簧骨默认参数'); return r.json(); }),
|
|
563
563
|
]).then(function (values) {
|
|
564
564
|
if (stopped) return;
|
|
565
565
|
var prefs = values[0].preferences || {}, models = values[1].models || [];
|
package/lib/model-picker.js
CHANGED
|
@@ -427,7 +427,7 @@
|
|
|
427
427
|
if (!doFetch) return Promise.reject(new Error(text.noFetchUpload));
|
|
428
428
|
return doFetch(path, {
|
|
429
429
|
method: 'POST',
|
|
430
|
-
credentials: '
|
|
430
|
+
credentials: 'same-origin',
|
|
431
431
|
headers: { 'content-type': 'application/json' },
|
|
432
432
|
body: JSON.stringify(body),
|
|
433
433
|
}).then(function (response) {
|
|
@@ -617,7 +617,7 @@
|
|
|
617
617
|
onStatus(text.listFailed, 'warn');
|
|
618
618
|
return Promise.resolve(null);
|
|
619
619
|
}
|
|
620
|
-
return doFetch(catalogUrl, { credentials: '
|
|
620
|
+
return doFetch(catalogUrl, { credentials: 'same-origin' })
|
|
621
621
|
.then(function (response) { return response.ok ? response.json() : null; })
|
|
622
622
|
.then(function (payload) {
|
|
623
623
|
if (!apply(payload)) onStatus(text.listFailed, 'warn');
|
|
@@ -692,10 +692,10 @@
|
|
|
692
692
|
setBusy(true, text.importingFile(name));
|
|
693
693
|
return doFetch(uploadUrl + '?file=' + encodeURIComponent(name), {
|
|
694
694
|
method: 'POST',
|
|
695
|
-
credentials: '
|
|
695
|
+
credentials: 'same-origin',
|
|
696
696
|
// The extension decided what this is; the CONTENT TYPE is what tells the host to read a raw body
|
|
697
697
|
// rather than JSON, so it is stated even when the browser has no type for the file.
|
|
698
|
-
headers: { 'content-type':
|
|
698
|
+
headers: { 'content-type': 'application/octet-stream' },
|
|
699
699
|
body: file,
|
|
700
700
|
}).then(function (response) {
|
|
701
701
|
if (!response.ok) {
|
package/lib/scene.js
CHANGED
|
@@ -2052,11 +2052,12 @@
|
|
|
2052
2052
|
* repeated GET of the same URL without touching the disk — which would make 重载模型 look like it did nothing. The
|
|
2053
2053
|
* one-shot buster from `bustModelCache` rides the query string, and is consumed here so an ordinary load stays clean.
|
|
2054
2054
|
*/
|
|
2055
|
-
|
|
2055
|
+
var reload = Boolean(this.modelBust);
|
|
2056
|
+
if (reload) {
|
|
2056
2057
|
url += (url.indexOf('?') === -1 ? '?' : '&') + 'v=' + this.modelBust;
|
|
2057
2058
|
this.modelBust = null;
|
|
2058
2059
|
}
|
|
2059
|
-
var promise = fetch(url, { credentials: '
|
|
2060
|
+
var promise = fetch(url, { credentials: 'same-origin', cache: reload ? 'reload' : 'default' })
|
|
2060
2061
|
.then(function (response) {
|
|
2061
2062
|
if (!response.ok) throw new Error('asset route returned HTTP ' + response.status);
|
|
2062
2063
|
return response.arrayBuffer();
|
package/lib/setup-page.js
CHANGED
|
@@ -301,7 +301,7 @@
|
|
|
301
301
|
function post(path, body) {
|
|
302
302
|
return fetch(path, {
|
|
303
303
|
method: 'POST',
|
|
304
|
-
credentials: '
|
|
304
|
+
credentials: 'same-origin',
|
|
305
305
|
headers: { 'content-type': 'application/json' },
|
|
306
306
|
body: JSON.stringify(body),
|
|
307
307
|
}).then(function (response) {
|
|
@@ -315,7 +315,7 @@
|
|
|
315
315
|
}
|
|
316
316
|
|
|
317
317
|
function getJson(path) {
|
|
318
|
-
return fetch(path, { credentials: '
|
|
318
|
+
return fetch(path, { credentials: 'same-origin' })
|
|
319
319
|
.then(function (response) { return response.ok ? response.json() : null; })
|
|
320
320
|
.catch(function () { return null; });
|
|
321
321
|
}
|
|
@@ -517,7 +517,7 @@
|
|
|
517
517
|
function renderRuntimeNote() {
|
|
518
518
|
var line = el('setup-runtime-path');
|
|
519
519
|
if (!line || typeof fetch !== 'function') return;
|
|
520
|
-
fetch('/vrmpet3d/desktop', { credentials: '
|
|
520
|
+
fetch('/vrmpet3d/desktop', { credentials: 'same-origin' })
|
|
521
521
|
.then(function (response) { return response.ok ? response.json() : null; })
|
|
522
522
|
.then(function (status) {
|
|
523
523
|
var dir = status && typeof status.runtimeDir === 'string' ? status.runtimeDir : '';
|