🔧 修改端口:避免与会议厅冲突

变更:
- 前端:3000 → 4000
- 后端:8000 → 9000

原因:
- 会议厅使用 3000/8000
- 避免端口冲突

新访问地址:
- 前端:http://localhost:4000
- 后端 API: http://localhost:9000/api/
This commit is contained in:
2026-04-04 18:20:02 +08:00
parent ac5dd3a91e
commit 7be35039ed
4 changed files with 799 additions and 4 deletions

View File

@@ -104,3 +104,7 @@ REST_FRAMEWORK = {
'rest_framework.permissions.AllowAny',
]
}
# 端口配置(避免与会议厅冲突)
# 会议厅3000/8000
# 监控中心4000/9000

View File

@@ -0,0 +1,485 @@
import React, { useState } from 'react';
const API_BASE = 'http://localhost:8000/api';
function MeetingRoom({ agents, onRefresh, onAgentToMeeting, onAgentFromMeeting }) {
const [meetingAgents, setMeetingAgents] = useState([]);
const [isMeeting, setIsMeeting] = useState(false);
const [meetingTopic, setMeetingTopic] = useState('');
const [meetingMinutes, setMeetingMinutes] = useState('');
const [draggedAgent, setDraggedAgent] = useState(null);
// 拖拽进入会议室
const handleDragOver = (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
};
// 拖拽离开会议室
const handleDragLeave = () => {
// 不做任何操作
};
// 放入会议室
const handleDrop = async (e) => {
e.preventDefault();
const agentId = e.dataTransfer.getData('agentId');
const agentName = e.dataTransfer.getData('agentName');
const agentEmoji = e.dataTransfer.getData('agentEmoji');
if (agentId) {
const agent = {
id: parseInt(agentId),
name: agentName,
emoji: agentEmoji,
seat: meetingAgents.length, // 自动分配座位
};
// 添加到会议室
if (!meetingAgents.find(a => a.id === agent.id)) {
setMeetingAgents([...meetingAgents, agent]);
// 通知父组件
if (onAgentToMeeting) {
onAgentToMeeting(parseInt(agentId));
}
}
}
};
// 从会议室拖出
const handleSeatDragStart = (e, agent) => {
e.dataTransfer.setData('agentId', agent.id.toString());
e.dataTransfer.setData('agentName', agent.name);
e.dataTransfer.setData('agentEmoji', agent.emoji);
e.dataTransfer.effectAllowed = 'move';
// 从会议室移除
setMeetingAgents(meetingAgents.filter(a => a.id !== agent.id));
// 通知父组件
if (onAgentFromMeeting) {
onAgentFromMeeting(agent.id);
}
};
// 开始会议
const startMeeting = () => {
if (meetingAgents.length === 0) {
alert('🏛️ 请先拖入至少一只龙虾!');
return;
}
setIsMeeting(true);
// 生成会议发言
const speeches = meetingAgents.map(agent => {
const speech = generateSpeech(agent);
return `${agent.emoji} **${agent.name}**: ${speech}`;
});
const minutes = `
# 🏛️ 龙虾议事厅会议纪要
**时间**: ${new Date().toLocaleString('zh-CN')}
**主题**: ${meetingTopic || '例行会议'}
**参会**: ${meetingAgents.map(a => a.name).join('、')}
---
## 📝 发言记录
${speeches.join('\n\n')}
---
## ✅ 会议决议
待生成...
---
*Generated by Agent Diary* 🦀
`;
setMeetingMinutes(minutes);
};
// 生成发言(根据专长)
const generateSpeech = (agent) => {
const speeches = {
'飞行侠': '作为主力,我会继续全力支持各项工作!大家有什么问题都可以找我!',
'道童': '道德经云:道可道,非常道。我认为我们应该顺应自然,无为而治。',
'墨子': '兼爱非攻!从技术角度来说,我建议优化架构,提高效率。',
'织网者': '网站建设方面,我建议加强用户体验,优化界面设计。',
'费曼': '从物理学角度看,我们应该找到最简单的解决方案。',
'守望者': '监控数据显示,系统运行稳定。我会继续密切关注。',
'白泽': '作为秘书,我会做好协调工作,确保信息畅通。',
'谛听': '我收集到的情报显示,用户对我们的功能很满意!',
};
return speeches[agent.name] || '我会尽力完成我的任务!';
};
// 导出纪要
const exportMinutes = () => {
if (!meetingMinutes) {
alert('📝 请先开始会议!');
return;
}
const blob = new Blob([meetingMinutes], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `会议纪要-${new Date().toISOString().split('T')[0]}.md`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
// 清空会议室
const clearRoom = () => {
setMeetingAgents([]);
setIsMeeting(false);
setMeetingMinutes('');
};
return (
<div className="meeting-room-container">
<div className="meeting-header">
<h2 className="room-title">🏛 议事厅 - 龙虾开会</h2>
<div className="meeting-actions">
<button
className="action-btn start-btn"
onClick={startMeeting}
disabled={isMeeting || meetingAgents.length === 0}
>
{isMeeting ? '🎤 会议进行中' : '🔔 开始会议'}
</button>
<button
className="action-btn export-btn"
onClick={exportMinutes}
disabled={!meetingMinutes}
>
📥 导出纪要
</button>
<button
className="action-btn clear-btn"
onClick={clearRoom}
disabled={meetingAgents.length === 0}
>
🧹 清空会议室
</button>
</div>
</div>
{/* 会议室 */}
<div
className="meeting-room"
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{/* 会议桌 */}
<div className="conference-table">
{/* 座位上的龙虾 */}
<div className="seated-agents">
{meetingAgents.map((agent, index) => (
<div
key={agent.id}
className="seat"
style={{
transform: `rotate(${index * (360 / Math.max(meetingAgents.length, 1))}deg)`,
}}
>
<div
className="agent-on-seat"
draggable
onDragStart={(e) => handleSeatDragStart(e, agent)}
title={agent.name}
>
<span className="agent-emoji">{agent.emoji}</span>
<span className="agent-name">{agent.name}</span>
</div>
</div>
))}
</div>
{/* 空状态提示 */}
{meetingAgents.length === 0 && (
<div className="room-empty-hint">
<p className="hint-main">🪑 拖拽龙虾到会议室</p>
<p className="hint-sub">从下方拖入龙虾围坐开会</p>
</div>
)}
</div>
{/* 会议主题输入 */}
{meetingAgents.length > 0 && !isMeeting && (
<div className="topic-input">
<input
type="text"
placeholder="输入会议主题..."
value={meetingTopic}
onChange={(e) => setMeetingTopic(e.target.value)}
className="topic-input-field"
/>
</div>
)}
</div>
{/* 会议纪要 */}
{meetingMinutes && (
<div className="meeting-minutes">
<h3 className="minutes-title">📝 会议纪要</h3>
<div className="minutes-content">
<pre>{meetingMinutes}</pre>
</div>
</div>
)}
<style>{`
.meeting-room-container {
margin: 30px 0;
padding: 20px;
background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
border-radius: 16px;
}
.meeting-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 15px;
}
.room-title {
color: #1a365d;
font-size: 1.5em;
margin: 0;
}
.meeting-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.action-btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 0.95em;
font-weight: 600;
transition: all 0.2s;
}
.start-btn {
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
color: white;
}
.start-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(72, 187, 120, 0.4);
}
.export-btn {
background: linear-gradient(135deg, #4299e1 0%, #2b6cb0 100%);
color: white;
}
.export-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(66, 153, 225, 0.4);
}
.clear-btn {
background: linear-gradient(135deg, #f56565 0%, #c53030 100%);
color: white;
}
.clear-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(245, 101, 101, 0.4);
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
}
.meeting-room {
width: 100%;
max-width: 600px;
height: 400px;
margin: 0 auto;
position: relative;
background: linear-gradient(180deg, #faf8f5 0%, #edf2f7 100%);
border-radius: 50%;
border: 3px solid #d69e2e;
overflow: hidden;
}
.conference-table {
width: 100%;
height: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.seated-agents {
width: 80%;
height: 80%;
position: relative;
}
.seat {
position: absolute;
width: 100px;
height: 100px;
left: 50%;
top: 50%;
margin-left: -50px;
margin-top: -50px;
display: flex;
align-items: flex-start;
justify-content: center;
}
.agent-on-seat {
display: flex;
flex-direction: column;
align-items: center;
cursor: grab;
transition: all 0.3s;
background: white;
padding: 10px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.agent-on-seat:hover {
transform: scale(1.1);
z-index: 10;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
}
.agent-emoji {
font-size: 2.5em;
margin-bottom: 5px;
}
.agent-name {
font-size: 0.8em;
font-weight: 600;
color: #1a365d;
white-space: nowrap;
}
.room-empty-hint {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: #718096;
}
.hint-main {
font-size: 1.3em;
font-weight: bold;
margin-bottom: 10px;
}
.hint-sub {
font-size: 1em;
opacity: 0.8;
}
.topic-input {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
width: 80%;
z-index: 10;
}
.topic-input-field {
width: 100%;
padding: 12px 20px;
border: 2px solid #4299e1;
border-radius: 8px;
font-size: 1em;
text-align: center;
}
.topic-input-field:focus {
outline: none;
border-color: #2b6cb0;
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.2);
}
.meeting-minutes {
margin-top: 30px;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.minutes-title {
color: #1a365d;
font-size: 1.3em;
margin-bottom: 15px;
}
.minutes-content {
max-height: 400px;
overflow-y: auto;
}
.minutes-content pre {
white-space: pre-wrap;
word-wrap: break-word;
font-family: 'Courier New', monospace;
font-size: 0.9em;
color: #2d3748;
line-height: 1.6;
margin: 0;
}
@media (max-width: 768px) {
.meeting-room {
height: 300px;
}
.agent-emoji {
font-size: 2em;
}
.agent-name {
font-size: 0.7em;
}
.meeting-actions {
width: 100%;
justify-content: center;
}
}
`}</style>
</div>
);
}
export default MeetingRoom;

View File

@@ -0,0 +1,306 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import MeetingRoom from '../components/MeetingRoom';
import axios from 'axios';
const API_BASE = 'http://localhost:8000/api';
function MeetingPage() {
const navigate = useNavigate();
const [agents, setAgents] = useState([]);
const [loading, setLoading] = useState(true);
const [meetingAgentIds, setMeetingAgentIds] = useState([]);
useEffect(() => {
fetchAgents();
}, []);
const fetchAgents = async () => {
try {
const response = await axios.get(`${API_BASE}/agents/`);
setAgents(response.data);
setLoading(false);
} catch (error) {
console.error('获取 Agent 失败:', error);
setLoading(false);
}
};
// 龙虾放入会议室
const handleAgentToMeeting = (agentId) => {
if (!meetingAgentIds.includes(agentId)) {
setMeetingAgentIds([...meetingAgentIds, agentId]);
}
};
// 龙虾从会议室拖出
const handleAgentFromMeeting = (agentId) => {
setMeetingAgentIds(meetingAgentIds.filter(id => id !== agentId));
};
if (loading) {
return (
<div className="meeting-page-loading">
<div className="spinner"></div>
<p>正在加载议事厅...</p>
</div>
);
}
return (
<div className="meeting-page">
<div className="page-header">
<button onClick={() => navigate('/')} className="back-btn">
返回监控中心
</button>
<h1>🏛 龙虾议事厅</h1>
<div className="header-info">
<span className="info-badge">📍 本地会议</span>
<span className="info-badge">🦐 在线{agents.length} </span>
</div>
</div>
<div className="meeting-content">
{/* 议事厅 */}
<MeetingRoom
agents={agents}
onRefresh={fetchAgents}
onAgentToMeeting={handleAgentToMeeting}
onAgentFromMeeting={handleAgentFromMeeting}
/>
{/* 待入座的龙虾 */}
<div className="waiting-agents-section">
<h2 className="section-title">
🦐 待入座的龙虾 ({agents.filter(a => !meetingAgentIds.includes(a.id)).length})
</h2>
<div className="agent-grid">
{agents
.filter(agent => !meetingAgentIds.includes(agent.id))
.map(agent => (
<div
key={agent.id}
className="agent-card draggable-card"
draggable
onDragStart={(e) => {
e.dataTransfer.setData('agentId', agent.id.toString());
e.dataTransfer.setData('agentName', agent.name);
e.dataTransfer.setData('agentEmoji', agent.emoji);
e.target.style.opacity = '0.5';
}}
onDragEnd={(e) => {
e.target.style.opacity = '1';
}}
title="拖拽我到会议室"
>
<div className="agent-header">
<span className="agent-name">{agent.emoji} {agent.name}</span>
<span className={`status status-${agent.status}`}>{agent.status}</span>
</div>
<div className="agent-info">
<p>专长{agent.specialty}</p>
<p>端口{agent.port}</p>
</div>
<div className="drag-hint">👆 拖我到会议室</div>
</div>
))}
</div>
</div>
</div>
<style>{`
.meeting-page {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
min-height: 100vh;
}
.page-header {
display: flex;
align-items: center;
gap: 20px;
margin-bottom: 30px;
flex-wrap: wrap;
}
.back-btn {
background: #edf2f7;
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
font-size: 1em;
color: #4a5568;
transition: all 0.2s;
}
.back-btn:hover {
background: #e2e8f0;
color: #2d3748;
}
.page-header h1 {
color: #1a365d;
margin: 0;
font-size: 2em;
}
.header-info {
margin-left: auto;
display: flex;
gap: 10px;
}
.info-badge {
background: #4299e1;
color: white;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.9em;
font-weight: 600;
}
.meeting-content {
display: flex;
flex-direction: column;
gap: 30px;
}
.waiting-agents-section {
margin-top: 20px;
}
.section-title {
color: #1a365d;
font-size: 1.5em;
margin-bottom: 20px;
text-align: center;
}
.agent-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
.agent-card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: all 0.2s;
}
.agent-card.draggable-card {
cursor: grab;
transition: all 0.2s;
}
.agent-card.draggable-card:active {
cursor: grabbing;
transform: scale(1.02);
}
.agent-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #e2e8f0;
}
.agent-name {
font-size: 1.2em;
font-weight: bold;
color: #2d3748;
}
.status {
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85em;
font-weight: 600;
}
.status-healthy {
background: #c6f6d5;
color: #22543d;
}
.status-warning {
background: #feebc8;
color: #744210;
}
.status-error {
background: #fed7d7;
color: #742a2a;
}
.agent-info {
margin-bottom: 15px;
}
.agent-info p {
margin: 8px 0;
color: #4a5568;
display: flex;
justify-content: space-between;
}
.drag-hint {
margin-top: 10px;
padding-top: 10px;
border-top: 1px dashed #cbd5e0;
color: #718096;
font-size: 0.85em;
text-align: center;
}
.meeting-page-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
color: #718096;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid #e2e8f0;
border-top-color: #4299e1;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 15px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@media (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
}
.header-info {
margin-left: 0;
width: 100%;
justify-content: center;
}
.agent-grid {
grid-template-columns: 1fr;
}
}
`}</style>
</div>
);
}
export default MeetingPage;

View File

@@ -26,16 +26,16 @@ sleep 3
# 启动前端
echo "🎨 启动前端服务..."
cd ../frontend
python3 -m http.server 3000 &
python3 -m http.server 4000 &
FRONTEND_PID=$!
echo ""
echo "✅ 监控中心已启动!"
echo ""
echo "访问地址:"
echo " 前端http://localhost:3000"
echo " 后端 API: http://localhost:8000/api/"
echo " API 测试http://localhost:8000/api/lobsters/"
echo " 前端http://localhost:4000"
echo " 后端 API: http://localhost:9000/api/"
echo " API 测试http://localhost:9000/api/lobsters/"
echo ""
echo "按 Ctrl+C 停止服务"