tao-shen commited on
Commit
86b44f8
·
1 Parent(s): 6cf7429

Revert "feat: add pixel animation homepage with Express proxy layer"

Browse files

This reverts commit 6cf74294015c25e76d77e66dfb929c3fb8ed5a2d.

Dockerfile CHANGED
@@ -1,5 +1,5 @@
1
- # OpenClaw on Hugging Face Spaces — v3 with Pixel Animation + A2A
2
- # Features: Star-Office-UI animation homepage, Express proxy, A2A gateway
3
  FROM node:22-bookworm
4
  SHELL ["/bin/bash", "-c"]
5
 
@@ -52,32 +52,10 @@ RUN echo "[build][layer2] Clone + install + build..." && START=$(date +%s) \
52
  && echo "[build] version: $(cat /app/openclaw/.version)" \
53
  && echo "[build][layer2] Total clone+install+build: $(($(date +%s) - START))s"
54
 
55
- # ── Layer 2b (node): A2A Gateway Plugin ──────────────────────────────────────
56
- RUN echo "[build][layer2b] A2A gateway plugin..." && START=$(date +%s) \
57
- && git clone --depth 1 https://github.com/win4r/openclaw-a2a-gateway.git \
58
- /app/openclaw/extensions/a2a-gateway \
59
- && cd /app/openclaw/extensions/a2a-gateway \
60
- && npm install --production 2>/dev/null || echo "[build] A2A npm install skipped (will retry at runtime)" \
61
- && echo "[build][layer2b] A2A gateway: $(($(date +%s) - START))s"
62
-
63
- # ── Layer 2c (node): Star-Office-UI frontend assets ──────────────────────────
64
- # Clone full asset set, then overlay our modified text files on top
65
- RUN echo "[build][layer2c] Star-Office-UI assets..." && START=$(date +%s) \
66
- && git clone --depth 1 https://github.com/ringhyacinth/Star-Office-UI.git /tmp/star-office \
67
- && cp -r /tmp/star-office/frontend /home/node/frontend \
68
- && rm -rf /tmp/star-office \
69
- && echo "[build][layer2c] Star-Office-UI: $(($(date +%s) - START))s"
70
-
71
- # ── Layer 3 (node): Scripts + Config + Frontend overrides ────────────────────
72
  COPY --chown=node:node scripts /home/node/scripts
