@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 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, applied-result delivery and autonomous target movement.
30
- Dragging instead freezes a visibly held candidate and cancels the old target;
31
- it never renames the running clip. Pounce requires an explicit play gesture by
32
- default; optional automatic play is bounded by idle and cooldown timers.
33
- Reduced motion keeps semantic poses static and suppresses roaming and pounce.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "pluginId": "official.companion",
3
- "version": "0.1.0-alpha.11",
3
+ "version": "0.1.0-alpha.12",
4
4
  "contractVersion": "0.1.0",
5
5
  "name": "猫猫球",
6
6
  "icon": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clowder-ai/companion",
3
- "version": "0.1.0-alpha.11",
3
+ "version": "0.1.0-alpha.12",
4
4
  "description": "The desktop voice body for your Clowder companion",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/plugin.yaml CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "pluginId": "official.companion",
3
- "version": "0.1.0-alpha.11",
3
+ "version": "0.1.0-alpha.12",
4
4
  "contractVersion": "0.1.0",
5
5
  "name": "猫猫球",
6
6
  "icon": {
@@ -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: travel && validId(travel.eventId) && travel.status === 'active' && Number.isFinite(travel.dx)
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;
@@ -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.sync(value, this.dragging);
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;
@@ -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.syncSnapshot(normalizeNativeWork(identity?.nativeWork));
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
  }