yao/job/jsapi/jsapi.go
Max 16b3651204 Add job.jsapi import and enhance session handling in goroutine execution
- Introduce the `job/jsapi` import in `main.go` to support new job functionalities.
- Update session handling in `goroutine.go` to restore authorized information from SharedData, improving context management during execution.
2026-02-12 23:02:22 +08:00

257 lines
7.7 KiB
Go

package jsapi
import (
"fmt"
v8 "github.com/yaoapp/gou/runtime/v8"
"github.com/yaoapp/gou/runtime/v8/bridge"
"github.com/yaoapp/yao/job"
"rogchap.com/v8go"
)
func init() {
v8.RegisterFunction("YaoJob", ExportFunction)
}
// ExportFunction exports the YaoJob constructor function template.
//
// Usage from JavaScript:
//
// // Create a persistent Job
// const j = new YaoJob({ name: "Fetch webpage", icon: "language", category_name: "Keeper" });
// j.Add("agents.yao.keeper.webfetch.URL", teamId, url, opts);
// j.Run();
// const jobId = j.id;
//
// // Static methods (no instance needed)
// const status = YaoJob.Status("job-id-xxx");
// YaoJob.Stop("job-id-xxx");
func ExportFunction(iso *v8go.Isolate) *v8go.FunctionTemplate {
tmpl := v8go.NewFunctionTemplate(iso, yaoJobConstructor)
// Register static methods on the constructor function itself
tmpl.Set("Status", yaoJobStatusStatic(iso))
tmpl.Set("Stop", yaoJobStopStatic(iso))
return tmpl
}
// yaoJobConstructor is the JavaScript constructor for YaoJob.
// Usage: new YaoJob({ name: "...", icon: "...", description: "...", category_name: "..." })
//
// Internally calls job.OnceAndSave("GOROUTINE", data).
// The JS object only stores job_id as a string — no Go pointer held.
func yaoJobConstructor(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
iso := ctx.Isolate()
args := info.Args()
// Parse data argument
data := make(map[string]interface{})
if len(args) > 0 && !args[0].IsNullOrUndefined() {
goVal, err := bridge.GoValue(args[0], ctx)
if err != nil {
return bridge.JsException(ctx, fmt.Sprintf("YaoJob: invalid argument: %s", err))
}
if m, ok := goVal.(map[string]interface{}); ok {
data = m
}
}
// Capture current V8 context's auth info to populate scope fields
if share, err := bridge.ShareData(ctx); err == nil && share != nil {
if share.Authorized != nil {
if teamID, ok := share.Authorized["team_id"].(string); ok && teamID != "" {
data["__yao_team_id"] = teamID
}
if userID, ok := share.Authorized["user_id"].(string); ok && userID != "" {
data["__yao_created_by"] = userID
}
}
}
// Create and persist job
j, err := job.OnceAndSave(job.GOROUTINE, data)
if err != nil {
return bridge.JsException(ctx, fmt.Sprintf("YaoJob: failed to create job: %s", err))
}
// Build the JS instance object — only stores job_id string
objTmpl := v8go.NewObjectTemplate(iso)
objTmpl.Set("id", j.JobID)
objTmpl.Set("Add", yaoJobAddMethod(iso, j.JobID))
objTmpl.Set("Run", yaoJobRunMethod(iso, j.JobID))
instance, err := objTmpl.NewInstance(ctx)
if err != nil {
return bridge.JsException(ctx, fmt.Sprintf("YaoJob: failed to create instance: %s", err))
}
return instance.Value
}
// yaoJobAddMethod creates the Add instance method.
// Usage: job.Add("processName", arg1, arg2, ...)
//
// Loads *Job from DB by job_id, calls job.Add(options, processName, args...), then *Job is discarded.
// Automatically captures the current V8 context's Sid and Authorized info into
// ExecutionOptions.SharedData, so the Job Worker can restore them when executing.
func yaoJobAddMethod(iso *v8go.Isolate, jobID string) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 || !args[0].IsString() {
return bridge.JsException(ctx, "YaoJob.Add: first argument must be a process name string")
}
processName := args[0].String()
// Convert remaining JS args to Go values
var processArgs []interface{}
if len(args) > 1 {
goArgs, err := bridge.GoValues(args[1:], ctx)
if err != nil {
return bridge.JsException(ctx, fmt.Sprintf("YaoJob.Add: invalid arguments: %s", err))
}
processArgs = goArgs
}
// Capture current V8 context's auth info for the Job Worker
opts := &job.ExecutionOptions{
SharedData: make(map[string]interface{}),
}
if share, err := bridge.ShareData(ctx); err == nil && share != nil {
if share.Sid != "" {
opts.SharedData["sid"] = share.Sid
}
if share.Authorized != nil {
opts.SharedData["authorized"] = share.Authorized
}
}
// Load job from DB (stateless — no Go pointer held)
j, err := job.GetJob(jobID)
if err != nil {
return bridge.JsException(ctx, fmt.Sprintf("YaoJob.Add: failed to load job %s: %s", jobID, err))
}
// Add execution with auth context
if err := j.Add(opts, processName, processArgs...); err != nil {
return bridge.JsException(ctx, fmt.Sprintf("YaoJob.Add: failed to add execution: %s", err))
}
// Return this for chaining
return info.This().Value
})
}
// yaoJobRunMethod creates the Run instance method.
// Usage: job.Run()
//
// Loads *Job from DB by job_id, calls job.Push() to submit to worker queue, then *Job is discarded.
func yaoJobRunMethod(iso *v8go.Isolate, jobID string) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
// Load job from DB (stateless)
j, err := job.GetJob(jobID)
if err != nil {
return bridge.JsException(ctx, fmt.Sprintf("YaoJob.Run: failed to load job %s: %s", jobID, err))
}
// Push to worker queue (async execution)
if err := j.Push(); err != nil {
return bridge.JsException(ctx, fmt.Sprintf("YaoJob.Run: failed to run job: %s", err))
}
return v8go.Undefined(iso)
})
}
// yaoJobStatusStatic creates the static YaoJob.Status(jobId) method.
// Usage: YaoJob.Status("job-id-xxx")
//
// Returns: { job_id, status, executions: [{ execution_id, status, progress, result?, error? }] }
func yaoJobStatusStatic(iso *v8go.Isolate) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 || !args[0].IsString() {
return bridge.JsException(ctx, "YaoJob.Status: job_id (string) is required")
}
jobID := args[0].String()
// Load job
j, err := job.GetJob(jobID)
if err != nil {
return bridge.JsException(ctx, fmt.Sprintf("YaoJob.Status: failed to load job %s: %s", jobID, err))
}
// Load executions
executions, err := job.GetExecutions(jobID)
if err != nil {
return bridge.JsException(ctx, fmt.Sprintf("YaoJob.Status: failed to load executions: %s", err))
}
// Build result
execList := make([]interface{}, 0, len(executions))
for _, exec := range executions {
entry := map[string]interface{}{
"execution_id": exec.ExecutionID,
"status": exec.Status,
"progress": exec.Progress,
}
if exec.Result != nil {
entry["result"] = string(*exec.Result)
}
if exec.ErrorInfo != nil {
entry["error"] = string(*exec.ErrorInfo)
}
execList = append(execList, entry)
}
result := map[string]interface{}{
"job_id": j.JobID,
"status": j.Status,
"executions": execList,
}
jsVal, err := bridge.JsValue(ctx, result)
if err != nil {
return bridge.JsException(ctx, fmt.Sprintf("YaoJob.Status: failed to convert result: %s", err))
}
return jsVal
})
}
// yaoJobStopStatic creates the static YaoJob.Stop(jobId) method.
// Usage: YaoJob.Stop("job-id-xxx")
func yaoJobStopStatic(iso *v8go.Isolate) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 || !args[0].IsString() {
return bridge.JsException(ctx, "YaoJob.Stop: job_id (string) is required")
}
jobID := args[0].String()
// Load job from DB
j, err := job.GetJob(jobID)
if err != nil {
return bridge.JsException(ctx, fmt.Sprintf("YaoJob.Stop: failed to load job %s: %s", jobID, err))
}
// Stop the job
if err := j.Stop(); err != nil {
return bridge.JsException(ctx, fmt.Sprintf("YaoJob.Stop: failed to stop job: %s", err))
}
return v8go.Undefined(iso)
})
}