yao/trace/jsapi/trace.go
Max 8dedd93a90 Remove deprecated agent_test.go and mcp package files; refactor context and trace handling for improved functionality.
- Deleted agent_test.go to streamline testing structure.
- Removed mcp package and its related fetch and search files to simplify the codebase.
- Enhanced context handling in jsapi_test.go and jsapi.go for better integration with JavaScript.
- Introduced no-op objects for Trace and Node to handle uninitialized states gracefully.
- Updated i18n translations to include new MCP-related labels and descriptions.
2025-11-28 19:26:33 +08:00

569 lines
17 KiB
Go

package jsapi
import (
"context"
"fmt"
v8 "github.com/yaoapp/gou/runtime/v8"
"github.com/yaoapp/gou/runtime/v8/bridge"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/trace"
"github.com/yaoapp/yao/trace/types"
"rogchap.com/v8go"
)
func init() {
// Auto-register Trace JavaScript API when package is imported
v8.RegisterFunction("Trace", ExportFunction)
}
// NewTraceObject creates a JavaScript Trace object
func NewTraceObject(v8ctx *v8go.Context, traceID string, manager types.Manager) (*v8go.Value, error) {
jsObject := v8go.NewObjectTemplate(v8ctx.Isolate())
// Set internal field count to 1 to store the __go_id
// Internal fields are not accessible from JavaScript, providing better security
jsObject.SetInternalFieldCount(1)
// Register manager in global bridge registry for efficient Go object retrieval
// The goValueID will be stored in internal field (index 0) after instance creation
// Internal fields are not accessible from JavaScript, providing better security
goValueID := bridge.RegisterGoObject(manager)
// Set primitive fields
jsObject.Set("id", traceID)
// Set release functions (both __release and Release do the same thing)
// __release: Internal cleanup (called by GC or Use())
// Release: Public method for manual cleanup (try-finally pattern)
releaseFunc := traceGoRelease(v8ctx.Isolate(), traceID)
jsObject.Set("__release", releaseFunc)
jsObject.Set("Release", releaseFunc)
// Set methods
jsObject.Set("Add", traceAddMethod(v8ctx.Isolate(), manager))
jsObject.Set("Parallel", traceParallelMethod(v8ctx.Isolate(), manager))
jsObject.Set("Info", traceInfoMethod(v8ctx.Isolate(), manager))
jsObject.Set("Debug", traceDebugMethod(v8ctx.Isolate(), manager))
jsObject.Set("Error", traceErrorMethod(v8ctx.Isolate(), manager))
jsObject.Set("Warn", traceWarnMethod(v8ctx.Isolate(), manager))
jsObject.Set("SetOutput", traceSetOutputMethod(v8ctx.Isolate(), manager))
jsObject.Set("SetMetadata", traceSetMetadataMethod(v8ctx.Isolate(), manager))
jsObject.Set("Complete", traceCompleteMethod(v8ctx.Isolate(), manager))
jsObject.Set("Fail", traceFailMethod(v8ctx.Isolate(), manager))
jsObject.Set("MarkComplete", traceMarkCompleteMethod(v8ctx.Isolate(), manager))
jsObject.Set("CreateSpace", traceCreateSpaceMethod(v8ctx.Isolate(), manager))
jsObject.Set("GetSpace", traceGetSpaceMethod(v8ctx.Isolate(), manager))
jsObject.Set("IsComplete", traceIsCompleteMethod(v8ctx.Isolate(), manager))
// Create instance
instance, err := jsObject.NewInstance(v8ctx)
if err != nil {
// Clean up: release from global registry if instance creation failed
bridge.ReleaseGoObject(goValueID)
return nil, err
}
// Store the goValueID in internal field (index 0)
// This is not accessible from JavaScript, providing better security
obj, err := instance.Value.AsObject()
if err != nil {
bridge.ReleaseGoObject(goValueID)
return nil, err
}
err = obj.SetInternalField(0, goValueID)
if err != nil {
bridge.ReleaseGoObject(goValueID)
return nil, err
}
return instance.Value, nil
}
// TraceNew creates a new Trace instance from JavaScript
// Usage: new Trace(options)
func TraceNew(v8ctx *v8go.Context, options map[string]interface{}) (*v8go.Value, error) {
cfg := config.Conf
// Prepare driver options
var driverOptions []any
var driverType string
// Allow override from options
driver, _ := options["driver"].(string)
if driver == "" {
driver = cfg.Trace.Driver
}
switch driver {
case "store":
driverType = trace.Store
storeID := cfg.Trace.Store
if sid, ok := options["store"].(string); ok && sid != "" {
storeID = sid
}
prefix := cfg.Trace.Prefix
if pfx, ok := options["prefix"].(string); ok {
prefix = pfx
}
driverOptions = []any{storeID, prefix}
case "local", "":
driverType = trace.Local
path := cfg.Trace.Path
if p, ok := options["path"].(string); ok && p != "" {
path = p
}
driverOptions = []any{path}
default:
return nil, fmt.Errorf("unsupported trace driver: %s", driver)
}
// Parse trace options
var traceOpt types.TraceOption
if id, ok := options["id"].(string); ok {
traceOpt.ID = id
}
// Create trace
goCtx := context.Background()
traceID, manager, err := trace.New(goCtx, driverType, &traceOpt, driverOptions...)
if err != nil {
return nil, err
}
return NewTraceObject(v8ctx, traceID, manager)
}
// TraceLoad loads an existing trace from JavaScript
// Usage: Trace.Load(traceID)
func TraceLoad(v8ctx *v8go.Context, traceID string) (*v8go.Value, error) {
manager, err := trace.Load(traceID)
if err != nil {
return nil, err
}
return NewTraceObject(v8ctx, traceID, manager)
}
// traceGoRelease releases the Go object from the global bridge registry
// It retrieves the goValueID from internal field (index 0) and releases the Go object
// It also calls trace.Release to cleanup the trace globally
func traceGoRelease(iso *v8go.Isolate, traceID string) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
// Get the trace object (this)
thisObj, err := info.This().AsObject()
if err == nil && thisObj.InternalFieldCount() > 0 {
// Get goValueID from internal field (index 0)
goValueIDValue := thisObj.GetInternalField(0)
if goValueIDValue != nil && goValueIDValue.IsString() {
goValueID := goValueIDValue.String()
// Release from global bridge registry
bridge.ReleaseGoObject(goValueID)
}
}
// Call global trace.Release to remove from registry and stop background goroutines
trace.Release(traceID)
return v8go.Undefined(info.Context().Isolate())
})
}
// Method templates
func traceAddMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 {
return bridge.JsException(ctx, "Add requires at least 1 argument: (input, option?)")
}
// Parse input
input, err := bridge.GoValue(args[0], ctx)
if err != nil {
return bridge.JsException(ctx, fmt.Sprintf("invalid input: %s", err))
}
// Parse option
var option types.TraceNodeOption
if len(args) > 1 && !args[1].IsNullOrUndefined() {
optionObj, err := args[1].AsObject()
if err == nil {
if labelVal, err := optionObj.Get("label"); err == nil && !labelVal.IsNullOrUndefined() {
option.Label = labelVal.String()
}
if iconVal, err := optionObj.Get("icon"); err == nil && !iconVal.IsNullOrUndefined() {
option.Icon = iconVal.String()
}
if descVal, err := optionObj.Get("description"); err == nil && !descVal.IsNullOrUndefined() {
option.Description = descVal.String()
}
}
}
// Call manager.Add
node, err := manager.Add(input, option)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
// Create node JS object
nodeObj, err := NewNodeObject(ctx, node)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
return nodeObj
})
}
func traceParallelMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 {
return bridge.JsException(ctx, "Parallel requires an array argument")
}
// Parse array
parallelInputsJS, err := bridge.GoValue(args[0], ctx)
if err != nil {
return bridge.JsException(ctx, fmt.Sprintf("invalid parallel inputs: %s", err))
}
// Convert to []types.TraceParallelInput
parallelInputsArray, ok := parallelInputsJS.([]interface{})
if !ok {
return bridge.JsException(ctx, "Parallel argument must be an array")
}
parallelInputs := make([]types.TraceParallelInput, 0, len(parallelInputsArray))
for _, item := range parallelInputsArray {
itemMap, ok := item.(map[string]interface{})
if !ok {
continue
}
parallelInput := types.TraceParallelInput{}
if input, ok := itemMap["input"]; ok {
parallelInput.Input = input
}
if option, ok := itemMap["option"].(map[string]interface{}); ok {
if label, ok := option["label"].(string); ok {
parallelInput.Option.Label = label
}
if icon, ok := option["icon"].(string); ok {
parallelInput.Option.Icon = icon
}
if desc, ok := option["description"].(string); ok {
parallelInput.Option.Description = desc
}
}
parallelInputs = append(parallelInputs, parallelInput)
}
// Call manager.Parallel
nodes, err := manager.Parallel(parallelInputs)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
// Create array of node objects
result := make([]interface{}, len(nodes))
for i, node := range nodes {
nodeObj, err := NewNodeObject(ctx, node)
if err != nil {
continue
}
result[i] = nodeObj
}
jsVal, err := bridge.JsValue(ctx, result)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
return jsVal
})
}
func traceInfoMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
args := info.Args()
if len(args) > 0 {
manager.Info(args[0].String())
}
return info.This().Value
})
}
func traceDebugMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
args := info.Args()
if len(args) > 0 {
manager.Debug(args[0].String())
}
return info.This().Value
})
}
func traceErrorMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
args := info.Args()
if len(args) > 0 {
manager.Error(args[0].String())
}
return info.This().Value
})
}
func traceWarnMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
args := info.Args()
if len(args) > 0 {
manager.Warn(args[0].String())
}
return info.This().Value
})
}
func traceSetOutputMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 {
return bridge.JsException(ctx, "SetOutput requires an output argument")
}
output, err := bridge.GoValue(args[0], ctx)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
if err := manager.SetOutput(output); err != nil {
return bridge.JsException(ctx, err.Error())
}
return info.This().Value
})
}
func traceSetMetadataMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 2 {
return bridge.JsException(ctx, "SetMetadata requires 2 arguments: (key, value)")
}
key := args[0].String()
value, err := bridge.GoValue(args[1], ctx)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
if err := manager.SetMetadata(key, value); err != nil {
return bridge.JsException(ctx, err.Error())
}
return info.This().Value
})
}
func traceCompleteMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
// Optional output parameter
if len(args) > 0 && !args[0].IsNullOrUndefined() {
output, err := bridge.GoValue(args[0], ctx)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
if err := manager.Complete(output); err != nil {
return bridge.JsException(ctx, err.Error())
}
} else {
if err := manager.Complete(); err != nil {
return bridge.JsException(ctx, err.Error())
}
}
return info.This().Value
})
}
func traceFailMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 {
return bridge.JsException(ctx, "Fail requires an error message")
}
errMsg := args[0].String()
if err := manager.Fail(fmt.Errorf("%s", errMsg)); err != nil {
return bridge.JsException(ctx, err.Error())
}
return info.This().Value
})
}
func traceMarkCompleteMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
if err := manager.MarkComplete(); err != nil {
return bridge.JsException(ctx, err.Error())
}
return info.This().Value
})
}
func traceCreateSpaceMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
// Parse option
var option types.TraceSpaceOption
if len(args) > 0 && !args[0].IsNullOrUndefined() {
optionObj, err := args[0].AsObject()
if err == nil {
if labelVal, err := optionObj.Get("label"); err == nil && !labelVal.IsNullOrUndefined() {
option.Label = labelVal.String()
}
if typeVal, err := optionObj.Get("type"); err == nil && !typeVal.IsNullOrUndefined() {
option.Type = typeVal.String()
}
if iconVal, err := optionObj.Get("icon"); err == nil && !iconVal.IsNullOrUndefined() {
option.Icon = iconVal.String()
}
if descVal, err := optionObj.Get("description"); err == nil && !descVal.IsNullOrUndefined() {
option.Description = descVal.String()
}
}
}
// Call manager.CreateSpace
space, err := manager.CreateSpace(option)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
// Create space JS object
spaceObj, err := NewSpaceObject(ctx, manager, space)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
return spaceObj
})
}
func traceGetSpaceMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 {
return bridge.JsException(ctx, "GetSpace requires a space ID")
}
spaceID := args[0].String()
space, err := manager.GetSpace(spaceID)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
if space == nil {
return v8go.Null(iso)
}
// Create space JS object
spaceObj, err := NewSpaceObject(ctx, manager, space)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
return spaceObj
})
}
func traceIsCompleteMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
isComplete := manager.IsComplete()
jsVal, _ := v8go.NewValue(iso, isComplete)
return jsVal
})
}
// NewNoOpTraceObject creates a no-op Trace object for when trace is not initialized
// All methods return undefined and do nothing
func NewNoOpTraceObject(v8ctx *v8go.Context) (*v8go.Value, error) {
jsObject := v8go.NewObjectTemplate(v8ctx.Isolate())
iso := v8ctx.Isolate()
// Set id to empty string
jsObject.Set("id", "")
// No-op method factory
noOpMethod := func() *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
return v8go.Undefined(iso)
})
}
// No-op node factory for Add and Parallel methods
noOpNodeMethod := func() *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
// Return a no-op node object
nodeObj, _ := NewNoOpNodeObject(v8ctx)
return nodeObj
})
}
// Set all methods to no-op
jsObject.Set("Add", noOpNodeMethod())
jsObject.Set("Parallel", noOpNodeMethod())
jsObject.Set("Info", noOpMethod())
jsObject.Set("Debug", noOpMethod())
jsObject.Set("Error", noOpMethod())
jsObject.Set("Warn", noOpMethod())
jsObject.Set("SetOutput", noOpMethod())
jsObject.Set("SetMetadata", noOpMethod())
jsObject.Set("Complete", noOpMethod())
jsObject.Set("Fail", noOpMethod())
jsObject.Set("MarkComplete", noOpMethod())
jsObject.Set("CreateSpace", noOpMethod())
jsObject.Set("GetSpace", noOpMethod())
jsObject.Set("IsComplete", v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
jsVal, _ := v8go.NewValue(iso, false)
return jsVal
}))
// Set release methods (no-op, but must be present for consistency)
jsObject.Set("__release", noOpMethod())
jsObject.Set("Release", noOpMethod())
// Create instance
instance, err := jsObject.NewInstance(v8ctx)
if err != nil {
return nil, err
}
return instance.Value, nil
}