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.
@@ -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
+ });