mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-08-09 04:21:35 +00:00
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.
143 lines
4.3 KiB
Go
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)
|
|
}
|