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

214 lines
6.6 KiB
Go

package agentstatus
import (
"context"
"encoding/json"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/tutti-os/tutti/packages/agent/daemon/runtimecmd"
)
func codexRuntimeSelectionNeedsUserInput(runtime providerRuntimeResolution) bool {
return runtime.CodexSelectionState == CodexRuntimeSelectionSelectionRequired ||
runtime.CodexSelectionState == CodexRuntimeSelectionStale
}
func (s Service) resolveProviderRuntime(ctx context.Context, spec ProviderSpec) providerRuntimeResolution {
resolver := s.commandResolver()
env := resolver.Env(spec.AdapterEnv)
if strings.TrimSpace(os.Getenv("TUTTI_MOCK_AGENT_UNBOUND")) == "1" && isCodexStatusSpec(spec) {
return providerRuntimeResolution{Env: env}
}
if strings.TrimSpace(spec.ExternalRegistryID) != "" {
return s.resolveExternalProviderRuntime(ctx, spec, resolver, env)
}
if isCodexStatusSpec(spec) && s.CodexRuntimeSelectionStore != nil {
selection, err := s.resolveCodexRuntimeSelection(ctx, spec)
result := providerRuntimeResolution{
AdapterEnv: cloneStrings(spec.AdapterEnv),
Env: env,
}
if err != nil {
result.ReasonCode = "codex_runtime_selection_unavailable"
return result
}
result.ReasonCode = selection.ReasonCode
result.CodexSelectionExplicit = selection.Explicit
result.CodexSelectionState = selection.State
candidate, found := selection.candidate()
if !found {
return result
}
result.CLIPath = candidate.Candidate.LauncherPath
if !selection.Launchable {
return result
}
result.AdapterPath = candidate.Candidate.LauncherPath
result.AdapterCommand = cloneStrings(spec.AdapterCommand)
if len(result.AdapterCommand) > 0 {
result.AdapterCommand[0] = candidate.Candidate.LauncherPath
}
return result
}
cliPath := resolveBinaryWithResolver(resolver, spec.BinaryNames, nil)
if isClaudeStatusSpec(spec) && strings.TrimSpace(cliPath) == "" {
cliPath = s.managedClaudeCodeExecutable()
}
adapterPath := resolveBinaryWithResolver(resolver, adapterBinaryNames(spec), spec.AdapterEnv)
if isStandardACPStatusSpec(spec) && len(spec.AdapterCommand) > 0 && s.executableFile(spec.AdapterCommand[0]) {
cliPath = spec.AdapterCommand[0]
adapterPath = spec.AdapterCommand[0]
}
if isCodexStatusSpec(spec) && len(spec.AdapterCommand) > 0 && s.executableFile(spec.AdapterCommand[0]) {
if cliPath == "" {
cliPath = spec.AdapterCommand[0]
}
if adapterPath == "" {
adapterPath = spec.AdapterCommand[0]
}
}
return providerRuntimeResolution{
CLIPath: cliPath,
AdapterPath: adapterPath,
AdapterVersion: resolveAdapterPackageVersion(adapterPath, spec.AdapterPackage),
AdapterCommand: cloneStrings(spec.AdapterCommand),
AdapterEnv: cloneStrings(spec.AdapterEnv),
Env: env,
}
}
func (s Service) resolveExternalProviderRuntime(
_ context.Context,
spec ProviderSpec,
resolver runtimecmd.Resolver,
env []string,
) providerRuntimeResolution {
result := providerRuntimeResolution{
CLIPath: resolveBinaryWithResolver(resolver, spec.BinaryNames, nil),
AdapterCommand: cloneStrings(spec.AdapterCommand),
AdapterEnv: cloneStrings(spec.AdapterEnv),
ReasonCode: spec.AdapterUnavailableReasonCode,
Env: env,
}
if spec.AdapterInstall.RegistryNPM != nil {
npm := spec.AdapterInstall.RegistryNPM
result.AdapterPath = strings.TrimSpace(npm.PackageDir)
result.AdapterVersion = installedNPMPackageVersion(npm.PackageDir, spec.AdapterPackage.Name)
if result.AdapterVersion == "" || len(spec.AdapterCommand) == 0 {
result.AdapterPath = ""
}
return result
}
if len(spec.AdapterCommand) > 0 {
path := strings.TrimSpace(spec.AdapterCommand[0])
if path != "" && s.executableFile(path) {
result.AdapterPath = path
result.AdapterVersion = spec.AdapterPackage.Version
}
}
return result
}
func resolveAdapterPackageVersion(adapterPath string, requirement AdapterPackageRequirement) string {
if strings.TrimSpace(adapterPath) == "" || strings.TrimSpace(requirement.Name) == "" {
return ""
}
packageJSONPath := findAdapterPackageJSON(adapterPath, requirement.Name)
if packageJSONPath == "" {
return ""
}
content, err := os.ReadFile(packageJSONPath)
if err != nil {
return ""
}
var manifest struct {
Name string `json:"name"`
Version string `json:"version"`
}
if err := json.Unmarshal(content, &manifest); err != nil {
return ""
}
if strings.TrimSpace(manifest.Name) != strings.TrimSpace(requirement.Name) {
return ""
}
return strings.TrimSpace(manifest.Version)
}
func findAdapterPackageJSON(adapterPath string, packageName string) string {
resolvedPath := strings.TrimSpace(adapterPath)
if resolved, err := filepath.EvalSymlinks(resolvedPath); err == nil {
resolvedPath = resolved
}
if runtime.GOOS == "windows" {
// Windows npm puts the .cmd/.ps1 shim beside the global
// node_modules tree. The shim itself is not inside the package, so
// ancestor walking cannot find package.json as it does for Unix symlinks.
candidate := filepath.Join(filepath.Dir(resolvedPath), "node_modules", packageName, "package.json")
if packageJSONHasName(candidate, packageName) {
return candidate
}
}
dir := filepath.Dir(resolvedPath)
for range 8 {
candidate := filepath.Join(dir, "package.json")
if packageJSONHasName(candidate, packageName) {
return candidate
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
return ""
}
func packageJSONHasName(path string, packageName string) bool {
content, err := os.ReadFile(path)
if err != nil {
return false
}
var manifest struct {
Name string `json:"name"`
}
if err := json.Unmarshal(content, &manifest); err != nil {
return false
}
return strings.TrimSpace(manifest.Name) == strings.TrimSpace(packageName)
}
func resolveBinaryWithResolver(resolver runtimecmd.Resolver, binaryNames []string, overrides []string) string {
if runtime.GOOS == "windows" {
// npm prefixes can contain both a POSIX shell shim (`opencode`) and
// Windows launchers (`opencode.cmd`/`.ps1`). Prefer the Windows
// launcher before LookPath, whose fallback may select the extensionless
// POSIX file.
expanded := make([]string, 0, len(binaryNames)*5)
for _, binaryName := range binaryNames {
if filepath.Ext(strings.TrimSpace(binaryName)) != "" {
expanded = append(expanded, binaryName)
continue
}
expanded = append(expanded,
binaryName+".cmd",
binaryName+".exe",
binaryName+".bat",
binaryName+".ps1",
binaryName,
)
}
binaryNames = expanded
}
return resolver.ResolveBinary(binaryNames, overrides)
}
func adapterBinaryNames(spec ProviderSpec) []string {
if len(spec.AdapterBinaryNames) > 0 {
return cloneStrings(spec.AdapterBinaryNames)
}
return cloneStrings(spec.BinaryNames)
}