feat(task): replace built-in task adaptors with a sandboxed JS plugin system (#7076)

This commit is contained in:
Calcium-Ion
2026-08-29 18:51:57 +08:00
committed by GitHub
parent 7037ac15bd
commit eb48396d5f
336 changed files with 52333 additions and 6369 deletions
+109 -19
View File
@@ -2,12 +2,14 @@ package model
import (
"bytes"
"context"
"database/sql/driver"
"encoding/json"
"time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/pkg/billingexpr"
commonRelay "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/relaykit/dto"
)
@@ -17,7 +19,7 @@ type TaskStatus string
func (t TaskStatus) ToVideoStatus() string {
var status string
switch t {
case TaskStatusQueued, TaskStatusSubmitted:
case TaskStatusNotStart, TaskStatusQueued, TaskStatusSubmitted:
status = dto.VideoStatusQueued
case TaskStatusInProgress:
status = dto.VideoStatusInProgress
@@ -104,22 +106,53 @@ type TaskPrivateData struct {
Key string `json:"key,omitempty"`
UpstreamTaskID string `json:"upstream_task_id,omitempty"` // 上游真实 task ID
ResultURL string `json:"result_url,omitempty"` // 任务成功后的结果 URL(视频地址等)
// Execution records safe, immutable request provenance. It lives next to
// other private task state so public task DTOs cannot expose it by accident.
Execution *TaskExecutionSnapshot `json:"execution,omitempty"`
// 计费上下文:用于异步退款/差额结算(轮询阶段读取)
BillingSource string `json:"billing_source,omitempty"` // "wallet" 或 "subscription"
SubscriptionId int `json:"subscription_id,omitempty"` // 订阅 ID,用于订阅退款
TokenId int `json:"token_id,omitempty"` // 令牌 ID,用于令牌额度退款
NodeName string `json:"node_name,omitempty"` // 发起任务的节点名,轮询结算阶段据此归属日志而非最后查询节点
BillingContext *TaskBillingContext `json:"billing_context,omitempty"` // 计费参数快照(用于轮询阶段重新计算)
// ResponsesBackground records that the openai_responses create request
// asked for background:true. Every task is durable and survives client
// disconnect regardless; this only echoes the protocol-level request
// attribute back on retrieval snapshots.
ResponsesBackground bool `json:"responses_background,omitempty"`
}
type TaskExecutionSnapshot struct {
RequestID string `json:"request_id,omitempty"`
RequestPath string `json:"request_path,omitempty"`
TaskPlugin *TaskPluginSnapshot `json:"task_plugin,omitempty"`
}
// TaskPluginSnapshot contains credential-free identity only. Plugin source,
// request/response payloads, and channel secrets must never be added here.
type TaskPluginSnapshot struct {
Key string `json:"key"`
Name string `json:"name"`
Version string `json:"version"`
Author *TaskPluginAuthorSnapshot `json:"author,omitempty"`
APIVersion int `json:"api_version"`
Generation uint64 `json:"generation"`
}
type TaskPluginAuthorSnapshot struct {
Name string `json:"name"`
URL string `json:"url,omitempty"`
}
// TaskBillingContext 记录任务提交时的计费参数,以便轮询阶段可以重新计算额度。
type TaskBillingContext struct {
ModelPrice float64 `json:"model_price,omitempty"` // 模型单价
GroupRatio float64 `json:"group_ratio,omitempty"` // 分组倍率
ModelRatio float64 `json:"model_ratio,omitempty"` // 模型倍率
OtherRatios map[string]float64 `json:"other_ratios,omitempty"` // 附加倍率(时长、分辨率等)
OriginModelName string `json:"origin_model_name,omitempty"` // 模型名称,必须为OriginModelName
PerCallBilling bool `json:"per_call_billing,omitempty"` // 按次计费:跳过轮询阶段的差额结算
ModelPrice float64 `json:"model_price,omitempty"` // 模型单价
GroupRatio float64 `json:"group_ratio,omitempty"` // 分组倍率
ModelRatio float64 `json:"model_ratio,omitempty"` // 模型倍率
OtherRatios map[string]float64 `json:"other_ratios,omitempty"` // 附加倍率(时长、分辨率等)
OriginModelName string `json:"origin_model_name,omitempty"` // 模型名称,必须为OriginModelName
PerCallBilling bool `json:"per_call_billing,omitempty"` // 按次计费:跳过轮询阶段的差额结算
TieredSnapshot *billingexpr.BillingSnapshot `json:"tiered_snapshot,omitempty"`
}
// GetUpstreamTaskID 获取上游真实 task ID(用于与 provider 通信)
@@ -334,6 +367,38 @@ func HasUnfinishedSyncTasks() bool {
return err == nil && id != 0
}
func GetByOnlyTaskId(taskId string) (*Task, bool, error) {
if taskId == "" {
return nil, false, nil
}
var task *Task
var err error
err = DB.Where("task_id = ?", taskId).First(&task).Error
exist, err := RecordExist(err)
if err != nil {
return nil, false, err
}
return task, exist, err
}
// GetUniqueByOnlyTaskId resolves a public task identifier only when exactly one
// row owns it. Historical task identifiers were not globally unique, so
// capability-based reads must fail closed instead of selecting an arbitrary
// tenant's row.
func GetUniqueByOnlyTaskId(taskId string) (*Task, bool, error) {
if taskId == "" {
return nil, false, nil
}
var tasks []*Task
if err := DB.Where("task_id = ?", taskId).Order("id").Limit(2).Find(&tasks).Error; err != nil {
return nil, false, err
}
if len(tasks) != 1 {
return nil, false, nil
}
return tasks[0], true, nil
}
func GetByTaskId(userId int, taskId string) (*Task, bool, error) {
if taskId == "" {
return nil, false, nil
@@ -349,24 +414,44 @@ func GetByTaskId(userId int, taskId string) (*Task, bool, error) {
return task, exist, err
}
func GetByTaskIds(userId int, taskIds []any) ([]*Task, error) {
if len(taskIds) == 0 {
func GetByTaskIdsForPlatforms(userID int, platforms []constant.TaskPlatform, taskIDs []string) ([]*Task, error) {
if len(platforms) == 0 || len(taskIDs) == 0 {
return nil, nil
}
var task []*Task
var err error
err = DB.Where("user_id = ? and task_id in (?)", userId, taskIds).
Find(&task).Error
var tasks []*Task
err := DB.
Where("user_id = ? AND platform IN ? AND task_id IN ?", userID, platforms, taskIDs).
Find(&tasks).Error
if err != nil {
return nil, err
}
return task, nil
return tasks, nil
}
// GetTaskForProtocolObservation reloads one public task through the ownership
// boundary used by long-lived plugin protocol observers. A missing task,
// foreign user, and wrong plugin platform are deliberately indistinguishable.
func GetTaskForProtocolObservation(ctx context.Context, userID int, platform constant.TaskPlatform, taskID string) (*Task, bool, error) {
if taskID == "" {
return nil, false, nil
}
var task Task
err := DB.WithContext(ctx).
Where("user_id = ? AND platform = ? AND task_id = ?", userID, platform, taskID).
First(&task).Error
exists, err := RecordExist(err)
if err != nil || !exists {
return nil, exists, err
}
return &task, true, nil
}
func (Task *Task) Insert() error {
var err error
err = DB.Create(Task).Error
return err
return Task.InsertWithContext(context.Background())
}
func (Task *Task) InsertWithContext(ctx context.Context) error {
return DB.WithContext(ctx).Create(Task).Error
}
type taskSnapshot struct {
@@ -514,7 +599,12 @@ func (t *Task) ToOpenAIVideo() *dto.OpenAIVideo {
openAIVideo.Model = t.Properties.OriginModelName
openAIVideo.SetProgressStr(t.Progress)
openAIVideo.CreatedAt = t.CreatedAt
openAIVideo.CompletedAt = t.UpdatedAt
openAIVideo.SetMetadata("url", t.GetResultURL())
if t.Status == TaskStatusSuccess {
if t.FinishTime != 0 {
openAIVideo.CompletedAt = t.FinishTime
} else {
openAIVideo.CompletedAt = t.UpdatedAt
}
}
return openAIVideo
}