mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-11 12:18:59 +00:00
feat(teams): restore active tasks on session switch via teams.tasks.active-by-session RPC
This commit is contained in:
1 parent
1355e4e0cb
commit
f793bba0e0
7 files changed
+105
-3
No files matched your search
@@ -47,6 +47,7 @@ func (m *TeamsMethods) Register(router *gateway.MethodRouter) {
|
||||
router.Register(protocol.MethodTeamsGet, m.handleGet)
|
||||
router.Register(protocol.MethodTeamsDelete, m.handleDelete)
|
||||
router.Register(protocol.MethodTeamsTaskList, m.handleTaskList)
|
||||
router.Register(protocol.MethodTeamsTaskActiveBySession, m.handleTaskActiveBySession)
|
||||
router.Register(protocol.MethodTeamsTaskApprove, m.handleTaskApprove)
|
||||
router.Register(protocol.MethodTeamsTaskReject, m.handleTaskReject)
|
||||
router.Register(protocol.MethodTeamsMembersAdd, m.handleAddMember)
|
||||
|
||||
@@ -195,6 +195,68 @@ func (m *TeamsMethods) handleTaskList(ctx context.Context, client *gateway.Clien
|
||||
}))
|
||||
}
|
||||
|
||||
// --- Active Tasks by Session ---
|
||||
|
||||
func (m *TeamsMethods) handleTaskActiveBySession(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
if m.teamStore == nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgTeamsNotConfigured)))
|
||||
return
|
||||
}
|
||||
|
||||
var params struct {
|
||||
SessionKey string `json:"sessionKey"`
|
||||
}
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
||||
return
|
||||
}
|
||||
if params.SessionKey == "" {
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"tasks": []any{}}))
|
||||
return
|
||||
}
|
||||
|
||||
tasks, err := m.teamStore.ListActiveTasksByChatID(ctx, params.SessionKey)
|
||||
if err != nil {
|
||||
slog.Warn("teams.tasks.active-by-session failed", "session_key", params.SessionKey, "error", err)
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Map to lightweight ActiveTeamTask shape for the sidebar
|
||||
type activeTask struct {
|
||||
TaskID string `json:"taskId"`
|
||||
TaskNumber int `json:"taskNumber"`
|
||||
Subject string `json:"subject"`
|
||||
Status string `json:"status"`
|
||||
OwnerAgentKey string `json:"ownerAgentKey,omitempty"`
|
||||
OwnerDisplayName string `json:"ownerDisplayName,omitempty"`
|
||||
ProgressPercent int `json:"progressPercent,omitempty"`
|
||||
ProgressStep string `json:"progressStep,omitempty"`
|
||||
CommentCount int `json:"commentCount,omitempty"`
|
||||
AttachmentCount int `json:"attachmentCount,omitempty"`
|
||||
}
|
||||
|
||||
result := make([]activeTask, 0, len(tasks))
|
||||
for _, t := range tasks {
|
||||
result = append(result, activeTask{
|
||||
TaskID: t.ID.String(),
|
||||
TaskNumber: t.TaskNumber,
|
||||
Subject: t.Subject,
|
||||
Status: t.Status,
|
||||
OwnerAgentKey: t.OwnerAgentKey,
|
||||
ProgressPercent: t.ProgressPercent,
|
||||
ProgressStep: t.ProgressStep,
|
||||
CommentCount: t.CommentCount,
|
||||
AttachmentCount: t.AttachmentCount,
|
||||
})
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"tasks": result,
|
||||
}))
|
||||
}
|
||||
|
||||
// --- Update (settings) ---
|
||||
|
||||
type teamsUpdateParams struct {
|
||||
|
||||
@@ -456,6 +456,36 @@ func (s *PGTeamStore) DeleteTasks(ctx context.Context, taskIDs []uuid.UUID, team
|
||||
return deleted, rows.Err()
|
||||
}
|
||||
|
||||
// ListActiveTasksByChatID returns non-terminal tasks for a given chat_id (session key).
|
||||
// Used to restore the active tasks sidebar when switching back to a session.
|
||||
func (s *PGTeamStore) ListActiveTasksByChatID(ctx context.Context, chatID string) ([]store.TeamTaskData, error) {
|
||||
if chatID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
args := []any{chatID}
|
||||
tenantWhere := ""
|
||||
if !store.IsCrossTenant(ctx) {
|
||||
tid := store.TenantIDFromContext(ctx)
|
||||
if tid == uuid.Nil {
|
||||
return nil, fmt.Errorf("tenant_id required")
|
||||
}
|
||||
tenantWhere = fmt.Sprintf(" AND t.tenant_id = $%d", len(args)+1)
|
||||
args = append(args, tid)
|
||||
}
|
||||
rows, err := s.db.QueryContext(ctx,
|
||||
`SELECT `+taskSelectCols+`
|
||||
`+taskJoinClause+`
|
||||
WHERE COALESCE(t.chat_id,'') = $1
|
||||
AND t.status IN ('pending','in_progress','blocked','in_review')`+tenantWhere+`
|
||||
ORDER BY t.task_number ASC
|
||||
LIMIT 50`, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanTaskRowsJoined(rows)
|
||||
}
|
||||
|
||||
func scanTaskRowsJoined(rows *sql.Rows) ([]store.TeamTaskData, error) {
|
||||
var tasks []store.TeamTaskData
|
||||
for rows.Next() {
|
||||
|
||||
@@ -231,6 +231,7 @@ type TaskStore interface {
|
||||
UpdateTaskProgress(ctx context.Context, taskID, teamID uuid.UUID, percent int, step string) error
|
||||
RenewTaskLock(ctx context.Context, taskID, teamID uuid.UUID) error
|
||||
ResetTaskStatus(ctx context.Context, taskID, teamID uuid.UUID) error
|
||||
ListActiveTasksByChatID(ctx context.Context, chatID string) ([]TeamTaskData, error)
|
||||
}
|
||||
|
||||
// TaskCommentStore manages task comments, audit events, and attachments.
|
||||
|
||||
@@ -136,7 +136,8 @@ const (
|
||||
MethodTeamsTaskCreate = "teams.tasks.create"
|
||||
MethodTeamsTaskDelete = "teams.tasks.delete"
|
||||
MethodTeamsTaskDeleteBulk = "teams.tasks.delete-bulk"
|
||||
MethodTeamsTaskAssign = "teams.tasks.assign"
|
||||
MethodTeamsTaskAssign = "teams.tasks.assign"
|
||||
MethodTeamsTaskActiveBySession = "teams.tasks.active-by-session"
|
||||
MethodTeamsMembersAdd = "teams.members.add"
|
||||
MethodTeamsMembersRemove = "teams.members.remove"
|
||||
MethodTeamsUpdate = "teams.update"
|
||||
|
||||
@@ -143,6 +143,7 @@ export const Methods = {
|
||||
TEAMS_TASK_DELETE: "teams.tasks.delete",
|
||||
TEAMS_TASK_DELETE_BULK: "teams.tasks.delete-bulk",
|
||||
TEAMS_TASK_ASSIGN: "teams.tasks.assign",
|
||||
TEAMS_TASK_ACTIVE_BY_SESSION: "teams.tasks.active-by-session",
|
||||
TEAMS_MEMBERS_ADD: "teams.members.add",
|
||||
TEAMS_MEMBERS_REMOVE: "teams.members.remove",
|
||||
TEAMS_UPDATE: "teams.update",
|
||||
|
||||
@@ -139,12 +139,18 @@ export function useChatMessages(sessionKey: string, agentId: string) {
|
||||
}
|
||||
}, [ws, agentId, sessionKey]);
|
||||
|
||||
// Load history when session changes
|
||||
// Load history and restore active tasks when session changes
|
||||
useEffect(() => {
|
||||
if (sessionKey) {
|
||||
loadHistory();
|
||||
// Restore active team tasks (fire-and-forget — teams module may not be configured)
|
||||
ws.call<{ tasks?: ActiveTeamTask[] }>(Methods.TEAMS_TASK_ACTIVE_BY_SESSION, { sessionKey })
|
||||
.then((res) => {
|
||||
if (res.tasks && res.tasks.length > 0) setTeamTasks(res.tasks);
|
||||
})
|
||||
.catch(() => {}); // teams not configured or RPC not registered
|
||||
}
|
||||
}, [sessionKey, loadHistory]);
|
||||
}, [sessionKey, loadHistory, ws]);
|
||||
|
||||
// Called before sending a message so the event handler knows to capture run.started
|
||||
const expectRun = useCallback(() => {
|
||||
|
||||
Reference in new issue
Block a user