@clowder-ai/companion 0.1.0-alpha.10 → 0.1.0-alpha.12
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/README.md +7 -5
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/plugin.yaml +1 -1
- package/renderer/companion-identity.mjs +41 -0
- package/renderer/living-body.mjs +12 -37
- package/renderer/living-geometry.mjs +96 -0
- package/renderer/motion-facts.mjs +33 -4
- package/renderer/native-window-travel.mjs +89 -0
- package/renderer/pet-motion-assets.mjs +22 -0
- package/renderer/pet-motion.mjs +26 -27
- package/renderer/surface.mjs +25 -10
package/README.md
CHANGED
|
@@ -26,11 +26,13 @@ SDP. Hiding releases media; the existing Clowder **聊聊** entry restores the c
|
|
|
26
26
|
|
|
27
27
|
Xianxian uses one layered sitting body or one VP9 clip at a time. Expiring Host
|
|
28
28
|
facts with task, result and event identities select tool work, workspace fetch,
|
|
29
|
-
screen reading
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
screen reading and applied-result delivery. Native travel is independent: the
|
|
30
|
+
renderer observes the actual desktop window coordinates, so a running clip can
|
|
31
|
+
appear only after the Host window really moves. Dragging suppresses that
|
|
32
|
+
observer and freezes a visibly held candidate instead of renaming the running
|
|
33
|
+
clip. Pounce requires an explicit play gesture by default; optional automatic
|
|
34
|
+
play is bounded by idle and cooldown timers. Reduced motion keeps semantic
|
|
35
|
+
poses static; a compatible Host also revokes its native movement lease.
|
|
34
36
|
Other cats retain their own skins. The v3 living assets and their hashes are
|
|
35
37
|
pinned in `source-lock.json`; the installable renderer carries all 21 assets
|
|
36
38
|
itself, including the mid-edge `peek_fade` variant.
|
package/manifest.json
CHANGED
package/package.json
CHANGED
package/plugin.yaml
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const named = value => typeof value === 'string' && value.trim().length > 0;
|
|
2
|
+
const actor = value => value && named(value.catId) && named(value.displayName);
|
|
3
|
+
const model = value => value === null || (named(value) && value.length <= 160);
|
|
4
|
+
|
|
5
|
+
function validSnapshot(value) {
|
|
6
|
+
return value?.v === 1 && value.name === '猫猫球'
|
|
7
|
+
&& actor(value.partner) && named(value.partner.skin)
|
|
8
|
+
&& actor(value.live) && value.live.transport === 'gpt_live_v3' && model(value.live.verifiedModel)
|
|
9
|
+
&& actor(value.deep) && value.deep.catId === value.partner.catId && model(value.deep.verifiedModel);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function display(snapshot, partnerLabel) {
|
|
13
|
+
return {
|
|
14
|
+
title: snapshot.name,
|
|
15
|
+
companionKnown: true,
|
|
16
|
+
partnerLabel,
|
|
17
|
+
avatarCatId: snapshot.partner.catId,
|
|
18
|
+
skin: snapshot.partner.skin,
|
|
19
|
+
liveLabel: `Live 快端:${snapshot.live.displayName} · ${snapshot.live.verifiedModel ?? '型号未核实'}`,
|
|
20
|
+
deepLabel: `深思端:${snapshot.deep.displayName} · ${snapshot.deep.verifiedModel ?? '型号未核实'}`,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The current face comes from the Host's selected duty, never the background carrier. */
|
|
25
|
+
export function currentCompanionIdentity(state) {
|
|
26
|
+
if (!actor(state?.duty) || !actor(state?.carrier) || !named(state.skin)
|
|
27
|
+
|| state.liveTransport?.kind !== 'gpt_live_v3' || !model(state.liveTransport.verifiedModel)) return null;
|
|
28
|
+
const snapshot = {
|
|
29
|
+
v: 1, name: '猫猫球',
|
|
30
|
+
partner: { ...state.duty, skin: state.skin },
|
|
31
|
+
live: { ...state.carrier, transport: state.liveTransport.kind, verifiedModel: state.liveTransport.verifiedModel },
|
|
32
|
+
deep: { ...state.duty, verifiedModel: null },
|
|
33
|
+
};
|
|
34
|
+
return display(snapshot, `${snapshot.partner.displayName}陪伴中`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A missing old snapshot never borrows today's preference. Keep the message's real author visible. */
|
|
38
|
+
export function historicalCompanionIdentity(snapshot, authorLabel) {
|
|
39
|
+
if (!validSnapshot(snapshot)) return { authorLabel, companionKnown: false };
|
|
40
|
+
return { ...display(snapshot, `当时由${snapshot.partner.displayName}陪伴`), authorLabel };
|
|
41
|
+
}
|
package/renderer/living-body.mjs
CHANGED
|
@@ -1,24 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const groundY = 10 + 262.5 * scale;
|
|
4
|
-
const clips = {
|
|
5
|
-
'running-left': { name: 'trot', size: [265, 236], ground: 234.8, centre: 132.5, loop: true },
|
|
6
|
-
'running-right': { name: 'trot', size: [265, 236], ground: 234.8, centre: 132.5, loop: true },
|
|
7
|
-
running: { name: 'trot', size: [265, 236], ground: 234.8, centre: 132.5, loop: true },
|
|
8
|
-
play: { name: 'pounce', size: [304.8, 312.6], ground: 303.9, centre: 201.9, rate: 1.5 },
|
|
9
|
-
sleeping: { name: 'sleep', size: [271.2, 168], ground: 161, centre: 133.2, loop: true },
|
|
10
|
-
waking: { name: 'wake', size: [400.2, 282.7], ground: 244.2, centre: 200.5,
|
|
11
|
-
endGround: 277.4, endCentre: 197.7, durationMs: 7_250, crossfadeMs: 250 },
|
|
12
|
-
peek: { name: 'peek', size: [153.5, 234.3], ground: 234.3, centre: 76.75, loop: true },
|
|
13
|
-
delivering: { name: 'carry', size: [344.3, 290.9], ground: 263, centre: 198.3 },
|
|
14
|
-
review: { name: 'carry', size: [344.3, 290.9], ground: 263, centre: 198.3 },
|
|
15
|
-
pending_decision: { name: 'wait', size: [197.9, 252.8], ground: 249.6, centre: 98.4, loop: true },
|
|
16
|
-
working: { name: 'think', size: [266.4, 256.9], ground: 253.7, centre: 132.1, loop: true },
|
|
17
|
-
fetching: { name: 'think', size: [266.4, 256.9], ground: 253.7, centre: 132.1, loop: true },
|
|
18
|
-
studying: { name: 'think', size: [266.4, 256.9], ground: 253.7, centre: 132.1, loop: true },
|
|
19
|
-
staged_thought: { name: 'think', size: [266.4, 256.9], ground: 253.7, centre: 132.1, loop: true },
|
|
20
|
-
};
|
|
21
|
-
export const isLivingClip = action => Object.hasOwn(clips, action);
|
|
1
|
+
import { isLivingClip, livingClipFor, placeLivingClip } from './living-geometry.mjs';
|
|
2
|
+
export { isLivingClip } from './living-geometry.mjs';
|
|
22
3
|
const setHidden = (node, hidden) => {
|
|
23
4
|
if (!node) return;
|
|
24
5
|
node.toggleAttribute?.('hidden', hidden);
|
|
@@ -51,10 +32,7 @@ export class LivingBody {
|
|
|
51
32
|
this.cancelFade();
|
|
52
33
|
this.root.dataset.pose = this.pose(action);
|
|
53
34
|
this.root.dataset.direction = action === 'running-left' || (action === 'peek' && dockEdge === 'left') ? 'left' : 'right';
|
|
54
|
-
const
|
|
55
|
-
const clip = !reducedMotion && base
|
|
56
|
-
? { ...base, name: action === 'peek' && dockZone !== 'corner' ? 'peek_fade' : base.name }
|
|
57
|
-
: null;
|
|
35
|
+
const clip = !reducedMotion ? livingClipFor(action, dockZone) : null;
|
|
58
36
|
if (!clip) {
|
|
59
37
|
this.hideVideos();
|
|
60
38
|
setHidden(this.sit, false);
|
|
@@ -111,21 +89,18 @@ export class LivingBody {
|
|
|
111
89
|
video.src = `./skins/xianxian-living/clips/${clip.name}.webm`;
|
|
112
90
|
video.loop = clip.loop === true;
|
|
113
91
|
video.playbackRate = clip.rate ?? 1;
|
|
114
|
-
|
|
115
|
-
video.style.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const top = this.action === 'peek' ? 138 - clip.size[1] * scale : groundY - clip.ground * scale;
|
|
120
|
-
video.style.left = `${left}px`;
|
|
121
|
-
video.style.top = `${top}px`;
|
|
92
|
+
const placement = placeLivingClip(this.action, clip, this.root.dataset.dockEdge);
|
|
93
|
+
video.style.width = `${placement.width}px`;
|
|
94
|
+
video.style.height = `${placement.height}px`;
|
|
95
|
+
video.style.left = `${placement.left}px`;
|
|
96
|
+
video.style.top = `${placement.top}px`;
|
|
122
97
|
video.style.animation = clip.endGround
|
|
123
98
|
? `living-wake-anchor ${clip.durationMs}ms linear forwards` : 'none';
|
|
124
99
|
if (clip.endGround) {
|
|
125
|
-
video.style.setProperty?.('--living-left-start', `${left}px`);
|
|
126
|
-
video.style.setProperty?.('--living-top-start', `${top}px`);
|
|
127
|
-
video.style.setProperty?.('--living-left-end', `${
|
|
128
|
-
video.style.setProperty?.('--living-top-end', `${
|
|
100
|
+
video.style.setProperty?.('--living-left-start', `${placement.left}px`);
|
|
101
|
+
video.style.setProperty?.('--living-top-start', `${placement.top}px`);
|
|
102
|
+
video.style.setProperty?.('--living-left-end', `${placement.endLeft}px`);
|
|
103
|
+
video.style.setProperty?.('--living-top-end', `${placement.endTop}px`);
|
|
129
104
|
}
|
|
130
105
|
try { video.currentTime = 0; } catch { /* New source is not yet seekable. */ }
|
|
131
106
|
video.onended = () => { if (revision === this.revision) onEnded(); };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// F317 living Xianxian package v3. All values are reviewed package CSS anchors;
|
|
2
|
+
// every clip uses one stage scale. content bounds are the union of decoded
|
|
3
|
+
// alpha pixels and exist to size the native transparent surface, not to apply
|
|
4
|
+
// per-action "looks right" scaling.
|
|
5
|
+
export const LIVING_GEOMETRY = Object.freeze({
|
|
6
|
+
pet: Object.freeze({ width: 120, height: 130 }),
|
|
7
|
+
stage: Object.freeze({ size: 290, centre: 145, ground: 262.5 }),
|
|
8
|
+
surface: Object.freeze({ left: -32, top: -18, right: 143, bottom: 138 }),
|
|
9
|
+
sitContent: Object.freeze({ left: 8.8, top: 3.4, right: 94.2, bottom: 109.2 }),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const LIVING_SCALE = LIVING_GEOMETRY.pet.width / LIVING_GEOMETRY.stage.size;
|
|
13
|
+
export const LIVING_REFERENCE = Object.freeze({
|
|
14
|
+
centre: LIVING_GEOMETRY.stage.centre * LIVING_SCALE,
|
|
15
|
+
ground: LIVING_GEOMETRY.stage.ground * LIVING_SCALE,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const clip = (name, size, ground, centre, content, options = {}) => Object.freeze({
|
|
19
|
+
name, size: Object.freeze(size), ground, centre, content: Object.freeze(content), ...options,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const library = Object.freeze({
|
|
23
|
+
trot: clip('trot', [265, 236], 234.8, 132.5,
|
|
24
|
+
{ left: 1.5, top: 2, right: 263.5, bottom: 235.5 }, { loop: true }),
|
|
25
|
+
pounce: clip('pounce', [304.8, 312.6], 303.9, 201.9,
|
|
26
|
+
{ left: 3, top: 0, right: 302, bottom: 312 }, { rate: 1.5 }),
|
|
27
|
+
sleep: clip('sleep', [271.2, 168], 161, 133.2,
|
|
28
|
+
{ left: 2.5, top: 2.5, right: 268.5, bottom: 166.5 }, { loop: true }),
|
|
29
|
+
wake: clip('wake', [400.2, 282.7], 244.2, 200.5,
|
|
30
|
+
{ left: 3, top: 3, right: 397, bottom: 280.5 },
|
|
31
|
+
{ endGround: 277.4, endCentre: 197.7, durationMs: 7_250, crossfadeMs: 250 }),
|
|
32
|
+
peek: clip('peek', [153.5, 234.3], 234.3, 76.75,
|
|
33
|
+
{ left: 2.5, top: 2.5, right: 153, bottom: 234 }, { loop: true }),
|
|
34
|
+
peek_fade: clip('peek_fade', [153.5, 234.3], 234.3, 76.75,
|
|
35
|
+
{ left: 2.5, top: 2.5, right: 153, bottom: 231.5 }, { loop: true }),
|
|
36
|
+
carry: clip('carry', [344.3, 290.9], 263, 198.3,
|
|
37
|
+
{ left: 4, top: 4, right: 340, bottom: 288.5 }),
|
|
38
|
+
wait: clip('wait', [197.9, 252.8], 249.6, 98.4,
|
|
39
|
+
{ left: 4, top: 4, right: 194, bottom: 251 }, { loop: true }),
|
|
40
|
+
think: clip('think', [266.4, 256.9], 253.7, 132.1,
|
|
41
|
+
{ left: 3.5, top: 3.5, right: 262, bottom: 255 }, { loop: true }),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const actionClip = Object.freeze({
|
|
45
|
+
'running-left': 'trot',
|
|
46
|
+
'running-right': 'trot',
|
|
47
|
+
running: 'trot',
|
|
48
|
+
play: 'pounce',
|
|
49
|
+
sleeping: 'sleep',
|
|
50
|
+
waking: 'wake',
|
|
51
|
+
peek: 'peek',
|
|
52
|
+
delivering: 'carry',
|
|
53
|
+
review: 'carry',
|
|
54
|
+
pending_decision: 'wait',
|
|
55
|
+
working: 'think',
|
|
56
|
+
fetching: 'think',
|
|
57
|
+
studying: 'think',
|
|
58
|
+
staged_thought: 'think',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export const isLivingClip = action => Object.hasOwn(actionClip, action);
|
|
62
|
+
|
|
63
|
+
export function livingClipFor(action, dockZone) {
|
|
64
|
+
const name = action === 'peek' && dockZone !== 'corner' ? 'peek_fade' : actionClip[action];
|
|
65
|
+
return name ? library[name] : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function placeLivingClip(action, selected, dockEdge) {
|
|
69
|
+
const width = selected.size[0] * LIVING_SCALE;
|
|
70
|
+
const height = selected.size[1] * LIVING_SCALE;
|
|
71
|
+
const left = action === 'peek'
|
|
72
|
+
? (dockEdge === 'left' ? LIVING_GEOMETRY.surface.left : LIVING_GEOMETRY.surface.right - width)
|
|
73
|
+
: LIVING_REFERENCE.centre - selected.centre * LIVING_SCALE;
|
|
74
|
+
const top = action === 'peek'
|
|
75
|
+
? LIVING_GEOMETRY.surface.bottom - height
|
|
76
|
+
: LIVING_REFERENCE.ground - selected.ground * LIVING_SCALE;
|
|
77
|
+
return Object.freeze({
|
|
78
|
+
left, top, width, height,
|
|
79
|
+
endLeft: selected.endCentre === undefined
|
|
80
|
+
? undefined : LIVING_REFERENCE.centre - selected.endCentre * LIVING_SCALE,
|
|
81
|
+
endTop: selected.endGround === undefined
|
|
82
|
+
? undefined : LIVING_REFERENCE.ground - selected.endGround * LIVING_SCALE,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function livingContentRect(selected, placement, endpoint = 'start') {
|
|
87
|
+
const left = endpoint === 'end' ? placement.endLeft : placement.left;
|
|
88
|
+
const top = endpoint === 'end' ? placement.endTop : placement.top;
|
|
89
|
+
if (!Number.isFinite(left) || !Number.isFinite(top)) return null;
|
|
90
|
+
return Object.freeze({
|
|
91
|
+
left: left + selected.content.left * LIVING_SCALE,
|
|
92
|
+
top: top + selected.content.top * LIVING_SCALE,
|
|
93
|
+
right: left + selected.content.right * LIVING_SCALE,
|
|
94
|
+
bottom: top + selected.content.bottom * LIVING_SCALE,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
const validId = value => typeof value === 'string' && value.length > 0 && value.length <= 256;
|
|
2
2
|
const validExpiry = (value, now) => Number.isFinite(value) && value > now;
|
|
3
3
|
const workKinds = new Set(['reasoning', 'tool', 'workspace', 'screen_reading']);
|
|
4
|
+
const normalizeTravel = (travel, now, suppressed) => travel && validId(travel.eventId)
|
|
5
|
+
&& travel.status === 'active' && Number.isFinite(travel.dx) && Number.isFinite(travel.dy)
|
|
6
|
+
&& Math.hypot(travel.dx, travel.dy) >= 1 && validExpiry(travel.expiresAt, now)
|
|
7
|
+
&& !suppressed.has(travel.eventId)
|
|
8
|
+
? { eventId: travel.eventId, dx: travel.dx, dy: travel.dy, expiresAt: travel.expiresAt } : null;
|
|
4
9
|
|
|
5
10
|
/** A bounded, expiring projection of Host-owned motion facts. */
|
|
6
11
|
export class MotionFacts {
|
|
@@ -28,15 +33,39 @@ export class MotionFacts {
|
|
|
28
33
|
delivery: delivery && validId(delivery.resultId) && delivery.status === 'applied'
|
|
29
34
|
&& validExpiry(delivery.expiresAt, now)
|
|
30
35
|
? { resultId: delivery.resultId, expiresAt: delivery.expiresAt } : null,
|
|
31
|
-
travel:
|
|
32
|
-
&& Number.isFinite(travel.dy) && Math.hypot(travel.dx, travel.dy) >= 1
|
|
33
|
-
&& validExpiry(travel.expiresAt, now) && !this.suppressedTravel.has(travel.eventId)
|
|
34
|
-
? { eventId: travel.eventId, dx: travel.dx, dy: travel.dy, expiresAt: travel.expiresAt } : null,
|
|
36
|
+
travel: normalizeTravel(travel, now, this.suppressedTravel),
|
|
35
37
|
};
|
|
36
38
|
if (suppressTravel) this.suppressTravel();
|
|
37
39
|
this.scheduleExpiry();
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
syncNative(value) {
|
|
43
|
+
const now = this.now();
|
|
44
|
+
this.prune(now);
|
|
45
|
+
const input = value && typeof value === 'object' ? value : {};
|
|
46
|
+
const work = input.work;
|
|
47
|
+
const delivery = input.delivery;
|
|
48
|
+
const travel = this.snapshot.travel?.expiresAt > now ? this.snapshot.travel : null;
|
|
49
|
+
this.snapshot = {
|
|
50
|
+
work: work && validId(work.taskId) && work.status === 'active' && workKinds.has(work.kind)
|
|
51
|
+
&& validExpiry(work.expiresAt, now)
|
|
52
|
+
? { taskId: work.taskId, kind: work.kind, expiresAt: work.expiresAt } : null,
|
|
53
|
+
delivery: delivery && validId(delivery.resultId) && delivery.status === 'applied'
|
|
54
|
+
&& validExpiry(delivery.expiresAt, now)
|
|
55
|
+
? { resultId: delivery.resultId, expiresAt: delivery.expiresAt } : null,
|
|
56
|
+
travel,
|
|
57
|
+
};
|
|
58
|
+
this.scheduleExpiry();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
syncTravel(value, suppressTravel = false) {
|
|
62
|
+
const now = this.now();
|
|
63
|
+
this.prune(now);
|
|
64
|
+
this.snapshot.travel = normalizeTravel(value, now, this.suppressedTravel);
|
|
65
|
+
if (suppressTravel) this.suppressTravel();
|
|
66
|
+
this.scheduleExpiry();
|
|
67
|
+
}
|
|
68
|
+
|
|
40
69
|
consumeDelivery() {
|
|
41
70
|
const delivery = this.snapshot.delivery;
|
|
42
71
|
if (!delivery || this.seenResults.has(delivery.resultId)) return null;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const validPosition = value => value && Number.isFinite(value.x) && Number.isFinite(value.y);
|
|
2
|
+
|
|
3
|
+
/** Observe the native window itself. Running animation follows measured desktop
|
|
4
|
+
* movement; renderer state never invents a target or claims a move succeeded. */
|
|
5
|
+
export class NativeWindowTravel {
|
|
6
|
+
constructor({
|
|
7
|
+
readPosition,
|
|
8
|
+
changed,
|
|
9
|
+
now = Date.now,
|
|
10
|
+
setTimer = globalThis.setTimeout,
|
|
11
|
+
clearTimer = globalThis.clearTimeout,
|
|
12
|
+
intervalMs = 80,
|
|
13
|
+
expiresMs = 240,
|
|
14
|
+
}) {
|
|
15
|
+
this.readPosition = readPosition;
|
|
16
|
+
this.changed = changed;
|
|
17
|
+
this.now = now;
|
|
18
|
+
this.setTimer = (callback, delay) => setTimer.call(globalThis, callback, delay);
|
|
19
|
+
this.clearTimer = id => clearTimer.call(globalThis, id);
|
|
20
|
+
this.intervalMs = Math.max(32, intervalMs);
|
|
21
|
+
this.expiresMs = Math.max(this.intervalMs, expiresMs);
|
|
22
|
+
this.sequence = 0;
|
|
23
|
+
this.manual = false;
|
|
24
|
+
this.closed = false;
|
|
25
|
+
this.active = null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
start() {
|
|
29
|
+
if (this.closed || this.timer !== undefined) return;
|
|
30
|
+
this.previous = this.read();
|
|
31
|
+
this.schedule();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setManual(value) {
|
|
35
|
+
const manual = value === true;
|
|
36
|
+
if (manual === this.manual) return;
|
|
37
|
+
this.manual = manual;
|
|
38
|
+
this.previous = this.read();
|
|
39
|
+
if (manual) this.clearActive();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
tick() {
|
|
43
|
+
this.timer = undefined;
|
|
44
|
+
if (this.closed) return;
|
|
45
|
+
const next = this.read();
|
|
46
|
+
const now = this.now();
|
|
47
|
+
if (next && this.previous) {
|
|
48
|
+
const dx = next.x - this.previous.x;
|
|
49
|
+
const dy = next.y - this.previous.y;
|
|
50
|
+
if (!this.manual && Math.hypot(dx, dy) >= 1) {
|
|
51
|
+
const eventId = this.active?.eventId ?? `native-${Math.trunc(now)}-${++this.sequence}`;
|
|
52
|
+
this.active = { eventId, status: 'active', dx, dy, expiresAt: now + this.expiresMs };
|
|
53
|
+
this.changed({ ...this.active });
|
|
54
|
+
} else if (this.active && this.active.expiresAt <= now) {
|
|
55
|
+
this.clearActive();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
this.previous = next;
|
|
59
|
+
this.schedule();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
close() {
|
|
63
|
+
if (this.closed) return;
|
|
64
|
+
this.closed = true;
|
|
65
|
+
if (this.timer !== undefined) this.clearTimer(this.timer);
|
|
66
|
+
this.timer = undefined;
|
|
67
|
+
this.clearActive();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
read() {
|
|
71
|
+
try {
|
|
72
|
+
const value = this.readPosition();
|
|
73
|
+
return validPosition(value) ? { x: value.x, y: value.y } : null;
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
schedule() {
|
|
80
|
+
if (this.closed || this.timer !== undefined) return;
|
|
81
|
+
this.timer = this.setTimer(() => this.tick(), this.intervalMs);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
clearActive() {
|
|
85
|
+
if (!this.active) return;
|
|
86
|
+
this.active = null;
|
|
87
|
+
this.changed(null);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// F229 pet.json frame durations, plus the three F258 rows pinned in source-lock.json.
|
|
2
|
+
export const atlas = {
|
|
3
|
+
idle: { row: 0, frames: [1200, 400, 400, 500, 500, 800] },
|
|
4
|
+
'running-right': { row: 1, frames: [120, 120, 120, 120, 120, 120, 120, 220] },
|
|
5
|
+
'running-left': { row: 2, frames: [120, 120, 120, 120, 120, 120, 120, 220] },
|
|
6
|
+
waving: { row: 3, frames: [140, 140, 140, 280] },
|
|
7
|
+
jumping: { row: 4, frames: [140, 140, 140, 140, 280] },
|
|
8
|
+
failed: { row: 5, frames: [140, 140, 140, 140, 140, 140, 140, 240] },
|
|
9
|
+
waiting: { row: 6, frames: [150, 150, 150, 150, 150, 260] },
|
|
10
|
+
running: { row: 7, frames: [120, 120, 120, 120, 120, 220] },
|
|
11
|
+
review: { row: 8, frames: [150, 150, 150, 150, 150, 280] },
|
|
12
|
+
};
|
|
13
|
+
export const additional = {
|
|
14
|
+
sleeping: { file: 'xianxian-sleeping-row.png', frames: [700, 700, 700, 700, 700, 700] },
|
|
15
|
+
working: { file: 'xianxian-working-row.png', frames: [520, 560, 520, 640, 520, 560, 520, 640] },
|
|
16
|
+
staged_thought: { file: 'xianxian-staged-thought-row.png', frames: [1200, 500, 700, 900, 1200, 500] },
|
|
17
|
+
};
|
|
18
|
+
export const visualAlias = { fetching: 'working', studying: 'staged_thought', delivering: 'review', held: 'idle' };
|
|
19
|
+
export const workAction = { reasoning: 'staged_thought', tool: 'working', workspace: 'fetching', screen_reading: 'studying' };
|
|
20
|
+
export const signals = { connected: 'waving', received: 'jumping', failed: 'failed' };
|
|
21
|
+
export const staticSkins = { 'ragdoll-v1': 'ragdoll-v1.png', 'yarn-ball': 'yarn-ball.png' };
|
|
22
|
+
export const validId = value => typeof value === 'string' && value.length > 0 && value.length <= 256;
|
package/renderer/pet-motion.mjs
CHANGED
|
@@ -1,28 +1,6 @@
|
|
|
1
1
|
import { isLivingClip } from './living-body.mjs';
|
|
2
2
|
import { MotionFacts } from './motion-facts.mjs';
|
|
3
|
-
|
|
4
|
-
// F229 pet.json frame durations, plus the three F258 rows pinned in source-lock.json.
|
|
5
|
-
const atlas = {
|
|
6
|
-
idle: { row: 0, frames: [1200, 400, 400, 500, 500, 800] },
|
|
7
|
-
'running-right': { row: 1, frames: [120, 120, 120, 120, 120, 120, 120, 220] },
|
|
8
|
-
'running-left': { row: 2, frames: [120, 120, 120, 120, 120, 120, 120, 220] },
|
|
9
|
-
waving: { row: 3, frames: [140, 140, 140, 280] },
|
|
10
|
-
jumping: { row: 4, frames: [140, 140, 140, 140, 280] },
|
|
11
|
-
failed: { row: 5, frames: [140, 140, 140, 140, 140, 140, 140, 240] },
|
|
12
|
-
waiting: { row: 6, frames: [150, 150, 150, 150, 150, 260] },
|
|
13
|
-
running: { row: 7, frames: [120, 120, 120, 120, 120, 220] },
|
|
14
|
-
review: { row: 8, frames: [150, 150, 150, 150, 150, 280] },
|
|
15
|
-
};
|
|
16
|
-
const additional = {
|
|
17
|
-
sleeping: { file: 'xianxian-sleeping-row.png', frames: [700, 700, 700, 700, 700, 700] },
|
|
18
|
-
working: { file: 'xianxian-working-row.png', frames: [520, 560, 520, 640, 520, 560, 520, 640] },
|
|
19
|
-
staged_thought: { file: 'xianxian-staged-thought-row.png', frames: [1200, 500, 700, 900, 1200, 500] },
|
|
20
|
-
};
|
|
21
|
-
const visualAlias = { fetching: 'working', studying: 'staged_thought', delivering: 'review', held: 'idle' };
|
|
22
|
-
const workAction = { reasoning: 'staged_thought', tool: 'working', workspace: 'fetching', screen_reading: 'studying' };
|
|
23
|
-
const signals = { connected: 'waving', received: 'jumping', failed: 'failed' };
|
|
24
|
-
const staticSkins = { 'ragdoll-v1': 'ragdoll-v1.png', 'yarn-ball': 'yarn-ball.png' };
|
|
25
|
-
const validId = value => typeof value === 'string' && value.length > 0 && value.length <= 256;
|
|
3
|
+
import { additional, atlas, signals, staticSkins, validId, visualAlias, workAction } from './pet-motion-assets.mjs';
|
|
26
4
|
|
|
27
5
|
/** Select one truthful body from Host facts and local, user-controlled gestures. */
|
|
28
6
|
export class PetMotion {
|
|
@@ -75,10 +53,19 @@ export class PetMotion {
|
|
|
75
53
|
}
|
|
76
54
|
|
|
77
55
|
syncSnapshot(value) {
|
|
56
|
+
this.syncFacts(value, false);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
syncNativeSnapshot(value) {
|
|
60
|
+
this.syncFacts(value, true);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
syncFacts(value, nativeOnly) {
|
|
78
64
|
const now = this.now();
|
|
79
65
|
this.hasNativeWork = value !== null && value !== undefined;
|
|
80
66
|
for (const [id, expiresAt] of this.seenEvents) if (expiresAt <= now) this.seenEvents.delete(id);
|
|
81
|
-
this.facts.
|
|
67
|
+
if (nativeOnly) this.facts.syncNative(value);
|
|
68
|
+
else this.facts.sync(value, this.dragging);
|
|
82
69
|
if (this.snapshot.work) {
|
|
83
70
|
this.resting = false;
|
|
84
71
|
this.cancelIdleTimers();
|
|
@@ -91,6 +78,16 @@ export class PetMotion {
|
|
|
91
78
|
this.refresh();
|
|
92
79
|
}
|
|
93
80
|
|
|
81
|
+
syncTravel(value) {
|
|
82
|
+
this.facts.syncTravel(value, this.dragging);
|
|
83
|
+
if (this.resting && this.snapshot.travel) {
|
|
84
|
+
this.resting = false;
|
|
85
|
+
this.cancelRest();
|
|
86
|
+
this.transient = { action: 'waking' };
|
|
87
|
+
}
|
|
88
|
+
this.refresh();
|
|
89
|
+
}
|
|
90
|
+
|
|
94
91
|
setPendingDecision(value) {
|
|
95
92
|
const pending = value === true;
|
|
96
93
|
if (this.pendingDecision === pending) return;
|
|
@@ -179,6 +176,7 @@ export class PetMotion {
|
|
|
179
176
|
}
|
|
180
177
|
|
|
181
178
|
refresh(force = false) {
|
|
179
|
+
this.element.dataset.listening = String(this.listening());
|
|
182
180
|
const selection = this.select();
|
|
183
181
|
this.base = this.baseAction();
|
|
184
182
|
const action = this.resolve(selection.action);
|
|
@@ -208,10 +206,9 @@ export class PetMotion {
|
|
|
208
206
|
const { dx, dy } = this.snapshot.travel;
|
|
209
207
|
return { action: Math.abs(dx) >= Math.abs(dy) ? dx < 0 ? 'running-left' : 'running-right' : 'running', oneShot: false };
|
|
210
208
|
}
|
|
211
|
-
if (['talking', 'connecting'].includes(this.phase) && !this.context?.muted)
|
|
212
|
-
return { action: 'waiting', oneShot: false };
|
|
213
209
|
if (this.transient?.action === 'play') return { action: 'play', oneShot: true };
|
|
214
210
|
if (this.dockedEdge) return { action: 'peek', oneShot: false };
|
|
211
|
+
if (this.listening()) return { action: 'waiting', oneShot: false };
|
|
215
212
|
if (this.resting) return { action: 'sleeping', oneShot: false };
|
|
216
213
|
return { action: 'idle', oneShot: false };
|
|
217
214
|
}
|
|
@@ -220,11 +217,13 @@ export class PetMotion {
|
|
|
220
217
|
if (this.pendingDecision) return 'pending_decision';
|
|
221
218
|
const work = this.activeWorkAction();
|
|
222
219
|
if (work) return work;
|
|
223
|
-
if (['talking', 'connecting'].includes(this.phase) && !this.context?.muted) return 'waiting';
|
|
224
220
|
if (this.dockedEdge) return 'peek';
|
|
221
|
+
if (this.listening()) return 'waiting';
|
|
225
222
|
return 'idle';
|
|
226
223
|
}
|
|
227
224
|
|
|
225
|
+
listening() { return ['talking', 'connecting'].includes(this.phase) && !this.context?.muted; }
|
|
226
|
+
|
|
228
227
|
activeWorkAction() {
|
|
229
228
|
if (this.snapshot.work) return workAction[this.snapshot.work.kind];
|
|
230
229
|
if (!this.hasNativeWork && this.context?.nativeActivity === 'tool_running') return 'working';
|
package/renderer/surface.mjs
CHANGED
|
@@ -11,6 +11,8 @@ import { detectDockedEdge } from './living-edge.mjs';
|
|
|
11
11
|
import { bindPetControls } from './pet-controls.mjs';
|
|
12
12
|
import { decisionBadge, decisionRows } from './decision-view.mjs';
|
|
13
13
|
import { normalizeNativeWork } from './native-work-motion.mjs';
|
|
14
|
+
import { NativeWindowTravel } from './native-window-travel.mjs';
|
|
15
|
+
import { currentCompanionIdentity } from './companion-identity.mjs';
|
|
14
16
|
|
|
15
17
|
const $ = id => document.getElementById(id);
|
|
16
18
|
const label = (id, text) => { $(id).querySelector('.label').textContent = text; };
|
|
@@ -19,6 +21,10 @@ const transcript = new TranscriptView($('transcript'));
|
|
|
19
21
|
const bubble = new RecentBubble([$('bubble-first'), $('bubble-second')]);
|
|
20
22
|
const motion = new PetMotion($('pet'), { livingBody: new LivingBody({ root: $('pet'), sit: $('living-sit'),
|
|
21
23
|
video: $('living-video'), transitionVideo: $('living-video-transition') }) });
|
|
24
|
+
const nativeTravel = new NativeWindowTravel({
|
|
25
|
+
readPosition: () => ({ x: window.screenX, y: window.screenY }),
|
|
26
|
+
changed: value => motion.syncTravel(value),
|
|
27
|
+
});
|
|
22
28
|
let sharing = false, pendingScreen = false, loading = false, latestHistory, previousPhase = 'idle';
|
|
23
29
|
let observedHistory = false, lastAssistantId;
|
|
24
30
|
let resultPreviewUntil = 0;
|
|
@@ -32,11 +38,13 @@ if (!window.clowderCompanion) {
|
|
|
32
38
|
status('请从 Clowder 的聊聊入口打开猫猫球');
|
|
33
39
|
document.querySelectorAll('button').forEach(button => { button.disabled = true; });
|
|
34
40
|
} else {
|
|
41
|
+
nativeTravel.start();
|
|
35
42
|
const client = createCompanionClient(window.clowderCompanion);
|
|
36
43
|
const controls = bindPetControls(client, {
|
|
37
44
|
error: error => status(explainError(error)),
|
|
38
45
|
moved: (dx, dy) => {
|
|
39
|
-
if (dx !== 'stop') { didMove = true; motion.move(dx, dy); return; }
|
|
46
|
+
if (dx !== 'stop') { didMove = true; nativeTravel.setManual(true); motion.move(dx, dy); return; }
|
|
47
|
+
nativeTravel.setManual(false);
|
|
40
48
|
motion.stopMove();
|
|
41
49
|
if (!didMove) return;
|
|
42
50
|
didMove = false;
|
|
@@ -108,21 +116,28 @@ if (!window.clowderCompanion) {
|
|
|
108
116
|
label('mic', value.muted ? '取消静音' : '静音'); $('mic').setAttribute('aria-pressed', String(value.muted));
|
|
109
117
|
label('speaker', value.silent ? '开启播音' : '关闭播音');
|
|
110
118
|
const identity = value.identity;
|
|
119
|
+
const companionIdentity = currentCompanionIdentity(identity);
|
|
111
120
|
motion.setContext({ skin: identity?.skin, phase: value.phase, nativeActivity: identity?.nativeActivity ?? 'none', muted: value.muted });
|
|
112
|
-
motion.
|
|
121
|
+
motion.syncNativeSnapshot(normalizeNativeWork(identity?.nativeWork));
|
|
113
122
|
if (justConnected) motion.signal('connected');
|
|
114
123
|
if (showFailure) motion.signal('failed');
|
|
115
124
|
if (identity) {
|
|
116
125
|
$('menu-play').hidden = identity.skin !== 'xianxian-codex';
|
|
117
|
-
|
|
118
|
-
|
|
126
|
+
const companionLabel = companionIdentity
|
|
127
|
+
? `${companionIdentity.title} · ${companionIdentity.partnerLabel}` : identity.displayName;
|
|
128
|
+
$('chat-name').textContent = threadTitle ?? companionLabel; $('menu-name').textContent = companionLabel;
|
|
129
|
+
$('pet').setAttribute('aria-label', `${companionLabel}:点击交流,右键更多,拖动移动`);
|
|
119
130
|
$('documents').querySelector('.menu-status').textContent = identity.documentsAllowed ? '开启' : '暂停';
|
|
120
131
|
$('documents').setAttribute('aria-pressed', String(identity.documentsAllowed));
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
132
|
+
if (companionIdentity) {
|
|
133
|
+
$('connection-scope').textContent = `${companionIdentity.partnerLabel} · ${companionIdentity.liveLabel} · ${companionIdentity.deepLabel}`;
|
|
134
|
+
} else {
|
|
135
|
+
const live = identity.liveTransport?.kind === 'gpt_live_v3'
|
|
136
|
+
? 'GPT Live · 实时型号未证实' : '实时载体未确认';
|
|
137
|
+
$('connection-scope').textContent = identity.duty.catId === identity.carrier.catId
|
|
138
|
+
? `${identity.duty.displayName} · ${live}`
|
|
139
|
+
: `${identity.duty.displayName} · ${live} · 绑定载体 ${identity.carrier.displayName}`;
|
|
140
|
+
}
|
|
126
141
|
}
|
|
127
142
|
if (showFailure) void controls.show('actions');
|
|
128
143
|
},
|
|
@@ -227,6 +242,6 @@ if (!window.clowderCompanion) {
|
|
|
227
242
|
void conversation.refresh(); void readHistory(); void readDecisions(); void controls.show('none');
|
|
228
243
|
window.addEventListener('beforeunload', () => {
|
|
229
244
|
clearInterval(statusMonitor); clearInterval(historyMonitor); clearInterval(decisionMonitor);
|
|
230
|
-
unsubscribe(); motion.close(); void conversation.end();
|
|
245
|
+
unsubscribe(); nativeTravel.close(); motion.close(); void conversation.end();
|
|
231
246
|
});
|
|
232
247
|
}
|