- 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.
257 lines
7.7 KiB
Go
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)
|
|
})
|
|
}
|