mirror of
https://github.com/xmanrui/OpenClaw-bot-review.git
synced 2026-08-14 08:51:57 +00:00
chore: save local workspace changes
This commit is contained in:
+179
-136
@@ -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 |
@@ -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
|
||||
}'
|
||||
@@ -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. 验收结论
|
||||
|
||||
- [ ] 通过(可上线)
|
||||
- [ ] 有条件通过(需修复低/中优先级问题)
|
||||
- [ ] 不通过(存在阻塞问题)
|
||||
|
||||
@@ -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 移动端交互/弹层/性能适配代码。
|
||||
- 测试记录(设备、断点、问题清单)。
|
||||
- 更新后的维护文档(适配规则与断点规范)。
|
||||
|
||||
@@ -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 活跃度”指标
|
||||
|
||||
@@ -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 无明显下降,现有交互无回归。
|
||||
@@ -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、切页返回场景。
|
||||
|
||||
---
|
||||
|
||||
本方案为设计稿,当前不包含代码实现。
|
||||
@@ -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<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 |
Reference in New Issue
Block a user