- Added steps to pull necessary Docker images for sandbox testing in both CI workflows. - Updated the AI test execution to utilize sandbox configurations, ensuring proper environment setup. - Introduced sandbox initialization in the Assistant's Stream method, allowing for execution of coding agents like Claude and Cursor. - Enhanced context management to support sandbox execution, improving flexibility in handling agent operations.
968 lines
31 KiB
Go
968 lines
31 KiB
Go
package context
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/yaoapp/gou/runtime/v8/bridge"
|
|
"github.com/yaoapp/yao/agent/memory"
|
|
"github.com/yaoapp/yao/agent/output"
|
|
"github.com/yaoapp/yao/agent/output/message"
|
|
traceJsapi "github.com/yaoapp/yao/trace/jsapi"
|
|
"rogchap.com/v8go"
|
|
)
|
|
|
|
// JsValue return the JavaScript value of the context
|
|
func (ctx *Context) JsValue(v8ctx *v8go.Context) (*v8go.Value, error) {
|
|
return ctx.NewObject(v8ctx)
|
|
}
|
|
|
|
// NewObject Create a new JavaScript object from the context
|
|
func (ctx *Context) NewObject(v8ctx *v8go.Context) (*v8go.Value, error) {
|
|
|
|
jsObject := v8go.NewObjectTemplate(v8ctx.Isolate())
|
|
|
|
// Set internal field count to 1 to store the goValueID
|
|
// Internal fields are not accessible from JavaScript, providing better security
|
|
jsObject.SetInternalFieldCount(1)
|
|
|
|
// Register context in global bridge registry for efficient Go object retrieval
|
|
// The goValueID will be stored in internal field (index 0) after instance creation
|
|
goValueID := bridge.RegisterGoObject(ctx)
|
|
|
|
// Set release function (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 := ctx.objectRelease(v8ctx.Isolate(), goValueID)
|
|
jsObject.Set("__release", releaseFunc)
|
|
jsObject.Set("Release", releaseFunc)
|
|
|
|
// Set primitive fields in template
|
|
jsObject.Set("chat_id", ctx.ChatID)
|
|
jsObject.Set("assistant_id", ctx.AssistantID)
|
|
jsObject.Set("locale", ctx.Locale)
|
|
jsObject.Set("theme", ctx.Theme)
|
|
jsObject.Set("referer", ctx.Referer)
|
|
jsObject.Set("accept", string(ctx.Accept))
|
|
jsObject.Set("route", ctx.Route)
|
|
|
|
// Set methods
|
|
jsObject.Set("Send", ctx.sendMethod(v8ctx.Isolate()))
|
|
jsObject.Set("SendStream", ctx.sendStreamMethod(v8ctx.Isolate()))
|
|
jsObject.Set("Replace", ctx.replaceMethod(v8ctx.Isolate()))
|
|
jsObject.Set("Append", ctx.appendMethod(v8ctx.Isolate()))
|
|
jsObject.Set("Merge", ctx.mergeMethod(v8ctx.Isolate()))
|
|
jsObject.Set("Set", ctx.setMethod(v8ctx.Isolate()))
|
|
jsObject.Set("End", ctx.endMethod(v8ctx.Isolate()))
|
|
|
|
// Set ID generator methods
|
|
jsObject.Set("MessageID", ctx.messageIDMethod(v8ctx.Isolate()))
|
|
jsObject.Set("BlockID", ctx.blockIDMethod(v8ctx.Isolate()))
|
|
jsObject.Set("ThreadID", ctx.threadIDMethod(v8ctx.Isolate()))
|
|
|
|
// Lifecycle methods
|
|
jsObject.Set("EndBlock", ctx.endBlockMethod(v8ctx.Isolate()))
|
|
|
|
// Set mcp object
|
|
jsObject.Set("mcp", ctx.newMCPObject(v8ctx.Isolate()))
|
|
|
|
// Set search object
|
|
jsObject.Set("search", ctx.newSearchObject(v8ctx.Isolate()))
|
|
|
|
// Set agent object for calling other agents
|
|
jsObject.Set("agent", ctx.newAgentObject(v8ctx.Isolate()))
|
|
|
|
// Set llm object for direct LLM calls
|
|
jsObject.Set("llm", ctx.newLlmObject(v8ctx.Isolate()))
|
|
|
|
// Note: Space object will be set after instance creation (requires v8ctx)
|
|
|
|
// 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
|
|
}
|
|
|
|
// Set trace object (property, not method)
|
|
// If trace is not initialized, use no-op object
|
|
traceObj := ctx.createTraceObject(v8ctx)
|
|
if traceObj != nil {
|
|
obj.Set("trace", traceObj)
|
|
}
|
|
|
|
// Set complex objects (maps, arrays) after instance creation using bridge
|
|
// Client object
|
|
clientData := map[string]interface{}{
|
|
"type": ctx.Client.Type,
|
|
"user_agent": ctx.Client.UserAgent,
|
|
"ip": ctx.Client.IP,
|
|
}
|
|
clientVal, err := bridge.JsValue(v8ctx, clientData)
|
|
if err == nil {
|
|
obj.Set("client", clientVal)
|
|
clientVal.Release() // Release Go-side Persistent handle, V8 internal reference remains
|
|
}
|
|
|
|
// Metadata object - always set to empty map if nil
|
|
metadataData := ctx.Metadata
|
|
if metadataData == nil {
|
|
metadataData = map[string]interface{}{}
|
|
}
|
|
metadataVal, err := bridge.JsValue(v8ctx, metadataData)
|
|
if err == nil {
|
|
obj.Set("metadata", metadataVal)
|
|
metadataVal.Release() // Release Go-side Persistent handle, V8 internal reference remains
|
|
}
|
|
|
|
// Authorized object - pass the complete structure
|
|
if ctx.Authorized != nil {
|
|
authorizedVal, err := bridge.JsValue(v8ctx, ctx.Authorized)
|
|
if err == nil {
|
|
obj.Set("authorized", authorizedVal)
|
|
authorizedVal.Release() // Release Go-side Persistent handle, V8 internal reference remains
|
|
}
|
|
} else {
|
|
// Set to empty object when nil
|
|
emptyObj, err := bridge.JsValue(v8ctx, map[string]interface{}{})
|
|
if err == nil {
|
|
obj.Set("authorized", emptyObj)
|
|
emptyObj.Release()
|
|
}
|
|
}
|
|
|
|
// Memory object - create a JavaScript object with User/Team/Chat/Context namespaces
|
|
if ctx.Memory != nil {
|
|
memoryObj := ctx.createMemoryObject(v8ctx)
|
|
obj.Set("memory", memoryObj)
|
|
memoryObj.Release()
|
|
}
|
|
|
|
// Sandbox object - only set if sandbox executor is available
|
|
if ctx.sandboxExecutor != nil {
|
|
sandboxObj := ctx.createSandboxInstance(v8ctx)
|
|
if sandboxObj != nil {
|
|
obj.Set("sandbox", sandboxObj)
|
|
sandboxObj.Release()
|
|
}
|
|
}
|
|
|
|
return instance.Value, nil
|
|
}
|
|
|
|
// objectRelease releases the Go object from the global bridge registry
|
|
// It retrieves the goValueID from internal field (index 0) and releases the Go object
|
|
// Also releases associated Trace object if present
|
|
func (ctx *Context) objectRelease(iso *v8go.Isolate, goValueID string) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
// Get the context object (this)
|
|
thisObj, err := info.This().AsObject()
|
|
if err == nil {
|
|
// NOTE: We do NOT automatically release Trace object here
|
|
//
|
|
// Rationale:
|
|
// 1. Each Hook execution creates a new V8 script context (scriptCtx)
|
|
// 2. The agent Context (ctx) is passed to the Hook as a parameter
|
|
// 3. When scriptCtx.Close() is called (via defer), V8 cleanup triggers ctx.__release()
|
|
// 4. If we release Trace here, it gets released after EVERY Hook execution
|
|
// 5. This causes "context canceled" errors in subsequent operations
|
|
//
|
|
// Trace lifecycle:
|
|
// - Trace is created when agent.Stream() starts (in Context.Trace())
|
|
// - Trace should persist across ALL Hook executions (Create, Next, Done)
|
|
// - Trace is released when agent Context.Release() is called (after agent.Stream() completes)
|
|
//
|
|
// Memory management:
|
|
// - If JS code explicitly calls trace.Release(), it will work (trace/jsapi/trace.go:traceGoRelease)
|
|
// - If not explicitly called, Context.Release() will clean it up (context/context.go:Release)
|
|
// - This is the correct lifecycle: one Context -> one Trace -> multiple Hook executions
|
|
|
|
// Release Context Go object from bridge registry
|
|
if thisObj.InternalFieldCount() > 0 {
|
|
// Get goValueID from internal field (index 0)
|
|
goValueID := thisObj.GetInternalField(0)
|
|
if goValueID != nil && goValueID.IsString() {
|
|
// Release from global bridge registry
|
|
bridge.ReleaseGoObject(goValueID.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
return v8go.Undefined(info.Context().Isolate())
|
|
})
|
|
}
|
|
|
|
// createTraceObject creates a Trace object instance
|
|
// Returns a no-op Trace object if trace is not initialized
|
|
func (ctx *Context) createTraceObject(v8ctx *v8go.Context) *v8go.Value {
|
|
// Try to get trace manager
|
|
manager, err := ctx.Trace()
|
|
if err != nil || manager == nil {
|
|
// Return no-op trace object if initialization fails
|
|
noOpTrace, _ := traceJsapi.NewNoOpTraceObject(v8ctx)
|
|
return noOpTrace
|
|
}
|
|
|
|
// Get trace ID
|
|
traceID := ""
|
|
if ctx.Stack != nil {
|
|
traceID = ctx.Stack.TraceID
|
|
}
|
|
|
|
// Create JavaScript Trace object
|
|
traceObj, err := traceJsapi.NewTraceObject(v8ctx, traceID, manager)
|
|
if err != nil {
|
|
// Return no-op trace object if creation fails
|
|
noOpTrace, _ := traceJsapi.NewNoOpTraceObject(v8ctx)
|
|
return noOpTrace
|
|
}
|
|
|
|
return traceObj
|
|
}
|
|
|
|
// sendMethod implements ctx.Send(message, blockId?)
|
|
// Usage: const messageId = ctx.Send({ type: "text", props: { content: "Hello" } })
|
|
// Usage: const messageId = ctx.Send("Hello") // shorthand for text message
|
|
// Usage: const messageId = ctx.Send("Hello", "B1") // specify block ID
|
|
// Automatically generates MessageID and BlockID (if not specified), flushes output
|
|
// Returns: message_id (string)
|
|
func (ctx *Context) sendMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
v8ctx := info.Context()
|
|
args := info.Args()
|
|
|
|
if len(args) < 1 {
|
|
return bridge.JsException(v8ctx, "Send requires a message argument")
|
|
}
|
|
|
|
// Parse message argument
|
|
msg, err := parseMessage(v8ctx, args[0])
|
|
if err != nil {
|
|
return bridge.JsException(v8ctx, "invalid message: "+err.Error())
|
|
}
|
|
|
|
// Get optional blockId argument (second argument)
|
|
// Note: message object's block_id has higher priority
|
|
if len(args) >= 2 && args[1].IsString() && msg.BlockID == "" {
|
|
msg.BlockID = args[1].String()
|
|
}
|
|
|
|
// Generate unique MessageID if not provided
|
|
if msg.MessageID == "" {
|
|
if ctx.IDGenerator != nil {
|
|
msg.MessageID = ctx.IDGenerator.GenerateMessageID()
|
|
} else {
|
|
msg.MessageID = output.GenerateID()
|
|
}
|
|
}
|
|
|
|
// Call ctx.Send (will auto-generate BlockID if still empty)
|
|
if err := ctx.Send(msg); err != nil {
|
|
return bridge.JsException(v8ctx, "Send failed: "+err.Error())
|
|
}
|
|
|
|
// Automatically flush after sending
|
|
if err := ctx.Flush(); err != nil {
|
|
return bridge.JsException(v8ctx, "Flush failed: "+err.Error())
|
|
}
|
|
|
|
// Return the message ID
|
|
messageID, err := v8go.NewValue(iso, msg.MessageID)
|
|
if err != nil {
|
|
return bridge.JsException(v8ctx, "Failed to create return value: "+err.Error())
|
|
}
|
|
return messageID
|
|
})
|
|
}
|
|
|
|
// sendStreamMethod implements ctx.SendStream(message)
|
|
// Usage: const msgId = ctx.SendStream({ type: "text", props: { content: "Initial content" } })
|
|
// Starts a streaming message that can be appended to with ctx.Append()
|
|
// Must be finalized with ctx.End(msgId) or ctx.End(msgId, "final content")
|
|
// Unlike Send(), this does NOT automatically send message_end event
|
|
// Returns: message_id (string)
|
|
func (ctx *Context) sendStreamMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
v8ctx := info.Context()
|
|
args := info.Args()
|
|
|
|
if len(args) < 1 {
|
|
return bridge.JsException(v8ctx, "SendStream requires a message argument")
|
|
}
|
|
|
|
// Parse message argument
|
|
msg, err := parseMessage(v8ctx, args[0])
|
|
if err != nil {
|
|
return bridge.JsException(v8ctx, "invalid message: "+err.Error())
|
|
}
|
|
|
|
// Get optional blockId argument (second argument)
|
|
if len(args) >= 2 && args[1].IsString() && msg.BlockID == "" {
|
|
msg.BlockID = args[1].String()
|
|
}
|
|
|
|
// Call ctx.SendStream
|
|
messageID, err := ctx.SendStream(msg)
|
|
if err != nil {
|
|
return bridge.JsException(v8ctx, "SendStream failed: "+err.Error())
|
|
}
|
|
|
|
// Automatically flush after sending
|
|
if err := ctx.Flush(); err != nil {
|
|
return bridge.JsException(v8ctx, "Flush failed: "+err.Error())
|
|
}
|
|
|
|
// Return the message ID
|
|
returnID, err := v8go.NewValue(iso, messageID)
|
|
if err != nil {
|
|
return bridge.JsException(v8ctx, "Failed to create return value: "+err.Error())
|
|
}
|
|
return returnID
|
|
})
|
|
}
|
|
|
|
// endMethod implements ctx.End(messageId, finalContent?)
|
|
// Usage: ctx.End(msgId) or ctx.End(msgId, "final content to append")
|
|
// Finalizes a streaming message started with SendStream()
|
|
// Sends message_end event with the complete accumulated content
|
|
// Returns: message_id (string)
|
|
func (ctx *Context) endMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
v8ctx := info.Context()
|
|
args := info.Args()
|
|
|
|
if len(args) < 1 {
|
|
return bridge.JsException(v8ctx, "End requires a messageId argument")
|
|
}
|
|
|
|
// Get message ID (first argument)
|
|
if !args[0].IsString() {
|
|
return bridge.JsException(v8ctx, "messageId must be a string")
|
|
}
|
|
messageID := args[0].String()
|
|
|
|
// Get optional final content (second argument)
|
|
var finalContent string
|
|
if len(args) >= 2 && args[1].IsString() {
|
|
finalContent = args[1].String()
|
|
}
|
|
|
|
// Call ctx.End
|
|
var err error
|
|
if finalContent != "" {
|
|
err = ctx.End(messageID, finalContent)
|
|
} else {
|
|
err = ctx.End(messageID)
|
|
}
|
|
if err != nil {
|
|
return bridge.JsException(v8ctx, "End failed: "+err.Error())
|
|
}
|
|
|
|
// Automatically flush after sending
|
|
if err := ctx.Flush(); err != nil {
|
|
return bridge.JsException(v8ctx, "Flush failed: "+err.Error())
|
|
}
|
|
|
|
// Return the message ID
|
|
returnID, err := v8go.NewValue(iso, messageID)
|
|
if err != nil {
|
|
return bridge.JsException(v8ctx, "Failed to create return value: "+err.Error())
|
|
}
|
|
return returnID
|
|
})
|
|
}
|
|
|
|
// replaceMethod implements ctx.Replace(messageId, message)
|
|
// Usage: ctx.Replace(messageId, { type: "text", props: { content: "Updated content" } })
|
|
// Replaces the entire message content with the specified message_id
|
|
// Automatically flushes output
|
|
// Returns: message_id (string)
|
|
func (ctx *Context) replaceMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
v8ctx := info.Context()
|
|
args := info.Args()
|
|
|
|
// Validate arguments
|
|
if len(args) < 2 {
|
|
return bridge.JsException(v8ctx, "Replace requires messageId and message arguments")
|
|
}
|
|
|
|
// Get message ID (first argument)
|
|
if !args[0].IsString() {
|
|
return bridge.JsException(v8ctx, "messageId must be a string")
|
|
}
|
|
messageID := args[0].String()
|
|
|
|
// Parse message argument (second argument)
|
|
msg, err := parseMessage(v8ctx, args[1])
|
|
if err != nil {
|
|
return bridge.JsException(v8ctx, "invalid message: "+err.Error())
|
|
}
|
|
|
|
// Set message ID to the provided ID
|
|
msg.MessageID = messageID
|
|
|
|
// Set delta mode for replacement
|
|
msg.Delta = true
|
|
msg.DeltaAction = message.DeltaReplace
|
|
msg.DeltaPath = "" // Empty path means replace entire message
|
|
|
|
// Call ctx.Send
|
|
if err := ctx.Send(msg); err != nil {
|
|
return bridge.JsException(v8ctx, "Replace failed: "+err.Error())
|
|
}
|
|
|
|
// Automatically flush after sending
|
|
if err := ctx.Flush(); err != nil {
|
|
return bridge.JsException(v8ctx, "Flush failed: "+err.Error())
|
|
}
|
|
|
|
// Return the message ID
|
|
returnID, err := v8go.NewValue(iso, messageID)
|
|
if err != nil {
|
|
return bridge.JsException(v8ctx, "Failed to create return value: "+err.Error())
|
|
}
|
|
return returnID
|
|
})
|
|
}
|
|
|
|
// appendMethod implements ctx.Append(messageId, content, path?)
|
|
// Usage: ctx.Append(messageId, "more text") // append to default content path
|
|
// Usage: ctx.Append(messageId, "more text", "props.content") // append to specific path
|
|
// Usage: ctx.Append(messageId, { type: "text", props: { content: "more text" } })
|
|
// Usage: ctx.Append(messageId, { props: { content: "more text" } }, "props.data") // append to custom path
|
|
// Appends content to an existing message (delta append operation)
|
|
// Automatically flushes output
|
|
// Returns: message_id (string)
|
|
func (ctx *Context) appendMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
v8ctx := info.Context()
|
|
args := info.Args()
|
|
|
|
// Validate arguments
|
|
if len(args) < 2 {
|
|
return bridge.JsException(v8ctx, "Append requires messageId and content arguments")
|
|
}
|
|
|
|
// Get message ID (first argument)
|
|
if !args[0].IsString() {
|
|
return bridge.JsException(v8ctx, "messageId must be a string")
|
|
}
|
|
messageID := args[0].String()
|
|
|
|
// Parse content argument (second argument)
|
|
msg, err := parseMessage(v8ctx, args[1])
|
|
if err != nil {
|
|
return bridge.JsException(v8ctx, "invalid content: "+err.Error())
|
|
}
|
|
|
|
// Get optional path argument (third argument)
|
|
deltaPath := ""
|
|
if len(args) >= 3 && args[2].IsString() {
|
|
deltaPath = args[2].String()
|
|
}
|
|
|
|
// Set message ID to the provided ID
|
|
msg.MessageID = messageID
|
|
|
|
// Set delta mode for append
|
|
msg.Delta = true
|
|
msg.DeltaAction = message.DeltaAppend
|
|
msg.DeltaPath = deltaPath // Empty path means append to default content, or specify custom path
|
|
|
|
// Call ctx.Send
|
|
if err := ctx.Send(msg); err != nil {
|
|
return bridge.JsException(v8ctx, "Append failed: "+err.Error())
|
|
}
|
|
|
|
// Automatically flush after sending
|
|
if err := ctx.Flush(); err != nil {
|
|
return bridge.JsException(v8ctx, "Flush failed: "+err.Error())
|
|
}
|
|
|
|
// Return the message ID
|
|
returnID, err := v8go.NewValue(iso, messageID)
|
|
if err != nil {
|
|
return bridge.JsException(v8ctx, "Failed to create return value: "+err.Error())
|
|
}
|
|
return returnID
|
|
})
|
|
}
|
|
|
|
// mergeMethod implements ctx.Merge(messageId, data, path?)
|
|
// Usage: ctx.Merge(messageId, { key: "value" }) // merge to default object path
|
|
// Usage: ctx.Merge(messageId, { status: "done" }, "props") // merge to specific path
|
|
// Usage: ctx.Merge(messageId, { props: { status: "done", progress: 100 } })
|
|
// Merges data into an existing message object (delta merge operation)
|
|
// Automatically flushes output
|
|
// Returns: message_id (string)
|
|
func (ctx *Context) mergeMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
v8ctx := info.Context()
|
|
args := info.Args()
|
|
|
|
// Validate arguments
|
|
if len(args) < 2 {
|
|
return bridge.JsException(v8ctx, "Merge requires messageId and data arguments")
|
|
}
|
|
|
|
// Get message ID (first argument)
|
|
if !args[0].IsString() {
|
|
return bridge.JsException(v8ctx, "messageId must be a string")
|
|
}
|
|
messageID := args[0].String()
|
|
|
|
// Parse data argument (second argument)
|
|
msg, err := parseMessage(v8ctx, args[1])
|
|
if err != nil {
|
|
return bridge.JsException(v8ctx, "invalid data: "+err.Error())
|
|
}
|
|
|
|
// Get optional path argument (third argument)
|
|
deltaPath := ""
|
|
if len(args) >= 3 && args[2].IsString() {
|
|
deltaPath = args[2].String()
|
|
}
|
|
|
|
// Set message ID to the provided ID
|
|
msg.MessageID = messageID
|
|
|
|
// Set delta mode for merge
|
|
msg.Delta = true
|
|
msg.DeltaAction = message.DeltaMerge
|
|
msg.DeltaPath = deltaPath // Empty path means merge to default object, or specify custom path
|
|
|
|
// Call ctx.Send
|
|
if err := ctx.Send(msg); err != nil {
|
|
return bridge.JsException(v8ctx, "Merge failed: "+err.Error())
|
|
}
|
|
|
|
// Automatically flush after sending
|
|
if err := ctx.Flush(); err != nil {
|
|
return bridge.JsException(v8ctx, "Flush failed: "+err.Error())
|
|
}
|
|
|
|
// Return the message ID
|
|
returnID, err := v8go.NewValue(iso, messageID)
|
|
if err != nil {
|
|
return bridge.JsException(v8ctx, "Failed to create return value: "+err.Error())
|
|
}
|
|
return returnID
|
|
})
|
|
}
|
|
|
|
// setMethod implements ctx.Set(messageId, data, path)
|
|
// Usage: ctx.Set(messageId, "value", "props.newField") // set new field at specific path
|
|
// Usage: ctx.Set(messageId, { newKey: "value" }, "props") // set new fields in props
|
|
// Sets a new field or value in an existing message (delta set operation)
|
|
// Automatically flushes output
|
|
// Returns: message_id (string)
|
|
func (ctx *Context) setMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
v8ctx := info.Context()
|
|
args := info.Args()
|
|
|
|
// Validate arguments (path is required for Set operation)
|
|
if len(args) < 3 {
|
|
return bridge.JsException(v8ctx, "Set requires messageId, data, and path arguments")
|
|
}
|
|
|
|
// Get message ID (first argument)
|
|
if !args[0].IsString() {
|
|
return bridge.JsException(v8ctx, "messageId must be a string")
|
|
}
|
|
messageID := args[0].String()
|
|
|
|
// Parse data argument (second argument)
|
|
msg, err := parseMessage(v8ctx, args[1])
|
|
if err != nil {
|
|
return bridge.JsException(v8ctx, "invalid data: "+err.Error())
|
|
}
|
|
|
|
// Get path argument (third argument - required)
|
|
if !args[2].IsString() {
|
|
return bridge.JsException(v8ctx, "path must be a string")
|
|
}
|
|
deltaPath := args[2].String()
|
|
|
|
if deltaPath == "" {
|
|
return bridge.JsException(v8ctx, "path cannot be empty for Set operation")
|
|
}
|
|
|
|
// Set message ID to the provided ID
|
|
msg.MessageID = messageID
|
|
|
|
// Set delta mode for set
|
|
msg.Delta = true
|
|
msg.DeltaAction = message.DeltaSet
|
|
msg.DeltaPath = deltaPath // Path is required for Set operation
|
|
|
|
// Call ctx.Send
|
|
if err := ctx.Send(msg); err != nil {
|
|
return bridge.JsException(v8ctx, "Set failed: "+err.Error())
|
|
}
|
|
|
|
// Automatically flush after sending
|
|
if err := ctx.Flush(); err != nil {
|
|
return bridge.JsException(v8ctx, "Flush failed: "+err.Error())
|
|
}
|
|
|
|
// Return the message ID
|
|
returnID, err := v8go.NewValue(iso, messageID)
|
|
if err != nil {
|
|
return bridge.JsException(v8ctx, "Failed to create return value: "+err.Error())
|
|
}
|
|
return returnID
|
|
})
|
|
}
|
|
|
|
// messageIDMethod implements ctx.MessageID()
|
|
// Usage: const msgId = ctx.MessageID() // Returns: "M1", "M2", "M3"...
|
|
// Generates a unique message ID for manual message management
|
|
// Returns: message_id (string)
|
|
func (ctx *Context) messageIDMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
v8ctx := info.Context()
|
|
|
|
var messageID string
|
|
if ctx.IDGenerator != nil {
|
|
messageID = ctx.IDGenerator.GenerateMessageID()
|
|
} else {
|
|
messageID = output.GenerateID()
|
|
}
|
|
|
|
// Return the generated ID
|
|
id, err := v8go.NewValue(iso, messageID)
|
|
if err != nil {
|
|
return bridge.JsException(v8ctx, "Failed to generate message ID: "+err.Error())
|
|
}
|
|
return id
|
|
})
|
|
}
|
|
|
|
// blockIDMethod implements ctx.BlockID()
|
|
// Usage: const blockId = ctx.BlockID() // Returns: "B1", "B2", "B3"...
|
|
// Generates a unique block ID for grouping messages
|
|
// Returns: block_id (string)
|
|
func (ctx *Context) blockIDMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
v8ctx := info.Context()
|
|
|
|
var blockID string
|
|
if ctx.IDGenerator != nil {
|
|
blockID = ctx.IDGenerator.GenerateBlockID()
|
|
} else {
|
|
blockID = output.GenerateID()
|
|
}
|
|
|
|
// Return the generated ID
|
|
id, err := v8go.NewValue(iso, blockID)
|
|
if err != nil {
|
|
return bridge.JsException(v8ctx, "Failed to generate block ID: "+err.Error())
|
|
}
|
|
return id
|
|
})
|
|
}
|
|
|
|
// threadIDMethod implements ctx.ThreadID()
|
|
// Usage: const threadId = ctx.ThreadID() // Returns: "T1", "T2", "T3"...
|
|
// Generates a unique thread ID for concurrent operations
|
|
// Returns: thread_id (string)
|
|
func (ctx *Context) threadIDMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
v8ctx := info.Context()
|
|
|
|
var threadID string
|
|
if ctx.IDGenerator != nil {
|
|
threadID = ctx.IDGenerator.GenerateThreadID()
|
|
} else {
|
|
threadID = output.GenerateID()
|
|
}
|
|
|
|
// Return the generated ID
|
|
id, err := v8go.NewValue(iso, threadID)
|
|
if err != nil {
|
|
return bridge.JsException(v8ctx, "Failed to generate thread ID: "+err.Error())
|
|
}
|
|
return id
|
|
})
|
|
}
|
|
|
|
// endBlockMethod implements ctx.EndBlock(block_id)
|
|
// Usage: ctx.EndBlock("B1")
|
|
// Sends a block_end event for the specified block
|
|
// Returns: undefined
|
|
func (ctx *Context) endBlockMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
v8ctx := info.Context()
|
|
args := info.Args()
|
|
|
|
// Validate arguments
|
|
if len(args) < 1 {
|
|
return bridge.JsException(v8ctx, "EndBlock requires block_id argument")
|
|
}
|
|
|
|
if !args[0].IsString() {
|
|
return bridge.JsException(v8ctx, "block_id must be a string")
|
|
}
|
|
|
|
blockID := args[0].String()
|
|
|
|
// Call ctx.EndBlock
|
|
if err := ctx.EndBlock(blockID); err != nil {
|
|
return bridge.JsException(v8ctx, "EndBlock failed: "+err.Error())
|
|
}
|
|
|
|
// Automatically flush after ending block
|
|
if err := ctx.Flush(); err != nil {
|
|
return bridge.JsException(v8ctx, "Flush failed: "+err.Error())
|
|
}
|
|
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// createMemoryObject creates a Memory object for JavaScript access
|
|
// Memory provides four namespaces: User, Team, Chat, Context
|
|
// Each namespace supports: Get, Set, Del, Has, Keys, Len, Clear, Incr, Decr
|
|
func (ctx *Context) createMemoryObject(v8ctx *v8go.Context) *v8go.Value {
|
|
iso := v8ctx.Isolate()
|
|
objTpl := v8go.NewObjectTemplate(iso)
|
|
obj, _ := objTpl.NewInstance(v8ctx)
|
|
|
|
// Create namespace accessors
|
|
if ctx.Memory.User != nil {
|
|
userObj := ctx.createNamespaceObject(v8ctx, ctx.Memory.User)
|
|
obj.Set("user", userObj)
|
|
userObj.Release()
|
|
}
|
|
|
|
if ctx.Memory.Team != nil {
|
|
teamObj := ctx.createNamespaceObject(v8ctx, ctx.Memory.Team)
|
|
obj.Set("team", teamObj)
|
|
teamObj.Release()
|
|
}
|
|
|
|
if ctx.Memory.Chat != nil {
|
|
chatObj := ctx.createNamespaceObject(v8ctx, ctx.Memory.Chat)
|
|
obj.Set("chat", chatObj)
|
|
chatObj.Release()
|
|
}
|
|
|
|
if ctx.Memory.Context != nil {
|
|
contextObj := ctx.createNamespaceObject(v8ctx, ctx.Memory.Context)
|
|
obj.Set("context", contextObj)
|
|
contextObj.Release()
|
|
}
|
|
|
|
return obj.Value
|
|
}
|
|
|
|
// createNamespaceObject creates a namespace object with KV store methods
|
|
func (ctx *Context) createNamespaceObject(v8ctx *v8go.Context, ns *memory.Namespace) *v8go.Value {
|
|
iso := v8ctx.Isolate()
|
|
objTpl := v8go.NewObjectTemplate(iso)
|
|
obj, _ := objTpl.NewInstance(v8ctx)
|
|
|
|
// Get method: ns.Get(key)
|
|
getFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
if len(info.Args()) < 1 {
|
|
return bridge.JsException(info.Context(), "Get requires a key argument")
|
|
}
|
|
|
|
key := info.Args()[0].String()
|
|
value, ok := ns.Get(key)
|
|
if !ok {
|
|
return v8go.Null(iso)
|
|
}
|
|
|
|
jsValue, err := bridge.JsValue(info.Context(), value)
|
|
if err != nil {
|
|
return v8go.Null(iso)
|
|
}
|
|
|
|
return jsValue
|
|
})
|
|
getFuncVal := getFunc.GetFunction(v8ctx)
|
|
obj.Set("Get", getFuncVal.Value)
|
|
|
|
// Set method: ns.Set(key, value, ttl?)
|
|
setFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
if len(info.Args()) < 2 {
|
|
return bridge.JsException(info.Context(), "Set requires key and value arguments")
|
|
}
|
|
|
|
key := info.Args()[0].String()
|
|
value, err := bridge.GoValue(info.Args()[1], info.Context())
|
|
if err != nil {
|
|
return bridge.JsException(info.Context(), "Failed to convert value: "+err.Error())
|
|
}
|
|
|
|
// Optional TTL in milliseconds (third argument)
|
|
var ttl time.Duration
|
|
if len(info.Args()) >= 3 && info.Args()[2].IsNumber() {
|
|
ttlMs := info.Args()[2].Integer()
|
|
ttl = time.Duration(ttlMs) * time.Millisecond
|
|
}
|
|
|
|
if err := ns.Set(key, value, ttl); err != nil {
|
|
return bridge.JsException(info.Context(), "Failed to set value: "+err.Error())
|
|
}
|
|
|
|
return v8go.Undefined(iso)
|
|
})
|
|
setFuncVal := setFunc.GetFunction(v8ctx)
|
|
obj.Set("Set", setFuncVal.Value)
|
|
|
|
// Del method: ns.Del(key)
|
|
delFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
if len(info.Args()) < 1 {
|
|
return bridge.JsException(info.Context(), "Del requires a key argument")
|
|
}
|
|
|
|
key := info.Args()[0].String()
|
|
if err := ns.Del(key); err != nil {
|
|
return bridge.JsException(info.Context(), "Failed to delete key: "+err.Error())
|
|
}
|
|
|
|
return v8go.Undefined(iso)
|
|
})
|
|
delFuncVal := delFunc.GetFunction(v8ctx)
|
|
obj.Set("Del", delFuncVal.Value)
|
|
|
|
// Has method: ns.Has(key)
|
|
hasFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
if len(info.Args()) < 1 {
|
|
return bridge.JsException(info.Context(), "Has requires a key argument")
|
|
}
|
|
|
|
key := info.Args()[0].String()
|
|
exists := ns.Has(key)
|
|
|
|
jsValue, _ := v8go.NewValue(iso, exists)
|
|
return jsValue
|
|
})
|
|
hasFuncVal := hasFunc.GetFunction(v8ctx)
|
|
obj.Set("Has", hasFuncVal.Value)
|
|
|
|
// Keys method: ns.Keys()
|
|
keysFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
keys := ns.Keys()
|
|
jsValue, err := bridge.JsValue(info.Context(), keys)
|
|
if err != nil {
|
|
return bridge.JsException(info.Context(), "Failed to get keys: "+err.Error())
|
|
}
|
|
return jsValue
|
|
})
|
|
keysFuncVal := keysFunc.GetFunction(v8ctx)
|
|
obj.Set("Keys", keysFuncVal.Value)
|
|
|
|
// Len method: ns.Len()
|
|
lenFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
length := ns.Len()
|
|
// Use int32 for JavaScript Number (int64 becomes BigInt which is incompatible)
|
|
jsValue, _ := v8go.NewValue(iso, int32(length))
|
|
return jsValue
|
|
})
|
|
lenFuncVal := lenFunc.GetFunction(v8ctx)
|
|
obj.Set("Len", lenFuncVal.Value)
|
|
|
|
// Clear method: ns.Clear()
|
|
clearFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
ns.Clear()
|
|
return v8go.Undefined(iso)
|
|
})
|
|
clearFuncVal := clearFunc.GetFunction(v8ctx)
|
|
obj.Set("Clear", clearFuncVal.Value)
|
|
|
|
// Incr method: ns.Incr(key, delta?)
|
|
incrFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
if len(info.Args()) < 1 {
|
|
return bridge.JsException(info.Context(), "Incr requires a key argument")
|
|
}
|
|
|
|
key := info.Args()[0].String()
|
|
delta := int64(1)
|
|
if len(info.Args()) >= 2 && info.Args()[1].IsNumber() {
|
|
delta = info.Args()[1].Integer()
|
|
}
|
|
|
|
newValue, err := ns.Incr(key, delta)
|
|
if err != nil {
|
|
return bridge.JsException(info.Context(), "Failed to increment: "+err.Error())
|
|
}
|
|
|
|
// Use int32 for JavaScript Number (int64 becomes BigInt which is incompatible with ===)
|
|
// For counters, int32 range (-2^31 to 2^31-1) is sufficient
|
|
jsValue, _ := v8go.NewValue(iso, int32(newValue))
|
|
return jsValue
|
|
})
|
|
incrFuncVal := incrFunc.GetFunction(v8ctx)
|
|
obj.Set("Incr", incrFuncVal.Value)
|
|
|
|
// Decr method: ns.Decr(key, delta?)
|
|
decrFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
if len(info.Args()) < 1 {
|
|
return bridge.JsException(info.Context(), "Decr requires a key argument")
|
|
}
|
|
|
|
key := info.Args()[0].String()
|
|
delta := int64(1)
|
|
if len(info.Args()) >= 2 && info.Args()[1].IsNumber() {
|
|
delta = info.Args()[1].Integer()
|
|
}
|
|
|
|
newValue, err := ns.Decr(key, delta)
|
|
if err != nil {
|
|
return bridge.JsException(info.Context(), "Failed to decrement: "+err.Error())
|
|
}
|
|
|
|
// Use int32 for JavaScript Number (int64 becomes BigInt which is incompatible with ===)
|
|
jsValue, _ := v8go.NewValue(iso, int32(newValue))
|
|
return jsValue
|
|
})
|
|
decrFuncVal := decrFunc.GetFunction(v8ctx)
|
|
obj.Set("Decr", decrFuncVal.Value)
|
|
|
|
// GetDel method: ns.GetDel(key) - Get value and delete immediately
|
|
getDelFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
if len(info.Args()) < 1 {
|
|
return bridge.JsException(info.Context(), "GetDel requires a key argument")
|
|
}
|
|
|
|
key := info.Args()[0].String()
|
|
value, ok := ns.GetDel(key)
|
|
if !ok {
|
|
return v8go.Null(iso)
|
|
}
|
|
|
|
jsValue, err := bridge.JsValue(info.Context(), value)
|
|
if err != nil {
|
|
return v8go.Null(iso)
|
|
}
|
|
|
|
return jsValue
|
|
})
|
|
getDelFuncVal := getDelFunc.GetFunction(v8ctx)
|
|
obj.Set("GetDel", getDelFuncVal.Value)
|
|
|
|
return obj.Value
|
|
}
|
|
|
|
// sendGroupMethod implements ctx.SendGroup(group)
|
|
// Usage: ctx.SendGroup({ id: "group1", messages: [...] })
|
|
// Automatically generates IDs, sends group_start/group_end events, and flushes output
|