Files
tutti/services/tuttid/api/daemon_agent_sessions_external_import.go
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

299 lines
12 KiB
Go

package api
import (
"context"
"strings"
tuttigenerated "github.com/tutti-os/tutti/services/tuttid/api/generated"
"github.com/tutti-os/tutti/services/tuttid/apierrors"
agentservice "github.com/tutti-os/tutti/services/tuttid/service/agent"
userprojectservice "github.com/tutti-os/tutti/services/tuttid/service/userproject"
)
func (api DaemonAPI) ScanWorkspaceExternalAgentSessionImports(ctx context.Context, request tuttigenerated.ScanWorkspaceExternalAgentSessionImportsRequestObject) (tuttigenerated.ScanWorkspaceExternalAgentSessionImportsResponseObject, error) {
if api.AgentSessionService == nil {
return tuttigenerated.ScanWorkspaceExternalAgentSessionImports503JSONResponse{
ServiceUnavailableErrorJSONResponse: agentSessionServiceUnavailableError(),
}, nil
}
input := agentservice.ExternalImportScanInput{}
if request.Body != nil {
input.ArchivePath = optionalStringValue(request.Body.ArchivePath)
if request.Body.ArchiveKind != nil && !request.Body.ArchiveKind.Valid() {
return tuttigenerated.ScanWorkspaceExternalAgentSessionImports400JSONResponse{
InvalidRequestErrorJSONResponse: invalidRequestError(apierrors.InvalidRequest(
apierrors.ReasonMalformedRequest,
apierrors.WithDeveloperMessage("unsupported archiveKind"),
)),
}, nil
}
input.ArchiveKind = externalImportArchiveKindValue(request.Body.ArchiveKind)
input.Providers = externalImportProvidersFromGenerated(request.Body.Providers)
if request.Body.Days != nil {
input.Days = *request.Body.Days
}
}
result, err := api.AgentSessionService.ScanExternalImports(ctx, input)
if err != nil {
return writeScanWorkspaceExternalAgentSessionImportsError(err), nil
}
return tuttigenerated.ScanWorkspaceExternalAgentSessionImports200JSONResponse(
generatedExternalImportScanResult(result),
), nil
}
func (api DaemonAPI) ImportWorkspaceExternalAgentSessions(ctx context.Context, request tuttigenerated.ImportWorkspaceExternalAgentSessionsRequestObject) (tuttigenerated.ImportWorkspaceExternalAgentSessionsResponseObject, error) {
if api.AgentSessionService == nil {
return tuttigenerated.ImportWorkspaceExternalAgentSessions503JSONResponse{
ServiceUnavailableErrorJSONResponse: agentSessionServiceUnavailableError(),
}, nil
}
if request.Body == nil || len(request.Body.Projects) == 0 {
return tuttigenerated.ImportWorkspaceExternalAgentSessions400JSONResponse{
InvalidRequestErrorJSONResponse: invalidRequestError(apierrors.EmptyBody(apierrors.WithDeveloperMessage("empty project selection"))),
}, nil
}
if request.Body.ArchiveKind != nil && !request.Body.ArchiveKind.Valid() {
return tuttigenerated.ImportWorkspaceExternalAgentSessions400JSONResponse{
InvalidRequestErrorJSONResponse: invalidRequestError(apierrors.InvalidRequest(
apierrors.ReasonMalformedRequest,
apierrors.WithDeveloperMessage("unsupported archiveKind"),
)),
}, nil
}
projects := externalImportProjectSelectionsFromGenerated(request.Body.Projects)
archivePath := optionalStringValue(request.Body.ArchivePath)
archiveKind := externalImportArchiveKindValue(request.Body.ArchiveKind)
register := request.Body.RegisterUserProjects == nil || *request.Body.RegisterUserProjects
importSessions := request.Body.ImportSessions == nil || *request.Body.ImportSessions
// Import sessions first, then register only the projects that actually
// matched at least one valid session so empty projects never get surfaced.
// validPaths are canonical project paths (see the agent service), so register
// them directly instead of mapping back to the raw request selections.
var result agentservice.ExternalImportResult
var validPaths []string
if importSessions {
var err error
result, err = api.AgentSessionService.ImportExternalSessions(ctx, string(request.WorkspaceID), agentservice.ExternalImportInput{
ArchivePath: archivePath,
ArchiveKind: archiveKind,
Projects: projects,
})
if err != nil {
return writeImportWorkspaceExternalAgentSessionsError(err), nil
}
validPaths = result.ProjectPaths
} else {
var err error
validPaths, err = api.AgentSessionService.ExternalImportValidProjectPaths(ctx, agentservice.ExternalImportInput{
ArchivePath: archivePath,
ArchiveKind: archiveKind,
Projects: projects,
})
if err != nil {
return writeImportWorkspaceExternalAgentSessionsError(err), nil
}
}
registeredSelections, registrationErrors := api.registerExternalImportUserProjects(ctx, externalImportSelectionsFromPaths(validPaths), register)
result.ImportedProjects = len(registeredSelections)
result.Errors = append(registrationErrors, result.Errors...)
return tuttigenerated.ImportWorkspaceExternalAgentSessions200JSONResponse(
generatedExternalImportResult(result),
), nil
}
func externalImportSelectionsFromPaths(paths []string) []agentservice.ExternalImportProjectSelection {
selections := make([]agentservice.ExternalImportProjectSelection, 0, len(paths))
for _, path := range paths {
if trimmed := strings.TrimSpace(path); trimmed != "" {
selections = append(selections, agentservice.ExternalImportProjectSelection{Path: trimmed})
}
}
return selections
}
func externalImportArchiveKindValue(input *tuttigenerated.ExternalAgentImportArchiveKind) string {
if input == nil {
return ""
}
return string(*input)
}
func externalImportProvidersFromGenerated(input *[]tuttigenerated.WorkspaceAgentProvider) []string {
if input == nil {
return nil
}
result := make([]string, 0, len(*input))
for _, provider := range *input {
result = append(result, string(provider))
}
return result
}
func externalImportProjectSelectionsFromGenerated(input []tuttigenerated.ExternalAgentImportProjectSelection) []agentservice.ExternalImportProjectSelection {
result := make([]agentservice.ExternalImportProjectSelection, 0, len(input))
for _, project := range input {
result = append(result, agentservice.ExternalImportProjectSelection{
Path: project.Path,
Providers: externalImportProvidersFromGenerated(project.Providers),
SessionIDs: optionalStringSlice(project.SessionIds),
})
}
return result
}
func optionalStringSlice(input *[]string) []string {
if input == nil {
return nil
}
return append([]string(nil), (*input)...)
}
func (api DaemonAPI) registerExternalImportUserProjects(
ctx context.Context,
projects []agentservice.ExternalImportProjectSelection,
register bool,
) ([]agentservice.ExternalImportProjectSelection, []agentservice.ExternalImportError) {
if !register || api.UserProjectService == nil {
return projects, nil
}
result := make([]agentservice.ExternalImportProjectSelection, 0, len(projects))
failures := make([]agentservice.ExternalImportError, 0)
paths := make([]string, len(projects))
for index, project := range projects {
paths[index] = strings.TrimSpace(project.Path)
}
registrationErrors := api.UserProjectService.UseMany(ctx, userprojectservice.UseManyInput{Paths: paths})
for index, project := range projects {
path := strings.TrimSpace(project.Path)
if err := registrationErrors[index]; err != nil {
failure := agentservice.ExternalImportError{
SourcePath: path,
Message: err.Error(),
}
if path == "" {
failure.SourcePath = ""
failure.Message = "project path is empty"
}
failures = append(failures, failure)
continue
}
result = append(result, project)
}
return result, failures
}
func generatedExternalImportScanResult(result agentservice.ExternalImportScanResult) tuttigenerated.ExternalAgentImportScanResponse {
return tuttigenerated.ExternalAgentImportScanResponse{
Errors: generatedExternalImportErrors(result.Errors),
Projects: generatedExternalImportProjects(result.Projects),
Providers: generatedExternalImportProviders(result.Providers),
Sessions: generatedExternalImportSessions(result.Sessions),
ScannedMessages: result.ScannedMessages,
ScannedSessions: result.ScannedSessions,
SkippedSessions: result.SkippedSessions,
}
}
func generatedExternalImportResult(result agentservice.ExternalImportResult) tuttigenerated.ExternalAgentImportResultResponse {
return tuttigenerated.ExternalAgentImportResultResponse{
Errors: generatedExternalImportErrors(result.Errors),
ImportedMessages: result.ImportedMessages,
ImportedProjects: result.ImportedProjects,
ImportedSessions: result.ImportedSessions,
SkippedSessions: result.SkippedSessions,
}
}
func generatedExternalImportProviders(providers []agentservice.ExternalImportProvider) []tuttigenerated.ExternalAgentImportProvider {
result := make([]tuttigenerated.ExternalAgentImportProvider, 0, len(providers))
for _, provider := range providers {
generated := tuttigenerated.ExternalAgentImportProvider{
Available: provider.Available,
MessageCount: provider.MessageCount,
Provider: tuttigenerated.WorkspaceAgentProvider(provider.Provider),
Root: provider.Root,
SessionCount: provider.SessionCount,
}
if errMessage := strings.TrimSpace(provider.Error); errMessage != "" {
generated.Error = optionalStringPointer(errMessage)
}
result = append(result, generated)
}
return result
}
func generatedExternalImportProjects(projects []agentservice.ExternalImportProject) []tuttigenerated.ExternalAgentImportProject {
result := make([]tuttigenerated.ExternalAgentImportProject, 0, len(projects))
for _, project := range projects {
generated := tuttigenerated.ExternalAgentImportProject{
Label: strings.TrimSpace(project.Label),
MessageCount: project.MessageCount,
Path: strings.TrimSpace(project.Path),
Providers: generatedExternalImportProviderNames(project.Providers),
SessionCount: project.SessionCount,
}
if project.LastUpdatedAtUnixMS > 0 {
generated.LastUpdatedAtUnixMs = &project.LastUpdatedAtUnixMS
}
result = append(result, generated)
}
return result
}
func generatedExternalImportSessions(sessions []agentservice.ExternalImportSession) []tuttigenerated.ExternalAgentImportSession {
result := make([]tuttigenerated.ExternalAgentImportSession, 0, len(sessions))
for _, session := range sessions {
generated := tuttigenerated.ExternalAgentImportSession{
Id: strings.TrimSpace(session.ID),
MessageCount: session.MessageCount,
ProjectPath: strings.TrimSpace(session.ProjectPath),
Provider: tuttigenerated.WorkspaceAgentProvider(session.Provider),
Title: strings.TrimSpace(session.Title),
}
// Archive-only imports (e.g. ChatGPT) redact the local path, so only
// surface sourcePath when present. The contract marks it nullable.
if sourcePath := strings.TrimSpace(session.SourcePath); sourcePath != "" {
generated.SourcePath = &sourcePath
}
if session.LastUpdatedAtUnixMS > 0 {
generated.LastUpdatedAtUnixMs = &session.LastUpdatedAtUnixMS
}
result = append(result, generated)
}
return result
}
func generatedExternalImportProviderNames(providers []string) []tuttigenerated.WorkspaceAgentProvider {
result := make([]tuttigenerated.WorkspaceAgentProvider, 0, len(providers))
for _, provider := range providers {
if strings.TrimSpace(provider) != "" {
result = append(result, tuttigenerated.WorkspaceAgentProvider(provider))
}
}
return result
}
func generatedExternalImportErrors(errors []agentservice.ExternalImportError) []tuttigenerated.ExternalAgentImportError {
result := make([]tuttigenerated.ExternalAgentImportError, 0, len(errors))
for _, item := range errors {
generated := tuttigenerated.ExternalAgentImportError{
Message: strings.TrimSpace(item.Message),
}
if generated.Message == "" {
generated.Message = "external import failed"
}
if provider := strings.TrimSpace(item.Provider); provider != "" {
value := tuttigenerated.WorkspaceAgentProvider(provider)
generated.Provider = &value
}
if sourcePath := strings.TrimSpace(item.SourcePath); sourcePath != "" {
generated.SourcePath = &sourcePath
}
result = append(result, generated)
}
return result
}