package context import ( "context" "fmt" "sync" "time" jsoniter "github.com/json-iterator/go" "github.com/yaoapp/gou/plan" "github.com/yaoapp/kun/log" "github.com/yaoapp/yao/config" "github.com/yaoapp/yao/openapi/oauth/types" "github.com/yaoapp/yao/trace" traceTypes "github.com/yaoapp/yao/trace/types" ) // Global context registry for interrupt management var ( contextRegistry = &sync.Map{} // map[contextID]*Context ) // New create a new context func New(parent context.Context, authorized *types.AuthorizedInfo, chatID, payload string) Context { if parent == nil { parent = context.Background() } // Validate the client type ctx := Context{ Context: parent, ID: generateContextID(), // Generate unique ID for the context Space: plan.NewMemorySharedSpace(), ChatID: chatID, } if payload == "" { return ctx } err := jsoniter.Unmarshal([]byte(payload), &ctx) if err != nil { log.Error("%s", err.Error()) } return ctx } // NewWithCancel create a new context with cancel func NewWithCancel(parent context.Context, authorized *types.AuthorizedInfo, chatID, payload string) (Context, context.CancelFunc) { ctx := New(parent, authorized, chatID, payload) return WithCancel(ctx) } // NewWithTimeout create a new context with timeout func NewWithTimeout(parent context.Context, authorized *types.AuthorizedInfo, chatID, payload string, timeout time.Duration) (Context, context.CancelFunc) { ctx := New(parent, authorized, chatID, payload) return WithTimeout(ctx, timeout) } // WithCancel create a new context func WithCancel(parent Context) (Context, context.CancelFunc) { new, cancel := context.WithCancel(parent.Context) parent.Context = new return parent, cancel } // WithTimeout create a new context func WithTimeout(parent Context, timeout time.Duration) (Context, context.CancelFunc) { new, cancel := context.WithTimeout(parent.Context, timeout) parent.Context = new return parent, cancel } // Release the context and clean up all resources including stacks and trace func (ctx *Context) Release() { // Unregister from global registry if ctx.ID != "" { Unregister(ctx.ID) } // Stop interrupt controller if ctx.Interrupt != nil { ctx.Interrupt.Stop() ctx.Interrupt = nil } // Complete and release trace if exists if ctx.trace != nil && ctx.Stack != nil && ctx.Stack.TraceID != "" { // Mark trace as complete (sends final event) _ = ctx.trace.MarkComplete() // Release from global registry (removes from registry and closes resources) _ = trace.Release(ctx.Stack.TraceID) ctx.trace = nil } // Clear space if ctx.Space != nil { ctx.Space.Clear() ctx.Space = nil } // Clear stacks if ctx.Stacks != nil { for k := range ctx.Stacks { delete(ctx.Stacks, k) } ctx.Stacks = nil } // Clear current stack reference ctx.Stack = nil // Clear writer reference ctx.Writer = nil ctx = nil } // Send sends data to the context's writer // This is used by the output module to send messages to the client func (ctx *Context) Send(data []byte) error { if ctx.Writer == nil { return nil // No writer, silently ignore } _, err := ctx.Writer.Write(data) return err } // Trace returns the trace manager for this context, lazily initialized on first call // Uses the TraceID from ctx.Stack if available, or generates a new one func (ctx *Context) Trace() (traceTypes.Manager, error) { // Return trace if already initialized if ctx.trace != nil { return ctx.trace, nil } // Get TraceID from Stack or generate new one var traceID string if ctx.Stack != nil && ctx.Stack.TraceID != "" { traceID = ctx.Stack.TraceID // Try to load existing trace first manager, err := trace.Load(traceID) if err == nil { // Found in registry, reuse it ctx.trace = manager return manager, nil } } // Get trace configuration from global config cfg := config.Conf // Prepare driver options var driverOptions []any var driverType string switch cfg.Trace.Driver { case "store": driverType = trace.Store if cfg.Trace.Store == "" { return nil, fmt.Errorf("trace store ID not configured") } driverOptions = []any{cfg.Trace.Store, cfg.Trace.Prefix} case "local", "": driverType = trace.Local driverOptions = []any{cfg.Trace.Path} default: return nil, fmt.Errorf("unsupported trace driver: %s", cfg.Trace.Driver) } // Create trace using trace.New (handles registry) createdTraceID, manager, err := trace.New(ctx.Context, driverType, &traceTypes.TraceOption{ ID: traceID, // Use existing ID from Stack or empty to generate new one }, driverOptions...) if err != nil { return nil, fmt.Errorf("failed to create trace: %w", err) } // Update Stack with the created TraceID if needed if ctx.Stack != nil && ctx.Stack.TraceID == "" { ctx.Stack.TraceID = createdTraceID } // Store for future calls ctx.trace = manager return manager, nil } // Map the context to a map func (ctx *Context) Map() map[string]interface{} { data := map[string]interface{}{} // Authorized information if ctx.Authorized != nil { data["authorized"] = ctx.Authorized } if ctx.ChatID != "" { data["chat_id"] = ctx.ChatID } if ctx.AssistantID != "" { data["assistant_id"] = ctx.AssistantID } if ctx.Connector != "" { data["connector"] = ctx.Connector } if ctx.Search != nil { data["search"] = *ctx.Search } // Arguments for call if len(ctx.Args) > 0 { data["args"] = ctx.Args } if ctx.Retry { data["retry"] = ctx.Retry } if ctx.RetryTimes > 0 { data["retry_times"] = ctx.RetryTimes } // Locale information if ctx.Locale != "" { data["locale"] = ctx.Locale } if ctx.Theme != "" { data["theme"] = ctx.Theme } // Request information if ctx.Client.Type != "" || ctx.Client.UserAgent != "" || ctx.Client.IP != "" { data["client"] = map[string]interface{}{ "type": ctx.Client.Type, "user_agent": ctx.Client.UserAgent, "ip": ctx.Client.IP, } } if ctx.Referer != "" { data["referer"] = ctx.Referer } if ctx.Accept != "" { data["accept"] = ctx.Accept } // CUI Context information if ctx.Route != "" { data["route"] = ctx.Route } if len(ctx.Metadata) > 0 { data["metadata"] = ctx.Metadata } return data } // Global Registry Functions // =================================== // Register registers a context to the global registry func Register(ctx *Context) error { if ctx == nil { return fmt.Errorf("context is nil") } if ctx.ID == "" { return fmt.Errorf("context ID is empty") } contextRegistry.Store(ctx.ID, ctx) return nil } // Unregister removes a context from the global registry func Unregister(contextID string) { contextRegistry.Delete(contextID) } // Get retrieves a context from the global registry by ID func Get(contextID string) (*Context, error) { value, ok := contextRegistry.Load(contextID) if !ok { return nil, fmt.Errorf("context not found: %s", contextID) } ctx, ok := value.(*Context) if !ok { return nil, fmt.Errorf("invalid context type") } return ctx, nil } // SendInterrupt sends an interrupt signal to a context by ID // This is the main entry point for external interrupt requests func SendInterrupt(contextID string, signal *InterruptSignal) error { ctx, err := Get(contextID) if err != nil { return err } if ctx.Interrupt == nil { return fmt.Errorf("interrupt controller not initialized for context: %s", contextID) } return ctx.Interrupt.SendSignal(signal) } // generateContextID generates a unique context ID func generateContextID() string { return fmt.Sprintf("ctx_%d", time.Now().UnixNano()) }