chore: save local workspace changes

This commit is contained in:
xmanrui
2026-03-12 00:28:37 +08:00
parent 9c14282cba
commit 4d89a0a4e0
12 changed files with 1063 additions and 136 deletions
+179 -136
View File
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
import { execFileSync } from "child_process";
import { OPENCLAW_CONFIG_PATH, OPENCLAW_HOME } from "@/lib/openclaw-paths";
const CONFIG_PATH = OPENCLAW_CONFIG_PATH;
const QQBOT_TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken";
@@ -16,6 +17,56 @@ interface PlatformTestResult {
elapsed: number;
}
function runOpenClawMessageSend(channel: string, target: string, message: string, extraArgs: string[] = []): string {
const args = [
"message", "send",
"--channel", channel,
"-t", target,
"--message", message,
"--json",
...extraArgs,
];
return execFileSync("openclaw", args, {
timeout: 30000,
encoding: "utf-8",
env: { ...process.env },
});
}
function runCurlJson(url: string, options: { method?: string; headers?: string[]; body?: string; timeoutSec?: number } = {}): { status: number; data: any; raw: string } {
const args = [
'-sS',
'--connect-timeout', String(options.timeoutSec ?? 10),
'--max-time', String(options.timeoutSec ?? 20),
'-X', options.method || 'GET',
];
for (const header of options.headers || []) {
args.push('-H', header);
}
if (typeof options.body === 'string') {
args.push('--data-raw', options.body);
}
args.push('-w', '\\n%{http_code}', url);
const raw = execFileSync('curl', args, {
timeout: (options.timeoutSec ?? 20) * 1000 + 1000,
encoding: 'utf-8',
env: { ...process.env },
});
const cut = raw.lastIndexOf('\n');
const body = cut >= 0 ? raw.slice(0, cut) : raw;
const status = Number(cut >= 0 ? raw.slice(cut + 1).trim() : 0);
let data: any = null;
try {
data = body ? JSON.parse(body) : null;
} catch {
data = null;
}
return { status, data, raw: body };
}
// Find the most recent feishu DM user open_id for a given agent
// Each feishu app has its own open_id namespace, so we must use per-agent open_ids
function getFeishuDmUser(agentId: string): string | null {
@@ -146,101 +197,128 @@ async function testFeishu(
}
}
// Discord: call /users/@me then send a DM to test user
// Discord: use curl so the host proxy settings are honored consistently
async function testDiscord(
agentId: string,
botToken: string,
testUserId: string | null
testUserId: string | null,
recipientSource: "session" | "allowFrom" | "none"
): Promise<PlatformTestResult> {
const startTime = Date.now();
try {
const meResp = await fetch("https://discord.com/api/v10/users/@me", {
method: "GET",
headers: { Authorization: `Bot ${botToken}` },
signal: AbortSignal.timeout(15000),
const meResp = runCurlJson('https://discord.com/api/v10/users/@me', {
headers: [`Authorization: Bot ${botToken}`],
timeoutSec: 15,
});
const meData = await meResp.json();
if (!meResp.ok || !meData.id) {
const meData = meResp.data;
if (meResp.status < 200 || meResp.status >= 300 || !meData?.id) {
return {
agentId, platform: "discord", ok: false,
error: `Discord API error: ${meData.message || JSON.stringify(meData)}`,
agentId, platform: 'discord', ok: false,
error: `Discord API error: ${meData?.message || meResp.raw || `HTTP ${meResp.status}`}`,
elapsed: Date.now() - startTime,
};
}
const botName = `${meData.username}#${meData.discriminator || "0"}`;
const botName = `${meData.username}#${meData.discriminator || '0'}`;
if (!testUserId) {
return {
agentId, platform: "discord", ok: true,
agentId, platform: 'discord', ok: true,
detail: `${botName} (bot reachable, no test user for DM)`,
elapsed: Date.now() - startTime,
};
}
// Create DM channel
const dmChanResp = await fetch("https://discord.com/api/v10/users/@me/channels", {
method: "POST",
headers: {
Authorization: `Bot ${botToken}`,
"Content-Type": "application/json",
},
const dmChanResp = runCurlJson('https://discord.com/api/v10/users/@me/channels', {
method: 'POST',
headers: [
`Authorization: Bot ${botToken}`,
'Content-Type: application/json',
],
body: JSON.stringify({ recipient_id: testUserId }),
signal: AbortSignal.timeout(15000),
timeoutSec: 15,
});
const dmChan = await dmChanResp.json();
if (!dmChanResp.ok || !dmChan.id) {
const dmChan = dmChanResp.data;
if (dmChanResp.status < 200 || dmChanResp.status >= 300 || !dmChan?.id) {
return {
agentId, platform: "discord", ok: false,
error: `Create DM channel failed: ${dmChan.message || JSON.stringify(dmChan)}`,
agentId, platform: 'discord', ok: false,
error: `Create DM channel failed: ${dmChan?.message || dmChanResp.raw || `HTTP ${dmChanResp.status}`}`,
elapsed: Date.now() - startTime,
};
}
const now = new Date().toLocaleTimeString("zh-CN", { timeZone: "Asia/Shanghai" });
const msgResp = await fetch(
`https://discord.com/api/v10/channels/${dmChan.id}/messages`,
{
method: "POST",
headers: {
Authorization: `Bot ${botToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
content: `[Platform Test] ${botName} connectivity test ✅ (${now})`,
}),
signal: AbortSignal.timeout(15000),
}
);
const msgData = await msgResp.json();
const now = new Date().toLocaleTimeString('zh-CN', { timeZone: 'Asia/Shanghai' });
const msgResp = runCurlJson(`https://discord.com/api/v10/channels/${dmChan.id}/messages`, {
method: 'POST',
headers: [
`Authorization: Bot ${botToken}`,
'Content-Type: application/json',
],
body: JSON.stringify({
content: `[Platform Test] ${botName} connectivity test ✅ (${now})`,
flags: 4096,
}),
timeoutSec: 15,
});
const msgData = msgResp.data;
const elapsed = Date.now() - startTime;
if (msgResp.ok && msgData.id) {
if (msgResp.status >= 200 && msgResp.status < 300 && msgData?.id) {
const sourceLabel = recipientSource === 'allowFrom' ? 'allowFrom' : 'session';
return {
agentId, platform: "discord", ok: true,
detail: `${botName} → DM sent (${elapsed}ms)`,
elapsed,
};
} else {
return {
agentId, platform: "discord", ok: false,
error: `Send DM failed: ${msgData.message || JSON.stringify(msgData)}`,
agentId, platform: 'discord', ok: true,
detail: `${botName} → DM sent (${elapsed}ms, via ${sourceLabel})`,
elapsed,
};
}
return {
agentId, platform: 'discord', ok: false,
error: `Send DM failed: ${msgData?.message || msgResp.raw || `HTTP ${msgResp.status}`}`,
elapsed,
};
} catch (err: any) {
return {
agentId, platform: "discord", ok: false,
error: err.message,
agentId, platform: 'discord', ok: false,
error: err.stderr || err.message || 'Unknown error',
elapsed: Date.now() - startTime,
};
}
}
function getDiscordDmUser(agentId: string): string | null {
try {
const sessionsPath = path.join(OPENCLAW_HOME, `agents/${agentId}/sessions/sessions.json`);
const raw = fs.readFileSync(sessionsPath, "utf-8");
const sessions = JSON.parse(raw);
let bestId: string | null = null;
let bestTime = 0;
for (const [key, val] of Object.entries(sessions)) {
const m = key.match(/^agent:[^:]+:discord:direct:(.+)$/);
if (m) {
const updatedAt = (val as any).updatedAt || 0;
if (updatedAt > bestTime) {
bestTime = updatedAt;
bestId = m[1];
}
}
}
return bestId;
} catch {
return null;
}
}
function getDiscordAllowlistUser(discordConfig: any): string | null {
const list = Array.isArray(discordConfig?.allowFrom)
? discordConfig.allowFrom
: Array.isArray(discordConfig?.dm?.allowFrom)
? discordConfig.dm.allowFrom
: [];
const first = list.find((v: any) => typeof v === "string" && v.trim().length > 0);
return first ? first.trim() : null;
}
// Find the most recent telegram DM chat_id for a given agent
function getTelegramDmUser(agentId: string): string | null {
try {
@@ -265,72 +343,40 @@ function getTelegramDmUser(agentId: string): string | null {
}
}
// Telegram: call /getMe to verify bot, then send test DM
// Telegram: send a real DM through local OpenClaw channel gateway
async function testTelegram(
agentId: string,
botToken: string,
testChatId: string | null
): Promise<PlatformTestResult> {
const startTime = Date.now();
if (!testChatId) {
return {
agentId, platform: "telegram", ok: false,
error: "No Telegram recipient configured. Start one DM session first",
elapsed: Date.now() - startTime,
};
}
try {
const meResp = await fetch(`https://api.telegram.org/bot${botToken}/getMe`, {
method: "GET",
signal: AbortSignal.timeout(15000),
});
const meData = await meResp.json();
if (!meResp.ok || !meData.ok || !meData.result) {
return {
agentId, platform: "telegram", ok: false,
error: `Telegram API error: ${meData.description || JSON.stringify(meData)}`,
elapsed: Date.now() - startTime,
};
}
const botName = meData.result.username ? `@${meData.result.username}` : meData.result.first_name;
if (!testChatId) {
return {
agentId, platform: "telegram", ok: true,
detail: `${botName} (bot reachable, no DM session found)`,
elapsed: Date.now() - startTime,
};
}
// Send test message
const now = new Date().toLocaleTimeString("zh-CN", { timeZone: "Asia/Shanghai" });
const msgResp = await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat_id: testChatId,
text: `[Platform Test] ${botName} 联通测试 ✅ (${now})`,
}),
signal: AbortSignal.timeout(15000),
});
const msgData = await msgResp.json();
const result = runOpenClawMessageSend(
"telegram",
testChatId,
`[Platform Test] Telegram 联通测试 ✅ (${now})`,
["--silent"]
);
const elapsed = Date.now() - startTime;
if (msgData.ok) {
return {
agentId, platform: "telegram", ok: true,
detail: `${botName} → DM sent (${elapsed}ms)`,
elapsed,
};
} else {
return {
agentId, platform: "telegram", ok: false,
error: `Send DM failed: ${msgData.description || JSON.stringify(msgData)}`,
elapsed,
};
}
const outputSummary = result.trim().slice(0, 120);
return {
agentId, platform: "telegram", ok: true,
detail: `Telegram → DM sent to ${testChatId} (${elapsed}ms)${outputSummary ? ` · ${outputSummary}` : ""}`,
elapsed,
};
} catch (err: any) {
return {
agentId, platform: "telegram", ok: false,
error: err.message,
error: (err.stderr || err.message || "Unknown error").slice(0, 300),
elapsed: Date.now() - startTime,
};
}
@@ -513,23 +559,12 @@ async function testWhatsapp(
}
try {
// WhatsApp has no public Bot API. Use `openclaw message send` CLI
// to send a real message via the gateway's WhatsApp Web connection.
const now = new Date().toLocaleTimeString("zh-CN", { timeZone: "Asia/Shanghai" });
const { execFileSync } = await import("child_process");
const args = [
"message", "send",
"--channel", "whatsapp",
"-t", testUserId,
"--message", `[Platform Test] WhatsApp 联通测试 ✅ (${now})`,
];
const result = execFileSync("openclaw", args, {
timeout: 30000,
encoding: "utf-8",
env: { ...process.env },
});
const result = runOpenClawMessageSend(
"whatsapp",
testUserId,
`[Platform Test] WhatsApp 联通测试 ✅ (${now})`
);
const elapsed = Date.now() - startTime;
const sourceLabel = recipientSource === "allowFrom" ? "allowFrom" : "session";
@@ -651,8 +686,6 @@ export async function POST() {
const feishuAccounts = feishuConfig.accounts || {};
const feishuDomain = feishuConfig.domain || "feishu";
const discordConfig = channels.discord || {};
const discordAllowFrom: string[] = discordConfig.dm?.allowFrom || [];
const discordTestUser = discordAllowFrom[0] || null;
const telegramConfig = channels.telegram || {};
const whatsappConfig = channels.whatsapp || {};
const qqbotConfig = channels.qqbot;
@@ -675,8 +708,10 @@ export async function POST() {
}
}
// Phase 1: Platform API tests (parallel)
// Phase 1: Feishu API tests can run in parallel.
// Local gateway / CLI-backed channel tests are run sequentially to avoid send-path contention.
const platformTests: Promise<PlatformTestResult>[] = [];
const sequentialPlatformTests: Array<() => Promise<PlatformTestResult>> = [];
const testedFeishuAccounts = new Set<string>();
for (const agent of agentList) {
@@ -701,15 +736,20 @@ export async function POST() {
}
}
// Discord: only test once
if (id === "main" && discordConfig.enabled && discordConfig.token) {
platformTests.push(testDiscord(id, discordConfig.token, discordTestUser));
// Discord: only test once, via local OpenClaw channel gateway
if (id === "main" && discordConfig.enabled) {
const recentDmUser = getDiscordDmUser(id);
const allowFromUser = getDiscordAllowlistUser(discordConfig);
const discordTestUser = recentDmUser || allowFromUser || null;
const source: "session" | "allowFrom" | "none" =
recentDmUser ? "session" : (allowFromUser ? "allowFrom" : "none");
sequentialPlatformTests.push(() => testDiscord(id, discordConfig.token, discordTestUser, source));
}
// Telegram: only test once
if (id === "main" && telegramConfig.enabled && telegramConfig.botToken) {
// Telegram: only test once, via local OpenClaw channel gateway
if (id === "main" && telegramConfig.enabled) {
const telegramTestUser = getTelegramDmUser(id);
platformTests.push(testTelegram(id, telegramConfig.botToken, telegramTestUser));
sequentialPlatformTests.push(() => testTelegram(id, telegramTestUser));
}
// WhatsApp: only test once, via gateway
@@ -719,7 +759,7 @@ export async function POST() {
const whatsappTestUser = recentDmUser || allowFromUser || null;
const source: "session" | "allowFrom" | "none" =
recentDmUser ? "session" : (allowFromUser ? "allowFrom" : "none");
platformTests.push(testWhatsapp(id, gatewayPort, gatewayToken, whatsappTestUser, source));
sequentialPlatformTests.push(() => testWhatsapp(id, gatewayPort, gatewayToken, whatsappTestUser, source));
}
// QQBot: only test once, via `openclaw message send`
@@ -729,11 +769,14 @@ export async function POST() {
const qqbotTestUser = recentDmUser || allowFromUser || null;
const source: "session" | "allowFrom" | "none" =
recentDmUser ? "session" : (allowFromUser ? "allowFrom" : "none");
platformTests.push(testQqbot(id, qqbotConfig, qqbotTestUser, source));
sequentialPlatformTests.push(() => testQqbot(id, qqbotConfig, qqbotTestUser, source));
}
}
const platformResults = await Promise.all(platformTests);
for (const runTest of sequentialPlatformTests) {
platformResults.push(await runTest());
}
return NextResponse.json({ results: platformResults });
} catch (err: any) {
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

+10
View File
@@ -0,0 +1,10 @@
curl https://gpt.qt.cool/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-qt-cool-rAesCG1982qKfefUDoKa2zREJMkMTDMY" \
-d '{
"model": "gpt-5.3-codex",
"messages": [
{"role": "user", "content": "hi"}
],
"max_tokens": 16
}'
+102
View File
@@ -0,0 +1,102 @@
# 手机端适配验收清单(PC 上测试)
> 项目:OpenClaw Bot Dashboard
> 测试日期:____年__月__日
> 测试人:________
## 1. 测试环境
- [ ] 浏览器:Chrome 最新版
- [ ] DevTools 设备模式已开启(`Cmd/Ctrl + Shift + M`
- [ ] `Disable cache` 已勾选
- [ ] 使用同一构建版本进行测试(避免本地缓存干扰)
## 2. 断点与设备矩阵
### 2.1 断点覆盖
- [ ] 360 宽(小屏 Android
- [ ] 390 宽(iPhone 12/13/14
- [ ] 412 宽(大屏 Android
- [ ] 768 宽(平板/小桌面边界)
- [ ] 1024 宽(桌面基线)
### 2.2 方向覆盖
- [ ] 竖屏(Portrait
- [ ] 横屏(Landscape
## 3. 全局布局与导航
- [ ] 页面无整体横向滚动条(非表格局部滚动除外)
- [ ] 顶部/侧边导航可正常打开和关闭
- [ ] 导航点击后可正确跳转页面
- [ ] 当前页面高亮状态正确
- [ ] 语言切换可用(中文/英文)
- [ ] 主题切换可用(深色/浅色)
## 4. 页面可用性检查
### 4.1 首页 `/`
- [ ] 首屏卡片不重叠、不溢出
- [ ] 顶部按钮组在手机宽度下可操作
- [ ] Agent 卡片内容可读,关键字段不被遮挡
### 4.2 Models `/models`
- [ ] Provider 区块布局正常
- [ ] 表格/明细区域在手机端可浏览(滚动或卡片化正确)
- [ ] 测试按钮可点击,状态反馈可见
### 4.3 Sessions `/sessions`
- [ ] Agent 列表卡片布局正常
- [ ] Session 列表项不重叠
- [ ] 测试按钮与时间信息显示正常
### 4.4 Stats `/stats`
- [ ] 统计卡片在手机端不挤压重叠
- [ ] 图表区域可见且可读
- [ ] 时间范围切换按钮可操作
### 4.5 Alerts `/alerts`
- [ ] 规则区与触发区可完整显示
- [ ] 表单输入和开关可用
- [ ] 按钮点击反馈正常
### 4.6 Skills `/skills`
- [ ] 技能卡片栅格在手机端正常换行
- [ ] 标签和按钮不溢出
## 5. Pixel Office 专项 `/pixel-office`
- [ ] 页面可进入且首屏可见(无白屏/卡死)
- [ ] 画布区域在手机端可正常显示
- [ ] 点击家具可打开对应弹层(手机/时钟/沙发等)
- [ ] 弹层内容可滚动、可关闭
- [ ] 返回其他页面后无残留遮罩
- [ ] 性能可接受(无明显掉帧卡顿)
## 6. 交互与可访问性
- [ ] 所有关键按钮点击区域足够大(约 >= 40px)
- [ ] 文本可读(无大面积 10px 以下关键文案)
- [ ] 颜色对比在浅色模式下仍可读
- [ ] 无仅依赖 hover 才能触发的关键功能
## 7. 回归与稳定性
- [ ] 连续切换页面 10 次无明显布局错乱
- [ ] 切换语言/主题后布局仍稳定
- [ ] 刷新页面后状态与布局正常
## 8. 问题记录模板
| 编号 | 页面 | 设备/宽度 | 复现步骤 | 实际表现 | 期望表现 | 严重级别 |
|---|---|---|---|---|---|---|
| 1 | | | | | | |
| 2 | | | | | | |
| 3 | | | | | | |
## 9. 验收结论
- [ ] 通过(可上线)
- [ ] 有条件通过(需修复低/中优先级问题)
- [ ] 不通过(存在阻塞问题)
+143
View File
@@ -0,0 +1,143 @@
# OpenClaw Bot Dashboard 手机端适配方案
## 1. 目标
- 在不牺牲桌面端体验的前提下,完整支持手机端(优先 iPhone 12/13/14、主流 Android 360~430 宽度)。
- 保证核心路径可用:查看 Agents、Models、Sessions、Stats、Alerts、Pixel Office。
- 将“能打开”提升为“可操作”:导航、筛选、测试按钮、弹窗、图表、表格、像素办公室交互都可用。
## 2. 当前现状与主要问题
基于当前代码结构,手机端主要风险如下:
- 全局布局采用固定左侧边栏(`RootLayout + Sidebar`),移动端会明显挤压主内容区。
- 多个页面使用桌面间距和密集按钮组(`p-8`、单行多个 CTA),在小屏上易换行错位或超出。
- 部分页面是“桌面信息密度优先”(尤其 Models 表格、Stats 四卡并排),手机端阅读成本高。
- Pixel Office 为重交互 Canvas 页面,移动端触控手势、性能、弹层可读性未做专项设计。
## 3. 适配范围
- 全局框架:`app/layout.tsx``app/sidebar.tsx``app/globals.css`
- 页面:`/``/models``/sessions``/stats``/alerts``/skills``/pixel-office`
- 组件:弹窗、图表容器、表格容器、按钮组、顶部信息条
- 非目标(本阶段不做):PWA 离线、原生 App、平板专属布局体系
## 4. 设计原则
- Mobile First:先定义手机布局,再向 `md/lg` 扩展。
- 同构信息:手机端不删关键能力,只调整呈现层级与交互方式。
- 降低操作成本:按钮可点面积 >= 40px;关键操作不依赖 hover。
- 性能优先:移动端默认降低高频动画与非必要轮询密度。
## 5. 方案设计
### 5.1 全局布局与导航
- 将固定左侧边栏改为响应式双形态:
- `md` 及以上:保持当前侧栏。
- `< md`:顶部栏 + Drawer 导航(汉堡按钮打开,遮罩点击关闭)。
- 主区域在手机端改为满宽流式,不再预留侧栏 spacer 宽度。
- 顶部栏保留最关键入口:菜单、页面标题、语言/主题(可折叠进菜单)。
### 5.2 通用页面框架
- 页面外层间距统一改为响应式:
- 手机:`p-3`/`p-4`
- 桌面:`md:p-6`/`lg:p-8`
- 顶部操作区从“单行横排”改为“可换行堆叠”:
- 手机:纵向分组(主按钮一行,次按钮折叠到更多菜单或下一行)。
- 桌面:保持横向布局。
- 统一引入移动端工具类:
- 小字号下限(正文 >= 12px
- 触控尺寸类(按钮、开关、输入控件)
### 5.3 数据展示策略
- Models 页面:
- 手机端改“宽表”模式为“卡片摘要 + 可展开详情”。
- 保留“测试模型”入口,但放入每卡片底部操作区。
- Stats 页面:
- 四个 summary 卡改为 `grid-cols-2`(超小屏可 `grid-cols-1`)。
- 图表保持横向滚动,但提供最小可读宽度和标题固定。
- Sessions 页面:
- 列表卡片信息分层,弱化次要字段(session key 默认折叠)。
- 测试按钮改为单独操作行,避免与时间戳争抢空间。
### 5.4 Pixel Office 专项适配
- 交互模型:
- 将鼠标语义补齐为触控语义(tap、long-press、drag)。
- 关键弹层(手机、时钟、沙发等)在手机端改为底部抽屉式(bottom sheet)优先。
- 视图与性能:
- 根据屏幕宽度动态设置默认缩放与 UI overlay 尺寸。
- 移动端降低非关键动画密度(虫子数量上限、浮动元素频率可降级)。
- 可用性:
- 顶部 agent chips 在手机端支持横向滚动,避免遮挡主画布。
- 保证弹层关闭区域与返回路径明显可点。
### 5.5 可访问性与可读性
- 增加触屏友好焦点样式、键盘可达性(抽屉/弹窗支持 Esc 关闭)。
- 颜色对比在浅色主题下复核(已有 light mode override,需移动端回归验证)。
- 文案截断策略统一:关键 ID/模型名使用 tooltip 或展开查看。
## 6. 实施计划(分阶段)
### Phase 1:骨架改造(1~2 天)
- 完成全局响应式布局与移动端 Drawer 导航。
- 页面容器与顶部操作区完成响应式重排。
- 建立通用移动端样式规范(spacing、touch target、font scale)。
### Phase 2:页面适配(2~3 天)
- 首页、Models、Sessions、Stats、Alerts、Skills 全量适配。
- 模型表格卡片化、统计卡片栅格优化、按钮区重排。
### Phase 3Pixel Office 适配(2~3 天)
- 触控事件补齐、弹层改造(bottom sheet)、性能降级策略。
- 移动端专项交互回归(点击家具、弹窗、返回、滚动冲突)。
### Phase 4:收尾与验收(1 天)
- 设备矩阵回归测试。
- 性能与可访问性检查。
- 文档更新与变更说明。
## 7. 验收标准
- 手机端 360px 宽度下无横向整体溢出。
- 核心页面首屏可在 3 秒内进入可操作状态(本地网络场景)。
- 所有关键按钮可点击区域 >= 40px。
- Pixel Office 在手机端可完成:打开页面、点击家具弹层、关闭弹层、返回其他页面。
- 无阻塞级 UI 缺陷(错位、遮挡、不可点)。
## 8. 测试计划
- 设备维度:
- iOS Safari390x844
- Android Chrome360x800 / 412x915
- 断点维度:
- `<640``640~767``768~1023``>=1024`
- 场景维度:
- 深浅主题切换
- 中英文切换
- 数据量高(agent 多、model 多、session 多)
## 9. 风险与应对
- 风险:桌面布局回归破坏
- 应对:采用断点增量改造,桌面样式默认保持不变。
- 风险:Pixel Office 触控改造影响现有鼠标事件
- 应对:抽象输入层(pointer/touch/mouse),先加兼容层再替换调用点。
- 风险:移动端性能波动
- 应对:默认启用轻量模式参数,并提供开关。
## 10. 交付物
- 响应式导航与页面布局改造代码。
- Pixel Office 移动端交互/弹层/性能适配代码。
- 测试记录(设备、断点、问题清单)。
- 更新后的维护文档(适配规则与断点规范)。
+251
View File
@@ -0,0 +1,251 @@
# OpenBug 迁移到 OpenClaw-bot-review 的设计方案
## 1. 背景与目标
### 1.1 背景
- `OpenBug` 当前是 **Windows WPF 桌面透明覆盖层应用**`net8.0-windows`),核心是程序化虫群动画与行为系统。
- 当前项目是 **Next.js Web 应用**,已具备 `pixel-office` 的 Canvas 渲染与实体状态机能力(角色、宠物、交互彩蛋)。
### 1.2 目标
- 将 OpenBug 的核心体验迁移到 Web
1. 程序化虫子(IK 风格步态视觉 + 群体行为)
2. 低干扰背景陪伴(focus companion
3. 可配置数量/行为权重/开关
4. 与现有 Pixel Office 共存并可扩展
### 1.3 非目标(首期不做)
- 不做 Windows 原生托盘能力(用 Web UI 替代)
- 不做真正系统级“桌面覆盖”(浏览器沙箱不支持)
- 不做逐行 1:1 C# 代码搬运(改为 TypeScript 重构)
---
## 2. 迁移策略(推荐)
采用 **“行为内核迁移 + 渲染层重建”**:
- 行为与数据结构从 C# 迁移到 TS(可测试、可演进)
- 渲染直接接入现有 `pixel-office` Canvas pipeline
- 分阶段上线:先可见、再拟真、再性能优化
理由:
- 当前仓库已有成熟循环:`OfficeState.update(dt)` + `renderFrame(...)`
- 直接复用能减少接线成本,避免并行维护两套引擎
---
## 3. 架构设计
### 3.1 模块落位
新增目录建议:
- `lib/pixel-office/bugs/types.ts`
- `lib/pixel-office/bugs/config.ts`
- `lib/pixel-office/bugs/bugEntity.ts`
- `lib/pixel-office/bugs/bugBehavior.ts`
- `lib/pixel-office/bugs/spatialGrid.ts`
- `lib/pixel-office/bugs/pheromoneField.ts`
- `lib/pixel-office/bugs/ik2d.ts`
- `lib/pixel-office/bugs/renderer.ts`
接入点:
- `lib/pixel-office/engine/officeState.ts`
- 持有 `bugSystem`,在 `update(dt)` 中 tick
- `lib/pixel-office/engine/renderer.ts`
- 在角色层之前或之后绘制虫群(可配置层级)
- `app/pixel-office/page.tsx`
- 提供 UI 控件(开关、数量、性能模式)
### 3.2 数据模型(核心)
`BugEntity`(建议):
- `id`
- `x,y, vx,vy, heading`
- `behaviorType: social | loner | edgeDweller`
- `state: idle | moving | interacting`
- `speedScale, turnScale`
- `legs[6]`(目标点、关节点、相位)
- `color, sizeScale`
- `trailTargetId?`(跟随对象)
- `visible`
`BugSystemState`
- `bugs: BugEntity[]`
- `spatialGrid`
- `pheromoneField`
- `spawnTimer`
- `swarmEventTimer/cooldown`
- `paused`
### 3.3 更新循环
每帧:
1. 建立/更新空间网格(邻域查询)
2. 更新信息素场(蒸发)
3. 每只虫子执行行为决策:
- 分离/对齐/凝聚(Boids
- 边缘偏好
- 游走噪声
- 鼠标斥力(若开启)
4. 移动学更新(位置/朝向)
5. 步态与 IK 更新(腿端落点 + 两段骨骼解算)
6. 写回渲染缓存
---
## 4. 功能映射(OpenBug -> Web
### 4.1 行为系统映射
- `AntBehaviorType` -> `BugBehaviorType`(保持三类)
- `_stateTimer`/`_wanderTimer` -> JS 定时器字段
- `_swarmActive` + 随机事件 -> 保持,参数可调
- `SpatialGrid.Query` -> TS 版本网格索引
- `PheromoneField` -> TS 2D 栅格(TypedArray
### 4.2 生命周期映射
- 初始 5 只、定时生成(默认 10 分钟)
- 最大数量(建议首发上限 120,性能模式可扩)
- 可手动加减数量(替代托盘菜单)
### 4.3 UI 替代托盘
- 在 Pixel Office 顶栏新增 “Bug Panel”:
1. `Enable Bugs` 开关
2. `Count` 数量滑条
3. `Spawn Interval` 生成间隔
4. `Performance Mode`low/medium/high
5. `Hide/Show`(暂停更新+渲染)
---
## 5. 渲染方案
### 5.1 首发渲染分级
#### V1(快速上线)
- 简化虫体:头/胸/腹 3 段椭圆 + 6条折线腿
- 不引入复杂抗锯齿特效
- 目标:先保证行为和性能
#### V2(增强拟真)
- 二段腿 IK 过渡插值
- 步态相位优化(tripod gait
- 颜色与体型渐进(随生命周期)
#### V3(视觉精修)
- 轻微阴影/景深错觉
- 群聚通信短暂停顿动作
- 可选“调试可视化层”(邻域半径、信息素热力)
### 5.2 层级建议
- 地板 -> 家具 -> 虫群 -> 角色/宠物(可配置)
- 避免覆盖关键交互热点(白板/时钟/模型面板)
---
## 6. 性能设计
### 6.1 预算
- 目标设备:中端笔记本浏览器
- 指标:
1. 60 FPS(目标)/ 30 FPS(可接受下限)
2. Bug 100 只时交互无明显卡顿
3. CPU 占用可控(空闲不应飙升)
### 6.2 手段
- 邻域查询必须走 Spatial Grid,禁止 O(n²) 全量扫描
- 信息素采用稀疏更新/低分辨率栅格
- IK 与行为 decouple:例如行为 30Hz、渲染 60Hz
- `requestAnimationFrame` 内减少对象分配,优先复用数组
- Debug overlay 默认关闭
---
## 7. 持久化与配置
配置存储优先级:
1. `localStorage`(用户本地偏好)
2. 可选后端配置接口(后续)
建议键:
- `pixel-office-bugs-enabled`
- `pixel-office-bugs-count`
- `pixel-office-bugs-mode`
- `pixel-office-bugs-spawn-interval`
---
## 8. 开发里程碑
### M1:最小可运行(2-3 天)
- BugSystem 基础数据结构
- 随机游走 + 边界处理
- Canvas 简化渲染
- 控制面板开关与数量调节
### M2:行为与生态(3-5 天)
- SpatialGrid 邻域
- Boids 三力 + 三类行为人格
- 定时生成、上限控制、暂停/恢复
### M3IK 与拟真(4-6 天)
- 二段腿 IK
- 三角步态相位
- 信息素场与 trail follow
### M4:优化与验收(2-3 天)
- 性能 profiling
- 参数调优与默认值固化
- 文档与回归测试
---
## 9. 风险与应对
1. **浏览器性能不足(高数量卡顿)**
- 应对:分级渲染、降低更新频率、上限保护
2. **与现有角色交互冲突(命中检测/视觉遮挡)**
- 应对:虫群层级可切换;点击命中先走业务实体后走虫群
3. **过度拟真导致开发周期失控**
- 应对:严格按 M1->M4 递增交付,不跨阶段
4. **参数过多难维护**
- 应对:集中 `config.ts`,提供 presetsfocus/calm/chaos
---
## 10. 验收标准(首版)
功能验收:
1. 能开启/关闭虫群系统
2. 可调整数量,新增/减少即时生效
3. 至少支持三类行为偏好(social/loner/edge
4. 定时生成与上限生效
性能验收:
1. 80 只虫时页面操作无明显掉帧
2. 连续运行 30 分钟无内存持续增长异常
体验验收:
1. 默认参数不抢主界面注意力
2. 支持“隐藏虫群”用于演示/录屏
---
## 11. 建议的实施顺序(落地)
1. 先上 `BugSystem` 空壳和最简渲染(M1
2. 接入 `officeState.update``renderer.renderScene`
3. 加 UI 控制面板
4. 再迁移 Boids + Grid + 信息素
5. 最后做 IK 精修和性能调优
---
## 12. 可选扩展
- “专注模式”预设:低密度、低速度、低干扰
- “混乱模式”预设:高密度、群聚事件频繁
- 和现有宠物系统联动(猫/龙虾对虫群反应)
- 统计页新增“Bug 活跃度”指标
+123
View File
@@ -0,0 +1,123 @@
# Pixel Office - Gateway 健康态 SRE 像素人方案(仅设计)
## 1. 目标
- 在像素办公室增加一个与 OpenClaw `gateway` 健康状态强绑定的像素人类型:`SRE 值班工程师(救火队长)`
- 通过可视化行为让用户一眼识别网关状态变化(正常/退化/故障),提升可观测性与“游戏化”反馈。
- 本方案只做设计,不做实现。
## 2. 非目标
- 不在本次引入自动重启 gateway、自动修复策略。
- 不修改现有 agent/subagent 业务状态语义。
- 不改变 PC/手机端现有布局结构(只加角色和行为层)。
## 3. 角色定义
- 角色名:`值班SRE`
- 显示文案:
- 中文:`值班SRE`
- 英文:`On-call SRE`
- 唯一性:每个像素办公室页面仅 1 个该类型像素人(系统角色,不占普通 agent 名额)。
## 4. 状态模型(Gateway -> 像素人)
定义三态:
1. `healthy`(健康)
- 条件:`/api/gateway-health` 返回 `ok=true`,且请求耗时低于阈值。
- 行为:低速巡检(固定区域慢速走动),绿色标签。
2. `degraded`(退化)
- 条件(建议):
- 连续 N 次(建议 2 次)请求超时接近上限;或
- `ok=true` 但响应耗时超过阈值(建议 >1500ms)。
- 行为:警戒巡检(更快移动 + 黄色闪烁),优先靠近 gateway 区域。
3. `down`(故障)
- 条件:`ok=false`(如 ECONNREFUSED/超时/HTTP 非 2xx)。
- 行为:快速冲刺到“救火点”(gateway 工位附近)并停留,红色闪烁标签。
恢复策略:
- `down/degraded -> healthy` 需满足连续 M 次(建议 2 次)健康结果,避免抖动。
## 5. 数据流设计
当前已有接口:`GET /api/gateway-health` 返回 `{ ok, error?, data?, webUrl? }`
建议扩展(向后兼容):
- `checkedAt`:毫秒时间戳
- `responseMs`:本次健康检查耗时
- `status``healthy | degraded | down`(若后端不算,则前端根据 `ok + responseMs + 连续失败计数`计算)
轮询策略:
- 复用现有 10s 轮询节奏(避免额外压力)。
- 像素办公室内部与顶部网关状态组件共享同一份健康态缓存(单次请求,多处消费)。
## 6. 像素办公室行为设计
### 6.1 出生与常驻
- 页面加载后创建系统角色 `sre-gateway-1`
- 若 gateway 状态未知:角色在“待命位”静止(灰色标签“监控中”)。
### 6.2 行为区域
- 巡检区:右办公室上半区 + 通道口(避免进入休息区造成干扰)。
- 救火点:右办公室 gateway 相关工位附近(建议现有 `pc-r` 附近 tile)。
### 6.3 动画与速度
- `healthy``moveSpeedMultiplier=0.9`
- `degraded``moveSpeedMultiplier=1.4` + 标签闪烁
- `down``moveSpeedMultiplier=2.2`,优先直达救火点,短暂停留后小范围折返
### 6.4 标签与提示
- 头顶标签:
- healthy`救火队长`(绿色)
- degraded`救火队长`(黄色)
- down`救火队长`(深红+闪烁)
- hover tooltip
- `Gateway healthy`
- `Gateway degraded: latency high`
- `Gateway down: <error message>`
## 7. 代码落点(实施指引)
仅列设计,不执行:
1. `app/api/gateway-health/route.ts`
- 增加 `responseMs/checkedAt`(可选 `status`)。
2. `app/pixel-office/page.tsx`
- 引入 gateway 健康态轮询(或复用共享 store)。
- 把健康态喂给 office engine。
3. `lib/pixel-office/engine/officeState.ts`
- 新增系统角色创建/更新入口:`ensureGatewaySre()``updateGatewaySreState()`
- 增加 gateway 三态行为状态机与目标点选择。
4. `lib/pixel-office/engine/characters.ts`
- 复用已有 WALK/IDLE 框架,增加 SRE 特定行为策略(巡检 vs 救火)。
5. `lib/pixel-office/engine/renderer.ts`
- 根据 SRE 状态渲染标签颜色和闪烁。
6. `lib/i18n.tsx`
- 新增文案键:`pixelOffice.gatewaySre.*`
## 8. 配置与阈值(建议可配置)
- `gatewaySre.pollMs = 10000`
- `gatewaySre.degradedLatencyMs = 1500`
- `gatewaySre.downConsecutiveFailures = 2`
- `gatewaySre.recoverConsecutiveSuccess = 2`
- `gatewaySre.patrolRadiusTiles = 5`
## 9. 测试方案
功能测试:
- 手动停 gateway:应在 1~2 个轮询周期内进入 `down` 行为。
- 恢复 gateway:应在连续成功后回到 `healthy`
- 人为注入慢响应:应进入 `degraded`
回归测试:
- 不影响普通 agent/subagent 的移动、选座、标签与 tooltip。
- 手机端与桌面端都能显示 SRE 角色且不卡顿。
## 10. 风险与规避
- 状态抖动风险:使用连续计数和滞后恢复。
- 渲染性能风险:仅 1 个系统角色,复用现有帧循环,影响可控。
- 语义冲突风险:SRE 角色使用独立 `id``isSystemRole` 标记,避免和业务 agent 混淆。
## 11. 验收标准
- 进入像素办公室后可见 SRE 角色。
- Gateway 健康状态变化时,SRE 行为在可接受延迟内切换。
- 整体页面 FPS 无明显下降,现有交互无回归。
+106
View File
@@ -0,0 +1,106 @@
# 像素办公室 Subagent 展示方案(仅设计)
## 1. 背景与现状
当前像素办公室已有部分 subagent 基础能力:
- `/api/agent-activity` 已能返回 `subagents[]`(基于 session 日志里的 `tool_use/tool_result` 推断)。
- `syncAgentsToOffice` 已支持 subagent 的创建与移除(`addSubagent/removeSubagent`)。
- 但页面顶部 Agent 区域只展示主 Agent,不展示 subagent。
- subagent 在画布中虽可创建,但名称未统一为“外包”。
因此用户感知上仍是“看不到 subagent 生命周期”。
## 2. 目标
- 像素办公室页面显式展示 subagent。
- subagent 统一命名为:`外包`
- subagent 在像素办公室中始终以 `working` 状态像素人展示(不进入 idle/waiting/offline)。
- subagent 创建时开始显示,结束时立即消失(以当前轮询粒度为准)。
- 同时兼容手机端与桌面端现有布局逻辑。
## 3. 非目标
- 不改动机器人主页(`/`)的卡片结构。
- 不新增复杂权限模型或独立 subagent 配置页。
- 不重构现有渲染引擎/寻路逻辑。
## 4. 核心方案
## 4.1 数据层(API
继续使用 `/api/agent-activity` 返回 `subagents`,但规范为“只返回活跃 subagent”:
- 创建判定:出现 `tool_use` 且匹配 subtask/subagent 模式。
- 结束判定:出现对应 `tool_result` 后从活跃集合移除。
- 当父 Agent 进入 `idle/offline` 时,强制返回 `subagents = []`(避免残留)。
建议返回结构保持不变(最小改动):
```ts
subagents?: Array<{
toolId: string
label: string // 前端暂不使用,统一展示“外包”
}>
```
## 4.2 同步层(agentBridge / officeState
- 保持现有 `toolId` 作为 subagent 生命周期主键。
- `syncAgentsToOffice` 继续基于 diff 执行:
- 新出现 `toolId` => `addSubagent`
- 消失 `toolId` => `removeSubagent`
- 在创建 subagent 后统一设置 `ch.label = '外包'`
- 在创建 subagent 后强制设置为工作态:
- `office.setAgentActive(subId, true)`
- 不设置 waiting bubble
- 不接入主 Agent 的 idle/offline 状态流转
- 移除时沿用现有 despawn 动画与 seat 释放逻辑。
## 4.3 展示层(像素办公室页面)
顶部 Agent 区域新增“主 Agent + subagent”的统一展示流:
-`agents` 扁平化为 `displayAgents`
- 主 Agent:沿用当前显示。
- subagent:生成虚拟展示项,建议 key 为 `${agentId}:${toolId}`
- subagent 展示规范:
- 名称固定:`外包`
- 状态固定映射:`working`
- 样式可与 working Agent 相同,但加轻量标识(如 `SUB` 小角标,非必须)。
移动端 3x3 分页与桌面端换行逻辑均直接消费 `displayAgents`,不需要额外分支。
## 5. 生命周期定义
- “开始显示”:本轮 `agent-activity` 轮询返回该 `toolId`
- “结束消失”:本轮轮询不再返回该 `toolId`,或父 Agent 非 working。
- “状态定义”:subagent 在存在期间始终视为 `working`,不做中间态切换。
- 一致性策略:前端不做长缓存,不做延迟保留,严格跟随轮询结果。
## 6. 边界情况
- 同一父 Agent 多个 subagent 并发:显示多个“外包”条目(允许重名)。
- 父 Agent 离线:主 Agent 与其 subagent 全部移除。
- 日志解析失败:该轮 subagent 为空,不阻塞主 Agent 展示。
- 快速创建/结束(小于轮询间隔):可能被跳过;属于轮询制约,非功能错误。
## 7. 验收标准(DoD
- 父 Agent 产生 subtask 后,像素办公室顶部出现“外包”条目。
- subtask 完成后,“外包”条目消失。
- 画布中对应 subagent 名称显示为“外包”。
- subagent 在画布和顶部列表中始终显示为 working 态(无 idle/waiting/offline)。
- 手机端和桌面端都可见且布局不乱。
- 不影响现有主 Agent 状态、提示气泡、统计面板与性能。
## 8. 实施步骤(建议)
1. API:校准活跃 subagent 集合输出与父 Agent 状态联动。
2. Bridge/Office:创建时统一命名 `外包`,结束即移除。
3. Pixel Office UI:引入 `displayAgents` 扁平渲染。
4. 联调与回归:手机/桌面、单 subagent/多 subagent、切页返回场景。
---
本方案为设计稿,当前不包含代码实现。
+149
View File
@@ -0,0 +1,149 @@
我想把/Users/manruixie/code/pixel_agent/pixel-agents的功能复刻到/Users/manruixie/code/pixel_agent/OpenClaw-bot-review项目,在OpenClaw-bot-review项目新增一个页面实现该功能,请设计技术方案,先不用实现
技术方案如下:
## Pixel Agents 页面移植到 OpenClaw Bot Dashboard — 技术方案
### 一、目标
在 OpenClaw-bot-reviewNext.js Web Dashboard)中新增一个 `/pixel-office` 页面,复刻 pixel-agents 的核心功能:像素风虚拟办公室,每个 OpenClaw Agent 对应一个动画角色,实时反映 Agent 工作状态。
### 二、核心差异与适配
| 维度 | 原版 (VS Code 插件) | 移植版 (Next.js Web) |
|---|---|---|
| 数据来源 | 监听 `~/.claude/projects/` 下的 JSONL 文件 | 通过 API 读取 OpenClaw 的 session 状态和 Agent 配置 |
| Agent 发现 | 手动创建终端 / 扫描 JSONL | 从 `openclaw.json` 读取所有 Agent 列表 |
| 状态推送 | VS Code Webview postMessage | SSE (Server-Sent Events) 或 WebSocket 轮询 |
| 运行环境 | VS Code Webview (受限浏览器) | 标准浏览器,无限制 |
| 资产加载 | 内嵌 base64 sprite data | 同样内嵌,或放 `/public` 静态资源 |
### 三、架构设计
app/
pixel-office/
page.tsx ← 页面入口(client component
components/
PixelOffice.tsx ← 主组件,管理 Canvas + 游戏循环
ToolOverlay.tsx ← 工具状态浮层(复用原版逻辑)
OfficeToolbar.tsx ← 底部工具栏(布局编辑、缩放等)
api/
agent-activity/
route.ts ← SSE 端点,推送 Agent 实时状态
lib/
pixel-office/
engine/
officeState.ts ← 办公室状态管理(直接移植)
characters.ts ← 角色状态机(直接移植)
gameLoop.ts ← Canvas 游戏循环(直接移植)
renderer.ts ← 渲染器(直接移植)
sprites/
spriteData.ts ← 像素精灵数据(直接移植)
spriteCache.ts ← 精灵缓存(直接移植)
layout/
tileMap.ts ← 地图寻路 BFS(直接移植)
layoutSerializer.ts ← 布局序列化(直接移植)
furnitureCatalog.ts ← 家具目录(直接移植)
types.ts ← 类型定义(移植 + 适配)
agentBridge.ts ← 🆕 核心适配层:将 OpenClaw Agent 状态映射为角色动作
### 四、关键模块设计
#### 1. Agent 状态桥接层 (`agentBridge.ts`)
这是移植的核心适配层。原版通过解析 Claude Code 的 JSONL transcript 获取 Agent 状态,移植版需要改为从 OpenClaw 获取:
// Agent 活动状态(替代 JSONL 解析)
interface AgentActivity {
agentId: string
state: 'idle' | 'working' | 'waiting' | 'offline'
currentTool?: string // 当前使用的工具名
toolStatus?: string // 工具状态描述
lastActive: number // 最后活跃时间戳
}
数据获取方式(两种方案选一):
方案 A — 轮询:前端每 10s 调用 `/api/agent-activity`,后端读取 OpenClaw session 文件解析最新状态
方案 B — SSE 推送:后端 watch session 文件变化,通过 SSE 实时推送状态变更
建议用方案 A 起步(简单可靠),后续可升级为 SSE。
#### 2. 后端 API (`/api/agent-activity/route.ts`)
// 读取每个 Agent 的最新 session 状态
// 数据来源:
// 1. ~/.openclaw/sessions/ 目录下的 session 文件
// 2. 解析最近的消息记录判断 Agent 当前状态
// 3. 从 openclaw.json 获取 Agent 列表和模型信息
interface AgentActivityResponse {
agents: Array<{
id: string
name: string
emoji: string
state: 'idle' | 'working' | 'waiting' | 'offline'
currentTool?: string
toolStatus?: string
}>
}
#### 3. 游戏引擎层(直接移植)
以下模块可以几乎原封不动地移植,只需去掉 `vscode` 依赖:
`officeState.ts` — 办公室状态管理,角色增删、座位分配、碰撞检测
`characters.ts` — 角色状态机 (IDLE → WALK → TYPE)BFS 寻路
`gameLoop.ts` — requestAnimationFrame 游戏循环
`renderer.ts` — Canvas 2D 渲染,z-sorting,精灵绘制
`sprites/` — 像素精灵数据和缓存
`layout/` — 地图、家具、寻路
#### 4. 前端主组件 (`PixelOffice.tsx`)
// 核心流程:
// 1. 从 /api/config 获取 Agent 列表 → 为每个 Agent 创建角色
// 2. 轮询 /api/agent-activity 获取实时状态 → 更新角色动画
// 3. Canvas 游戏循环渲染像素办公室
// 4. 点击角色 → 显示 Agent 详情 / 跳转到 session 页面
function PixelOffice() {
const canvasRef = useRef<HTMLCanvasElement>(null)
const officeRef = useRef<OfficeState>(new OfficeState())
// 初始化:加载 Agent 列表,创建角色
// 轮询:定时获取 Agent 状态,更新角色
// 渲染:启动 gameLoop
}
#### 5. 侧边栏集成
`sidebar.tsx` 的 NAV_ITEMS 中新增:
{
group: "nav.monitor",
items: [
// ... 现有项
{ href: "/pixel-office", icon: "🎮", labelKey: "nav.pixelOffice" },
],
}
### 五、状态映射规则
| OpenClaw Agent 状态 | 角色行为 |
|---|---|
| `working` (有活跃 session) | 坐在工位打字动画 (TYPE) |
| `working` + 使用 Read/Grep 工具 | 坐在工位阅读动画 |
| `idle` (>5min 无活动) | 起身闲逛 (IDLE → WALK) |
| `waiting` (等待用户输入) | 显示气泡提示 (💬) |
| `offline` (无 session) | 不显示 / 灰色静止 |
### 六、布局持久化
原版存在 `~/.pixel-agents/layout.json`,移植版可以:
存到 `~/.openclaw/pixel-office/layout.json`
通过 `/api/pixel-office/layout` 读写
### 七、实施步骤(建议顺序)
搭建页面骨架 — `/pixel-office/page.tsx` + 侧边栏入口 + i18n
移植引擎层 — 把 `lib/pixel-office/` 下的纯逻辑代码搬过来,去掉 vscode 依赖
回复 bot主理人: 
我想把/Users/manruixie/code/pixel_agent/pixel-agents的功能复刻到/Users/manruixie/code/pixel_agent/OpenClaw-bot-review项目,在OpenClaw-bot-review项目新增一个pixel-agents页面实现该功能,请设计技术方案,先不用实现
实现 Agent 状态 API — `/api/agent-activity`,读取 OpenClaw session 数据
实现桥接层 — `agentBridge.ts`,将 API 数据映射为角色状态
组装主组件 — Canvas 渲染 + 状态轮询 + 交互
布局编辑器 — 移植编辑模式(可作为 Phase 2)
### 八、工作量估算
Phase 1(核心功能):引擎移植 + API + 基础页面 → 约 2-3 天
Phase 2(完整体验):布局编辑器 + 音效 + 子 Agent 可视化 → 约 1-2 天
---
这个方案的核心思路是:游戏引擎层几乎原样搬,重点工作在于用 OpenClaw 的 Agent/Session 数据替代原来的 Claude Code JSONL 解析。要开始实现吗?
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB