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

175 lines
6.2 KiB
Go

package runtimeprep
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
// OpenCodePreparer creates a session-scoped native config directory containing
// Tutti runtime instructions and Skills. A bound OpenAI-compatible model plan
// also gains an opencode.json provider block in that directory; its API key
// stays out of the file because the config references an {env:...} token.
type OpenCodePreparer struct{}
func (OpenCodePreparer) Provider() string {
return "opencode"
}
func (OpenCodePreparer) Prepare(_ context.Context, input ProviderPrepareInput) (ProviderPrepareResult, error) {
configDir := filepath.Join(input.RuntimeRoot, "opencode")
if err := os.MkdirAll(configDir, 0o700); err != nil {
return ProviderPrepareResult{}, fmt.Errorf("create opencode config directory: %w", err)
}
policy, err := tuttiCLIPolicy(input.PrepareInput)
if err != nil {
return ProviderPrepareResult{}, err
}
instructionsPath := filepath.Join(configDir, "AGENTS.md")
writeResult, err := input.Store.WriteManagedBlock(instructionsPath, policy)
if err != nil {
return ProviderPrepareResult{}, err
}
if err := ensureRTKInstructionsReferenceFirst(instructionsPath, input.PrepareInput); err != nil {
return ProviderPrepareResult{}, err
}
if _, err := installProviderNativeSkillsSessionScoped(filepath.Join(configDir, "skills"), input.PrepareInput); err != nil {
return ProviderPrepareResult{}, fmt.Errorf("install opencode tutti skills: %w", err)
}
if input.RTKSaverMode {
if err := installOpenCodeRTKPlugin(configDir); err != nil {
return ProviderPrepareResult{}, err
}
}
if input.Manifest != nil {
input.Manifest.RecordManagedFile(instructionsPath, "provider-instructions", writeResult.Created)
input.Manifest.RecordManagedFile(configDir, "opencode-home", true)
}
env := []string{"OPENCODE_CONFIG_DIR=" + configDir}
if !input.ModelEndpoint.supportsOpenCode() {
return ProviderPrepareResult{Cwd: input.Cwd, Env: env}, nil
}
content, err := openCodeModelPlanConfig(input.ModelEndpoint)
if err != nil {
return ProviderPrepareResult{}, err
}
configPath := filepath.Join(configDir, "opencode.json")
if err := os.WriteFile(configPath, content, 0o600); err != nil {
return ProviderPrepareResult{}, fmt.Errorf("write opencode model plan config: %w", err)
}
if input.Manifest != nil {
input.Manifest.RecordManagedFile(configPath, "opencode-model-plan-config", true)
}
return ProviderPrepareResult{
Cwd: input.Cwd,
Env: append(env,
// The session-scoped config merges as OpenCode's environment
// config layer; the adapter's OPENCODE_CONFIG_CONTENT (session
// settings) still overrides scalar keys such as `model`.
"OPENCODE_CONFIG="+configPath,
ModelPlanAPIKeyEnv+"="+input.ModelEndpoint.APIKey,
),
}, nil
}
const openCodeRTKPlugin = `import type { Plugin } from "@opencode-ai/plugin"
// Session-scoped RTK adapter. Rewrite behavior stays in the bundled RTK
// executable so this plugin does not duplicate the command registry.
export const RtkOpenCodePlugin: Plugin = async ({ $ }) => ({
"tool.execute.before": async (input, output) => {
const tool = String(input?.tool ?? "").toLowerCase()
if (tool !== "bash" && tool !== "shell") return
const args = output?.args
if (!args || typeof args !== "object") return
const command = (args as Record<string, unknown>).command
if (typeof command !== "string" || !command) return
try {
const result = await $` + "`rtk rewrite ${command}`" + `.quiet().nothrow()
const rewritten = String(result.stdout).trim()
if (rewritten && rewritten !== command) {
;(args as Record<string, unknown>).command = rewritten
}
} catch {
// RTK is an optimization. Preserve the original command on failure.
}
},
})
`
func installOpenCodeRTKPlugin(configDir string) error {
pluginsDir := filepath.Join(configDir, "plugins")
if err := os.MkdirAll(pluginsDir, 0o700); err != nil {
return fmt.Errorf("create opencode RTK plugin directory: %w", err)
}
if err := os.WriteFile(filepath.Join(pluginsDir, "rtk.ts"), []byte(openCodeRTKPlugin), 0o600); err != nil {
return fmt.Errorf("write opencode RTK plugin: %w", err)
}
return nil
}
type openCodeConfigDocument struct {
Schema string `json:"$schema,omitempty"`
Model string `json:"model,omitempty"`
Provider map[string]openCodeProviderConfig `json:"provider"`
}
type openCodeProviderConfig struct {
NPM string `json:"npm"`
Name string `json:"name,omitempty"`
Options openCodeProviderOptions `json:"options"`
Models map[string]openCodeModelConfig `json:"models"`
}
type openCodeProviderOptions struct {
BaseURL string `json:"baseURL"`
APIKey string `json:"apiKey"`
}
type openCodeModelConfig struct {
Name string `json:"name,omitempty"`
}
// openCodeModelPlanConfig renders the session-scoped opencode.json for a bound
// plan. The credential never enters the file: options.apiKey carries an
// {env:...} token resolved by OpenCode from the session process environment.
func openCodeModelPlanConfig(endpoint *ModelEndpointConfig) ([]byte, error) {
models := make(map[string]openCodeModelConfig, len(endpoint.Models))
for _, model := range endpoint.Models {
id := strings.TrimSpace(model.ID)
if id == "" {
continue
}
models[id] = openCodeModelConfig{Name: strings.TrimSpace(model.Name)}
}
if defaultModel := OpenCodePlanModelID(endpoint.Model); defaultModel != "" {
if _, exists := models[defaultModel]; !exists {
models[defaultModel] = openCodeModelConfig{}
}
}
document := openCodeConfigDocument{
Schema: "https://opencode.ai/config.json",
Model: OpenCodePlanModelValue(endpoint.Model),
Provider: map[string]openCodeProviderConfig{
ModelPlanProviderID: {
NPM: "@ai-sdk/openai-compatible",
Name: planProviderDisplayName(endpoint),
Options: openCodeProviderOptions{
BaseURL: strings.TrimSpace(endpoint.BaseURL),
APIKey: "{env:" + ModelPlanAPIKeyEnv + "}",
},
Models: models,
},
},
}
content, err := json.MarshalIndent(document, "", " ")
if err != nil {
return nil, fmt.Errorf("encode opencode model plan config: %w", err)
}
return append(content, '\n'), nil
}