peepface 0.0.0-stage → 0.1.0
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/LICENSE +202 -0
- package/README.md +213 -2
- package/core.js +328 -0
- package/dist/dsh-client.js +665 -0
- package/dist/peepface.js +329 -0
- package/dist/peepface.mjs +10 -0
- package/package.json +52 -3
- package/peepface.d.ts +43 -0
- package/states.json +274 -0
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
/* peepface v0.1.0 — 由 build.mjs 从 core.js 生成,不要手改 */
|
|
2
|
+
window.__ModuleLoader__.load({
|
|
3
|
+
id: "dsh-plugin-peepface",
|
|
4
|
+
factory(require) {
|
|
5
|
+
/* peepface core — 工厂版
|
|
6
|
+
* createFace(container, options) → { setState, getState, destroy }
|
|
7
|
+
* 无状态判定、无全局单例,可在一页里同时跑多个实例。
|
|
8
|
+
*/
|
|
9
|
+
(function (root) {
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const DEG = Math.PI / 180;
|
|
13
|
+
const SAMPLES = 64;
|
|
14
|
+
const R = 56;
|
|
15
|
+
|
|
16
|
+
/* ---------------- 几何 ---------------- */
|
|
17
|
+
|
|
18
|
+
const cross = (a, b) => [a[1]*b[2]-a[2]*b[1], a[2]*b[0]-a[0]*b[2], a[0]*b[1]-a[1]*b[0]];
|
|
19
|
+
const norm = v => { const l = Math.hypot(...v) || 1; return [v[0]/l, v[1]/l, v[2]/l]; };
|
|
20
|
+
|
|
21
|
+
const sphere = (lon, lat) => {
|
|
22
|
+
const lo = lon * DEG, la = lat * DEG;
|
|
23
|
+
return [Math.sin(lo)*Math.cos(la), Math.sin(la), Math.cos(lo)*Math.cos(la)];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const rotate = (n, yaw, pitch) => {
|
|
27
|
+
const a = yaw * DEG, b = pitch * DEG;
|
|
28
|
+
const x1 = n[0]*Math.cos(a) + n[2]*Math.sin(a);
|
|
29
|
+
const z1 = -n[0]*Math.sin(a) + n[2]*Math.cos(a);
|
|
30
|
+
const y2 = n[1]*Math.cos(b) - z1*Math.sin(b);
|
|
31
|
+
const z2 = n[1]*Math.sin(b) + z1*Math.cos(b);
|
|
32
|
+
return [x1, y2, z2];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const tangentFrame = n => {
|
|
36
|
+
let u = cross([0, 1, 0], n);
|
|
37
|
+
if (Math.hypot(...u) < 1e-6) u = [1, 0, 0];
|
|
38
|
+
u = norm(u);
|
|
39
|
+
return { u, v: norm(cross(n, u)) };
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/* 椭圆眼形:用两段 A 命令,带旋转角。
|
|
43
|
+
之前的 capsule 实现在 hw > hh(横条)时两端圆心重合,会退化成圆。 */
|
|
44
|
+
function eyePath(cx, cy, ux, uy, hw, hh) {
|
|
45
|
+
const theta = Math.atan2(uy, ux) * 180 / Math.PI;
|
|
46
|
+
const p0x = cx + ux * hw, p0y = cy + uy * hw;
|
|
47
|
+
const p1x = cx - ux * hw, p1y = cy - uy * hw;
|
|
48
|
+
return 'M' + p0x.toFixed(2) + ' ' + p0y.toFixed(2) +
|
|
49
|
+
'A' + hw.toFixed(2) + ' ' + hh.toFixed(2) + ' ' + theta.toFixed(1) +
|
|
50
|
+
' 0 1 ' + p1x.toFixed(2) + ' ' + p1y.toFixed(2) +
|
|
51
|
+
'A' + hw.toFixed(2) + ' ' + hh.toFixed(2) + ' ' + theta.toFixed(1) +
|
|
52
|
+
' 0 1 ' + p0x.toFixed(2) + ' ' + p0y.toFixed(2) + 'Z';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* ---------------- 形体 ---------------- */
|
|
56
|
+
|
|
57
|
+
const SHAPES = {
|
|
58
|
+
circle: () => 1,
|
|
59
|
+
blob: th => 1 + 0.05*Math.cos(3*th + 0.6) + 0.02*Math.cos(5*th - 1.1),
|
|
60
|
+
tall: th => { const a=0.90, b=1.12, c=Math.cos(th), s=Math.sin(th); return a*b/Math.hypot(b*c, a*s); },
|
|
61
|
+
wide: th => { const a=1.14, b=0.88, c=Math.cos(th), s=Math.sin(th); return a*b/Math.hypot(b*c, a*s); },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const TABLE = {};
|
|
65
|
+
for (const k in SHAPES) {
|
|
66
|
+
TABLE[k] = Array.from({ length: SAMPLES }, (_, i) => SHAPES[k](i / SAMPLES * Math.PI * 2));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function radiusPath(radii, scale) {
|
|
70
|
+
let d = '';
|
|
71
|
+
for (let i = 0; i < SAMPLES; i++) {
|
|
72
|
+
const th = i / SAMPLES * Math.PI * 2;
|
|
73
|
+
const r = radii[i] * R * scale;
|
|
74
|
+
d += (i ? 'L' : 'M') + (Math.cos(th) * r).toFixed(1) + ' ' + (Math.sin(th) * r).toFixed(1);
|
|
75
|
+
}
|
|
76
|
+
return d + 'Z';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* ---------------- 表情 ---------------- */
|
|
80
|
+
|
|
81
|
+
const EXPR = {
|
|
82
|
+
neutral: { w: 11.5, h: 24, tilt: -13 },
|
|
83
|
+
sad: { w: 11.5, h: 21, tilt: -28 },
|
|
84
|
+
focused: { w: 19, h: 8.5, tilt: 0 },
|
|
85
|
+
happy: { w: 16, h: 7, tilt: -18 },
|
|
86
|
+
surprised: { w: 13, h: 29, tilt: -8 },
|
|
87
|
+
sleepy: { w: 20, h: 3.2, tilt: -2 },
|
|
88
|
+
confused: { w: 10.5, h: 19, tilt: 30 },
|
|
89
|
+
droop: { w: 11, h: 16, tilt: -44 },
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/* ---------------- 状态(来自 states.json) ---------------- */
|
|
93
|
+
|
|
94
|
+
const STATES = {
|
|
95
|
+
idle: { expr:'neutral', shape:'circle', gaze:[0, 0.05], breath:0.008, blink:[2.6, 6.0] },
|
|
96
|
+
listening: { expr:'neutral', shape:'circle', gaze:[0, 0.60], breath:0.010, blink:[2.4, 5.0] },
|
|
97
|
+
thinking: { expr:'sad', shape:'blob', gaze:[0, 0.60], breath:0.014, blink:[2.0, 4.0], ring:1 },
|
|
98
|
+
searching: { expr:'focused', shape:'circle', gaze:[0, 0.10], breath:0.012, blink:[3.0, 6.0], scan:1 },
|
|
99
|
+
streaming: { expr:'neutral', shape:'circle', gaze:[0, 0.15], breath:0.012, blink:[3.0, 6.0] },
|
|
100
|
+
working: { expr:'focused', shape:'circle', gaze:[0, 0.15], breath:0.020, blink:[3.0, 7.0] },
|
|
101
|
+
waiting: { expr:'neutral', shape:'blob', gaze:[0.55, 0.05], breath:0.006, blink:[3.0, 7.0], ring:0.5 },
|
|
102
|
+
blocked: { expr:'confused', shape:'tall', gaze:[-0.40, 0.10], breath:0.005, blink:[4.0, 8.0] },
|
|
103
|
+
finished: { expr:'happy', shape:'wide', gaze:[0, 0.20], breath:0.006, blink:[3.0, 6.0] },
|
|
104
|
+
failed: { expr:'droop', shape:'circle', gaze:[0, 0.42], breath:0.004, blink:[5.0, 9.0] },
|
|
105
|
+
sleeping: { expr:'sleepy', shape:'circle', gaze:[0, -0.20], breath:0.004, blink:[9, 16], asleep:1 },
|
|
106
|
+
waking: { expr:'surprised', shape:'circle', gaze:[0, 0.35], breath:0.016, blink:[1.4, 2.6] },
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const ease = (cur, tgt, tau, dt) => cur + (tgt - cur) * (1 - Math.exp(-dt / tau));
|
|
110
|
+
|
|
111
|
+
/* ---------------- 工厂 ---------------- */
|
|
112
|
+
|
|
113
|
+
let uid = 0;
|
|
114
|
+
|
|
115
|
+
function createFace(container, options) {
|
|
116
|
+
if (!container) throw new Error('peepface: container required');
|
|
117
|
+
const opts = options || {};
|
|
118
|
+
const size = opts.size || 84;
|
|
119
|
+
const dark = !!opts.dark;
|
|
120
|
+
const follow = opts.follow !== false;
|
|
121
|
+
const id = 'pf' + (++uid);
|
|
122
|
+
|
|
123
|
+
const host = document.createElement('div');
|
|
124
|
+
host.className = 'peepface';
|
|
125
|
+
const shadow = host.attachShadow({ mode: 'open' });
|
|
126
|
+
|
|
127
|
+
shadow.innerHTML = `
|
|
128
|
+
<style>
|
|
129
|
+
:host { all: initial; }
|
|
130
|
+
.wrap {
|
|
131
|
+
position: relative; width: ${size}px; height: ${size}px;
|
|
132
|
+
display: block; user-select: none;
|
|
133
|
+
}
|
|
134
|
+
svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
|
135
|
+
.face { fill: ${dark ? '#e8e8e8' : '#0a0a0c'}; }
|
|
136
|
+
.ring { fill: none; stroke: ${dark ? '#e8e8e8' : '#0a0a0c'}; stroke-width: 1.4; opacity: 0; }
|
|
137
|
+
</style>
|
|
138
|
+
<div class="wrap">
|
|
139
|
+
<svg viewBox="-70 -70 140 140">
|
|
140
|
+
<defs>
|
|
141
|
+
<mask id="${id}-holes" maskUnits="userSpaceOnUse" x="-70" y="-70" width="140" height="140">
|
|
142
|
+
<path id="${id}-body-mask" fill="#fff" d=""/>
|
|
143
|
+
<path id="${id}-eye-l" fill="#000" d=""/>
|
|
144
|
+
<path id="${id}-eye-r" fill="#000" d=""/>
|
|
145
|
+
</mask>
|
|
146
|
+
</defs>
|
|
147
|
+
<path id="${id}-ring" class="ring" d=""/>
|
|
148
|
+
<path id="${id}-body" class="face" mask="url(#${id}-holes)" d=""/>
|
|
149
|
+
</svg>
|
|
150
|
+
</div>`;
|
|
151
|
+
|
|
152
|
+
const el = {
|
|
153
|
+
body: shadow.getElementById(id + '-body'),
|
|
154
|
+
bodyMask: shadow.getElementById(id + '-body-mask'),
|
|
155
|
+
eyeL: shadow.getElementById(id + '-eye-l'),
|
|
156
|
+
eyeR: shadow.getElementById(id + '-eye-r'),
|
|
157
|
+
ring: shadow.getElementById(id + '-ring'),
|
|
158
|
+
wrap: shadow.querySelector('.wrap'),
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const S = {
|
|
162
|
+
state: opts.state && STATES[opts.state] ? opts.state : 'idle',
|
|
163
|
+
yaw: 0, pitch: 0,
|
|
164
|
+
expr: 'neutral', shape: 'circle',
|
|
165
|
+
radii: TABLE.circle.slice(),
|
|
166
|
+
blink: 0, blinkT: -1, blinkDur: 0.26, nextBlink: 2 + Math.random() * 3,
|
|
167
|
+
breath: 0.008, ring: 0, tRing: 0,
|
|
168
|
+
pointer: null, time: Math.random() * 5, morph: null, asleep: 0,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
function applyState(name) {
|
|
172
|
+
const st = STATES[name] || STATES.idle;
|
|
173
|
+
S.state = name;
|
|
174
|
+
S.expr = st.expr;
|
|
175
|
+
S.shape = st.shape;
|
|
176
|
+
S.tRing = st.ring || 0;
|
|
177
|
+
S.breath = st.breath;
|
|
178
|
+
S.asleep = st.asleep ? 1 : 0;
|
|
179
|
+
if (st.blink) S.nextBlink = st.blink[0] + Math.random() * (st.blink[1] - st.blink[0]);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function blink(dur) {
|
|
183
|
+
if (S.blinkT >= 0 && S.blinkT < 0.5) return;
|
|
184
|
+
S.blinkT = 0; S.blinkDur = dur || 0.26;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function onPointer(e) {
|
|
188
|
+
if (!follow) return;
|
|
189
|
+
const r = el.wrap.getBoundingClientRect();
|
|
190
|
+
if (!r.width) return;
|
|
191
|
+
const cx = r.left + r.width / 2, cy = r.top + r.height / 2;
|
|
192
|
+
const half = Math.max(r.width, 1);
|
|
193
|
+
let gx = (e.clientX - cx) / half, gy = -(e.clientY - cy) / half;
|
|
194
|
+
const len = Math.hypot(gx, gy);
|
|
195
|
+
if (len > 1) { gx /= len; gy /= len; }
|
|
196
|
+
S.pointer = [gx, gy];
|
|
197
|
+
}
|
|
198
|
+
const onLeave = () => { S.pointer = null; };
|
|
199
|
+
|
|
200
|
+
if (follow) {
|
|
201
|
+
window.addEventListener('pointermove', onPointer, { passive: true });
|
|
202
|
+
document.addEventListener('mouseleave', onLeave);
|
|
203
|
+
window.addEventListener('blur', onLeave);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let last = performance.now();
|
|
207
|
+
let alive = true;
|
|
208
|
+
|
|
209
|
+
function tick(now) {
|
|
210
|
+
const dt = Math.min(0.05, (now - last) / 1000);
|
|
211
|
+
last = now; S.time += dt;
|
|
212
|
+
|
|
213
|
+
const cur = STATES[S.state];
|
|
214
|
+
const bias = cur.gaze || [0, 0];
|
|
215
|
+
|
|
216
|
+
let ty, tp;
|
|
217
|
+
if (S.pointer) {
|
|
218
|
+
ty = S.pointer[0] * 40 + bias[0] * 18;
|
|
219
|
+
tp = -S.pointer[1] * 28 - bias[1] * 16;
|
|
220
|
+
} else {
|
|
221
|
+
ty = Math.sin(S.time * 0.42) * 8 + Math.sin(S.time * 0.17 + 1.3) * 4 + bias[0] * 18;
|
|
222
|
+
tp = Math.cos(S.time * 0.31) * 5 - bias[1] * 16;
|
|
223
|
+
}
|
|
224
|
+
if (cur.scan) ty += Math.sin(S.time * 3.6) * 30;
|
|
225
|
+
|
|
226
|
+
S.yaw = ease(S.yaw, ty, 0.08, dt);
|
|
227
|
+
S.pitch = ease(S.pitch, tp, 0.08, dt);
|
|
228
|
+
|
|
229
|
+
if (S.asleep) {
|
|
230
|
+
S.blink = 0; // 闭眼交给 sleepy 眼形本身表达
|
|
231
|
+
S.blinkT = -1;
|
|
232
|
+
S.nextBlink = 1e9; // 睡着不眨
|
|
233
|
+
} else if (S.blinkT >= 0) {
|
|
234
|
+
S.blinkT += dt / S.blinkDur;
|
|
235
|
+
if (S.blinkT >= 1) { S.blinkT = -1; S.blink = 0; }
|
|
236
|
+
else {
|
|
237
|
+
const t = S.blinkT, U1 = 0.42, U2 = 0.56;
|
|
238
|
+
const ss = x => x * x * (3 - 2 * x);
|
|
239
|
+
S.blink = t < U1 ? ss(t / U1) : t <= U2 ? 1 : 1 - ss((t - U2) / (1 - U2));
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
S.nextBlink -= dt;
|
|
243
|
+
if (S.nextBlink <= 0) {
|
|
244
|
+
blink(0.26);
|
|
245
|
+
const b = cur.blink || [2.6, 6];
|
|
246
|
+
S.nextBlink = b[0] + Math.random() * (b[1] - b[0]);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const target = TABLE[S.shape] || TABLE.circle;
|
|
251
|
+
for (let i = 0; i < SAMPLES; i++) S.radii[i] = ease(S.radii[i], target[i], 0.12, dt);
|
|
252
|
+
|
|
253
|
+
const scale = 1 + Math.sin(S.time * 1.4) * S.breath;
|
|
254
|
+
const d = radiusPath(S.radii, scale);
|
|
255
|
+
el.body.setAttribute('d', d);
|
|
256
|
+
el.bodyMask.setAttribute('d', d);
|
|
257
|
+
|
|
258
|
+
const ex = EXPR[S.expr] || EXPR.neutral;
|
|
259
|
+
const eyes = [{ id: 'eyeL', lon: -27 }, { id: 'eyeR', lon: 27 }];
|
|
260
|
+
for (let k = 0; k < eyes.length; k++) {
|
|
261
|
+
const e = eyes[k];
|
|
262
|
+
const n = rotate(sphere(e.lon, 10), S.yaw, S.pitch);
|
|
263
|
+
const px = n[0] * R, py = -n[1] * R;
|
|
264
|
+
const tf = tangentFrame(n);
|
|
265
|
+
const uS = [tf.u[0], -tf.u[1]], vS = [tf.v[0], -tf.v[1]];
|
|
266
|
+
const uz = Math.hypot(uS[0], uS[1]) || 1e-4;
|
|
267
|
+
const vz = Math.hypot(vS[0], vS[1]) || 1e-4;
|
|
268
|
+
const ux = uS[0] / uz, uy = uS[1] / uz;
|
|
269
|
+
const roll = -S.yaw * 0.28 + ex.tilt;
|
|
270
|
+
const cR = Math.cos(roll * DEG), sR = Math.sin(roll * DEG);
|
|
271
|
+
const wx = ux * cR - uy * sR, wy = ux * sR + uy * cR;
|
|
272
|
+
const hw = (ex.w / 2) * Math.max(0.22, uz);
|
|
273
|
+
const hh = Math.max(0.3, (ex.h / 2) * vz * (1 - S.blink * 0.995));
|
|
274
|
+
el[e.id].setAttribute('d', eyePath(px, py, wx, wy, hw, hh));
|
|
275
|
+
const vis = Math.max(0, Math.min(1, (n[2] - 0.02) / 0.22));
|
|
276
|
+
el[e.id].setAttribute('opacity', vis.toFixed(2));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
S.ring = ease(S.ring, S.tRing, 0.25, dt);
|
|
280
|
+
if (S.ring > 0.02) {
|
|
281
|
+
const rx = 74, ry = 26, rot = -18 * DEG, a0 = S.time * 0.9;
|
|
282
|
+
let p = '';
|
|
283
|
+
for (let i = 0; i <= 60; i++) {
|
|
284
|
+
const a = i / 60 * Math.PI * 2 + a0;
|
|
285
|
+
const x0 = Math.cos(a) * rx, y0 = Math.sin(a) * ry;
|
|
286
|
+
p += (i ? 'L' : 'M') + (x0 * Math.cos(rot) - y0 * Math.sin(rot)).toFixed(1) + ' '
|
|
287
|
+
+ (x0 * Math.sin(rot) + y0 * Math.cos(rot)).toFixed(1);
|
|
288
|
+
}
|
|
289
|
+
el.ring.setAttribute('d', p + 'Z');
|
|
290
|
+
el.ring.style.opacity = (S.ring * 0.45).toFixed(2);
|
|
291
|
+
} else {
|
|
292
|
+
el.ring.style.opacity = 0;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function frame(now) {
|
|
297
|
+
if (!alive) return;
|
|
298
|
+
try { tick(now); } catch (e) { /* 一帧出错不杀循环 */ }
|
|
299
|
+
requestAnimationFrame(frame);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
container.appendChild(host);
|
|
303
|
+
applyState(S.state);
|
|
304
|
+
requestAnimationFrame(frame);
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
setState(name) {
|
|
308
|
+
applyState(name);
|
|
309
|
+
// 换状态的瞬间眨一次眼盖住形变
|
|
310
|
+
blink(0.26);
|
|
311
|
+
},
|
|
312
|
+
getState() { return S.state; },
|
|
313
|
+
blink() { blink(0.26); },
|
|
314
|
+
destroy() {
|
|
315
|
+
alive = false;
|
|
316
|
+
if (follow) {
|
|
317
|
+
window.removeEventListener('pointermove', onPointer);
|
|
318
|
+
document.removeEventListener('mouseleave', onLeave);
|
|
319
|
+
window.removeEventListener('blur', onLeave);
|
|
320
|
+
}
|
|
321
|
+
host.remove();
|
|
322
|
+
},
|
|
323
|
+
el: host,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
root.peepface = {
|
|
328
|
+
createFace,
|
|
329
|
+
STATES: Object.keys(STATES),
|
|
330
|
+
VERSION: '0.1.0',
|
|
331
|
+
};
|
|
332
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
333
|
+
|
|
334
|
+
/* peepface × DSH 适配层
|
|
335
|
+
* 依赖 window.peepface.createFace(由 core.js 提供)
|
|
336
|
+
* 负责:状态判定、拖拽、位置记忆、主题
|
|
337
|
+
*/
|
|
338
|
+
(function (root) {
|
|
339
|
+
'use strict';
|
|
340
|
+
|
|
341
|
+
var POS_KEY = 'peepface:pos';
|
|
342
|
+
var SIZE = 84;
|
|
343
|
+
|
|
344
|
+
/* ---------- 主题 ---------- */
|
|
345
|
+
var RGB = /rgba?\((\d+)[,\s]+(\d+)[,\s]+(\d+)(?:[,\s\/]+([\d.]+))?\)/;
|
|
346
|
+
|
|
347
|
+
function parseRGB(str) {
|
|
348
|
+
var m = RGB.exec(str || '');
|
|
349
|
+
if (!m) return null;
|
|
350
|
+
if (m[4] !== undefined && +m[4] <= 0.5) return null;
|
|
351
|
+
return { r: +m[1], g: +m[2], b: +m[3] };
|
|
352
|
+
}
|
|
353
|
+
function lum(c) { return 0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b; }
|
|
354
|
+
|
|
355
|
+
function pageDark() {
|
|
356
|
+
var nodes = [document.body, document.documentElement];
|
|
357
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
358
|
+
if (!nodes[i]) continue;
|
|
359
|
+
var c = parseRGB(getComputedStyle(nodes[i]).backgroundColor);
|
|
360
|
+
if (c) return lum(c) < 110;
|
|
361
|
+
}
|
|
362
|
+
return matchMedia('(prefers-color-scheme: dark)').matches;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/* ---------- 拖拽 ---------- */
|
|
366
|
+
function clampPos(x, y) {
|
|
367
|
+
var maxX = Math.max(0, window.innerWidth - SIZE - 4);
|
|
368
|
+
var maxY = Math.max(0, window.innerHeight - SIZE - 4);
|
|
369
|
+
return [Math.min(Math.max(0, x), maxX), Math.min(Math.max(0, y), maxY)];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function attachDrag(host, onTap) {
|
|
373
|
+
function place(x, y) {
|
|
374
|
+
host.style.left = x + 'px';
|
|
375
|
+
host.style.top = y + 'px';
|
|
376
|
+
host.style.right = 'auto';
|
|
377
|
+
host.style.bottom = 'auto';
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
var saved = JSON.parse(localStorage.getItem(POS_KEY) || 'null');
|
|
382
|
+
if (saved && typeof saved.x === 'number') {
|
|
383
|
+
var xy = clampPos(saved.x, saved.y);
|
|
384
|
+
place(xy[0], xy[1]);
|
|
385
|
+
}
|
|
386
|
+
} catch (e) {}
|
|
387
|
+
|
|
388
|
+
var dragging = false, gx = 0, gy = 0, travel = 0;
|
|
389
|
+
|
|
390
|
+
host.addEventListener('pointerdown', function (e) {
|
|
391
|
+
if (e.button !== 0) return;
|
|
392
|
+
var r = host.getBoundingClientRect();
|
|
393
|
+
gx = e.clientX - r.left;
|
|
394
|
+
gy = e.clientY - r.top;
|
|
395
|
+
dragging = true;
|
|
396
|
+
travel = 0;
|
|
397
|
+
try { host.setPointerCapture(e.pointerId); } catch (_) {}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
host.addEventListener('pointermove', function (e) {
|
|
401
|
+
if (!dragging) return;
|
|
402
|
+
travel += Math.abs(e.movementX || 0) + Math.abs(e.movementY || 0);
|
|
403
|
+
var p = clampPos(e.clientX - gx, e.clientY - gy);
|
|
404
|
+
place(p[0], p[1]);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
function end(e) {
|
|
408
|
+
if (!dragging) return;
|
|
409
|
+
dragging = false;
|
|
410
|
+
try { host.releasePointerCapture(e.pointerId); } catch (_) {}
|
|
411
|
+
if (travel < 6) {
|
|
412
|
+
onTap();
|
|
413
|
+
} else {
|
|
414
|
+
var r = host.getBoundingClientRect();
|
|
415
|
+
try {
|
|
416
|
+
localStorage.setItem(POS_KEY, JSON.stringify({ x: Math.round(r.left), y: Math.round(r.top) }));
|
|
417
|
+
} catch (_) {}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
host.addEventListener('pointerup', end);
|
|
421
|
+
host.addEventListener('pointercancel', end);
|
|
422
|
+
|
|
423
|
+
window.addEventListener('resize', function () {
|
|
424
|
+
if (!host.style.left) return;
|
|
425
|
+
var r = host.getBoundingClientRect();
|
|
426
|
+
var p = clampPos(r.left, r.top);
|
|
427
|
+
place(p[0], p[1]);
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/* ---------- 状态判定 ---------- */
|
|
432
|
+
|
|
433
|
+
var MANUAL_ORDER = ['idle', 'listening', 'thinking', 'searching', 'streaming', 'working', 'finished'];
|
|
434
|
+
|
|
435
|
+
function startProbe(face) {
|
|
436
|
+
var probe = { lastMut: 0, rate: 0, failedUntil: 0, finishedUntil: 0, searchUntil: 0, waitUntil: 0, lastError: false };
|
|
437
|
+
var net = { last: 0, lastPost: 0, lastErr: 0, events: [] };
|
|
438
|
+
var fresh = { text: '', at: 0 };
|
|
439
|
+
var lastTextChange = 0, lastBusyAt = 0, lastKeyAt = 0, wokeUntil = 0;
|
|
440
|
+
var pending = null, pendingCount = 0;
|
|
441
|
+
var manualUntil = 0;
|
|
442
|
+
|
|
443
|
+
var bodyText = function () { return document.body ? (document.body.innerText || '') : ''; };
|
|
444
|
+
var prevLen = bodyText().length, prevT = performance.now();
|
|
445
|
+
|
|
446
|
+
var ERROR_RE = /(失败|错误|异常|Error|Failed)/;
|
|
447
|
+
var SEARCH_RE = /(web_search|web_fetch|\bgrep\b|\bglob\b|\bbrowse\b|检索|搜索结果|正在搜索|读取目录)/i;
|
|
448
|
+
var APPROVAL_RE = /(批准|允许|拒绝|同意执行|需要你确认|等待确认|Approve|Deny|Allow once)/i;
|
|
449
|
+
|
|
450
|
+
function markNet() {
|
|
451
|
+
var t = performance.now();
|
|
452
|
+
net.last = t;
|
|
453
|
+
net.events.push(t);
|
|
454
|
+
if (net.events.length > 80) net.events.shift();
|
|
455
|
+
}
|
|
456
|
+
function pushFresh(str) {
|
|
457
|
+
if (!str) return;
|
|
458
|
+
fresh.text = (fresh.text + ' ' + str).slice(-3000);
|
|
459
|
+
fresh.at = performance.now();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
var mo = new MutationObserver(function (muts) {
|
|
463
|
+
probe.lastMut = performance.now();
|
|
464
|
+
for (var i = 0; i < muts.length; i++) {
|
|
465
|
+
var m = muts[i];
|
|
466
|
+
if (m.type === 'characterData') pushFresh(m.target.data);
|
|
467
|
+
else if (m.addedNodes) {
|
|
468
|
+
for (var j = 0; j < m.addedNodes.length; j++) {
|
|
469
|
+
var n = m.addedNodes[j];
|
|
470
|
+
if (n.nodeType === 1 || n.nodeType === 3) pushFresh(n.textContent);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
if (document.body) mo.observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
476
|
+
|
|
477
|
+
/* 网络:只用来判断「有在途请求」,不作为工作状态的主信号 */
|
|
478
|
+
var origFetch = window.fetch;
|
|
479
|
+
if (typeof origFetch === 'function') {
|
|
480
|
+
window.fetch = function () {
|
|
481
|
+
markNet();
|
|
482
|
+
var p = origFetch.apply(this, arguments);
|
|
483
|
+
if (p && typeof p.then === 'function') {
|
|
484
|
+
p.then(function (res) {
|
|
485
|
+
markNet();
|
|
486
|
+
if (!res.ok) net.lastErr = performance.now();
|
|
487
|
+
}, function () {
|
|
488
|
+
net.lastErr = performance.now();
|
|
489
|
+
markNet();
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
return p;
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
['WebSocket', 'EventSource'].forEach(function (key) {
|
|
497
|
+
var Orig = window[key];
|
|
498
|
+
if (!Orig) return;
|
|
499
|
+
try {
|
|
500
|
+
window[key] = new Proxy(Orig, {
|
|
501
|
+
construct: function (t, args) {
|
|
502
|
+
var inst = new t(...args);
|
|
503
|
+
try { inst.addEventListener('message', markNet); } catch (e) {}
|
|
504
|
+
return inst;
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
} catch (e) {}
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
document.addEventListener('keydown', function (e) {
|
|
511
|
+
if (e.isComposing || e.keyCode === 229) return;
|
|
512
|
+
if (e.key !== 'Enter' || e.shiftKey) return;
|
|
513
|
+
var t = e.target;
|
|
514
|
+
if (!t) return;
|
|
515
|
+
if (t.tagName === 'TEXTAREA' || t.tagName === 'INPUT' || t.isContentEditable === true) {
|
|
516
|
+
lastKeyAt = performance.now();
|
|
517
|
+
net.lastPost = lastKeyAt;
|
|
518
|
+
} else {
|
|
519
|
+
lastKeyAt = performance.now();
|
|
520
|
+
}
|
|
521
|
+
}, true);
|
|
522
|
+
document.addEventListener('keydown', function () { lastKeyAt = performance.now(); }, true);
|
|
523
|
+
|
|
524
|
+
function inputHasText() {
|
|
525
|
+
var a = document.activeElement;
|
|
526
|
+
if (!a) return false;
|
|
527
|
+
if (a.tagName === 'TEXTAREA' || a.tagName === 'INPUT') return (a.value || '').trim().length > 0;
|
|
528
|
+
if (a.isContentEditable) return (a.innerText || '').trim().length > 0;
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/* 迟滞:连续 3 次一致才切;finished / failed 例外 */
|
|
533
|
+
function commit(next) {
|
|
534
|
+
if (next === 'finished' || next === 'failed') {
|
|
535
|
+
if (face.getState() !== next) face.setState(next);
|
|
536
|
+
pending = null; pendingCount = 0;
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (next === face.getState()) { pending = null; pendingCount = 0; return; }
|
|
540
|
+
if (pending === next) {
|
|
541
|
+
if (++pendingCount >= 3) {
|
|
542
|
+
face.setState(next);
|
|
543
|
+
pending = null; pendingCount = 0;
|
|
544
|
+
}
|
|
545
|
+
} else {
|
|
546
|
+
pending = next; pendingCount = 1;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
var timer = setInterval(function () {
|
|
551
|
+
var now = performance.now();
|
|
552
|
+
var text = bodyText();
|
|
553
|
+
var len = text.length;
|
|
554
|
+
var dt = Math.max(0.001, (now - prevT) / 1000);
|
|
555
|
+
if (len !== prevLen) lastTextChange = now;
|
|
556
|
+
var inst = (len - prevLen) / dt;
|
|
557
|
+
probe.rate = probe.rate * 0.55 + inst * 0.45;
|
|
558
|
+
prevLen = len; prevT = now;
|
|
559
|
+
|
|
560
|
+
if (now - probe.lastMut < 400) {
|
|
561
|
+
var hit = ERROR_RE.test(text.slice(-240));
|
|
562
|
+
if (hit && !probe.lastError) probe.failedUntil = now + 2600;
|
|
563
|
+
probe.lastError = hit;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (fresh.text && now - fresh.at < 1500) {
|
|
567
|
+
if (SEARCH_RE.test(fresh.text)) probe.searchUntil = now + 3000;
|
|
568
|
+
if (APPROVAL_RE.test(fresh.text)) probe.waitUntil = now + 4000;
|
|
569
|
+
fresh.text = '';
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
var idleFor = now - lastTextChange;
|
|
573
|
+
|
|
574
|
+
if (idleFor > 300000 && face.getState() !== 'sleeping' && face.getState() !== 'waking') {
|
|
575
|
+
wokeUntil = 0;
|
|
576
|
+
commit('sleeping');
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (face.getState() === 'sleeping' && idleFor < 1500) wokeUntil = now + 2000;
|
|
580
|
+
|
|
581
|
+
if (now < manualUntil) return;
|
|
582
|
+
|
|
583
|
+
if (wokeUntil && now < wokeUntil) { commit('waking'); return; }
|
|
584
|
+
|
|
585
|
+
var textFlowing = now - lastTextChange < 1400 && probe.rate > 2;
|
|
586
|
+
var typing = now - lastKeyAt < 3000 && inputHasText();
|
|
587
|
+
var justSent = now - net.lastPost < 2000 && now - lastKeyAt < 2000;
|
|
588
|
+
|
|
589
|
+
var next = 'idle';
|
|
590
|
+
if (now < probe.failedUntil) next = 'failed';
|
|
591
|
+
else if (now < probe.waitUntil) next = 'waiting';
|
|
592
|
+
else if (now < probe.finishedUntil) next = 'finished';
|
|
593
|
+
else if (now < probe.searchUntil) next = 'searching';
|
|
594
|
+
else if (probe.rate > 55) { next = 'streaming'; lastBusyAt = now; }
|
|
595
|
+
else if (textFlowing) { next = 'working'; lastBusyAt = now; }
|
|
596
|
+
else if (typing) next = 'listening';
|
|
597
|
+
else if (justSent && probe.rate < 1) next = 'thinking';
|
|
598
|
+
else if (idleFor > 1400 && now - lastBusyAt < 8000) {
|
|
599
|
+
probe.finishedUntil = now + 1800;
|
|
600
|
+
next = 'finished';
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
commit(next);
|
|
604
|
+
}, 200);
|
|
605
|
+
|
|
606
|
+
return {
|
|
607
|
+
tap: function () {
|
|
608
|
+
var cur = face.getState();
|
|
609
|
+
var i = MANUAL_ORDER.indexOf(cur);
|
|
610
|
+
face.setState(MANUAL_ORDER[(i + 1) % MANUAL_ORDER.length] || 'idle');
|
|
611
|
+
manualUntil = performance.now() + 4000;
|
|
612
|
+
},
|
|
613
|
+
stop: function () {
|
|
614
|
+
clearInterval(timer);
|
|
615
|
+
try { mo.disconnect(); } catch (e) {}
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/* ---------- 挂载 ---------- */
|
|
621
|
+
|
|
622
|
+
function mount(container) {
|
|
623
|
+
if (!root.peepface) {
|
|
624
|
+
console.error('[peepface] core.js 未加载');
|
|
625
|
+
return function () {};
|
|
626
|
+
}
|
|
627
|
+
if (document.getElementById('peepface-root')) return function () {};
|
|
628
|
+
|
|
629
|
+
var host = document.createElement('div');
|
|
630
|
+
host.id = 'peepface-root';
|
|
631
|
+
host.style.cssText = 'position:fixed;right:22px;bottom:96px;z-index:2147483000;';
|
|
632
|
+
(container || document.body).appendChild(host);
|
|
633
|
+
|
|
634
|
+
var face = root.peepface.createFace(host, {
|
|
635
|
+
state: 'idle',
|
|
636
|
+
size: SIZE,
|
|
637
|
+
dark: pageDark(),
|
|
638
|
+
follow: true,
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
var probe = startProbe(face);
|
|
642
|
+
attachDrag(host, probe.tap);
|
|
643
|
+
|
|
644
|
+
return function () {
|
|
645
|
+
probe.stop();
|
|
646
|
+
face.destroy();
|
|
647
|
+
try { host.remove(); } catch (e) {}
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
root.__peepfaceDsh = { mount: mount };
|
|
652
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
653
|
+
|
|
654
|
+
return {
|
|
655
|
+
inject: [],
|
|
656
|
+
apply(ctx) {
|
|
657
|
+
try {
|
|
658
|
+
ctx.effect(() => window.__peepfaceDsh.mount(document.body), "dsh-plugin-peepface: overlay");
|
|
659
|
+
} catch (error) {
|
|
660
|
+
console.error("[peepface] apply failed:", error);
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
};
|
|
664
|
+
},
|
|
665
|
+
});
|