diff --git a/CLAUDE.md b/CLAUDE.md index 6b19690..f503933 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -290,7 +290,7 @@ wallets 表包含 `frozen_balance` 字段,用于支付中/退款中的金额 | `aud_` | 审计日志 | 三 | `aud_request_logs` `aud_operation_logs` `aud_sensitive_access_logs` `aud_login_history` | | `spt_` | 客户支持(工单+帮助+反馈) | 三/六 | `spt_tickets` `spt_replies` `spt_attachments` `spt_categories` `spt_articles` `spt_feedbacks` | | `ops_` | 运维监控 | 四 | `ops_alert_rules` `ops_alert_events` `ops_system_metrics` | -| `tsk_` | 异步任务 | 四 | `tsk_tasks` `tsk_task_logs` `tsk_async_tasks` | +| `tsk_` | 异步任务 | 四 | `tsk_tasks` `tsk_task_logs` `tsk_model_tasks` | | `fil_` | 文件存储 | 四 | `fil_files` | | `opn_` | 开放平台 + Webhook | 五 | `opn_apps` `opn_webhook_configs` `opn_webhook_events` `opn_webhook_delivery_logs` | | `clg_` | 更新日志 | 六 | `clg_changelogs` | diff --git a/api/admin/admin.go b/api/admin/admin.go index 9cdcdef..ae6ebff 100644 --- a/api/admin/admin.go +++ b/api/admin/admin.go @@ -196,7 +196,6 @@ type IAdminV1 interface { TaskList(ctx context.Context, req *v1.TaskListReq) (res *v1.TaskListRes, err error) TaskDetail(ctx context.Context, req *v1.TaskDetailReq) (res *v1.TaskDetailRes, err error) TaskCancel(ctx context.Context, req *v1.TaskCancelReq) (res *v1.TaskCancelRes, err error) - TaskRetry(ctx context.Context, req *v1.TaskRetryReq) (res *v1.TaskRetryRes, err error) TenantCreate(ctx context.Context, req *v1.TenantCreateReq) (res *v1.TenantCreateRes, err error) TenantList(ctx context.Context, req *v1.TenantListReq) (res *v1.TenantListRes, err error) TenantGet(ctx context.Context, req *v1.TenantGetReq) (res *v1.TenantGetRes, err error) diff --git a/api/admin/v1/task_management.go b/api/admin/v1/task_management.go index 1f923d9..153c2dd 100644 --- a/api/admin/v1/task_management.go +++ b/api/admin/v1/task_management.go @@ -2,65 +2,55 @@ package v1 import "github.com/gogf/gf/v2/frame/g" -// === 任务管理 === +// === 大模型异步任务管理 === type TaskListReq struct { - g.Meta `path:"/tasks" method:"get" mime:"json" tags:"管理后台-任务管理" summary:"任务列表"` + g.Meta `path:"/tasks" method:"get" mime:"json" tags:"管理后台-任务管理" summary:"大模型异步任务列表"` Page int `json:"page" in:"query" d:"1" v:"min:1" dc:"页码"` PageSize int `json:"page_size" in:"query" d:"20" v:"min:1|max:100" dc:"每页数量"` Status string `json:"status" in:"query" dc:"筛选状态"` - Handler string `json:"handler" in:"query" dc:"筛选handler"` + Platform string `json:"platform" in:"query" dc:"筛选平台(sora/kling/midjourney/suno)"` } -type TaskItem struct { - ID int64 `json:"id"` - Name string `json:"name"` - Handler string `json:"handler"` - Status string `json:"status"` - MaxRetries int `json:"max_retries"` - RetryCount int `json:"retry_count"` - ErrorMessage string `json:"error_message,omitempty"` - StartedAt string `json:"started_at,omitempty"` - FinishedAt string `json:"finished_at,omitempty"` - ScheduledAt string `json:"scheduled_at,omitempty"` - CreatedAt string `json:"created_at"` +type ModelTaskItem struct { + ID int64 `json:"id"` + PublicTaskID string `json:"public_task_id"` + Platform string `json:"platform"` + Action string `json:"action"` + Status string `json:"status"` + Progress string `json:"progress"` + ModelName string `json:"model_name"` + FailReason string `json:"fail_reason,omitempty"` + PreDeductAmount float64 `json:"pre_deduct_amount"` + ActualCost float64 `json:"actual_cost"` + BillingSettled bool `json:"billing_settled"` + ResultURL string `json:"result_url,omitempty"` + TenantID int64 `json:"tenant_id"` + UserID int64 `json:"user_id"` + SubmitTime string `json:"submit_time,omitempty"` + FinishTime string `json:"finish_time,omitempty"` + CreatedAt string `json:"created_at"` } type TaskListRes struct { - List []TaskItem `json:"list"` - Total int `json:"total"` - Page int `json:"page"` - PageSize int `json:"page_size"` + List []ModelTaskItem `json:"list"` + Total int `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` } type TaskDetailReq struct { - g.Meta `path:"/tasks/{id}" method:"get" mime:"json" tags:"管理后台-任务管理" summary:"任务详情"` + g.Meta `path:"/tasks/{id}" method:"get" mime:"json" tags:"管理后台-任务管理" summary:"大模型异步任务详情"` ID int64 `json:"id" in:"path" v:"required" dc:"任务ID"` } -type TaskLogItem struct { - Level string `json:"level"` - Message string `json:"message"` - CreatedAt string `json:"created_at"` -} - type TaskDetailRes struct { - Task TaskItem `json:"task"` - RecentLogs []TaskLogItem `json:"recent_logs"` + Task ModelTaskItem `json:"task"` } type TaskCancelReq struct { - g.Meta `path:"/tasks/{id}/cancel" method:"post" mime:"json" tags:"管理后台-任务管理" summary:"取消任务"` + g.Meta `path:"/tasks/{id}/cancel" method:"post" mime:"json" tags:"管理后台-任务管理" summary:"取消大模型异步任务"` ID int64 `json:"id" in:"path" v:"required" dc:"任务ID"` } type TaskCancelRes struct{} - -type TaskRetryReq struct { - g.Meta `path:"/tasks/{id}/retry" method:"post" mime:"json" tags:"管理后台-任务管理" summary:"重试任务"` - ID int64 `json:"id" in:"path" v:"required" dc:"任务ID"` -} - -type TaskRetryRes struct { - TaskID int64 `json:"task_id"` -} diff --git a/internal/controller/admin/admin_v1_task_retry.go b/internal/controller/admin/admin_v1_task_retry.go deleted file mode 100644 index ca548a3..0000000 --- a/internal/controller/admin/admin_v1_task_retry.go +++ /dev/null @@ -1,12 +0,0 @@ -package admin - -import ( - "context" - - "github.com/qianfree/team-api/api/admin/v1" - "github.com/qianfree/team-api/internal/service" -) - -func (c *ControllerV1) TaskRetry(ctx context.Context, req *v1.TaskRetryReq) (res *v1.TaskRetryRes, err error) { - return service.Admin().TaskRetry(ctx, req) -} diff --git a/internal/dao/internal/chn_channel_keys.go b/internal/dao/internal/chn_channel_keys.go index 218ce66..5a21b93 100644 --- a/internal/dao/internal/chn_channel_keys.go +++ b/internal/dao/internal/chn_channel_keys.go @@ -29,9 +29,9 @@ type ChnChannelKeysColumns struct { LastUsedAt string // 最后使用时间 LastError string // 最后一次错误信息 CreatedAt string // 创建时间 + UpdatedAt string // 更新时间 KeyType string // Key 类型:apikey(传统静态密钥)/ oauth(OAuth 令牌) TokenExpiresAt string // OAuth access_token 过期时间(仅 key_type=oauth 时有值) - UpdatedAt string // 更新时间 } // chnChannelKeysColumns holds the columns for the table chn_channel_keys. @@ -44,9 +44,9 @@ var chnChannelKeysColumns = ChnChannelKeysColumns{ LastUsedAt: "last_used_at", LastError: "last_error", CreatedAt: "created_at", + UpdatedAt: "updated_at", KeyType: "key_type", TokenExpiresAt: "token_expires_at", - UpdatedAt: "updated_at", } // NewChnChannelKeysDao creates and returns a new DAO object for table data access. diff --git a/internal/dao/internal/mdl_models.go b/internal/dao/internal/mdl_models.go index 88fe9ec..6e7d88d 100644 --- a/internal/dao/internal/mdl_models.go +++ b/internal/dao/internal/mdl_models.go @@ -30,12 +30,12 @@ type MdlModelsColumns struct { MaxOutputTokens string // 最大输出 token 数 Description string // 模型描述 Tags string // 标签(如 reasoning、vision、function_calling) - Capabilities string // 模型能力特性(如 vision、function_calling、reasoning 等) CreatedAt string // 创建时间 UpdatedAt string // 更新时间 DeprecatedAt string // 标记弃用的时间(NULL表示未弃用) SunsetDate string // 计划下线日期(到达后返回410 Gone,NULL表示未设置) ReplacementModel string // 推荐替代模型名(NULL表示无替代) + Capabilities string // 模型能力特性(如 vision、function_calling、reasoning 等) } // mdlModelsColumns holds the columns for the table mdl_models. @@ -49,12 +49,12 @@ var mdlModelsColumns = MdlModelsColumns{ MaxOutputTokens: "max_output_tokens", Description: "description", Tags: "tags", - Capabilities: "capabilities", CreatedAt: "created_at", UpdatedAt: "updated_at", DeprecatedAt: "deprecated_at", SunsetDate: "sunset_date", ReplacementModel: "replacement_model", + Capabilities: "capabilities", } // NewMdlModelsDao creates and returns a new DAO object for table data access. diff --git a/internal/dao/internal/ntf_messages.go b/internal/dao/internal/ntf_messages.go index e84cd42..c6d277b 100644 --- a/internal/dao/internal/ntf_messages.go +++ b/internal/dao/internal/ntf_messages.go @@ -31,8 +31,8 @@ type NtfMessagesColumns struct { IsRead string // 是否已读:0=未读, 1=已读 IsBroadcast string // 是否广播消息:0=个人消息, 1=广播消息 Metadata string // 附加元数据(JSONB,如关联资源ID、跳转链接等) - TargetRoles string // 目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色) CreatedAt string // 创建时间 + TargetRoles string // 目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色) } // ntfMessagesColumns holds the columns for the table ntf_messages. @@ -47,8 +47,8 @@ var ntfMessagesColumns = NtfMessagesColumns{ IsRead: "is_read", IsBroadcast: "is_broadcast", Metadata: "metadata", - TargetRoles: "target_roles", CreatedAt: "created_at", + TargetRoles: "target_roles", } // NewNtfMessagesDao creates and returns a new DAO object for table data access. diff --git a/internal/dao/internal/plg_example_logs.go b/internal/dao/internal/plg_example_logs.go new file mode 100644 index 0000000..bb527de --- /dev/null +++ b/internal/dao/internal/plg_example_logs.go @@ -0,0 +1,87 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// PlgExampleLogsDao is the data access object for the table plg_example_logs. +type PlgExampleLogsDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns PlgExampleLogsColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// PlgExampleLogsColumns defines and stores column names for the table plg_example_logs. +type PlgExampleLogsColumns struct { + Id string // + TenantId string // + Action string // + Detail string // + CreatedAt string // +} + +// plgExampleLogsColumns holds the columns for the table plg_example_logs. +var plgExampleLogsColumns = PlgExampleLogsColumns{ + Id: "id", + TenantId: "tenant_id", + Action: "action", + Detail: "detail", + CreatedAt: "created_at", +} + +// NewPlgExampleLogsDao creates and returns a new DAO object for table data access. +func NewPlgExampleLogsDao(handlers ...gdb.ModelHandler) *PlgExampleLogsDao { + return &PlgExampleLogsDao{ + group: "default", + table: "plg_example_logs", + columns: plgExampleLogsColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *PlgExampleLogsDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *PlgExampleLogsDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *PlgExampleLogsDao) Columns() PlgExampleLogsColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *PlgExampleLogsDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *PlgExampleLogsDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *PlgExampleLogsDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/tsk_async_tasks.go b/internal/dao/internal/tsk_model_tasks.go similarity index 76% rename from internal/dao/internal/tsk_async_tasks.go rename to internal/dao/internal/tsk_model_tasks.go index a3600fc..8752f84 100644 --- a/internal/dao/internal/tsk_async_tasks.go +++ b/internal/dao/internal/tsk_model_tasks.go @@ -11,16 +11,16 @@ import ( "github.com/gogf/gf/v2/frame/g" ) -// TskAsyncTasksDao is the data access object for the table tsk_async_tasks. -type TskAsyncTasksDao struct { +// TskModelTasksDao is the data access object for the table tsk_model_tasks. +type TskModelTasksDao struct { table string // table is the underlying table name of the DAO. group string // group is the database configuration group name of the current DAO. - columns TskAsyncTasksColumns // columns contains all the column names of Table for convenient usage. + columns TskModelTasksColumns // columns contains all the column names of Table for convenient usage. handlers []gdb.ModelHandler // handlers for customized model modification. } -// TskAsyncTasksColumns defines and stores column names for the table tsk_async_tasks. -type TskAsyncTasksColumns struct { +// TskModelTasksColumns defines and stores column names for the table tsk_model_tasks. +type TskModelTasksColumns struct { Id string // PublicTaskId string // Platform string // @@ -47,8 +47,8 @@ type TskAsyncTasksColumns struct { UpdatedAt string // } -// tskAsyncTasksColumns holds the columns for the table tsk_async_tasks. -var tskAsyncTasksColumns = TskAsyncTasksColumns{ +// tskModelTasksColumns holds the columns for the table tsk_model_tasks. +var tskModelTasksColumns = TskModelTasksColumns{ Id: "id", PublicTaskId: "public_task_id", Platform: "platform", @@ -75,38 +75,38 @@ var tskAsyncTasksColumns = TskAsyncTasksColumns{ UpdatedAt: "updated_at", } -// NewTskAsyncTasksDao creates and returns a new DAO object for table data access. -func NewTskAsyncTasksDao(handlers ...gdb.ModelHandler) *TskAsyncTasksDao { - return &TskAsyncTasksDao{ +// NewTskModelTasksDao creates and returns a new DAO object for table data access. +func NewTskModelTasksDao(handlers ...gdb.ModelHandler) *TskModelTasksDao { + return &TskModelTasksDao{ group: "default", - table: "tsk_async_tasks", - columns: tskAsyncTasksColumns, + table: "tsk_model_tasks", + columns: tskModelTasksColumns, handlers: handlers, } } // DB retrieves and returns the underlying raw database management object of the current DAO. -func (dao *TskAsyncTasksDao) DB() gdb.DB { +func (dao *TskModelTasksDao) DB() gdb.DB { return g.DB(dao.group) } // Table returns the table name of the current DAO. -func (dao *TskAsyncTasksDao) Table() string { +func (dao *TskModelTasksDao) Table() string { return dao.table } // Columns returns all column names of the current DAO. -func (dao *TskAsyncTasksDao) Columns() TskAsyncTasksColumns { +func (dao *TskModelTasksDao) Columns() TskModelTasksColumns { return dao.columns } // Group returns the database configuration group name of the current DAO. -func (dao *TskAsyncTasksDao) Group() string { +func (dao *TskModelTasksDao) Group() string { return dao.group } // Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. -func (dao *TskAsyncTasksDao) Ctx(ctx context.Context) *gdb.Model { +func (dao *TskModelTasksDao) Ctx(ctx context.Context) *gdb.Model { model := dao.DB().Model(dao.table) for _, handler := range dao.handlers { model = handler(model) @@ -120,6 +120,6 @@ func (dao *TskAsyncTasksDao) Ctx(ctx context.Context) *gdb.Model { // // Note: Do not commit or roll back the transaction in function f, // as it is automatically handled by this function. -func (dao *TskAsyncTasksDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { +func (dao *TskModelTasksDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { return dao.Ctx(ctx).Transaction(ctx, f) } diff --git a/internal/dao/tsk_async_tasks.go b/internal/dao/plg_example_logs.go similarity index 51% rename from internal/dao/tsk_async_tasks.go rename to internal/dao/plg_example_logs.go index eb3907d..65fd27a 100644 --- a/internal/dao/tsk_async_tasks.go +++ b/internal/dao/plg_example_logs.go @@ -8,20 +8,15 @@ import ( "github.com/qianfree/team-api/internal/dao/internal" ) -// internalTskAsyncTasksDao is an internal type for wrapping the internal DAO implementation. -type internalTskAsyncTasksDao = *internal.TskAsyncTasksDao - -// tskAsyncTasksDao is the data access object for the table tsk_async_tasks. +// plgExampleLogsDao is the data access object for the table plg_example_logs. // You can define custom methods on it to extend its functionality as needed. -type tskAsyncTasksDao struct { - internalTskAsyncTasksDao +type plgExampleLogsDao struct { + *internal.PlgExampleLogsDao } var ( - // TskAsyncTasks is a globally accessible object for table tsk_async_tasks operations. - TskAsyncTasks = tskAsyncTasksDao{ - internal.NewTskAsyncTasksDao(), - } + // PlgExampleLogs is a globally accessible object for table plg_example_logs operations. + PlgExampleLogs = plgExampleLogsDao{internal.NewPlgExampleLogsDao()} ) // Add your custom methods and functionality below. diff --git a/internal/dao/tsk_model_tasks.go b/internal/dao/tsk_model_tasks.go new file mode 100644 index 0000000..66e0fed --- /dev/null +++ b/internal/dao/tsk_model_tasks.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "github.com/qianfree/team-api/internal/dao/internal" +) + +// tskModelTasksDao is the data access object for the table tsk_model_tasks. +// You can define custom methods on it to extend its functionality as needed. +type tskModelTasksDao struct { + *internal.TskModelTasksDao +} + +var ( + // TskModelTasks is a globally accessible object for table tsk_model_tasks operations. + TskModelTasks = tskModelTasksDao{internal.NewTskModelTasksDao()} +) + +// Add your custom methods and functionality below. diff --git a/internal/logic/admin/task_management.go b/internal/logic/admin/task_management.go index 82c7c40..f4e5cfb 100644 --- a/internal/logic/admin/task_management.go +++ b/internal/logic/admin/task_management.go @@ -14,28 +14,34 @@ import ( do "github.com/qianfree/team-api/internal/model/do" ) -// TaskList 任务列表 +// TaskList 大模型异步任务列表 func (s *sAdmin) TaskList(ctx context.Context, req *v1.TaskListReq) (*v1.TaskListRes, error) { - m := dao.TskTasks.Ctx(ctx).OrderDesc("id") + m := dao.TskModelTasks.Ctx(ctx).OrderDesc("id") if req.Status != "" { m = m.Where("status", req.Status) } - if req.Handler != "" { - m = m.Where("handler", req.Handler) + if req.Platform != "" { + m = m.Where("platform", req.Platform) } var tasks []struct { - Id int64 `json:"id"` - Name string `json:"name"` - Handler string `json:"handler"` - Status string `json:"status"` - MaxRetries int `json:"max_retries"` - RetryCount int `json:"retry_count"` - ErrorMessage string `json:"error_message"` - StartedAt *gtime.Time `json:"started_at"` - FinishedAt *gtime.Time `json:"finished_at"` - ScheduledAt *gtime.Time `json:"scheduled_at"` - CreatedAt *gtime.Time `json:"created_at"` + Id int64 `json:"id"` + PublicTaskId string `json:"public_task_id"` + Platform string `json:"platform"` + Action string `json:"action"` + Status string `json:"status"` + Progress string `json:"progress"` + ModelName string `json:"model_name"` + FailReason string `json:"fail_reason"` + PreDeductAmount float64 `json:"pre_deduct_amount"` + ActualCost float64 `json:"actual_cost"` + BillingSettled bool `json:"billing_settled"` + ResultUrl string `json:"result_url"` + TenantId int64 `json:"tenant_id"` + UserId int64 `json:"user_id"` + SubmitTime *gtime.Time `json:"submit_time"` + FinishTime *gtime.Time `json:"finish_time"` + CreatedAt *gtime.Time `json:"created_at"` } var total int @@ -44,26 +50,30 @@ func (s *sAdmin) TaskList(ctx context.Context, req *v1.TaskListReq) (*v1.TaskLis return nil, err } - list := make([]v1.TaskItem, 0, len(tasks)) + list := make([]v1.ModelTaskItem, 0, len(tasks)) for _, t := range tasks { - item := v1.TaskItem{ - ID: t.Id, - Name: t.Name, - Handler: t.Handler, - Status: t.Status, - MaxRetries: t.MaxRetries, - RetryCount: t.RetryCount, - ErrorMessage: t.ErrorMessage, - CreatedAt: t.CreatedAt.Format("Y-m-d H:i:s"), + item := v1.ModelTaskItem{ + ID: t.Id, + PublicTaskID: t.PublicTaskId, + Platform: t.Platform, + Action: t.Action, + Status: t.Status, + Progress: t.Progress, + ModelName: t.ModelName, + FailReason: t.FailReason, + PreDeductAmount: t.PreDeductAmount, + ActualCost: t.ActualCost, + BillingSettled: t.BillingSettled, + ResultURL: t.ResultUrl, + TenantID: t.TenantId, + UserID: t.UserId, + CreatedAt: t.CreatedAt.Format("Y-m-d H:i:s"), } - if t.StartedAt != nil { - item.StartedAt = t.StartedAt.Format("Y-m-d H:i:s") + if t.SubmitTime != nil { + item.SubmitTime = t.SubmitTime.Format("Y-m-d H:i:s") } - if t.FinishedAt != nil { - item.FinishedAt = t.FinishedAt.Format("Y-m-d H:i:s") - } - if t.ScheduledAt != nil { - item.ScheduledAt = t.ScheduledAt.Format("Y-m-d H:i:s") + if t.FinishTime != nil { + item.FinishTime = t.FinishTime.Format("Y-m-d H:i:s") } list = append(list, item) } @@ -76,22 +86,30 @@ func (s *sAdmin) TaskList(ctx context.Context, req *v1.TaskListReq) (*v1.TaskLis }, nil } -// TaskDetail 任务详情 +// TaskDetail 大模型异步任务详情 func (s *sAdmin) TaskDetail(ctx context.Context, req *v1.TaskDetailReq) (*v1.TaskDetailRes, error) { var task *struct { - Id int64 `json:"id"` - Name string `json:"name"` - Handler string `json:"handler"` - Status string `json:"status"` - MaxRetries int `json:"max_retries"` - RetryCount int `json:"retry_count"` - ErrorMessage string `json:"error_message"` - StartedAt *gtime.Time `json:"started_at"` - FinishedAt *gtime.Time `json:"finished_at"` - ScheduledAt *gtime.Time `json:"scheduled_at"` - CreatedAt *gtime.Time `json:"created_at"` + Id int64 `json:"id"` + PublicTaskId string `json:"public_task_id"` + Platform string `json:"platform"` + Action string `json:"action"` + Status string `json:"status"` + Progress string `json:"progress"` + ModelName string `json:"model_name"` + UpstreamModel string `json:"upstream_model"` + FailReason string `json:"fail_reason"` + PreDeductAmount float64 `json:"pre_deduct_amount"` + ActualCost float64 `json:"actual_cost"` + BillingSettled bool `json:"billing_settled"` + ResultUrl string `json:"result_url"` + TenantId int64 `json:"tenant_id"` + UserId int64 `json:"user_id"` + SubmitTime *gtime.Time `json:"submit_time"` + StartTime *gtime.Time `json:"start_time"` + FinishTime *gtime.Time `json:"finish_time"` + CreatedAt *gtime.Time `json:"created_at"` } - err := dao.TskTasks.Ctx(ctx).Where("id", req.ID).Scan(&task) + err := dao.TskModelTasks.Ctx(ctx).Where("id", req.ID).Scan(&task) if err != nil { return nil, err } @@ -99,106 +117,56 @@ func (s *sAdmin) TaskDetail(ctx context.Context, req *v1.TaskDetailReq) (*v1.Tas return nil, gerror.NewCode(gcode.New(consts.CodeNotFound, consts.MsgNotFound, nil), consts.MsgNotFound) } - taskItem := v1.TaskItem{ - ID: task.Id, - Name: task.Name, - Handler: task.Handler, - Status: task.Status, - MaxRetries: task.MaxRetries, - RetryCount: task.RetryCount, - ErrorMessage: task.ErrorMessage, - CreatedAt: task.CreatedAt.Format("Y-m-d H:i:s"), + item := v1.ModelTaskItem{ + ID: task.Id, + PublicTaskID: task.PublicTaskId, + Platform: task.Platform, + Action: task.Action, + Status: task.Status, + Progress: task.Progress, + ModelName: task.ModelName, + FailReason: task.FailReason, + PreDeductAmount: task.PreDeductAmount, + ActualCost: task.ActualCost, + BillingSettled: task.BillingSettled, + ResultURL: task.ResultUrl, + TenantID: task.TenantId, + UserID: task.UserId, + CreatedAt: task.CreatedAt.Format("Y-m-d H:i:s"), } - if task.StartedAt != nil { - taskItem.StartedAt = task.StartedAt.Format("Y-m-d H:i:s") + if task.SubmitTime != nil { + item.SubmitTime = task.SubmitTime.Format("Y-m-d H:i:s") } - if task.FinishedAt != nil { - taskItem.FinishedAt = task.FinishedAt.Format("Y-m-d H:i:s") - } - if task.ScheduledAt != nil { - taskItem.ScheduledAt = task.ScheduledAt.Format("Y-m-d H:i:s") - } - - // 查询最近日志 - var logs []struct { - Level string `json:"level"` - Message string `json:"message"` - CreatedAt *gtime.Time `json:"created_at"` - } - _ = dao.TskTaskLogs.Ctx(ctx). - Where("task_id", req.ID). - OrderDesc("id"). - Limit(50). - Scan(&logs) - - logList := make([]v1.TaskLogItem, 0, len(logs)) - for _, l := range logs { - logList = append(logList, v1.TaskLogItem{ - Level: l.Level, - Message: l.Message, - CreatedAt: l.CreatedAt.Format("Y-m-d H:i:s"), - }) + if task.FinishTime != nil { + item.FinishTime = task.FinishTime.Format("Y-m-d H:i:s") } return &v1.TaskDetailRes{ - Task: taskItem, - RecentLogs: logList, + Task: item, }, nil } -// TaskCancel 取消任务 +// TaskCancel 取消大模型异步任务 func (s *sAdmin) TaskCancel(ctx context.Context, req *v1.TaskCancelReq) (*v1.TaskCancelRes, error) { - result, err := dao.TskTasks.Ctx(ctx). + result, err := dao.TskModelTasks.Ctx(ctx). Where("id", req.ID). - WhereIn("status", []string{"pending", "running"}). - Data(do.TskTasks{ - Status: "cancelled", - FinishedAt: gtime.Now(), + WhereIn("status", []string{"NOT_START", "SUBMITTED", "IN_PROGRESS"}). + Data(do.TskModelTasks{ + Status: "FAILURE", + FailReason: "管理员手动取消", + FinishTime: gtime.Now(), }).Update() if err != nil { return nil, err } rowsAffected, _ := result.RowsAffected() if rowsAffected == 0 { - return nil, gerror.New("任务无法取消(可能已完成或已取消)") + return nil, gerror.New("任务无法取消(可能已完成或已失败)") } return &v1.TaskCancelRes{}, nil } -// TaskRetry 重试失败任务 -func (s *sAdmin) TaskRetry(ctx context.Context, req *v1.TaskRetryReq) (*v1.TaskRetryRes, error) { - var task *struct { - Id int64 `json:"id"` - Status string `json:"status"` - RetryCount int `json:"retry_count"` - MaxRetries int `json:"max_retries"` - } - err := dao.TskTasks.Ctx(ctx).Where("id", req.ID).Scan(&task) - if err != nil { - return nil, err - } - if task == nil { - return nil, gerror.NewCode(gcode.New(consts.CodeNotFound, consts.MsgNotFound, nil), consts.MsgNotFound) - } - if task.Status != "failed" { - return nil, gerror.NewCode(gcode.New(consts.CodeTaskNotRetryable, consts.MsgTaskNotRetryable, nil), - consts.MsgTaskNotRetryable) - } - - _, err = dao.TskTasks.Ctx(ctx).Where("id", req.ID).Data(do.TskTasks{ - Status: "pending", - RetryCount: task.RetryCount + 1, - ErrorMessage: "", - StartedAt: nil, - FinishedAt: nil, - }).Update() - if err != nil { - return nil, err - } - return &v1.TaskRetryRes{TaskID: task.Id}, nil -} - -// MarkStuckTasksFailed 标记卡住的任务为失败(定时任务) +// MarkStuckTasksFailed 标记卡住的系统任务为失败(定时任务,操作 tsk_tasks 表) func MarkStuckTasksFailed(ctx context.Context) error { threshold := time.Now().Add(-30 * time.Minute) _, err := dao.TskTasks.Ctx(ctx). diff --git a/internal/logic/task/active_counter.go b/internal/logic/task/active_counter.go index 98c2c76..84f464d 100644 --- a/internal/logic/task/active_counter.go +++ b/internal/logic/task/active_counter.go @@ -30,7 +30,7 @@ func HasActiveTasks() bool { // InitActiveCount initializes the counter from the database at startup. func InitActiveCount(ctx context.Context) { - count, err := dao.TskAsyncTasks.Ctx(ctx). + count, err := dao.TskModelTasks.Ctx(ctx). Where("status NOT IN (?, ?)", "SUCCESS", "FAILURE"). Count() if err != nil { diff --git a/internal/logic/task/async_provider.go b/internal/logic/task/async_provider.go index c178446..f25170d 100644 --- a/internal/logic/task/async_provider.go +++ b/internal/logic/task/async_provider.go @@ -19,7 +19,7 @@ var DefaultAsyncProvider = &AsyncProvider{} // CreateTask 创建异步任务记录 func (p *AsyncProvider) CreateTask(ctx context.Context, task *common.AsyncTask) error { - _, err := dao.TskAsyncTasks.Ctx(ctx).Insert(map[string]any{ + _, err := dao.TskModelTasks.Ctx(ctx).Insert(map[string]any{ "public_task_id": task.PublicTaskID, "platform": task.Platform, "action": task.Action, @@ -49,7 +49,7 @@ func (p *AsyncProvider) CreateTask(ctx context.Context, task *common.AsyncTask) // UpdateTask 更新任务记录 func (p *AsyncProvider) UpdateTask(ctx context.Context, task *common.AsyncTask) error { - _, err := dao.TskAsyncTasks.Ctx(ctx). + _, err := dao.TskModelTasks.Ctx(ctx). Where("id", task.ID). Update(map[string]any{ "status": task.Status, @@ -71,7 +71,7 @@ func (p *AsyncProvider) UpdateTask(ctx context.Context, task *common.AsyncTask) // UpdateTaskCAS CAS 状态更新 func (p *AsyncProvider) UpdateTaskCAS(ctx context.Context, task *common.AsyncTask, oldStatus string) error { - result, err := dao.TskAsyncTasks.Ctx(ctx). + result, err := dao.TskModelTasks.Ctx(ctx). Where("id", task.ID). Where("status", oldStatus). Update(map[string]any{ @@ -124,7 +124,7 @@ func (p *AsyncProvider) GetTaskByPublicID(ctx context.Context, publicTaskID stri CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } - err := dao.TskAsyncTasks.Ctx(ctx). + err := dao.TskModelTasks.Ctx(ctx). Where("public_task_id", publicTaskID). Scan(&row) if err != nil { @@ -188,7 +188,7 @@ func (p *AsyncProvider) GetTaskByPublicIDAndUser(ctx context.Context, publicTask CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } - err := dao.TskAsyncTasks.Ctx(ctx). + err := dao.TskModelTasks.Ctx(ctx). Where("public_task_id", publicTaskID). Where("user_id", userID). Scan(&row) @@ -248,7 +248,7 @@ func (p *AsyncProvider) GetNonTerminalTasks(ctx context.Context, limit int) ([]* SubmitTime *time.Time `json:"submit_time"` CreatedAt time.Time `json:"created_at"` } - err := dao.TskAsyncTasks.Ctx(ctx). + err := dao.TskModelTasks.Ctx(ctx). Where("status NOT IN (?, ?)", "SUCCESS", "FAILURE"). Order("submit_time ASC"). Limit(limit). @@ -301,7 +301,7 @@ func (p *AsyncProvider) GetTimedOutTasks(ctx context.Context, cutoffUnix int64, PrivateData json.RawMessage `json:"private_data"` SubmitTime *time.Time `json:"submit_time"` } - err := dao.TskAsyncTasks.Ctx(ctx). + err := dao.TskModelTasks.Ctx(ctx). Where("status NOT IN (?, ?)", "SUCCESS", "FAILURE"). Where("submit_time < ?", cutoffTime). Order("submit_time ASC"). diff --git a/internal/model/do/chn_channel_keys.go b/internal/model/do/chn_channel_keys.go index 529e757..011dd40 100644 --- a/internal/model/do/chn_channel_keys.go +++ b/internal/model/do/chn_channel_keys.go @@ -20,7 +20,7 @@ type ChnChannelKeys struct { LastUsedAt *gtime.Time // 最后使用时间 LastError any // 最后一次错误信息 CreatedAt *gtime.Time // 创建时间 + UpdatedAt *gtime.Time // 更新时间 KeyType any // Key 类型:apikey(传统静态密钥)/ oauth(OAuth 令牌) TokenExpiresAt *gtime.Time // OAuth access_token 过期时间(仅 key_type=oauth 时有值) - UpdatedAt *gtime.Time // 更新时间 } diff --git a/internal/model/do/mdl_models.go b/internal/model/do/mdl_models.go index 0a54954..34083f2 100644 --- a/internal/model/do/mdl_models.go +++ b/internal/model/do/mdl_models.go @@ -21,10 +21,10 @@ type MdlModels struct { MaxOutputTokens any // 最大输出 token 数 Description any // 模型描述 Tags []string // 标签(如 reasoning、vision、function_calling) - Capabilities any // 模型能力特性(如 vision、function_calling、reasoning 等) CreatedAt *gtime.Time // 创建时间 UpdatedAt *gtime.Time // 更新时间 DeprecatedAt *gtime.Time // 标记弃用的时间(NULL表示未弃用) SunsetDate *gtime.Time // 计划下线日期(到达后返回410 Gone,NULL表示未设置) ReplacementModel any // 推荐替代模型名(NULL表示无替代) + Capabilities any // 模型能力特性(如 vision、function_calling、reasoning 等) } diff --git a/internal/model/do/ntf_messages.go b/internal/model/do/ntf_messages.go index fad9fdb..0ab3f96 100644 --- a/internal/model/do/ntf_messages.go +++ b/internal/model/do/ntf_messages.go @@ -22,6 +22,6 @@ type NtfMessages struct { IsRead any // 是否已读:0=未读, 1=已读 IsBroadcast any // 是否广播消息:0=个人消息, 1=广播消息 Metadata any // 附加元数据(JSONB,如关联资源ID、跳转链接等) - TargetRoles any // 目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色) CreatedAt *gtime.Time // 创建时间 + TargetRoles any // 目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色) } diff --git a/internal/model/do/plg_example_logs.go b/internal/model/do/plg_example_logs.go new file mode 100644 index 0000000..e846fff --- /dev/null +++ b/internal/model/do/plg_example_logs.go @@ -0,0 +1,20 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// PlgExampleLogs is the golang structure of table plg_example_logs for DAO operations like Where/Data. +type PlgExampleLogs struct { + g.Meta `orm:"table:plg_example_logs, do:true"` + Id any // + TenantId any // + Action any // + Detail any // + CreatedAt *gtime.Time // +} diff --git a/internal/model/do/tsk_async_tasks.go b/internal/model/do/tsk_model_tasks.go similarity index 88% rename from internal/model/do/tsk_async_tasks.go rename to internal/model/do/tsk_model_tasks.go index 2f23c43..d4f7055 100644 --- a/internal/model/do/tsk_async_tasks.go +++ b/internal/model/do/tsk_model_tasks.go @@ -9,9 +9,9 @@ import ( "github.com/gogf/gf/v2/os/gtime" ) -// TskAsyncTasks is the golang structure of table tsk_async_tasks for DAO operations like Where/Data. -type TskAsyncTasks struct { - g.Meta `orm:"table:tsk_async_tasks, do:true"` +// TskModelTasks is the golang structure of table tsk_model_tasks for DAO operations like Where/Data. +type TskModelTasks struct { + g.Meta `orm:"table:tsk_model_tasks, do:true"` Id any // PublicTaskId any // Platform any // diff --git a/internal/model/entity/chn_channel_keys.go b/internal/model/entity/chn_channel_keys.go index a913972..729b72d 100644 --- a/internal/model/entity/chn_channel_keys.go +++ b/internal/model/entity/chn_channel_keys.go @@ -18,7 +18,7 @@ type ChnChannelKeys struct { LastUsedAt *gtime.Time `json:"last_used_at" orm:"last_used_at" description:"最后使用时间"` // 最后使用时间 LastError string `json:"last_error" orm:"last_error" description:"最后一次错误信息"` // 最后一次错误信息 CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间 + UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间 KeyType string `json:"key_type" orm:"key_type" description:"Key 类型:apikey(传统静态密钥)/ oauth(OAuth 令牌)"` // Key 类型:apikey(传统静态密钥)/ oauth(OAuth 令牌) TokenExpiresAt *gtime.Time `json:"token_expires_at" orm:"token_expires_at" description:"OAuth access_token 过期时间(仅 key_type=oauth 时有值)"` // OAuth access_token 过期时间(仅 key_type=oauth 时有值) - UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间 } diff --git a/internal/model/entity/mdl_models.go b/internal/model/entity/mdl_models.go index 5fa76c0..96dfe97 100644 --- a/internal/model/entity/mdl_models.go +++ b/internal/model/entity/mdl_models.go @@ -19,10 +19,10 @@ type MdlModels struct { MaxOutputTokens int `json:"max_output_tokens" orm:"max_output_tokens" description:"最大输出 token 数"` // 最大输出 token 数 Description string `json:"description" orm:"description" description:"模型描述"` // 模型描述 Tags []string `json:"tags" orm:"tags" description:"标签(如 reasoning、vision、function_calling)"` // 标签(如 reasoning、vision、function_calling) - Capabilities string `json:"capabilities" orm:"capabilities" description:"模型能力特性(如 vision、function_calling、reasoning 等)"` // 模型能力特性(如 vision、function_calling、reasoning 等) CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间 UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间 DeprecatedAt *gtime.Time `json:"deprecated_at" orm:"deprecated_at" description:"标记弃用的时间(NULL表示未弃用)"` // 标记弃用的时间(NULL表示未弃用) SunsetDate *gtime.Time `json:"sunset_date" orm:"sunset_date" description:"计划下线日期(到达后返回410 Gone,NULL表示未设置)"` // 计划下线日期(到达后返回410 Gone,NULL表示未设置) ReplacementModel string `json:"replacement_model" orm:"replacement_model" description:"推荐替代模型名(NULL表示无替代)"` // 推荐替代模型名(NULL表示无替代) + Capabilities string `json:"capabilities" orm:"capabilities" description:"模型能力特性(如 vision、function_calling、reasoning 等)"` // 模型能力特性(如 vision、function_calling、reasoning 等) } diff --git a/internal/model/entity/ntf_messages.go b/internal/model/entity/ntf_messages.go index 267790e..e0a3751 100644 --- a/internal/model/entity/ntf_messages.go +++ b/internal/model/entity/ntf_messages.go @@ -20,6 +20,6 @@ type NtfMessages struct { IsRead int `json:"is_read" orm:"is_read" description:"是否已读:0=未读, 1=已读"` // 是否已读:0=未读, 1=已读 IsBroadcast int `json:"is_broadcast" orm:"is_broadcast" description:"是否广播消息:0=个人消息, 1=广播消息"` // 是否广播消息:0=个人消息, 1=广播消息 Metadata string `json:"metadata" orm:"metadata" description:"附加元数据(JSONB,如关联资源ID、跳转链接等)"` // 附加元数据(JSONB,如关联资源ID、跳转链接等) - TargetRoles string `json:"target_roles" orm:"target_roles" description:"目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色)"` // 目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色) CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间 + TargetRoles string `json:"target_roles" orm:"target_roles" description:"目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色)"` // 目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色) } diff --git a/internal/model/entity/plg_example_logs.go b/internal/model/entity/plg_example_logs.go new file mode 100644 index 0000000..937dc17 --- /dev/null +++ b/internal/model/entity/plg_example_logs.go @@ -0,0 +1,18 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// PlgExampleLogs is the golang structure for table plg_example_logs. +type PlgExampleLogs struct { + Id int64 `json:"id" orm:"id" description:""` // + TenantId int64 `json:"tenant_id" orm:"tenant_id" description:""` // + Action string `json:"action" orm:"action" description:""` // + Detail string `json:"detail" orm:"detail" description:""` // + CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:""` // +} diff --git a/internal/model/entity/tsk_async_tasks.go b/internal/model/entity/tsk_model_tasks.go similarity index 96% rename from internal/model/entity/tsk_async_tasks.go rename to internal/model/entity/tsk_model_tasks.go index 13f4343..2124e86 100644 --- a/internal/model/entity/tsk_async_tasks.go +++ b/internal/model/entity/tsk_model_tasks.go @@ -8,8 +8,8 @@ import ( "github.com/gogf/gf/v2/os/gtime" ) -// TskAsyncTasks is the golang structure for table tsk_async_tasks. -type TskAsyncTasks struct { +// TskModelTasks is the golang structure for table tsk_model_tasks. +type TskModelTasks struct { Id int64 `json:"id" orm:"id" description:""` // PublicTaskId string `json:"public_task_id" orm:"public_task_id" description:""` // Platform string `json:"platform" orm:"platform" description:""` // diff --git a/internal/service/admin.go b/internal/service/admin.go index 2a8457e..9678c97 100644 --- a/internal/service/admin.go +++ b/internal/service/admin.go @@ -337,14 +337,12 @@ type ( GetSettings(ctx context.Context, req *v1.AdminSettingsGetReq) (*v1.AdminSettingsGetRes, error) // UpdateSettings batch-updates settings for a given category. UpdateSettings(ctx context.Context, req *v1.AdminSettingsUpdateReq) (*v1.AdminSettingsUpdateRes, error) - // TaskList 任务列表 + // TaskList 大模型异步任务列表 TaskList(ctx context.Context, req *v1.TaskListReq) (*v1.TaskListRes, error) - // TaskDetail 任务详情 + // TaskDetail 大模型异步任务详情 TaskDetail(ctx context.Context, req *v1.TaskDetailReq) (*v1.TaskDetailRes, error) - // TaskCancel 取消任务 + // TaskCancel 取消大模型异步任务 TaskCancel(ctx context.Context, req *v1.TaskCancelReq) (*v1.TaskCancelRes, error) - // TaskRetry 重试失败任务 - TaskRetry(ctx context.Context, req *v1.TaskRetryReq) (*v1.TaskRetryRes, error) // TenantSelect returns a lightweight paginated tenant list for dropdown selectors. TenantSelect(ctx context.Context, req *v1.TenantSelectReq) (*v1.TenantSelectRes, error) // CreateTenant creates a new tenant with its owner user and wallet. diff --git a/migrations/000002_add_tenant_model_custom_pricing.sql b/migrations/000002_add_tenant_model_custom_pricing.sql deleted file mode 100644 index 85f9546..0000000 --- a/migrations/000002_add_tenant_model_custom_pricing.sql +++ /dev/null @@ -1,22 +0,0 @@ --- +goose Up --- 租户模型分配增加自定义缓存定价覆盖字段和阶梯定价支持 - --- 缓存定价覆盖 -ALTER TABLE mdl_tenant_models - ADD COLUMN IF NOT EXISTS custom_cache_read_price NUMERIC(20,10), - ADD COLUMN IF NOT EXISTS custom_cache_creation_price NUMERIC(20,10); - -COMMENT ON COLUMN mdl_tenant_models.custom_cache_read_price IS '自定义缓存读取价格($/1M token),NULL 表示使用基础定价'; -COMMENT ON COLUMN mdl_tenant_models.custom_cache_creation_price IS '自定义缓存创建价格($/1M token),NULL 表示使用基础定价'; - --- 阶梯定价 -ALTER TABLE mdl_tenant_models - ADD COLUMN IF NOT EXISTS custom_pricing_tiers JSONB; - -COMMENT ON COLUMN mdl_tenant_models.custom_pricing_tiers IS '自定义阶梯定价(JSONB 数组),格式: [{"min_tokens":0,"max_tokens":100000,"input_price":0.5,"output_price":1.5,"cache_read_price":0.1,"cache_creation_price":0.2}]'; - --- +goose Down -ALTER TABLE mdl_tenant_models - DROP COLUMN IF EXISTS custom_cache_read_price, - DROP COLUMN IF EXISTS custom_cache_creation_price, - DROP COLUMN IF EXISTS custom_pricing_tiers; diff --git a/migrations/000002_tenant_model_pricing_and_task_table_rename.sql b/migrations/000002_tenant_model_pricing_and_task_table_rename.sql new file mode 100644 index 0000000..33bd714 --- /dev/null +++ b/migrations/000002_tenant_model_pricing_and_task_table_rename.sql @@ -0,0 +1,97 @@ +-- +goose Up +-- 租户模型自定义定价 + 异步任务表重命名 + +-- 1. 租户模型分配增加自定义缓存定价覆盖字段和阶梯定价支持 +ALTER TABLE mdl_tenant_models + ADD COLUMN IF NOT EXISTS custom_cache_read_price NUMERIC(20,10), + ADD COLUMN IF NOT EXISTS custom_cache_creation_price NUMERIC(20,10); + +COMMENT ON COLUMN mdl_tenant_models.custom_cache_read_price IS '自定义缓存读取价格($/1M token),NULL 表示使用基础定价'; +COMMENT ON COLUMN mdl_tenant_models.custom_cache_creation_price IS '自定义缓存创建价格($/1M token),NULL 表示使用基础定价'; + +ALTER TABLE mdl_tenant_models + ADD COLUMN IF NOT EXISTS custom_pricing_tiers JSONB; + +COMMENT ON COLUMN mdl_tenant_models.custom_pricing_tiers IS '自定义阶梯定价(JSONB 数组),格式: [{"min_tokens":0,"max_tokens":100000,"input_price":0.5,"output_price":1.5,"cache_read_price":0.1,"cache_creation_price":0.2}]'; + +-- 2. 将 tsk_async_tasks 重命名为 tsk_model_tasks,并添加表和字段注释 +ALTER TABLE tsk_async_tasks RENAME TO tsk_model_tasks; + +ALTER TABLE tsk_model_tasks RENAME CONSTRAINT uk_tsk_async_tasks_public_id TO uk_tsk_model_tasks_public_id; + +ALTER INDEX idx_tsk_async_tasks_active RENAME TO idx_tsk_model_tasks_active; +ALTER INDEX idx_tsk_async_tasks_status RENAME TO idx_tsk_model_tasks_status; +ALTER INDEX idx_tsk_async_tasks_submit_time RENAME TO idx_tsk_model_tasks_submit_time; +ALTER INDEX idx_tsk_async_tasks_user RENAME TO idx_tsk_model_tasks_user; + +COMMENT ON TABLE tsk_model_tasks IS '异步模型任务表,存储 AI 模型异步调用任务(如视频生成、图像生成、音乐生成等)的状态和结果'; +COMMENT ON COLUMN tsk_model_tasks.id IS '主键ID'; +COMMENT ON COLUMN tsk_model_tasks.public_task_id IS '对外公开的任务ID(task_xxxxx 格式),用于 API 响应和客户端查询'; +COMMENT ON COLUMN tsk_model_tasks.platform IS '任务所属平台:sora、kling、suno、midjourney、volcengine'; +COMMENT ON COLUMN tsk_model_tasks.action IS '任务动作类型。通用:generate;Suno:music、lyrics;'; +COMMENT ON COLUMN tsk_model_tasks.status IS '任务状态:NOT_START-未开始、SUBMITTED-已提交、IN_PROGRESS-进行中、SUCCESS-成功、FAILURE-失败'; +COMMENT ON COLUMN tsk_model_tasks.progress IS '任务进度,字符串格式百分比(如 0%、50%、100%)'; +COMMENT ON COLUMN tsk_model_tasks.fail_reason IS '任务失败原因文本'; +COMMENT ON COLUMN tsk_model_tasks.tenant_id IS '发起任务的租户ID'; +COMMENT ON COLUMN tsk_model_tasks.user_id IS '发起任务的租户用户ID'; +COMMENT ON COLUMN tsk_model_tasks.api_key_id IS '调用方使用的 API Key ID'; +COMMENT ON COLUMN tsk_model_tasks.channel_id IS '转发请求的渠道ID'; +COMMENT ON COLUMN tsk_model_tasks.model_name IS '用户请求的模型名称(如 sora-1.0-turbo、midjourney 等)'; +COMMENT ON COLUMN tsk_model_tasks.upstream_model IS '上游供应商实际使用的模型名称,可能与请求模型不同'; +COMMENT ON COLUMN tsk_model_tasks.pre_deduct_amount IS '提交任务时的预扣金额(USD),任务完成后根据实际用量结算差额'; +COMMENT ON COLUMN tsk_model_tasks.actual_cost IS '任务实际消费金额(USD),成功时按此结算,失败时退还预扣金额'; +COMMENT ON COLUMN tsk_model_tasks.billing_settled IS '计费是否已结算(true 表示已完成预扣与实际金额的多退少补)'; +COMMENT ON COLUMN tsk_model_tasks.result_url IS '任务完成后的结果资源 URL(如生成的视频/图片/音频的下载地址)'; +COMMENT ON COLUMN tsk_model_tasks.data IS '上游供应商返回的完整响应数据(JSONB),可返回给用户查看'; +COMMENT ON COLUMN tsk_model_tasks.private_data IS '内部私有数据(JSONB),含 upstream_task_id 等敏感字段,用于轮询上游状态,不返回给用户'; +COMMENT ON COLUMN tsk_model_tasks.submit_time IS '任务提交到上游供应商的时间'; +COMMENT ON COLUMN tsk_model_tasks.start_time IS '上游供应商开始执行任务的时间'; +COMMENT ON COLUMN tsk_model_tasks.finish_time IS '任务完成(成功或失败)的时间'; +COMMENT ON COLUMN tsk_model_tasks.created_at IS '记录创建时间'; +COMMENT ON COLUMN tsk_model_tasks.updated_at IS '记录更新时间'; + +-- +goose Down +-- 移除注释 +COMMENT ON TABLE tsk_model_tasks IS NULL; +COMMENT ON COLUMN tsk_model_tasks.id IS NULL; +COMMENT ON COLUMN tsk_model_tasks.public_task_id IS NULL; +COMMENT ON COLUMN tsk_model_tasks.platform IS NULL; +COMMENT ON COLUMN tsk_model_tasks.action IS NULL; +COMMENT ON COLUMN tsk_model_tasks.status IS NULL; +COMMENT ON COLUMN tsk_model_tasks.progress IS NULL; +COMMENT ON COLUMN tsk_model_tasks.fail_reason IS NULL; +COMMENT ON COLUMN tsk_model_tasks.tenant_id IS NULL; +COMMENT ON COLUMN tsk_model_tasks.user_id IS NULL; +COMMENT ON COLUMN tsk_model_tasks.api_key_id IS NULL; +COMMENT ON COLUMN tsk_model_tasks.channel_id IS NULL; +COMMENT ON COLUMN tsk_model_tasks.model_name IS NULL; +COMMENT ON COLUMN tsk_model_tasks.upstream_model IS NULL; +COMMENT ON COLUMN tsk_model_tasks.pre_deduct_amount IS NULL; +COMMENT ON COLUMN tsk_model_tasks.actual_cost IS NULL; +COMMENT ON COLUMN tsk_model_tasks.billing_settled IS NULL; +COMMENT ON COLUMN tsk_model_tasks.result_url IS NULL; +COMMENT ON COLUMN tsk_model_tasks.data IS NULL; +COMMENT ON COLUMN tsk_model_tasks.private_data IS NULL; +COMMENT ON COLUMN tsk_model_tasks.submit_time IS NULL; +COMMENT ON COLUMN tsk_model_tasks.start_time IS NULL; +COMMENT ON COLUMN tsk_model_tasks.finish_time IS NULL; +COMMENT ON COLUMN tsk_model_tasks.created_at IS NULL; +COMMENT ON COLUMN tsk_model_tasks.updated_at IS NULL; + +-- 恢复索引名 +ALTER INDEX idx_tsk_model_tasks_user RENAME TO idx_tsk_async_tasks_user; +ALTER INDEX idx_tsk_model_tasks_submit_time RENAME TO idx_tsk_async_tasks_submit_time; +ALTER INDEX idx_tsk_model_tasks_status RENAME TO idx_tsk_async_tasks_status; +ALTER INDEX idx_tsk_model_tasks_active RENAME TO idx_tsk_async_tasks_active; + +-- 恢复约束名 +ALTER TABLE tsk_model_tasks RENAME CONSTRAINT uk_tsk_model_tasks_public_id TO uk_tsk_async_tasks_public_id; + +-- 恢复表名 +ALTER TABLE tsk_model_tasks RENAME TO tsk_async_tasks; + +-- 移除定价字段 +ALTER TABLE mdl_tenant_models + DROP COLUMN IF EXISTS custom_cache_read_price, + DROP COLUMN IF EXISTS custom_cache_creation_price, + DROP COLUMN IF EXISTS custom_pricing_tiers; diff --git a/web/admin/src/views/ai/TaskLogsPage.vue b/web/admin/src/views/ai/TaskLogsPage.vue index eb68380..6d8ece8 100644 --- a/web/admin/src/views/ai/TaskLogsPage.vue +++ b/web/admin/src/views/ai/TaskLogsPage.vue @@ -18,38 +18,66 @@ const pagination = reactive({ }) const filterStatus = ref(null) -const filterHandler = ref('') -const filterDateRange = ref([]) +const filterPlatform = ref(null) const statusOptions = [ { label: '全部状态', value: '' }, - { label: '待执行', value: 'pending' }, - { label: '执行中', value: 'running' }, - { label: '成功', value: 'succeeded' }, - { label: '失败', value: 'failed' }, - { label: '已取消', value: 'cancelled' }, + { label: '未开始', value: 'NOT_START' }, + { label: '已提交', value: 'SUBMITTED' }, + { label: '进行中', value: 'IN_PROGRESS' }, + { label: '成功', value: 'SUCCESS' }, + { label: '失败', value: 'FAILURE' }, +] + +const platformOptions = [ + { label: '全部平台', value: '' }, + { label: 'Sora', value: 'sora' }, + { label: 'Kling', value: 'kling' }, + { label: 'Midjourney', value: 'midjourney' }, + { label: 'Suno', value: 'suno' }, ] const statusTagColor: Record = { - pending: 'gray', - running: 'arcoblue', - succeeded: 'green', - failed: 'red', - cancelled: 'orangered', + NOT_START: 'gray', + SUBMITTED: 'arcoblue', + IN_PROGRESS: 'arcoblue', + SUCCESS: 'green', + FAILURE: 'red', } const statusTagLabel: Record = { - pending: '待执行', - running: '执行中', - succeeded: '成功', - failed: '失败', - cancelled: '已取消', + NOT_START: '未开始', + SUBMITTED: '已提交', + IN_PROGRESS: '进行中', + SUCCESS: '成功', + FAILURE: '失败', +} + +const platformLabel: Record = { + sora: 'Sora', + kling: 'Kling', + midjourney: 'Midjourney', + suno: 'Suno', } const columns: TableColumnData[] = [ { title: 'ID', dataIndex: 'id', width: 70 }, - { title: '任务名称', dataIndex: 'name', width: 180, ellipsis: true }, - { title: 'Handler', dataIndex: 'handler', width: 220, ellipsis: true }, + { title: '任务ID', dataIndex: 'public_task_id', width: 160, ellipsis: true, tooltip: true }, + { + title: '平台', + dataIndex: 'platform', + width: 110, + render({ record }) { + return platformLabel[record.platform] || record.platform + }, + }, + { + title: '动作', + dataIndex: 'action', + width: 120, + ellipsis: true, + tooltip: true, + }, { title: '状态', dataIndex: 'status', @@ -62,26 +90,43 @@ const columns: TableColumnData[] = [ }, }, { - title: '重试', - dataIndex: 'retry_count', + title: '进度', + dataIndex: 'progress', width: 80, + }, + { + title: '模型', + dataIndex: 'model_name', + width: 150, + ellipsis: true, + tooltip: true, + }, + { + title: '费用', + dataIndex: 'actual_cost', + width: 100, render({ record }) { - return `${record.retry_count}/${record.max_retries}` + if (record.billing_settled && record.actual_cost > 0) { + return `$${record.actual_cost.toFixed(4)}` + } + if (record.pre_deduct_amount > 0) { + return `$${record.pre_deduct_amount.toFixed(4)} (预扣)` + } + return '-' }, }, - { title: '开始时间', dataIndex: 'started_at', width: 170 }, - { title: '完成时间', dataIndex: 'finished_at', width: 170 }, - { title: '创建时间', dataIndex: 'created_at', width: 170 }, + { title: '提交时间', dataIndex: 'submit_time', width: 170 }, + { title: '完成时间', dataIndex: 'finish_time', width: 170 }, { title: '操作', dataIndex: 'actions', - width: 160, + width: 120, fixed: 'right', render({ record }) { const buttons: any[] = [ h(Button, { size: 'small', type: 'text', onClick: () => openDetail(record) }, () => '详情'), ] - if (record.status === 'pending' || record.status === 'running') { + if (record.status === 'NOT_START' || record.status === 'SUBMITTED' || record.status === 'IN_PROGRESS') { buttons.push( h(Popconfirm, { content: '确定取消该任务?', @@ -89,14 +134,6 @@ const columns: TableColumnData[] = [ }, () => h(Button, { size: 'small', type: 'text', status: 'warning' }, () => '取消')), ) } - if (record.status === 'failed') { - buttons.push( - h(Popconfirm, { - content: '确定重试该任务?', - onOk: () => retryTask(record), - }, () => h(Button, { size: 'small', type: 'text', status: 'success' }, () => '重试')), - ) - } return h(Space, { size: 0 }, () => buttons) }, }, @@ -110,11 +147,7 @@ async function fetchData() { page_size: pagination.pageSize, } if (filterStatus.value) params.status = filterStatus.value - if (filterHandler.value) params.handler = filterHandler.value - if (filterDateRange.value.length === 2) { - params.start_date = filterDateRange.value[0] - params.end_date = filterDateRange.value[1] - } + if (filterPlatform.value) params.platform = filterPlatform.value const res: any = await request.get('/admin/tasks', { params }) const raw = res.data?.data data.value = (raw?.list || []).filter(Boolean) @@ -134,8 +167,7 @@ function handleFilter() { function resetFilter() { filterStatus.value = null - filterHandler.value = '' - filterDateRange.value = [] + filterPlatform.value = null pagination.current = 1 fetchData() } @@ -150,36 +182,20 @@ async function cancelTask(row: any) { } } -async function retryTask(row: any) { - try { - await request.post(`/admin/tasks/${row.id}/retry`) - Message.success('任务已重新排队') - fetchData() - } catch { - // error handled by interceptor - } -} - // === Detail Drawer === const showDetail = ref(false) const detailLoading = ref(false) const detailData = ref(null) -const detailExecutions = ref([]) -const detailLogs = ref([]) async function openDetail(row: any) { detailData.value = row - detailExecutions.value = [] - detailLogs.value = [] showDetail.value = true detailLoading.value = true try { const res: any = await request.get(`/admin/tasks/${row.id}`) const raw = res.data?.data - if (raw) { - detailData.value = raw.task || row - detailExecutions.value = (raw.executions || []).filter(Boolean) - detailLogs.value = (raw.recent_logs || []).filter(Boolean) + if (raw?.task) { + detailData.value = raw.task } } catch { // error handled by interceptor @@ -188,40 +204,6 @@ async function openDetail(row: any) { } } -const logLevelColor: Record = { - info: 'arcoblue', - warn: 'orangered', - error: 'red', -} - -const execColumns: TableColumnData[] = [ - { title: '#', dataIndex: 'execution_order', width: 50 }, - { - title: '状态', - dataIndex: 'status', - width: 80, - render({ record }) { - return h(Tag, { - color: statusTagColor[record.status], - size: 'small', - }, () => record.status) - }, - }, - { title: '开始时间', dataIndex: 'started_at', width: 170 }, - { title: '完成时间', dataIndex: 'finished_at', width: 170 }, - { - title: '耗时', - dataIndex: 'duration_ms', - width: 100, - render({ record }) { - if (!record.duration_ms) return '-' - if (record.duration_ms < 1000) return `${record.duration_ms}ms` - return `${(record.duration_ms / 1000).toFixed(1)}s` - }, - }, - { title: '错误信息', dataIndex: 'error_message', ellipsis: true }, -] - onMounted(() => { fetchData() }) @@ -229,7 +211,7 @@ onMounted(() => {