源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
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
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
This commit is contained in:
commit
2ed3415fb7
8018 files changed
+1936476
No files matched your search
@@ -0,0 +1,306 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
token string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
const (
|
||||
// Built-in commands use this broad fallback budget. App commands publish
|
||||
// their handler timeout through capability metadata and override it per
|
||||
// invocation, including the transport margin owned by the terminal CLI.
|
||||
defaultClientTimeout = 360 * time.Second
|
||||
healthPath = "/v1/health"
|
||||
cliCapabilitiesPath = "/v1/cli/capabilities"
|
||||
cliCommandInvokePattern = "/v1/cli/commands/{commandID}/invoke"
|
||||
)
|
||||
|
||||
type HealthStatus struct {
|
||||
Service string `json:"service"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type CapabilityList struct {
|
||||
Commands []Capability `json:"commands"`
|
||||
}
|
||||
|
||||
type Capability struct {
|
||||
ID string `json:"id"`
|
||||
Path []string `json:"path"`
|
||||
Summary string `json:"summary"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Visibility string `json:"visibility,omitempty"`
|
||||
InputSchema map[string]any `json:"inputSchema,omitempty"`
|
||||
Output CapabilityOutput `json:"output"`
|
||||
Execution *CommandExecution `json:"execution,omitempty"`
|
||||
HandlerTimeoutMs int `json:"handlerTimeoutMs,omitempty"`
|
||||
Source CapabilitySource `json:"source"`
|
||||
}
|
||||
|
||||
type CommandExecution struct {
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
|
||||
type CapabilityListOptions struct {
|
||||
IncludeHidden bool
|
||||
IncludeIntegration bool
|
||||
AgentSessionID string
|
||||
}
|
||||
|
||||
type CapabilitySource struct {
|
||||
Kind string `json:"kind"`
|
||||
AppID string `json:"appId,omitempty"`
|
||||
AppName string `json:"appName,omitempty"`
|
||||
CLIDescription string `json:"cliDescription,omitempty"`
|
||||
AppDescription string `json:"appDescription,omitempty"`
|
||||
DocumentationFile string `json:"documentationFile,omitempty"`
|
||||
DocumentationPath string `json:"documentationPath,omitempty"`
|
||||
}
|
||||
|
||||
type CapabilityOutput struct {
|
||||
DefaultMode string `json:"defaultMode"`
|
||||
JSON bool `json:"json"`
|
||||
Table *struct {
|
||||
Columns []TableColumn `json:"columns"`
|
||||
} `json:"table"`
|
||||
}
|
||||
|
||||
type TableColumn struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
type InvokeRequest struct {
|
||||
Input map[string]any `json:"input,omitempty"`
|
||||
OutputMode string `json:"outputMode,omitempty"`
|
||||
Context InvokeContext `json:"context"`
|
||||
}
|
||||
|
||||
type InvokeContext struct {
|
||||
AppID string `json:"appId,omitempty"`
|
||||
Source string `json:"source"`
|
||||
WorkspaceID string `json:"workspaceID,omitempty"`
|
||||
ParentCommandID string `json:"parentCommandId,omitempty"`
|
||||
AgentSessionID string `json:"agentSessionId,omitempty"`
|
||||
AgentCWD string `json:"agentCwd,omitempty"`
|
||||
AgentRailPlacementJSON string `json:"agentRailPlacementJSON,omitempty"`
|
||||
}
|
||||
|
||||
type InvokeResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
Output *CommandOutput `json:"output,omitempty"`
|
||||
}
|
||||
|
||||
type CommandOutput struct {
|
||||
Kind string `json:"kind"`
|
||||
Columns []TableColumn `json:"columns,omitempty"`
|
||||
Rows []map[string]any `json:"rows,omitempty"`
|
||||
Value map[string]any `json:"value,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Warnings []CommandWarning `json:"warnings,omitempty"`
|
||||
Continuation *CommandContinuation `json:"continuation,omitempty"`
|
||||
}
|
||||
|
||||
type CommandContinuation struct {
|
||||
State string `json:"state"`
|
||||
RetryAfterMs int `json:"retryAfterMs"`
|
||||
}
|
||||
|
||||
type CommandWarning struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type apiErrorEnvelope struct {
|
||||
Error struct {
|
||||
Code string `json:"code"`
|
||||
Reason string `json:"reason"`
|
||||
Retryable bool `json:"retryable"`
|
||||
DeveloperMessage string `json:"developerMessage"`
|
||||
CorrelationID string `json:"correlationId"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
func NewClient(endpoint Endpoint) (*Client, error) {
|
||||
baseURL, err := endpoint.BaseURL()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Client{
|
||||
baseURL: baseURL,
|
||||
token: endpoint.Token,
|
||||
httpClient: &http.Client{
|
||||
Timeout: defaultClientTimeout,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (client *Client) GetHealth(ctx context.Context) (HealthStatus, error) {
|
||||
var result HealthStatus
|
||||
if err := client.DoJSON(ctx, http.MethodGet, healthPath, nil, &result); err != nil {
|
||||
return HealthStatus{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (client *Client) ListCapabilitiesForWorkspaceWithOptions(ctx context.Context, workspaceID string, options CapabilityListOptions) (CapabilityList, error) {
|
||||
var result CapabilityList
|
||||
path := cliCapabilitiesPath
|
||||
query := url.Values{}
|
||||
if strings.TrimSpace(workspaceID) != "" {
|
||||
query.Set("workspaceID", strings.TrimSpace(workspaceID))
|
||||
}
|
||||
if strings.TrimSpace(options.AgentSessionID) != "" {
|
||||
query.Set("agentSessionID", strings.TrimSpace(options.AgentSessionID))
|
||||
}
|
||||
if options.IncludeHidden {
|
||||
query.Set("includeHidden", "true")
|
||||
}
|
||||
if options.IncludeIntegration {
|
||||
query.Set("includeIntegration", "true")
|
||||
}
|
||||
if len(query) > 0 {
|
||||
path += "?" + query.Encode()
|
||||
}
|
||||
if err := client.DoJSON(ctx, http.MethodGet, path, nil, &result); err != nil {
|
||||
return CapabilityList{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (client *Client) Invoke(ctx context.Context, commandID string, request InvokeRequest) (InvokeResponse, error) {
|
||||
return client.InvokeWithTimeout(ctx, commandID, request, defaultClientTimeout)
|
||||
}
|
||||
|
||||
func (client *Client) InvokeWithTimeout(ctx context.Context, commandID string, request InvokeRequest, timeout time.Duration) (InvokeResponse, error) {
|
||||
var result InvokeResponse
|
||||
path := strings.Replace(cliCommandInvokePattern, "{commandID}", urlPathEscape(commandID), 1)
|
||||
httpClient := client.httpClient
|
||||
if timeout > 0 {
|
||||
cloned := *client.httpClient
|
||||
cloned.Timeout = timeout
|
||||
httpClient = &cloned
|
||||
}
|
||||
if err := client.doJSON(ctx, httpClient, http.MethodPost, path, request, &result); err != nil {
|
||||
return InvokeResponse{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (client *Client) DoJSON(ctx context.Context, method string, path string, body any, result any) error {
|
||||
return client.doJSON(ctx, client.httpClient, method, path, body, result)
|
||||
}
|
||||
|
||||
func (client *Client) doJSON(ctx context.Context, httpClient *http.Client, method string, path string, body any, result any) error {
|
||||
var requestBody io.Reader
|
||||
if body != nil {
|
||||
encoded, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return newRequestError(ErrorDetails{ReasonCode: "daemon_request_invalid", Message: fmt.Sprintf("encode request body: %v", err)})
|
||||
}
|
||||
requestBody = bytes.NewReader(encoded)
|
||||
}
|
||||
|
||||
url := client.baseURL + path
|
||||
request, err := http.NewRequestWithContext(ctx, method, url, requestBody)
|
||||
if err != nil {
|
||||
return newRequestError(ErrorDetails{ReasonCode: "daemon_request_invalid", Message: fmt.Sprintf("create daemon request: %v", err)})
|
||||
}
|
||||
request.Header.Set("Accept", "application/json")
|
||||
request.Header.Set("Authorization", "Bearer "+client.token)
|
||||
if body != nil {
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return daemonRequestError(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
content, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return newRequestError(ErrorDetails{ReasonCode: "daemon_response_read_failed", Message: fmt.Sprintf("read daemon response: %v", err)})
|
||||
}
|
||||
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||||
return daemonResponseError(response.StatusCode, response.Status, content)
|
||||
}
|
||||
if result == nil {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(content, result); err != nil {
|
||||
return newRequestError(ErrorDetails{ReasonCode: "daemon_response_invalid", Message: fmt.Sprintf("decode daemon response: %v", err)})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func daemonResponseError(statusCode int, status string, content []byte) error {
|
||||
var envelope apiErrorEnvelope
|
||||
if err := json.Unmarshal(content, &envelope); err == nil {
|
||||
reasonCode := strings.TrimSpace(envelope.Error.Reason)
|
||||
if reasonCode == "" {
|
||||
reasonCode = strings.TrimSpace(envelope.Error.Code)
|
||||
}
|
||||
if reasonCode != "" {
|
||||
message := strings.TrimSpace(envelope.Error.DeveloperMessage)
|
||||
if message == "" {
|
||||
message = reasonCode
|
||||
}
|
||||
return newRequestError(ErrorDetails{
|
||||
ReasonCode: reasonCode, Message: message, Retryable: envelope.Error.Retryable,
|
||||
CorrelationID: strings.TrimSpace(envelope.Error.CorrelationID), StatusCode: statusCode,
|
||||
})
|
||||
}
|
||||
}
|
||||
if statusCode == http.StatusUnauthorized {
|
||||
return newRequestError(ErrorDetails{ReasonCode: "unauthorized", Message: "daemon authentication failed", StatusCode: statusCode})
|
||||
}
|
||||
message := strings.TrimSpace(string(content))
|
||||
if message == "" {
|
||||
message = strings.TrimSpace(status)
|
||||
}
|
||||
return newRequestError(ErrorDetails{
|
||||
ReasonCode: "daemon_request_failed", Message: "daemon request failed: " + message, StatusCode: statusCode,
|
||||
})
|
||||
}
|
||||
|
||||
func daemonRequestError(err error) error {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return newRequestError(ErrorDetails{ReasonCode: "daemon_request_canceled", Message: "daemon request canceled"})
|
||||
}
|
||||
if errors.Is(err, context.DeadlineExceeded) || os.IsTimeout(err) {
|
||||
return newRequestError(ErrorDetails{ReasonCode: "daemon_request_timed_out", Message: "daemon request timed out", Retryable: true})
|
||||
}
|
||||
if runningInAgentEnvironment() {
|
||||
return newRequestError(ErrorDetails{
|
||||
ReasonCode: "daemon_unavailable", Retryable: true,
|
||||
Message: "daemon is not reachable from this agent execution environment; rerun the command in an execution environment with localhost/IPC access",
|
||||
})
|
||||
}
|
||||
return newRequestError(ErrorDetails{ReasonCode: "daemon_unavailable", Message: "daemon is not reachable", Retryable: true})
|
||||
}
|
||||
|
||||
func runningInAgentEnvironment() bool {
|
||||
return strings.TrimSpace(os.Getenv("TUTTI_AGENT_SESSION_ID")) != "" ||
|
||||
strings.TrimSpace(os.Getenv("TUTTI_AGENT_ROUTING")) != ""
|
||||
}
|
||||
|
||||
func urlPathEscape(value string) string {
|
||||
replacer := strings.NewReplacer("%", "%25", "/", "%2F", "?", "%3F", "#", "%23")
|
||||
return replacer.Replace(value)
|
||||
}
|
||||
Reference in new issue
Block a user