Files
goclaw/internal/http/agents_export_team.go
Viet TranandGitHub db7a34e4f3 feat: standalone Import & Export page with Teams, Agents, Skills & MCP
Refactors Agent Transfer into standalone /import-export page with 3 scope tabs.

Teams: auto-bundle member agents (config, context files, memory, KG, cron, profiles, workspace). 
Agents: section picker with presets, merge mode support.
Skills & MCP: custom skills + MCP server configs (stripped secrets), conflict detection.

Security: scopeClauseAlias for JOINs, tenant scope on all queries, zip bomb protection, adminMiddleware on import routes.
DRY: shared storeExportToken + readTarGzEntries helpers.
i18n: complete en/vi/zh coverage.
2026-03-29 18:01:08 +07:00

143 lines
4.3 KiB
Go

package http
import (
"archive/tar"
"context"
"fmt"
"log/slog"
"path/filepath"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/config"
"github.com/nextlevelbuilder/goclaw/internal/store/pg"
)
// exportTeamSection exports team metadata, members, tasks, comments, events, links,
// and team workspace files into the tar archive under the "team/" prefix.
func (h *AgentsHandler) exportTeamSection(ctx context.Context, tw *tar.Writer, agentID uuid.UUID, manifest *ExportManifest, progressFn func(ProgressEvent)) error {
team, teamID, members, err := pg.ExportTeamByLead(ctx, h.db, agentID)
if err != nil {
return fmt.Errorf("query team: %w", err)
}
if team == nil {
// Agent is not a team lead — nothing to export
manifest.Sections["team"] = map[string]any{"lead": false}
return nil
}
// team/team.json
teamJSON, err := jsonIndent(team)
if err != nil {
return fmt.Errorf("marshal team: %w", err)
}
if err := addToTar(tw, "team/team.json", teamJSON); err != nil {
return fmt.Errorf("write team/team.json: %w", err)
}
// team/members.jsonl
if len(members) > 0 {
data, err := marshalJSONL(members)
if err != nil {
return fmt.Errorf("marshal members: %w", err)
}
if err := addToTar(tw, "team/members.jsonl", data); err != nil {
return fmt.Errorf("write team/members.jsonl: %w", err)
}
}
// team/tasks.jsonl
tasksExport, err := pg.ExportTeamTasks(ctx, h.db, teamID)
if err != nil {
slog.Warn("export: team tasks query failed", "team_id", teamID, "error", err)
tasksExport = &pg.TeamTasksExport{}
}
if len(tasksExport.Tasks) > 0 {
data, err := marshalJSONL(tasksExport.Tasks)
if err != nil {
return fmt.Errorf("marshal tasks: %w", err)
}
if err := addToTar(tw, "team/tasks.jsonl", data); err != nil {
return fmt.Errorf("write team/tasks.jsonl: %w", err)
}
}
// team/comments.jsonl
comments, err := pg.ExportTeamComments(ctx, h.db, teamID, tasksExport.TaskUIDs)
if err != nil {
slog.Warn("export: team comments query failed", "team_id", teamID, "error", err)
}
if len(comments) > 0 {
data, err := marshalJSONL(comments)
if err != nil {
return fmt.Errorf("marshal comments: %w", err)
}
if err := addToTar(tw, "team/comments.jsonl", data); err != nil {
return fmt.Errorf("write team/comments.jsonl: %w", err)
}
}
// team/events.jsonl
events, err := pg.ExportTeamEvents(ctx, h.db, teamID, tasksExport.TaskUIDs)
if err != nil {
slog.Warn("export: team events query failed", "team_id", teamID, "error", err)
}
if len(events) > 0 {
data, err := marshalJSONL(events)
if err != nil {
return fmt.Errorf("marshal events: %w", err)
}
if err := addToTar(tw, "team/events.jsonl", data); err != nil {
return fmt.Errorf("write team/events.jsonl: %w", err)
}
}
// team/links.jsonl — agent_links for this agent
links, err := pg.ExportAgentLinks(ctx, h.db, agentID)
if err != nil {
slog.Warn("export: agent links query failed", "agent_id", agentID, "error", err)
}
if len(links) > 0 {
data, err := marshalJSONL(links)
if err != nil {
return fmt.Errorf("marshal links: %w", err)
}
if err := addToTar(tw, "team/links.jsonl", data); err != nil {
return fmt.Errorf("write team/links.jsonl: %w", err)
}
}
// team/workspace/ — team filesystem workspace
var wsFileCount int
var wsTotalBytes int64
if h.dataDir != "" {
wsPath := filepath.Join(config.ExpandHome(h.dataDir), "teams", teamID.String())
wsFileCount, wsTotalBytes, _ = h.exportTeamWorkspaceFiles(ctx, tw, wsPath, progressFn)
}
manifest.Sections["team"] = map[string]any{
"lead": true,
"members": len(members),
"tasks": len(tasksExport.Tasks),
"comments": len(comments),
"events": len(events),
"links": len(links),
"ws_files": wsFileCount,
"ws_bytes": wsTotalBytes,
}
if progressFn != nil {
progressFn(ProgressEvent{
Phase: "team",
Status: "done",
Current: len(tasksExport.Tasks),
Total: len(tasksExport.Tasks),
})
}
return nil
}
// exportTeamWorkspaceFiles walks the team workspace directory and adds files under "team/workspace/".
func (h *AgentsHandler) exportTeamWorkspaceFiles(ctx context.Context, tw *tar.Writer, wsPath string, progressFn func(ProgressEvent)) (int, int64, error) {
return h.exportWorkspaceFilesWithPrefix(ctx, tw, wsPath, "team/workspace/", progressFn)
}