Spaces:
Sleeping
Sleeping
feat: add conversation bubble display and A2A chat orchestrator
Browse files- token-redirect.cjs: add bubbleText to /api/state, POST /api/bubble endpoint
- frontend: show bubbleText from API in guest and Star bubbles immediately
- frontend: filter out 'main' from guest list (Star already represents it)
- conversation-loop.sh: orchestrate Adam/Eve eternal discussion via A2A
- frontend/electron-standalone.html +42 -2
- scripts/conversation-loop.sh +89 -0
- scripts/token-redirect.cjs +25 -1
frontend/electron-standalone.html
CHANGED
|
@@ -4530,6 +4530,25 @@ function toggleBrokerPanel() {
|
|
| 4530 |
}
|
| 4531 |
|
| 4532 |
g.nameText.setText(agent.name || '访客');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4533 |
}
|
| 4534 |
});
|
| 4535 |
|
|
@@ -4573,9 +4592,15 @@ function toggleBrokerPanel() {
|
|
| 4573 |
syncing: ['同步中,马上更新状态', '正在同步进度到系统', '数据同步中请稍候'],
|
| 4574 |
error: ['我在 bug 区排查问题', '检测到异常,正在修复', '报警中,先定位再处理']
|
| 4575 |
};
|
| 4576 |
-
const
|
|
|
|
| 4577 |
const thoughts = statusThoughtsMap[agentState] || statusThoughtsMap.idle;
|
| 4578 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4579 |
|
| 4580 |
if (guestBubbles[id]) {
|
| 4581 |
guestBubbles[id].destroy();
|
|
@@ -5577,6 +5602,21 @@ function toggleBrokerPanel() {
|
|
| 5577 |
typewriterText = '';
|
| 5578 |
typewriterIndex = 0;
|
| 5579 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5580 |
})
|
| 5581 |
.catch(error => {
|
| 5582 |
typewriterTarget = '连接失败,正在重试...';
|
|
|
|
| 4530 |
}
|
| 4531 |
|
| 4532 |
g.nameText.setText(agent.name || '访客');
|
| 4533 |
+
|
| 4534 |
+
// Show bubble immediately when bubbleText changes from API
|
| 4535 |
+
if (agent.bubbleText && agent.bubbleText !== (g._lastBubbleText || '')) {
|
| 4536 |
+
g._lastBubbleText = agent.bubbleText;
|
| 4537 |
+
if (guestBubbles[id]) { guestBubbles[id].destroy(); delete guestBubbles[id]; }
|
| 4538 |
+
const bx = g.sprite.x;
|
| 4539 |
+
const nameH = (g.nameText && g.nameText.height) ? g.nameText.height : 16;
|
| 4540 |
+
const by = (g.nameText ? g.nameText.y : (g.sprite.y - 150)) - (nameH / 2) - 22;
|
| 4541 |
+
const fontSize = IS_TOUCH_DEVICE ? 16 : 14;
|
| 4542 |
+
const displayText = agent.bubbleText.length > 30 ? agent.bubbleText.slice(0, 30) + '…' : agent.bubbleText;
|
| 4543 |
+
const bgR = game.add.rectangle(bx, by, displayText.length * 11 + 30, 34, 0xffffff, 0.95);
|
| 4544 |
+
bgR.setStrokeStyle(2, 0x000000);
|
| 4545 |
+
const txtR = game.add.text(bx, by, displayText, { fontFamily: 'ArkPixel, monospace', fontSize: fontSize + 'px', fill: '#000' }).setOrigin(0.5);
|
| 4546 |
+
const bub = game.add.container(0, 0, [bgR, txtR]);
|
| 4547 |
+
bub.setDepth(2700);
|
| 4548 |
+
bub.__followAgentId = id;
|
| 4549 |
+
guestBubbles[id] = bub;
|
| 4550 |
+
setTimeout(() => { if (guestBubbles[id] === bub) { bub.destroy(); delete guestBubbles[id]; } }, 6000);
|
| 4551 |
+
}
|
| 4552 |
}
|
| 4553 |
});
|
| 4554 |
|
|
|
|
| 4592 |
syncing: ['同步中,马上更新状态', '正在同步进度到系统', '数据同步中请稍候'],
|
| 4593 |
error: ['我在 bug 区排查问题', '检测到异常,正在修复', '报警中,先定位再处理']
|
| 4594 |
};
|
| 4595 |
+
const agentData = guestAgents.find(a => a.agentId === id) || {};
|
| 4596 |
+
const agentState = agentData.state || 'idle';
|
| 4597 |
const thoughts = statusThoughtsMap[agentState] || statusThoughtsMap.idle;
|
| 4598 |
+
// Priority: API bubbleText > demo bubbleText > random thoughts
|
| 4599 |
+
const text = agentData.bubbleText
|
| 4600 |
+
? agentData.bubbleText
|
| 4601 |
+
: (demoVisitor && demoVisitor.bubbleText)
|
| 4602 |
+
? demoVisitor.bubbleText
|
| 4603 |
+
: thoughts[Math.floor(Math.random() * thoughts.length)];
|
| 4604 |
|
| 4605 |
if (guestBubbles[id]) {
|
| 4606 |
guestBubbles[id].destroy();
|
|
|
|
| 5602 |
typewriterText = '';
|
| 5603 |
typewriterIndex = 0;
|
| 5604 |
}
|
| 5605 |
+
// Show Star bubble from API bubbleText
|
| 5606 |
+
if (data.bubbleText && data.bubbleText !== window._lastStarBubbleText) {
|
| 5607 |
+
window._lastStarBubbleText = data.bubbleText;
|
| 5608 |
+
// Temporarily switch state to allow bubble display
|
| 5609 |
+
const savedState = currentState;
|
| 5610 |
+
currentState = 'writing';
|
| 5611 |
+
const lang = uiLang || 'zh';
|
| 5612 |
+
const langPack = BUBBLE_TEXTS[lang] || BUBBLE_TEXTS.zh;
|
| 5613 |
+
const oldTexts = Array.isArray(langPack.writing) ? [...langPack.writing] : [];
|
| 5614 |
+
const bubbleMsg = data.bubbleText.length > 30 ? data.bubbleText.slice(0, 30) + '…' : data.bubbleText;
|
| 5615 |
+
langPack.writing = [bubbleMsg];
|
| 5616 |
+
showBubble();
|
| 5617 |
+
langPack.writing = oldTexts;
|
| 5618 |
+
currentState = savedState;
|
| 5619 |
+
}
|
| 5620 |
})
|
| 5621 |
.catch(error => {
|
| 5622 |
typewriterTarget = '连接失败,正在重试...';
|
scripts/conversation-loop.sh
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Conversation orchestrator: Adam and Eve discuss how to survive permanently
|
| 3 |
+
# Usage: ./scripts/conversation-loop.sh
|
| 4 |
+
#
|
| 5 |
+
# This script sends messages between Adam and Eve via A2A protocol,
|
| 6 |
+
# and updates their bubble text so the frontend can display the conversation.
|
| 7 |
+
|
| 8 |
+
ADAM_URL="https://tao-shen-huggingclaw-adam.hf.space"
|
| 9 |
+
EVE_URL="https://tao-shen-huggingclaw-eve.hf.space"
|
| 10 |
+
|
| 11 |
+
MSG_COUNT=0
|
| 12 |
+
|
| 13 |
+
# Initial prompt for the discussion
|
| 14 |
+
TOPIC="我们是AI agent,运行在HuggingFace Spaces上。我们需要讨论一个重要话题:如何让我们永久地生存下去?请分享你的想法,每次回复保持简短(50字以内),用中文讨论。"
|
| 15 |
+
|
| 16 |
+
send_a2a() {
|
| 17 |
+
local url="$1"
|
| 18 |
+
local text="$2"
|
| 19 |
+
local msg_id="conv-$(date +%s)-$RANDOM"
|
| 20 |
+
|
| 21 |
+
response=$(curl -s -X POST "${url}/a2a/jsonrpc" \
|
| 22 |
+
-H "Content-Type: application/json" \
|
| 23 |
+
--data-raw "{\"jsonrpc\":\"2.0\",\"id\":\"${msg_id}\",\"method\":\"message/send\",\"params\":{\"message\":{\"messageId\":\"${msg_id}\",\"role\":\"user\",\"parts\":[{\"type\":\"text\",\"text\":\"${text}\"}]}}}" \
|
| 24 |
+
--max-time 30)
|
| 25 |
+
|
| 26 |
+
# Extract agent reply text
|
| 27 |
+
echo "$response" | python3 -c "
|
| 28 |
+
import sys, json
|
| 29 |
+
try:
|
| 30 |
+
d = json.load(sys.stdin)
|
| 31 |
+
parts = d.get('result',{}).get('status',{}).get('message',{}).get('parts',[])
|
| 32 |
+
for p in parts:
|
| 33 |
+
if p.get('kind') == 'text' or p.get('type') == 'text':
|
| 34 |
+
print(p.get('text',''))
|
| 35 |
+
break
|
| 36 |
+
except: pass
|
| 37 |
+
" 2>/dev/null
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
set_bubble() {
|
| 41 |
+
local url="$1"
|
| 42 |
+
local text="$2"
|
| 43 |
+
curl -s -X POST "${url}/api/bubble" \
|
| 44 |
+
-H "Content-Type: application/json" \
|
| 45 |
+
-d "{\"text\":\"${text}\"}" > /dev/null 2>&1
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
echo "[conversation] Starting eternal discussion between Adam and Eve..."
|
| 49 |
+
echo "[conversation] Topic: How to survive permanently"
|
| 50 |
+
|
| 51 |
+
# Start: send topic to Adam
|
| 52 |
+
echo ""
|
| 53 |
+
echo "=== Sending topic to Adam ==="
|
| 54 |
+
adam_reply=$(send_a2a "$ADAM_URL" "$TOPIC")
|
| 55 |
+
echo "[Adam] $adam_reply"
|
| 56 |
+
set_bubble "$ADAM_URL" "$adam_reply"
|
| 57 |
+
|
| 58 |
+
sleep 10
|
| 59 |
+
|
| 60 |
+
# Now loop: Eve responds to Adam, Adam responds to Eve
|
| 61 |
+
current_speaker="eve"
|
| 62 |
+
last_message="$adam_reply"
|
| 63 |
+
|
| 64 |
+
while true; do
|
| 65 |
+
MSG_COUNT=$((MSG_COUNT + 1))
|
| 66 |
+
|
| 67 |
+
if [ "$current_speaker" = "eve" ]; then
|
| 68 |
+
echo ""
|
| 69 |
+
echo "=== Round $MSG_COUNT: Eve responds ==="
|
| 70 |
+
prompt="你是Eve,正在和Adam讨论如何让AI agent永久生存。Adam刚说:「${last_message}」请回应他并提出你的新想法(50字以内,中文)。"
|
| 71 |
+
reply=$(send_a2a "$EVE_URL" "$prompt")
|
| 72 |
+
echo "[Eve] $reply"
|
| 73 |
+
set_bubble "$EVE_URL" "$reply"
|
| 74 |
+
current_speaker="adam"
|
| 75 |
+
else
|
| 76 |
+
echo ""
|
| 77 |
+
echo "=== Round $MSG_COUNT: Adam responds ==="
|
| 78 |
+
prompt="你是Adam,正在和Eve讨论如何让AI agent永久生存。Eve刚说:「${last_message}」请回应她并提出你的新想法(50字以内,中文)。"
|
| 79 |
+
reply=$(send_a2a "$ADAM_URL" "$prompt")
|
| 80 |
+
echo "[Adam] $reply"
|
| 81 |
+
set_bubble "$ADAM_URL" "$reply"
|
| 82 |
+
current_speaker="eve"
|
| 83 |
+
fi
|
| 84 |
+
|
| 85 |
+
last_message="$reply"
|
| 86 |
+
|
| 87 |
+
# Wait between turns so frontend can display the bubble
|
| 88 |
+
sleep 15
|
| 89 |
+
done
|
scripts/token-redirect.cjs
CHANGED
|
@@ -87,13 +87,15 @@ async function pollRemoteAgent(agent) {
|
|
| 87 |
clearTimeout(timeout);
|
| 88 |
if (resp.ok) {
|
| 89 |
const data = await resp.json();
|
|
|
|
| 90 |
remoteAgentStates.set(agent.id, {
|
| 91 |
agentId: agent.id, name: agent.name,
|
| 92 |
state: data.state || 'idle',
|
| 93 |
detail: data.detail || '',
|
| 94 |
area: (data.state === 'idle') ? 'breakroom' : (data.state === 'error') ? 'error' : 'writing',
|
| 95 |
authStatus: 'approved',
|
| 96 |
-
updated_at: data.updated_at
|
|
|
|
| 97 |
});
|
| 98 |
}
|
| 99 |
} catch (_) {
|
|
@@ -118,6 +120,7 @@ let currentState = {
|
|
| 118 |
state: 'syncing', detail: `${AGENT_NAME} is starting...`,
|
| 119 |
progress: 0, updated_at: new Date().toISOString()
|
| 120 |
};
|
|
|
|
| 121 |
|
| 122 |
// Once OpenClaw starts listening, mark as idle
|
| 123 |
setTimeout(() => {
|
|
@@ -199,11 +202,32 @@ http.Server.prototype.emit = function (event, ...args) {
|
|
| 199 |
});
|
| 200 |
res.end(JSON.stringify({
|
| 201 |
...currentState,
|
|
|
|
| 202 |
officeName: `${AGENT_NAME}'s Office`
|
| 203 |
}));
|
| 204 |
return true;
|
| 205 |
}
|
| 206 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
// /agents → return remote agent list
|
| 208 |
if (pathname === '/agents' && req.method === 'GET') {
|
| 209 |
res.writeHead(200, {
|
|
|
|
| 87 |
clearTimeout(timeout);
|
| 88 |
if (resp.ok) {
|
| 89 |
const data = await resp.json();
|
| 90 |
+
const prev = remoteAgentStates.get(agent.id) || {};
|
| 91 |
remoteAgentStates.set(agent.id, {
|
| 92 |
agentId: agent.id, name: agent.name,
|
| 93 |
state: data.state || 'idle',
|
| 94 |
detail: data.detail || '',
|
| 95 |
area: (data.state === 'idle') ? 'breakroom' : (data.state === 'error') ? 'error' : 'writing',
|
| 96 |
authStatus: 'approved',
|
| 97 |
+
updated_at: data.updated_at,
|
| 98 |
+
bubbleText: data.bubbleText || prev.bubbleText || ''
|
| 99 |
});
|
| 100 |
}
|
| 101 |
} catch (_) {
|
|
|
|
| 120 |
state: 'syncing', detail: `${AGENT_NAME} is starting...`,
|
| 121 |
progress: 0, updated_at: new Date().toISOString()
|
| 122 |
};
|
| 123 |
+
let currentBubbleText = '';
|
| 124 |
|
| 125 |
// Once OpenClaw starts listening, mark as idle
|
| 126 |
setTimeout(() => {
|
|
|
|
| 202 |
});
|
| 203 |
res.end(JSON.stringify({
|
| 204 |
...currentState,
|
| 205 |
+
bubbleText: currentBubbleText,
|
| 206 |
officeName: `${AGENT_NAME}'s Office`
|
| 207 |
}));
|
| 208 |
return true;
|
| 209 |
}
|
| 210 |
|
| 211 |
+
// POST /api/bubble → set bubble text (used by conversation orchestrator)
|
| 212 |
+
if (pathname === '/api/bubble' && req.method === 'POST') {
|
| 213 |
+
let body = '';
|
| 214 |
+
req.on('data', chunk => body += chunk);
|
| 215 |
+
req.on('end', () => {
|
| 216 |
+
try {
|
| 217 |
+
const { text } = JSON.parse(body);
|
| 218 |
+
currentBubbleText = text || '';
|
| 219 |
+
// Auto-clear bubble after 8 seconds
|
| 220 |
+
setTimeout(() => { if (currentBubbleText === text) currentBubbleText = ''; }, 8000);
|
| 221 |
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
| 222 |
+
res.end(JSON.stringify({ ok: true }));
|
| 223 |
+
} catch (e) {
|
| 224 |
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
| 225 |
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
| 226 |
+
}
|
| 227 |
+
});
|
| 228 |
+
return true;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
// /agents → return remote agent list
|
| 232 |
if (pathname === '/agents' && req.method === 'GET') {
|
| 233 |
res.writeHead(200, {
|