@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 +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/pet-motion.mjs +5 -3
- package/renderer/surface.mjs +15 -7
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
|
+
}
|
package/renderer/pet-motion.mjs
CHANGED
|
@@ -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';
|
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 { 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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
},
|