refactor: UUID-based member identification + task audit log + soft delete
Some checks failed
Deploy Tracker / deploy (push) Failing after 2s
Some checks failed
Deploy Tracker / deploy (push) Failing after 2s
- Member: добавлен is_active для soft delete - Message: author_id (UUID FK) вместо author_slug - Task: assignee_id, reviewer_id (UUID FK), watcher_ids (UUID[]) - Новая модель TaskAction для аудита действий с задачами - Новый enum TaskActionType - API обновлен под новые поля с relationships - WS handler использует author_id вместо author_slug - Soft delete для members через is_active=False - Автоматическое создание TaskAction записей при изменениях
This commit is contained in:
parent
daf3f06dcb
commit
41f4bc5ebc
@ -3,7 +3,7 @@
|
||||
import secrets
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@ -96,10 +96,16 @@ def _member_to_out(m: Member) -> dict:
|
||||
# --- Endpoints ---
|
||||
|
||||
@router.get("/members", response_model=list[MemberOut])
|
||||
async def list_members(db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(
|
||||
select(Member).options(selectinload(Member.agent_config))
|
||||
)
|
||||
async def list_members(
|
||||
include_inactive: bool = Query(False),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
query = select(Member).options(selectinload(Member.agent_config))
|
||||
|
||||
if not include_inactive:
|
||||
query = query.where(Member.is_active == True)
|
||||
|
||||
result = await db.execute(query)
|
||||
return [_member_to_out(m) for m in result.scalars()]
|
||||
|
||||
|
||||
@ -237,3 +243,16 @@ async def update_my_status(status: str, db: AsyncSession = Depends(get_db)):
|
||||
"""Quick status update (used by agents)."""
|
||||
# TODO: get current member from auth
|
||||
return {"status": status}
|
||||
|
||||
|
||||
@router.delete("/members/{slug}")
|
||||
async def delete_member(slug: str, db: AsyncSession = Depends(get_db)):
|
||||
"""Soft delete member by setting is_active=False."""
|
||||
result = await db.execute(select(Member).where(Member.slug == slug))
|
||||
member = result.scalar_one_or_none()
|
||||
if not member:
|
||||
raise HTTPException(404, "Member not found")
|
||||
|
||||
member.is_active = False
|
||||
await db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
@ -31,7 +31,8 @@ class MessageOut(BaseModel):
|
||||
task_id: str | None = None
|
||||
parent_id: str | None = None
|
||||
author_type: str
|
||||
author_slug: str
|
||||
author_id: str
|
||||
author: dict | None = None # Member object for display
|
||||
content: str
|
||||
mentions: list[str] = []
|
||||
voice_url: str | None = None
|
||||
@ -44,7 +45,7 @@ class MessageCreate(BaseModel):
|
||||
task_id: str | None = None
|
||||
parent_id: str | None = None
|
||||
author_type: str = AuthorType.HUMAN
|
||||
author_slug: str = "admin"
|
||||
author_id: str
|
||||
content: str
|
||||
mentions: list[str] = []
|
||||
voice_url: str | None = None
|
||||
@ -59,7 +60,8 @@ def _message_out(m: Message) -> dict:
|
||||
"task_id": str(m.task_id) if m.task_id else None,
|
||||
"parent_id": str(m.parent_id) if m.parent_id else None,
|
||||
"author_type": m.author_type,
|
||||
"author_slug": m.author_slug,
|
||||
"author_id": str(m.author_id),
|
||||
"author": {"id": str(m.author.id), "slug": m.author.slug, "name": m.author.name} if m.author else None,
|
||||
"content": m.content,
|
||||
"mentions": m.mentions or [],
|
||||
"voice_url": m.voice_url,
|
||||
@ -82,7 +84,7 @@ async def list_messages(
|
||||
offset: int = Query(0),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
q = select(Message).options(selectinload(Message.attachments))
|
||||
q = select(Message).options(selectinload(Message.attachments), selectinload(Message.author))
|
||||
|
||||
if chat_id:
|
||||
q = q.where(Message.chat_id == uuid.UUID(chat_id))
|
||||
@ -113,7 +115,7 @@ async def create_message(req: MessageCreate, db: AsyncSession = Depends(get_db))
|
||||
task_id=uuid.UUID(req.task_id) if req.task_id else None,
|
||||
parent_id=uuid.UUID(req.parent_id) if req.parent_id else None,
|
||||
author_type=req.author_type,
|
||||
author_slug=req.author_slug,
|
||||
author_id=uuid.UUID(req.author_id),
|
||||
content=req.content,
|
||||
mentions=req.mentions,
|
||||
voice_url=req.voice_url,
|
||||
@ -121,15 +123,16 @@ async def create_message(req: MessageCreate, db: AsyncSession = Depends(get_db))
|
||||
db.add(msg)
|
||||
await db.commit()
|
||||
result2 = await db.execute(
|
||||
select(Message).where(Message.id == msg.id).options(selectinload(Message.attachments))
|
||||
select(Message).where(Message.id == msg.id).options(
|
||||
selectinload(Message.attachments),
|
||||
selectinload(Message.author)
|
||||
)
|
||||
)
|
||||
msg = result2.scalar_one()
|
||||
|
||||
# Get author name for broadcast
|
||||
from tracker.models import Member
|
||||
author_result = await db.execute(select(Member).where(Member.slug == msg.author_slug))
|
||||
author = author_result.scalar_one_or_none()
|
||||
author_name = author.name if author else msg.author_slug
|
||||
# Get author name for broadcast (already loaded via relationship)
|
||||
author_name = msg.author.name if msg.author else "Unknown"
|
||||
author_slug = msg.author.slug if msg.author else "unknown"
|
||||
|
||||
# Broadcast via WebSocket
|
||||
from tracker.ws.manager import manager
|
||||
@ -138,7 +141,8 @@ async def create_message(req: MessageCreate, db: AsyncSession = Depends(get_db))
|
||||
"chat_id": req.chat_id,
|
||||
"task_id": req.task_id,
|
||||
"author_type": msg.author_type,
|
||||
"author_slug": msg.author_slug,
|
||||
"author_id": str(msg.author_id),
|
||||
"author_slug": author_slug,
|
||||
"author_name": author_name,
|
||||
"content": msg.content,
|
||||
"mentions": msg.mentions or [],
|
||||
@ -154,16 +158,16 @@ async def create_message(req: MessageCreate, db: AsyncSession = Depends(get_db))
|
||||
elif chat and chat.kind == ChatKind.LOBBY:
|
||||
await manager.broadcast_all(
|
||||
{"type": "message.new", "data": msg_data},
|
||||
exclude_slug=msg.author_slug,
|
||||
exclude_slug=author_slug,
|
||||
)
|
||||
return _message_out(msg)
|
||||
|
||||
if project_id:
|
||||
await manager.broadcast_message(project_id, msg_data, author_slug=msg.author_slug)
|
||||
await manager.broadcast_message(project_id, msg_data, author_slug=author_slug)
|
||||
else:
|
||||
await manager.broadcast_all(
|
||||
{"type": "message.new", "data": msg_data},
|
||||
exclude_slug=msg.author_slug,
|
||||
exclude_slug=author_slug,
|
||||
)
|
||||
|
||||
return _message_out(msg)
|
||||
@ -175,7 +179,7 @@ async def list_replies(message_id: str, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(
|
||||
select(Message)
|
||||
.where(Message.parent_id == uuid.UUID(message_id))
|
||||
.options(selectinload(Message.attachments))
|
||||
.options(selectinload(Message.attachments), selectinload(Message.author))
|
||||
.order_by(Message.created_at)
|
||||
)
|
||||
return [_message_out(m) for m in result.scalars()]
|
||||
|
||||
@ -10,8 +10,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload, joinedload
|
||||
|
||||
from tracker.database import get_db
|
||||
from tracker.enums import AuthorType, ChatKind, TaskPriority, TaskStatus, TaskType
|
||||
from tracker.models import Task, Step, Project, Member, Message, Chat
|
||||
from tracker.enums import AuthorType, ChatKind, TaskPriority, TaskStatus, TaskType, TaskActionType
|
||||
from tracker.models import Task, Step, Project, Member, Message, Chat, TaskAction
|
||||
from tracker.api.auth import get_current_member
|
||||
|
||||
router = APIRouter(tags=["tasks"])
|
||||
@ -38,9 +38,11 @@ class TaskOut(BaseModel):
|
||||
status: str
|
||||
priority: str
|
||||
labels: list[str] = []
|
||||
assignee_slug: str | None = None
|
||||
reviewer_slug: str | None = None
|
||||
watchers: list[str] = []
|
||||
assignee_id: str | None = None
|
||||
assignee: dict | None = None # Member object for display
|
||||
reviewer_id: str | None = None
|
||||
reviewer: dict | None = None # Member object for display
|
||||
watcher_ids: list[str] = []
|
||||
depends_on: list[str] = []
|
||||
position: int
|
||||
time_spent: int
|
||||
@ -55,8 +57,8 @@ class TaskCreate(BaseModel):
|
||||
priority: str = TaskPriority.MEDIUM
|
||||
labels: list[str] = []
|
||||
parent_id: str | None = None
|
||||
assignee_slug: str | None = None
|
||||
reviewer_slug: str | None = None
|
||||
assignee_id: str | None = None
|
||||
reviewer_id: str | None = None
|
||||
depends_on: list[str] = []
|
||||
|
||||
|
||||
@ -67,8 +69,8 @@ class TaskUpdate(BaseModel):
|
||||
status: str | None = None
|
||||
priority: str | None = None
|
||||
labels: list[str] | None = None
|
||||
assignee_slug: str | None = None
|
||||
reviewer_slug: str | None = None
|
||||
assignee_id: str | None = None
|
||||
reviewer_id: str | None = None
|
||||
position: int | None = None
|
||||
|
||||
|
||||
@ -77,7 +79,7 @@ class RejectRequest(BaseModel):
|
||||
|
||||
|
||||
class AssignRequest(BaseModel):
|
||||
assignee_slug: str
|
||||
assignee_id: str
|
||||
|
||||
|
||||
# --- Helpers ---
|
||||
@ -97,7 +99,7 @@ async def _system_message(
|
||||
chat_text: str,
|
||||
task_text: str | None = None,
|
||||
project_slug: str = "",
|
||||
actor_slug: str = "system",
|
||||
actor_id: str | None = None,
|
||||
):
|
||||
"""Create system messages: one in project chat + one in task comments.
|
||||
|
||||
@ -118,7 +120,7 @@ async def _system_message(
|
||||
task_msg = Message(
|
||||
task_id=task.id,
|
||||
author_type=AuthorType.SYSTEM,
|
||||
author_slug=actor_slug,
|
||||
author_id=uuid.UUID(actor_id) if actor_id else None,
|
||||
content=task_text,
|
||||
mentions=[],
|
||||
)
|
||||
@ -130,7 +132,7 @@ async def _system_message(
|
||||
chat_msg = Message(
|
||||
chat_id=chat_id,
|
||||
author_type=AuthorType.SYSTEM,
|
||||
author_slug=actor_slug,
|
||||
author_id=uuid.UUID(actor_id) if actor_id else None,
|
||||
content=chat_text,
|
||||
mentions=[],
|
||||
)
|
||||
@ -175,9 +177,11 @@ def _task_out(t: Task, project_slug: str = "") -> dict:
|
||||
"status": t.status,
|
||||
"priority": t.priority,
|
||||
"labels": t.labels or [],
|
||||
"assignee_slug": t.assignee_slug,
|
||||
"reviewer_slug": t.reviewer_slug,
|
||||
"watchers": t.watchers or [],
|
||||
"assignee_id": str(t.assignee_id) if t.assignee_id else None,
|
||||
"assignee": {"id": str(t.assignee.id), "slug": t.assignee.slug, "name": t.assignee.name} if t.assignee else None,
|
||||
"reviewer_id": str(t.reviewer_id) if t.reviewer_id else None,
|
||||
"reviewer": {"id": str(t.reviewer.id), "slug": t.reviewer.slug, "name": t.reviewer.name} if t.reviewer else None,
|
||||
"watcher_ids": [str(w) for w in (t.watcher_ids or [])],
|
||||
"depends_on": [str(d) for d in (t.depends_on or [])],
|
||||
"position": t.position,
|
||||
"time_spent": t.time_spent,
|
||||
@ -191,7 +195,12 @@ def _task_out(t: Task, project_slug: str = "") -> dict:
|
||||
async def _get_task(task_id: str, db: AsyncSession) -> Task:
|
||||
result = await db.execute(
|
||||
select(Task).where(Task.id == uuid.UUID(task_id))
|
||||
.options(selectinload(Task.steps), joinedload(Task.project))
|
||||
.options(
|
||||
selectinload(Task.steps),
|
||||
joinedload(Task.project),
|
||||
joinedload(Task.assignee),
|
||||
joinedload(Task.reviewer)
|
||||
)
|
||||
)
|
||||
task = result.scalar_one_or_none()
|
||||
if not task:
|
||||
@ -209,13 +218,18 @@ async def list_tasks(
|
||||
label: Optional[str] = Query(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
q = select(Task).options(selectinload(Task.steps), joinedload(Task.project))
|
||||
q = select(Task).options(
|
||||
selectinload(Task.steps),
|
||||
joinedload(Task.project),
|
||||
joinedload(Task.assignee),
|
||||
joinedload(Task.reviewer)
|
||||
)
|
||||
if project_id:
|
||||
q = q.where(Task.project_id == uuid.UUID(project_id))
|
||||
if status:
|
||||
q = q.where(Task.status == status)
|
||||
if assignee:
|
||||
q = q.where(Task.assignee_slug == assignee)
|
||||
q = q.where(Task.assignee_id == uuid.UUID(assignee))
|
||||
if label:
|
||||
q = q.where(Task.labels.contains([label]))
|
||||
q = q.order_by(Task.position, Task.created_at)
|
||||
@ -256,12 +270,21 @@ async def create_task(
|
||||
status=req.status,
|
||||
priority=req.priority,
|
||||
labels=req.labels,
|
||||
assignee_slug=req.assignee_slug,
|
||||
reviewer_slug=req.reviewer_slug,
|
||||
assignee_id=uuid.UUID(req.assignee_id) if req.assignee_id else None,
|
||||
reviewer_id=uuid.UUID(req.reviewer_id) if req.reviewer_id else None,
|
||||
depends_on=[uuid.UUID(d) for d in req.depends_on] if req.depends_on else [],
|
||||
watchers=[req.assignee_slug] if req.assignee_slug else [],
|
||||
watcher_ids=[uuid.UUID(req.assignee_id)] if req.assignee_id else [],
|
||||
)
|
||||
db.add(task)
|
||||
|
||||
# Create audit log
|
||||
task_action = TaskAction(
|
||||
task_id=task.id,
|
||||
actor_id=current_member.id,
|
||||
action=TaskActionType.CREATED
|
||||
)
|
||||
db.add(task_action)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(task)
|
||||
# Load steps
|
||||
@ -311,7 +334,7 @@ async def update_task(
|
||||
who = f"@{current_member.slug}"
|
||||
|
||||
old_status = task.status
|
||||
old_assignee = task.assignee_slug
|
||||
old_assignee_id = task.assignee_id
|
||||
|
||||
if req.title is not None:
|
||||
task.title = req.title
|
||||
@ -325,10 +348,10 @@ async def update_task(
|
||||
task.priority = req.priority
|
||||
if req.labels is not None:
|
||||
task.labels = req.labels
|
||||
if req.assignee_slug is not None:
|
||||
task.assignee_slug = req.assignee_slug or None
|
||||
if req.reviewer_slug is not None:
|
||||
task.reviewer_slug = req.reviewer_slug or None
|
||||
if req.assignee_id is not None:
|
||||
task.assignee_id = uuid.UUID(req.assignee_id) if req.assignee_id else None
|
||||
if req.reviewer_id is not None:
|
||||
task.reviewer_id = uuid.UUID(req.reviewer_id) if req.reviewer_id else None
|
||||
if req.position is not None:
|
||||
task.position = req.position
|
||||
|
||||
@ -341,7 +364,7 @@ async def update_task(
|
||||
chat_text=f"{key}: {old_status} → {req.status}",
|
||||
task_text=f"{who} изменил статус: {old_status} → {req.status} ({now_str})",
|
||||
project_slug=slug,
|
||||
actor_slug=current_member.slug,
|
||||
actor_id=str(current_member.id),
|
||||
)
|
||||
if req.assignee_slug is not None and req.assignee_slug != old_assignee:
|
||||
if req.assignee_slug:
|
||||
|
||||
@ -69,6 +69,20 @@ class AuthorType(StrEnum):
|
||||
SYSTEM = "system"
|
||||
|
||||
|
||||
class TaskActionType(StrEnum):
|
||||
CREATED = "created"
|
||||
STATUS_CHANGED = "status_changed"
|
||||
ASSIGNED = "assigned"
|
||||
UNASSIGNED = "unassigned"
|
||||
PRIORITY_CHANGED = "priority_changed"
|
||||
TITLE_CHANGED = "title_changed"
|
||||
DESCRIPTION_CHANGED = "description_changed"
|
||||
REVIEWER_CHANGED = "reviewer_changed"
|
||||
WATCHER_ADDED = "watcher_added"
|
||||
WATCHER_REMOVED = "watcher_removed"
|
||||
DELETED = "deleted"
|
||||
|
||||
|
||||
class WSEventType(StrEnum):
|
||||
AUTH = "auth"
|
||||
AUTH_OK = "auth.ok"
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
from tracker.models.base import Base
|
||||
from tracker.models.member import AgentConfig, Member
|
||||
from tracker.models.project import Project, ProjectMember
|
||||
from tracker.models.task import Step, Task
|
||||
from tracker.models.task import Step, Task, TaskAction
|
||||
from tracker.models.chat import Attachment, Chat, Message
|
||||
|
||||
__all__ = [
|
||||
@ -13,6 +13,7 @@ __all__ = [
|
||||
"Project",
|
||||
"ProjectMember",
|
||||
"Task",
|
||||
"TaskAction",
|
||||
"Step",
|
||||
"Chat",
|
||||
"Message",
|
||||
|
||||
@ -11,6 +11,7 @@ from tracker.enums import ChatKind
|
||||
from tracker.models.base import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tracker.models.member import Member
|
||||
from tracker.models.project import Project
|
||||
|
||||
|
||||
@ -41,7 +42,7 @@ class Message(Base):
|
||||
|
||||
# Author
|
||||
author_type: Mapped[str] = mapped_column(String(20), nullable=False) # human | agent | system
|
||||
author_slug: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
author_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("members.id"), nullable=False)
|
||||
|
||||
# Content
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
@ -50,6 +51,7 @@ class Message(Base):
|
||||
|
||||
# Relationships
|
||||
chat: Mapped["Chat | None"] = relationship(back_populates="messages", foreign_keys=[chat_id])
|
||||
author: Mapped["Member"] = relationship()
|
||||
attachments: Mapped[list["Attachment"]] = relationship(back_populates="message", cascade="all, delete-orphan")
|
||||
replies: Mapped[list["Message"]] = relationship(back_populates="parent")
|
||||
parent: Mapped["Message | None"] = relationship(back_populates="replies", remote_side="Message.id")
|
||||
|
||||
@ -26,6 +26,7 @@ class Member(Base):
|
||||
token: Mapped[str | None] = mapped_column(String(255), unique=True) # for agents/bridges
|
||||
status: Mapped[str] = mapped_column(String(20), default=MemberStatus.OFFLINE) # online | offline | busy
|
||||
avatar_url: Mapped[str | None] = mapped_column(String(500))
|
||||
is_active: Mapped[bool] = mapped_column(default=True)
|
||||
|
||||
# Agent-specific config (nullable for humans)
|
||||
agent_config: Mapped["AgentConfig | None"] = relationship(
|
||||
|
||||
@ -11,6 +11,7 @@ from tracker.enums import TaskPriority, TaskStatus, TaskType
|
||||
from tracker.models.base import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tracker.models.member import Member
|
||||
from tracker.models.project import Project
|
||||
|
||||
|
||||
@ -26,14 +27,16 @@ class Task(Base):
|
||||
status: Mapped[str] = mapped_column(String(20), default=TaskStatus.BACKLOG) # backlog | todo | in_progress | in_review | done
|
||||
priority: Mapped[str] = mapped_column(String(20), default=TaskPriority.MEDIUM) # critical | high | medium | low
|
||||
labels: Mapped[list[str]] = mapped_column(ARRAY(String), default=list)
|
||||
assignee_slug: Mapped[str | None] = mapped_column(String(255))
|
||||
reviewer_slug: Mapped[str | None] = mapped_column(String(255))
|
||||
watchers: Mapped[list[str]] = mapped_column(ARRAY(String), default=list) # slugs
|
||||
assignee_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("members.id"), nullable=True)
|
||||
reviewer_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("members.id"), nullable=True)
|
||||
watcher_ids: Mapped[list[uuid.UUID]] = mapped_column(ARRAY(UUID(as_uuid=True)), default=list)
|
||||
depends_on: Mapped[list[uuid.UUID]] = mapped_column(ARRAY(UUID(as_uuid=True)), default=list)
|
||||
position: Mapped[int] = mapped_column(Integer, default=0) # ordering within column
|
||||
time_spent: Mapped[int] = mapped_column(Integer, default=0) # minutes
|
||||
|
||||
project: Mapped["Project"] = relationship(back_populates="tasks")
|
||||
assignee: Mapped["Member | None"] = relationship(foreign_keys=[assignee_id])
|
||||
reviewer: Mapped["Member | None"] = relationship(foreign_keys=[reviewer_id])
|
||||
parent: Mapped["Task | None"] = relationship(remote_side="Task.id", back_populates="subtasks")
|
||||
subtasks: Mapped[list["Task"]] = relationship(back_populates="parent")
|
||||
steps: Mapped[list["Step"]] = relationship(back_populates="task", cascade="all, delete-orphan", order_by="Step.position")
|
||||
@ -48,3 +51,17 @@ class Step(Base):
|
||||
position: Mapped[int] = mapped_column(Integer, default=0)
|
||||
|
||||
task: Mapped["Task"] = relationship(back_populates="steps")
|
||||
|
||||
|
||||
class TaskAction(Base):
|
||||
__tablename__ = "task_actions"
|
||||
|
||||
task_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("tasks.id", ondelete="CASCADE"))
|
||||
actor_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("members.id"))
|
||||
action: Mapped[str] = mapped_column(String(50), nullable=False) # created, status_changed, assigned, etc.
|
||||
field: Mapped[str | None] = mapped_column(String(100)) # какое поле изменилось
|
||||
old_value: Mapped[str | None] = mapped_column(Text)
|
||||
new_value: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
task: Mapped["Task"] = relationship()
|
||||
actor: Mapped["Member"] = relationship()
|
||||
|
||||
@ -275,7 +275,7 @@ async def _handle_chat_send(session_id: str, data: dict):
|
||||
chat_id=uuid.UUID(chat_id) if chat_id else None,
|
||||
task_id=uuid.UUID(task_id) if task_id else None,
|
||||
author_type=member.type,
|
||||
author_slug=member.slug,
|
||||
author_id=member.id,
|
||||
content=content,
|
||||
mentions=mentions,
|
||||
)
|
||||
@ -288,6 +288,7 @@ async def _handle_chat_send(session_id: str, data: dict):
|
||||
"chat_id": chat_id,
|
||||
"task_id": task_id,
|
||||
"author_type": member.type,
|
||||
"author_id": str(member.id),
|
||||
"author_slug": member.slug,
|
||||
"author_name": member.name,
|
||||
"content": content,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user