Add Docker Compose Lingma bootstrap support

Package the proxy for Docker Compose deployments and add Lingma bootstrap, session restore, and runtime status support for containerized remote usage.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
GitHub Actions
2026-05-07 23:56:05 +08:00
parent 86fbdbc40c
commit a4cedecca6
10 changed files with 1067 additions and 68 deletions

View File

@@ -14,8 +14,10 @@ import (
"sync"
"time"
"lingma-ipc-proxy/internal/bootstrap"
"lingma-ipc-proxy/internal/lingmaipc"
"lingma-ipc-proxy/internal/remote"
"lingma-ipc-proxy/internal/sessionbundle"
"lingma-ipc-proxy/internal/toolemulation"
)
@@ -35,24 +37,36 @@ const (
)
type Config struct {
Host string
Port int
Backend BackendMode
Transport lingmaipc.Transport
Pipe string
WebSocketURL string
RemoteBaseURL string
RemoteAuthFile string
RemoteVersion string
Cwd string
CurrentFilePath string
Mode string
Model string
ShellType string
SessionMode SessionMode
Timeout time.Duration
RemoteFallbackEnabled bool
RemoteFallbackModels []string
Host string
Port int
Backend BackendMode
Transport lingmaipc.Transport
Pipe string
WebSocketURL string
RemoteBaseURL string
RemoteAuthFile string
RemoteVersion string
Cwd string
CurrentFilePath string
Mode string
Model string
ShellType string
SessionMode SessionMode
Timeout time.Duration
RemoteFallbackEnabled bool
RemoteFallbackModels []string
LingmaBootstrapEnabled bool
LingmaSourceType string
LingmaVSIXURL string
LingmaMarketplacePublisher string
LingmaMarketplaceExtension string
LingmaBootstrapOutputDir string
LingmaBinaryPath string
LingmaBootstrapAlways bool
LingmaForceRefresh bool
LingmaWorkDir string
LingmaSessionBundle string
LingmaSessionBundleFile string
}
type Image struct {
@@ -127,12 +141,15 @@ type Model struct {
}
type State struct {
PipePath string `json:"pipe_path,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
Transport string `json:"transport,omitempty"`
Connected bool `json:"connected"`
StickySessionID string `json:"sticky_session_id,omitempty"`
SessionMode SessionMode `json:"session_mode"`
PipePath string `json:"pipe_path,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
Transport string `json:"transport,omitempty"`
Connected bool `json:"connected"`
StickySessionID string `json:"sticky_session_id,omitempty"`
SessionMode SessionMode `json:"session_mode"`
Bootstrap bootstrap.Result `json:"bootstrap,omitempty"`
SessionBundle sessionbundle.Result `json:"session_bundle,omitempty"`
RemoteAuth *remote.CredentialStatus `json:"remote_auth,omitempty"`
}
type Service struct {
@@ -146,6 +163,8 @@ type Service struct {
stickyModelID string
modelMap map[string]string // official name -> internal id
remoteClient *remote.Client
bootstrapState bootstrap.Result
sessionState sessionbundle.Result
}
type promptRunResult struct {
@@ -184,6 +203,24 @@ func New(cfg Config) *Service {
if cfg.SessionMode == "" {
cfg.SessionMode = SessionModeAuto
}
if strings.TrimSpace(cfg.LingmaSourceType) == "" {
cfg.LingmaSourceType = "marketplace"
}
if strings.TrimSpace(cfg.LingmaMarketplacePublisher) == "" {
cfg.LingmaMarketplacePublisher = "Alibaba-Cloud"
}
if strings.TrimSpace(cfg.LingmaMarketplaceExtension) == "" {
cfg.LingmaMarketplaceExtension = "tongyi-lingma"
}
if strings.TrimSpace(cfg.LingmaBinaryPath) == "" {
cfg.LingmaBinaryPath = filepath.Join(os.TempDir(), "lingma-proxy", "bin", "Lingma")
}
if strings.TrimSpace(cfg.LingmaBootstrapOutputDir) == "" {
cfg.LingmaBootstrapOutputDir = filepath.Join(filepath.Dir(cfg.LingmaBinaryPath), "release")
}
if strings.TrimSpace(cfg.LingmaWorkDir) == "" {
cfg.LingmaWorkDir = filepath.Join(filepath.Dir(filepath.Dir(cfg.LingmaBinaryPath)), ".lingma", "vscode", "sharedClientCache")
}
return &Service{cfg: cfg}
}
@@ -210,6 +247,44 @@ func (s *Service) DefaultModel() string {
return strings.TrimSpace(s.cfg.Model)
}
func (s *Service) PrepareRuntime() error {
s.mu.Lock()
cfg := s.cfg
s.mu.Unlock()
sessionState, err := sessionbundle.Restore(cfg.LingmaWorkDir, cfg.LingmaSessionBundle, cfg.LingmaSessionBundleFile)
if err != nil {
return err
}
if sessionState.Restored {
if err := os.Setenv("LINGMA_CACHE_DIR", cfg.LingmaWorkDir); err != nil {
return err
}
}
bootstrapState, err := bootstrap.Ensure(bootstrap.Config{
Enabled: cfg.LingmaBootstrapEnabled,
SourceType: cfg.LingmaSourceType,
VSIXURL: cfg.LingmaVSIXURL,
MarketplacePublisher: cfg.LingmaMarketplacePublisher,
MarketplaceExtension: cfg.LingmaMarketplaceExtension,
OutputDir: cfg.LingmaBootstrapOutputDir,
BinaryPath: cfg.LingmaBinaryPath,
AlwaysRefresh: cfg.LingmaBootstrapAlways,
ForceRefresh: cfg.LingmaForceRefresh,
HTTPTimeout: 30 * time.Second,
})
if err != nil {
return err
}
s.mu.Lock()
s.sessionState = sessionState
s.bootstrapState = bootstrapState
s.mu.Unlock()
return nil
}
func (s *Service) Warmup(ctx context.Context) error {
if s.backend() == BackendRemote {
return s.remoteClientLocked().Warmup(ctx)
@@ -234,22 +309,26 @@ func contextWithOptionalTimeout(parent context.Context, timeout time.Duration) (
func (s *Service) State() State {
s.mu.Lock()
defer s.mu.Unlock()
state := State{
SessionMode: s.cfg.SessionMode,
Bootstrap: s.bootstrapState,
SessionBundle: s.sessionState,
}
if s.cfg.Backend == BackendRemote {
return State{
Endpoint: remote.ResolveBaseURL(s.cfg.RemoteBaseURL),
Transport: "remote",
Connected: s.remoteClient != nil,
SessionMode: s.cfg.SessionMode,
state.Endpoint = remote.ResolveBaseURL(s.cfg.RemoteBaseURL)
state.Transport = "remote"
if status, err := remote.LoadCredentialStatus(s.cfg.RemoteAuthFile); err == nil {
state.RemoteAuth = &status
state.Connected = status.Loaded && !status.Expired
}
return state
}
return State{
PipePath: s.pipePath,
Endpoint: s.endpoint,
Transport: string(s.transport),
Connected: s.client != nil,
StickySessionID: s.stickySessionID,
SessionMode: s.cfg.SessionMode,
}
state.PipePath = s.pipePath
state.Endpoint = s.endpoint
state.Transport = string(s.transport)
state.Connected = s.client != nil
state.StickySessionID = s.stickySessionID
return state
}
func (s *Service) ListModels(ctx context.Context) ([]Model, error) {