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/core.js
CHANGED
|
@@ -1179,9 +1179,8 @@
|
|
|
1179
1179
|
}
|
|
1180
1180
|
|
|
1181
1181
|
/**
|
|
1182
|
-
*
|
|
1183
|
-
*
|
|
1184
|
-
* same thing in the floating window (the screen's width) and in the app overlay (its window's width).
|
|
1182
|
+
* A positive setting overrides automatic distance. Both axes and both surfaces
|
|
1183
|
+
* pass the screen width here; automatic distance is half of that width.
|
|
1185
1184
|
*/
|
|
1186
1185
|
function resolveGazeSpan(span, fallback) {
|
|
1187
1186
|
var wanted = Number(span);
|
|
@@ -1189,7 +1188,7 @@
|
|
|
1189
1188
|
return clamp(wanted, GAZE_SPAN_RANGE.min, GAZE_SPAN_RANGE.max);
|
|
1190
1189
|
}
|
|
1191
1190
|
var base = Number(fallback);
|
|
1192
|
-
return isFiniteNumber(base) && base > 0 ? base : GAZE_SPAN.x;
|
|
1191
|
+
return isFiniteNumber(base) && base > 0 ? base / 2 : GAZE_SPAN.x;
|
|
1193
1192
|
}
|
|
1194
1193
|
|
|
1195
1194
|
/**
|
|
@@ -1211,7 +1210,9 @@
|
|
|
1211
1210
|
* Pure numbers, no DOM, so the logic suite covers the clamping and the signs.
|
|
1212
1211
|
*
|
|
1213
1212
|
* @param {{pointerX:number, pointerY:number, centreX:number, centreY:number,
|
|
1214
|
-
* spanX?:number, spanY?:number, curve?:number
|
|
1213
|
+
* spanX?:number, spanY?:number, curve?:number, curveY?:number,
|
|
1214
|
+
* curveRadial?:number, curveSpatial?:number, distance?:number,
|
|
1215
|
+
* mode?:'axes'|'radial'|'spatial'}} input
|
|
1215
1216
|
* @returns {{x:number, y:number}|null} `null` when the input is unusable; `y` is
|
|
1216
1217
|
* positive BELOW the pet, i.e. in screen coordinates
|
|
1217
1218
|
*/
|
|
@@ -1230,12 +1231,39 @@
|
|
|
1230
1231
|
: GAZE_SPAN.y;
|
|
1231
1232
|
// The settings' curve, unless the caller passes its own (the suite does, to pin a shape).
|
|
1232
1233
|
var edgeSlope = isFiniteNumber(Number(input.curve)) ? Number(input.curve) : DEFAULTS.gazeCurve;
|
|
1234
|
+
var slopeY = isFiniteNumber(Number(input.curveY)) ? Number(input.curveY) : edgeSlope;
|
|
1235
|
+
if (input.mode === 'spatial') {
|
|
1236
|
+
var spatialX = (pointerX - centreX) / spanX;
|
|
1237
|
+
var spatialY = (pointerY - centreY) / spanY;
|
|
1238
|
+
if (!isFiniteNumber(spatialX) || !isFiniteNumber(spatialY)) return null;
|
|
1239
|
+
var depth = isFiniteNumber(Number(input.distance)) && Number(input.distance) > 0
|
|
1240
|
+
? clamp(Number(input.distance), 0.1, 5) : DEFAULTS.gazeDistance;
|
|
1241
|
+
// A virtual target plane in span units: sideways distance reduces pitch
|
|
1242
|
+
// at a fixed pointer height. Curves shape angles, rather than pixel offsets.
|
|
1243
|
+
var yawAngle = Math.atan2(spatialX, depth) / (Math.PI / 2);
|
|
1244
|
+
var pitchAngle = Math.atan2(spatialY, Math.hypot(spatialX, depth)) / (Math.PI / 2);
|
|
1245
|
+
var spatialSlope = isFiniteNumber(Number(input.curveSpatial)) ? Number(input.curveSpatial) : DEFAULTS.gazeCurveSpatial;
|
|
1246
|
+
return {
|
|
1247
|
+
x: (yawAngle < 0 ? -1 : 1) * gazeCurve(Math.abs(yawAngle), spatialSlope),
|
|
1248
|
+
y: (pitchAngle < 0 ? -1 : 1) * gazeCurve(Math.abs(pitchAngle), spatialSlope),
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
if (input.mode === 'radial') {
|
|
1252
|
+
var dx = (pointerX - centreX) / spanX;
|
|
1253
|
+
var dy = (pointerY - centreY) / spanY;
|
|
1254
|
+
var distance = Math.hypot(dx, dy);
|
|
1255
|
+
if (!isFiniteNumber(distance)) return null;
|
|
1256
|
+
if (distance === 0) return { x: 0, y: 0 };
|
|
1257
|
+
var radialSlope = isFiniteNumber(Number(input.curveRadial)) ? Number(input.curveRadial) : edgeSlope;
|
|
1258
|
+
var gain = gazeCurve(Math.min(1, distance), radialSlope) / distance;
|
|
1259
|
+
return { x: dx * gain, y: dy * gain };
|
|
1260
|
+
}
|
|
1233
1261
|
var rawX = clamp((pointerX - centreX) / spanX, -1, 1);
|
|
1234
1262
|
var rawY = clamp((pointerY - centreY) / spanY, -1, 1);
|
|
1235
1263
|
// Mirrored through `gazeCurve` so the sign survives and the response is the curve on both sides.
|
|
1236
1264
|
return {
|
|
1237
1265
|
x: rawX < 0 ? -gazeCurve(-rawX, edgeSlope) : gazeCurve(rawX, edgeSlope),
|
|
1238
|
-
y: rawY < 0 ? -gazeCurve(-rawY,
|
|
1266
|
+
y: rawY < 0 ? -gazeCurve(-rawY, slopeY) : gazeCurve(rawY, slopeY)
|
|
1239
1267
|
};
|
|
1240
1268
|
}
|
|
1241
1269
|
|
|
@@ -1348,12 +1376,21 @@
|
|
|
1348
1376
|
* operator's own (2026-10-13: 「视线:曲率设置默认为0.1,提供0.05-1的选择(原先是0.1-1),静止停看默认为2秒」).
|
|
1349
1377
|
* `gazeCurve` is the curve's slope at the frame's edge — 1 is a straight line, smaller is a
|
|
1350
1378
|
* steeper start and a flatter tail — and `gazeSpanX`/`gazeSpanY` are the full-deflection
|
|
1351
|
-
* distances in CSS pixels, where **0 means AUTO**:
|
|
1352
|
-
*
|
|
1379
|
+
* distances in CSS pixels, where **0 means AUTO**: half the screen width on both axes.
|
|
1380
|
+
* `gazeCurve` / `gazeCurveY` remain readable for older delivered clients; the current
|
|
1381
|
+
* settings expose only spatial (default) and radial modes.
|
|
1353
1382
|
*/
|
|
1354
1383
|
gazeCurve: 0.1,
|
|
1384
|
+
gazeCurveMode: 'spatial',
|
|
1385
|
+
gazeCurveY: 0.1,
|
|
1386
|
+
gazeCurveRadial: 0.1,
|
|
1387
|
+
gazeCurveSpatial: 1,
|
|
1388
|
+
gazeDistance: 0.1,
|
|
1355
1389
|
gazeSpanX: 0,
|
|
1356
1390
|
gazeSpanY: 0,
|
|
1391
|
+
// Actual head pitch in degrees, after the renderer's gaze weight.
|
|
1392
|
+
gazePitchUp: 60,
|
|
1393
|
+
gazePitchDown: 60,
|
|
1357
1394
|
/**
|
|
1358
1395
|
* How long the cursor may sit still before the pet stops following it, in seconds.
|
|
1359
1396
|
*
|
|
@@ -1465,6 +1502,7 @@
|
|
|
1465
1502
|
* not silently pin drag and gravity to the numbers the panel happened to be showing.
|
|
1466
1503
|
*/
|
|
1467
1504
|
springs: {},
|
|
1505
|
+
toon: {},
|
|
1468
1506
|
/**
|
|
1469
1507
|
* Per-model size multiplier for BOTH texts: `{ [modelId]: number }`.
|
|
1470
1508
|
*
|
|
@@ -2199,12 +2237,25 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
2199
2237
|
out.gazeCurve = Number.isFinite(Number(source.gazeCurve))
|
|
2200
2238
|
? clamp(Number(source.gazeCurve), GAZE_CURVE_RANGE.min, GAZE_CURVE_RANGE.max)
|
|
2201
2239
|
: DEFAULTS.gazeCurve;
|
|
2240
|
+
out.gazeCurveMode = source.gazeCurveMode === 'radial' ? 'radial' : 'spatial';
|
|
2241
|
+
['gazeCurveY', 'gazeCurveRadial'].forEach(function (key) {
|
|
2242
|
+
out[key] = typeof source[key] === 'number' && Number.isFinite(source[key])
|
|
2243
|
+
? clamp(source[key], GAZE_CURVE_RANGE.min, GAZE_CURVE_RANGE.max) : DEFAULTS[key];
|
|
2244
|
+
});
|
|
2245
|
+
out.gazeCurveSpatial = typeof source.gazeCurveSpatial === 'number' && Number.isFinite(source.gazeCurveSpatial)
|
|
2246
|
+
? clamp(source.gazeCurveSpatial, GAZE_CURVE_RANGE.min, GAZE_CURVE_RANGE.max) : DEFAULTS.gazeCurveSpatial;
|
|
2247
|
+
out.gazeDistance = typeof source.gazeDistance === 'number' && Number.isFinite(source.gazeDistance)
|
|
2248
|
+
? clamp(source.gazeDistance, 0.1, 5) : DEFAULTS.gazeDistance;
|
|
2202
2249
|
out.gazeSpanX = Number.isFinite(Number(source.gazeSpanX))
|
|
2203
2250
|
? clamp(Number(source.gazeSpanX), GAZE_SPAN_RANGE.min, GAZE_SPAN_RANGE.max)
|
|
2204
2251
|
: DEFAULTS.gazeSpanX;
|
|
2205
2252
|
out.gazeSpanY = Number.isFinite(Number(source.gazeSpanY))
|
|
2206
2253
|
? clamp(Number(source.gazeSpanY), GAZE_SPAN_RANGE.min, GAZE_SPAN_RANGE.max)
|
|
2207
2254
|
: DEFAULTS.gazeSpanY;
|
|
2255
|
+
['gazePitchUp', 'gazePitchDown'].forEach(function (key) {
|
|
2256
|
+
out[key] = typeof source[key] === 'number' && Number.isFinite(source[key])
|
|
2257
|
+
? clamp(source[key], 0, 60) : DEFAULTS[key];
|
|
2258
|
+
});
|
|
2208
2259
|
// 0 is a legitimate value here — it turns the idle rule off — so this cannot fall back to the default the
|
|
2209
2260
|
// way an unparseable value does. A negative number is unparseable for this purpose and becomes 0.
|
|
2210
2261
|
out.gazeIdleSeconds = Number.isFinite(Number(source.gazeIdleSeconds))
|
|
@@ -2259,6 +2310,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
2259
2310
|
? source.expressions.emotion.parts
|
|
2260
2311
|
: null);
|
|
2261
2312
|
out.springs = cleanSprings(source.springs);
|
|
2313
|
+
out.toon = cleanToon(source.toon);
|
|
2262
2314
|
return out;
|
|
2263
2315
|
}
|
|
2264
2316
|
|
|
@@ -2269,6 +2321,25 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
2269
2321
|
* directory's manifest says `loop: false`) has to be OVERRIDABLE back to looping, and "no entry" cannot
|
|
2270
2322
|
* express that — absence means "ask the catalog".
|
|
2271
2323
|
*/
|
|
2324
|
+
function cleanToon(raw) {
|
|
2325
|
+
var out = {};
|
|
2326
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return out;
|
|
2327
|
+
Object.keys(raw).slice(0, 256).forEach(function (id) {
|
|
2328
|
+
if (id === '__proto__' || id === 'constructor' || id === 'prototype') return;
|
|
2329
|
+
var entry = raw[id];
|
|
2330
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return;
|
|
2331
|
+
var next = {};
|
|
2332
|
+
if (typeof entry.enabled === 'boolean') next.enabled = entry.enabled;
|
|
2333
|
+
var ranges = { shadingToonyFactor: [0, 1], shadingShiftFactor: [-1, 1], shadeStrength: [0, 1], outlineScale: [0, 3] };
|
|
2334
|
+
Object.keys(ranges).forEach(function (key) {
|
|
2335
|
+
var value = entry[key];
|
|
2336
|
+
if (typeof value === 'number' && Number.isFinite(value)) next[key] = clamp(value, ranges[key][0], ranges[key][1]);
|
|
2337
|
+
});
|
|
2338
|
+
if (Object.keys(next).length) out[id] = next;
|
|
2339
|
+
});
|
|
2340
|
+
return out;
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2272
2343
|
function cleanMotionLoop(raw) {
|
|
2273
2344
|
var out = {};
|
|
2274
2345
|
var source = raw && typeof raw === 'object' ? raw : {};
|
|
@@ -3552,6 +3623,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
3552
3623
|
* @returns {string|null} the line to show, or null for silence
|
|
3553
3624
|
*/
|
|
3554
3625
|
SpeechMachine.prototype.observe = function (preferences, state, pendingKind, language, random, motion) {
|
|
3626
|
+
if (state === 'dancing') {
|
|
3627
|
+
this.current = null;
|
|
3628
|
+
this.wantsAi = null;
|
|
3629
|
+
this.lastEvent = null;
|
|
3630
|
+
this.idleAt = null;
|
|
3631
|
+
this.chatterAt = null;
|
|
3632
|
+
return null;
|
|
3633
|
+
}
|
|
3555
3634
|
var speech = cleanSpeech(preferences && preferences.speech);
|
|
3556
3635
|
var t = this.now();
|
|
3557
3636
|
if (!speech.enabled) {
|
|
@@ -3816,7 +3895,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
3816
3895
|
*
|
|
3817
3896
|
* @returns {{state: string, text: string, tag: string, animation: string, seconds: number}|null}
|
|
3818
3897
|
*/
|
|
3819
|
-
function statusFor(preferences, state, enteredAt, now, clock, language) {
|
|
3898
|
+
function statusFor(preferences, state, enteredAt, now, clock, language, completedCount) {
|
|
3820
3899
|
if (STATUS_STATES.indexOf(state) === -1) return null;
|
|
3821
3900
|
var entry = statusEntryFor(preferences, state, language);
|
|
3822
3901
|
if (!entry.text && !entry.tag) return null;
|
|
@@ -3873,6 +3952,9 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
3873
3952
|
? durationClauseRemoved(text)
|
|
3874
3953
|
: text.split(STATUS_DURATION_TOKEN).join(formatRunDuration(elapsed, language));
|
|
3875
3954
|
}
|
|
3955
|
+
if (state === 'done' && Number(completedCount) > 1 && text) {
|
|
3956
|
+
text = language === 'en' ? (text === 'Task complete' ? 'All tasks complete' : 'All ' + text) : '全部' + text;
|
|
3957
|
+
}
|
|
3876
3958
|
var look = statusTone(state);
|
|
3877
3959
|
return {
|
|
3878
3960
|
state: state,
|
|
@@ -4906,6 +4988,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
4906
4988
|
* @param {object} options url, intervalMs, fetchImpl, onPreview(preview|null), onUnsupported
|
|
4907
4989
|
*/
|
|
4908
4990
|
function watchHostPreview(options) {
|
|
4991
|
+
var navigationSeen = null;
|
|
4909
4992
|
var settings = options || {};
|
|
4910
4993
|
var url = settings.url || '/vrmpet3d/preview';
|
|
4911
4994
|
// Four hundred milliseconds: this drives a hover preview, so it has to feel immediate, and the route is a
|
|
@@ -4914,11 +4997,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
4914
4997
|
var doFetch = settings.fetchImpl || (typeof fetch === 'function' ? fetch : null);
|
|
4915
4998
|
var timer = null;
|
|
4916
4999
|
var seen = -1;
|
|
5000
|
+
var issued = 0;
|
|
5001
|
+
var delivered = 0;
|
|
4917
5002
|
var supported = Boolean(doFetch);
|
|
4918
5003
|
var stopped = false;
|
|
4919
5004
|
|
|
4920
5005
|
function load() {
|
|
4921
5006
|
if (!supported || stopped) return Promise.resolve(null);
|
|
5007
|
+
var request = ++issued;
|
|
4922
5008
|
return doFetch(url, { credentials: 'omit' })
|
|
4923
5009
|
.then(function (response) {
|
|
4924
5010
|
if (response.status === 404 || response.status === 405) {
|
|
@@ -4930,6 +5016,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
4930
5016
|
return response.json ? response.json() : null;
|
|
4931
5017
|
})
|
|
4932
5018
|
.then(function (body) {
|
|
5019
|
+
if (request < delivered) return null;
|
|
5020
|
+
if (body && typeof body.seq === 'number') delivered = request;
|
|
5021
|
+
if (body && body.navigation && typeof body.navigation.seq === 'number') {
|
|
5022
|
+
var navigation = body.navigation;
|
|
5023
|
+
if (navigationSeen !== null && navigationSeen !== navigation.seq && typeof navigation.sessionId === 'string'
|
|
5024
|
+
&& typeof settings.onNavigate === 'function') settings.onNavigate(navigation.sessionId);
|
|
5025
|
+
navigationSeen = navigation.seq;
|
|
5026
|
+
}
|
|
4933
5027
|
if (!body || typeof body.seq !== 'number' || body.seq === seen) return null;
|
|
4934
5028
|
seen = body.seq;
|
|
4935
5029
|
var preview = cleanPreview(body.preview);
|
|
@@ -5067,11 +5161,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5067
5161
|
var doFetch = settings.fetchImpl || (typeof fetch === 'function' ? fetch : null);
|
|
5068
5162
|
var timer = null;
|
|
5069
5163
|
var seen = -1;
|
|
5164
|
+
var issued = 0;
|
|
5165
|
+
var delivered = 0;
|
|
5070
5166
|
var supported = Boolean(doFetch);
|
|
5071
5167
|
var stopped = false;
|
|
5072
5168
|
|
|
5073
5169
|
function load() {
|
|
5074
5170
|
if (!supported || stopped) return Promise.resolve(null);
|
|
5171
|
+
var request = ++issued;
|
|
5075
5172
|
return doFetch(url, { credentials: 'omit' })
|
|
5076
5173
|
.then(function (response) {
|
|
5077
5174
|
if (response.status === 404 || response.status === 405) {
|
|
@@ -5083,6 +5180,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5083
5180
|
return response.json ? response.json() : null;
|
|
5084
5181
|
})
|
|
5085
5182
|
.then(function (body) {
|
|
5183
|
+
if (request < delivered) return null;
|
|
5184
|
+
if (body && typeof body.seq === 'number') delivered = request;
|
|
5086
5185
|
if (!body || typeof body.seq !== 'number' || body.seq === seen) return null;
|
|
5087
5186
|
seen = body.seq;
|
|
5088
5187
|
var state = STATES.indexOf(body.state) === -1 ? null : body.state;
|
|
@@ -5192,6 +5291,12 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5192
5291
|
}
|
|
5193
5292
|
if (typeof why.scope === 'string' && why.scope) out.scope = why.scope.slice(0, 16);
|
|
5194
5293
|
if (typeof why.session === 'string' && why.session) out.session = why.session.slice(0, 64);
|
|
5294
|
+
if (typeof why.statusSessionId === 'string' && why.statusSessionId) out.statusSessionId = why.statusSessionId.slice(0, 64);
|
|
5295
|
+
if (typeof why.completedSessionId === 'string' && why.completedSessionId) out.completedSessionId = why.completedSessionId.slice(0, 64);
|
|
5296
|
+
['waitingSessionIds', 'completedSessionIds'].forEach(function (key) {
|
|
5297
|
+
if (Array.isArray(why[key])) out[key] = why[key].filter(function (id) { return typeof id === 'string' && id; })
|
|
5298
|
+
.slice(0, 256).map(function (id) { return id.slice(0, 64); });
|
|
5299
|
+
});
|
|
5195
5300
|
if (typeof why.interaction === 'string' && why.interaction) out.interaction = why.interaction.slice(0, 16);
|
|
5196
5301
|
if (Array.isArray(why.runningIds)) {
|
|
5197
5302
|
out.runningIds = why.runningIds
|
|
@@ -5349,15 +5454,19 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5349
5454
|
* its 进行中 line has to travel with the state. A whole number, or nothing — an older host that does not send it
|
|
5350
5455
|
* leaves the window's dot as it was rather than inventing a count.
|
|
5351
5456
|
*/
|
|
5352
|
-
var nextUnread = Number.isFinite(
|
|
5457
|
+
var nextUnread = typeof unread === 'number' && Number.isFinite(unread) && unread >= 0 ? Math.floor(unread) : null;
|
|
5353
5458
|
/**
|
|
5354
5459
|
* The diagnostic, kept OUT of the change key on purpose: it carries counts and clock readings that move
|
|
5355
5460
|
* constantly, and letting them force a report would turn "the pet announces a change" into a 1 Hz feed. It
|
|
5356
5461
|
* rides along with the reports the state and the line already earn, and the heartbeat re-states the last one.
|
|
5357
5462
|
*/
|
|
5358
5463
|
var nextWhy = cleanWhy(why);
|
|
5464
|
+
var navigationKey = function (value) {
|
|
5465
|
+
return JSON.stringify(value ? [value.waitingSessionIds || [], value.completedSessionIds || [],
|
|
5466
|
+
value.statusSessionId || null, value.completedSessionId || null] : [[], [], null, null]);
|
|
5467
|
+
};
|
|
5359
5468
|
if (force !== true && next === sent && nextLine === sentLine && nextTouchKey === sentTouchKey
|
|
5360
|
-
&& nextClockKey === sentClockKey && nextUnread === sentUnread) {
|
|
5469
|
+
&& nextClockKey === sentClockKey && nextUnread === sentUnread && navigationKey(nextWhy) === navigationKey(sentWhy)) {
|
|
5361
5470
|
return Promise.resolve(null);
|
|
5362
5471
|
}
|
|
5363
5472
|
sent = next;
|
|
@@ -5482,10 +5591,15 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5482
5591
|
var doFetch = settings.fetchImpl || (typeof fetch === 'function' ? fetch : null);
|
|
5483
5592
|
var timer = null;
|
|
5484
5593
|
var seen = -1;
|
|
5594
|
+
var issued = 0;
|
|
5595
|
+
var delivered = 0;
|
|
5485
5596
|
var supported = Boolean(doFetch);
|
|
5486
5597
|
var stopped = false;
|
|
5487
5598
|
|
|
5488
|
-
function deliver(snapshot) {
|
|
5599
|
+
function deliver(snapshot, request) {
|
|
5600
|
+
// A slow poll/write must not roll a live gesture back to an older reply.
|
|
5601
|
+
if (request < delivered) return null;
|
|
5602
|
+
if (snapshot && typeof snapshot.version === 'number') delivered = request;
|
|
5489
5603
|
if (!snapshot || typeof snapshot.version !== 'number' || snapshot.version === seen) return null;
|
|
5490
5604
|
seen = snapshot.version;
|
|
5491
5605
|
if (typeof settings.onSnapshot === 'function') settings.onSnapshot(snapshot);
|
|
@@ -5495,6 +5609,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5495
5609
|
/** One look at the host. Returns a promise for the snapshot it delivered, if any. */
|
|
5496
5610
|
function load() {
|
|
5497
5611
|
if (!supported || stopped) return Promise.resolve(null);
|
|
5612
|
+
var request = ++issued;
|
|
5498
5613
|
return doFetch(url, { credentials: 'omit' })
|
|
5499
5614
|
.then(function (response) {
|
|
5500
5615
|
if (response.status === 404 || response.status === 405) {
|
|
@@ -5506,7 +5621,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5506
5621
|
if (!response.ok) return null;
|
|
5507
5622
|
return response.json ? response.json() : null;
|
|
5508
5623
|
})
|
|
5509
|
-
.then(deliver)
|
|
5624
|
+
.then(function (snapshot) { return deliver(snapshot, request); })
|
|
5510
5625
|
.catch(function () { return null; });
|
|
5511
5626
|
}
|
|
5512
5627
|
|
|
@@ -5518,6 +5633,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5518
5633
|
*/
|
|
5519
5634
|
function push(body) {
|
|
5520
5635
|
if (!supported || stopped) return Promise.resolve(null);
|
|
5636
|
+
var request = ++issued;
|
|
5521
5637
|
return doFetch(url, {
|
|
5522
5638
|
method: 'POST',
|
|
5523
5639
|
credentials: 'omit',
|
|
@@ -5525,7 +5641,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5525
5641
|
body: JSON.stringify(body || {}),
|
|
5526
5642
|
})
|
|
5527
5643
|
.then(function (response) { return response.ok ? response.json() : null; })
|
|
5528
|
-
.then(deliver)
|
|
5644
|
+
.then(function (snapshot) { return deliver(snapshot, request); })
|
|
5529
5645
|
.catch(function () { return null; });
|
|
5530
5646
|
}
|
|
5531
5647
|
|
|
@@ -5738,6 +5854,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5738
5854
|
* sits above the watermark and is adopted as usual.
|
|
5739
5855
|
*/
|
|
5740
5856
|
this.dismissedNoticeAt = 0;
|
|
5857
|
+
this.dismissedSessions = new Map();
|
|
5741
5858
|
/**
|
|
5742
5859
|
* The TURN the pet is currently describing, and the one that just ended.
|
|
5743
5860
|
*
|
|
@@ -6020,6 +6137,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6020
6137
|
PetMachine.prototype.dismissNotice = function (keepDebt) {
|
|
6021
6138
|
if (this.notice && Number.isFinite(this.notice.at)) {
|
|
6022
6139
|
this.dismissedNoticeAt = Math.max(this.dismissedNoticeAt, this.notice.at);
|
|
6140
|
+
if (this.notice.sessionId) this.dismissedSessions.set(this.notice.sessionId, this.notice.at);
|
|
6023
6141
|
}
|
|
6024
6142
|
if (keepDebt !== true && this.notice && this.notice.sessionId) {
|
|
6025
6143
|
delete this.retractions[this.notice.sessionId];
|
|
@@ -6142,7 +6260,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6142
6260
|
* well as in the acknowledgement rule in `resolve`. (The floating window never takes focus, which is why the same
|
|
6143
6261
|
* drag could not do this there.)
|
|
6144
6262
|
*/
|
|
6145
|
-
if (this.notice &&
|
|
6263
|
+
if (this.notice && !this.notice.sessionId && !presenceKnown && this.interaction === null) {
|
|
6146
6264
|
this.noteDismissal('focus-return');
|
|
6147
6265
|
this.dismissNotice();
|
|
6148
6266
|
}
|
|
@@ -6254,6 +6372,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6254
6372
|
// reports as its own `at`, so the minimum hold starts when the user can first see it.
|
|
6255
6373
|
endedAt: at,
|
|
6256
6374
|
settledAt: this.now(),
|
|
6375
|
+
confirmed: payload.confirmed === true,
|
|
6257
6376
|
sessionId: sessionId,
|
|
6258
6377
|
};
|
|
6259
6378
|
}
|
|
@@ -6279,8 +6398,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6279
6398
|
* than a startup that has not finished.
|
|
6280
6399
|
*/
|
|
6281
6400
|
if (snap.connected === true) this.everConnected = true;
|
|
6282
|
-
var busy = snap.
|
|
6401
|
+
var busy = snap.pending === true || (snap.available === true && snap.running === true);
|
|
6283
6402
|
if (busy) this.lastBusyAt = t;
|
|
6403
|
+
var viewedCompletions = Array.isArray(snap.viewedCompletions) ? snap.viewedCompletions : [];
|
|
6404
|
+
if (this.pendingNotice && viewedCompletions.indexOf(this.pendingNotice.sessionId) !== -1) this.pendingNotice = undefined;
|
|
6405
|
+
if (this.notice && viewedCompletions.indexOf(this.notice.sessionId) !== -1) {
|
|
6406
|
+
this.noteDismissal('acknowledged');
|
|
6407
|
+
this.dismissNotice();
|
|
6408
|
+
}
|
|
6284
6409
|
|
|
6285
6410
|
/**
|
|
6286
6411
|
* Adopt the SNAPSHOT's own notice when it is newer than this machine's.
|
|
@@ -6299,9 +6424,11 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6299
6424
|
* what the aggregate watches, and it falls between the rounds of one task exactly like a turn does.
|
|
6300
6425
|
*/
|
|
6301
6426
|
if (snap.notice && typeof snap.notice.at === 'number'
|
|
6302
|
-
&& snap.notice.
|
|
6303
|
-
|
|
6304
|
-
|
|
6427
|
+
&& (snap.notice.queued === true ? (!this.notice && !this.pendingNotice
|
|
6428
|
+
&& (!this.dismissedSessions.has(snap.notice.sessionId) || snap.notice.at > this.dismissedSessions.get(snap.notice.sessionId)))
|
|
6429
|
+
: (snap.notice.at > this.dismissedNoticeAt
|
|
6430
|
+
&& (!this.notice || snap.notice.at > this.notice.at)
|
|
6431
|
+
&& (!this.pendingNotice || snap.notice.at > this.pendingNotice.endedAt)))) {
|
|
6305
6432
|
var snapSession = typeof snap.notice.sessionId === 'string' && snap.notice.sessionId ? snap.notice.sessionId : null;
|
|
6306
6433
|
if (snap.notice.reason === 'error') {
|
|
6307
6434
|
// A failure is not a completion and is never settled: it is shown at once, exactly as a `turn/end` failure is.
|
|
@@ -6311,7 +6438,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6311
6438
|
this.pendingNotice = {
|
|
6312
6439
|
reason: snap.notice.reason,
|
|
6313
6440
|
endedAt: snap.notice.at,
|
|
6314
|
-
settledAt: t,
|
|
6441
|
+
settledAt: snap.notice.queued === true ? snap.notice.at : t,
|
|
6442
|
+
confirmed: snap.notice.confirmed === true,
|
|
6315
6443
|
// The session the notice is ABOUT. Without it there is nothing to come back to, and the notice falls
|
|
6316
6444
|
// back to the older focus/timer rules below.
|
|
6317
6445
|
sessionId: snapSession,
|
|
@@ -6327,6 +6455,9 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6327
6455
|
* (a `turn/end` frame and the store's falling edge) are decided together.
|
|
6328
6456
|
*/
|
|
6329
6457
|
if (this.pendingNotice) {
|
|
6458
|
+
if (snap.notice && snap.notice.sessionId === this.pendingNotice.sessionId && snap.notice.confirmed === true) {
|
|
6459
|
+
this.pendingNotice.confirmed = true;
|
|
6460
|
+
}
|
|
6330
6461
|
var pendingSession = this.pendingNotice.sessionId;
|
|
6331
6462
|
/**
|
|
6332
6463
|
* Cancelled by WORK RESUMING, and only that — the ordering is the whole point.
|
|
@@ -6349,7 +6480,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6349
6480
|
* never produced one.
|
|
6350
6481
|
*/
|
|
6351
6482
|
this.pendingNotice = undefined;
|
|
6352
|
-
} else if (t - this.pendingNotice.settledAt >= this.settleFor(this.pendingNotice.sessionId)) {
|
|
6483
|
+
} else if (this.pendingNotice.confirmed || t - this.pendingNotice.settledAt >= this.settleFor(this.pendingNotice.sessionId)) {
|
|
6353
6484
|
this.notice = {
|
|
6354
6485
|
reason: this.pendingNotice.reason,
|
|
6355
6486
|
// Visible from NOW: the settle window was spent waiting, and a hold measured from the turn's end would
|
|
@@ -6425,8 +6556,10 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6425
6556
|
*/
|
|
6426
6557
|
var unreadDropped = isFiniteNumber(this.unreadAtNotice) && this.unreadCount < this.unreadAtNotice;
|
|
6427
6558
|
var neverUnread = this.unreadAtNotice === 0 && this.unreadCount === 0;
|
|
6428
|
-
|
|
6429
|
-
|
|
6559
|
+
var viewed = Array.isArray(snap.viewedCompletions) && snap.viewedCompletions.indexOf(notice.sessionId) !== -1;
|
|
6560
|
+
acknowledged = notice.sessionId
|
|
6561
|
+
? (viewed || ((!this.requireViewedAcknowledgement || notice.reason !== 'completed')
|
|
6562
|
+
&& presentId === notice.sessionId && this.focused === true))
|
|
6430
6563
|
: (notice.reason !== 'error' && this.focused === true
|
|
6431
6564
|
&& (unreadDropped || (neverUnread && this.interaction === null)));
|
|
6432
6565
|
this.pendingDismissalKind = notice.sessionId && presentId ? 'acknowledged' : 'no-session';
|
|
@@ -6448,8 +6581,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6448
6581
|
* still on screen would flip the pet to 出错 and hide the very prompt the user has to answer, and a
|
|
6449
6582
|
* turn that completes would show 任务达成 over an unanswered approval.
|
|
6450
6583
|
*
|
|
6451
|
-
*
|
|
6452
|
-
*
|
|
6584
|
+
* While the session reports a pending interaction, EVERY other state is skipped, including gestures
|
|
6585
|
+
* and greetings. When the interaction is answered the session
|
|
6453
6586
|
* stops reporting it, this latch releases, and the same frame's cascade decides where the pet goes.
|
|
6454
6587
|
*/
|
|
6455
6588
|
var pending = snap.pending === true;
|
|
@@ -6476,7 +6609,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6476
6609
|
/**
|
|
6477
6610
|
* 跳舞 — the user's own state, and the two rules that make it a good guest (operator, 2026-10-14).
|
|
6478
6611
|
*
|
|
6479
|
-
*
|
|
6612
|
+
* Attention comes first, followed by drag states, dance, greeting and offline.
|
|
6480
6613
|
* and it is dropped the moment the session is BUSY, which is the operator's 「跳舞也会被进入工作状态打断」 without a rule of its
|
|
6481
6614
|
* own: work outranks it because the busy branch below is reached first once this field is cleared. A dance also ends when the
|
|
6482
6615
|
* clip ends (the client watches the clip and calls `stopDancing`) or when the user double-clicks the pet, and both of those
|
|
@@ -6487,14 +6620,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6487
6620
|
this.stopDancing();
|
|
6488
6621
|
dance = null;
|
|
6489
6622
|
}
|
|
6490
|
-
if (
|
|
6623
|
+
if (pending) {
|
|
6624
|
+
state = 'waiting';
|
|
6625
|
+
} else if (this.interaction) {
|
|
6491
6626
|
state = this.interaction;
|
|
6492
6627
|
} else if (dance) {
|
|
6493
6628
|
state = 'dancing';
|
|
6494
6629
|
} else if (t < this.greetingUntil) {
|
|
6495
6630
|
state = 'greeting';
|
|
6496
|
-
} else if (pending) {
|
|
6497
|
-
state = 'waiting';
|
|
6498
6631
|
} else if (snap.connected === false && this.everConnected) {
|
|
6499
6632
|
/**
|
|
6500
6633
|
* 离线 means the host is GONE — not that it has nothing to say yet.
|
|
@@ -6594,6 +6727,11 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6594
6727
|
this.records = new Map();
|
|
6595
6728
|
/** Each session's own `running` flag as the store last reported it (see `observeStoreEdge`). */
|
|
6596
6729
|
this.storeRunning = new Map();
|
|
6730
|
+
this.completionChecks = new Map();
|
|
6731
|
+
this.attentionSessions = new Map();
|
|
6732
|
+
this.completionQueue = new Map();
|
|
6733
|
+
this.completionSeen = new Set();
|
|
6734
|
+
this.viewedCompletions = new Set();
|
|
6597
6735
|
/**
|
|
6598
6736
|
* When this aggregate's `running` answer last became true, and what it was last published as.
|
|
6599
6737
|
*
|
|
@@ -6653,6 +6791,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6653
6791
|
var status = statuses && statuses.get ? statuses.get(id) : undefined;
|
|
6654
6792
|
var running = status && typeof status.running === 'boolean' ? status.running : undefined;
|
|
6655
6793
|
if (running === undefined) return;
|
|
6794
|
+
if (running || status.pendingInteraction != null) self.completionChecks.delete(id);
|
|
6795
|
+
if (running) self.forgetCompletion(id);
|
|
6656
6796
|
var previous = self.storeRunning.get(id);
|
|
6657
6797
|
self.storeRunning.set(id, running);
|
|
6658
6798
|
if (previous === running) return;
|
|
@@ -6676,13 +6816,18 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6676
6816
|
* stream covers that case when it is wired, and a false 任务完成 on every boot is worse than a missed one.
|
|
6677
6817
|
*/
|
|
6678
6818
|
if (previous !== true) return;
|
|
6819
|
+
var boundaryRecord = self.records.get(id);
|
|
6820
|
+
if (boundaryRecord && boundaryRecord.lastEndReason && boundaryRecord.lastEndReason !== 'completed') return;
|
|
6679
6821
|
// Stopped: a completion. A failure already latched outranks it, the same rule the boundary path uses.
|
|
6822
|
+
var completion = { id: id + ':store', reason: 'completed', sessionId: id, at: self.now() };
|
|
6823
|
+
self.rememberCompletion(completion);
|
|
6680
6824
|
if (self.notice && self.notice.reason === 'error') return;
|
|
6681
|
-
self.notice =
|
|
6825
|
+
self.notice = completion;
|
|
6682
6826
|
});
|
|
6683
6827
|
};
|
|
6684
6828
|
|
|
6685
6829
|
SessionAggregate.prototype.reset = function (identities) {
|
|
6830
|
+
this.completionChecks.clear();
|
|
6686
6831
|
this.records.clear();
|
|
6687
6832
|
this.notice = undefined;
|
|
6688
6833
|
if (Array.isArray(identities)) {
|
|
@@ -6696,6 +6841,34 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6696
6841
|
}
|
|
6697
6842
|
};
|
|
6698
6843
|
|
|
6844
|
+
SessionAggregate.prototype.rememberCompletion = function (notice, announce) {
|
|
6845
|
+
var id = notice && notice.sessionId;
|
|
6846
|
+
if (!id || this.completionSeen.has(id)) return;
|
|
6847
|
+
this.completionSeen.add(id);
|
|
6848
|
+
this.completionQueue.set(id, Object.assign({}, notice, {
|
|
6849
|
+
announce: announce !== false, queued: true, confirmed: announce !== false,
|
|
6850
|
+
}));
|
|
6851
|
+
this.completionChecks.set(id, this.now());
|
|
6852
|
+
};
|
|
6853
|
+
|
|
6854
|
+
SessionAggregate.prototype.forgetCompletion = function (id) {
|
|
6855
|
+
this.completionQueue.delete(id);
|
|
6856
|
+
this.completionChecks.delete(id);
|
|
6857
|
+
this.completionSeen.delete(id);
|
|
6858
|
+
this.viewedCompletions.delete(id);
|
|
6859
|
+
};
|
|
6860
|
+
|
|
6861
|
+
SessionAggregate.prototype.acknowledgeViewed = function (id) {
|
|
6862
|
+
if (!id || this.running(id) !== false) return;
|
|
6863
|
+
var status = this.input.statuses.get ? this.input.statuses.get(id) : null;
|
|
6864
|
+
if (!this.completionQueue.has(id) && !(status && status.completionUnread === true)) return;
|
|
6865
|
+
this.completionSeen.add(id);
|
|
6866
|
+
this.viewedCompletions.add(id);
|
|
6867
|
+
this.completionQueue.delete(id);
|
|
6868
|
+
this.completionChecks.delete(id);
|
|
6869
|
+
if (this.notice && this.notice.sessionId === id && this.notice.reason === 'completed') this.notice = undefined;
|
|
6870
|
+
};
|
|
6871
|
+
|
|
6699
6872
|
SessionAggregate.prototype.boundary = function (event) {
|
|
6700
6873
|
if (!event || typeof event !== 'object') return this.snapshot();
|
|
6701
6874
|
var id = event.sessionId;
|
|
@@ -6722,12 +6895,16 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6722
6895
|
// The host's live answer for a session it vouches for: every root it classified arrives as `false`.
|
|
6723
6896
|
if (event.isSubagent === false) record.isSubagent = false;
|
|
6724
6897
|
if (event.type === 'turn/start') {
|
|
6898
|
+
this.forgetCompletion(id);
|
|
6725
6899
|
record.running = true;
|
|
6900
|
+
record.lastEndReason = null;
|
|
6901
|
+
this.completionChecks.delete(id);
|
|
6726
6902
|
record.candidate = undefined;
|
|
6727
6903
|
// A new turn supersedes whatever the last one reported, error included.
|
|
6728
6904
|
this.notice = undefined;
|
|
6729
6905
|
} else if (event.type === 'turn/end') {
|
|
6730
6906
|
record.running = false;
|
|
6907
|
+
record.lastEndReason = event.reason;
|
|
6731
6908
|
record.candidate = { id: id + ':' + event.seq, reason: event.reason, at: this.now() };
|
|
6732
6909
|
}
|
|
6733
6910
|
this.records.set(id, record);
|
|
@@ -6737,7 +6914,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6737
6914
|
* a frame for a session nobody can vouch for — not classified, not listed by the app — is silence rather than a
|
|
6738
6915
|
* 任务达成, because the pet has no way to tell it from a helper's turn ending (2026-10-14).
|
|
6739
6916
|
*/
|
|
6740
|
-
if (candidate && (candidate.reason === 'completed' ||
|
|
6917
|
+
if (candidate && (candidate.reason === 'completed' || candidate.reason === 'error') && this.mainSession(id)) {
|
|
6918
|
+
if (candidate.reason === 'completed') this.rememberCompletion({ id: candidate.id, reason: candidate.reason, sessionId: id, at: candidate.at });
|
|
6741
6919
|
record.candidate = undefined;
|
|
6742
6920
|
// A failure latches: a success arriving moments later must not erase the
|
|
6743
6921
|
// fact that a turn just died. The machine owns the same rule, so the two
|
|
@@ -6758,6 +6936,19 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6758
6936
|
};
|
|
6759
6937
|
|
|
6760
6938
|
SessionAggregate.prototype.subagent = function (id) {
|
|
6939
|
+
var list = this.input.catalog || {};
|
|
6940
|
+
var row = list.byId && list.byId[id];
|
|
6941
|
+
// The current DSH store publishes origin and its parents' subagent catalogs, not isSubagent.
|
|
6942
|
+
// These explicit identities outrank an older boundary baseline that called the agent a root.
|
|
6943
|
+
if (row && (row.origin === 'subagent' || row.isSubagent === true)) return true;
|
|
6944
|
+
var parents = Object.keys(list.byId || {});
|
|
6945
|
+
for (var p = 0; p < parents.length; p += 1) {
|
|
6946
|
+
var parent = list.byId[parents[p]];
|
|
6947
|
+
var projection = list.projectionsBySession && list.projectionsBySession[parents[p]];
|
|
6948
|
+
var values = (projection && projection.values) || (parent && parent.projectionValues);
|
|
6949
|
+
var children = values && values.subagentCatalog;
|
|
6950
|
+
if (Array.isArray(children) && children.some(function (child) { return child && child.id === id; })) return true;
|
|
6951
|
+
}
|
|
6761
6952
|
var record = this.records.get(id);
|
|
6762
6953
|
if (record && record.isSubagent !== undefined) return record.isSubagent === true;
|
|
6763
6954
|
var catalog = this.input.catalog && this.input.catalog.byId ? this.input.catalog.byId[id] : undefined;
|
|
@@ -6839,7 +7030,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6839
7030
|
* only ever make the answer STRICTER on a store that has the list.
|
|
6840
7031
|
*/
|
|
6841
7032
|
var listed = catalog.ids;
|
|
6842
|
-
if (listed && typeof listed.length === 'number'
|
|
7033
|
+
if (listed && typeof listed.length === 'number') {
|
|
6843
7034
|
for (var i = 0; i < listed.length; i += 1) {
|
|
6844
7035
|
if (listed[i] === id) return true;
|
|
6845
7036
|
}
|
|
@@ -6857,6 +7048,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6857
7048
|
|
|
6858
7049
|
var workingCount = 0;
|
|
6859
7050
|
var waitingCount = 0;
|
|
7051
|
+
var waitingSessionId = null;
|
|
6860
7052
|
/**
|
|
6861
7053
|
* How many sessions have a completion the user has NOT been back to.
|
|
6862
7054
|
*
|
|
@@ -6873,6 +7065,9 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6873
7065
|
* contributes nothing.
|
|
6874
7066
|
*/
|
|
6875
7067
|
var unreadCount = 0;
|
|
7068
|
+
var completedUnreadCount = 0;
|
|
7069
|
+
var completedSessionId = null;
|
|
7070
|
+
var completedSessionIds = [];
|
|
6876
7071
|
/**
|
|
6877
7072
|
* WHY that count is what it is, as counts rather than ids or titles — added with the 绿点 fix of 2026-10-14, so
|
|
6878
7073
|
* "the dot is lit and no session finished" is answerable from `/vrmpet3d/state` alone instead of by guesswork.
|
|
@@ -6888,9 +7083,32 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6888
7083
|
/** Which pending interaction wins when several sessions are waiting at once. Approval is the rarer. */
|
|
6889
7084
|
var waitingKind = null;
|
|
6890
7085
|
var self = this;
|
|
7086
|
+
this.attentionSessions.forEach(function (_kind, id) {
|
|
7087
|
+
var status = statuses.get ? statuses.get(id) : undefined;
|
|
7088
|
+
// Navigating away, a missing report, or losing focus is not an answer to the problem.
|
|
7089
|
+
if (status) {
|
|
7090
|
+
if (status.pendingInteraction == null) self.attentionSessions.delete(id);
|
|
7091
|
+
else self.attentionSessions.set(id, pendingInteractionKind(status.pendingInteraction));
|
|
7092
|
+
}
|
|
7093
|
+
});
|
|
6891
7094
|
ids.forEach(function (id) {
|
|
6892
7095
|
if (!self.inScope(id)) return;
|
|
6893
7096
|
var status = statuses.get ? statuses.get(id) : undefined;
|
|
7097
|
+
var record = self.records.get(id);
|
|
7098
|
+
if (status && status.completionUnread === true && self.mainSession(id) && !self.completionSeen.has(id)
|
|
7099
|
+
&& self.running(id) === false && status.pendingInteraction == null) {
|
|
7100
|
+
self.rememberCompletion({ sessionId: id, reason: 'completed', at: self.now() }, false);
|
|
7101
|
+
}
|
|
7102
|
+
var completed = self.completionQueue.has(id) && !self.viewedCompletions.has(id) && self.mainSession(id)
|
|
7103
|
+
&& self.running(id) === false && (!status || status.pendingInteraction == null)
|
|
7104
|
+
&& !(record && record.lastEndReason && record.lastEndReason !== 'completed');
|
|
7105
|
+
if (!completed) self.completionChecks.delete(id);
|
|
7106
|
+
else {
|
|
7107
|
+
if (!self.completionChecks.has(id)) self.completionChecks.set(id, self.now());
|
|
7108
|
+
if (self.completionQueue.get(id).confirmed || self.now() - self.completionChecks.get(id) >= COMPLETION_SETTLE_MS) {
|
|
7109
|
+
completedSessionIds.push(id);
|
|
7110
|
+
}
|
|
7111
|
+
}
|
|
6894
7112
|
if (status && status.completionUnread === true) {
|
|
6895
7113
|
unreadDetail.candidates += 1;
|
|
6896
7114
|
var entry = (catalog.byId || {})[id];
|
|
@@ -6913,14 +7131,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6913
7131
|
}
|
|
6914
7132
|
var pending = status ? status.pendingInteraction != null : false;
|
|
6915
7133
|
if (pending) {
|
|
6916
|
-
|
|
6917
|
-
var kind = pendingInteractionKind(status.pendingInteraction);
|
|
6918
|
-
// Ranked rather than first-wins: a global-scope pet watching ten sessions must not report
|
|
6919
|
-
// "a question" just because that session happens to come out of the map first when another one
|
|
6920
|
-
// is blocked on a permission prompt.
|
|
6921
|
-
if (waitingKind === null || PENDING_KIND_RANK.indexOf(kind) < PENDING_KIND_RANK.indexOf(waitingKind)) {
|
|
6922
|
-
waitingKind = kind;
|
|
6923
|
-
}
|
|
7134
|
+
self.attentionSessions.set(id, pendingInteractionKind(status.pendingInteraction));
|
|
6924
7135
|
}
|
|
6925
7136
|
// One rule for "this is the user's own session", shared with the two completion paths (`mainSession`).
|
|
6926
7137
|
if (self.mainSession(id) && self.running(id) === true) {
|
|
@@ -6931,7 +7142,23 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6931
7142
|
}
|
|
6932
7143
|
});
|
|
6933
7144
|
|
|
7145
|
+
this.attentionSessions.forEach(function (kind, id) {
|
|
7146
|
+
waitingCount++;
|
|
7147
|
+
if (waitingKind === null || PENDING_KIND_RANK.indexOf(kind) < PENDING_KIND_RANK.indexOf(waitingKind)) {
|
|
7148
|
+
waitingKind = kind;
|
|
7149
|
+
waitingSessionId = id;
|
|
7150
|
+
}
|
|
7151
|
+
});
|
|
7152
|
+
completedSessionIds.sort(function (a, b) { return self.completionQueue.get(a).at - self.completionQueue.get(b).at; });
|
|
7153
|
+
completedUnreadCount = completedSessionIds.length;
|
|
7154
|
+
completedSessionId = completedSessionIds[0] || null;
|
|
7155
|
+
var waitingSessionIds = Array.from(this.attentionSessions.keys()).sort(function (a, b) {
|
|
7156
|
+
return PENDING_KIND_RANK.indexOf(self.attentionSessions.get(a)) - PENDING_KIND_RANK.indexOf(self.attentionSessions.get(b));
|
|
7157
|
+
});
|
|
6934
7158
|
var ready = catalog.phase === 'ready';
|
|
7159
|
+
self.completionChecks.forEach(function (_at, id) {
|
|
7160
|
+
if (!ids.has(id) || !self.inScope(id)) self.completionChecks.delete(id);
|
|
7161
|
+
});
|
|
6935
7162
|
|
|
6936
7163
|
// Availability is "there is a session whose activity this pet could show".
|
|
6937
7164
|
// In `current` scope that must be proved by the selected session actually
|
|
@@ -6944,7 +7171,12 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6944
7171
|
|
|
6945
7172
|
var hasAnything = ready || workingCount > 0 || waitingCount > 0 || Boolean(this.notice);
|
|
6946
7173
|
var available = this.input.connected === true && hasAnything && hasSelectedSession;
|
|
6947
|
-
var
|
|
7174
|
+
var queuedNotice;
|
|
7175
|
+
this.completionQueue.forEach(function (entry, id) {
|
|
7176
|
+
if (!queuedNotice && entry.announce && self.mainSession(id) && self.inScope(id) && self.running(id) === false) queuedNotice = entry;
|
|
7177
|
+
});
|
|
7178
|
+
var notice = available ? (this.notice && this.notice.reason === 'error' ? this.notice
|
|
7179
|
+
: (queuedNotice || (this.notice && !this.viewedCompletions.has(this.notice.sessionId) ? this.notice : undefined))) : undefined;
|
|
6948
7180
|
/**
|
|
6949
7181
|
* `connected` rides along, because 离线 is decided by the CONNECTION and not by `available`.
|
|
6950
7182
|
*
|
|
@@ -7000,6 +7232,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
7000
7232
|
pendingKind: waitingCount > 0 ? waitingKind : null,
|
|
7001
7233
|
workingCount: workingCount,
|
|
7002
7234
|
waitingCount: waitingCount,
|
|
7235
|
+
waitingSessionId: waitingSessionId,
|
|
7236
|
+
waitingSessionIds: waitingSessionIds,
|
|
7003
7237
|
/**
|
|
7004
7238
|
* Sessions whose completion is still UNREAD (see above), and whether there are any.
|
|
7005
7239
|
*
|
|
@@ -7007,6 +7241,10 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
7007
7241
|
* surface could say more later without another plumbing round.
|
|
7008
7242
|
*/
|
|
7009
7243
|
unreadCount: unreadCount,
|
|
7244
|
+
completedUnreadCount: completedUnreadCount,
|
|
7245
|
+
completedSessionId: completedSessionId,
|
|
7246
|
+
completedSessionIds: completedSessionIds,
|
|
7247
|
+
viewedCompletions: Array.from(this.viewedCompletions),
|
|
7010
7248
|
hasUnread: unreadCount > 0,
|
|
7011
7249
|
/** The breakdown behind that count — see `unreadDetail` where it is built. */
|
|
7012
7250
|
unreadDetail: unreadDetail,
|