@clowder-ai/companion 0.1.0-alpha.10 → 0.1.0-alpha.11

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/manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "pluginId": "official.companion",
3
- "version": "0.1.0-alpha.10",
3
+ "version": "0.1.0-alpha.11",
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.10",
3
+ "version": "0.1.0-alpha.11",
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.10",
3
+ "version": "0.1.0-alpha.11",
4
4
  "contractVersion": "0.1.0",
5
5
  "name": "猫猫球",
6
6
  "icon": {
@@ -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
+ }
@@ -1,24 +1,5 @@
1
- // F317 living Xianxian package v3. Values are CSS anchors from its reviewed manifest.
2
- const scale = 120 / 290;
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 base = clips[action];
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
- video.style.width = `${clip.size[0] * scale}px`;
115
- video.style.height = `${clip.size[1] * scale}px`;
116
- const left = this.action === 'peek'
117
- ? (this.root.dataset.dockEdge === 'left' ? -32 : 128 - clip.size[0] * scale)
118
- : 60 - clip.centre * scale;
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', `${60 - clip.endCentre * scale}px`);
128
- video.style.setProperty?.('--living-top-end', `${groundY - clip.endGround * scale}px`);
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
+ }
@@ -179,6 +179,7 @@ export class PetMotion {
179
179
  }
180
180
 
181
181
  refresh(force = false) {
182
+ this.element.dataset.listening = String(this.listening());
182
183
  const selection = this.select();
183
184
  this.base = this.baseAction();
184
185
  const action = this.resolve(selection.action);
@@ -208,10 +209,9 @@ export class PetMotion {
208
209
  const { dx, dy } = this.snapshot.travel;
209
210
  return { action: Math.abs(dx) >= Math.abs(dy) ? dx < 0 ? 'running-left' : 'running-right' : 'running', oneShot: false };
210
211
  }
211
- if (['talking', 'connecting'].includes(this.phase) && !this.context?.muted)
212
- return { action: 'waiting', oneShot: false };
213
212
  if (this.transient?.action === 'play') return { action: 'play', oneShot: true };
214
213
  if (this.dockedEdge) return { action: 'peek', oneShot: false };
214
+ if (this.listening()) return { action: 'waiting', oneShot: false };
215
215
  if (this.resting) return { action: 'sleeping', oneShot: false };
216
216
  return { action: 'idle', oneShot: false };
217
217
  }
@@ -220,11 +220,13 @@ export class PetMotion {
220
220
  if (this.pendingDecision) return 'pending_decision';
221
221
  const work = this.activeWorkAction();
222
222
  if (work) return work;
223
- if (['talking', 'connecting'].includes(this.phase) && !this.context?.muted) return 'waiting';
224
223
  if (this.dockedEdge) return 'peek';
224
+ if (this.listening()) return 'waiting';
225
225
  return 'idle';
226
226
  }
227
227
 
228
+ listening() { return ['talking', 'connecting'].includes(this.phase) && !this.context?.muted; }
229
+
228
230
  activeWorkAction() {
229
231
  if (this.snapshot.work) return workAction[this.snapshot.work.kind];
230
232
  if (!this.hasNativeWork && this.context?.nativeActivity === 'tool_running') return 'working';
@@ -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 { currentCompanionIdentity } from './companion-identity.mjs';
14
15
 
15
16
  const $ = id => document.getElementById(id);
16
17
  const label = (id, text) => { $(id).querySelector('.label').textContent = text; };
@@ -108,21 +109,28 @@ if (!window.clowderCompanion) {
108
109
  label('mic', value.muted ? '取消静音' : '静音'); $('mic').setAttribute('aria-pressed', String(value.muted));
109
110
  label('speaker', value.silent ? '开启播音' : '关闭播音');
110
111
  const identity = value.identity;
112
+ const companionIdentity = currentCompanionIdentity(identity);
111
113
  motion.setContext({ skin: identity?.skin, phase: value.phase, nativeActivity: identity?.nativeActivity ?? 'none', muted: value.muted });
112
114
  motion.syncSnapshot(normalizeNativeWork(identity?.nativeWork));
113
115
  if (justConnected) motion.signal('connected');
114
116
  if (showFailure) motion.signal('failed');
115
117
  if (identity) {
116
118
  $('menu-play').hidden = identity.skin !== 'xianxian-codex';
117
- $('chat-name').textContent = threadTitle ?? identity.displayName; $('menu-name').textContent = identity.displayName;
118
- $('pet').setAttribute('aria-label', `${identity.displayName}:点击交流,右键更多,拖动移动`);
119
+ const companionLabel = companionIdentity
120
+ ? `${companionIdentity.title} · ${companionIdentity.partnerLabel}` : identity.displayName;
121
+ $('chat-name').textContent = threadTitle ?? companionLabel; $('menu-name').textContent = companionLabel;
122
+ $('pet').setAttribute('aria-label', `${companionLabel}:点击交流,右键更多,拖动移动`);
119
123
  $('documents').querySelector('.menu-status').textContent = identity.documentsAllowed ? '开启' : '暂停';
120
124
  $('documents').setAttribute('aria-pressed', String(identity.documentsAllowed));
121
- const live = identity.liveTransport?.kind === 'gpt_live_v3'
122
- ? 'GPT Live · 实时型号未证实' : '实时载体未确认';
123
- $('connection-scope').textContent = identity.duty.catId === identity.carrier.catId
124
- ? `${identity.duty.displayName} · ${live}`
125
- : `${identity.duty.displayName} · ${live} · 绑定载体 ${identity.carrier.displayName}`;
125
+ if (companionIdentity) {
126
+ $('connection-scope').textContent = `${companionIdentity.partnerLabel} · ${companionIdentity.liveLabel} · ${companionIdentity.deepLabel}`;
127
+ } else {
128
+ const live = identity.liveTransport?.kind === 'gpt_live_v3'
129
+ ? 'GPT Live · 实时型号未证实' : '实时载体未确认';
130
+ $('connection-scope').textContent = identity.duty.catId === identity.carrier.catId
131
+ ? `${identity.duty.displayName} · ${live}`
132
+ : `${identity.duty.displayName} · ${live} · 绑定载体 ${identity.carrier.displayName}`;
133
+ }
126
134
  }
127
135
  if (showFailure) void controls.show('actions');
128
136
  },