feat: add lead notification for new task creation and improve comment ordering
This commit is contained in:
@@ -21,9 +21,16 @@ from app.api.deps import (
|
|||||||
)
|
)
|
||||||
from app.core.auth import AuthContext
|
from app.core.auth import AuthContext
|
||||||
from app.db.session import engine, get_session
|
from app.db.session import engine, get_session
|
||||||
|
from app.integrations.openclaw_gateway import (
|
||||||
|
GatewayConfig as GatewayClientConfig,
|
||||||
|
OpenClawGatewayError,
|
||||||
|
ensure_session,
|
||||||
|
send_message,
|
||||||
|
)
|
||||||
from app.models.activity_events import ActivityEvent
|
from app.models.activity_events import ActivityEvent
|
||||||
from app.models.agents import Agent
|
from app.models.agents import Agent
|
||||||
from app.models.boards import Board
|
from app.models.boards import Board
|
||||||
|
from app.models.gateways import Gateway
|
||||||
from app.models.tasks import Task
|
from app.models.tasks import Task
|
||||||
from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskCreate, TaskRead, TaskUpdate
|
from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskCreate, TaskRead, TaskUpdate
|
||||||
from app.services.activity_log import record_activity
|
from app.services.activity_log import record_activity
|
||||||
@@ -124,6 +131,84 @@ def _serialize_comment(event: ActivityEvent) -> dict[str, object]:
|
|||||||
return TaskCommentRead.model_validate(event).model_dump(mode="json")
|
return TaskCommentRead.model_validate(event).model_dump(mode="json")
|
||||||
|
|
||||||
|
|
||||||
|
def _gateway_config(session: Session, board: Board) -> GatewayClientConfig | None:
|
||||||
|
if not board.gateway_id:
|
||||||
|
return None
|
||||||
|
gateway = session.get(Gateway, board.gateway_id)
|
||||||
|
if gateway is None or not gateway.url:
|
||||||
|
return None
|
||||||
|
return GatewayClientConfig(url=gateway.url, token=gateway.token)
|
||||||
|
|
||||||
|
|
||||||
|
async def _send_lead_task_message(
|
||||||
|
*,
|
||||||
|
session_key: str,
|
||||||
|
config: GatewayClientConfig,
|
||||||
|
message: str,
|
||||||
|
) -> None:
|
||||||
|
await ensure_session(session_key, config=config, label="Lead Agent")
|
||||||
|
await send_message(message, session_key=session_key, config=config, deliver=False)
|
||||||
|
|
||||||
|
|
||||||
|
def _notify_lead_on_task_create(
|
||||||
|
*,
|
||||||
|
session: Session,
|
||||||
|
board: Board,
|
||||||
|
task: Task,
|
||||||
|
) -> None:
|
||||||
|
lead = session.exec(
|
||||||
|
select(Agent)
|
||||||
|
.where(Agent.board_id == board.id)
|
||||||
|
.where(Agent.is_board_lead.is_(True))
|
||||||
|
).first()
|
||||||
|
if lead is None or not lead.openclaw_session_id:
|
||||||
|
return
|
||||||
|
config = _gateway_config(session, board)
|
||||||
|
if config is None:
|
||||||
|
return
|
||||||
|
description = (task.description or "").strip()
|
||||||
|
if len(description) > 500:
|
||||||
|
description = f"{description[:497]}..."
|
||||||
|
details = [
|
||||||
|
f"Board: {board.name}",
|
||||||
|
f"Task: {task.title}",
|
||||||
|
f"Task ID: {task.id}",
|
||||||
|
f"Status: {task.status}",
|
||||||
|
]
|
||||||
|
if description:
|
||||||
|
details.append(f"Description: {description}")
|
||||||
|
message = (
|
||||||
|
"NEW TASK ADDED\n"
|
||||||
|
+ "\n".join(details)
|
||||||
|
+ "\n\nTake action: triage, assign, or plan next steps."
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
asyncio.run(
|
||||||
|
_send_lead_task_message(
|
||||||
|
session_key=lead.openclaw_session_id,
|
||||||
|
config=config,
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
record_activity(
|
||||||
|
session,
|
||||||
|
event_type="task.lead_notified",
|
||||||
|
message=f"Lead agent notified for task: {task.title}.",
|
||||||
|
agent_id=lead.id,
|
||||||
|
task_id=task.id,
|
||||||
|
)
|
||||||
|
session.commit()
|
||||||
|
except OpenClawGatewayError as exc:
|
||||||
|
record_activity(
|
||||||
|
session,
|
||||||
|
event_type="task.lead_notify_failed",
|
||||||
|
message=f"Lead notify failed: {exc}",
|
||||||
|
agent_id=lead.id,
|
||||||
|
task_id=task.id,
|
||||||
|
)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/stream")
|
@router.get("/stream")
|
||||||
async def stream_tasks(
|
async def stream_tasks(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -214,6 +299,7 @@ def create_task(
|
|||||||
message=f"Task created: {task.title}.",
|
message=f"Task created: {task.title}.",
|
||||||
)
|
)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
_notify_lead_on_task_create(session=session, board=board, task=task)
|
||||||
return task
|
return task
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -358,6 +358,14 @@ export default function BoardDetailPage() {
|
|||||||
[tasks, assigneeById],
|
[tasks, assigneeById],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const orderedComments = useMemo(() => {
|
||||||
|
return [...comments].sort((a, b) => {
|
||||||
|
const aTime = new Date(a.created_at).getTime();
|
||||||
|
const bTime = new Date(b.created_at).getTime();
|
||||||
|
return bTime - aTime;
|
||||||
|
});
|
||||||
|
}, [comments]);
|
||||||
|
|
||||||
const boardAgents = useMemo(
|
const boardAgents = useMemo(
|
||||||
() => agents.filter((agent) => !boardId || agent.board_id === boardId),
|
() => agents.filter((agent) => !boardId || agent.board_id === boardId),
|
||||||
[agents, boardId],
|
[agents, boardId],
|
||||||
@@ -621,7 +629,7 @@ export default function BoardDetailPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
<aside
|
<aside
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed right-0 top-0 z-50 h-full w-[420px] max-w-[92vw] transform bg-white shadow-2xl transition-transform",
|
"fixed right-0 top-0 z-50 h-full w-[760px] max-w-[99vw] transform bg-white shadow-2xl transition-transform",
|
||||||
isDetailOpen ? "translate-x-0" : "translate-x-full",
|
isDetailOpen ? "translate-x-0" : "translate-x-full",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -648,7 +656,7 @@ export default function BoardDetailPage() {
|
|||||||
<p className="text-xs font-semibold uppercase tracking-wider text-slate-500">
|
<p className="text-xs font-semibold uppercase tracking-wider text-slate-500">
|
||||||
Description
|
Description
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-slate-700">
|
<p className="text-sm text-slate-700 whitespace-pre-wrap break-words">
|
||||||
{selectedTask?.description || "No description provided."}
|
{selectedTask?.description || "No description provided."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -666,7 +674,7 @@ export default function BoardDetailPage() {
|
|||||||
<p className="text-sm text-slate-500">No comments yet.</p>
|
<p className="text-sm text-slate-500">No comments yet.</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{comments.map((comment) => (
|
{orderedComments.map((comment) => (
|
||||||
<div
|
<div
|
||||||
key={comment.id}
|
key={comment.id}
|
||||||
className="rounded-xl border border-slate-200 bg-white p-3"
|
className="rounded-xl border border-slate-200 bg-white p-3"
|
||||||
@@ -681,20 +689,26 @@ export default function BoardDetailPage() {
|
|||||||
<span>{formatCommentTimestamp(comment.created_at)}</span>
|
<span>{formatCommentTimestamp(comment.created_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
{comment.message?.trim() ? (
|
{comment.message?.trim() ? (
|
||||||
<div className="mt-2 text-sm text-slate-900">
|
<div className="mt-2 text-sm text-slate-900 whitespace-pre-wrap break-words">
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
components={{
|
components={{
|
||||||
p: ({ ...props }) => (
|
p: ({ ...props }) => (
|
||||||
<p className="text-sm text-slate-900" {...props} />
|
<p
|
||||||
|
className="text-sm text-slate-900 whitespace-pre-wrap break-words"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
ul: ({ ...props }) => (
|
ul: ({ ...props }) => (
|
||||||
<ul
|
<ul
|
||||||
className="list-disc pl-5 text-sm text-slate-900"
|
className="list-disc pl-5 text-sm text-slate-900 whitespace-pre-wrap break-words"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
li: ({ ...props }) => (
|
li: ({ ...props }) => (
|
||||||
<li className="mb-1 text-sm text-slate-900" {...props} />
|
<li
|
||||||
|
className="mb-1 text-sm text-slate-900 whitespace-pre-wrap break-words"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
strong: ({ ...props }) => (
|
strong: ({ ...props }) => (
|
||||||
<strong
|
<strong
|
||||||
|
|||||||
Reference in New Issue
Block a user