diff --git a/app/api/test-platforms/route.ts b/app/api/test-platforms/route.ts index dee1a9c..a0eb4d3 100644 --- a/app/api/test-platforms/route.ts +++ b/app/api/test-platforms/route.ts @@ -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 { 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 { 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[] = []; + const sequentialPlatformTests: Array<() => Promise> = []; const testedFeishuAccounts = new Set(); 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) { diff --git a/docs/coffee-machine.gif b/docs/coffee-machine.gif new file mode 100644 index 0000000..3d24971 Binary files /dev/null and b/docs/coffee-machine.gif differ diff --git a/docs/test.txt b/docs/test.txt new file mode 100644 index 0000000..369a2b1 --- /dev/null +++ b/docs/test.txt @@ -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 + }' \ No newline at end of file diff --git a/prd/mobile_adaptation_checklist.md b/prd/mobile_adaptation_checklist.md new file mode 100644 index 0000000..6073d93 --- /dev/null +++ b/prd/mobile_adaptation_checklist.md @@ -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. 验收结论 + +- [ ] 通过(可上线) +- [ ] 有条件通过(需修复低/中优先级问题) +- [ ] 不通过(存在阻塞问题) + diff --git a/prd/mobile_adaptation_plan.md b/prd/mobile_adaptation_plan.md new file mode 100644 index 0000000..71d0ee8 --- /dev/null +++ b/prd/mobile_adaptation_plan.md @@ -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 3:Pixel Office 适配(2~3 天) + +- 触控事件补齐、弹层改造(bottom sheet)、性能降级策略。 +- 移动端专项交互回归(点击家具、弹窗、返回、滚动冲突)。 + +### Phase 4:收尾与验收(1 天) + +- 设备矩阵回归测试。 +- 性能与可访问性检查。 +- 文档更新与变更说明。 + +## 7. 验收标准 + +- 手机端 360px 宽度下无横向整体溢出。 +- 核心页面首屏可在 3 秒内进入可操作状态(本地网络场景)。 +- 所有关键按钮可点击区域 >= 40px。 +- Pixel Office 在手机端可完成:打开页面、点击家具弹层、关闭弹层、返回其他页面。 +- 无阻塞级 UI 缺陷(错位、遮挡、不可点)。 + +## 8. 测试计划 + +- 设备维度: + - iOS Safari(390x844) + - Android Chrome(360x800 / 412x915) +- 断点维度: + - `<640`、`640~767`、`768~1023`、`>=1024` +- 场景维度: + - 深浅主题切换 + - 中英文切换 + - 数据量高(agent 多、model 多、session 多) + +## 9. 风险与应对 + +- 风险:桌面布局回归破坏 + - 应对:采用断点增量改造,桌面样式默认保持不变。 +- 风险:Pixel Office 触控改造影响现有鼠标事件 + - 应对:抽象输入层(pointer/touch/mouse),先加兼容层再替换调用点。 +- 风险:移动端性能波动 + - 应对:默认启用轻量模式参数,并提供开关。 + +## 10. 交付物 + +- 响应式导航与页面布局改造代码。 +- Pixel Office 移动端交互/弹层/性能适配代码。 +- 测试记录(设备、断点、问题清单)。 +- 更新后的维护文档(适配规则与断点规范)。 + diff --git a/prd/openbug_migration_plan.md b/prd/openbug_migration_plan.md new file mode 100644 index 0000000..3a242f7 --- /dev/null +++ b/prd/openbug_migration_plan.md @@ -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 三力 + 三类行为人格 +- 定时生成、上限控制、暂停/恢复 + +### M3:IK 与拟真(4-6 天) +- 二段腿 IK +- 三角步态相位 +- 信息素场与 trail follow + +### M4:优化与验收(2-3 天) +- 性能 profiling +- 参数调优与默认值固化 +- 文档与回归测试 + +--- + +## 9. 风险与应对 + +1. **浏览器性能不足(高数量卡顿)** +- 应对:分级渲染、降低更新频率、上限保护 + +2. **与现有角色交互冲突(命中检测/视觉遮挡)** +- 应对:虫群层级可切换;点击命中先走业务实体后走虫群 + +3. **过度拟真导致开发周期失控** +- 应对:严格按 M1->M4 递增交付,不跨阶段 + +4. **参数过多难维护** +- 应对:集中 `config.ts`,提供 presets(focus/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 活跃度”指标 + diff --git a/prd/pixel_office_gateway_sre_plan.md b/prd/pixel_office_gateway_sre_plan.md new file mode 100644 index 0000000..2766b42 --- /dev/null +++ b/prd/pixel_office_gateway_sre_plan.md @@ -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: ` + +## 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 无明显下降,现有交互无回归。 diff --git a/prd/pixel_office_subagent_display_plan.md b/prd/pixel_office_subagent_display_plan.md new file mode 100644 index 0000000..9979b30 --- /dev/null +++ b/prd/pixel_office_subagent_display_plan.md @@ -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、切页返回场景。 + +--- + +本方案为设计稿,当前不包含代码实现。 diff --git a/prd/pixel_prd.md b/prd/pixel_prd.md new file mode 100644 index 0000000..4919b29 --- /dev/null +++ b/prd/pixel_prd.md @@ -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-review(Next.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(null) + const officeRef = useRef(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 解析。要开始实现吗? \ No newline at end of file diff --git a/public/assets/pixel-office/coffee-machine.gif b/public/assets/pixel-office/coffee-machine.gif new file mode 100644 index 0000000..d707824 Binary files /dev/null and b/public/assets/pixel-office/coffee-machine.gif differ diff --git a/public/assets/pixel-office/server.gif b/public/assets/pixel-office/server.gif new file mode 100644 index 0000000..8bdc2f9 Binary files /dev/null and b/public/assets/pixel-office/server.gif differ diff --git a/public/assets/pixel-office/server.png b/public/assets/pixel-office/server.png new file mode 100644 index 0000000..561ee39 Binary files /dev/null and b/public/assets/pixel-office/server.png differ