Spaces:
Running
Running
Revert "feat: add pixel animation homepage with Express proxy layer"
Browse filesThis reverts commit 6cf74294015c25e76d77e66dfb929c3fb8ed5a2d.
- Dockerfile +4 -26
- frontend/.gitignore +0 -15
- frontend/game.js +0 -1034
- frontend/index.html +0 -0
- frontend/layout.js +0 -133
- package.json +1 -3
- scripts/entrypoint.sh +2 -3
- scripts/proxy.cjs +0 -374
- scripts/sync_hf.py +4 -93
Dockerfile
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
# OpenClaw on Hugging Face Spaces —
|
| 2 |
-
#
|
| 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
|
| 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 |
-
|
| 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 |
-
#
|
| 30 |
-
|
| 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":
|
| 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":
|
| 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 |
-
|
| 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)
|