dsh-plugin-vrmpet3d 0.1.1 → 0.1.2
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/main.js +2 -1
- package/desktop/preload.js +1 -0
- package/lib/client.core.js +279 -41
- package/lib/client.js +114 -16
- package/lib/client.menu.js +14 -4
- package/lib/client.scene.js +134 -7
- package/lib/config-page.html +11 -0
- package/lib/config-page.js +153 -25
- package/lib/core.js +279 -41
- package/lib/desktop-page.js +158 -40
- package/lib/desktop.js +18 -1
- package/lib/global-page.html +34 -4
- package/lib/index.js +46 -25
- package/lib/menu.js +14 -4
- package/lib/scene.js +134 -7
- package/lib/vrm-info.js +16 -1
- package/package.json +1 -1
package/lib/client.core.js
CHANGED
|
@@ -1187,9 +1187,8 @@
|
|
|
1187
1187
|
}
|
|
1188
1188
|
|
|
1189
1189
|
/**
|
|
1190
|
-
*
|
|
1191
|
-
*
|
|
1192
|
-
* same thing in the floating window (the screen's width) and in the app overlay (its window's width).
|
|
1190
|
+
* A positive setting overrides automatic distance. Both axes and both surfaces
|
|
1191
|
+
* pass the screen width here; automatic distance is half of that width.
|
|
1193
1192
|
*/
|
|
1194
1193
|
function resolveGazeSpan(span, fallback) {
|
|
1195
1194
|
var wanted = Number(span);
|
|
@@ -1197,7 +1196,7 @@
|
|
|
1197
1196
|
return clamp(wanted, GAZE_SPAN_RANGE.min, GAZE_SPAN_RANGE.max);
|
|
1198
1197
|
}
|
|
1199
1198
|
var base = Number(fallback);
|
|
1200
|
-
return isFiniteNumber(base) && base > 0 ? base : GAZE_SPAN.x;
|
|
1199
|
+
return isFiniteNumber(base) && base > 0 ? base / 2 : GAZE_SPAN.x;
|
|
1201
1200
|
}
|
|
1202
1201
|
|
|
1203
1202
|
/**
|
|
@@ -1219,7 +1218,9 @@
|
|
|
1219
1218
|
* Pure numbers, no DOM, so the logic suite covers the clamping and the signs.
|
|
1220
1219
|
*
|
|
1221
1220
|
* @param {{pointerX:number, pointerY:number, centreX:number, centreY:number,
|
|
1222
|
-
* spanX?:number, spanY?:number, curve?:number
|
|
1221
|
+
* spanX?:number, spanY?:number, curve?:number, curveY?:number,
|
|
1222
|
+
* curveRadial?:number, curveSpatial?:number, distance?:number,
|
|
1223
|
+
* mode?:'axes'|'radial'|'spatial'}} input
|
|
1223
1224
|
* @returns {{x:number, y:number}|null} `null` when the input is unusable; `y` is
|
|
1224
1225
|
* positive BELOW the pet, i.e. in screen coordinates
|
|
1225
1226
|
*/
|
|
@@ -1238,12 +1239,39 @@
|
|
|
1238
1239
|
: GAZE_SPAN.y;
|
|
1239
1240
|
// The settings' curve, unless the caller passes its own (the suite does, to pin a shape).
|
|
1240
1241
|
var edgeSlope = isFiniteNumber(Number(input.curve)) ? Number(input.curve) : DEFAULTS.gazeCurve;
|
|
1242
|
+
var slopeY = isFiniteNumber(Number(input.curveY)) ? Number(input.curveY) : edgeSlope;
|
|
1243
|
+
if (input.mode === 'spatial') {
|
|
1244
|
+
var spatialX = (pointerX - centreX) / spanX;
|
|
1245
|
+
var spatialY = (pointerY - centreY) / spanY;
|
|
1246
|
+
if (!isFiniteNumber(spatialX) || !isFiniteNumber(spatialY)) return null;
|
|
1247
|
+
var depth = isFiniteNumber(Number(input.distance)) && Number(input.distance) > 0
|
|
1248
|
+
? clamp(Number(input.distance), 0.1, 5) : DEFAULTS.gazeDistance;
|
|
1249
|
+
// A virtual target plane in span units: sideways distance reduces pitch
|
|
1250
|
+
// at a fixed pointer height. Curves shape angles, rather than pixel offsets.
|
|
1251
|
+
var yawAngle = Math.atan2(spatialX, depth) / (Math.PI / 2);
|
|
1252
|
+
var pitchAngle = Math.atan2(spatialY, Math.hypot(spatialX, depth)) / (Math.PI / 2);
|
|
1253
|
+
var spatialSlope = isFiniteNumber(Number(input.curveSpatial)) ? Number(input.curveSpatial) : DEFAULTS.gazeCurveSpatial;
|
|
1254
|
+
return {
|
|
1255
|
+
x: (yawAngle < 0 ? -1 : 1) * gazeCurve(Math.abs(yawAngle), spatialSlope),
|
|
1256
|
+
y: (pitchAngle < 0 ? -1 : 1) * gazeCurve(Math.abs(pitchAngle), spatialSlope),
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
if (input.mode === 'radial') {
|
|
1260
|
+
var dx = (pointerX - centreX) / spanX;
|
|
1261
|
+
var dy = (pointerY - centreY) / spanY;
|
|
1262
|
+
var distance = Math.hypot(dx, dy);
|
|
1263
|
+
if (!isFiniteNumber(distance)) return null;
|
|
1264
|
+
if (distance === 0) return { x: 0, y: 0 };
|
|
1265
|
+
var radialSlope = isFiniteNumber(Number(input.curveRadial)) ? Number(input.curveRadial) : edgeSlope;
|
|
1266
|
+
var gain = gazeCurve(Math.min(1, distance), radialSlope) / distance;
|
|
1267
|
+
return { x: dx * gain, y: dy * gain };
|
|
1268
|
+
}
|
|
1241
1269
|
var rawX = clamp((pointerX - centreX) / spanX, -1, 1);
|
|
1242
1270
|
var rawY = clamp((pointerY - centreY) / spanY, -1, 1);
|
|
1243
1271
|
// Mirrored through `gazeCurve` so the sign survives and the response is the curve on both sides.
|
|
1244
1272
|
return {
|
|
1245
1273
|
x: rawX < 0 ? -gazeCurve(-rawX, edgeSlope) : gazeCurve(rawX, edgeSlope),
|
|
1246
|
-
y: rawY < 0 ? -gazeCurve(-rawY,
|
|
1274
|
+
y: rawY < 0 ? -gazeCurve(-rawY, slopeY) : gazeCurve(rawY, slopeY)
|
|
1247
1275
|
};
|
|
1248
1276
|
}
|
|
1249
1277
|
|
|
@@ -1356,12 +1384,21 @@
|
|
|
1356
1384
|
* operator's own (2026-10-13: 「视线:曲率设置默认为0.1,提供0.05-1的选择(原先是0.1-1),静止停看默认为2秒」).
|
|
1357
1385
|
* `gazeCurve` is the curve's slope at the frame's edge — 1 is a straight line, smaller is a
|
|
1358
1386
|
* steeper start and a flatter tail — and `gazeSpanX`/`gazeSpanY` are the full-deflection
|
|
1359
|
-
* distances in CSS pixels, where **0 means AUTO**:
|
|
1360
|
-
*
|
|
1387
|
+
* distances in CSS pixels, where **0 means AUTO**: half the screen width on both axes.
|
|
1388
|
+
* `gazeCurve` / `gazeCurveY` remain readable for older delivered clients; the current
|
|
1389
|
+
* settings expose only spatial (default) and radial modes.
|
|
1361
1390
|
*/
|
|
1362
1391
|
gazeCurve: 0.1,
|
|
1392
|
+
gazeCurveMode: 'spatial',
|
|
1393
|
+
gazeCurveY: 0.1,
|
|
1394
|
+
gazeCurveRadial: 0.1,
|
|
1395
|
+
gazeCurveSpatial: 1,
|
|
1396
|
+
gazeDistance: 0.1,
|
|
1363
1397
|
gazeSpanX: 0,
|
|
1364
1398
|
gazeSpanY: 0,
|
|
1399
|
+
// Actual head pitch in degrees, after the renderer's gaze weight.
|
|
1400
|
+
gazePitchUp: 60,
|
|
1401
|
+
gazePitchDown: 60,
|
|
1365
1402
|
/**
|
|
1366
1403
|
* How long the cursor may sit still before the pet stops following it, in seconds.
|
|
1367
1404
|
*
|
|
@@ -1473,6 +1510,7 @@
|
|
|
1473
1510
|
* not silently pin drag and gravity to the numbers the panel happened to be showing.
|
|
1474
1511
|
*/
|
|
1475
1512
|
springs: {},
|
|
1513
|
+
toon: {},
|
|
1476
1514
|
/**
|
|
1477
1515
|
* Per-model size multiplier for BOTH texts: `{ [modelId]: number }`.
|
|
1478
1516
|
*
|
|
@@ -2207,12 +2245,25 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
2207
2245
|
out.gazeCurve = Number.isFinite(Number(source.gazeCurve))
|
|
2208
2246
|
? clamp(Number(source.gazeCurve), GAZE_CURVE_RANGE.min, GAZE_CURVE_RANGE.max)
|
|
2209
2247
|
: DEFAULTS.gazeCurve;
|
|
2248
|
+
out.gazeCurveMode = source.gazeCurveMode === 'radial' ? 'radial' : 'spatial';
|
|
2249
|
+
['gazeCurveY', 'gazeCurveRadial'].forEach(function (key) {
|
|
2250
|
+
out[key] = typeof source[key] === 'number' && Number.isFinite(source[key])
|
|
2251
|
+
? clamp(source[key], GAZE_CURVE_RANGE.min, GAZE_CURVE_RANGE.max) : DEFAULTS[key];
|
|
2252
|
+
});
|
|
2253
|
+
out.gazeCurveSpatial = typeof source.gazeCurveSpatial === 'number' && Number.isFinite(source.gazeCurveSpatial)
|
|
2254
|
+
? clamp(source.gazeCurveSpatial, GAZE_CURVE_RANGE.min, GAZE_CURVE_RANGE.max) : DEFAULTS.gazeCurveSpatial;
|
|
2255
|
+
out.gazeDistance = typeof source.gazeDistance === 'number' && Number.isFinite(source.gazeDistance)
|
|
2256
|
+
? clamp(source.gazeDistance, 0.1, 5) : DEFAULTS.gazeDistance;
|
|
2210
2257
|
out.gazeSpanX = Number.isFinite(Number(source.gazeSpanX))
|
|
2211
2258
|
? clamp(Number(source.gazeSpanX), GAZE_SPAN_RANGE.min, GAZE_SPAN_RANGE.max)
|
|
2212
2259
|
: DEFAULTS.gazeSpanX;
|
|
2213
2260
|
out.gazeSpanY = Number.isFinite(Number(source.gazeSpanY))
|
|
2214
2261
|
? clamp(Number(source.gazeSpanY), GAZE_SPAN_RANGE.min, GAZE_SPAN_RANGE.max)
|
|
2215
2262
|
: DEFAULTS.gazeSpanY;
|
|
2263
|
+
['gazePitchUp', 'gazePitchDown'].forEach(function (key) {
|
|
2264
|
+
out[key] = typeof source[key] === 'number' && Number.isFinite(source[key])
|
|
2265
|
+
? clamp(source[key], 0, 60) : DEFAULTS[key];
|
|
2266
|
+
});
|
|
2216
2267
|
// 0 is a legitimate value here — it turns the idle rule off — so this cannot fall back to the default the
|
|
2217
2268
|
// way an unparseable value does. A negative number is unparseable for this purpose and becomes 0.
|
|
2218
2269
|
out.gazeIdleSeconds = Number.isFinite(Number(source.gazeIdleSeconds))
|
|
@@ -2267,6 +2318,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
2267
2318
|
? source.expressions.emotion.parts
|
|
2268
2319
|
: null);
|
|
2269
2320
|
out.springs = cleanSprings(source.springs);
|
|
2321
|
+
out.toon = cleanToon(source.toon);
|
|
2270
2322
|
return out;
|
|
2271
2323
|
}
|
|
2272
2324
|
|
|
@@ -2277,6 +2329,25 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
2277
2329
|
* directory's manifest says `loop: false`) has to be OVERRIDABLE back to looping, and "no entry" cannot
|
|
2278
2330
|
* express that — absence means "ask the catalog".
|
|
2279
2331
|
*/
|
|
2332
|
+
function cleanToon(raw) {
|
|
2333
|
+
var out = {};
|
|
2334
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return out;
|
|
2335
|
+
Object.keys(raw).slice(0, 256).forEach(function (id) {
|
|
2336
|
+
if (id === '__proto__' || id === 'constructor' || id === 'prototype') return;
|
|
2337
|
+
var entry = raw[id];
|
|
2338
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return;
|
|
2339
|
+
var next = {};
|
|
2340
|
+
if (typeof entry.enabled === 'boolean') next.enabled = entry.enabled;
|
|
2341
|
+
var ranges = { shadingToonyFactor: [0, 1], shadingShiftFactor: [-1, 1], shadeStrength: [0, 1], outlineScale: [0, 3] };
|
|
2342
|
+
Object.keys(ranges).forEach(function (key) {
|
|
2343
|
+
var value = entry[key];
|
|
2344
|
+
if (typeof value === 'number' && Number.isFinite(value)) next[key] = clamp(value, ranges[key][0], ranges[key][1]);
|
|
2345
|
+
});
|
|
2346
|
+
if (Object.keys(next).length) out[id] = next;
|
|
2347
|
+
});
|
|
2348
|
+
return out;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2280
2351
|
function cleanMotionLoop(raw) {
|
|
2281
2352
|
var out = {};
|
|
2282
2353
|
var source = raw && typeof raw === 'object' ? raw : {};
|
|
@@ -3560,6 +3631,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
3560
3631
|
* @returns {string|null} the line to show, or null for silence
|
|
3561
3632
|
*/
|
|
3562
3633
|
SpeechMachine.prototype.observe = function (preferences, state, pendingKind, language, random, motion) {
|
|
3634
|
+
if (state === 'dancing') {
|
|
3635
|
+
this.current = null;
|
|
3636
|
+
this.wantsAi = null;
|
|
3637
|
+
this.lastEvent = null;
|
|
3638
|
+
this.idleAt = null;
|
|
3639
|
+
this.chatterAt = null;
|
|
3640
|
+
return null;
|
|
3641
|
+
}
|
|
3563
3642
|
var speech = cleanSpeech(preferences && preferences.speech);
|
|
3564
3643
|
var t = this.now();
|
|
3565
3644
|
if (!speech.enabled) {
|
|
@@ -3824,7 +3903,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
3824
3903
|
*
|
|
3825
3904
|
* @returns {{state: string, text: string, tag: string, animation: string, seconds: number}|null}
|
|
3826
3905
|
*/
|
|
3827
|
-
function statusFor(preferences, state, enteredAt, now, clock, language) {
|
|
3906
|
+
function statusFor(preferences, state, enteredAt, now, clock, language, completedCount) {
|
|
3828
3907
|
if (STATUS_STATES.indexOf(state) === -1) return null;
|
|
3829
3908
|
var entry = statusEntryFor(preferences, state, language);
|
|
3830
3909
|
if (!entry.text && !entry.tag) return null;
|
|
@@ -3881,6 +3960,9 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
3881
3960
|
? durationClauseRemoved(text)
|
|
3882
3961
|
: text.split(STATUS_DURATION_TOKEN).join(formatRunDuration(elapsed, language));
|
|
3883
3962
|
}
|
|
3963
|
+
if (state === 'done' && Number(completedCount) > 1 && text) {
|
|
3964
|
+
text = language === 'en' ? (text === 'Task complete' ? 'All tasks complete' : 'All ' + text) : '全部' + text;
|
|
3965
|
+
}
|
|
3884
3966
|
var look = statusTone(state);
|
|
3885
3967
|
return {
|
|
3886
3968
|
state: state,
|
|
@@ -4914,6 +4996,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
4914
4996
|
* @param {object} options url, intervalMs, fetchImpl, onPreview(preview|null), onUnsupported
|
|
4915
4997
|
*/
|
|
4916
4998
|
function watchHostPreview(options) {
|
|
4999
|
+
var navigationSeen = null;
|
|
4917
5000
|
var settings = options || {};
|
|
4918
5001
|
var url = settings.url || '/vrmpet3d/preview';
|
|
4919
5002
|
// Four hundred milliseconds: this drives a hover preview, so it has to feel immediate, and the route is a
|
|
@@ -4922,11 +5005,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
4922
5005
|
var doFetch = settings.fetchImpl || (typeof fetch === 'function' ? fetch : null);
|
|
4923
5006
|
var timer = null;
|
|
4924
5007
|
var seen = -1;
|
|
5008
|
+
var issued = 0;
|
|
5009
|
+
var delivered = 0;
|
|
4925
5010
|
var supported = Boolean(doFetch);
|
|
4926
5011
|
var stopped = false;
|
|
4927
5012
|
|
|
4928
5013
|
function load() {
|
|
4929
5014
|
if (!supported || stopped) return Promise.resolve(null);
|
|
5015
|
+
var request = ++issued;
|
|
4930
5016
|
return doFetch(url, { credentials: 'omit' })
|
|
4931
5017
|
.then(function (response) {
|
|
4932
5018
|
if (response.status === 404 || response.status === 405) {
|
|
@@ -4938,6 +5024,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
4938
5024
|
return response.json ? response.json() : null;
|
|
4939
5025
|
})
|
|
4940
5026
|
.then(function (body) {
|
|
5027
|
+
if (request < delivered) return null;
|
|
5028
|
+
if (body && typeof body.seq === 'number') delivered = request;
|
|
5029
|
+
if (body && body.navigation && typeof body.navigation.seq === 'number') {
|
|
5030
|
+
var navigation = body.navigation;
|
|
5031
|
+
if (navigationSeen !== null && navigationSeen !== navigation.seq && typeof navigation.sessionId === 'string'
|
|
5032
|
+
&& typeof settings.onNavigate === 'function') settings.onNavigate(navigation.sessionId);
|
|
5033
|
+
navigationSeen = navigation.seq;
|
|
5034
|
+
}
|
|
4941
5035
|
if (!body || typeof body.seq !== 'number' || body.seq === seen) return null;
|
|
4942
5036
|
seen = body.seq;
|
|
4943
5037
|
var preview = cleanPreview(body.preview);
|
|
@@ -5075,11 +5169,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5075
5169
|
var doFetch = settings.fetchImpl || (typeof fetch === 'function' ? fetch : null);
|
|
5076
5170
|
var timer = null;
|
|
5077
5171
|
var seen = -1;
|
|
5172
|
+
var issued = 0;
|
|
5173
|
+
var delivered = 0;
|
|
5078
5174
|
var supported = Boolean(doFetch);
|
|
5079
5175
|
var stopped = false;
|
|
5080
5176
|
|
|
5081
5177
|
function load() {
|
|
5082
5178
|
if (!supported || stopped) return Promise.resolve(null);
|
|
5179
|
+
var request = ++issued;
|
|
5083
5180
|
return doFetch(url, { credentials: 'omit' })
|
|
5084
5181
|
.then(function (response) {
|
|
5085
5182
|
if (response.status === 404 || response.status === 405) {
|
|
@@ -5091,6 +5188,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5091
5188
|
return response.json ? response.json() : null;
|
|
5092
5189
|
})
|
|
5093
5190
|
.then(function (body) {
|
|
5191
|
+
if (request < delivered) return null;
|
|
5192
|
+
if (body && typeof body.seq === 'number') delivered = request;
|
|
5094
5193
|
if (!body || typeof body.seq !== 'number' || body.seq === seen) return null;
|
|
5095
5194
|
seen = body.seq;
|
|
5096
5195
|
var state = STATES.indexOf(body.state) === -1 ? null : body.state;
|
|
@@ -5200,6 +5299,12 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5200
5299
|
}
|
|
5201
5300
|
if (typeof why.scope === 'string' && why.scope) out.scope = why.scope.slice(0, 16);
|
|
5202
5301
|
if (typeof why.session === 'string' && why.session) out.session = why.session.slice(0, 64);
|
|
5302
|
+
if (typeof why.statusSessionId === 'string' && why.statusSessionId) out.statusSessionId = why.statusSessionId.slice(0, 64);
|
|
5303
|
+
if (typeof why.completedSessionId === 'string' && why.completedSessionId) out.completedSessionId = why.completedSessionId.slice(0, 64);
|
|
5304
|
+
['waitingSessionIds', 'completedSessionIds'].forEach(function (key) {
|
|
5305
|
+
if (Array.isArray(why[key])) out[key] = why[key].filter(function (id) { return typeof id === 'string' && id; })
|
|
5306
|
+
.slice(0, 256).map(function (id) { return id.slice(0, 64); });
|
|
5307
|
+
});
|
|
5203
5308
|
if (typeof why.interaction === 'string' && why.interaction) out.interaction = why.interaction.slice(0, 16);
|
|
5204
5309
|
if (Array.isArray(why.runningIds)) {
|
|
5205
5310
|
out.runningIds = why.runningIds
|
|
@@ -5357,15 +5462,19 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5357
5462
|
* its 进行中 line has to travel with the state. A whole number, or nothing — an older host that does not send it
|
|
5358
5463
|
* leaves the window's dot as it was rather than inventing a count.
|
|
5359
5464
|
*/
|
|
5360
|
-
var nextUnread = Number.isFinite(
|
|
5465
|
+
var nextUnread = typeof unread === 'number' && Number.isFinite(unread) && unread >= 0 ? Math.floor(unread) : null;
|
|
5361
5466
|
/**
|
|
5362
5467
|
* The diagnostic, kept OUT of the change key on purpose: it carries counts and clock readings that move
|
|
5363
5468
|
* constantly, and letting them force a report would turn "the pet announces a change" into a 1 Hz feed. It
|
|
5364
5469
|
* rides along with the reports the state and the line already earn, and the heartbeat re-states the last one.
|
|
5365
5470
|
*/
|
|
5366
5471
|
var nextWhy = cleanWhy(why);
|
|
5472
|
+
var navigationKey = function (value) {
|
|
5473
|
+
return JSON.stringify(value ? [value.waitingSessionIds || [], value.completedSessionIds || [],
|
|
5474
|
+
value.statusSessionId || null, value.completedSessionId || null] : [[], [], null, null]);
|
|
5475
|
+
};
|
|
5367
5476
|
if (force !== true && next === sent && nextLine === sentLine && nextTouchKey === sentTouchKey
|
|
5368
|
-
&& nextClockKey === sentClockKey && nextUnread === sentUnread) {
|
|
5477
|
+
&& nextClockKey === sentClockKey && nextUnread === sentUnread && navigationKey(nextWhy) === navigationKey(sentWhy)) {
|
|
5369
5478
|
return Promise.resolve(null);
|
|
5370
5479
|
}
|
|
5371
5480
|
sent = next;
|
|
@@ -5490,10 +5599,15 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5490
5599
|
var doFetch = settings.fetchImpl || (typeof fetch === 'function' ? fetch : null);
|
|
5491
5600
|
var timer = null;
|
|
5492
5601
|
var seen = -1;
|
|
5602
|
+
var issued = 0;
|
|
5603
|
+
var delivered = 0;
|
|
5493
5604
|
var supported = Boolean(doFetch);
|
|
5494
5605
|
var stopped = false;
|
|
5495
5606
|
|
|
5496
|
-
function deliver(snapshot) {
|
|
5607
|
+
function deliver(snapshot, request) {
|
|
5608
|
+
// A slow poll/write must not roll a live gesture back to an older reply.
|
|
5609
|
+
if (request < delivered) return null;
|
|
5610
|
+
if (snapshot && typeof snapshot.version === 'number') delivered = request;
|
|
5497
5611
|
if (!snapshot || typeof snapshot.version !== 'number' || snapshot.version === seen) return null;
|
|
5498
5612
|
seen = snapshot.version;
|
|
5499
5613
|
if (typeof settings.onSnapshot === 'function') settings.onSnapshot(snapshot);
|
|
@@ -5503,6 +5617,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5503
5617
|
/** One look at the host. Returns a promise for the snapshot it delivered, if any. */
|
|
5504
5618
|
function load() {
|
|
5505
5619
|
if (!supported || stopped) return Promise.resolve(null);
|
|
5620
|
+
var request = ++issued;
|
|
5506
5621
|
return doFetch(url, { credentials: 'omit' })
|
|
5507
5622
|
.then(function (response) {
|
|
5508
5623
|
if (response.status === 404 || response.status === 405) {
|
|
@@ -5514,7 +5629,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5514
5629
|
if (!response.ok) return null;
|
|
5515
5630
|
return response.json ? response.json() : null;
|
|
5516
5631
|
})
|
|
5517
|
-
.then(deliver)
|
|
5632
|
+
.then(function (snapshot) { return deliver(snapshot, request); })
|
|
5518
5633
|
.catch(function () { return null; });
|
|
5519
5634
|
}
|
|
5520
5635
|
|
|
@@ -5526,6 +5641,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5526
5641
|
*/
|
|
5527
5642
|
function push(body) {
|
|
5528
5643
|
if (!supported || stopped) return Promise.resolve(null);
|
|
5644
|
+
var request = ++issued;
|
|
5529
5645
|
return doFetch(url, {
|
|
5530
5646
|
method: 'POST',
|
|
5531
5647
|
credentials: 'omit',
|
|
@@ -5533,7 +5649,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5533
5649
|
body: JSON.stringify(body || {}),
|
|
5534
5650
|
})
|
|
5535
5651
|
.then(function (response) { return response.ok ? response.json() : null; })
|
|
5536
|
-
.then(deliver)
|
|
5652
|
+
.then(function (snapshot) { return deliver(snapshot, request); })
|
|
5537
5653
|
.catch(function () { return null; });
|
|
5538
5654
|
}
|
|
5539
5655
|
|
|
@@ -5746,6 +5862,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5746
5862
|
* sits above the watermark and is adopted as usual.
|
|
5747
5863
|
*/
|
|
5748
5864
|
this.dismissedNoticeAt = 0;
|
|
5865
|
+
this.dismissedSessions = new Map();
|
|
5749
5866
|
/**
|
|
5750
5867
|
* The TURN the pet is currently describing, and the one that just ended.
|
|
5751
5868
|
*
|
|
@@ -6028,6 +6145,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6028
6145
|
PetMachine.prototype.dismissNotice = function (keepDebt) {
|
|
6029
6146
|
if (this.notice && Number.isFinite(this.notice.at)) {
|
|
6030
6147
|
this.dismissedNoticeAt = Math.max(this.dismissedNoticeAt, this.notice.at);
|
|
6148
|
+
if (this.notice.sessionId) this.dismissedSessions.set(this.notice.sessionId, this.notice.at);
|
|
6031
6149
|
}
|
|
6032
6150
|
if (keepDebt !== true && this.notice && this.notice.sessionId) {
|
|
6033
6151
|
delete this.retractions[this.notice.sessionId];
|
|
@@ -6150,7 +6268,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6150
6268
|
* well as in the acknowledgement rule in `resolve`. (The floating window never takes focus, which is why the same
|
|
6151
6269
|
* drag could not do this there.)
|
|
6152
6270
|
*/
|
|
6153
|
-
if (this.notice &&
|
|
6271
|
+
if (this.notice && !this.notice.sessionId && !presenceKnown && this.interaction === null) {
|
|
6154
6272
|
this.noteDismissal('focus-return');
|
|
6155
6273
|
this.dismissNotice();
|
|
6156
6274
|
}
|
|
@@ -6262,6 +6380,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6262
6380
|
// reports as its own `at`, so the minimum hold starts when the user can first see it.
|
|
6263
6381
|
endedAt: at,
|
|
6264
6382
|
settledAt: this.now(),
|
|
6383
|
+
confirmed: payload.confirmed === true,
|
|
6265
6384
|
sessionId: sessionId,
|
|
6266
6385
|
};
|
|
6267
6386
|
}
|
|
@@ -6287,8 +6406,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6287
6406
|
* than a startup that has not finished.
|
|
6288
6407
|
*/
|
|
6289
6408
|
if (snap.connected === true) this.everConnected = true;
|
|
6290
|
-
var busy = snap.
|
|
6409
|
+
var busy = snap.pending === true || (snap.available === true && snap.running === true);
|
|
6291
6410
|
if (busy) this.lastBusyAt = t;
|
|
6411
|
+
var viewedCompletions = Array.isArray(snap.viewedCompletions) ? snap.viewedCompletions : [];
|
|
6412
|
+
if (this.pendingNotice && viewedCompletions.indexOf(this.pendingNotice.sessionId) !== -1) this.pendingNotice = undefined;
|
|
6413
|
+
if (this.notice && viewedCompletions.indexOf(this.notice.sessionId) !== -1) {
|
|
6414
|
+
this.noteDismissal('acknowledged');
|
|
6415
|
+
this.dismissNotice();
|
|
6416
|
+
}
|
|
6292
6417
|
|
|
6293
6418
|
/**
|
|
6294
6419
|
* Adopt the SNAPSHOT's own notice when it is newer than this machine's.
|
|
@@ -6307,9 +6432,11 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6307
6432
|
* what the aggregate watches, and it falls between the rounds of one task exactly like a turn does.
|
|
6308
6433
|
*/
|
|
6309
6434
|
if (snap.notice && typeof snap.notice.at === 'number'
|
|
6310
|
-
&& snap.notice.
|
|
6311
|
-
|
|
6312
|
-
|
|
6435
|
+
&& (snap.notice.queued === true ? (!this.notice && !this.pendingNotice
|
|
6436
|
+
&& (!this.dismissedSessions.has(snap.notice.sessionId) || snap.notice.at > this.dismissedSessions.get(snap.notice.sessionId)))
|
|
6437
|
+
: (snap.notice.at > this.dismissedNoticeAt
|
|
6438
|
+
&& (!this.notice || snap.notice.at > this.notice.at)
|
|
6439
|
+
&& (!this.pendingNotice || snap.notice.at > this.pendingNotice.endedAt)))) {
|
|
6313
6440
|
var snapSession = typeof snap.notice.sessionId === 'string' && snap.notice.sessionId ? snap.notice.sessionId : null;
|
|
6314
6441
|
if (snap.notice.reason === 'error') {
|
|
6315
6442
|
// A failure is not a completion and is never settled: it is shown at once, exactly as a `turn/end` failure is.
|
|
@@ -6319,7 +6446,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6319
6446
|
this.pendingNotice = {
|
|
6320
6447
|
reason: snap.notice.reason,
|
|
6321
6448
|
endedAt: snap.notice.at,
|
|
6322
|
-
settledAt: t,
|
|
6449
|
+
settledAt: snap.notice.queued === true ? snap.notice.at : t,
|
|
6450
|
+
confirmed: snap.notice.confirmed === true,
|
|
6323
6451
|
// The session the notice is ABOUT. Without it there is nothing to come back to, and the notice falls
|
|
6324
6452
|
// back to the older focus/timer rules below.
|
|
6325
6453
|
sessionId: snapSession,
|
|
@@ -6335,6 +6463,9 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6335
6463
|
* (a `turn/end` frame and the store's falling edge) are decided together.
|
|
6336
6464
|
*/
|
|
6337
6465
|
if (this.pendingNotice) {
|
|
6466
|
+
if (snap.notice && snap.notice.sessionId === this.pendingNotice.sessionId && snap.notice.confirmed === true) {
|
|
6467
|
+
this.pendingNotice.confirmed = true;
|
|
6468
|
+
}
|
|
6338
6469
|
var pendingSession = this.pendingNotice.sessionId;
|
|
6339
6470
|
/**
|
|
6340
6471
|
* Cancelled by WORK RESUMING, and only that — the ordering is the whole point.
|
|
@@ -6357,7 +6488,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6357
6488
|
* never produced one.
|
|
6358
6489
|
*/
|
|
6359
6490
|
this.pendingNotice = undefined;
|
|
6360
|
-
} else if (t - this.pendingNotice.settledAt >= this.settleFor(this.pendingNotice.sessionId)) {
|
|
6491
|
+
} else if (this.pendingNotice.confirmed || t - this.pendingNotice.settledAt >= this.settleFor(this.pendingNotice.sessionId)) {
|
|
6361
6492
|
this.notice = {
|
|
6362
6493
|
reason: this.pendingNotice.reason,
|
|
6363
6494
|
// Visible from NOW: the settle window was spent waiting, and a hold measured from the turn's end would
|
|
@@ -6433,8 +6564,10 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6433
6564
|
*/
|
|
6434
6565
|
var unreadDropped = isFiniteNumber(this.unreadAtNotice) && this.unreadCount < this.unreadAtNotice;
|
|
6435
6566
|
var neverUnread = this.unreadAtNotice === 0 && this.unreadCount === 0;
|
|
6436
|
-
|
|
6437
|
-
|
|
6567
|
+
var viewed = Array.isArray(snap.viewedCompletions) && snap.viewedCompletions.indexOf(notice.sessionId) !== -1;
|
|
6568
|
+
acknowledged = notice.sessionId
|
|
6569
|
+
? (viewed || ((!this.requireViewedAcknowledgement || notice.reason !== 'completed')
|
|
6570
|
+
&& presentId === notice.sessionId && this.focused === true))
|
|
6438
6571
|
: (notice.reason !== 'error' && this.focused === true
|
|
6439
6572
|
&& (unreadDropped || (neverUnread && this.interaction === null)));
|
|
6440
6573
|
this.pendingDismissalKind = notice.sessionId && presentId ? 'acknowledged' : 'no-session';
|
|
@@ -6456,8 +6589,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6456
6589
|
* still on screen would flip the pet to 出错 and hide the very prompt the user has to answer, and a
|
|
6457
6590
|
* turn that completes would show 任务达成 over an unanswered approval.
|
|
6458
6591
|
*
|
|
6459
|
-
*
|
|
6460
|
-
*
|
|
6592
|
+
* While the session reports a pending interaction, EVERY other state is skipped, including gestures
|
|
6593
|
+
* and greetings. When the interaction is answered the session
|
|
6461
6594
|
* stops reporting it, this latch releases, and the same frame's cascade decides where the pet goes.
|
|
6462
6595
|
*/
|
|
6463
6596
|
var pending = snap.pending === true;
|
|
@@ -6484,7 +6617,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6484
6617
|
/**
|
|
6485
6618
|
* 跳舞 — the user's own state, and the two rules that make it a good guest (operator, 2026-10-14).
|
|
6486
6619
|
*
|
|
6487
|
-
*
|
|
6620
|
+
* Attention comes first, followed by drag states, dance, greeting and offline.
|
|
6488
6621
|
* and it is dropped the moment the session is BUSY, which is the operator's 「跳舞也会被进入工作状态打断」 without a rule of its
|
|
6489
6622
|
* own: work outranks it because the busy branch below is reached first once this field is cleared. A dance also ends when the
|
|
6490
6623
|
* clip ends (the client watches the clip and calls `stopDancing`) or when the user double-clicks the pet, and both of those
|
|
@@ -6495,14 +6628,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6495
6628
|
this.stopDancing();
|
|
6496
6629
|
dance = null;
|
|
6497
6630
|
}
|
|
6498
|
-
if (
|
|
6631
|
+
if (pending) {
|
|
6632
|
+
state = 'waiting';
|
|
6633
|
+
} else if (this.interaction) {
|
|
6499
6634
|
state = this.interaction;
|
|
6500
6635
|
} else if (dance) {
|
|
6501
6636
|
state = 'dancing';
|
|
6502
6637
|
} else if (t < this.greetingUntil) {
|
|
6503
6638
|
state = 'greeting';
|
|
6504
|
-
} else if (pending) {
|
|
6505
|
-
state = 'waiting';
|
|
6506
6639
|
} else if (snap.connected === false && this.everConnected) {
|
|
6507
6640
|
/**
|
|
6508
6641
|
* 离线 means the host is GONE — not that it has nothing to say yet.
|
|
@@ -6602,6 +6735,11 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6602
6735
|
this.records = new Map();
|
|
6603
6736
|
/** Each session's own `running` flag as the store last reported it (see `observeStoreEdge`). */
|
|
6604
6737
|
this.storeRunning = new Map();
|
|
6738
|
+
this.completionChecks = new Map();
|
|
6739
|
+
this.attentionSessions = new Map();
|
|
6740
|
+
this.completionQueue = new Map();
|
|
6741
|
+
this.completionSeen = new Set();
|
|
6742
|
+
this.viewedCompletions = new Set();
|
|
6605
6743
|
/**
|
|
6606
6744
|
* When this aggregate's `running` answer last became true, and what it was last published as.
|
|
6607
6745
|
*
|
|
@@ -6661,6 +6799,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6661
6799
|
var status = statuses && statuses.get ? statuses.get(id) : undefined;
|
|
6662
6800
|
var running = status && typeof status.running === 'boolean' ? status.running : undefined;
|
|
6663
6801
|
if (running === undefined) return;
|
|
6802
|
+
if (running || status.pendingInteraction != null) self.completionChecks.delete(id);
|
|
6803
|
+
if (running) self.forgetCompletion(id);
|
|
6664
6804
|
var previous = self.storeRunning.get(id);
|
|
6665
6805
|
self.storeRunning.set(id, running);
|
|
6666
6806
|
if (previous === running) return;
|
|
@@ -6684,13 +6824,18 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6684
6824
|
* stream covers that case when it is wired, and a false 任务完成 on every boot is worse than a missed one.
|
|
6685
6825
|
*/
|
|
6686
6826
|
if (previous !== true) return;
|
|
6827
|
+
var boundaryRecord = self.records.get(id);
|
|
6828
|
+
if (boundaryRecord && boundaryRecord.lastEndReason && boundaryRecord.lastEndReason !== 'completed') return;
|
|
6687
6829
|
// Stopped: a completion. A failure already latched outranks it, the same rule the boundary path uses.
|
|
6830
|
+
var completion = { id: id + ':store', reason: 'completed', sessionId: id, at: self.now() };
|
|
6831
|
+
self.rememberCompletion(completion);
|
|
6688
6832
|
if (self.notice && self.notice.reason === 'error') return;
|
|
6689
|
-
self.notice =
|
|
6833
|
+
self.notice = completion;
|
|
6690
6834
|
});
|
|
6691
6835
|
};
|
|
6692
6836
|
|
|
6693
6837
|
SessionAggregate.prototype.reset = function (identities) {
|
|
6838
|
+
this.completionChecks.clear();
|
|
6694
6839
|
this.records.clear();
|
|
6695
6840
|
this.notice = undefined;
|
|
6696
6841
|
if (Array.isArray(identities)) {
|
|
@@ -6704,6 +6849,34 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6704
6849
|
}
|
|
6705
6850
|
};
|
|
6706
6851
|
|
|
6852
|
+
SessionAggregate.prototype.rememberCompletion = function (notice, announce) {
|
|
6853
|
+
var id = notice && notice.sessionId;
|
|
6854
|
+
if (!id || this.completionSeen.has(id)) return;
|
|
6855
|
+
this.completionSeen.add(id);
|
|
6856
|
+
this.completionQueue.set(id, Object.assign({}, notice, {
|
|
6857
|
+
announce: announce !== false, queued: true, confirmed: announce !== false,
|
|
6858
|
+
}));
|
|
6859
|
+
this.completionChecks.set(id, this.now());
|
|
6860
|
+
};
|
|
6861
|
+
|
|
6862
|
+
SessionAggregate.prototype.forgetCompletion = function (id) {
|
|
6863
|
+
this.completionQueue.delete(id);
|
|
6864
|
+
this.completionChecks.delete(id);
|
|
6865
|
+
this.completionSeen.delete(id);
|
|
6866
|
+
this.viewedCompletions.delete(id);
|
|
6867
|
+
};
|
|
6868
|
+
|
|
6869
|
+
SessionAggregate.prototype.acknowledgeViewed = function (id) {
|
|
6870
|
+
if (!id || this.running(id) !== false) return;
|
|
6871
|
+
var status = this.input.statuses.get ? this.input.statuses.get(id) : null;
|
|
6872
|
+
if (!this.completionQueue.has(id) && !(status && status.completionUnread === true)) return;
|
|
6873
|
+
this.completionSeen.add(id);
|
|
6874
|
+
this.viewedCompletions.add(id);
|
|
6875
|
+
this.completionQueue.delete(id);
|
|
6876
|
+
this.completionChecks.delete(id);
|
|
6877
|
+
if (this.notice && this.notice.sessionId === id && this.notice.reason === 'completed') this.notice = undefined;
|
|
6878
|
+
};
|
|
6879
|
+
|
|
6707
6880
|
SessionAggregate.prototype.boundary = function (event) {
|
|
6708
6881
|
if (!event || typeof event !== 'object') return this.snapshot();
|
|
6709
6882
|
var id = event.sessionId;
|
|
@@ -6730,12 +6903,16 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6730
6903
|
// The host's live answer for a session it vouches for: every root it classified arrives as `false`.
|
|
6731
6904
|
if (event.isSubagent === false) record.isSubagent = false;
|
|
6732
6905
|
if (event.type === 'turn/start') {
|
|
6906
|
+
this.forgetCompletion(id);
|
|
6733
6907
|
record.running = true;
|
|
6908
|
+
record.lastEndReason = null;
|
|
6909
|
+
this.completionChecks.delete(id);
|
|
6734
6910
|
record.candidate = undefined;
|
|
6735
6911
|
// A new turn supersedes whatever the last one reported, error included.
|
|
6736
6912
|
this.notice = undefined;
|
|
6737
6913
|
} else if (event.type === 'turn/end') {
|
|
6738
6914
|
record.running = false;
|
|
6915
|
+
record.lastEndReason = event.reason;
|
|
6739
6916
|
record.candidate = { id: id + ':' + event.seq, reason: event.reason, at: this.now() };
|
|
6740
6917
|
}
|
|
6741
6918
|
this.records.set(id, record);
|
|
@@ -6745,7 +6922,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6745
6922
|
* a frame for a session nobody can vouch for — not classified, not listed by the app — is silence rather than a
|
|
6746
6923
|
* 任务达成, because the pet has no way to tell it from a helper's turn ending (2026-10-14).
|
|
6747
6924
|
*/
|
|
6748
|
-
if (candidate && (candidate.reason === 'completed' ||
|
|
6925
|
+
if (candidate && (candidate.reason === 'completed' || candidate.reason === 'error') && this.mainSession(id)) {
|
|
6926
|
+
if (candidate.reason === 'completed') this.rememberCompletion({ id: candidate.id, reason: candidate.reason, sessionId: id, at: candidate.at });
|
|
6749
6927
|
record.candidate = undefined;
|
|
6750
6928
|
// A failure latches: a success arriving moments later must not erase the
|
|
6751
6929
|
// fact that a turn just died. The machine owns the same rule, so the two
|
|
@@ -6766,6 +6944,19 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6766
6944
|
};
|
|
6767
6945
|
|
|
6768
6946
|
SessionAggregate.prototype.subagent = function (id) {
|
|
6947
|
+
var list = this.input.catalog || {};
|
|
6948
|
+
var row = list.byId && list.byId[id];
|
|
6949
|
+
// The current DSH store publishes origin and its parents' subagent catalogs, not isSubagent.
|
|
6950
|
+
// These explicit identities outrank an older boundary baseline that called the agent a root.
|
|
6951
|
+
if (row && (row.origin === 'subagent' || row.isSubagent === true)) return true;
|
|
6952
|
+
var parents = Object.keys(list.byId || {});
|
|
6953
|
+
for (var p = 0; p < parents.length; p += 1) {
|
|
6954
|
+
var parent = list.byId[parents[p]];
|
|
6955
|
+
var projection = list.projectionsBySession && list.projectionsBySession[parents[p]];
|
|
6956
|
+
var values = (projection && projection.values) || (parent && parent.projectionValues);
|
|
6957
|
+
var children = values && values.subagentCatalog;
|
|
6958
|
+
if (Array.isArray(children) && children.some(function (child) { return child && child.id === id; })) return true;
|
|
6959
|
+
}
|
|
6769
6960
|
var record = this.records.get(id);
|
|
6770
6961
|
if (record && record.isSubagent !== undefined) return record.isSubagent === true;
|
|
6771
6962
|
var catalog = this.input.catalog && this.input.catalog.byId ? this.input.catalog.byId[id] : undefined;
|
|
@@ -6847,7 +7038,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6847
7038
|
* only ever make the answer STRICTER on a store that has the list.
|
|
6848
7039
|
*/
|
|
6849
7040
|
var listed = catalog.ids;
|
|
6850
|
-
if (listed && typeof listed.length === 'number'
|
|
7041
|
+
if (listed && typeof listed.length === 'number') {
|
|
6851
7042
|
for (var i = 0; i < listed.length; i += 1) {
|
|
6852
7043
|
if (listed[i] === id) return true;
|
|
6853
7044
|
}
|
|
@@ -6865,6 +7056,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6865
7056
|
|
|
6866
7057
|
var workingCount = 0;
|
|
6867
7058
|
var waitingCount = 0;
|
|
7059
|
+
var waitingSessionId = null;
|
|
6868
7060
|
/**
|
|
6869
7061
|
* How many sessions have a completion the user has NOT been back to.
|
|
6870
7062
|
*
|
|
@@ -6881,6 +7073,9 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6881
7073
|
* contributes nothing.
|
|
6882
7074
|
*/
|
|
6883
7075
|
var unreadCount = 0;
|
|
7076
|
+
var completedUnreadCount = 0;
|
|
7077
|
+
var completedSessionId = null;
|
|
7078
|
+
var completedSessionIds = [];
|
|
6884
7079
|
/**
|
|
6885
7080
|
* WHY that count is what it is, as counts rather than ids or titles — added with the 绿点 fix of 2026-10-14, so
|
|
6886
7081
|
* "the dot is lit and no session finished" is answerable from `/vrmpet3d/state` alone instead of by guesswork.
|
|
@@ -6896,9 +7091,32 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6896
7091
|
/** Which pending interaction wins when several sessions are waiting at once. Approval is the rarer. */
|
|
6897
7092
|
var waitingKind = null;
|
|
6898
7093
|
var self = this;
|
|
7094
|
+
this.attentionSessions.forEach(function (_kind, id) {
|
|
7095
|
+
var status = statuses.get ? statuses.get(id) : undefined;
|
|
7096
|
+
// Navigating away, a missing report, or losing focus is not an answer to the problem.
|
|
7097
|
+
if (status) {
|
|
7098
|
+
if (status.pendingInteraction == null) self.attentionSessions.delete(id);
|
|
7099
|
+
else self.attentionSessions.set(id, pendingInteractionKind(status.pendingInteraction));
|
|
7100
|
+
}
|
|
7101
|
+
});
|
|
6899
7102
|
ids.forEach(function (id) {
|
|
6900
7103
|
if (!self.inScope(id)) return;
|
|
6901
7104
|
var status = statuses.get ? statuses.get(id) : undefined;
|
|
7105
|
+
var record = self.records.get(id);
|
|
7106
|
+
if (status && status.completionUnread === true && self.mainSession(id) && !self.completionSeen.has(id)
|
|
7107
|
+
&& self.running(id) === false && status.pendingInteraction == null) {
|
|
7108
|
+
self.rememberCompletion({ sessionId: id, reason: 'completed', at: self.now() }, false);
|
|
7109
|
+
}
|
|
7110
|
+
var completed = self.completionQueue.has(id) && !self.viewedCompletions.has(id) && self.mainSession(id)
|
|
7111
|
+
&& self.running(id) === false && (!status || status.pendingInteraction == null)
|
|
7112
|
+
&& !(record && record.lastEndReason && record.lastEndReason !== 'completed');
|
|
7113
|
+
if (!completed) self.completionChecks.delete(id);
|
|
7114
|
+
else {
|
|
7115
|
+
if (!self.completionChecks.has(id)) self.completionChecks.set(id, self.now());
|
|
7116
|
+
if (self.completionQueue.get(id).confirmed || self.now() - self.completionChecks.get(id) >= COMPLETION_SETTLE_MS) {
|
|
7117
|
+
completedSessionIds.push(id);
|
|
7118
|
+
}
|
|
7119
|
+
}
|
|
6902
7120
|
if (status && status.completionUnread === true) {
|
|
6903
7121
|
unreadDetail.candidates += 1;
|
|
6904
7122
|
var entry = (catalog.byId || {})[id];
|
|
@@ -6921,14 +7139,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6921
7139
|
}
|
|
6922
7140
|
var pending = status ? status.pendingInteraction != null : false;
|
|
6923
7141
|
if (pending) {
|
|
6924
|
-
|
|
6925
|
-
var kind = pendingInteractionKind(status.pendingInteraction);
|
|
6926
|
-
// Ranked rather than first-wins: a global-scope pet watching ten sessions must not report
|
|
6927
|
-
// "a question" just because that session happens to come out of the map first when another one
|
|
6928
|
-
// is blocked on a permission prompt.
|
|
6929
|
-
if (waitingKind === null || PENDING_KIND_RANK.indexOf(kind) < PENDING_KIND_RANK.indexOf(waitingKind)) {
|
|
6930
|
-
waitingKind = kind;
|
|
6931
|
-
}
|
|
7142
|
+
self.attentionSessions.set(id, pendingInteractionKind(status.pendingInteraction));
|
|
6932
7143
|
}
|
|
6933
7144
|
// One rule for "this is the user's own session", shared with the two completion paths (`mainSession`).
|
|
6934
7145
|
if (self.mainSession(id) && self.running(id) === true) {
|
|
@@ -6939,7 +7150,23 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6939
7150
|
}
|
|
6940
7151
|
});
|
|
6941
7152
|
|
|
7153
|
+
this.attentionSessions.forEach(function (kind, id) {
|
|
7154
|
+
waitingCount++;
|
|
7155
|
+
if (waitingKind === null || PENDING_KIND_RANK.indexOf(kind) < PENDING_KIND_RANK.indexOf(waitingKind)) {
|
|
7156
|
+
waitingKind = kind;
|
|
7157
|
+
waitingSessionId = id;
|
|
7158
|
+
}
|
|
7159
|
+
});
|
|
7160
|
+
completedSessionIds.sort(function (a, b) { return self.completionQueue.get(a).at - self.completionQueue.get(b).at; });
|
|
7161
|
+
completedUnreadCount = completedSessionIds.length;
|
|
7162
|
+
completedSessionId = completedSessionIds[0] || null;
|
|
7163
|
+
var waitingSessionIds = Array.from(this.attentionSessions.keys()).sort(function (a, b) {
|
|
7164
|
+
return PENDING_KIND_RANK.indexOf(self.attentionSessions.get(a)) - PENDING_KIND_RANK.indexOf(self.attentionSessions.get(b));
|
|
7165
|
+
});
|
|
6942
7166
|
var ready = catalog.phase === 'ready';
|
|
7167
|
+
self.completionChecks.forEach(function (_at, id) {
|
|
7168
|
+
if (!ids.has(id) || !self.inScope(id)) self.completionChecks.delete(id);
|
|
7169
|
+
});
|
|
6943
7170
|
|
|
6944
7171
|
// Availability is "there is a session whose activity this pet could show".
|
|
6945
7172
|
// In `current` scope that must be proved by the selected session actually
|
|
@@ -6952,7 +7179,12 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6952
7179
|
|
|
6953
7180
|
var hasAnything = ready || workingCount > 0 || waitingCount > 0 || Boolean(this.notice);
|
|
6954
7181
|
var available = this.input.connected === true && hasAnything && hasSelectedSession;
|
|
6955
|
-
var
|
|
7182
|
+
var queuedNotice;
|
|
7183
|
+
this.completionQueue.forEach(function (entry, id) {
|
|
7184
|
+
if (!queuedNotice && entry.announce && self.mainSession(id) && self.inScope(id) && self.running(id) === false) queuedNotice = entry;
|
|
7185
|
+
});
|
|
7186
|
+
var notice = available ? (this.notice && this.notice.reason === 'error' ? this.notice
|
|
7187
|
+
: (queuedNotice || (this.notice && !this.viewedCompletions.has(this.notice.sessionId) ? this.notice : undefined))) : undefined;
|
|
6956
7188
|
/**
|
|
6957
7189
|
* `connected` rides along, because 离线 is decided by the CONNECTION and not by `available`.
|
|
6958
7190
|
*
|
|
@@ -7008,6 +7240,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
7008
7240
|
pendingKind: waitingCount > 0 ? waitingKind : null,
|
|
7009
7241
|
workingCount: workingCount,
|
|
7010
7242
|
waitingCount: waitingCount,
|
|
7243
|
+
waitingSessionId: waitingSessionId,
|
|
7244
|
+
waitingSessionIds: waitingSessionIds,
|
|
7011
7245
|
/**
|
|
7012
7246
|
* Sessions whose completion is still UNREAD (see above), and whether there are any.
|
|
7013
7247
|
*
|
|
@@ -7015,6 +7249,10 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
7015
7249
|
* surface could say more later without another plumbing round.
|
|
7016
7250
|
*/
|
|
7017
7251
|
unreadCount: unreadCount,
|
|
7252
|
+
completedUnreadCount: completedUnreadCount,
|
|
7253
|
+
completedSessionId: completedSessionId,
|
|
7254
|
+
completedSessionIds: completedSessionIds,
|
|
7255
|
+
viewedCompletions: Array.from(this.viewedCompletions),
|
|
7018
7256
|
hasUnread: unreadCount > 0,
|
|
7019
7257
|
/** The breakdown behind that count — see `unreadDetail` where it is built. */
|
|
7020
7258
|
unreadDetail: unreadDetail,
|