mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-11 22:49:57 +00:00
feat(task): replace built-in task adaptors with a sandboxed JS plugin system (#7076)
This commit is contained in:
+109
-19
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user