@clowder-ai/companion 0.1.0-alpha.11 → 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/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 +21 -24
- package/renderer/surface.mjs +10 -3
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
|
@@ -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;
|
package/renderer/surface.mjs
CHANGED
|
@@ -11,6 +11,7 @@ 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';
|
|
14
15
|
import { currentCompanionIdentity } from './companion-identity.mjs';
|
|
15
16
|
|
|
16
17
|
const $ = id => document.getElementById(id);
|
|
@@ -20,6 +21,10 @@ const transcript = new TranscriptView($('transcript'));
|
|
|
20
21
|
const bubble = new RecentBubble([$('bubble-first'), $('bubble-second')]);
|
|
21
22
|
const motion = new PetMotion($('pet'), { livingBody: new LivingBody({ root: $('pet'), sit: $('living-sit'),
|
|
22
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
|
+
});
|
|
23
28
|
let sharing = false, pendingScreen = false, loading = false, latestHistory, previousPhase = 'idle';
|
|
24
29
|
let observedHistory = false, lastAssistantId;
|
|
25
30
|
let resultPreviewUntil = 0;
|
|
@@ -33,11 +38,13 @@ if (!window.clowderCompanion) {
|
|
|
33
38
|
status('请从 Clowder 的聊聊入口打开猫猫球');
|
|
34
39
|
document.querySelectorAll('button').forEach(button => { button.disabled = true; });
|
|
35
40
|
} else {
|
|
41
|
+
nativeTravel.start();
|
|
36
42
|
const client = createCompanionClient(window.clowderCompanion);
|
|
37
43
|
const controls = bindPetControls(client, {
|
|
38
44
|
error: error => status(explainError(error)),
|
|
39
45
|
moved: (dx, dy) => {
|
|
40
|
-
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);
|
|
41
48
|
motion.stopMove();
|
|
42
49
|
if (!didMove) return;
|
|
43
50
|
didMove = false;
|
|
@@ -111,7 +118,7 @@ if (!window.clowderCompanion) {
|
|
|
111
118
|
const identity = value.identity;
|
|
112
119
|
const companionIdentity = currentCompanionIdentity(identity);
|
|
113
120
|
motion.setContext({ skin: identity?.skin, phase: value.phase, nativeActivity: identity?.nativeActivity ?? 'none', muted: value.muted });
|
|
114
|
-
motion.
|
|
121
|
+
motion.syncNativeSnapshot(normalizeNativeWork(identity?.nativeWork));
|
|
115
122
|
if (justConnected) motion.signal('connected');
|
|
116
123
|
if (showFailure) motion.signal('failed');
|
|
117
124
|
if (identity) {
|
|
@@ -235,6 +242,6 @@ if (!window.clowderCompanion) {
|
|
|
235
242
|
void conversation.refresh(); void readHistory(); void readDecisions(); void controls.show('none');
|
|
236
243
|
window.addEventListener('beforeunload', () => {
|
|
237
244
|
clearInterval(statusMonitor); clearInterval(historyMonitor); clearInterval(decisionMonitor);
|
|
238
|
-
unsubscribe(); motion.close(); void conversation.end();
|
|
245
|
+
unsubscribe(); nativeTravel.close(); motion.close(); void conversation.end();
|
|
239
246
|
});
|
|
240
247
|
}
|