73
- # Overlay modified frontend files (index.html, game.js, layout.js) on top of cloned assets
74
- COPY --chown=node:node frontend/*.html /home/node/frontend/
75
- COPY --chown=node:node frontend/*.js /home/node/frontend/
76
  COPY --chown=node:node openclaw.json /home/node/scripts/openclaw.json.default
77
- COPY --chown=node:node package.json /home/node/package.json
78
- RUN chmod +x /home/node/scripts/entrypoint.sh /home/node/scripts/sync_hf.py \
79
- && cd /home/node && npm install --production \
80
- && echo "[build] Proxy dependencies installed"
81
 
82
  ENV NODE_ENV=production
83
  ENV OPENCLAW_BUNDLED_PLUGINS_DIR=/app/openclaw/empty-bundled-plugins
 
1
+ # OpenClaw on Hugging Face Spaces — 优化构建(v2)
2
+ # 优化点:node 用户构建(消除 chown)、合并 RUN 层(减少层开销)
3
  FROM node:22-bookworm
4
  SHELL ["/bin/bash", "-c"]
5
 
 
52
  && echo "[build] version: $(cat /app/openclaw/.version)" \
53
  && echo "[build][layer2] Total clone+install+build: $(($(date +%s) - START))s"
54
 
55
+ # ── Layer 3 (node): Scripts + Config ──────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  COPY --chown=node:node scripts /home/node/scripts
 
 
 
57
  COPY --chown=node:node openclaw.json /home/node/scripts/openclaw.json.default
58
+ RUN chmod +x /home/node/scripts/entrypoint.sh /home/node/scripts/sync_hf.py
 
 
 
59
 
60
  ENV NODE_ENV=production
61
  ENV OPENCLAW_BUNDLED_PLUGINS_DIR=/app/openclaw/empty-bundled-plugins
frontend/.gitignore DELETED
@@ -1,15 +0,0 @@
1
- # Binary assets are cloned from Star-Office-UI during Docker build
2
- # Only modified text files (index.html, game.js, layout.js) are tracked in git
3
- *.webp
4
- *.png
5
- *.jpg
6
- *.woff2
7
- *.zip
8
- *.min.js
9
- vendor/
10
- fonts/
11
- *.py
12
- *.md
13
- electron-standalone.html
14
- invite.html
15
- join.html
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/game.js DELETED
@@ -1,1034 +0,0 @@
1
- // Star Office UI - 游戏主逻辑
2
- // 依赖: layout.js(必须在这个之前加载)
3
-
4
- // 检测浏览器是否支持 WebP
5
- let supportsWebP = false;
6
-
7
- // 方法 1: 使用 canvas 检测
8
- function checkWebPSupport() {
9
- return new Promise((resolve) => {
10
- const canvas = document.createElement('canvas');
11
- if (canvas.getContext && canvas.getContext('2d')) {
12
- resolve(canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0);
13
- } else {
14
- resolve(false);
15
- }
16
- });
17
- }
18
-
19
- // 方法 2: 使用 image 检测(备用)
20
- function checkWebPSupportFallback() {
21
- return new Promise((resolve) => {
22
- const img = new Image();
23
- img.onload = () => resolve(true);
24
- img.onerror = () => resolve(false);
25
- img.src = 'data:image/webp;base64,UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAwAAAABBxAR/Q9ERP8DAABWUDggGAAAADABAJ0BKgEAAQADADQlpAADcAD++/1QAA==';
26
- });
27
- }
28
-
29
- // 获取文件扩展名(根据 WebP 支持情况 + 布局配置的 forcePng)
30
- function getExt(pngFile) {
31
- // star-working-spritesheet.png 太宽了,WebP 不支持,始终用 PNG
32
- if (pngFile === 'star-working-spritesheet.png') {
33
- return '.png';
34
- }
35
- // 如果布局配置里强制用 PNG,就用 .png
36
- if (LAYOUT.forcePng && LAYOUT.forcePng[pngFile.replace(/\.(png|webp)$/, '')]) {
37
- return '.png';
38
- }
39
- return supportsWebP ? '.webp' : '.png';
40
- }
41
-
42
- const config = {
43
- type: Phaser.AUTO,
44
- width: LAYOUT.game.width,
45
- height: LAYOUT.game.height,
46
- parent: 'game-container',
47
- pixelArt: true,
48
- physics: { default: 'arcade', arcade: { gravity: { y: 0 }, debug: false } },
49
- scene: { preload: preload, create: create, update: update }
50
- };
51
-
52
- let totalAssets = 0;
53
- let loadedAssets = 0;
54
- let loadingProgressBar, loadingProgressContainer, loadingOverlay, loadingText;
55
-
56
- // Memo 相关函数
57
- async function loadMemo() {
58
- const memoDate = document.getElementById('memo-date');
59
- const memoContent = document.getElementById('memo-content');
60
-
61
- try {
62
- const response = await fetch('/yesterday-memo?t=' + Date.now(), { cache: 'no-store' });
63
- const data = await response.json();
64
-
65
- if (data.success && data.memo) {
66
- memoDate.textContent = data.date || '';
67
- memoContent.innerHTML = data.memo.replace(/\n/g, '<br>');
68
- } else {
69
- memoContent.innerHTML = '<div id="memo-placeholder">暂无昨日日记</div>';
70
- }
71
- } catch (e) {
72
- console.error('加载 memo 失败:', e);
73
- memoContent.innerHTML = '<div id="memo-placeholder">加载失败</div>';
74
- }
75
- }
76
-
77
- // 更新加载进度
78
- function updateLoadingProgress() {
79
- loadedAssets++;
80
- const percent = Math.min(100, Math.round((loadedAssets / totalAssets) * 100));
81
- if (loadingProgressBar) {
82
- loadingProgressBar.style.width = percent + '%';
83
- }
84
- if (loadingText) {
85
- loadingText.textContent = `正在加载 Star 的像素办公室... ${percent}%`;
86
- }
87
- }
88
-
89
- // 隐藏加载界面
90
- function hideLoadingOverlay() {
91
- setTimeout(() => {
92
- if (loadingOverlay) {
93
- loadingOverlay.style.transition = 'opacity 0.5s ease';
94
- loadingOverlay.style.opacity = '0';
95
- setTimeout(() => {
96
- loadingOverlay.style.display = 'none';
97
- }, 500);
98
- }
99
- }, 300);
100
- }
101
-
102
- const STATES = {
103
- idle: { name: '待命', area: 'breakroom' },
104
- writing: { name: '整理文档', area: 'writing' },
105
- researching: { name: '搜索信息', area: 'researching' },
106
- executing: { name: '执行任务', area: 'writing' },
107
- syncing: { name: '同步备份', area: 'writing' },
108
- error: { name: '出错了', area: 'error' }
109
- };
110
-
111
- const BUBBLE_TEXTS = {
112
- idle: [
113
- '待命中:耳朵竖起来了',
114
- '我在这儿,随时可以开工',
115
- '先把桌面收拾干净再说',
116
- '呼——给大脑放个风',
117
- '今天也要优雅地高效',
118
- '等待,是为了更准确的一击',
119
- '咖啡还热,灵感也还在',
120
- '我在后台给你加 Buff',
121
- '状态:静心 / 充电',
122
- '小猫说:慢一点也没关系'
123
- ],
124
- writing: [
125
- '进入专注模式:勿扰',
126
- '先把关键路径跑通',
127
- '我来把复杂变简单',
128
- '把 bug 关进笼子里',
129
- '写到一半,先保存',
130
- '把每一步都做成可回滚',
131
- '今天的进度,明天的底气',
132
- '先收敛,再发散',
133
- '让系统变得更可解释',
134
- '稳住,我们能赢'
135
- ],
136
- researching: [
137
- '我在挖证据链',
138
- '让我把信息熬成结论',
139
- '找到了:关键在这里',
140
- '先把变量控制住',
141
- '我在查:它为什么会这样',
142
- '把直觉写成验证',
143
- '先定位,再优化',
144
- '别急,先画因果图'
145
- ],
146
- executing: [
147
- '执行中:不要眨眼',
148
- '把任务切成小块逐个击破',
149
- '开始跑 pipeline',
150
- '一键推进:走你',
151
- '让结果自己说话',
152
- '先做最小可行,再做最美版本'
153
- ],
154
- syncing: [
155
- '同步中:把今天锁进云里',
156
- '备份不是仪式,是安全感',
157
- '写入中…别断电',
158
- '把变更交给时间戳',
159
- '云端对齐:咔哒',
160
- '同步完成前先��乱动',
161
- '把未来的自己从灾难里救出来',
162
- '多一份备份,少一份后悔'
163
- ],
164
- error: [
165
- '警报响了:先别慌',
166
- '我闻到 bug 的味道了',
167
- '先复现,再谈修复',
168
- '把日志给我,我会说人话',
169
- '错误不是敌人,是线索',
170
- '把影响面圈起来',
171
- '先止血,再手术',
172
- '我在:马上定位根因',
173
- '别怕,这种我见多了',
174
- '报警中:让问题自己现形'
175
- ],
176
- cat: [
177
- '喵~',
178
- '咕噜咕噜…',
179
- '尾巴摇一摇',
180
- '晒太阳最开心',
181
- '有人来看我啦',
182
- '我是这个办公室的吉祥物',
183
- '伸个懒腰',
184
- '今天的罐罐准备好了吗',
185
- '呼噜呼噜',
186
- '这个位置视野最好'
187
- ]
188
- };
189
-
190
- let game, star, sofa, serverroom, areas = {}, currentState = 'idle', pendingDesiredState = null, statusText, lastFetch = 0, lastBlink = 0, lastBubble = 0, targetX = 660, targetY = 170, bubble = null, typewriterText = '', typewriterTarget = '', typewriterIndex = 0, lastTypewriter = 0, syncAnimSprite = null, catBubble = null;
191
- let isMoving = false;
192
- let waypoints = [];
193
- let lastWanderAt = 0;
194
- let coordsOverlay, coordsDisplay, coordsToggle;
195
- let showCoords = false;
196
- const FETCH_INTERVAL = 2000;
197
- const BLINK_INTERVAL = 2500;
198
- const BUBBLE_INTERVAL = 8000;
199
- const CAT_BUBBLE_INTERVAL = 18000;
200
- let lastCatBubble = 0;
201
- const TYPEWRITER_DELAY = 50;
202
- let agents = {}; // agentId -> sprite/container
203
- let lastAgentsFetch = 0;
204
- const AGENTS_FETCH_INTERVAL = 2500;
205
-
206
- // agent 颜色配置
207
- const AGENT_COLORS = {
208
- star: 0xffd700,
209
- npc1: 0x00aaff,
210
- agent_nika: 0xff69b4,
211
- default: 0x94a3b8
212
- };
213
-
214
- // agent 名字颜色
215
- const NAME_TAG_COLORS = {
216
- approved: 0x22c55e,
217
- pending: 0xf59e0b,
218
- rejected: 0xef4444,
219
- offline: 0x64748b,
220
- default: 0x1f2937
221
- };
222
-
223
- // breakroom / writing / error 区域的 agent 分布位置(多 agent 时错开)
224
- const AREA_POSITIONS = {
225
- breakroom: [
226
- { x: 620, y: 180 },
227
- { x: 560, y: 220 },
228
- { x: 680, y: 210 },
229
- { x: 540, y: 170 },
230
- { x: 700, y: 240 },
231
- { x: 600, y: 250 },
232
- { x: 650, y: 160 },
233
- { x: 580, y: 200 }
234
- ],
235
- writing: [
236
- { x: 760, y: 320 },
237
- { x: 830, y: 280 },
238
- { x: 690, y: 350 },
239
- { x: 770, y: 260 },
240
- { x: 850, y: 340 },
241
- { x: 720, y: 300 },
242
- { x: 800, y: 370 },
243
- { x: 750, y: 240 }
244
- ],
245
- error: [
246
- { x: 180, y: 260 },
247
- { x: 120, y: 220 },
248
- { x: 240, y: 230 },
249
- { x: 160, y: 200 },
250
- { x: 220, y: 270 },
251
- { x: 140, y: 250 },
252
- { x: 200, y: 210 },
253
- { x: 260, y: 260 }
254
- ]
255
- };
256
-
257
-
258
- // 状态控制栏函数(用于测试)
259
- function setState(state, detail) {
260
- fetch('/set_state', {
261
- method: 'POST',
262
- headers: { 'Content-Type': 'application/json' },
263
- body: JSON.stringify({ state, detail })
264
- }).then(() => fetchStatus());
265
- }
266
-
267
- // 初始化:先检测 WebP 支持,再启动游戏
268
- async function initGame() {
269
- try {
270
- supportsWebP = await checkWebPSupport();
271
- } catch (e) {
272
- try {
273
- supportsWebP = await checkWebPSupportFallback();
274
- } catch (e2) {
275
- supportsWebP = false;
276
- }
277
- }
278
-
279
- console.log('WebP 支持:', supportsWebP);
280
- new Phaser.Game(config);
281
- }
282
-
283
- function preload() {
284
- loadingOverlay = document.getElementById('loading-overlay');
285
- loadingProgressBar = document.getElementById('loading-progress-bar');
286
- loadingText = document.getElementById('loading-text');
287
- loadingProgressContainer = document.getElementById('loading-progress-container');
288
-
289
- // 从 LAYOUT 读取总资源数量(避免 magic number)
290
- totalAssets = LAYOUT.totalAssets || 15;
291
- loadedAssets = 0;
292
-
293
- this.load.on('filecomplete', () => {
294
- updateLoadingProgress();
295
- });
296
-
297
- this.load.on('complete', () => {
298
- hideLoadingOverlay();
299
- });
300
-
301
- this.load.image('office_bg', '/static/office_bg_small' + (supportsWebP ? '.webp' : '.png') + '?v={{VERSION_TIMESTAMP}}');
302
- this.load.spritesheet('star_idle', '/static/star-idle-spritesheet' + getExt('star-idle-spritesheet.png'), { frameWidth: 128, frameHeight: 128 });
303
- this.load.spritesheet('star_researching', '/static/star-researching-spritesheet' + getExt('star-researching-spritesheet.png'), { frameWidth: 128, frameHeight: 105 });
304
-
305
- this.load.image('sofa_idle', '/static/sofa-idle' + getExt('sofa-idle.png'));
306
- this.load.spritesheet('sofa_busy', '/static/sofa-busy-spritesheet' + getExt('sofa-busy-spritesheet.png'), { frameWidth: 256, frameHeight: 256 });
307
-
308
- this.load.spritesheet('plants', '/static/plants-spritesheet' + getExt('plants-spritesheet.png'), { frameWidth: 160, frameHeight: 160 });
309
- this.load.spritesheet('posters', '/static/posters-spritesheet' + getExt('posters-spritesheet.png'), { frameWidth: 160, frameHeight: 160 });
310
- this.load.spritesheet('coffee_machine', '/static/coffee-machine-spritesheet' + getExt('coffee-machine-spritesheet.png'), { frameWidth: 230, frameHeight: 230 });
311
- this.load.spritesheet('serverroom', '/static/serverroom-spritesheet' + getExt('serverroom-spritesheet.png'), { frameWidth: 180, frameHeight: 251 });
312
-
313
- this.load.spritesheet('error_bug', '/static/error-bug-spritesheet-grid' + (supportsWebP ? '.webp' : '.png'), { frameWidth: 180, frameHeight: 180 });
314
- this.load.spritesheet('cats', '/static/cats-spritesheet' + (supportsWebP ? '.webp' : '.png'), { frameWidth: 160, frameHeight: 160 });
315
- this.load.image('desk', '/static/desk' + getExt('desk.png'));
316
- this.load.spritesheet('star_working', '/static/star-working-spritesheet-grid' + (supportsWebP ? '.webp' : '.png'), { frameWidth: 230, frameHeight: 144 });
317
- this.load.spritesheet('sync_anim', '/static/sync-animation-spritesheet-grid' + (supportsWebP ? '.webp' : '.png'), { frameWidth: 256, frameHeight: 256 });
318
- this.load.image('memo_bg', '/static/memo-bg' + (supportsWebP ? '.webp' : '.png'));
319
-
320
- // 新办公桌:强制 PNG(透明)
321
- this.load.image('desk_v2', '/static/desk-v2.png');
322
- this.load.spritesheet('flowers', '/static/flowers-spritesheet' + (supportsWebP ? '.webp' : '.png'), { frameWidth: 65, frameHeight: 65 });
323
- }
324
-
325
- function create() {
326
- game = this;
327
- this.add.image(640, 360, 'office_bg');
328
-
329
- // === 沙发(来自 LAYOUT)===
330
- sofa = this.add.sprite(
331
- LAYOUT.furniture.sofa.x,
332
- LAYOUT.furniture.sofa.y,
333
- 'sofa_busy'
334
- ).setOrigin(LAYOUT.furniture.sofa.origin.x, LAYOUT.furniture.sofa.origin.y);
335
- sofa.setDepth(LAYOUT.furniture.sofa.depth);
336
-
337
- this.anims.create({
338
- key: 'sofa_busy',
339
- frames: this.anims.generateFrameNumbers('sofa_busy', { start: 0, end: 47 }),
340
- frameRate: 12,
341
- repeat: -1
342
- });
343
-
344
- areas = LAYOUT.areas;
345
-
346
- this.anims.create({
347
- key: 'star_idle',
348
- frames: this.anims.generateFrameNumbers('star_idle', { start: 0, end: 29 }),
349
- frameRate: 12,
350
- repeat: -1
351
- });
352
- this.anims.create({
353
- key: 'star_researching',
354
- frames: this.anims.generateFrameNumbers('star_researching', { start: 0, end: 95 }),
355
- frameRate: 12,
356
- repeat: -1
357
- });
358
-
359
- star = game.physics.add.sprite(areas.breakroom.x, areas.breakroom.y, 'star_idle');
360
- star.setOrigin(0.5);
361
- star.setScale(1.4);
362
- star.setAlpha(0.95);
363
- star.setDepth(20);
364
- star.setVisible(false);
365
- star.anims.stop();
366
-
367
- if (game.textures.exists('sofa_busy')) {
368
- sofa.setTexture('sofa_busy');
369
- sofa.anims.play('sofa_busy', true);
370
- }
371
-
372
- // === 牌匾(来自 LAYOUT)===
373
- const plaqueX = LAYOUT.plaque.x;
374
- const plaqueY = LAYOUT.plaque.y;
375
- const plaqueBg = game.add.rectangle(plaqueX, plaqueY, LAYOUT.plaque.width, LAYOUT.plaque.height, 0x5d4037);
376
- plaqueBg.setStrokeStyle(3, 0x3e2723);
377
- const plaqueText = game.add.text(plaqueX, plaqueY, 'HuggingClaw Pixel Office', {
378
- fontFamily: 'ArkPixel, monospace',
379
- fontSize: '18px',
380
- fill: '#ffd700',
381
- fontWeight: 'bold',
382
- stroke: '#000',
383
- strokeThickness: 2
384
- }).setOrigin(0.5);
385
- game.add.text(plaqueX - 190, plaqueY, '⭐', { fontFamily: 'ArkPixel, monospace', fontSize: '20px' }).setOrigin(0.5);
386
- game.add.text(plaqueX + 190, plaqueY, '⭐', { fontFamily: 'ArkPixel, monospace', fontSize: '20px' }).setOrigin(0.5);
387
-
388
- // === 植物们(来自 LAYOUT)===
389
- const plantFrameCount = 16;
390
- for (let i = 0; i < LAYOUT.furniture.plants.length; i++) {
391
- const p = LAYOUT.furniture.plants[i];
392
- const randomPlantFrame = Math.floor(Math.random() * plantFrameCount);
393
- const plant = game.add.sprite(p.x, p.y, 'plants', randomPlantFrame).setOrigin(0.5);
394
- plant.setDepth(p.depth);
395
- plant.setInteractive({ useHandCursor: true });
396
- window[`plantSprite${i === 0 ? '' : i + 1}`] = plant;
397
- plant.on('pointerdown', (() => {
398
- const next = Math.floor(Math.random() * plantFrameCount);
399
- plant.setFrame(next);
400
- }));
401
- }
402
-
403
- // === 海报(来自 LAYOUT)===
404
- const postersFrameCount = 32;
405
- const randomPosterFrame = Math.floor(Math.random() * postersFrameCount);
406
- const poster = game.add.sprite(LAYOUT.furniture.poster.x, LAYOUT.furniture.poster.y, 'posters', randomPosterFrame).setOrigin(0.5);
407
- poster.setDepth(LAYOUT.furniture.poster.depth);
408
- poster.setInteractive({ useHandCursor: true });
409
- window.posterSprite = poster;
410
- window.posterFrameCount = postersFrameCount;
411
- poster.on('pointerdown', () => {
412
- const next = Math.floor(Math.random() * window.posterFrameCount);
413
- window.posterSprite.setFrame(next);
414
- });
415
-
416
- // === 小猫(来自 LAYOUT)===
417
- const catsFrameCount = 16;
418
- const randomCatFrame = Math.floor(Math.random() * catsFrameCount);
419
- const cat = game.add.sprite(LAYOUT.furniture.cat.x, LAYOUT.furniture.cat.y, 'cats', randomCatFrame).setOrigin(LAYOUT.furniture.cat.origin.x, LAYOUT.furniture.cat.origin.y);
420
- cat.setDepth(LAYOUT.furniture.cat.depth);
421
- cat.setInteractive({ useHandCursor: true });
422
- window.catSprite = cat;
423
- window.catsFrameCount = catsFrameCount;
424
- cat.on('pointerdown', () => {
425
- const next = Math.floor(Math.random() * window.catsFrameCount);
426
- window.catSprite.setFrame(next);
427
- });
428
-
429
- // === 咖啡机(来自 LAYOUT)===
430
- this.anims.create({
431
- key: 'coffee_machine',
432
- frames: this.anims.generateFrameNumbers('coffee_machine', { start: 0, end: 95 }),
433
- frameRate: 12.5,
434
- repeat: -1
435
- });
436
- const coffeeMachine = this.add.sprite(
437
- LAYOUT.furniture.coffeeMachine.x,
438
- LAYOUT.furniture.coffeeMachine.y,
439
- 'coffee_machine'
440
- ).setOrigin(LAYOUT.furniture.coffeeMachine.origin.x, LAYOUT.furniture.coffeeMachine.origin.y);
441
- coffeeMachine.setDepth(LAYOUT.furniture.coffeeMachine.depth);
442
- coffeeMachine.anims.play('coffee_machine', true);
443
-
444
- // === 服务器区(来自 LAYOUT)===
445
- this.anims.create({
446
- key: 'serverroom_on',
447
- frames: this.anims.generateFrameNumbers('serverroom', { start: 0, end: 39 }),
448
- frameRate: 6,
449
- repeat: -1
450
- });
451
- serverroom = this.add.sprite(
452
- LAYOUT.furniture.serverroom.x,
453
- LAYOUT.furniture.serverroom.y,
454
- 'serverroom',
455
- 0
456
- ).setOrigin(LAYOUT.furniture.serverroom.origin.x, LAYOUT.furniture.serverroom.origin.y);
457
- serverroom.setDepth(LAYOUT.furniture.serverroom.depth);
458
- serverroom.anims.stop();
459
- serverroom.setFrame(0);
460
-
461
- // === 新办公桌(来自 LAYOUT,强制透明 PNG)===
462
- const desk = this.add.image(
463
- LAYOUT.furniture.desk.x,
464
- LAYOUT.furniture.desk.y,
465
- 'desk_v2'
466
- ).setOrigin(LAYOUT.furniture.desk.origin.x, LAYOUT.furniture.desk.origin.y);
467
- desk.setDepth(LAYOUT.furniture.desk.depth);
468
-
469
- // === 花盆(来自 LAYOUT)===
470
- const flowerFrameCount = 16;
471
- const randomFlowerFrame = Math.floor(Math.random() * flowerFrameCount);
472
- const flower = this.add.sprite(
473
- LAYOUT.furniture.flower.x,
474
- LAYOUT.furniture.flower.y,
475
- 'flowers',
476
- randomFlowerFrame
477
- ).setOrigin(LAYOUT.furniture.flower.origin.x, LAYOUT.furniture.flower.origin.y);
478
- flower.setScale(LAYOUT.furniture.flower.scale || 1);
479
- flower.setDepth(LAYOUT.furniture.flower.depth);
480
- flower.setInteractive({ useHandCursor: true });
481
- window.flowerSprite = flower;
482
- window.flowerFrameCount = flowerFrameCount;
483
- flower.on('pointerdown', () => {
484
- const next = Math.floor(Math.random() * window.flowerFrameCount);
485
- window.flowerSprite.setFrame(next);
486
- });
487
-
488
- // === Star 在桌前工作(来自 LAYOUT)===
489
- this.anims.create({
490
- key: 'star_working',
491
- frames: this.anims.generateFrameNumbers('star_working', { start: 0, end: 191 }),
492
- frameRate: 12,
493
- repeat: -1
494
- });
495
- this.anims.create({
496
- key: 'error_bug',
497
- frames: this.anims.generateFrameNumbers('error_bug', { start: 0, end: 95 }),
498
- frameRate: 12,
499
- repeat: -1
500
- });
501
-
502
- // === 错误 bug(来自 LAYOUT)===
503
- const errorBug = this.add.sprite(
504
- LAYOUT.furniture.errorBug.x,
505
- LAYOUT.furniture.errorBug.y,
506
- 'error_bug',
507
- 0
508
- ).setOrigin(LAYOUT.furniture.errorBug.origin.x, LAYOUT.furniture.errorBug.origin.y);
509
- errorBug.setDepth(LAYOUT.furniture.errorBug.depth);
510
- errorBug.setVisible(false);
511
- errorBug.setScale(LAYOUT.furniture.errorBug.scale);
512
- errorBug.anims.play('error_bug', true);
513
- window.errorBug = errorBug;
514
- window.errorBugDir = 1;
515
-
516
- const starWorking = this.add.sprite(
517
- LAYOUT.furniture.starWorking.x,
518
- LAYOUT.furniture.starWorking.y,
519
- 'star_working',
520
- 0
521
- ).setOrigin(LAYOUT.furniture.starWorking.origin.x, LAYOUT.furniture.starWorking.origin.y);
522
- starWorking.setVisible(false);
523
- starWorking.setScale(LAYOUT.furniture.starWorking.scale);
524
- starWorking.setDepth(LAYOUT.furniture.starWorking.depth);
525
- window.starWorking = starWorking;
526
-
527
- // === 同步动画(来自 LAYOUT)===
528
- this.anims.create({
529
- key: 'sync_anim',
530
- frames: this.anims.generateFrameNumbers('sync_anim', { start: 1, end: 52 }),
531
- frameRate: 12,
532
- repeat: -1
533
- });
534
- syncAnimSprite = this.add.sprite(
535
- LAYOUT.furniture.syncAnim.x,
536
- LAYOUT.furniture.syncAnim.y,
537
- 'sync_anim',
538
- 0
539
- ).setOrigin(LAYOUT.furniture.syncAnim.origin.x, LAYOUT.furniture.syncAnim.origin.y);
540
- syncAnimSprite.setDepth(LAYOUT.furniture.syncAnim.depth);
541
- syncAnimSprite.anims.stop();
542
- syncAnimSprite.setFrame(0);
543
-
544
- window.starSprite = star;
545
-
546
- statusText = document.getElementById('status-text');
547
- coordsOverlay = document.getElementById('coords-overlay');
548
- coordsDisplay = document.getElementById('coords-display');
549
- coordsToggle = document.getElementById('coords-toggle');
550
-
551
- coordsToggle.addEventListener('click', () => {
552
- showCoords = !showCoords;
553
- coordsOverlay.style.display = showCoords ? 'block' : 'none';
554
- coordsToggle.textContent = showCoords ? '隐藏坐标' : '显示坐标';
555
- coordsToggle.style.background = showCoords ? '#e94560' : '#333';
556
- });
557
-
558
- game.input.on('pointermove', (pointer) => {
559
- if (!showCoords) return;
560
- const x = Math.max(0, Math.min(config.width - 1, Math.round(pointer.x)));
561
- const y = Math.max(0, Math.min(config.height - 1, Math.round(pointer.y)));
562
- coordsDisplay.textContent = `${x}, ${y}`;
563
- coordsOverlay.style.left = (pointer.x + 18) + 'px';
564
- coordsOverlay.style.top = (pointer.y + 18) + 'px';
565
- });
566
-
567
- loadMemo();
568
- fetchStatus();
569
- fetchAgents();
570
-
571
- // 可选调试:仅在显式开启 debug 模式时渲染测试用尼卡 agent
572
- let debugAgents = false;
573
- try {
574
- if (typeof window !== 'undefined') {
575
- if (window.STAR_OFFICE_DEBUG_AGENTS === true) {
576
- debugAgents = true;
577
- } else if (window.location && window.location.search && typeof URLSearchParams !== 'undefined') {
578
- const sp = new URLSearchParams(window.location.search);
579
- if (sp.get('debugAgents') === '1') {
580
- debugAgents = true;
581
- }
582
- }
583
- }
584
- } catch (e) {
585
- debugAgents = false;
586
- }
587
-
588
- if (debugAgents) {
589
- const testNika = {
590
- agentId: 'agent_nika',
591
- name: '尼卡',
592
- isMain: false,
593
- state: 'writing',
594
- detail: '在画像素画...',
595
- area: 'writing',
596
- authStatus: 'approved',
597
- updated_at: new Date().toISOString()
598
- };
599
- renderAgent(testNika);
600
-
601
- window.testNikaState = 'writing';
602
- window.testNikaTimer = setInterval(() => {
603
- const states = ['idle', 'writing', 'researching', 'executing'];
604
- const areas = { idle: 'breakroom', writing: 'writing', researching: 'writing', executing: 'writing' };
605
- window.testNikaState = states[Math.floor(Math.random() * states.length)];
606
- const testAgent = {
607
- agentId: 'agent_nika',
608
- name: '尼卡',
609
- isMain: false,
610
- state: window.testNikaState,
611
- detail: '在画像素画...',
612
- area: areas[window.testNikaState],
613
- authStatus: 'approved',
614
- updated_at: new Date().toISOString()
615
- };
616
- renderAgent(testAgent);
617
- }, 5000);
618
- }
619
- }
620
-
621
- function update(time) {
622
- if (time - lastFetch > FETCH_INTERVAL) { fetchStatus(); lastFetch = time; }
623
- if (time - lastAgentsFetch > AGENTS_FETCH_INTERVAL) { fetchAgents(); lastAgentsFetch = time; }
624
-
625
- const effectiveStateForServer = pendingDesiredState || currentState;
626
- if (serverroom) {
627
- if (effectiveStateForServer === 'idle') {
628
- if (serverroom.anims.isPlaying) {
629
- serverroom.anims.stop();
630
- serverroom.setFrame(0);
631
- }
632
- } else {
633
- if (!serverroom.anims.isPlaying || serverroom.anims.currentAnim?.key !== 'serverroom_on') {
634
- serverroom.anims.play('serverroom_on', true);
635
- }
636
- }
637
- }
638
-
639
- if (window.errorBug) {
640
- if (effectiveStateForServer === 'error') {
641
- window.errorBug.setVisible(true);
642
- if (!window.errorBug.anims.isPlaying || window.errorBug.anims.currentAnim?.key !== 'error_bug') {
643
- window.errorBug.anims.play('error_bug', true);
644
- }
645
- const leftX = LAYOUT.furniture.errorBug.pingPong.leftX;
646
- const rightX = LAYOUT.furniture.errorBug.pingPong.rightX;
647
- const speed = LAYOUT.furniture.errorBug.pingPong.speed;
648
- const dir = window.errorBugDir || 1;
649
- window.errorBug.x += speed * dir;
650
- window.errorBug.y = LAYOUT.furniture.errorBug.y;
651
- if (window.errorBug.x >= rightX) {
652
- window.errorBug.x = rightX;
653
- window.errorBugDir = -1;
654
- } else if (window.errorBug.x <= leftX) {
655
- window.errorBug.x = leftX;
656
- window.errorBugDir = 1;
657
- }
658
- } else {
659
- window.errorBug.setVisible(false);
660
- window.errorBug.anims.stop();
661
- }
662
- }
663
-
664
- if (syncAnimSprite) {
665
- if (effectiveStateForServer === 'syncing') {
666
- if (!syncAnimSprite.anims.isPlaying || syncAnimSprite.anims.currentAnim?.key !== 'sync_anim') {
667
- syncAnimSprite.anims.play('sync_anim', true);
668
- }
669
- } else {
670
- if (syncAnimSprite.anims.isPlaying) syncAnimSprite.anims.stop();
671
- syncAnimSprite.setFrame(0);
672
- }
673
- }
674
-
675
- if (time - lastBubble > BUBBLE_INTERVAL) {
676
- showBubble();
677
- lastBubble = time;
678
- }
679
- if (time - lastCatBubble > CAT_BUBBLE_INTERVAL) {
680
- showCatBubble();
681
- lastCatBubble = time;
682
- }
683
-
684
- if (typewriterIndex < typewriterTarget.length && time - lastTypewriter > TYPEWRITER_DELAY) {
685
- typewriterText += typewriterTarget[typewriterIndex];
686
- statusText.textContent = typewriterText;
687
- typewriterIndex++;
688
- lastTypewriter = time;
689
- }
690
-
691
- moveStar(time);
692
- }
693
-
694
- function normalizeState(s) {
695
- if (!s) return 'idle';
696
- if (s === 'working') return 'writing';
697
- if (s === 'run' || s === 'running') return 'executing';
698
- if (s === 'sync') return 'syncing';
699
- if (s === 'research') return 'researching';
700
- return s;
701
- }
702
-
703
- function fetchStatus() {
704
- fetch('/status')
705
- .then(response => response.json())
706
- .then(data => {
707
- const nextState = normalizeState(data.state);
708
- const stateInfo = STATES[nextState] || STATES.idle;
709
- const changed = (pendingDesiredState === null) && (nextState !== currentState);
710
- const nextLine = '[' + stateInfo.name + '] ' + (data.detail || '...');
711
- if (changed) {
712
- typewriterTarget = nextLine;
713
- typewriterText = '';
714
- typewriterIndex = 0;
715
-
716
- pendingDesiredState = null;
717
- currentState = nextState;
718
-
719
- if (nextState === 'idle') {
720
- if (game.textures.exists('sofa_busy')) {
721
- sofa.setTexture('sofa_busy');
722
- sofa.anims.play('sofa_busy', true);
723
- }
724
- star.setVisible(false);
725
- star.anims.stop();
726
- if (window.starWorking) {
727
- window.starWorking.setVisible(false);
728
- window.starWorking.anims.stop();
729
- }
730
- } else if (nextState === 'error') {
731
- sofa.anims.stop();
732
- sofa.setTexture('sofa_idle');
733
- star.setVisible(false);
734
- star.anims.stop();
735
- if (window.starWorking) {
736
- window.starWorking.setVisible(false);
737
- window.starWorking.anims.stop();
738
- }
739
- } else if (nextState === 'syncing') {
740
- sofa.anims.stop();
741
- sofa.setTexture('sofa_idle');
742
- star.setVisible(false);
743
- star.anims.stop();
744
- if (window.starWorking) {
745
- window.starWorking.setVisible(false);
746
- window.starWorking.anims.stop();
747
- }
748
- } else {
749
- sofa.anims.stop();
750
- sofa.setTexture('sofa_idle');
751
- star.setVisible(false);
752
- star.anims.stop();
753
- if (window.starWorking) {
754
- window.starWorking.setVisible(true);
755
- window.starWorking.anims.play('star_working', true);
756
- }
757
- }
758
-
759
- if (serverroom) {
760
- if (nextState === 'idle') {
761
- serverroom.anims.stop();
762
- serverroom.setFrame(0);
763
- } else {
764
- serverroom.anims.play('serverroom_on', true);
765
- }
766
- }
767
-
768
- if (syncAnimSprite) {
769
- if (nextState === 'syncing') {
770
- if (!syncAnimSprite.anims.isPlaying || syncAnimSprite.anims.currentAnim?.key !== 'sync_anim') {
771
- syncAnimSprite.anims.play('sync_anim', true);
772
- }
773
- } else {
774
- if (syncAnimSprite.anims.isPlaying) syncAnimSprite.anims.stop();
775
- syncAnimSprite.setFrame(0);
776
- }
777
- }
778
- } else {
779
- if (!typewriterTarget || typewriterTarget !== nextLine) {
780
- typewriterTarget = nextLine;
781
- typewriterText = '';
782
- typewriterIndex = 0;
783
- }
784
- }
785
- })
786
- .catch(error => {
787
- typewriterTarget = '连接失败,正在重试...';
788
- typewriterText = '';
789
- typewriterIndex = 0;
790
- });
791
- }
792
-
793
- function moveStar(time) {
794
- const effectiveState = pendingDesiredState || currentState;
795
- const stateInfo = STATES[effectiveState] || STATES.idle;
796
- const baseTarget = areas[stateInfo.area] || areas.breakroom;
797
-
798
- const dx = targetX - star.x;
799
- const dy = targetY - star.y;
800
- const dist = Math.sqrt(dx * dx + dy * dy);
801
- const speed = 1.4;
802
- const wobble = Math.sin(time / 200) * 0.8;
803
-
804
- if (dist > 3) {
805
- star.x += (dx / dist) * speed;
806
- star.y += (dy / dist) * speed;
807
- star.setY(star.y + wobble);
808
- isMoving = true;
809
- } else {
810
- if (waypoints && waypoints.length > 0) {
811
- waypoints.shift();
812
- if (waypoints.length > 0) {
813
- targetX = waypoints[0].x;
814
- targetY = waypoints[0].y;
815
- isMoving = true;
816
- } else {
817
- if (pendingDesiredState !== null) {
818
- isMoving = false;
819
- currentState = pendingDesiredState;
820
- pendingDesiredState = null;
821
-
822
- if (currentState === 'idle') {
823
- star.setVisible(false);
824
- star.anims.stop();
825
- if (window.starWorking) {
826
- window.starWorking.setVisible(false);
827
- window.starWorking.anims.stop();
828
- }
829
- } else {
830
- star.setVisible(false);
831
- star.anims.stop();
832
- if (window.starWorking) {
833
- window.starWorking.setVisible(true);
834
- window.starWorking.anims.play('star_working', true);
835
- }
836
- }
837
- }
838
- }
839
- } else {
840
- if (pendingDesiredState !== null) {
841
- isMoving = false;
842
- currentState = pendingDesiredState;
843
- pendingDesiredState = null;
844
-
845
- if (currentState === 'idle') {
846
- star.setVisible(false);
847
- star.anims.stop();
848
- if (window.starWorking) {
849
- window.starWorking.setVisible(false);
850
- window.starWorking.anims.stop();
851
- }
852
- if (game.textures.exists('sofa_busy')) {
853
- sofa.setTexture('sofa_busy');
854
- sofa.anims.play('sofa_busy', true);
855
- }
856
- } else {
857
- star.setVisible(false);
858
- star.anims.stop();
859
- if (window.starWorking) {
860
- window.starWorking.setVisible(true);
861
- window.starWorking.anims.play('star_working', true);
862
- }
863
- sofa.anims.stop();
864
- sofa.setTexture('sofa_idle');
865
- }
866
- }
867
- }
868
- }
869
- }
870
-
871
- function showBubble() {
872
- if (bubble) { bubble.destroy(); bubble = null; }
873
- const texts = BUBBLE_TEXTS[currentState] || BUBBLE_TEXTS.idle;
874
- if (currentState === 'idle') return;
875
-
876
- let anchorX = star.x;
877
- let anchorY = star.y;
878
- if (currentState === 'syncing' && syncAnimSprite && syncAnimSprite.visible) {
879
- anchorX = syncAnimSprite.x;
880
- anchorY = syncAnimSprite.y;
881
- } else if (currentState === 'error' && window.errorBug && window.errorBug.visible) {
882
- anchorX = window.errorBug.x;
883
- anchorY = window.errorBug.y;
884
- } else if (!star.visible && window.starWorking && window.starWorking.visible) {
885
- anchorX = window.starWorking.x;
886
- anchorY = window.starWorking.y;
887
- }
888
-
889
- const text = texts[Math.floor(Math.random() * texts.length)];
890
- const bubbleY = anchorY - 70;
891
- const bg = game.add.rectangle(anchorX, bubbleY, text.length * 10 + 20, 28, 0xffffff, 0.95);
892
- bg.setStrokeStyle(2, 0x000000);
893
- const txt = game.add.text(anchorX, bubbleY, text, { fontFamily: 'ArkPixel, monospace', fontSize: '12px', fill: '#000', align: 'center' }).setOrigin(0.5);
894
- bubble = game.add.container(0, 0, [bg, txt]);
895
- bubble.setDepth(1200);
896
- setTimeout(() => { if (bubble) { bubble.destroy(); bubble = null; } }, 3000);
897
- }
898
-
899
- function showCatBubble() {
900
- if (!window.catSprite) return;
901
- if (window.catBubble) { window.catBubble.destroy(); window.catBubble = null; }
902
- const texts = BUBBLE_TEXTS.cat || ['喵~', '咕噜咕噜…'];
903
- const text = texts[Math.floor(Math.random() * texts.length)];
904
- const anchorX = window.catSprite.x;
905
- const anchorY = window.catSprite.y - 60;
906
- const bg = game.add.rectangle(anchorX, anchorY, text.length * 10 + 20, 24, 0xfffbeb, 0.95);
907
- bg.setStrokeStyle(2, 0xd4a574);
908
- const txt = game.add.text(anchorX, anchorY, text, { fontFamily: 'ArkPixel, monospace', fontSize: '11px', fill: '#8b6914', align: 'center' }).setOrigin(0.5);
909
- window.catBubble = game.add.container(0, 0, [bg, txt]);
910
- window.catBubble.setDepth(2100);
911
- setTimeout(() => { if (window.catBubble) { window.catBubble.destroy(); window.catBubble = null; } }, 4000);
912
- }
913
-
914
- function fetchAgents() {
915
- fetch('/agents?t=' + Date.now(), { cache: 'no-store' })
916
- .then(response => response.json())
917
- .then(data => {
918
- if (!Array.isArray(data)) return;
919
- // 重置位置计数器
920
- // 按区域分配不同位置索引,避免重叠
921
- const areaSlots = { breakroom: 0, writing: 0, error: 0 };
922
- for (let agent of data) {
923
- const area = agent.area || 'breakroom';
924
- agent._slotIndex = areaSlots[area] || 0;
925
- areaSlots[area] = (areaSlots[area] || 0) + 1;
926
- renderAgent(agent);
927
- }
928
- // 移除不再存在的 agent
929
- const currentIds = new Set(data.map(a => a.agentId));
930
- for (let id in agents) {
931
- if (!currentIds.has(id)) {
932
- if (agents[id]) {
933
- agents[id].destroy();
934
- delete agents[id];
935
- }
936
- }
937
- }
938
- })
939
- .catch(error => {
940
- console.error('拉取 agents 失败:', error);
941
- });
942
- }
943
-
944
- function getAreaPosition(area, slotIndex) {
945
- const positions = AREA_POSITIONS[area] || AREA_POSITIONS.breakroom;
946
- const idx = (slotIndex || 0) % positions.length;
947
- return positions[idx];
948
- }
949
-
950
- function renderAgent(agent) {
951
- const agentId = agent.agentId;
952
- const name = agent.name || 'Agent';
953
- const area = agent.area || 'breakroom';
954
- const authStatus = agent.authStatus || 'pending';
955
- const isMain = !!agent.isMain;
956
-
957
- // 获取这个 agent 在区域里的位置
958
- const pos = getAreaPosition(area, agent._slotIndex || 0);
959
- const baseX = pos.x;
960
- const baseY = pos.y;
961
-
962
- // 颜色
963
- const bodyColor = AGENT_COLORS[agentId] || AGENT_COLORS.default;
964
- const nameColor = NAME_TAG_COLORS[authStatus] || NAME_TAG_COLORS.default;
965
-
966
- // 透明度(离线/待批准/拒绝时变半透明)
967
- let alpha = 1;
968
- if (authStatus === 'pending') alpha = 0.7;
969
- if (authStatus === 'rejected') alpha = 0.4;
970
- if (authStatus === 'offline') alpha = 0.5;
971
-
972
- if (!agents[agentId]) {
973
- // 新建 agent
974
- const container = game.add.container(baseX, baseY);
975
- container.setDepth(1200 + (isMain ? 100 : 0)); // 放到最顶层!
976
-
977
- // 像素小人:用星星图标,更明显
978
- const starIcon = game.add.text(0, 0, '⭐', {
979
- fontFamily: 'ArkPixel, monospace',
980
- fontSize: '32px'
981
- }).setOrigin(0.5);
982
- starIcon.name = 'starIcon';
983
-
984
- // 名字标签(漂浮)
985
- const nameTag = game.add.text(0, -36, name, {
986
- fontFamily: 'ArkPixel, monospace',
987
- fontSize: '14px',
988
- fill: '#' + nameColor.toString(16).padStart(6, '0'),
989
- stroke: '#000',
990
- strokeThickness: 3,
991
- backgroundColor: 'rgba(255,255,255,0.95)'
992
- }).setOrigin(0.5);
993
- nameTag.name = 'nameTag';
994
-
995
- // 状态小点(绿色/黄色/红色)
996
- let dotColor = 0x64748b;
997
- if (authStatus === 'approved') dotColor = 0x22c55e;
998
- if (authStatus === 'pending') dotColor = 0xf59e0b;
999
- if (authStatus === 'rejected') dotColor = 0xef4444;
1000
- if (authStatus === 'offline') dotColor = 0x94a3b8;
1001
- const statusDot = game.add.circle(20, -20, 5, dotColor, alpha);
1002
- statusDot.setStrokeStyle(2, 0x000000, alpha);
1003
- statusDot.name = 'statusDot';
1004
-
1005
- container.add([starIcon, statusDot, nameTag]);
1006
- agents[agentId] = container;
1007
- } else {
1008
- // 更新 agent
1009
- const container = agents[agentId];
1010
- container.setPosition(baseX, baseY);
1011
- container.setAlpha(alpha);
1012
- container.setDepth(1200 + (isMain ? 100 : 0));
1013
-
1014
- // 更新名字和颜色(如果变化)
1015
- const nameTag = container.getAt(2);
1016
- if (nameTag && nameTag.name === 'nameTag') {
1017
- nameTag.setText(name);
1018
- nameTag.setFill('#' + (NAME_TAG_COLORS[authStatus] || NAME_TAG_COLORS.default).toString(16).padStart(6, '0'));
1019
- }
1020
- // 更新状态点颜色
1021
- const statusDot = container.getAt(1);
1022
- if (statusDot && statusDot.name === 'statusDot') {
1023
- let dotColor = 0x64748b;
1024
- if (authStatus === 'approved') dotColor = 0x22c55e;
1025
- if (authStatus === 'pending') dotColor = 0xf59e0b;
1026
- if (authStatus === 'rejected') dotColor = 0xef4444;
1027
- if (authStatus === 'offline') dotColor = 0x94a3b8;
1028
- statusDot.fillColor = dotColor;
1029
- }
1030
- }
1031
- }
1032
-
1033
- // 启动游戏
1034
- initGame();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/index.html DELETED
The diff for this file is too large to render. See raw diff
 
frontend/layout.js DELETED
@@ -1,133 +0,0 @@
1
- // Star Office UI - 布局与层级配置
2
- // 所有坐标、depth、资源路径统一管理在这里
3
- // 避免 magic numbers,降低改错风险
4
-
5
- // 核心规则:
6
- // - 透明资源(如办公桌)强制 .png,不透明优先 .webp
7
- // - 层级:低 → sofa(10) → starWorking(900) → desk(1000) → flower(1100)
8
-
9
- const LAYOUT = {
10
- // === 游戏画布 ===
11
- game: {
12
- width: 1280,
13
- height: 720
14
- },
15
-
16
- // === 各区域坐标 ===
17
- areas: {
18
- door: { x: 640, y: 550 },
19
- writing: { x: 320, y: 360 },
20
- researching: { x: 320, y: 360 },
21
- error: { x: 1066, y: 180 },
22
- breakroom: { x: 640, y: 360 }
23
- },
24
-
25
- // === 装饰与家具:坐标 + 原点 + depth ===
26
- furniture: {
27
- // 沙发
28
- sofa: {
29
- x: 670,
30
- y: 144,
31
- origin: { x: 0, y: 0 },
32
- depth: 10
33
- },
34
-
35
- // 新办公桌(透明 PNG 强制)
36
- desk: {
37
- x: 218,
38
- y: 417,
39
- origin: { x: 0.5, y: 0.5 },
40
- depth: 1000
41
- },
42
-
43
- // 桌上花盆
44
- flower: {
45
- x: 310,
46
- y: 390,
47
- origin: { x: 0.5, y: 0.5 },
48
- depth: 1100,
49
- scale: 0.8
50
- },
51
-
52
- // Star 在桌前工作(在 desk 下面)
53
- starWorking: {
54
- x: 217,
55
- y: 333,
56
- origin: { x: 0.5, y: 0.5 },
57
- depth: 900,
58
- scale: 1.32
59
- },
60
-
61
- // 植物们
62
- plants: [
63
- { x: 565, y: 178, depth: 5 },
64
- { x: 230, y: 185, depth: 5 },
65
- { x: 977, y: 496, depth: 5 }
66
- ],
67
-
68
- // 海报
69
- poster: {
70
- x: 252,
71
- y: 66,
72
- depth: 4
73
- },
74
-
75
- // 咖啡机
76
- coffeeMachine: {
77
- x: 659,
78
- y: 397,
79
- origin: { x: 0.5, y: 0.5 },
80
- depth: 99
81
- },
82
-
83
- // 服务器区
84
- serverroom: {
85
- x: 1021,
86
- y: 142,
87
- origin: { x: 0.5, y: 0.5 },
88
- depth: 2
89
- },
90
-
91
- // 错误 bug
92
- errorBug: {
93
- x: 1007,
94
- y: 221,
95
- origin: { x: 0.5, y: 0.5 },
96
- depth: 50,
97
- scale: 0.9,
98
- pingPong: { leftX: 1007, rightX: 1111, speed: 0.6 }
99
- },
100
-
101
- // 同步动画
102
- syncAnim: {
103
- x: 1157,
104
- y: 592,
105
- origin: { x: 0.5, y: 0.5 },
106
- depth: 40
107
- },
108
-
109
- // 小猫
110
- cat: {
111
- x: 94,
112
- y: 557,
113
- origin: { x: 0.5, y: 0.5 },
114
- depth: 2000
115
- }
116
- },
117
-
118
- // === 牌匾 ===
119
- plaque: {
120
- x: 640,
121
- y: 720 - 36,
122
- width: 420,
123
- height: 44
124
- },
125
-
126
- // === 资源加载规则:哪些强制用 PNG(透明资源) ===
127
- forcePng: {
128
- desk_v2: true // 新办公桌必须透明,强制 PNG
129
- },
130
-
131
- // === 总资源数量(用于加载进度条) ===
132
- totalAssets: 15
133
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
package.json CHANGED
@@ -1,7 +1,5 @@
1
  {
2
  "dependencies": {
3
- "ws": "^8.19.0",
4
- "express": "^4.21.0",
5
- "http-proxy-middleware": "^3.0.0"
6
  }
7
  }
 
1
  {
2
  "dependencies": {
3
+ "ws": "^8.19.0"
 
 
4
  }
5
  }
scripts/entrypoint.sh CHANGED
@@ -26,9 +26,8 @@ export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require /home/node/scripts
26
  # Enable Telegram API proxy (redirects fetch() to working mirror if needed)
27
  export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require /home/node/scripts/telegram-proxy.cjs"
28
 
29
- # token-redirect.cjs is no longer needed the Express proxy (proxy.cjs)
30
- # handles token injection for /admin and serves the animation at /
31
- # export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require /home/node/scripts/token-redirect.cjs"
32
 
33
  # ── Extensions symlink ──────────────────────────────────────────────────────
34
  SYMLINK_START=$(date +%s)
 
26
  # Enable Telegram API proxy (redirects fetch() to working mirror if needed)
27
  export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require /home/node/scripts/telegram-proxy.cjs"
28
 
29
+ # Auto-fill gateway token in Control UI (redirects "/" to "/?token=GATEWAY_TOKEN")
30
+ export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require /home/node/scripts/token-redirect.cjs"
 
31
 
32
  # ── Extensions symlink ──────────────────────────────────────────────────────
33
  SYMLINK_START=$(date +%s)
scripts/proxy.cjs DELETED
@@ -1,374 +0,0 @@
1
- /**
2
- * proxy.cjs — Express reverse proxy + state bridge for HuggingClaw
3
- *
4
- * Serves the Star-Office-UI animation at "/" and proxies
5
- * OpenClaw's Control UI at "/admin/*" (port 7861).
6
- * Also provides state bridge API and A2A pass-through.
7
- */
8
- 'use strict';
9
-
10
- const express = require('express');
11
- const { createProxyMiddleware } = require('http-proxy-middleware');
12
- const http = require('http');
13
- const fs = require('fs');
14
- const path = require('path');
15
-
16
- const LISTEN_PORT = 7860;
17
- const OPENCLAW_PORT = 7861;
18
- const OPENCLAW_TARGET = `http://localhost:${OPENCLAW_PORT}`;
19
- const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || 'huggingclaw';
20
- const ROLE = process.env.HUGGINGCLAW_ROLE || 'primary';
21
- const PEERS = (process.env.A2A_PEERS || '').split(',').filter(Boolean);
22
- const AGENT_NAME = process.env.AGENT_NAME || 'Star';
23
- const FRONTEND_DIR = path.resolve(__dirname, '..', 'frontend');
24
- const STATE_FILE = '/tmp/openclaw-sync-state.json';
25
- const LOG_FILE = path.join(process.env.HOME || '/home/node', '.openclaw', 'workspace', 'startup.log');
26
-
27
- // ── State Bridge ──────────────────────────────────────────────────────────
28
-
29
- let currentState = {
30
- state: 'syncing',
31
- detail: 'Starting up...',
32
- progress: 0,
33
- updated_at: new Date().toISOString()
34
- };
35
-
36
- let peerStates = {}; // url -> { state, detail, name, ... }
37
- let openclawReady = false;
38
-
39
- function readSyncState() {
40
- try {
41
- if (fs.existsSync(STATE_FILE)) {
42
- const data = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
43
- return data;
44
- }
45
- } catch (_) {}
46
- return null;
47
- }
48
-
49
- function checkOpenClawHealth() {
50
- return new Promise((resolve) => {
51
- const req = http.get(`${OPENCLAW_TARGET}/`, { timeout: 2000 }, (res) => {
52
- resolve(res.statusCode < 500);
53
- });
54
- req.on('error', () => resolve(false));
55
- req.on('timeout', () => { req.destroy(); resolve(false); });
56
- });
57
- }
58
-
59
- async function updateState() {
60
- const syncState = readSyncState();
61
-
62
- if (syncState && syncState.phase === 'syncing') {
63
- currentState = {
64
- state: 'syncing',
65
- detail: syncState.detail || 'Syncing data...',
66
- progress: syncState.progress || 0,
67
- updated_at: new Date().toISOString()
68
- };
69
- return;
70
- }
71
-
72
- const healthy = await checkOpenClawHealth();
73
- if (!healthy) {
74
- if (!openclawReady) {
75
- currentState = {
76
- state: 'syncing',
77
- detail: 'OpenClaw starting up...',
78
- progress: 0,
79
- updated_at: new Date().toISOString()
80
- };
81
- } else {
82
- currentState = {
83
- state: 'error',
84
- detail: 'OpenClaw not responding',
85
- progress: 0,
86
- updated_at: new Date().toISOString()
87
- };
88
- }
89
- return;
90
- }
91
-
92
- openclawReady = true;
93
-
94
- // Try to detect activity from log file
95
- try {
96
- if (fs.existsSync(LOG_FILE)) {
97
- const stat = fs.statSync(LOG_FILE);
98
- const readSize = Math.min(stat.size, 2048);
99
- const fd = fs.openSync(LOG_FILE, 'r');
100
- const buf = Buffer.alloc(readSize);
101
- fs.readSync(fd, buf, 0, readSize, Math.max(0, stat.size - readSize));
102
- fs.closeSync(fd);
103
- const tail = buf.toString('utf-8');
104
- const lines = tail.split('\n').filter(Boolean);
105
- const lastLine = lines[lines.length - 1] || '';
106
- const secondAgo = Date.now() - 5000;
107
-
108
- // Detect LLM call
109
- if (lastLine.includes('completion') || lastLine.includes('LLM') || lastLine.includes('model')) {
110
- currentState = { state: 'writing', detail: 'Calling LLM...', progress: 0, updated_at: new Date().toISOString() };
111
- return;
112
- }
113
- // Detect agent execution
114
- if (lastLine.includes('agent') || lastLine.includes('executing') || lastLine.includes('running')) {
115
- currentState = { state: 'executing', detail: 'Agent processing...', progress: 0, updated_at: new Date().toISOString() };
116
- return;
117
- }
118
- }
119
- } catch (_) {}
120
-
121
- // Default: idle
122
- if (syncState && syncState.phase === 'idle') {
123
- currentState = {
124
- state: 'idle',
125
- detail: 'Ready',
126
- progress: 0,
127
- updated_at: new Date().toISOString()
128
- };
129
- } else if (currentState.state === 'syncing') {
130
- // Transition from syncing to idle once healthy
131
- currentState = {
132
- state: 'idle',
133
- detail: 'Ready',
134
- progress: 0,
135
- updated_at: new Date().toISOString()
136
- };
137
- }
138
- }
139
-
140
- // Poll state every 2 seconds
141
- setInterval(updateState, 2000);
142
- updateState();
143
-
144
- // ── Peer State Polling (primary only) ─────────────────────────────────────
145
-
146
- async function fetchPeerStates() {
147
- if (ROLE !== 'primary' || PEERS.length === 0) return;
148
-
149
- for (const peerUrl of PEERS) {
150
- try {
151
- const url = `${peerUrl.replace(/\/$/, '')}/api/state`;
152
- const controller = new AbortController();
153
- const timeout = setTimeout(() => controller.abort(), 5000);
154
- const res = await fetch(url, { signal: controller.signal, cache: 'no-store' });
155
- clearTimeout(timeout);
156
- if (res.ok) {
157
- const data = await res.json();
158
- peerStates[peerUrl] = {
159
- ...data,
160
- online: true,
161
- peerUrl
162
- };
163
- } else {
164
- peerStates[peerUrl] = { state: 'idle', detail: 'Unreachable', online: false, peerUrl };
165
- }
166
- } catch (_) {
167
- peerStates[peerUrl] = { state: 'idle', detail: 'Offline', online: false, peerUrl };
168
- }
169
- }
170
- }
171
-
172
- if (ROLE === 'primary' && PEERS.length > 0) {
173
- setInterval(fetchPeerStates, 5000);
174
- fetchPeerStates();
175
- }
176
-
177
- // ── Express App ───────────────────────────────────────────────────────────
178
-
179
- const app = express();
180
- app.use(express.json());
181
-
182
- // --- API routes (before proxy) ---
183
-
184
- // State endpoint
185
- app.get('/api/state', (req, res) => {
186
- res.json(currentState);
187
- });
188
-
189
- // Compat: Star-Office-UI uses /status
190
- app.get('/status', (req, res) => {
191
- res.json(currentState);
192
- });
193
-
194
- // Agents/guests endpoint (for Star-Office-UI multi-agent rendering)
195
- app.get('/agents', (req, res) => {
196
- const agents = [];
197
- let slotIndex = 0;
198
-
199
- for (const [peerUrl, peerState] of Object.entries(peerStates)) {
200
- // Derive agent name from peer URL
201
- let name = 'Agent';
202
- if (peerUrl.includes('adam')) name = 'Adam';
203
- else if (peerUrl.includes('eve')) name = 'Eve';
204
-
205
- const stateMap = {
206
- idle: 'breakroom',
207
- writing: 'writing',
208
- researching: 'writing',
209
- executing: 'writing',
210
- syncing: 'writing',
211
- error: 'error'
212
- };
213
-
214
- agents.push({
215
- agentId: name.toLowerCase(),
216
- name: name,
217
- area: stateMap[peerState.state] || 'breakroom',
218
- authStatus: peerState.online ? 'approved' : 'offline',
219
- detail: peerState.detail || '',
220
- state: peerState.state || 'idle',
221
- _slotIndex: slotIndex++
222
- });
223
- }
224
- res.json(agents);
225
- });
226
-
227
- app.get('/api/guests', (req, res) => {
228
- // Alias for /agents
229
- res.redirect('/agents');
230
- });
231
-
232
- // Stub endpoints that Star-Office-UI tries to call
233
- app.get('/yesterday-memo', (req, res) => {
234
- res.json({ success: false, memo: null });
235
- });
236
-
237
- app.post('/set_state', (req, res) => {
238
- const { state, detail } = req.body || {};
239
- if (state) {
240
- currentState = {
241
- state,
242
- detail: detail || '',
243
- progress: 0,
244
- updated_at: new Date().toISOString()
245
- };
246
- // Also write to state file so it persists
247
- try {
248
- fs.writeFileSync(STATE_FILE, JSON.stringify({ phase: state, detail }));
249
- } catch (_) {}
250
- }
251
- res.json({ ok: true });
252
- });
253
-
254
- // Asset-related stubs (drawer features we don't use)
255
- app.get('/assets/auth/status', (req, res) => {
256
- res.json({ authenticated: false });
257
- });
258
- app.get('/assets', (req, res) => {
259
- res.json([]);
260
- });
261
-
262
- // --- Static files ---
263
- app.use('/frontend', express.static(FRONTEND_DIR, {
264
- maxAge: '1h',
265
- setHeaders: (res, filePath) => {
266
- if (filePath.endsWith('.webp') || filePath.endsWith('.png')) {
267
- res.setHeader('Cache-Control', 'public, max-age=86400');
268
- }
269
- }
270
- }));
271
-
272
- // Serve /static as alias for /frontend (Star-Office-UI references /static/)
273
- app.use('/static', express.static(FRONTEND_DIR, {
274
- maxAge: '1h',
275
- setHeaders: (res, filePath) => {
276
- if (filePath.endsWith('.webp') || filePath.endsWith('.png')) {
277
- res.setHeader('Cache-Control', 'public, max-age=86400');
278
- }
279
- }
280
- }));
281
-
282
- // --- Animation homepage ---
283
- app.get('/', (req, res) => {
284
- const indexPath = path.join(FRONTEND_DIR, 'index.html');
285
- if (fs.existsSync(indexPath)) {
286
- let html = fs.readFileSync(indexPath, 'utf-8');
287
- // Replace version timestamps
288
- html = html.replace(/\{\{VERSION_TIMESTAMP\}\}/g, Date.now().toString());
289
- res.type('html').send(html);
290
- } else {
291
- res.status(503).send('Frontend not ready. Please wait...');
292
- }
293
- });
294
-
295
- // --- Admin panel (reverse proxy to OpenClaw) ---
296
-
297
- // Redirect /admin to /admin/ for consistent path handling
298
- app.get('/admin', (req, res) => {
299
- const tokenParam = req.query.token ? `?token=${req.query.token}` : `?token=${GATEWAY_TOKEN}`;
300
- res.redirect(`/admin/${tokenParam}`);
301
- });
302
-
303
- // Create the proxy middleware
304
- const openclawProxy = createProxyMiddleware({
305
- target: OPENCLAW_TARGET,
306
- changeOrigin: true,
307
- pathRewrite: { '^/admin': '' },
308
- ws: true,
309
- on: {
310
- proxyRes: (proxyRes, req, res) => {
311
- // Inject <base href="/admin/"> for HTML responses
312
- const contentType = proxyRes.headers['content-type'] || '';
313
- if (contentType.includes('text/html')) {
314
- const origWrite = res.write;
315
- const origEnd = res.end;
316
- let body = '';
317
-
318
- res.write = function(chunk) {
319
- body += chunk.toString();
320
- return true;
321
- };
322
-
323
- res.end = function(chunk) {
324
- if (chunk) body += chunk.toString();
325
- // Inject base tag after <head>
326
- body = body.replace(/<head([^>]*)>/i, '<head$1><base href="/admin/">');
327
- // Remove content-length since we modified the body
328
- delete proxyRes.headers['content-length'];
329
- res.setHeader('content-length', Buffer.byteLength(body));
330
- origWrite.call(res, body);
331
- origEnd.call(res);
332
- };
333
- }
334
- }
335
- }
336
- });
337
-
338
- app.use('/admin', openclawProxy);
339
-
340
- // --- A2A pass-through ---
341
- app.use('/a2a', createProxyMiddleware({
342
- target: OPENCLAW_TARGET,
343
- changeOrigin: true,
344
- ws: true
345
- }));
346
-
347
- app.use('/.well-known', createProxyMiddleware({
348
- target: OPENCLAW_TARGET,
349
- changeOrigin: true
350
- }));
351
-
352
- // ── Start server ──────────────────────────────────────────────────────────
353
-
354
- const server = http.createServer(app);
355
-
356
- // Handle WebSocket upgrades for /admin
357
- server.on('upgrade', (req, socket, head) => {
358
- if (req.url.startsWith('/admin')) {
359
- openclawProxy.upgrade(req, socket, head);
360
- } else if (req.url.startsWith('/a2a')) {
361
- // A2A WebSocket if needed
362
- }
363
- });
364
-
365
- server.listen(LISTEN_PORT, '0.0.0.0', () => {
366
- console.log(`[proxy] HuggingClaw proxy listening on port ${LISTEN_PORT}`);
367
- console.log(`[proxy] Role: ${ROLE}`);
368
- console.log(`[proxy] Animation: http://localhost:${LISTEN_PORT}/`);
369
- console.log(`[proxy] Admin: http://localhost:${LISTEN_PORT}/admin`);
370
- console.log(`[proxy] OpenClaw backend: ${OPENCLAW_TARGET}`);
371
- if (PEERS.length > 0) {
372
- console.log(`[proxy] A2A peers: ${PEERS.join(', ')}`);
373
- }
374
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/sync_hf.py CHANGED
@@ -286,11 +286,6 @@ class OpenClawFullSync:
286
  return
287
 
288
  print(f"[SYNC] ▶ Uploading ~/.openclaw → dataset {HF_REPO_ID}/{DATASET_PATH}/ ...")
289
- # Notify state bridge
290
- try:
291
- Path("/tmp/openclaw-sync-state.json").write_text(json.dumps({"phase": "syncing", "detail": "Uploading data..."}))
292
- except Exception:
293
- pass
294
 
295
  try:
296
  # Log what will be uploaded
@@ -327,11 +322,6 @@ class OpenClawFullSync:
327
  ],
328
  )
