feat(microdao-rooms): Add MicroDAO rooms creation/deletion and agent chat
Backend:
- POST /city/microdao/{slug}/rooms - create new room for MicroDAO
- DELETE /city/microdao/{slug}/rooms/{room_id} - soft-delete room
- POST /city/agents/{agent_id}/ensure-room - create personal agent room
Frontend:
- MicrodaoRoomsSection: Added create room modal with name, description, type
- MicrodaoRoomsSection: Added delete room functionality for managers
- Agent page: Added 'Поговорити' button to open chat in City Room
Models:
- Added CreateMicrodaoRoomRequest model
Task: TASK_PHASE_MICRODAO_ROOMS_AND_PUBLIC_CHAT_v3
This commit is contained in:
@@ -268,6 +268,26 @@ export default function AgentConsolePage() {
|
|||||||
|
|
||||||
{/* Status & Actions */}
|
{/* Status & Actions */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
{/* Chat Button */}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/city/agents/${agentId}/ensure-room`, { method: 'POST' });
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.room_slug) {
|
||||||
|
window.location.href = `/city/${data.room_slug}`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to open chat', e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="bg-cyan-600 hover:bg-cyan-500"
|
||||||
|
>
|
||||||
|
<MessageCircle className="w-4 h-4 mr-2" />
|
||||||
|
Поговорити
|
||||||
|
</Button>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className={`w-2 h-2 rounded-full ${
|
<span className={`w-2 h-2 rounded-full ${
|
||||||
profile?.status === 'online' ? 'bg-emerald-500' : 'bg-white/30'
|
profile?.status === 'online' ? 'bg-emerald-500' : 'bg-white/30'
|
||||||
|
|||||||
@@ -246,9 +246,12 @@ export default function MicrodaoDetailPage() {
|
|||||||
{/* Multi-Room Section with Chats (includes Team Chat) */}
|
{/* Multi-Room Section with Chats (includes Team Chat) */}
|
||||||
<MicrodaoRoomsSection
|
<MicrodaoRoomsSection
|
||||||
rooms={displayRooms}
|
rooms={displayRooms}
|
||||||
|
microdaoSlug={slug}
|
||||||
primaryRoomSlug={microdao.primary_city_room?.slug}
|
primaryRoomSlug={microdao.primary_city_room?.slug}
|
||||||
canManage={canManage}
|
canManage={canManage}
|
||||||
onEnsureOrchestratorRoom={handleEnsureOrchestratorRoom}
|
onEnsureOrchestratorRoom={handleEnsureOrchestratorRoom}
|
||||||
|
onRoomCreated={handleRoomUpdated}
|
||||||
|
onRoomDeleted={handleRoomUpdated}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* MicroDAO Agents Section - using new API */}
|
{/* MicroDAO Agents Section - using new API */}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { MessageCircle, Home, Users, FlaskConical, Shield, Gavel, Hash, Users2, Bot, PlusCircle, Crown } from "lucide-react";
|
import { MessageCircle, Home, Users, FlaskConical, Shield, Gavel, Hash, Users2, Bot, PlusCircle, Crown, Plus, Trash2, X, Loader2 } from "lucide-react";
|
||||||
import { CityRoomSummary } from "@/lib/types/microdao";
|
import { CityRoomSummary } from "@/lib/types/microdao";
|
||||||
import { CityChatWidget } from "@/components/city/CityChatWidget";
|
import { CityChatWidget } from "@/components/city/CityChatWidget";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -9,10 +9,13 @@ import { useState } from "react";
|
|||||||
|
|
||||||
interface MicrodaoRoomsSectionProps {
|
interface MicrodaoRoomsSectionProps {
|
||||||
rooms: CityRoomSummary[];
|
rooms: CityRoomSummary[];
|
||||||
|
microdaoSlug?: string;
|
||||||
primaryRoomSlug?: string | null;
|
primaryRoomSlug?: string | null;
|
||||||
showAllChats?: boolean;
|
showAllChats?: boolean;
|
||||||
canManage?: boolean;
|
canManage?: boolean;
|
||||||
onEnsureOrchestratorRoom?: () => Promise<void>;
|
onEnsureOrchestratorRoom?: () => Promise<void>;
|
||||||
|
onRoomCreated?: () => void;
|
||||||
|
onRoomDeleted?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ROLE_META: Record<string, { label: string; chipClass: string; icon: React.ReactNode }> = {
|
const ROLE_META: Record<string, { label: string; chipClass: string; icon: React.ReactNode }> = {
|
||||||
@@ -107,12 +110,22 @@ const ROLE_META: Record<string, { label: string; chipClass: string; icon: React.
|
|||||||
|
|
||||||
export function MicrodaoRoomsSection({
|
export function MicrodaoRoomsSection({
|
||||||
rooms,
|
rooms,
|
||||||
|
microdaoSlug,
|
||||||
primaryRoomSlug,
|
primaryRoomSlug,
|
||||||
showAllChats = false,
|
showAllChats = false,
|
||||||
canManage = false,
|
canManage = false,
|
||||||
onEnsureOrchestratorRoom
|
onEnsureOrchestratorRoom,
|
||||||
|
onRoomCreated,
|
||||||
|
onRoomDeleted
|
||||||
}: MicrodaoRoomsSectionProps) {
|
}: MicrodaoRoomsSectionProps) {
|
||||||
const [isCreatingTeam, setIsCreatingTeam] = useState(false);
|
const [isCreatingTeam, setIsCreatingTeam] = useState(false);
|
||||||
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
|
const [newRoomName, setNewRoomName] = useState("");
|
||||||
|
const [newRoomDescription, setNewRoomDescription] = useState("");
|
||||||
|
const [newRoomRole, setNewRoomRole] = useState("general");
|
||||||
|
const [newRoomIsPublic, setNewRoomIsPublic] = useState(true);
|
||||||
|
const [deletingRoomId, setDeletingRoomId] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleCreateTeam = async () => {
|
const handleCreateTeam = async () => {
|
||||||
if (!onEnsureOrchestratorRoom) return;
|
if (!onEnsureOrchestratorRoom) return;
|
||||||
@@ -126,6 +139,53 @@ export function MicrodaoRoomsSection({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCreateRoom = async () => {
|
||||||
|
if (!microdaoSlug || !newRoomName.trim()) return;
|
||||||
|
setIsCreating(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/city/microdao/${microdaoSlug}/rooms`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: newRoomName.trim(),
|
||||||
|
description: newRoomDescription.trim() || null,
|
||||||
|
room_role: newRoomRole,
|
||||||
|
is_public: newRoomIsPublic,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("Failed to create room");
|
||||||
|
setShowCreateModal(false);
|
||||||
|
setNewRoomName("");
|
||||||
|
setNewRoomDescription("");
|
||||||
|
setNewRoomRole("general");
|
||||||
|
setNewRoomIsPublic(true);
|
||||||
|
onRoomCreated?.();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to create room", e);
|
||||||
|
alert("Не вдалося створити кімнату");
|
||||||
|
} finally {
|
||||||
|
setIsCreating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteRoom = async (roomId: string, roomName: string) => {
|
||||||
|
if (!microdaoSlug) return;
|
||||||
|
if (!confirm(`Видалити кімнату "${roomName}"?`)) return;
|
||||||
|
setDeletingRoomId(roomId);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/city/microdao/${microdaoSlug}/rooms/${roomId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("Failed to delete room");
|
||||||
|
onRoomDeleted?.();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to delete room", e);
|
||||||
|
alert("Не вдалося видалити кімнату");
|
||||||
|
} finally {
|
||||||
|
setDeletingRoomId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!rooms || rooms.length === 0) {
|
if (!rooms || rooms.length === 0) {
|
||||||
return (
|
return (
|
||||||
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4">
|
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4">
|
||||||
@@ -213,14 +273,113 @@ export function MicrodaoRoomsSection({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Create Room Modal */}
|
||||||
|
{showCreateModal && (
|
||||||
|
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-slate-900 border border-slate-700 rounded-2xl p-6 w-full max-w-md space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-semibold text-white">Нова кімната</h3>
|
||||||
|
<button onClick={() => setShowCreateModal(false)} className="text-slate-400 hover:text-white">
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-slate-400 mb-1">Назва *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newRoomName}
|
||||||
|
onChange={(e) => setNewRoomName(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:border-cyan-500"
|
||||||
|
placeholder="Назва кімнати"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-slate-400 mb-1">Опис</label>
|
||||||
|
<textarea
|
||||||
|
value={newRoomDescription}
|
||||||
|
onChange={(e) => setNewRoomDescription(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:border-cyan-500 h-20 resize-none"
|
||||||
|
placeholder="Опис кімнати (опціонально)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-slate-400 mb-1">Тип кімнати</label>
|
||||||
|
<select
|
||||||
|
value={newRoomRole}
|
||||||
|
onChange={(e) => setNewRoomRole(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-cyan-500"
|
||||||
|
>
|
||||||
|
<option value="general">Загальна</option>
|
||||||
|
<option value="lobby">Лобі</option>
|
||||||
|
<option value="team">Команда</option>
|
||||||
|
<option value="research">Дослідження</option>
|
||||||
|
<option value="governance">Управління</option>
|
||||||
|
<option value="security">Безпека</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={newRoomIsPublic}
|
||||||
|
onChange={(e) => setNewRoomIsPublic(e.target.checked)}
|
||||||
|
className="w-4 h-4 rounded bg-slate-800 border-slate-600 text-cyan-500 focus:ring-cyan-500"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-slate-300">Публічна кімната</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3 pt-2">
|
||||||
|
<Button variant="outline" onClick={() => setShowCreateModal(false)}>
|
||||||
|
Скасувати
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleCreateRoom}
|
||||||
|
disabled={isCreating || !newRoomName.trim()}
|
||||||
|
className="bg-cyan-600 hover:bg-cyan-500"
|
||||||
|
>
|
||||||
|
{isCreating ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
Створення...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Створити
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* General Rooms Section */}
|
{/* General Rooms Section */}
|
||||||
<div className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-6">
|
<div className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-6">
|
||||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||||
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
|
<div className="flex items-center gap-3">
|
||||||
<MessageCircle className="w-5 h-5 text-cyan-400" />
|
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
|
||||||
Кімнати MicroDAO
|
<MessageCircle className="w-5 h-5 text-cyan-400" />
|
||||||
<span className="text-sm font-normal text-slate-500">({rooms.length})</span>
|
Кімнати MicroDAO
|
||||||
</h2>
|
<span className="text-sm font-normal text-slate-500">({rooms.length})</span>
|
||||||
|
</h2>
|
||||||
|
{canManage && microdaoSlug && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setShowCreateModal(true)}
|
||||||
|
className="border-cyan-500/50 text-cyan-400 hover:bg-cyan-500/10"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 mr-1" />
|
||||||
|
Нова кімната
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Mini-map */}
|
{/* Mini-map */}
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
|
|||||||
46
docs/tasks/TASK_PHASE_MICRODAO_ROOMS_AND_PUBLIC_CHAT_v3.md
Normal file
46
docs/tasks/TASK_PHASE_MICRODAO_ROOMS_AND_PUBLIC_CHAT_v3.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# TASK_PHASE_MICRODAO_ROOMS_AND_PUBLIC_CHAT_v3
|
||||||
|
|
||||||
|
Статус: IN_PROGRESS
|
||||||
|
Відповідальний агент: `Vector` (API) + `Canvas` (Frontend)
|
||||||
|
Ноди: NODE1 (prod API), NODE2 (dev DAGI)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Цілі фази
|
||||||
|
|
||||||
|
1. **MicroDAO Rooms**
|
||||||
|
- У кожного MicroDAO є власні кімнати (лоббі, воркруми тощо).
|
||||||
|
- На сторінці MicroDAO є секція "Кімнати MicroDAO" + кнопка `+ Нова кімната`.
|
||||||
|
- Можна створювати/видаляти кімнати MicroDAO (через API + UI).
|
||||||
|
|
||||||
|
2. **Публічний чат з DAARWIZZ**
|
||||||
|
- На головній сторінці є CTA "Поспілкуватися з DAARWIZZ".
|
||||||
|
- Клік → відкриває кімнату `DAARION City Lobby` (room slug: `city-lobby`).
|
||||||
|
|
||||||
|
3. **Чат із конкретним агентом**
|
||||||
|
- На сторінці агента є кнопка "Поговорити з агентом".
|
||||||
|
- Якщо agent має прив'язану кімнату, відкриваємо її; якщо ні — створюємо персональну.
|
||||||
|
|
||||||
|
4. **Фікс Banner для MicroDAO**
|
||||||
|
- Upload банера працює без помилок.
|
||||||
|
- Банер відображається в hero-секції MicroDAO.
|
||||||
|
|
||||||
|
5. **(Опційно) Crew / Teams**
|
||||||
|
- Для DAGI-агентів можна створювати "команди" (групові кімнати).
|
||||||
|
- У Agent Console з'являється фільтр за "Crew / Team".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Чекліст перед завершенням
|
||||||
|
|
||||||
|
* [ ] На `/microdao/{slug}` видно секцію "Кімнати MicroDAO".
|
||||||
|
* [ ] Працює створення/видалення кімнати MicroDAO.
|
||||||
|
* [ ] Відкриття кімнати MicroDAO веде в `/city/rooms/{room_slug}`.
|
||||||
|
* [ ] На `/` є CTA "Поспілкуватися з DAARWIZZ" → переходить в `city-lobby`.
|
||||||
|
* [ ] На сторінці агента є кнопка "Поговорити з агентом", яка:
|
||||||
|
* [ ] створює персональну кімнату, якщо її ще немає,
|
||||||
|
* [ ] відкриває відповідну кімнату в City Rooms.
|
||||||
|
* [ ] Upload банера працює (без помилок).
|
||||||
|
* [ ] Банер MicroDAO відображається в hero-секції.
|
||||||
|
* [ ] NODE1/NODE2 після перезапуску показують консистентні дані.
|
||||||
|
|
||||||
@@ -526,6 +526,15 @@ class AttachExistingRoomRequest(BaseModel):
|
|||||||
sort_order: int = 100
|
sort_order: int = 100
|
||||||
|
|
||||||
|
|
||||||
|
class CreateMicrodaoRoomRequest(BaseModel):
|
||||||
|
"""Request to create a new room for a MicroDAO"""
|
||||||
|
name: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
room_role: str = "general" # primary, lobby, team, research, governance, etc.
|
||||||
|
is_public: bool = True
|
||||||
|
zone_key: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class MicrodaoDetail(BaseModel):
|
class MicrodaoDetail(BaseModel):
|
||||||
"""Full MicroDAO detail view"""
|
"""Full MicroDAO detail view"""
|
||||||
id: str
|
id: str
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ from models_city import (
|
|||||||
NodeSwapperDetail,
|
NodeSwapperDetail,
|
||||||
CreateAgentRequest,
|
CreateAgentRequest,
|
||||||
CreateAgentResponse,
|
CreateAgentResponse,
|
||||||
DeleteAgentResponse
|
DeleteAgentResponse,
|
||||||
|
CreateMicrodaoRoomRequest
|
||||||
)
|
)
|
||||||
import repo_city
|
import repo_city
|
||||||
from common.redis_client import PresenceRedis, get_redis
|
from common.redis_client import PresenceRedis, get_redis
|
||||||
@@ -2652,6 +2653,73 @@ async def delete_agent(agent_id: str):
|
|||||||
raise HTTPException(status_code=500, detail=f"Failed to delete agent: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Failed to delete agent: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/agents/{agent_id}/ensure-room")
|
||||||
|
async def ensure_agent_room_endpoint(agent_id: str):
|
||||||
|
"""
|
||||||
|
Забезпечити існування персональної кімнати агента (Task v3).
|
||||||
|
Якщо кімнати немає - створює нову.
|
||||||
|
Повертає room_slug для переходу в чат.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
pool = await repo_city.get_pool()
|
||||||
|
|
||||||
|
# Get agent
|
||||||
|
agent = await pool.fetchrow("""
|
||||||
|
SELECT id, display_name, primary_room_slug, district
|
||||||
|
FROM agents
|
||||||
|
WHERE id = $1 AND deleted_at IS NULL
|
||||||
|
""", agent_id)
|
||||||
|
|
||||||
|
if not agent:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
||||||
|
|
||||||
|
# If agent already has a room, return it
|
||||||
|
if agent["primary_room_slug"]:
|
||||||
|
return {"room_slug": agent["primary_room_slug"], "created": False}
|
||||||
|
|
||||||
|
# Create personal room for agent
|
||||||
|
import re
|
||||||
|
room_slug = f"agent-{re.sub(r'[^a-z0-9]+', '-', agent_id.lower()).strip('-')}"
|
||||||
|
|
||||||
|
# Check if slug exists
|
||||||
|
existing = await pool.fetchrow("SELECT id FROM city_rooms WHERE slug = $1", room_slug)
|
||||||
|
if existing:
|
||||||
|
room_slug = f"{room_slug}-{str(uuid.uuid4())[:8]}"
|
||||||
|
|
||||||
|
# Create room
|
||||||
|
await pool.execute("""
|
||||||
|
INSERT INTO city_rooms (
|
||||||
|
slug, name, description, owner_type, owner_id,
|
||||||
|
room_type, room_role, is_public, zone, space_scope
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, 'agent', $4,
|
||||||
|
'agent', 'personal', FALSE, $5, 'personal'
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
room_slug,
|
||||||
|
f"Чат з {agent['display_name']}",
|
||||||
|
f"Персональна кімната агента {agent['display_name']}",
|
||||||
|
agent_id,
|
||||||
|
agent.get("district") or "agents"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update agent with room_slug
|
||||||
|
await pool.execute("""
|
||||||
|
UPDATE agents SET primary_room_slug = $1, updated_at = NOW()
|
||||||
|
WHERE id = $2
|
||||||
|
""", room_slug, agent_id)
|
||||||
|
|
||||||
|
logger.info(f"Created personal room {room_slug} for agent {agent_id}")
|
||||||
|
|
||||||
|
return {"room_slug": room_slug, "created": True}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to ensure room for agent {agent_id}: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to ensure room: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/agents/online", response_model=List[AgentPresence])
|
@router.get("/agents/online", response_model=List[AgentPresence])
|
||||||
async def get_online_agents():
|
async def get_online_agents():
|
||||||
"""
|
"""
|
||||||
@@ -2944,6 +3012,109 @@ async def get_microdao_rooms_endpoint(slug: str):
|
|||||||
raise HTTPException(status_code=500, detail="Failed to get microdao rooms")
|
raise HTTPException(status_code=500, detail="Failed to get microdao rooms")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/microdao/{slug}/rooms", response_model=CityRoomSummary)
|
||||||
|
async def create_microdao_room_endpoint(slug: str, payload: CreateMicrodaoRoomRequest):
|
||||||
|
"""
|
||||||
|
Створити нову кімнату для MicroDAO (Task v3).
|
||||||
|
Створює city_room та прив'язує до MicroDAO.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get microdao by slug
|
||||||
|
dao = await repo_city.get_microdao_by_slug(slug)
|
||||||
|
if not dao:
|
||||||
|
raise HTTPException(status_code=404, detail=f"MicroDAO not found: {slug}")
|
||||||
|
|
||||||
|
pool = await repo_city.get_pool()
|
||||||
|
|
||||||
|
# Generate slug from name
|
||||||
|
import re
|
||||||
|
room_slug = re.sub(r'[^a-z0-9]+', '-', payload.name.lower()).strip('-')
|
||||||
|
room_slug = f"{slug}-{room_slug}"
|
||||||
|
|
||||||
|
# Check if slug already exists
|
||||||
|
existing = await pool.fetchrow("SELECT id FROM city_rooms WHERE slug = $1", room_slug)
|
||||||
|
if existing:
|
||||||
|
room_slug = f"{room_slug}-{str(uuid.uuid4())[:8]}"
|
||||||
|
|
||||||
|
# Create room in city_rooms
|
||||||
|
row = await pool.fetchrow("""
|
||||||
|
INSERT INTO city_rooms (
|
||||||
|
slug, name, description, owner_type, owner_id,
|
||||||
|
room_type, room_role, is_public, zone, space_scope
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, 'microdao', $4,
|
||||||
|
'microdao', $5, $6, $7, 'microdao'
|
||||||
|
)
|
||||||
|
RETURNING id, slug, name, description, room_role, is_public, zone
|
||||||
|
""",
|
||||||
|
room_slug,
|
||||||
|
payload.name,
|
||||||
|
payload.description,
|
||||||
|
dao["id"],
|
||||||
|
payload.room_role,
|
||||||
|
payload.is_public,
|
||||||
|
payload.zone_key
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Created room {room_slug} for MicroDAO {slug}")
|
||||||
|
|
||||||
|
return CityRoomSummary(
|
||||||
|
id=str(row["id"]),
|
||||||
|
slug=row["slug"],
|
||||||
|
name=row["name"],
|
||||||
|
microdao_id=dao["id"],
|
||||||
|
microdao_slug=slug,
|
||||||
|
room_role=row["room_role"],
|
||||||
|
is_public=row["is_public"],
|
||||||
|
sort_order=100
|
||||||
|
)
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to create room for microdao {slug}: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to create room: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/microdao/{slug}/rooms/{room_id}")
|
||||||
|
async def delete_microdao_room_endpoint(slug: str, room_id: str):
|
||||||
|
"""
|
||||||
|
Видалити кімнату MicroDAO (Task v3).
|
||||||
|
Soft-delete: встановлює deleted_at.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get microdao by slug
|
||||||
|
dao = await repo_city.get_microdao_by_slug(slug)
|
||||||
|
if not dao:
|
||||||
|
raise HTTPException(status_code=404, detail=f"MicroDAO not found: {slug}")
|
||||||
|
|
||||||
|
pool = await repo_city.get_pool()
|
||||||
|
|
||||||
|
# Check if room belongs to this microdao
|
||||||
|
room = await pool.fetchrow("""
|
||||||
|
SELECT id, slug FROM city_rooms
|
||||||
|
WHERE id = $1 AND owner_id = $2 AND owner_type = 'microdao'
|
||||||
|
""", room_id, dao["id"])
|
||||||
|
|
||||||
|
if not room:
|
||||||
|
raise HTTPException(status_code=404, detail="Room not found or not owned by this MicroDAO")
|
||||||
|
|
||||||
|
# Soft delete
|
||||||
|
await pool.execute("""
|
||||||
|
UPDATE city_rooms
|
||||||
|
SET deleted_at = NOW(), updated_at = NOW()
|
||||||
|
WHERE id = $1
|
||||||
|
""", room_id)
|
||||||
|
|
||||||
|
logger.info(f"Deleted room {room['slug']} from MicroDAO {slug}")
|
||||||
|
|
||||||
|
return {"ok": True, "message": f"Room '{room['slug']}' deleted"}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to delete room {room_id} from microdao {slug}: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to delete room: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/microdao/{slug}/agents")
|
@router.get("/microdao/{slug}/agents")
|
||||||
async def get_microdao_agents_endpoint(slug: str):
|
async def get_microdao_agents_endpoint(slug: str):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user