mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-09-11 00:22:56 -05:00
fix(picoclaw): cancel runtime lifecycle on mode switch
Cancel PicoClaw start/readiness waits when MCP takes control, clear held manual input state after failed release reports, and keep paste duration below the control-mode wait budget. Also add MCP copy failure translations and avoid reconnect effect churn when the locale changes.
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
package hid
|
package hid
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
func TestReportLengthValidation(t *testing.T) {
|
func TestReportLengthValidation(t *testing.T) {
|
||||||
h := &Hid{}
|
h := &Hid{}
|
||||||
@@ -14,3 +17,12 @@ func TestReportLengthValidation(t *testing.T) {
|
|||||||
t.Fatal("expected absolute mouse length error")
|
t.Fatal("expected absolute mouse length error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPasteDurationLeavesModeSwitchMargin(t *testing.T) {
|
||||||
|
if maxPasteDuration >= 30*time.Second {
|
||||||
|
t.Fatalf("maxPasteDuration = %s, want below 30s mode switch wait budget", maxPasteDuration)
|
||||||
|
}
|
||||||
|
if got := time.Duration(maxPasteContentRunes) * defaultPasteDelay; got > maxPasteDuration {
|
||||||
|
t.Fatalf("max paste content duration = %s, want <= %s", got, maxPasteDuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type PasteReq struct {
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
defaultPasteDelay = 30 * time.Millisecond
|
defaultPasteDelay = 30 * time.Millisecond
|
||||||
maxPasteDuration = 30 * time.Second
|
maxPasteDuration = 25 * time.Second
|
||||||
maxPasteContentRunes = int(maxPasteDuration / defaultPasteDelay)
|
maxPasteContentRunes = int(maxPasteDuration / defaultPasteDelay)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -193,7 +193,7 @@ func (s *Service) Paste(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if time.Duration(typeableRunes)*defaultPasteDelay > maxPasteDuration {
|
if time.Duration(typeableRunes)*defaultPasteDelay > maxPasteDuration {
|
||||||
rsp.ErrRsp(c, -2, "paste duration exceeds 30s")
|
rsp.ErrRsp(c, -2, "paste duration exceeds 25s")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -429,6 +429,10 @@ func (s *ManualSession) complete(generation uint64, kind ManualReportKind, held
|
|||||||
case ManualAbsoluteMouse:
|
case ManualAbsoluteMouse:
|
||||||
s.absoluteMouseHeld = held
|
s.absoluteMouseHeld = held
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
s.keyboardHeld = false
|
||||||
|
s.relativeMouseHeld = false
|
||||||
|
s.absoluteMouseHeld = false
|
||||||
}
|
}
|
||||||
releaseControl, end, startCooldown := s.finishIfIdleLocked()
|
releaseControl, end, startCooldown := s.finishIfIdleLocked()
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|||||||
@@ -162,6 +162,49 @@ func TestFailedHeldManualInputDoesNotRemainHeld(t *testing.T) {
|
|||||||
release()
|
release()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFailedReleaseReportClearsHeldManualInput(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
kind ManualReportKind
|
||||||
|
}{
|
||||||
|
{name: "keyboard", kind: ManualKeyboard},
|
||||||
|
{name: "relative mouse", kind: ManualRelativeMouse},
|
||||||
|
{name: "absolute mouse", kind: ManualAbsoluteMouse},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
now := time.Unix(260, 0)
|
||||||
|
coordinator := newCoordinator(time.Second, func() time.Time { return now })
|
||||||
|
control := controlmode.NewManager(filepath.Join(t.TempDir(), "mode"), controlmode.ModeMCP)
|
||||||
|
manual := NewManualSession(control, coordinator)
|
||||||
|
defer manual.Close()
|
||||||
|
|
||||||
|
down, err := manual.Reserve(context.Background(), tc.kind, true, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
down.Complete(true)
|
||||||
|
|
||||||
|
up, err := manual.Reserve(context.Background(), tc.kind, false, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
up.Complete(false)
|
||||||
|
|
||||||
|
if _, _, err := coordinator.BeginMCP(context.Background(), OperationHID); !errors.Is(err, ErrManualControlActive) {
|
||||||
|
t.Fatalf("cooldown error = %v, want %v", err, ErrManualControlActive)
|
||||||
|
}
|
||||||
|
now = now.Add(2 * time.Second)
|
||||||
|
_, release, err := coordinator.BeginMCP(context.Background(), OperationHID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed release report left held input active: %v", err)
|
||||||
|
}
|
||||||
|
release()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPointerMoveWithoutButtonsDoesNotStartCooldown(t *testing.T) {
|
func TestPointerMoveWithoutButtonsDoesNotStartCooldown(t *testing.T) {
|
||||||
coordinator := newCoordinator(time.Second, time.Now)
|
coordinator := newCoordinator(time.Second, time.Now)
|
||||||
control := controlmode.NewManager(filepath.Join(t.TempDir(), "mode"), controlmode.ModeMCP)
|
control := controlmode.NewManager(filepath.Join(t.TempDir(), "mode"), controlmode.ModeMCP)
|
||||||
@@ -327,6 +370,40 @@ func TestBlockedActiveSessionStillAllowsReleaseReport(t *testing.T) {
|
|||||||
up.Complete(true)
|
up.Complete(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBlockedActiveSessionFailedReleaseClearsHeldReport(t *testing.T) {
|
||||||
|
now := time.Unix(325, 0)
|
||||||
|
coordinator := newCoordinator(time.Second, func() time.Time { return now })
|
||||||
|
control := controlmode.NewManager(filepath.Join(t.TempDir(), "mode"), controlmode.ModePicoclaw)
|
||||||
|
manual := NewManualSession(control, coordinator)
|
||||||
|
defer manual.Close()
|
||||||
|
|
||||||
|
down, err := manual.Reserve(context.Background(), ManualKeyboard, true, func(controlmode.Mode) bool { return true })
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
down.Complete(true)
|
||||||
|
|
||||||
|
if _, err := manual.Reserve(context.Background(), ManualKeyboard, true, func(controlmode.Mode) bool { return false }); !errors.Is(err, ErrManualInputBlocked) {
|
||||||
|
t.Fatalf("new held report error = %v, want %v", err, ErrManualInputBlocked)
|
||||||
|
}
|
||||||
|
|
||||||
|
up, err := manual.Reserve(context.Background(), ManualKeyboard, false, func(controlmode.Mode) bool { return false })
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("release report was blocked: %v", err)
|
||||||
|
}
|
||||||
|
up.Complete(false)
|
||||||
|
|
||||||
|
if _, _, err := coordinator.BeginMCP(context.Background(), OperationHID); !errors.Is(err, ErrManualControlActive) {
|
||||||
|
t.Fatalf("cooldown error = %v, want %v", err, ErrManualControlActive)
|
||||||
|
}
|
||||||
|
now = now.Add(2 * time.Second)
|
||||||
|
_, release, err := coordinator.BeginMCP(context.Background(), OperationHID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed blocked release report left held input active: %v", err)
|
||||||
|
}
|
||||||
|
release()
|
||||||
|
}
|
||||||
|
|
||||||
func TestReadOnlyMCPAllowedDuringManualControl(t *testing.T) {
|
func TestReadOnlyMCPAllowedDuringManualControl(t *testing.T) {
|
||||||
coordinator := newCoordinator(defaultManualCooldown, time.Now)
|
coordinator := newCoordinator(defaultManualCooldown, time.Now)
|
||||||
control := controlmode.NewManager(filepath.Join(t.TempDir(), "mode"), controlmode.ModeMCP)
|
control := controlmode.NewManager(filepath.Join(t.TempDir(), "mode"), controlmode.ModeMCP)
|
||||||
|
|||||||
@@ -104,6 +104,17 @@ func (s *Service) beginControlOperation(parent context.Context) (context.Context
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) beginRuntimeLifecycleOperation(parent context.Context) (context.Context, func()) {
|
||||||
|
if parent == nil {
|
||||||
|
parent = context.Background()
|
||||||
|
}
|
||||||
|
if s == nil || s.operations == nil {
|
||||||
|
ctx, cancel := context.WithCancelCause(parent)
|
||||||
|
return ctx, func() { cancel(context.Canceled) }
|
||||||
|
}
|
||||||
|
return s.operations.begin(parent)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) CancelActiveControlOperations() int {
|
func (s *Service) CancelActiveControlOperations() int {
|
||||||
if s == nil || s.operations == nil {
|
if s == nil || s.operations == nil {
|
||||||
return 0
|
return 0
|
||||||
@@ -115,6 +126,22 @@ func (s *Service) CancelActiveControlOperations() int {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runtimeLifecycleOperationError(ctx context.Context) *PicoclawError {
|
||||||
|
if ctx == nil || ctx.Err() == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cause := context.Cause(ctx)
|
||||||
|
switch {
|
||||||
|
case errors.Is(cause, errControlModeSwitch):
|
||||||
|
return newPicoclawError(CodeControlModeConflict, "PicoClaw runtime lifecycle canceled because the control mode is switching")
|
||||||
|
case errors.Is(cause, context.DeadlineExceeded):
|
||||||
|
return newPicoclawError(CodeRuntimeUnavailable, "PicoClaw runtime lifecycle timed out")
|
||||||
|
default:
|
||||||
|
return newPicoclawError(CodeRuntimeUnavailable, "PicoClaw runtime lifecycle was canceled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func controlOperationError(ctx context.Context) *PicoclawError {
|
func controlOperationError(ctx context.Context) *PicoclawError {
|
||||||
if ctx == nil || ctx.Err() == nil {
|
if ctx == nil || ctx.Err() == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -67,6 +67,70 @@ func TestControlModeSwitchCancelsActiveWait(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestControlModeSwitchCancelsRuntimeLifecycle(t *testing.T) {
|
||||||
|
control := controlmode.NewManager(filepath.Join(t.TempDir(), "mode"), controlmode.ModePicoclaw)
|
||||||
|
service := &Service{
|
||||||
|
control: control,
|
||||||
|
operations: newControlOperationTracker(),
|
||||||
|
}
|
||||||
|
|
||||||
|
operationCtx, releaseOperation := service.beginRuntimeLifecycleOperation(context.Background())
|
||||||
|
releaseMode, modeErr := service.acquireControlMode()
|
||||||
|
if modeErr != nil {
|
||||||
|
t.Fatal(modeErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleDone := make(chan *PicoclawError, 1)
|
||||||
|
go func() {
|
||||||
|
<-operationCtx.Done()
|
||||||
|
lifecycleErr := runtimeLifecycleOperationError(operationCtx)
|
||||||
|
releaseOperation()
|
||||||
|
releaseMode()
|
||||||
|
lifecycleDone <- lifecycleErr
|
||||||
|
}()
|
||||||
|
|
||||||
|
switchDone := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
switchDone <- control.Switch(controlmode.ModeMCP, func() error {
|
||||||
|
service.CancelActiveControlOperations()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-switchDone:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("control mode switch did not cancel the runtime lifecycle operation")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case lifecycleErr := <-lifecycleDone:
|
||||||
|
if lifecycleErr == nil || lifecycleErr.Code != CodeControlModeConflict {
|
||||||
|
t.Fatalf("lifecycle error = %+v, want %s", lifecycleErr, CodeControlModeConflict)
|
||||||
|
}
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("canceled runtime lifecycle operation did not return")
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := control.Current(); got != controlmode.ModeMCP {
|
||||||
|
t.Fatalf("mode = %q, want %q", got, controlmode.ModeMCP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRuntimeReadyWaitHonorsLifecycleCancellation(t *testing.T) {
|
||||||
|
service := &Service{}
|
||||||
|
ctx, cancel := context.WithCancelCause(context.Background())
|
||||||
|
cancel(errControlModeSwitch)
|
||||||
|
|
||||||
|
err := service.waitForRuntimeReadyContext(ctx, time.Hour)
|
||||||
|
if err == nil || err.Code != CodeControlModeConflict {
|
||||||
|
t.Fatalf("error = %+v, want %s", err, CodeControlModeConflict)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestWaitDurationIsBounded(t *testing.T) {
|
func TestWaitDurationIsBounded(t *testing.T) {
|
||||||
service := &Service{}
|
service := &Service{}
|
||||||
_, err := service.executeAction(context.Background(), Action{
|
_, err := service.executeAction(context.Background(), Action{
|
||||||
|
|||||||
@@ -35,16 +35,24 @@ func (s *Service) StartRuntime(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
defer releaseControl()
|
defer releaseControl()
|
||||||
|
|
||||||
|
operationCtx, releaseOperation := s.beginRuntimeLifecycleOperation(c.Request.Context())
|
||||||
|
defer releaseOperation()
|
||||||
|
|
||||||
unlockLifecycle := s.lockRuntimeLifecycle()
|
unlockLifecycle := s.lockRuntimeLifecycle()
|
||||||
defer unlockLifecycle()
|
defer unlockLifecycle()
|
||||||
|
|
||||||
|
if lifecycleErr := runtimeLifecycleOperationError(operationCtx); lifecycleErr != nil {
|
||||||
|
writePicoclawErrorWithData(c, lifecycleErr, gin.H{"status": s.runtimeStatus()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if currentStatus := s.runtime.Get(); currentStatus.Installing {
|
if currentStatus := s.runtime.Get(); currentStatus.Installing {
|
||||||
runtimeErr := newPicoclawError(CodeRuntimeUnavailable, "picoclaw installation is in progress")
|
runtimeErr := newPicoclawError(CodeRuntimeUnavailable, "picoclaw installation is in progress")
|
||||||
writePicoclawErrorWithData(c, runtimeErr, gin.H{"status": s.runtimeStatus()})
|
writePicoclawErrorWithData(c, runtimeErr, gin.H{"status": s.runtimeStatus()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if readyErr := s.ensureRuntimeReadyForLifecycle(); readyErr == nil {
|
if readyErr := s.ensureRuntimeReadyForLifecycleContext(operationCtx); readyErr == nil {
|
||||||
s.setRuntimeIntentDesired(true, "web")
|
s.setRuntimeIntentDesired(true, "web")
|
||||||
status := s.runtimeStatus()
|
status := s.runtimeStatus()
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
@@ -67,8 +75,9 @@ func (s *Service) StartRuntime(c *gin.Context) {
|
|||||||
status.CheckedAt = time.Now()
|
status.CheckedAt = time.Now()
|
||||||
})
|
})
|
||||||
|
|
||||||
command, output, startErr := s.startRuntime()
|
command, output, startErr := s.startRuntimeContext(operationCtx)
|
||||||
if startErr != nil {
|
if startErr != nil {
|
||||||
|
s.markRuntimeLifecycleCanceled(startErr)
|
||||||
s.setRuntimeIntentError(startErr.Message)
|
s.setRuntimeIntentError(startErr.Message)
|
||||||
status := s.runtimeStatus()
|
status := s.runtimeStatus()
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
|
|||||||
@@ -14,9 +14,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) startRuntime() (string, string, *PicoclawError) {
|
func (s *Service) startRuntime() (string, string, *PicoclawError) {
|
||||||
|
return s.startRuntimeContext(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) startRuntimeContext(ctx context.Context) (string, string, *PicoclawError) {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return "", "", newPicoclawError(CodeRuntimeStartFailed, "picoclaw service is unavailable")
|
return "", "", newPicoclawError(CodeRuntimeStartFailed, "picoclaw service is unavailable")
|
||||||
}
|
}
|
||||||
|
if lifecycleErr := runtimeLifecycleOperationError(ctx); lifecycleErr != nil {
|
||||||
|
return "", "", lifecycleErr
|
||||||
|
}
|
||||||
s.ensureDependencies()
|
s.ensureDependencies()
|
||||||
|
|
||||||
if installed, statErr := isPicoclawInstalled(); statErr != nil {
|
if installed, statErr := isPicoclawInstalled(); statErr != nil {
|
||||||
@@ -77,12 +87,15 @@ func (s *Service) startRuntime() (string, string, *PicoclawError) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
command := scriptPath + " start"
|
command := scriptPath + " start"
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), picoclawStartTimeout)
|
startCtx, cancel := context.WithTimeout(ctx, picoclawStartTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
output, execErr := exec.CommandContext(ctx, "sh", "-c", command).CombinedOutput()
|
output, execErr := exec.CommandContext(startCtx, "sh", "-c", command).CombinedOutput()
|
||||||
trimmedOutput := strings.TrimSpace(string(output))
|
trimmedOutput := strings.TrimSpace(string(output))
|
||||||
if execErr != nil {
|
if execErr != nil {
|
||||||
|
if lifecycleErr := runtimeLifecycleOperationError(ctx); lifecycleErr != nil {
|
||||||
|
return command, trimmedOutput, lifecycleErr
|
||||||
|
}
|
||||||
s.runtime.Update(func(status *RuntimeStatus) {
|
s.runtime.Update(func(status *RuntimeStatus) {
|
||||||
status.Ready = false
|
status.Ready = false
|
||||||
status.Installed = true
|
status.Installed = true
|
||||||
@@ -96,17 +109,24 @@ func (s *Service) startRuntime() (string, string, *PicoclawError) {
|
|||||||
return command, trimmedOutput, newPicoclawError(CodeRuntimeStartFailed, "failed to start picoclaw runtime")
|
return command, trimmedOutput, newPicoclawError(CodeRuntimeStartFailed, "failed to start picoclaw runtime")
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(picoclawStartWaitPeriod)
|
if lifecycleErr := sleepRuntimeLifecycleContext(ctx, picoclawStartWaitPeriod); lifecycleErr != nil {
|
||||||
if runtimeErr := s.waitForRuntimeReady(picoclawStartTimeout); runtimeErr != nil {
|
return command, trimmedOutput, lifecycleErr
|
||||||
|
}
|
||||||
|
if runtimeErr := s.waitForRuntimeReadyContext(ctx, picoclawStartTimeout); runtimeErr != nil {
|
||||||
startErr := newPicoclawError(CodeRuntimeStartFailed, runtimeErr.Message)
|
startErr := newPicoclawError(CodeRuntimeStartFailed, runtimeErr.Message)
|
||||||
failureStatus := "unavailable"
|
failureStatus := "unavailable"
|
||||||
if cleanupErr := s.stopRuntimeAndVerify(true); cleanupErr != nil {
|
if runtimeErr.Code == CodeControlModeConflict {
|
||||||
failureStatus = "error"
|
startErr = runtimeErr
|
||||||
startErr.Message = fmt.Sprintf(
|
failureStatus = "starting"
|
||||||
"%s; failed to stop partially started runtime: %v",
|
} else {
|
||||||
startErr.Message,
|
if cleanupErr := s.stopRuntimeAndVerify(true); cleanupErr != nil {
|
||||||
cleanupErr,
|
failureStatus = "error"
|
||||||
)
|
startErr.Message = fmt.Sprintf(
|
||||||
|
"%s; failed to stop partially started runtime: %v",
|
||||||
|
startErr.Message,
|
||||||
|
cleanupErr,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
s.runtime.Update(func(status *RuntimeStatus) {
|
s.runtime.Update(func(status *RuntimeStatus) {
|
||||||
status.Ready = false
|
status.Ready = false
|
||||||
@@ -121,6 +141,23 @@ func (s *Service) startRuntime() (string, string, *PicoclawError) {
|
|||||||
return command, trimmedOutput, nil
|
return command, trimmedOutput, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) markRuntimeLifecycleCanceled(err *PicoclawError) {
|
||||||
|
if s == nil || err == nil || err.Code != CodeControlModeConflict {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.ensureDependencies()
|
||||||
|
s.runtime.Update(func(status *RuntimeStatus) {
|
||||||
|
status.Ready = false
|
||||||
|
status.Restoring = false
|
||||||
|
if isRuntimeLifecycleStatusPending(*status) {
|
||||||
|
status.Status = "stopped"
|
||||||
|
}
|
||||||
|
status.LastError = err.Message
|
||||||
|
status.CurrentSession = ""
|
||||||
|
status.CheckedAt = time.Now()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) stopRuntime() (string, string, *PicoclawError) {
|
func (s *Service) stopRuntime() (string, string, *PicoclawError) {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return "", "", newPicoclawError(CodeRuntimeStartFailed, "picoclaw service is unavailable")
|
return "", "", newPicoclawError(CodeRuntimeStartFailed, "picoclaw service is unavailable")
|
||||||
@@ -183,11 +220,22 @@ func (s *Service) stopRuntime() (string, string, *PicoclawError) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) waitForRuntimeReady(timeout time.Duration) *PicoclawError {
|
func (s *Service) waitForRuntimeReady(timeout time.Duration) *PicoclawError {
|
||||||
|
return s.waitForRuntimeReadyContext(context.Background(), timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) waitForRuntimeReadyContext(ctx context.Context, timeout time.Duration) *PicoclawError {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
deadline := time.Now().Add(timeout)
|
deadline := time.Now().Add(timeout)
|
||||||
var lastErr *PicoclawError
|
var lastErr *PicoclawError
|
||||||
|
|
||||||
for {
|
for {
|
||||||
runtimeErr := s.ensureRuntimeReadyForLifecycle()
|
if lifecycleErr := runtimeLifecycleOperationError(ctx); lifecycleErr != nil {
|
||||||
|
return lifecycleErr
|
||||||
|
}
|
||||||
|
|
||||||
|
runtimeErr := s.ensureRuntimeReadyForLifecycleContext(ctx)
|
||||||
if runtimeErr == nil {
|
if runtimeErr == nil {
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
@@ -198,7 +246,9 @@ func (s *Service) waitForRuntimeReady(timeout time.Duration) *PicoclawError {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(picoclawReadyPollPeriod)
|
if lifecycleErr := sleepRuntimeLifecycleContext(ctx, picoclawReadyPollPeriod); lifecycleErr != nil {
|
||||||
|
return lifecycleErr
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if lastErr != nil {
|
if lastErr != nil {
|
||||||
@@ -218,18 +268,31 @@ func resolvePicoclawStartScript() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runPicoclawOnboard() (string, *PicoclawError) {
|
func runPicoclawOnboard() (string, *PicoclawError) {
|
||||||
|
return runPicoclawOnboardContext(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPicoclawOnboardContext(ctx context.Context) (string, *PicoclawError) {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
scriptPath, err := resolvePicoclawStartScript()
|
scriptPath, err := resolvePicoclawStartScript()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", newPicoclawError(CodeRuntimeUnavailable, err.Error())
|
return "", newPicoclawError(CodeRuntimeUnavailable, err.Error())
|
||||||
}
|
}
|
||||||
|
if lifecycleErr := runtimeLifecycleOperationError(ctx); lifecycleErr != nil {
|
||||||
|
return "", lifecycleErr
|
||||||
|
}
|
||||||
|
|
||||||
command := scriptPath + " onboard"
|
command := scriptPath + " onboard"
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), picoclawOnboardTimeout)
|
onboardCtx, cancel := context.WithTimeout(ctx, picoclawOnboardTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
output, execErr := exec.CommandContext(ctx, "sh", "-c", command).CombinedOutput()
|
output, execErr := exec.CommandContext(onboardCtx, "sh", "-c", command).CombinedOutput()
|
||||||
trimmedOutput := strings.TrimSpace(string(output))
|
trimmedOutput := strings.TrimSpace(string(output))
|
||||||
if execErr != nil {
|
if execErr != nil {
|
||||||
|
if lifecycleErr := runtimeLifecycleOperationError(ctx); lifecycleErr != nil {
|
||||||
|
return trimmedOutput, lifecycleErr
|
||||||
|
}
|
||||||
if trimmedOutput == "" {
|
if trimmedOutput == "" {
|
||||||
trimmedOutput = execErr.Error()
|
trimmedOutput = execErr.Error()
|
||||||
}
|
}
|
||||||
@@ -239,6 +302,23 @@ func runPicoclawOnboard() (string, *PicoclawError) {
|
|||||||
return trimmedOutput, nil
|
return trimmedOutput, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sleepRuntimeLifecycleContext(ctx context.Context, delay time.Duration) *PicoclawError {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
if delay <= 0 {
|
||||||
|
return runtimeLifecycleOperationError(ctx)
|
||||||
|
}
|
||||||
|
timer := time.NewTimer(delay)
|
||||||
|
defer timer.Stop()
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return runtimeLifecycleOperationError(ctx)
|
||||||
|
case <-timer.C:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func isPicoclawInstalled() (bool, error) {
|
func isPicoclawInstalled() (bool, error) {
|
||||||
info, err := os.Stat(picoclawBinaryPath)
|
info, err := os.Stat(picoclawBinaryPath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package picoclaw
|
package picoclaw
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -355,17 +356,24 @@ func (s *Service) lockRuntimeLifecycle() func() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ensureRuntimeReady() *PicoclawError {
|
func (s *Service) ensureRuntimeReady() *PicoclawError {
|
||||||
return s.ensureRuntimeReadyWithProbeProtection(false)
|
return s.ensureRuntimeReadyWithProbeProtection(context.Background(), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ensureRuntimeReadyForLifecycle() *PicoclawError {
|
func (s *Service) ensureRuntimeReadyForLifecycle() *PicoclawError {
|
||||||
return s.ensureRuntimeReadyWithProbeProtection(true)
|
return s.ensureRuntimeReadyForLifecycleContext(context.Background())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ensureRuntimeReadyWithProbeProtection(allowLifecycleOverwrite bool) *PicoclawError {
|
func (s *Service) ensureRuntimeReadyForLifecycleContext(ctx context.Context) *PicoclawError {
|
||||||
|
return s.ensureRuntimeReadyWithProbeProtection(ctx, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ensureRuntimeReadyWithProbeProtection(ctx context.Context, allowLifecycleOverwrite bool) *PicoclawError {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return newPicoclawError(CodeRuntimeUnavailable, "picoclaw service is unavailable")
|
return newPicoclawError(CodeRuntimeUnavailable, "picoclaw service is unavailable")
|
||||||
}
|
}
|
||||||
|
if lifecycleErr := runtimeLifecycleOperationError(ctx); lifecycleErr != nil {
|
||||||
|
return lifecycleErr
|
||||||
|
}
|
||||||
s.ensureDependencies()
|
s.ensureDependencies()
|
||||||
setStatus := func(status RuntimeStatus) {
|
setStatus := func(status RuntimeStatus) {
|
||||||
if allowLifecycleOverwrite {
|
if allowLifecycleOverwrite {
|
||||||
@@ -440,7 +448,10 @@ func (s *Service) ensureRuntimeReadyWithProbeProtection(allowLifecycleOverwrite
|
|||||||
}
|
}
|
||||||
if _, err := os.Stat(configPath); err != nil {
|
if _, err := os.Stat(configPath); err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
if _, onboardErr := runPicoclawOnboard(); onboardErr != nil {
|
if lifecycleErr := runtimeLifecycleOperationError(ctx); lifecycleErr != nil {
|
||||||
|
return lifecycleErr
|
||||||
|
}
|
||||||
|
if _, onboardErr := runPicoclawOnboardContext(ctx); onboardErr != nil {
|
||||||
setStatus(RuntimeStatus{
|
setStatus(RuntimeStatus{
|
||||||
Ready: false,
|
Ready: false,
|
||||||
Installed: true,
|
Installed: true,
|
||||||
@@ -455,6 +466,9 @@ func (s *Service) ensureRuntimeReadyWithProbeProtection(allowLifecycleOverwrite
|
|||||||
})
|
})
|
||||||
return newPicoclawError(CodeRuntimeUnavailable, "picoclaw model is not configured")
|
return newPicoclawError(CodeRuntimeUnavailable, "picoclaw model is not configured")
|
||||||
}
|
}
|
||||||
|
if lifecycleErr := runtimeLifecycleOperationError(ctx); lifecycleErr != nil {
|
||||||
|
return lifecycleErr
|
||||||
|
}
|
||||||
if _, statErr := os.Stat(configPath); statErr == nil {
|
if _, statErr := os.Stat(configPath); statErr == nil {
|
||||||
goto configReady
|
goto configReady
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -290,6 +290,7 @@ const ca = {
|
|||||||
enableConfirmDesc:
|
enableConfirmDesc:
|
||||||
'En activar MCP, PicoClaw s’aturarà i es tancarà qualsevol sessió activa de PicoClaw.',
|
'En activar MCP, PicoClaw s’aturarà i es tancarà qualsevol sessió activa de PicoClaw.',
|
||||||
failed: 'L’operació MCP ha fallat',
|
failed: 'L’operació MCP ha fallat',
|
||||||
|
copyFailed: 'La còpia ha fallat. Copieu-ho manualment.',
|
||||||
okBtn: 'Confirma',
|
okBtn: 'Confirma',
|
||||||
cancelBtn: 'Cancel·la'
|
cancelBtn: 'Cancel·la'
|
||||||
},
|
},
|
||||||
@@ -599,7 +600,6 @@ const ca = {
|
|||||||
success: 'PicoClaw instal·lat correctament',
|
success: 'PicoClaw instal·lat correctament',
|
||||||
failed: "No s'ha pogut instal·lar PicoClaw",
|
failed: "No s'ha pogut instal·lar PicoClaw",
|
||||||
uninstalling: "S'està desinstal·lant el temps d'execució...",
|
uninstalling: "S'està desinstal·lant el temps d'execució...",
|
||||||
uninstaling: "S'està desinstal·lant el temps d'execució...",
|
|
||||||
uninstalled: "El temps d'execució s'ha desinstal·lat correctament.",
|
uninstalled: "El temps d'execució s'ha desinstal·lat correctament.",
|
||||||
uninstallFailed: 'La desinstal·lació ha fallat.',
|
uninstallFailed: 'La desinstal·lació ha fallat.',
|
||||||
requiredTitle: 'PicoClaw no està instal·lat',
|
requiredTitle: 'PicoClaw no està instal·lat',
|
||||||
|
|||||||
@@ -293,6 +293,7 @@ const cz = {
|
|||||||
enableConfirmDesc:
|
enableConfirmDesc:
|
||||||
'Povolením MCP se zastaví PicoClaw a ukončí se všechny aktivní relace PicoClaw.',
|
'Povolením MCP se zastaví PicoClaw a ukončí se všechny aktivní relace PicoClaw.',
|
||||||
failed: 'Operace MCP se nezdařila',
|
failed: 'Operace MCP se nezdařila',
|
||||||
|
copyFailed: 'Kopírování se nezdařilo. Zkopírujte ručně.',
|
||||||
okBtn: 'Potvrdit',
|
okBtn: 'Potvrdit',
|
||||||
cancelBtn: 'Zrušit'
|
cancelBtn: 'Zrušit'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -290,6 +290,7 @@ const da = {
|
|||||||
enableConfirmDesc:
|
enableConfirmDesc:
|
||||||
'Aktivering af MCP stopper PicoClaw og lukker alle aktive PicoClaw-sessioner.',
|
'Aktivering af MCP stopper PicoClaw og lukker alle aktive PicoClaw-sessioner.',
|
||||||
failed: 'MCP-handlingen mislykkedes',
|
failed: 'MCP-handlingen mislykkedes',
|
||||||
|
copyFailed: 'Kopiering mislykkedes. Kopiér manuelt.',
|
||||||
okBtn: 'Bekræft',
|
okBtn: 'Bekræft',
|
||||||
cancelBtn: 'Annuller'
|
cancelBtn: 'Annuller'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -296,6 +296,7 @@ const de = {
|
|||||||
enableConfirmDesc:
|
enableConfirmDesc:
|
||||||
'Durch Aktivieren von MCP wird PicoClaw gestoppt und jede aktive PicoClaw-Sitzung geschlossen.',
|
'Durch Aktivieren von MCP wird PicoClaw gestoppt und jede aktive PicoClaw-Sitzung geschlossen.',
|
||||||
failed: 'MCP-Aktion fehlgeschlagen',
|
failed: 'MCP-Aktion fehlgeschlagen',
|
||||||
|
copyFailed: 'Kopieren fehlgeschlagen. Bitte manuell kopieren.',
|
||||||
okBtn: 'Bestätigen',
|
okBtn: 'Bestätigen',
|
||||||
cancelBtn: 'Abbrechen'
|
cancelBtn: 'Abbrechen'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -293,6 +293,7 @@ const es = {
|
|||||||
enableConfirmDesc:
|
enableConfirmDesc:
|
||||||
'Al activar MCP se detendrá PicoClaw y se cerrará cualquier sesión activa de PicoClaw.',
|
'Al activar MCP se detendrá PicoClaw y se cerrará cualquier sesión activa de PicoClaw.',
|
||||||
failed: 'Error en la operación MCP',
|
failed: 'Error en la operación MCP',
|
||||||
|
copyFailed: 'Error al copiar. Copia manualmente.',
|
||||||
okBtn: 'Confirmar',
|
okBtn: 'Confirmar',
|
||||||
cancelBtn: 'Cancelar'
|
cancelBtn: 'Cancelar'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -295,6 +295,7 @@ const fr = {
|
|||||||
enableConfirmDesc:
|
enableConfirmDesc:
|
||||||
'L’activation de MCP arrêtera PicoClaw et fermera toute session PicoClaw active.',
|
'L’activation de MCP arrêtera PicoClaw et fermera toute session PicoClaw active.',
|
||||||
failed: 'Échec de l’opération MCP',
|
failed: 'Échec de l’opération MCP',
|
||||||
|
copyFailed: 'La copie a échoué. Copiez manuellement.',
|
||||||
okBtn: 'Confirmer',
|
okBtn: 'Confirmer',
|
||||||
cancelBtn: 'Annuler'
|
cancelBtn: 'Annuler'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -294,6 +294,7 @@ const hu = {
|
|||||||
enableConfirmDesc:
|
enableConfirmDesc:
|
||||||
'Az MCP engedélyezése leállítja a PicoClaw-t, és bezár minden aktív PicoClaw-munkamenetet.',
|
'Az MCP engedélyezése leállítja a PicoClaw-t, és bezár minden aktív PicoClaw-munkamenetet.',
|
||||||
failed: 'Az MCP-művelet sikertelen',
|
failed: 'Az MCP-művelet sikertelen',
|
||||||
|
copyFailed: 'A másolás sikertelen. Másolja kézzel.',
|
||||||
okBtn: 'Megerősítés',
|
okBtn: 'Megerősítés',
|
||||||
cancelBtn: 'Mégse'
|
cancelBtn: 'Mégse'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -292,6 +292,7 @@ const id = {
|
|||||||
enableConfirmDesc:
|
enableConfirmDesc:
|
||||||
'Mengaktifkan MCP akan menghentikan PicoClaw dan menutup semua sesi PicoClaw yang aktif.',
|
'Mengaktifkan MCP akan menghentikan PicoClaw dan menutup semua sesi PicoClaw yang aktif.',
|
||||||
failed: 'Operasi MCP gagal',
|
failed: 'Operasi MCP gagal',
|
||||||
|
copyFailed: 'Gagal menyalin. Salin secara manual.',
|
||||||
okBtn: 'Konfirmasi',
|
okBtn: 'Konfirmasi',
|
||||||
cancelBtn: 'Batal'
|
cancelBtn: 'Batal'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -294,6 +294,7 @@ const it = {
|
|||||||
enableConfirmDesc:
|
enableConfirmDesc:
|
||||||
'L’abilitazione di MCP arresterà PicoClaw e chiuderà tutte le sessioni PicoClaw attive.',
|
'L’abilitazione di MCP arresterà PicoClaw e chiuderà tutte le sessioni PicoClaw attive.',
|
||||||
failed: 'Operazione MCP non riuscita',
|
failed: 'Operazione MCP non riuscita',
|
||||||
|
copyFailed: 'Copia non riuscita. Copia manualmente.',
|
||||||
okBtn: 'Conferma',
|
okBtn: 'Conferma',
|
||||||
cancelBtn: 'Annulla'
|
cancelBtn: 'Annulla'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -293,6 +293,7 @@ const ja = {
|
|||||||
enableConfirmDesc:
|
enableConfirmDesc:
|
||||||
'MCP を有効にすると PicoClaw が停止し、アクティブな PicoClaw セッションがすべて終了します。',
|
'MCP を有効にすると PicoClaw が停止し、アクティブな PicoClaw セッションがすべて終了します。',
|
||||||
failed: 'MCP 操作に失敗しました',
|
failed: 'MCP 操作に失敗しました',
|
||||||
|
copyFailed: 'コピーに失敗しました。手動でコピーしてください。',
|
||||||
okBtn: '確認',
|
okBtn: '確認',
|
||||||
cancelBtn: 'キャンセル'
|
cancelBtn: 'キャンセル'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ const ko = {
|
|||||||
enableConfirmDesc:
|
enableConfirmDesc:
|
||||||
'MCP를 활성화하면 PicoClaw가 중지되고 활성 PicoClaw 세션이 모두 닫힙니다.',
|
'MCP를 활성화하면 PicoClaw가 중지되고 활성 PicoClaw 세션이 모두 닫힙니다.',
|
||||||
failed: 'MCP 작업에 실패했습니다',
|
failed: 'MCP 작업에 실패했습니다',
|
||||||
|
copyFailed: '복사에 실패했습니다. 수동으로 복사하세요.',
|
||||||
okBtn: '확인',
|
okBtn: '확인',
|
||||||
cancelBtn: '취소'
|
cancelBtn: '취소'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ const nb = {
|
|||||||
enableConfirmDesc:
|
enableConfirmDesc:
|
||||||
'Aktivering av MCP stopper PicoClaw og lukker alle aktive PicoClaw-økter.',
|
'Aktivering av MCP stopper PicoClaw og lukker alle aktive PicoClaw-økter.',
|
||||||
failed: 'MCP-operasjonen mislyktes',
|
failed: 'MCP-operasjonen mislyktes',
|
||||||
|
copyFailed: 'Kopiering mislyktes. Kopier manuelt.',
|
||||||
okBtn: 'Bekreft',
|
okBtn: 'Bekreft',
|
||||||
cancelBtn: 'Avbryt'
|
cancelBtn: 'Avbryt'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -294,6 +294,7 @@ const nl = {
|
|||||||
enableConfirmDesc:
|
enableConfirmDesc:
|
||||||
'Als MCP wordt ingeschakeld, stopt PicoClaw en worden alle actieve PicoClaw-sessies gesloten.',
|
'Als MCP wordt ingeschakeld, stopt PicoClaw en worden alle actieve PicoClaw-sessies gesloten.',
|
||||||
failed: 'MCP-bewerking mislukt',
|
failed: 'MCP-bewerking mislukt',
|
||||||
|
copyFailed: 'Kopiëren mislukt. Kopieer handmatig.',
|
||||||
okBtn: 'Bevestigen',
|
okBtn: 'Bevestigen',
|
||||||
cancelBtn: 'Annuleren'
|
cancelBtn: 'Annuleren'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -293,6 +293,7 @@ const pl = {
|
|||||||
enableConfirmDesc:
|
enableConfirmDesc:
|
||||||
'Włączenie MCP zatrzyma PicoClaw i zamknie wszystkie aktywne sesje PicoClaw.',
|
'Włączenie MCP zatrzyma PicoClaw i zamknie wszystkie aktywne sesje PicoClaw.',
|
||||||
failed: 'Operacja MCP nie powiodła się',
|
failed: 'Operacja MCP nie powiodła się',
|
||||||
|
copyFailed: 'Kopiowanie nie powiodło się. Skopiuj ręcznie.',
|
||||||
okBtn: 'Potwierdź',
|
okBtn: 'Potwierdź',
|
||||||
cancelBtn: 'Anuluj'
|
cancelBtn: 'Anuluj'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -292,6 +292,7 @@ const pt_br = {
|
|||||||
enableConfirmDesc:
|
enableConfirmDesc:
|
||||||
'Habilitar o MCP interromperá o PicoClaw e fechará todas as sessões ativas do PicoClaw.',
|
'Habilitar o MCP interromperá o PicoClaw e fechará todas as sessões ativas do PicoClaw.',
|
||||||
failed: 'Falha na operação MCP',
|
failed: 'Falha na operação MCP',
|
||||||
|
copyFailed: 'Falha ao copiar. Copie manualmente.',
|
||||||
okBtn: 'Confirmar',
|
okBtn: 'Confirmar',
|
||||||
cancelBtn: 'Cancelar'
|
cancelBtn: 'Cancelar'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -292,6 +292,7 @@ const ru = {
|
|||||||
enableConfirmDesc:
|
enableConfirmDesc:
|
||||||
'Включение MCP остановит PicoClaw и закроет все активные сеансы PicoClaw.',
|
'Включение MCP остановит PicoClaw и закроет все активные сеансы PicoClaw.',
|
||||||
failed: 'Операция MCP завершилась с ошибкой',
|
failed: 'Операция MCP завершилась с ошибкой',
|
||||||
|
copyFailed: 'Не удалось скопировать. Скопируйте вручную.',
|
||||||
okBtn: 'Подтвердить',
|
okBtn: 'Подтвердить',
|
||||||
cancelBtn: 'Отмена'
|
cancelBtn: 'Отмена'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -289,6 +289,7 @@ const se = {
|
|||||||
enableConfirmDesc:
|
enableConfirmDesc:
|
||||||
'Om MCP aktiveras stoppas PicoClaw och alla aktiva PicoClaw-sessioner stängs.',
|
'Om MCP aktiveras stoppas PicoClaw och alla aktiva PicoClaw-sessioner stängs.',
|
||||||
failed: 'MCP-åtgärden misslyckades',
|
failed: 'MCP-åtgärden misslyckades',
|
||||||
|
copyFailed: 'Kopiering misslyckades. Kopiera manuellt.',
|
||||||
okBtn: 'Bekräfta',
|
okBtn: 'Bekräfta',
|
||||||
cancelBtn: 'Avbryt'
|
cancelBtn: 'Avbryt'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -285,6 +285,7 @@ const th = {
|
|||||||
enableConfirmDesc:
|
enableConfirmDesc:
|
||||||
'การเปิดใช้งาน MCP จะหยุด PicoClaw และปิดเซสชัน PicoClaw ที่ใช้งานอยู่ทั้งหมด',
|
'การเปิดใช้งาน MCP จะหยุด PicoClaw และปิดเซสชัน PicoClaw ที่ใช้งานอยู่ทั้งหมด',
|
||||||
failed: 'การดำเนินการ MCP ล้มเหลว',
|
failed: 'การดำเนินการ MCP ล้มเหลว',
|
||||||
|
copyFailed: 'คัดลอกไม่สำเร็จ โปรดคัดลอกด้วยตนเอง',
|
||||||
okBtn: 'ยืนยัน',
|
okBtn: 'ยืนยัน',
|
||||||
cancelBtn: 'ยกเลิก'
|
cancelBtn: 'ยกเลิก'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ const tr = {
|
|||||||
enableConfirmDesc:
|
enableConfirmDesc:
|
||||||
'MCP etkinleştirildiğinde PicoClaw durdurulur ve tüm etkin PicoClaw oturumları kapatılır.',
|
'MCP etkinleştirildiğinde PicoClaw durdurulur ve tüm etkin PicoClaw oturumları kapatılır.',
|
||||||
failed: 'MCP işlemi başarısız oldu',
|
failed: 'MCP işlemi başarısız oldu',
|
||||||
|
copyFailed: 'Kopyalama başarısız. Elle kopyalayın.',
|
||||||
okBtn: 'Onayla',
|
okBtn: 'Onayla',
|
||||||
cancelBtn: 'İptal'
|
cancelBtn: 'İptal'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -292,6 +292,7 @@ const uk = {
|
|||||||
enableConfirmTitle: 'Увімкнути зовнішнє керування MCP?',
|
enableConfirmTitle: 'Увімкнути зовнішнє керування MCP?',
|
||||||
enableConfirmDesc: 'Увімкнення MCP зупинить PicoClaw і закриє всі активні сеанси PicoClaw.',
|
enableConfirmDesc: 'Увімкнення MCP зупинить PicoClaw і закриє всі активні сеанси PicoClaw.',
|
||||||
failed: 'Операція MCP завершилася помилкою',
|
failed: 'Операція MCP завершилася помилкою',
|
||||||
|
copyFailed: 'Не вдалося скопіювати. Скопіюйте вручну.',
|
||||||
okBtn: 'Підтвердити',
|
okBtn: 'Підтвердити',
|
||||||
cancelBtn: 'Скасувати'
|
cancelBtn: 'Скасувати'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -289,6 +289,7 @@ const vi = {
|
|||||||
enableConfirmTitle: 'Bật điều khiển MCP bên ngoài?',
|
enableConfirmTitle: 'Bật điều khiển MCP bên ngoài?',
|
||||||
enableConfirmDesc: 'Bật MCP sẽ dừng PicoClaw và đóng tất cả phiên PicoClaw đang hoạt động.',
|
enableConfirmDesc: 'Bật MCP sẽ dừng PicoClaw và đóng tất cả phiên PicoClaw đang hoạt động.',
|
||||||
failed: 'Thao tác MCP không thành công',
|
failed: 'Thao tác MCP không thành công',
|
||||||
|
copyFailed: 'Sao chép thất bại. Vui lòng sao chép thủ công.',
|
||||||
okBtn: 'Xác nhận',
|
okBtn: 'Xác nhận',
|
||||||
cancelBtn: 'Hủy'
|
cancelBtn: 'Hủy'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -379,6 +379,12 @@ export function usePicoclawSidebarLifecycle({
|
|||||||
setTransportState,
|
setTransportState,
|
||||||
setRunState
|
setRunState
|
||||||
}: LifecycleOptions) {
|
}: LifecycleOptions) {
|
||||||
|
const tRef = useRef(t);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
tRef.current = t;
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
@@ -396,7 +402,7 @@ export function usePicoclawSidebarLifecycle({
|
|||||||
if (nextRuntimeStatus?.installing) {
|
if (nextRuntimeStatus?.installing) {
|
||||||
setMessages((current) => [
|
setMessages((current) => [
|
||||||
...current,
|
...current,
|
||||||
createStatusMessage(t('picoclaw.install.installing'))
|
createStatusMessage(tRef.current('picoclaw.install.installing'))
|
||||||
]);
|
]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -404,7 +410,7 @@ export function usePicoclawSidebarLifecycle({
|
|||||||
if (nextRuntimeStatus?.installed === false) {
|
if (nextRuntimeStatus?.installed === false) {
|
||||||
setMessages((current) => [
|
setMessages((current) => [
|
||||||
...current,
|
...current,
|
||||||
createStatusMessage(t('picoclaw.install.requiredDescription'))
|
createStatusMessage(tRef.current('picoclaw.install.requiredDescription'))
|
||||||
]);
|
]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -412,7 +418,7 @@ export function usePicoclawSidebarLifecycle({
|
|||||||
if (nextRuntimeStatus?.model_configured === false) {
|
if (nextRuntimeStatus?.model_configured === false) {
|
||||||
setMessages((current) => [
|
setMessages((current) => [
|
||||||
...current,
|
...current,
|
||||||
createStatusMessage(t('picoclaw.model.requiredDescription'))
|
createStatusMessage(tRef.current('picoclaw.model.requiredDescription'))
|
||||||
]);
|
]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -423,7 +429,7 @@ export function usePicoclawSidebarLifecycle({
|
|||||||
|
|
||||||
setMessages((current) => [
|
setMessages((current) => [
|
||||||
...current,
|
...current,
|
||||||
createStatusMessage(t('picoclaw.status.connecting'))
|
createStatusMessage(tRef.current('picoclaw.status.connecting'))
|
||||||
]);
|
]);
|
||||||
try {
|
try {
|
||||||
await connectGateway(nextSessionId);
|
await connectGateway(nextSessionId);
|
||||||
@@ -460,8 +466,7 @@ export function usePicoclawSidebarLifecycle({
|
|||||||
setOverlay,
|
setOverlay,
|
||||||
setRunState,
|
setRunState,
|
||||||
setTakeover,
|
setTakeover,
|
||||||
setTransportState,
|
setTransportState
|
||||||
t
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user