329
  print(f"[SYNC] ✓ Upload completed at {datetime.now().isoformat()}")
330
- # Notify state bridge
331
- try:
332
- Path("/tmp/openclaw-sync-state.json").write_text(json.dumps({"phase": "idle", "detail": "Ready"}))
333
- except Exception:
334
- pass
335
 
336
  # Verify
337
  try:
@@ -348,11 +338,6 @@ class OpenClawFullSync:
348
  except Exception as e:
349
  print(f"[SYNC] ✗ Upload failed: {e}")
350
  traceback.print_exc()
351
- # Notify state bridge of error
352
- try:
353
- Path("/tmp/openclaw-sync-state.json").write_text(json.dumps({"phase": "idle", "detail": "Sync failed"}))
354
- except Exception:
355
- pass
356
 
357
  # ── Config helpers ─────────────────────────────────────────────────
358
 
@@ -393,7 +378,7 @@ class OpenClawFullSync:
393
  with open(config_path, "w") as f:
394
  json.dump({
395
  "gateway": {
396
- "mode": "local", "bind": "lan", "port": 7861,
397
  "trustedProxies": ["0.0.0.0/0"],
398
  "controlUi": {
399
  "allowInsecureAuth": True,
@@ -451,7 +436,7 @@ class OpenClawFullSync:
451
  data["gateway"] = {
452
  "mode": "local",
453
  "bind": "lan",
454
- "port": 7861,
455
  "auth": {"token": GATEWAY_TOKEN},
456
  "trustedProxies": ["0.0.0.0/0"],
457
  "controlUi": {
@@ -505,34 +490,9 @@ class OpenClawFullSync:
505
  data.setdefault("models", {})["providers"] = providers
506
  data["agents"]["defaults"]["model"]["primary"] = OPENCLAW_DEFAULT_MODEL
507
 
508
- # Plugin whitelist
509
  data.setdefault("plugins", {}).setdefault("entries", {})
510
- plugin_allow = ["telegram", "whatsapp"]
511
-
512
- # ── A2A Gateway Plugin ──────────────────────────────────────────
513
- a2a_peers_env = os.environ.get("A2A_PEERS", "")
514
- if a2a_peers_env:
515
- plugin_allow.append("a2a-gateway")
516
- peers_list = [
517
- {"url": f"{url.rstrip('/')}/.well-known/agent.json"}
518
- for url in a2a_peers_env.split(",") if url.strip()
519
- ]
520
- a2a_token = os.environ.get("A2A_PEER_TOKEN", "")
521
- a2a_config = {
522
- "enabled": True,
523
- "config": {
524
- "server": {"port": 7861},
525
- "peers": peers_list,
526
- }
527
- }
528
- if a2a_token:
529
- a2a_config["config"]["security"] = {"inbound": {"token": a2a_token}}
530
- for p in peers_list:
531
- p["auth"] = {"type": "bearer", "token": a2a_token}
532
- data["plugins"]["entries"]["a2a-gateway"] = a2a_config
533
- print(f"[SYNC] Set A2A gateway plugin with {len(peers_list)} peer(s)")
534
-
535
- data["plugins"]["allow"] = plugin_allow
536
  if "telegram" not in data["plugins"]["entries"]:
537
  data["plugins"]["entries"]["telegram"] = {"enabled": True}
538
  elif isinstance(data["plugins"]["entries"]["telegram"], dict):
@@ -687,42 +647,6 @@ class OpenClawFullSync:
687
 
688
  # ── Main ────────────────────────────────────────────────────────────────────
689
 
690
- def start_proxy():
691
- """Start the Express proxy server (port 7860) before OpenClaw.
692
- This ensures HF Spaces sees the container as ready immediately."""
693
- proxy_script = Path(__file__).parent / "proxy.cjs"
694
- if not proxy_script.exists():
695
- print("[PROXY] WARNING: proxy.cjs not found, skipping proxy startup")
696
- return None
697
-
698
- print("[PROXY] Starting Express proxy on port 7860...")
699
- env = os.environ.copy()
700
- try:
701
- proxy_proc = subprocess.Popen(
702
- ["node", str(proxy_script)],
703
- env=env,
704
- stdout=subprocess.PIPE,
705
- stderr=subprocess.STDOUT,
706
- text=True,
707
- bufsize=1
708
- )
709
-
710
- # Copy proxy output in background
711
- def copy_proxy_output():
712
- try:
713
- for line in proxy_proc.stdout:
714
- print(f"[PROXY] {line}", end='')
715
- except Exception:
716
- pass
717
-
718
- threading.Thread(target=copy_proxy_output, daemon=True).start()
719
- print(f"[PROXY] Proxy started with PID: {proxy_proc.pid}")
720
- return proxy_proc
721
- except Exception as e:
722
- print(f"[PROXY] ERROR: Failed to start proxy: {e}")
723
- return None
724
-
725
-
726
  def main():
727
  try:
728
  t_main_start = time.time()
@@ -731,9 +655,6 @@ def main():
731
  sync = OpenClawFullSync()
732
  print(f"[TIMER] sync_hf init: {time.time() - t0:.1f}s")
733
 
734
- # 0. Start proxy first (so HF Spaces sees port 7860 ready)
735
- proxy_process = start_proxy()
736
-
737
  # 1. Restore
738
  t0 = time.time()
739
  sync.load_from_repo()
@@ -762,12 +683,6 @@ def main():
762
  process.wait(timeout=5)
763
  except subprocess.TimeoutExpired:
764
  process.kill()
765
- if proxy_process:
766
- proxy_process.terminate()
767
- try:
768
- proxy_process.wait(timeout=3)
769
- except subprocess.TimeoutExpired:
770
- proxy_process.kill()
771
  print("[SYNC] Final sync...")
772
  sync.save_to_repo()
773
  sys.exit(0)
@@ -780,16 +695,12 @@ def main():
780
  print("[SYNC] ERROR: Failed to start OpenClaw process. Exiting.")
781
  stop_event.set()
782
  t.join(timeout=5)
783
- if proxy_process:
784
- proxy_process.terminate()
785
  sys.exit(1)
786
 
787
  exit_code = process.wait()
788
  print(f"[SYNC] OpenClaw exited with code {exit_code}")
789
  stop_event.set()
790
  t.join(timeout=10)
791
- if proxy_process:
792
- proxy_process.terminate()
793
  print("[SYNC] Final sync...")
794
  sync.save_to_repo()
795
  sys.exit(exit_code)
 
286
  return
287
 
288
  print(f"[SYNC] ▶ Uploading ~/.openclaw → dataset {HF_REPO_ID}/{DATASET_PATH}/ ...")
 
 
 
 
 
289
 
290
  try:
291
  # Log what will be uploaded
 
322
  ],
323
  )
324
  print(f"[SYNC] ✓ Upload completed at {datetime.now().isoformat()}")
 
 
 
 
 
325
 
326
  # Verify
327
  try:
 
338
  except Exception as e:
339
  print(f"[SYNC] ✗ Upload failed: {e}")
340
  traceback.print_exc()
 
 
 
 
 
341
 
342
  # ── Config helpers ─────────────────────────────────────────────────
343
 
 
378
  with open(config_path, "w") as f:
379
  json.dump({
380
  "gateway": {
381
+ "mode": "local", "bind": "lan", "port": 7860,
382
  "trustedProxies": ["0.0.0.0/0"],
383
  "controlUi": {
384
  "allowInsecureAuth": True,
 
436
  data["gateway"] = {
437
  "mode": "local",
438
  "bind": "lan",
439
+ "port": 7860,
440
  "auth": {"token": GATEWAY_TOKEN},
441
  "trustedProxies": ["0.0.0.0/0"],
442
  "controlUi": {
 
490
  data.setdefault("models", {})["providers"] = providers
491
  data["agents"]["defaults"]["model"]["primary"] = OPENCLAW_DEFAULT_MODEL
492
 
493
+ # Plugin whitelist (only load telegram + whatsapp to speed up startup)
494
  data.setdefault("plugins", {}).setdefault("entries", {})
495
+ data["plugins"]["allow"] = ["telegram", "whatsapp"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
496
  if "telegram" not in data["plugins"]["entries"]:
497
  data["plugins"]["entries"]["telegram"] = {"enabled": True}
498
  elif isinstance(data["plugins"]["entries"]["telegram"], dict):
 
647
 
648
  # ── Main ────────────────────────────────────────────────────────────────────
649
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
650
  def main():
651
  try:
652
  t_main_start = time.time()
 
655
  sync = OpenClawFullSync()
656
  print(f"[TIMER] sync_hf init: {time.time() - t0:.1f}s")
657
 
 
 
 
658
  # 1. Restore
659
  t0 = time.time()
660
  sync.load_from_repo()
 
683
  process.wait(timeout=5)
684
  except subprocess.TimeoutExpired:
685
  process.kill()
 
 
 
 
 
 
686
  print("[SYNC] Final sync...")
687
  sync.save_to_repo()
688
  sys.exit(0)
 
695
  print("[SYNC] ERROR: Failed to start OpenClaw process. Exiting.")
696
  stop_event.set()
697
  t.join(timeout=5)
 
 
698
  sys.exit(1)
699
 
700
  exit_code = process.wait()
701
  print(f"[SYNC] OpenClaw exited with code {exit_code}")
702
  stop_event.set()
703
  t.join(timeout=10)
 
 
704
  print("[SYNC] Final sync...")
705
  sync.save_to_repo()
706
  sys.exit(exit_code)