mirror of
https://github.com/xmanrui/OpenClaw-bot-review.git
synced 2026-08-14 00:47:49 +00:00
調整pix office 行為
This commit is contained in:
@@ -919,9 +919,14 @@ async function detectStateFromSession(
|
||||
now: number,
|
||||
lastActive: number,
|
||||
): Promise<'idle' | 'working' | 'offline'> {
|
||||
const OFFLINE_MS = 10 * 60 * 1000
|
||||
if (lastActive === 0) return 'offline'
|
||||
|
||||
const OFFLINE_MS = 10 * 60 * 1000 // idle > 10 min → offline
|
||||
const WORKING_MAX_MS = 10 * 60 * 1000 // working > 10 min → force idle
|
||||
const timeDiff = now - lastActive
|
||||
if (lastActive === 0 || timeDiff > OFFLINE_MS) return 'offline'
|
||||
|
||||
// Last activity > 10 min ago → offline regardless of session content
|
||||
if (timeDiff > OFFLINE_MS) return 'offline'
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(sessionFilePath, 'utf8')
|
||||
@@ -943,17 +948,15 @@ async function detectStateFromSession(
|
||||
} catch { /* skip malformed line */ }
|
||||
}
|
||||
|
||||
// User message is the last written record → agent is generating a response right now
|
||||
if (lastRole === 'user') return 'working'
|
||||
// toolResult as the latest message → agent is processing the tool output
|
||||
if (lastRole === 'toolResult') return 'working'
|
||||
// Agent called a tool and is waiting for the result
|
||||
if (lastStopReason === 'toolUse') return 'working'
|
||||
// Agent completed its turn — it's now idle
|
||||
// Work completed → immediately idle
|
||||
if (lastStopReason === 'stop') return 'idle'
|
||||
// Still processing — but cap at 10 min, after that force idle
|
||||
if (lastRole === 'user' || lastRole === 'toolResult' || lastStopReason === 'toolUse') {
|
||||
return timeDiff <= WORKING_MAX_MS ? 'working' : 'idle'
|
||||
}
|
||||
} catch { /* file unreadable — fall through */ }
|
||||
|
||||
// Fallback: time-based (no session content available)
|
||||
// Fallback within the 10-min window
|
||||
return timeDiff <= 2 * 60 * 1000 ? 'working' : 'idle'
|
||||
}
|
||||
|
||||
@@ -978,6 +981,36 @@ export async function GET() {
|
||||
let mostRecentSessionFile: string | null = null
|
||||
let agentSessionsDir = ''
|
||||
|
||||
// Resolve emoji: IDENTITY.md > agent.json > openclaw.json > default
|
||||
let agentJsonEmoji: string | undefined
|
||||
if (existsSync(agentsDir)) {
|
||||
// 1. Read from workspace IDENTITY.md ("- **Emoji:** 🌸")
|
||||
const workspaceDir = typeof (agent as any).workspace === 'string' ? (agent as any).workspace : null
|
||||
if (workspaceDir) {
|
||||
const identityPath = path.join(workspaceDir, 'IDENTITY.md')
|
||||
if (existsSync(identityPath)) {
|
||||
try {
|
||||
const identityRaw = await fs.readFile(identityPath, 'utf8')
|
||||
const m = identityRaw.match(/\*\*Emoji:\*\*\s*(\S+)/)
|
||||
if (m?.[1]) agentJsonEmoji = m[1]
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
// 2. Fallback: read from agent's agent.json emoji field
|
||||
if (!agentJsonEmoji) {
|
||||
const agentJsonPath = path.join(agentsDir, agent.id, 'agent', 'agent.json')
|
||||
if (existsSync(agentJsonPath)) {
|
||||
try {
|
||||
const raw = await fs.readFile(agentJsonPath, 'utf8')
|
||||
const parsed = JSON.parse(raw)
|
||||
if (typeof parsed?.emoji === 'string' && parsed.emoji.trim()) {
|
||||
agentJsonEmoji = parsed.emoji.trim()
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (existsSync(agentsDir)) {
|
||||
agentSessionsDir = path.join(agentsDir, agent.id, 'sessions')
|
||||
if (existsSync(agentSessionsDir)) {
|
||||
@@ -1062,7 +1095,7 @@ export async function GET() {
|
||||
agents.push({
|
||||
agentId: agent.id,
|
||||
name: agent.name || agent.id,
|
||||
emoji: agent.identity?.emoji || agent.emoji || '🤖',
|
||||
emoji: agentJsonEmoji || agent.identity?.emoji || agent.emoji || '🤖',
|
||||
state,
|
||||
lastActive,
|
||||
subagents,
|
||||
|
||||
+21
-1
@@ -231,6 +231,25 @@ function getChannelDirectPeerIds(
|
||||
return map;
|
||||
}
|
||||
// 从 IDENTITY.md 读取机器人名字
|
||||
function readIdentityEmoji(agentId: string, agentDir?: string, workspace?: string): string | null {
|
||||
const candidates = [
|
||||
agentDir ? path.join(agentDir, "IDENTITY.md") : null,
|
||||
workspace ? path.join(workspace, "IDENTITY.md") : null,
|
||||
path.join(OPENCLAW_DIR, `agents/${agentId}/agent/IDENTITY.md`),
|
||||
path.join(OPENCLAW_DIR, `workspace-${agentId}/IDENTITY.md`),
|
||||
agentId === "main" ? path.join(OPENCLAW_DIR, `workspace/IDENTITY.md`) : null,
|
||||
].filter(Boolean) as string[];
|
||||
|
||||
for (const p of candidates) {
|
||||
try {
|
||||
const content = fs.readFileSync(p, "utf-8");
|
||||
const match = content.match(/\*\*Emoji:\*\*\s*(\S+)/);
|
||||
if (match?.[1]) return match[1].trim();
|
||||
} catch {}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function readIdentityName(agentId: string, agentDir?: string, workspace?: string): string | null {
|
||||
const candidates = [
|
||||
agentDir ? path.join(agentDir, "IDENTITY.md") : null,
|
||||
@@ -350,7 +369,8 @@ export async function GET() {
|
||||
const id = agent.id;
|
||||
const identityName = readIdentityName(id, agent.agentDir, agent.workspace);
|
||||
const name = identityName || agent.name || id;
|
||||
const emoji = agent.identity?.emoji || "🤖";
|
||||
const identityEmoji = readIdentityEmoji(id, agent.agentDir, agent.workspace);
|
||||
const emoji = identityEmoji || agent.identity?.emoji || agent.emoji || "🤖";
|
||||
const model = normalizeModelRef(agent.model, defaultModel);
|
||||
|
||||
// 查找绑定的平台
|
||||
|
||||
@@ -134,3 +134,6 @@ body {
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
|
||||
+10
-16
@@ -1814,22 +1814,7 @@ export default function PixelOfficePage() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:hidden overflow-x-auto pb-1">
|
||||
{displayAgents.length === 0 ? (
|
||||
<div className="text-[var(--text-muted)] text-sm">{t('common.noData')}</div>
|
||||
) : (
|
||||
<div className="flex gap-2 min-w-full snap-x snap-mandatory">
|
||||
{mobileAgentPages.map((page, pageIndex) => (
|
||||
<div key={`mobile-agent-page-${pageIndex}`} className="grid grid-cols-3 grid-rows-3 gap-2 min-w-full h-[8.4rem] shrink-0 snap-start">
|
||||
{page.map((agent) => renderAgentChip(agent, true))}
|
||||
{page.length < 9 && Array.from({ length: 9 - page.length }).map((_, i) => (
|
||||
<div key={`mobile-agent-page-${pageIndex}-placeholder-${i}`} className="rounded-lg border border-transparent" />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Mobile agent list moved to canvas overlay below */}
|
||||
<div className="hidden md:flex gap-2 flex-1 flex-wrap">
|
||||
{displayAgents.map((agent) => renderAgentChip(agent))}
|
||||
{displayAgents.length === 0 && (
|
||||
@@ -1858,6 +1843,15 @@ export default function PixelOfficePage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile agent list overlay at bottom of canvas */}
|
||||
{isMobileViewport && (
|
||||
<div className="absolute bottom-0 left-0 right-0 z-10 px-2 pb-1 pt-1 bg-gradient-to-t from-black/60 to-transparent pointer-events-none">
|
||||
<div className="flex gap-1.5 overflow-x-auto no-scrollbar pointer-events-auto">
|
||||
{displayAgents.map((agent) => renderAgentChip(agent, true))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Broadcast notifications */}
|
||||
{broadcasts.length > 0 && (
|
||||
<div className="absolute top-3 left-1/2 -translate-x-1/2 z-10 flex flex-col gap-2 pointer-events-none">
|
||||
|
||||
@@ -399,6 +399,8 @@ interface GreetingSequence {
|
||||
isExit: boolean
|
||||
/** Tile to walk back to after farewell, before despawning */
|
||||
exitReturnPos: { col: number; row: number } | null
|
||||
/** True when child arrived at greeting tile but MainAgent wasn't physically present */
|
||||
parentAbsent?: boolean
|
||||
}
|
||||
|
||||
export class OfficeState {
|
||||
@@ -1091,6 +1093,14 @@ export class OfficeState {
|
||||
parent.state = CharacterState.IDLE
|
||||
child.dir = this.directionToward(child.tileCol, child.tileRow, parent.tileCol, parent.tileRow)
|
||||
parent.dir = this.directionToward(parent.tileCol, parent.tileRow, child.tileCol, child.tileRow)
|
||||
// Detect if parent is not physically nearby (wandered away from greeting tile)
|
||||
const distToParent = Math.abs(parent.tileCol - child.tileCol) + Math.abs(parent.tileRow - child.tileRow)
|
||||
if (seq.isExit && distToParent > 3) {
|
||||
seq.parentAbsent = true
|
||||
}
|
||||
} else {
|
||||
// Parent character doesn't exist
|
||||
if (seq.isExit) seq.parentAbsent = true
|
||||
}
|
||||
seq.phase = 'pause'
|
||||
seq.timer = 1.0
|
||||
@@ -1169,13 +1179,8 @@ export class OfficeState {
|
||||
|
||||
if (seq.isExit) {
|
||||
child.bubbleType = null
|
||||
const walkBack = this.findExitWalkPath(child, seq.exitReturnPos)
|
||||
if (walkBack) {
|
||||
child.path = walkBack.path
|
||||
child.state = CharacterState.WALK
|
||||
child.moveProgress = 0
|
||||
child.pendingDespawn = walkBack.target
|
||||
} else {
|
||||
// After farewell (whether MainAgent was present or not), walk to bottom-right corner
|
||||
if (!this.walkToBottomRightThenDespawn(child)) {
|
||||
child.matrixEffect = 'despawn'
|
||||
child.matrixEffectTimer = 0
|
||||
child.matrixEffectSeeds = matrixEffectSeeds()
|
||||
@@ -1231,6 +1236,38 @@ export class OfficeState {
|
||||
return this.findClosestWalkable(sofa.col, sofa.row)
|
||||
}
|
||||
|
||||
/** Find the walkable tile closest to the bottom-right corner of the map */
|
||||
private findBottomRightCornerTile(): { col: number; row: number } | null {
|
||||
if (this.walkableTiles.length === 0) return null
|
||||
// Bottom-right in tile space = max col + max row
|
||||
const maxCol = Math.max(...this.walkableTiles.map((t) => t.col))
|
||||
const maxRow = Math.max(...this.walkableTiles.map((t) => t.row))
|
||||
return this.findClosestWalkable(maxCol, maxRow)
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk character to the bottom-right corner then despawn.
|
||||
* Unlike findExitWalkPath, accepts any path length >= 1 so nearby agents still move.
|
||||
*/
|
||||
private walkToBottomRightThenDespawn(ch: Character): boolean {
|
||||
const corner = this.findBottomRightCornerTile()
|
||||
if (!corner) return false
|
||||
if (corner.col === ch.tileCol && corner.row === ch.tileRow) {
|
||||
// Already there — despawn directly
|
||||
ch.matrixEffect = 'despawn'
|
||||
ch.matrixEffectTimer = 0
|
||||
ch.matrixEffectSeeds = matrixEffectSeeds()
|
||||
return true
|
||||
}
|
||||
const path = findPath(ch.tileCol, ch.tileRow, corner.col, corner.row, this.tileMap, this.blockedTiles)
|
||||
if (path.length === 0) return false
|
||||
ch.path = path
|
||||
ch.state = CharacterState.WALK
|
||||
ch.moveProgress = 0
|
||||
ch.pendingDespawn = corner
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a walk-back path for a departing temp worker.
|
||||
* Ensures the path is long enough that the worker visibly walks away from MainAgent.
|
||||
@@ -1780,14 +1817,8 @@ export class OfficeState {
|
||||
// Stash the return position until the greeting actually starts
|
||||
if (exitReturnPos) this.exitReturnStash.set(id, exitReturnPos)
|
||||
} else {
|
||||
// No MainAgent reachable — walk back directly then despawn
|
||||
const walkBack = this.findExitWalkPath(ch, exitReturnPos)
|
||||
if (walkBack) {
|
||||
ch.pendingDespawn = walkBack.target
|
||||
ch.path = walkBack.path
|
||||
ch.state = CharacterState.WALK
|
||||
ch.moveProgress = 0
|
||||
} else {
|
||||
// No MainAgent reachable — walk to bottom-right corner then despawn
|
||||
if (!this.walkToBottomRightThenDespawn(ch)) {
|
||||
ch.matrixEffect = 'despawn'
|
||||
ch.matrixEffectTimer = 0
|
||||
ch.matrixEffectSeeds = matrixEffectSeeds()
|
||||
@@ -1854,9 +1885,12 @@ export class OfficeState {
|
||||
continue
|
||||
}
|
||||
}
|
||||
ch.matrixEffect = 'despawn'
|
||||
ch.matrixEffectTimer = 0
|
||||
ch.matrixEffectSeeds = matrixEffectSeeds()
|
||||
// No greeting path — walk to bottom-right corner then despawn
|
||||
if (!this.walkToBottomRightThenDespawn(ch)) {
|
||||
ch.matrixEffect = 'despawn'
|
||||
ch.matrixEffectTimer = 0
|
||||
ch.matrixEffectSeeds = matrixEffectSeeds()
|
||||
}
|
||||
}
|
||||
this.subagentMeta.delete(id)
|
||||
if (this.selectedAgentId === id) this.selectedAgentId = null
|
||||
@@ -2144,6 +2178,9 @@ export class OfficeState {
|
||||
if (dodgeTile) {
|
||||
ch.path = [dodgeTile]
|
||||
ch.moveProgress = 0
|
||||
// Face the dodge direction immediately so the character doesn't appear to
|
||||
// move backward while still facing forward.
|
||||
ch.dir = this.directionToward(ch.tileCol, ch.tileRow, dodgeTile.col, dodgeTile.row)
|
||||
ch.yieldDestination = { col: dest.col, row: dest.row }
|
||||
claimedNext.set(`${ch.tileCol},${ch.tileRow}`, ch.id)
|
||||
claimedNext.set(`${dodgeTile.col},${dodgeTile.row}`, ch.id)
|
||||
@@ -2192,6 +2229,8 @@ export class OfficeState {
|
||||
dodger.state = CharacterState.WALK
|
||||
dodger.frame = 0
|
||||
dodger.frameTimer = 0
|
||||
// Face the dodge direction immediately
|
||||
dodger.dir = this.directionToward(dodger.tileCol, dodger.tileRow, dodgeTile.col, dodgeTile.row)
|
||||
claimedNext.set(`${dodgeTile.col},${dodgeTile.row}`, dodger.id)
|
||||
occupiedKeys.delete(`${dodger.tileCol},${dodger.tileRow}`)
|
||||
occupiedKeys.add(`${dodgeTile.col},${dodgeTile.row}`)
|
||||
|
||||
@@ -594,7 +594,7 @@ export function renderScene(
|
||||
})
|
||||
}
|
||||
|
||||
// Task text bubble: scrolling marquee above the agent's head when working
|
||||
// Task text bubble: scrolling marquee above (or below if near top) the agent's head
|
||||
if (ch.taskText && ch.isActive && ch.state === CharacterState.TYPE && !ch.isSubagent) {
|
||||
const taskX = Math.round(offsetX + ch.x * zoom)
|
||||
const labelFontSize = Math.max(12, Math.round(5.25 * zoom))
|
||||
@@ -602,9 +602,13 @@ export function renderScene(
|
||||
const padX = 5 * zoom
|
||||
const padY = 3 * zoom
|
||||
const bubbleH = taskFontSize + padY * 2
|
||||
const bubbleW = Math.round(72 * zoom) // fixed width window
|
||||
const boxX = taskX - bubbleW / 2
|
||||
const boxY = drawY - 2 * zoom - labelFontSize - 4 * zoom - bubbleH - 4 * zoom
|
||||
const bubbleW = Math.round(72 * zoom)
|
||||
const boxX = Math.max(2, Math.min(taskX - bubbleW / 2, (ctx.canvas.width - bubbleW - 2)))
|
||||
const idealBoxY = drawY - 2 * zoom - labelFontSize - 4 * zoom - bubbleH - 4 * zoom
|
||||
// If bubble would be clipped by top edge, flip it below the character instead
|
||||
const belowY = drawY + cached.height + labelFontSize + 4 * zoom
|
||||
const tailUp = idealBoxY >= 4 // tail points down when bubble is above, up when below
|
||||
const boxY = idealBoxY >= 4 ? idealBoxY : belowY
|
||||
const fullText = ch.taskText
|
||||
drawables.push({
|
||||
zY: charZY + 0.2,
|
||||
@@ -612,30 +616,39 @@ export function renderScene(
|
||||
c.save()
|
||||
c.font = `${taskFontSize}px sans-serif`
|
||||
const fullW = c.measureText(fullText).width
|
||||
// Scroll speed: pixels per ms. Pause at start/end with a gap.
|
||||
const gap = bubbleW * 0.5
|
||||
const cycle = fullW + gap
|
||||
const speed = 30 // px/s at zoom=1, scaled below
|
||||
const speed = 30
|
||||
const scrollPx = ((Date.now() / 1000 * speed * zoom) % cycle)
|
||||
const textX = boxX + padX + (fullW > bubbleW - padX * 2 ? gap - scrollPx : 0)
|
||||
|
||||
// Bubble background
|
||||
const r = 3 * zoom
|
||||
const tailW = 5 * zoom
|
||||
c.beginPath()
|
||||
c.moveTo(boxX + r, boxY)
|
||||
c.lineTo(boxX + bubbleW - r, boxY)
|
||||
c.arcTo(boxX + bubbleW, boxY, boxX + bubbleW, boxY + r, r)
|
||||
c.lineTo(boxX + bubbleW, boxY + bubbleH - r)
|
||||
c.arcTo(boxX + bubbleW, boxY + bubbleH, boxX + bubbleW - r, boxY + bubbleH, r)
|
||||
// Tail pointing down
|
||||
const tailW = 5 * zoom
|
||||
c.lineTo(taskX + tailW, boxY + bubbleH)
|
||||
c.lineTo(taskX, boxY + bubbleH + 4 * zoom)
|
||||
c.lineTo(taskX - tailW, boxY + bubbleH)
|
||||
if (tailUp) {
|
||||
// Tail at bottom pointing down toward label
|
||||
c.lineTo(taskX + tailW, boxY + bubbleH)
|
||||
c.lineTo(taskX, boxY + bubbleH + 4 * zoom)
|
||||
c.lineTo(taskX - tailW, boxY + bubbleH)
|
||||
}
|
||||
c.lineTo(boxX + r, boxY + bubbleH)
|
||||
c.arcTo(boxX, boxY + bubbleH, boxX, boxY + bubbleH - r, r)
|
||||
c.lineTo(boxX, boxY + r)
|
||||
c.arcTo(boxX, boxY, boxX + r, boxY, r)
|
||||
if (!tailUp) {
|
||||
// Tail at top pointing up toward character
|
||||
c.lineTo(boxX, boxY + r)
|
||||
c.arcTo(boxX, boxY, boxX + r, boxY, r)
|
||||
c.lineTo(taskX - tailW, boxY)
|
||||
c.lineTo(taskX, boxY - 4 * zoom)
|
||||
c.lineTo(taskX + tailW, boxY)
|
||||
} else {
|
||||
c.lineTo(boxX, boxY + r)
|
||||
c.arcTo(boxX, boxY, boxX + r, boxY, r)
|
||||
}
|
||||
c.closePath()
|
||||
c.fillStyle = 'rgba(15,23,42,0.88)'
|
||||
c.fill()
|
||||
@@ -643,7 +656,6 @@ export function renderScene(
|
||||
c.lineWidth = zoom
|
||||
c.stroke()
|
||||
|
||||
// Clip to bubble interior and draw scrolling text
|
||||
c.beginPath()
|
||||
c.rect(boxX + padX, boxY, bubbleW - padX * 2, bubbleH)
|
||||
c.clip()
|
||||
|
||||
Reference in New Issue
Block a user