dsh-agent-office 4.2.0 → 4.5.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/panel-src/main.js CHANGED
@@ -13,6 +13,7 @@ import faceDizzy from './assets/exp-dizzy.png'
13
13
  import faceSurprised from './assets/exp-surprised.png'
14
14
  import faceThinking from './assets/exp-thinking.png'
15
15
  import faceLoved from './assets/exp-loved.png'
16
+ import faceSlits from './assets/exp-slits.png'
16
17
 
17
18
  const API = '/agent-office/api/state'
18
19
  const FLOOR_GAP = 4.0
@@ -285,7 +286,7 @@ function applyThemeFrame() {
285
286
  for (const material of cityMats) material.color.copy(NIGHT_THEME.city).lerp(DAY_THEME.city, mix)
286
287
  const neonMaterial = neonSign?.material
287
288
  if (neonMaterial) neonMaterial.opacity = NIGHT_THEME.neon + (DAY_THEME.neon - NIGHT_THEME.neon) * mix
288
- for (const item of themeLights) item.light.intensity = item.night + (item.day - item.night) * mix
289
+ for (const item of [...themeLights, ...annexLights]) item.light.intensity = item.night + (item.day - item.night) * mix
289
290
  }
290
291
 
291
292
  function syncThemeByClock(immediate = false) {
@@ -350,6 +351,7 @@ const FACE_SRC = {
350
351
  surprised: faceSurprised,
351
352
  thinking: faceThinking,
352
353
  loved: faceLoved,
354
+ slits: faceSlits,
353
355
  }
354
356
  const faceTextures = {}
355
357
  const faceLoader = new THREE.TextureLoader()
@@ -359,13 +361,15 @@ for (const [name, src] of Object.entries(FACE_SRC)) {
359
361
  faceTextures[name] = texture
360
362
  }
361
363
  const CUSTOM_FACE_KEY = 'agent-office-custom-face'
364
+ const GLASS_KEY = 'agent-office-ui-opacity'
362
365
  let customFaceTexture = null
366
+ let customFaceData = ''
363
367
  const FACE_FOR_STATUS = {
364
368
  working: 'happy',
365
369
  need_me: 'surprised',
366
370
  stuck: 'dizzy',
367
371
  done: 'loved',
368
- resting: 'sleepy',
372
+ resting: 'slits',
369
373
  }
370
374
  const zTexture = (() => {
371
375
  const canvas = document.createElement('canvas')
@@ -384,6 +388,63 @@ const zTexture = (() => {
384
388
  return texture
385
389
  })()
386
390
 
391
+
392
+ const sweatTexture = (() => {
393
+ const canvas = document.createElement('canvas')
394
+ canvas.width = 64
395
+ canvas.height = 96
396
+ const ctx = canvas.getContext('2d')
397
+ ctx.clearRect(0, 0, 64, 96)
398
+ ctx.fillStyle = '#bfeaff'
399
+ ctx.beginPath()
400
+ ctx.moveTo(32, 8)
401
+ ctx.bezierCurveTo(46, 30, 55, 48, 55, 62)
402
+ ctx.bezierCurveTo(55, 79, 45, 89, 32, 89)
403
+ ctx.bezierCurveTo(19, 89, 9, 79, 9, 62)
404
+ ctx.bezierCurveTo(9, 48, 18, 30, 32, 8)
405
+ ctx.fill()
406
+ ctx.fillStyle = 'rgba(255,255,255,.85)'
407
+ ctx.beginPath()
408
+ ctx.ellipse(23, 58, 5, 9, -0.35, 0, Math.PI * 2)
409
+ ctx.fill()
410
+ const texture = new THREE.CanvasTexture(canvas)
411
+ texture.colorSpace = THREE.SRGBColorSpace
412
+ return texture
413
+ })()
414
+
415
+ const scribbleTexture = (() => {
416
+ const canvas = document.createElement('canvas')
417
+ canvas.width = 256
418
+ canvas.height = 160
419
+ const ctx = canvas.getContext('2d')
420
+ ctx.clearRect(0, 0, 256, 160)
421
+ const strokes = [
422
+ [[34, 92], [64, 34], [92, 112], [124, 40], [154, 108], [186, 48], [224, 96]],
423
+ [[48, 54], [92, 78], [60, 126], [126, 92], [172, 124], [208, 66]],
424
+ [[78, 24], [104, 66], [144, 26], [168, 74], [208, 32]],
425
+ ]
426
+ ctx.lineCap = 'round'
427
+ ctx.lineJoin = 'round'
428
+ for (const pass of [{ color: 'rgba(255,255,255,.18)', width: 13 }, { color: '#0b0d10', width: 8 }]) {
429
+ ctx.strokeStyle = pass.color
430
+ ctx.lineWidth = pass.width
431
+ for (const points of strokes) {
432
+ ctx.beginPath()
433
+ ctx.moveTo(points[0][0], points[0][1])
434
+ for (let i = 1; i < points.length - 1; i += 2) {
435
+ const [cx, cy] = points[i]
436
+ const [x, y] = points[i + 1]
437
+ ctx.quadraticCurveTo(cx, cy, x, y)
438
+ }
439
+ ctx.stroke()
440
+ }
441
+ }
442
+ const texture = new THREE.CanvasTexture(canvas)
443
+ texture.colorSpace = THREE.SRGBColorSpace
444
+ return texture
445
+ })()
446
+ const OVERLOAD_COLOR = new THREE.Color(0xff5a5a)
447
+
387
448
  const floorTexture = (() => {
388
449
  const canvas = document.createElement('canvas')
389
450
  canvas.width = canvas.height = 256
@@ -414,7 +475,7 @@ function disposeGroup(group) {
414
475
  if (object.material) {
415
476
  const materials = Array.isArray(object.material) ? object.material : [object.material]
416
477
  for (const material of materials) {
417
- if (material.map && material.map !== glowTexture && material.map !== shadowTexture && material.map !== floorTexture) material.map.dispose()
478
+ if (material.map && material.map !== glowTexture && material.map !== shadowTexture && material.map !== floorTexture && material.map !== sweatTexture && material.map !== scribbleTexture) material.map.dispose()
418
479
  material.dispose()
419
480
  }
420
481
  }
@@ -456,6 +517,25 @@ function robotFor(agent, index = 0, sitY = 0) {
456
517
  face.position.y = baseY
457
518
  group.add(face)
458
519
 
520
+ // 正常工作:大汗淋漓;长时间工作:红温 + 头顶乱线
521
+ const sweat = []
522
+ for (let i = 0; i < 4; i++) {
523
+ const mat = new THREE.SpriteMaterial({ map: sweatTexture, transparent: true, opacity: 0, depthWrite: false, toneMapped: false, color: 0xbfeaff })
524
+ const drop = new THREE.Sprite(mat)
525
+ drop.scale.set(0.095, 0.145, 1)
526
+ drop.visible = false
527
+ group.add(drop)
528
+ sweat.push({ sprite: drop, phase: i / 4, x: i % 2 === 0 ? -0.27 : 0.27, drift: i < 2 ? -0.04 : 0.04 })
529
+ }
530
+ const scribbleGlow = new THREE.Sprite(new THREE.SpriteMaterial({ map: glowTexture, color: 0xff4a4a, transparent: true, opacity: 0, depthWrite: false, blending: THREE.AdditiveBlending }))
531
+ scribbleGlow.scale.set(1.0, 0.62, 1)
532
+ scribbleGlow.position.set(0, baseY + 1.55, 0.03)
533
+ group.add(scribbleGlow)
534
+ const scribble = new THREE.Sprite(new THREE.SpriteMaterial({ map: scribbleTexture, transparent: true, opacity: 0, depthWrite: false, toneMapped: false }))
535
+ scribble.scale.set(0.84, 0.5, 1)
536
+ scribble.position.set(0, baseY + 1.55, 0.04)
537
+ group.add(scribble)
538
+
459
539
  // 猫咪上下浮动的 GSAP 循环(用统一相位对象驱动,tick 里再乘幅度)
460
540
  const motion = { bob: 0.5 }
461
541
  if (!reduceMotion) {
@@ -567,7 +647,7 @@ function robotFor(agent, index = 0, sitY = 0) {
567
647
  group.userData = {
568
648
  agentId: agent.id,
569
649
  index,
570
- parts: { face, faceMat, ring, glow, badge, label, zzz, ripples, motion, seatShadow, shadow, tint, baseY },
650
+ parts: { face, faceMat, ring, glow, badge, label, zzz, ripples, motion, seatShadow, shadow, tint, baseY, sweat, scribble, scribbleGlow },
571
651
  }
572
652
  return group
573
653
  }
@@ -1099,7 +1179,7 @@ function buildFloor(team, index, teamAgents) {
1099
1179
 
1100
1180
  // 彩色货架(参考原版的彩色格子)
1101
1181
  const shelf = new THREE.Group()
1102
- shelf.position.set(-W / 2 + 0.26, 0, -0.4)
1182
+ shelf.position.set(-W / 2 + 0.38, 0, -0.4)
1103
1183
  shelf.rotation.y = Math.PI / 2
1104
1184
  const shelfMat = new THREE.MeshStandardMaterial({ color: 0x1b2430, roughness: 0.7, metalness: 0.3 })
1105
1185
  for (const y of [0.5, 1.15, 1.8]) {
@@ -1215,6 +1295,10 @@ function applyCustomFaceToAll() {
1215
1295
  parts.faceMat.color.set(customFaceTexture ? 0xffffff : parts.tint ?? 0xffffff)
1216
1296
  parts.faceMat.needsUpdate = true
1217
1297
  }
1298
+ if (selectedId !== null) {
1299
+ const entry = agentsById.get(selectedId)
1300
+ if (entry) renderCard(entry.agent)
1301
+ }
1218
1302
  }
1219
1303
 
1220
1304
  function loadCustomFace() {
@@ -1222,9 +1306,11 @@ function loadCustomFace() {
1222
1306
  try { data = localStorage.getItem(CUSTOM_FACE_KEY) ?? '' } catch {}
1223
1307
  if (!data) {
1224
1308
  customFaceTexture = null
1309
+ customFaceData = ''
1225
1310
  applyCustomFaceToAll()
1226
1311
  return
1227
1312
  }
1313
+ customFaceData = data
1228
1314
  faceLoader.load(data, texture => {
1229
1315
  texture.colorSpace = THREE.SRGBColorSpace
1230
1316
  customFaceTexture = texture
@@ -1232,8 +1318,20 @@ function loadCustomFace() {
1232
1318
  })
1233
1319
  }
1234
1320
 
1321
+ function applyGlassSetting() {
1322
+ let raw = ''
1323
+ try { raw = localStorage.getItem(GLASS_KEY) ?? '' } catch {}
1324
+ const value = Number.parseFloat(raw)
1325
+ if (Number.isFinite(value)) {
1326
+ document.documentElement.style.setProperty('--glass-alpha', String(Math.min(0.98, Math.max(0.5, value / 100))))
1327
+ } else {
1328
+ document.documentElement.style.removeProperty('--glass-alpha')
1329
+ }
1330
+ }
1331
+
1235
1332
  window.addEventListener('storage', event => {
1236
1333
  if (event.key === CUSTOM_FACE_KEY) loadCustomFace()
1334
+ if (event.key === GLASS_KEY) applyGlassSetting()
1237
1335
  })
1238
1336
  let usage = { tokens_week: 0, tokens_today: 0 }
1239
1337
  let selectedId = null
@@ -1258,8 +1356,8 @@ function globalPose(floorsCount) {
1258
1356
  const height = Math.max(0, (floorsCount - 1) * FLOOR_GAP)
1259
1357
  const centerX = ((floorsCount - 1) * OFFSET_X) / 2
1260
1358
  const centerZ = ((floorsCount - 1) * OFFSET_Z) / 2
1261
- const dist = (13.5 + floorsCount * 2.5) * viewScale()
1262
- const target = new THREE.Vector3(centerX, height / 2 + 1.2, centerZ)
1359
+ const dist = (19 + floorsCount * 3) * viewScale()
1360
+ const target = new THREE.Vector3(centerX + 3.6, height / 2 + 1.0, centerZ + 0.8)
1263
1361
  return {
1264
1362
  target,
1265
1363
  position: new THREE.Vector3(centerX + dist * 0.42, target.y + dist * 0.82, centerZ + dist * 0.9),
@@ -1302,6 +1400,8 @@ function setFloorFocus(index) {
1302
1400
  for (const child of floor.children) child.robot.userData.parts.label.visible = visible
1303
1401
  }
1304
1402
  neonSign.visible = !focused
1403
+ if (annex) annex.visible = !focused
1404
+ if (mcpGroup) mcpGroup.visible = !focused
1305
1405
  const chip = $('#focusChip')
1306
1406
  if (focused) {
1307
1407
  const floor = floors.find(item => item.index === index)
@@ -1409,7 +1509,7 @@ function renderChecks() {
1409
1509
  ]
1410
1510
  const done = checks.filter(item => item.ok).length
1411
1511
  $('#checkScore').textContent = `${done}/${checks.length}`
1412
- $('#checkBody').innerHTML = checks.map(item => `<div class="check ${item.ok ? 'ok' : ''}"><i>✓</i><span>${item.text}</span></div>`).join('')
1512
+ $('#checkBody').innerHTML = checks.map(item => `<div class="check ${item.ok ? 'ok' : ''}"><i><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3.5 8.5l3 3 6-6"/></svg></i><span>${item.text}</span></div>`).join('')
1413
1513
  }
1414
1514
 
1415
1515
  function renderMeter() {
@@ -1432,31 +1532,36 @@ function renderFeed() {
1432
1532
  }).join('')
1433
1533
  }
1434
1534
 
1535
+ let cardHideTimer = null
1536
+
1435
1537
  function renderCard(agent) {
1436
1538
  const card = $('#card')
1437
1539
  if (agent === null) {
1438
- card.classList.remove('show')
1540
+ if (!card.classList.contains('show') || card.classList.contains('is-out')) return
1541
+ card.classList.add('is-out')
1542
+ clearTimeout(cardHideTimer)
1543
+ cardHideTimer = setTimeout(() => card.classList.remove('show', 'is-out'), 160)
1439
1544
  return
1440
1545
  }
1546
+ clearTimeout(cardHideTimer)
1547
+ card.classList.remove('is-out')
1441
1548
  card.classList.add('show')
1442
1549
  const status = STATUS[agent.status] ?? STATUS.resting
1443
1550
  const floor = floors.find(item => item.team === agent.team)
1444
1551
  $('#cardTeam').textContent = floor === undefined ? agent.team : `楼层 ${floor.index + 1} · ${agent.team}`
1445
1552
  const glyphEl = $('#cardGlyph')
1446
1553
  glyphEl.textContent = ''
1447
- glyphEl.style.backgroundImage = `url(${faceDefault})`
1554
+ glyphEl.style.backgroundImage = `url(${customFaceData || faceDefault})`
1448
1555
  glyphEl.style.backgroundSize = '82% 82%'
1449
1556
  glyphEl.style.backgroundRepeat = 'no-repeat'
1450
1557
  glyphEl.style.backgroundPosition = 'center'
1451
- glyphEl.style.backgroundColor = `${agent.color}22`
1452
- glyphEl.style.border = `1px solid ${agent.color}66`
1558
+ glyphEl.style.backgroundColor = customFaceData ? 'transparent' : `${agent.color}22`
1559
+ glyphEl.style.border = customFaceData ? '1px solid var(--line-2)' : `1px solid ${agent.color}66`
1453
1560
  $('#cardName').textContent = agent.name
1454
1561
  $('#cardSub').textContent = agent.schedule
1455
1562
  const statusEl = $('#cardStatus')
1456
1563
  statusEl.textContent = `● ${status.label}`
1457
- statusEl.style.color = status.color
1458
- statusEl.style.borderColor = status.color + '66'
1459
- statusEl.style.background = status.color + '18'
1564
+ statusEl.dataset.status = agent.status
1460
1565
  $('#cardRuns').textContent = String(agent.runs_today ?? 0)
1461
1566
  $('#cardShare').textContent = `${Math.round(agent.usage_pct || 0)}%`
1462
1567
  $('#cardTokens').textContent = fmt(agent.tokens_today || 0)
@@ -1482,6 +1587,255 @@ function teamList(list) {
1482
1587
  return teams.slice(0, 4)
1483
1588
  }
1484
1589
 
1590
+
1591
+ /* ------------------------------------------------------------------ *
1592
+ * 右侧 Annex:三张固定工位 + 讲台 + 小健身房 + 备考隔间
1593
+ * ------------------------------------------------------------------ */
1594
+ const ANNEX_X = 9.6
1595
+ const ANNEX_Z = 0.3
1596
+ const ANNEX_W = 7.2
1597
+ const ANNEX_D = 6.0
1598
+ let annex = null
1599
+ let annexLights = []
1600
+ let mcpCurve = null
1601
+ let mcpBeads = []
1602
+ let mcpDashes = []
1603
+ let mcpGroup = null
1604
+ let mcpSign = null
1605
+
1606
+ function buildAnnex() {
1607
+ annex = new THREE.Group()
1608
+ annex.position.set(ANNEX_X, 0, ANNEX_Z)
1609
+ annex.scale.setScalar(0.84)
1610
+ scene.add(annex)
1611
+
1612
+ const wood = new THREE.MeshStandardMaterial({ color: 0x2a2119, roughness: 0.72, metalness: 0.2 })
1613
+ const dark = new THREE.MeshStandardMaterial({ color: 0x17212b, roughness: 0.5, metalness: 0.5 })
1614
+ const metal = new THREE.MeshStandardMaterial({ color: 0x334654, roughness: 0.35, metalness: 0.75 })
1615
+ const glass = new THREE.MeshStandardMaterial({ color: 0x8fc8ff, transparent: true, opacity: 0.06, roughness: 0.08, metalness: 0.1, depthWrite: false, side: THREE.DoubleSide })
1616
+ const cyan = new THREE.MeshBasicMaterial({ color: 0x7fd7ff, toneMapped: false })
1617
+ const warm = new THREE.MeshBasicMaterial({ color: 0xffcf9a, toneMapped: false })
1618
+ const box = (w, h, d, mat, x, y, z, parent = annex) => {
1619
+ const mesh = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), mat)
1620
+ mesh.position.set(x, y, z)
1621
+ parent.add(mesh)
1622
+ return mesh
1623
+ }
1624
+
1625
+ // 地板 / 边线
1626
+ box(ANNEX_W, 0.3, ANNEX_D, wood, 0, -0.15, 0)
1627
+ const top = new THREE.Mesh(new THREE.PlaneGeometry(ANNEX_W - 0.18, ANNEX_D - 0.18), new THREE.MeshStandardMaterial({ map: floorTexture, roughness: 0.8, metalness: 0.25 }))
1628
+ top.rotation.x = -Math.PI / 2
1629
+ top.position.y = 0.012
1630
+ annex.add(top)
1631
+ const edge = new THREE.LineSegments(
1632
+ new THREE.EdgesGeometry(new THREE.BoxGeometry(ANNEX_W + 0.04, 0.34, ANNEX_D + 0.04)),
1633
+ new THREE.LineBasicMaterial({ color: 0x8fd4ff, transparent: true, opacity: 0.75 }),
1634
+ )
1635
+ annex.add(edge)
1636
+
1637
+ // 玻璃外墙(正面留空)
1638
+ box(ANNEX_W, 2.35, 0.05, glass, 0, 1.18, -ANNEX_D / 2 + 0.08)
1639
+ box(0.05, 2.35, ANNEX_D, glass, -ANNEX_W / 2 + 0.08, 1.18, 0)
1640
+ box(0.05, 2.35, ANNEX_D, glass, ANNEX_W / 2 - 0.08, 1.18, 0)
1641
+ for (const [x, z] of [[-ANNEX_W / 2 + 0.08, -ANNEX_D / 2 + 0.08], [ANNEX_W / 2 - 0.08, -ANNEX_D / 2 + 0.08], [-ANNEX_W / 2 + 0.08, ANNEX_D / 2 - 0.08], [ANNEX_W / 2 - 0.08, ANNEX_D / 2 - 0.08]]) {
1642
+ box(0.09, 2.45, 0.09, dark, x, 1.22, z)
1643
+ }
1644
+ box(ANNEX_W, 0.06, 0.06, metal, 0, 2.38, -ANNEX_D / 2 + 0.08)
1645
+ box(0.06, 0.06, ANNEX_D, metal, -ANNEX_W / 2 + 0.08, 2.38, 0)
1646
+ box(0.06, 0.06, ANNEX_D, metal, ANNEX_W / 2 - 0.08, 2.38, 0)
1647
+ box(ANNEX_W - 1.0, 0.025, 0.02, cyan, 0, 2.31, -ANNEX_D / 2 + 0.16)
1648
+
1649
+ // 三张固定工位
1650
+ function addDesk(x, z) {
1651
+ const g = new THREE.Group()
1652
+ g.position.set(x, 0, z)
1653
+ box(1.16, 0.055, 0.62, wood, 0, 0.72, 0, g)
1654
+ for (const [lx, lz] of [[-0.5, -0.24], [0.5, -0.24], [-0.5, 0.24], [0.5, 0.24]]) {
1655
+ box(0.055, 0.7, 0.055, dark, lx, 0.36, lz, g)
1656
+ }
1657
+ box(0.62, 0.4, 0.045, dark, 0, 0.95, -0.18, g)
1658
+ const screen = new THREE.Mesh(new THREE.PlaneGeometry(0.54, 0.3), cyan)
1659
+ screen.position.set(0, 0.95, -0.155)
1660
+ g.add(screen)
1661
+ box(0.02, 0.06, 0.02, metal, 0, 0.73, 0.12, g)
1662
+ box(0.5, 0.055, 0.06, warm, 0, 0.76, 0.3, g)
1663
+ // 椅子
1664
+ box(0.42, 0.06, 0.4, dark, 0, 0.45, 0.55, g)
1665
+ box(0.42, 0.5, 0.06, dark, 0, 0.7, 0.75, g)
1666
+ const stem = new THREE.Mesh(new THREE.CylinderGeometry(0.025, 0.025, 0.42, 8), metal)
1667
+ stem.position.set(0, 0.22, 0.55)
1668
+ g.add(stem)
1669
+ const base = new THREE.Mesh(new THREE.CylinderGeometry(0.23, 0.23, 0.03, 16), dark)
1670
+ base.position.set(0, 0.02, 0.55)
1671
+ g.add(base)
1672
+ annex.add(g)
1673
+ }
1674
+ addDesk(-2.35, -1.55)
1675
+ addDesk(0, -1.55)
1676
+ addDesk(2.35, -1.55)
1677
+
1678
+ // 讲台
1679
+ const podium = new THREE.Group()
1680
+ podium.position.set(0, 0, 1.55)
1681
+ box(1.35, 0.14, 0.95, dark, 0, 0.07, 0, podium)
1682
+ const lectern = box(0.7, 0.98, 0.5, wood, 0, 0.56, 0, podium)
1683
+ lectern.rotation.x = -0.04
1684
+ const topPlate = box(0.86, 0.06, 0.58, metal, 0, 1.08, 0, podium)
1685
+ topPlate.rotation.x = -0.16
1686
+ const panel = new THREE.Mesh(new THREE.PlaneGeometry(0.46, 0.26), cyan)
1687
+ panel.position.set(0, 0.64, 0.256)
1688
+ podium.add(panel)
1689
+ const mic = new THREE.Mesh(new THREE.CylinderGeometry(0.014, 0.014, 0.36, 6), metal)
1690
+ mic.position.set(0.2, 1.22, 0.02)
1691
+ mic.rotation.z = -0.22
1692
+ podium.add(mic)
1693
+ const micHead = new THREE.Mesh(new THREE.SphereGeometry(0.04, 10, 10), cyan)
1694
+ micHead.position.set(0.24, 1.4, 0.02)
1695
+ podium.add(micHead)
1696
+ annex.add(podium)
1697
+
1698
+ // 小健身房
1699
+ const gym = new THREE.Group()
1700
+ gym.position.set(2.45, 0, 1.5)
1701
+ const belt = box(0.95, 0.14, 0.48, dark, 0, 0.16, 0, gym)
1702
+ box(0.72, 0.025, 0.34, new THREE.MeshStandardMaterial({ color: 0x101820, roughness: 0.9 }), 0, 0.24, 0, gym)
1703
+ for (const side of [-0.5, 0.5]) {
1704
+ box(0.05, 0.62, 0.05, metal, side, 0.46, -0.17, gym)
1705
+ box(0.05, 0.05, 0.5, metal, side, 0.76, -0.02, gym)
1706
+ }
1707
+ box(0.6, 0.05, 0.05, metal, 0, 0.72, -0.17, gym)
1708
+ box(0.3, 0.2, 0.04, dark, 0, 0.83, -0.22, gym)
1709
+ const mat = new THREE.Mesh(new THREE.PlaneGeometry(1.2, 0.8), new THREE.MeshBasicMaterial({ color: 0x1c3b3d, transparent: true, opacity: 0.85 }))
1710
+ mat.rotation.x = -Math.PI / 2
1711
+ mat.position.set(-0.7, 0.018, 0.15)
1712
+ gym.add(mat)
1713
+ // 哑铃架
1714
+ box(0.65, 0.24, 0.3, dark, -0.15, 0.12, -0.62, gym)
1715
+ for (const [dx, w] of [[-0.2, 0.26], [0, 0.3], [0.2, 0.22]]) {
1716
+ const bar = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.035, w, 8), metal)
1717
+ bar.rotation.z = Math.PI / 2
1718
+ bar.position.set(-0.15 + dx, 0.3, -0.62)
1719
+ gym.add(bar)
1720
+ }
1721
+ annex.add(gym)
1722
+
1723
+ // 备考隔间
1724
+ const study = new THREE.Group()
1725
+ study.position.set(-2.55, 0, 1.5)
1726
+ box(1.55, 1.78, 0.04, glass, 0, 0.9, -0.66, study)
1727
+ box(0.04, 1.78, 1.28, glass, -0.76, 0.9, 0, study)
1728
+ box(0.04, 1.78, 1.28, glass, 0.76, 0.9, 0, study)
1729
+ box(1.55, 0.06, 0.06, metal, 0, 1.79, -0.66, study)
1730
+ box(0.06, 0.06, 1.32, metal, -0.76, 1.79, 0, study)
1731
+ box(0.06, 0.06, 1.32, metal, 0.76, 1.79, 0, study)
1732
+ box(0.7, 0.05, 0.42, wood, 0, 0.7, -0.28, study)
1733
+ box(0.7, 0.58, 0.04, dark, 0, 0.95, -0.49, study)
1734
+ box(0.4, 0.05, 0.36, dark, 0, 0.43, 0.14, study)
1735
+ box(0.4, 0.42, 0.05, dark, 0, 0.65, 0.32, study)
1736
+ const lamp = new THREE.Mesh(new THREE.SphereGeometry(0.05, 10, 10), warm)
1737
+ lamp.position.set(0.48, 1.35, -0.3)
1738
+ study.add(lamp)
1739
+ annex.add(study)
1740
+
1741
+ // 室内灯
1742
+ const lightWarm = new THREE.PointLight(0xffd8b0, 2.6, 11, 2)
1743
+ lightWarm.position.set(0, 2.15, 0.4)
1744
+ annex.add(lightWarm)
1745
+ annexLights.push({ light: lightWarm, night: 2.6, day: 1.1 })
1746
+ const lightCool = new THREE.PointLight(0x8fc8ff, 1.8, 10, 2)
1747
+ lightCool.position.set(0, 2.3, -1.8)
1748
+ annex.add(lightCool)
1749
+ annexLights.push({ light: lightCool, night: 1.8, day: 0.8 })
1750
+
1751
+ // 区域标签
1752
+ const tag = (text, x, y, z) => {
1753
+ const el = document.createElement('div')
1754
+ el.className = 'officeTag'
1755
+ el.textContent = text
1756
+ const obj = new CSS2DObject(el)
1757
+ obj.position.set(x, y, z)
1758
+ annex.add(obj)
1759
+ }
1760
+ tag('讲台', 0, 1.8, 1.55)
1761
+ tag('健身', 2.45, 1.95, 1.5)
1762
+ tag('备考', -2.55, 2.0, 1.5)
1763
+
1764
+ // MCP 虚线 + 流光 + 暂未接通标牌
1765
+ mcpGroup = new THREE.Group()
1766
+ scene.add(mcpGroup)
1767
+ mcpCurve = new THREE.CatmullRomCurve3([
1768
+ new THREE.Vector3(4.55, 2.55, 0.35),
1769
+ new THREE.Vector3(5.15, 3.35, 0.55),
1770
+ new THREE.Vector3(5.8, 3.05, 0.8),
1771
+ new THREE.Vector3(6.58, 2.3, 1.14),
1772
+ ])
1773
+ const pts = mcpCurve.getPoints(90)
1774
+ const cableGeo = new THREE.BufferGeometry().setFromPoints(pts)
1775
+ const cable = new THREE.Line(cableGeo, new THREE.LineDashedMaterial({
1776
+ color: 0x2f7fff, dashSize: 0.2, gapSize: 0.16, transparent: true, opacity: 0.9,
1777
+ }))
1778
+ cable.computeLineDistances()
1779
+ mcpGroup.add(cable)
1780
+
1781
+ // 沿曲线流动的短划,形成可见的“流光”
1782
+ const dashGeo = new THREE.BoxGeometry(0.18, 0.05, 0.05)
1783
+ const dashMat = new THREE.MeshBasicMaterial({ color: 0x2f7fff, transparent: true, opacity: 0.95, depthWrite: false, toneMapped: false })
1784
+ for (let i = 0; i < 8; i++) {
1785
+ const dash = new THREE.Mesh(dashGeo, dashMat)
1786
+ mcpGroup.add(dash)
1787
+ mcpDashes.push({ mesh: dash, phase: i / 8 })
1788
+ }
1789
+
1790
+ const beadGeo = new THREE.SphereGeometry(0.042, 8, 8)
1791
+ const beadMat = new THREE.MeshBasicMaterial({ color: 0x66b8ff, transparent: true, opacity: 0.95, depthWrite: false, toneMapped: false })
1792
+ for (let i = 0; i < 9; i++) {
1793
+ const bead = new THREE.Mesh(beadGeo, beadMat)
1794
+ mcpGroup.add(bead)
1795
+ mcpBeads.push({ mesh: bead, phase: i / 9 })
1796
+ }
1797
+
1798
+ const signCanvas = document.createElement('canvas')
1799
+ signCanvas.width = 512
1800
+ signCanvas.height = 160
1801
+ const sctx = signCanvas.getContext('2d')
1802
+ sctx.clearRect(0, 0, 512, 160)
1803
+ sctx.fillStyle = 'rgba(5,16,28,.88)'
1804
+ sctx.beginPath()
1805
+ sctx.roundRect(6, 6, 500, 148, 24)
1806
+ sctx.fill()
1807
+ sctx.strokeStyle = 'rgba(84,168,255,.55)'
1808
+ sctx.lineWidth = 2
1809
+ sctx.stroke()
1810
+ sctx.strokeStyle = '#54a8ff'
1811
+ sctx.lineWidth = 13
1812
+ sctx.lineCap = 'round'
1813
+ const drawX = (cx, cy, size) => {
1814
+ sctx.beginPath(); sctx.moveTo(cx - size, cy - size); sctx.lineTo(cx + size, cy + size); sctx.stroke()
1815
+ sctx.beginPath(); sctx.moveTo(cx + size, cy - size); sctx.lineTo(cx - size, cy + size); sctx.stroke()
1816
+ }
1817
+ drawX(58, 70, 19)
1818
+ drawX(454, 70, 19)
1819
+ sctx.fillStyle = '#d8efff'
1820
+ sctx.textAlign = 'center'
1821
+ sctx.font = '700 58px ui-monospace, Menlo, monospace'
1822
+ sctx.fillText('MCP', 256, 89)
1823
+ sctx.fillStyle = 'rgba(120,190,255,.82)'
1824
+ sctx.font = '500 22px ui-monospace, Menlo, monospace'
1825
+ sctx.fillText('暂未接通', 256, 128)
1826
+ const signTex = new THREE.CanvasTexture(signCanvas)
1827
+ signTex.colorSpace = THREE.SRGBColorSpace
1828
+ mcpSign = new THREE.Sprite(new THREE.SpriteMaterial({ map: signTex, transparent: true, depthWrite: false, toneMapped: false }))
1829
+ mcpSign.position.set(5.6, 4.55, 1.18)
1830
+ mcpSign.scale.set(3.35, 1.05, 1)
1831
+ mcpGroup.add(mcpSign)
1832
+
1833
+ const signGlow = new THREE.Sprite(new THREE.SpriteMaterial({ map: glowTexture, color: 0x3f9dff, transparent: true, opacity: 0.24, blending: THREE.AdditiveBlending, depthWrite: false }))
1834
+ signGlow.position.copy(mcpSign.position)
1835
+ signGlow.scale.set(4.4, 1.8, 1)
1836
+ mcpGroup.add(signGlow)
1837
+ }
1838
+
1485
1839
  function rebuild(list) {
1486
1840
  for (const [, entry] of agentsById) {
1487
1841
  const motion = entry.robot?.userData?.parts?.motion
@@ -1510,6 +1864,8 @@ function rebuild(list) {
1510
1864
  neonSign.position.set((teams.length - 1) * OFFSET_X * 0.5, height + 5.2, (teams.length - 1) * OFFSET_Z * 0.5)
1511
1865
  selectedId = null
1512
1866
  focusFloor = null
1867
+ if (annex) annex.visible = true
1868
+ if (mcpGroup) mcpGroup.visible = true
1513
1869
  $('#focusChip').hidden = true
1514
1870
  const pose = globalPose(teams.length)
1515
1871
  controls.target.copy(pose.target)
@@ -1615,6 +1971,25 @@ function tick() {
1615
1971
  const t = clock.getElapsedTime()
1616
1972
  requestAnimationFrame(tick)
1617
1973
  applyThemeFrame()
1974
+ if (mcpCurve && mcpBeads.length > 0) {
1975
+ const flow = clock.getElapsedTime()
1976
+ for (const bead of mcpBeads) {
1977
+ const p = mcpCurve.getPointAt((flow * 0.11 + bead.phase) % 1)
1978
+ bead.mesh.position.copy(p)
1979
+ bead.mesh.scale.setScalar(0.75 + 0.35 * Math.sin(flow * 5 + bead.phase * 18))
1980
+ }
1981
+ const axis = new THREE.Vector3(1, 0, 0)
1982
+ const point = new THREE.Vector3()
1983
+ const tangent = new THREE.Vector3()
1984
+ for (const dash of mcpDashes) {
1985
+ const t = (flow * 0.075 + dash.phase) % 1
1986
+ mcpCurve.getPointAt(t, point)
1987
+ mcpCurve.getTangentAt(t, tangent)
1988
+ dash.mesh.position.copy(point)
1989
+ dash.mesh.quaternion.setFromUnitVectors(axis, tangent.normalize())
1990
+ }
1991
+ if (mcpSign) mcpSign.position.y = 4.55 + Math.sin(flow * 0.9) * 0.04
1992
+ }
1618
1993
 
1619
1994
  const focusedTeam = focusFloor === null ? null : floors.find(floor => floor.index === focusFloor)?.team
1620
1995
  for (const [id, entry] of agentsById) {
@@ -1628,6 +2003,13 @@ function tick() {
1628
2003
  const working = status === 'working'
1629
2004
  const sleeping = status === 'resting'
1630
2005
  const phase = (id.charCodeAt(2) || 0) * 0.7
2006
+ const nowMs = Date.now()
2007
+ if (working) {
2008
+ if (!entry.workingSince) entry.workingSince = nowMs
2009
+ } else {
2010
+ entry.workingSince = 0
2011
+ }
2012
+ const overloaded = working && entry.workingSince > 0 && nowMs - entry.workingSince > LOAD_WARN_MS
1631
2013
  const amp = working ? 0.09 : 0.055
1632
2014
  const bob = reduceMotion ? 0 : (parts.motion.bob * 2 - 1) * amp
1633
2015
  parts.face.position.y = parts.baseY + bob
@@ -1638,7 +2020,7 @@ function tick() {
1638
2020
  }
1639
2021
 
1640
2022
  // 表情只随状态切换;有自定义猫咪图片时优先使用
1641
- const expr = FACE_FOR_STATUS[status] ?? 'default'
2023
+ const expr = overloaded ? 'dizzy' : (FACE_FOR_STATUS[status] ?? 'default')
1642
2024
  if (customFaceTexture) {
1643
2025
  if (parts.faceMat.map !== customFaceTexture) {
1644
2026
  parts.faceMat.map = customFaceTexture
@@ -1650,18 +2032,41 @@ function tick() {
1650
2032
  parts.faceMat.map = faceTextures[expr]
1651
2033
  parts.faceMat.needsUpdate = true
1652
2034
  }
1653
- parts.faceMat.color.copy(parts.tint)
2035
+ parts.faceMat.color.copy(parts.tint).lerp(OVERLOAD_COLOR, overloaded ? 0.55 : 0)
2036
+ }
2037
+
2038
+ // 正常工作:汗滴;长时间工作:红温乱线
2039
+ const sweaty = working && !overloaded
2040
+ for (const drop of parts.sweat) {
2041
+ drop.sprite.visible = sweaty
2042
+ if (!sweaty) continue
2043
+ const p = (t * 0.55 + drop.phase) % 1
2044
+ drop.sprite.position.set(drop.x + Math.sin(t * 2 + drop.phase * 8) * drop.drift, parts.baseY + 0.26 - p * 0.34, 0.12)
2045
+ drop.sprite.material.opacity = Math.sin(p * Math.PI) * 0.92
2046
+ drop.sprite.scale.set(0.085 + p * 0.03, 0.13 + p * 0.05, 1)
2047
+ }
2048
+ parts.scribble.visible = overloaded
2049
+ parts.scribbleGlow.visible = overloaded
2050
+ if (overloaded) {
2051
+ const jitter = Math.sin(t * 17 + phase) * 0.012
2052
+ parts.scribble.position.set(jitter, parts.baseY + 1.55 + Math.sin(t * 3.2 + phase) * 0.03, 0.04)
2053
+ parts.scribble.rotation = Math.sin(t * 6 + phase) * 0.08
2054
+ parts.scribble.material.opacity = 0.96
2055
+ parts.scribbleGlow.position.copy(parts.scribble.position)
2056
+ parts.scribbleGlow.position.z = 0.02
2057
+ parts.scribbleGlow.material.opacity = 0.26 + Math.sin(t * 8 + phase) * 0.08
1654
2058
  }
1655
2059
 
1656
2060
  // 呼吸/浮动
1657
2061
  const breathe = sleeping ? 1 + Math.sin(t * 1.5 + phase) * 0.02 : 1 + Math.sin(t * 2.2 + phase) * 0.012
1658
2062
  parts.face.scale.set(0.6 * breathe, 0.6 * breathe, 1)
1659
2063
 
1660
- const statusColor = STATUS[status]?.color ?? STATUS.resting.color
2064
+ const statusColor = overloaded ? '#ff5a5a' : (STATUS[status]?.color ?? STATUS.resting.color)
1661
2065
  parts.ring.material.color.set(statusColor)
1662
2066
  parts.glow.material.color.set(statusColor)
1663
2067
  parts.ring.rotation.z = status === 'stuck' ? t * 2.6 : 0
1664
- const ringPulse = working ? 1 + Math.sin(t * 3.4 + phase) * 0.08
2068
+ const ringPulse = overloaded ? 1 + Math.sin(t * 6.5 + phase) * 0.12
2069
+ : working ? 1 + Math.sin(t * 3.4 + phase) * 0.08
1665
2070
  : status === 'need_me' ? 1 + Math.sin(t * 5 + phase) * 0.1 : 1
1666
2071
  parts.ring.scale.setScalar(ringPulse * (id === selectedId ? 1.24 : 1))
1667
2072
  for (let k = 0; k < parts.ripples.length; k++) {
@@ -1673,7 +2078,8 @@ function tick() {
1673
2078
  ripple.scale.setScalar(1 + p * 1.1)
1674
2079
  ripple.material.opacity = (1 - p) * 0.3
1675
2080
  }
1676
- parts.glow.material.opacity = working ? 0.34 + Math.sin(t * 3.4 + phase) * 0.1
2081
+ parts.glow.material.opacity = overloaded ? 0.38 + Math.sin(t * 6.5 + phase) * 0.08
2082
+ : working ? 0.34 + Math.sin(t * 3.4 + phase) * 0.1
1677
2083
  : status === 'need_me' ? 0.32 : status === 'stuck' ? 0.32 : 0.14
1678
2084
 
1679
2085
  // 空闲睡觉:飘 Z
@@ -1855,6 +2261,8 @@ window.__NS = {
1855
2261
  rebuild([])
1856
2262
  $('#offline').classList.add('show')
1857
2263
  $('#offline').textContent = '正在读取本机会话…'
2264
+ buildAnnex()
2265
+ applyGlassSetting()
1858
2266
  loadCustomFace()
1859
2267
  poll()
1860
2268
  setInterval(poll, 2000)
Binary file
Binary file
package/screenshot.png CHANGED
Binary file