Files
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

286 lines
11 KiB
Go

package workspace
import (
"context"
"errors"
"fmt"
"log/slog"
"path/filepath"
"strings"
"time"
workspacebiz "github.com/tutti-os/tutti/services/tuttid/biz/workspace"
builtinapps "github.com/tutti-os/tutti/services/tuttid/builtin-apps"
workspacedata "github.com/tutti-os/tutti/services/tuttid/data/workspace"
)
var ErrInvalidWorkspaceAppRuntimeState = errors.New("invalid workspace app runtime state")
func (s *AppCenterService) Launch(ctx context.Context, workspaceID string, appID string) (workspacebiz.WorkspaceApp, error) {
if _, err := s.workspaceSummary(ctx, workspaceID); err != nil {
return workspacebiz.WorkspaceApp{}, err
}
appPackage, installation, err := s.installedPackage(ctx, workspaceID, appID)
if err != nil {
return workspacebiz.WorkspaceApp{}, err
}
runtimeState := s.runner().State(workspaceID, appPackage.AppID)
switch runtimeState.Status {
case workspacebiz.AppRuntimeStatusIdle:
runtimeState, err = s.startPackage(ctx, workspaceID, appPackage, false)
if err != nil {
return workspacebiz.WorkspaceApp{}, err
}
case workspacebiz.AppRuntimeStatusPreparing, workspacebiz.AppRuntimeStatusStarting, workspacebiz.AppRuntimeStatusRunning:
case workspacebiz.AppRuntimeStatusFailed:
return workspacebiz.WorkspaceApp{}, fmt.Errorf("%w: failed workspace apps must be retried before launch", ErrInvalidWorkspaceAppRuntimeState)
case workspacebiz.AppRuntimeStatusStopping:
return workspacebiz.WorkspaceApp{}, fmt.Errorf("%w: workspace app is stopping", ErrInvalidWorkspaceAppRuntimeState)
default:
return workspacebiz.WorkspaceApp{}, fmt.Errorf("%w: unsupported status %q", ErrInvalidWorkspaceAppRuntimeState, runtimeState.Status)
}
return s.publishInstalledAppRuntime(ctx, workspaceID, appPackage, installation, runtimeState), nil
}
func (s *AppCenterService) Retry(ctx context.Context, workspaceID string, appID string) (workspacebiz.WorkspaceApp, error) {
if _, err := s.workspaceSummary(ctx, workspaceID); err != nil {
return workspacebiz.WorkspaceApp{}, err
}
appPackage, installation, err := s.installedPackage(ctx, workspaceID, appID)
if err != nil {
return workspacebiz.WorkspaceApp{}, err
}
if runtimeState := s.runner().State(workspaceID, appPackage.AppID); runtimeState.Status != workspacebiz.AppRuntimeStatusFailed {
return workspacebiz.WorkspaceApp{}, fmt.Errorf("%w: workspace app retry requires failed runtime status", ErrInvalidWorkspaceAppRuntimeState)
}
runtimeState, err := s.startPackage(ctx, workspaceID, appPackage, true)
if err != nil {
return workspacebiz.WorkspaceApp{}, err
}
return s.publishInstalledAppRuntime(ctx, workspaceID, appPackage, installation, runtimeState), nil
}
func (s *AppCenterService) StartEnabled(ctx context.Context, workspaceID string) ([]workspacebiz.WorkspaceApp, error) {
startedAt := time.Now()
if _, err := s.workspaceSummary(ctx, workspaceID); err != nil {
return nil, err
}
installations, err := s.Store.ListWorkspaceAppInstallations(ctx, workspaceID)
if err != nil {
return nil, err
}
enabledAppIDs := make(map[string]struct{}, len(installations))
for _, installation := range installations {
if installation.Enabled {
enabledAppIDs[installation.AppID] = struct{}{}
}
}
var builtins []builtinapps.App
slog.Info("workspace app start enabled started", "workspaceId", workspaceID, "enabledAppCount", len(enabledAppIDs))
if len(enabledAppIDs) > 0 {
refreshStartedAt := time.Now()
var err error
builtins, err = s.refreshBuiltinCatalogForStartEnabled(ctx, workspaceID)
if err != nil {
slog.Warn(
"workspace app start enabled remote catalog refresh failed; continuing with cached builtin catalog",
"workspaceId", workspaceID,
"error", err,
)
builtins, err = s.builtinCatalog(ctx)
if err != nil {
return nil, err
}
}
slog.Info("workspace app start enabled remote catalog refresh completed", "workspaceId", workspaceID, "enabledAppCount", len(enabledAppIDs), "durationMs", time.Since(refreshStartedAt).Milliseconds())
} else {
slog.Info("workspace app start enabled remote catalog refresh skipped", "workspaceId", workspaceID, "enabledAppCount", len(enabledAppIDs), "reason", "no-enabled-apps")
}
remoteBuiltins := remoteBuiltinByAppID(builtins)
for _, installation := range installations {
if !installation.Enabled {
continue
}
appPackage, err := s.Store.GetAppPackage(ctx, installation.AppID)
if err != nil {
if !errors.Is(err, workspacedata.ErrWorkspaceAppNotFound) {
return nil, err
}
remoteBuiltin, ok := remoteBuiltins[installation.AppID]
if !ok {
return nil, err
}
s.startRemoteBuiltinInstallJob(workspaceID, remoteBuiltin)
slog.Warn("workspace app start enabled deferred app; package unavailable locally", "workspaceId", workspaceID, "appId", installation.AppID)
continue
}
if remoteBuiltin, ok := remoteBuiltins[installation.AppID]; ok && s.shouldMaterializeRemoteBuiltin(appPackage, remoteBuiltin) {
s.startRemoteBuiltinInstallJob(workspaceID, remoteBuiltin)
slog.Info(
"workspace app start enabled deferred app; remote builtin update available",
"workspaceId", workspaceID,
"appId", installation.AppID,
"currentVersion", appPackage.Version,
"availableVersion", remoteBuiltin.Manifest.Version,
)
continue
}
runtimeState := s.runner().State(workspaceID, appPackage.AppID)
switch runtimeState.Status {
case workspacebiz.AppRuntimeStatusIdle:
case workspacebiz.AppRuntimeStatusPreparing, workspacebiz.AppRuntimeStatusStarting, workspacebiz.AppRuntimeStatusRunning:
continue
case workspacebiz.AppRuntimeStatusFailed, workspacebiz.AppRuntimeStatusStopping:
slog.Info(
"workspace app start enabled skipped app in non-launchable runtime state",
"workspaceId", workspaceID,
"appId", appPackage.AppID,
"runtimeStatus", runtimeState.Status,
)
continue
default:
slog.Warn(
"workspace app start enabled skipped app in unknown runtime state",
"workspaceId", workspaceID,
"appId", appPackage.AppID,
"runtimeStatus", runtimeState.Status,
)
continue
}
if _, err := s.startPackage(ctx, workspaceID, appPackage, false); err != nil {
return nil, err
}
}
slog.Info("workspace app start enabled completed", "workspaceId", workspaceID, "enabledAppCount", len(enabledAppIDs), "duration", time.Since(startedAt), "durationMs", time.Since(startedAt).Milliseconds())
if len(enabledAppIDs) > 0 {
return s.listWithBuiltins(ctx, workspaceID, builtins)
}
return s.List(ctx, workspaceID)
}
func (s *AppCenterService) refreshBuiltinCatalogForStartEnabled(ctx context.Context, workspaceID string) ([]builtinapps.App, error) {
if s.BuiltinCatalog != nil {
return s.BuiltinCatalog()
}
snapshot, err := s.refreshBuiltinCatalogAndWait(ctx)
if err != nil {
return nil, err
}
if snapshot.RemoteCatalog.Status == builtinapps.RemoteCatalogLoadStatusFailed {
slog.Warn(
"workspace app start enabled remote catalog refresh failed",
"workspaceId", workspaceID,
"error", snapshot.RemoteCatalog.LastError,
)
}
return snapshot.Apps, nil
}
func (s *AppCenterService) StopAll(ctx context.Context, workspaceID string) ([]workspacebiz.WorkspaceApp, error) {
if _, err := s.workspaceSummary(ctx, workspaceID); err != nil {
return nil, err
}
s.deactivateWorkspaceAppCLI(ctx, workspaceID)
s.runner().StopWorkspace(ctx, workspaceID)
return s.List(ctx, workspaceID)
}
func (s *AppCenterService) publishInstalledAppRuntime(ctx context.Context, workspaceID string, appPackage workspacebiz.AppPackage, installation workspacebiz.AppInstallation, runtimeState workspacebiz.AppRuntimeState) workspacebiz.WorkspaceApp {
app := workspacebiz.WorkspaceApp{
Package: appPackage,
Installation: &installation,
Runtime: runtimeStateForActivePackage(runtimeState, appPackage),
}
app.CLI = s.appCLIState(workspaceID, app)
return s.publishAppIfChanged(ctx, workspaceID, appPackage.AppID, app)
}
func (s *AppCenterService) startRemoteBuiltinInstallJob(workspaceID string, builtin builtinapps.App) bool {
appID := strings.TrimSpace(builtin.Manifest.AppID)
return s.startInstallJob(workspaceID, appID, InstallOptions{}, appRuntimeProfileForManifest(builtin.Manifest), func(ctx context.Context) (workspacebiz.AppPackage, error) {
return s.packageForRemoteBuiltinInstall(ctx, builtin)
})
}
func (s *AppCenterService) packageForRemoteBuiltinInstall(ctx context.Context, builtin builtinapps.App) (workspacebiz.AppPackage, error) {
appID := strings.TrimSpace(builtin.Manifest.AppID)
version := strings.TrimSpace(builtin.Manifest.Version)
if appID == "" || version == "" {
return workspacebiz.AppPackage{}, errors.New("remote builtin app id and version are required")
}
existing, err := s.Store.GetAppPackageVersion(ctx, appID, version)
if err == nil && existing.Source == workspacebiz.AppPackageSourceBuiltin {
if s.shouldMaterializeRemoteBuiltin(existing, builtin) {
return s.downloadRemoteBuiltinPackage(ctx, builtin)
}
return existing, nil
}
if err != nil && !errors.Is(err, workspacedata.ErrWorkspaceAppNotFound) {
return workspacebiz.AppPackage{}, err
}
return s.downloadRemoteBuiltinPackage(ctx, builtin)
}
func remoteBuiltinByAppID(builtins []builtinapps.App) map[string]builtinapps.App {
apps := make(map[string]builtinapps.App, len(builtins))
for _, builtin := range builtins {
if builtin.Distribution.Kind != builtinapps.DistributionRemote {
continue
}
appID := strings.TrimSpace(builtin.Manifest.AppID)
if appID == "" {
continue
}
apps[appID] = builtin
}
return apps
}
func (s *AppCenterService) installedPackage(ctx context.Context, workspaceID string, appID string) (workspacebiz.AppPackage, workspacebiz.AppInstallation, error) {
appPackage, err := s.Store.GetAppPackage(ctx, appID)
if err != nil {
return workspacebiz.AppPackage{}, workspacebiz.AppInstallation{}, err
}
installations, err := s.Store.ListWorkspaceAppInstallations(ctx, workspaceID)
if err != nil {
return workspacebiz.AppPackage{}, workspacebiz.AppInstallation{}, err
}
for _, installation := range installations {
if installation.AppID == appPackage.AppID {
return appPackage, installation, nil
}
}
return workspacebiz.AppPackage{}, workspacebiz.AppInstallation{}, workspacedata.ErrWorkspaceAppNotFound
}
func (s *AppCenterService) startPackage(ctx context.Context, workspaceID string, appPackage workspacebiz.AppPackage, restart bool) (workspacebiz.AppRuntimeState, error) {
workspace, err := s.workspaceSummary(ctx, workspaceID)
if err != nil {
return workspacebiz.AppRuntimeState{}, err
}
root := s.workspaceAppStateRoot(workspaceID, appPackage.AppID)
return s.runner().Start(ctx, AppStartInput{
WorkspaceID: workspaceID,
WorkspaceName: workspace.Name,
AppID: appPackage.AppID,
PackageDir: appPackage.PackageDir,
Bootstrap: appPackage.Manifest.Runtime.Bootstrap,
HealthcheckPath: appPackage.Manifest.Runtime.HealthcheckPath,
RuntimeProfile: appRuntimeProfileForPackage(appPackage),
RuntimeDir: filepath.Join(root, "runtime"),
DataDir: filepath.Join(root, "data"),
DatabaseDir: filepath.Join(root, "database"),
LogDir: filepath.Join(root, "logs"),
Restart: restart,
})
}