Files
ttstd-infinite-canvas/backend/internal/app/provider_protocol.go
T

221 lines
8.4 KiB
Go

package app
// 声明式协议插件宿主与接口类型校验。
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/google/uuid"
"infinite-canvas/backend/internal/protocol"
)
// runDeclarativeProtocolTask 是 JSON manifest 插件的宿主运行时。
// manifest 只能描述请求/响应映射;凭证、出站安全策略、轮询和结果下载必须由宿主统一掌握,
// 这样插件不能绕过服务端的鉴权、超时和 SSRF 防护边界。
func runDeclarativeProtocolTask(ctx context.Context, input canvasGenerationInput) (map[string]interface{}, error) {
adapter, ok := declarativeProtocolAdapterForContext(ctx, input.Config.InterfaceType)
if !ok {
return nil, fmt.Errorf("接口类型 %s 未安装声明式适配器", input.Config.InterfaceType)
}
return runProtocolAdapterTask(ctx, input, adapter)
}
func runProtocolAdapterTask(ctx context.Context, input canvasGenerationInput, adapter protocol.Adapter) (map[string]interface{}, error) {
return runProtocolAdapterTaskWithPolicy(ctx, input, adapter, declarativeProtocolPollPolicy(input.Mode))
}
func declarativeProtocolPollPolicy(mode string) videoPollPolicy {
if mode == "video" {
return defaultVideoPollPolicy()
}
return videoPollPolicy{
Interval: 2500 * time.Millisecond,
MaxNotFoundMisses: 1,
MaxDownloadTries: 3,
Sleep: sleepContext,
}
}
func runProtocolAdapterTaskWithPolicy(ctx context.Context, input canvasGenerationInput, adapter protocol.Adapter, policy videoPollPolicy) (map[string]interface{}, error) {
// create、poll、download 是同一个外部任务的三个阶段:任一阶段失败都向上返回真实错误,
// 不把“已提交但结果未知”伪装成成功,也不把下载失败降级成空结果。
request := protocolRequestFromInput(input)
taskID := resumedProviderRequestID(ctx)
var created protocol.CreateResult
if taskID == "" {
// 幂等键只存在于宿主请求元数据中,声明式插件可以把它映射到 Header,
// 但不能把宿主控制字段泄漏到供应商 JSON body。恢复已有 taskID 时不会进入 create 分支。
key, _ := ctx.Value(providerSubmissionKeyContext{}).(string)
if key == "" {
key = uuid.NewString()
}
request.Extra["idempotencyKey"] = key
spec, err := adapter.BuildCreate(ctx, protocol.RequestContext{BaseURL: input.Config.BaseURL, Request: request})
if err != nil {
return nil, err
}
body, streamedResult, err := executeProtocolCreateRequest(withProviderRequestKind(ctx, "create"), input, spec)
if err != nil {
return nil, err
}
if streamedResult != nil {
created = protocol.CreateResult{Status: protocol.StatusSucceeded, Result: streamedResult}
} else {
created, err = adapter.ParseCreate(ctx, body)
}
if err != nil {
return nil, err
}
taskID = created.TaskID
if taskID == "" {
extracted, extractErr := extractProviderTaskID(body)
if extractErr != nil {
return nil, extractErr
}
taskID = extracted
}
if created.Status == protocol.StatusFailed || created.Status == protocol.StatusCancelled {
return nil, protocolResultError(created.Message, taskID)
}
if created.Status == protocol.StatusSucceeded {
return finishProtocolAdapterResult(ctx, input, adapter, request, taskID, created.Result, policy)
}
if taskID == "" {
return nil, errors.New("声明式协议创建请求没有返回任务 ID")
}
}
return runVideoPollLoop(ctx, taskID, policy, func(ctx context.Context) (videoPollOutcome, error) {
spec, err := adapter.BuildPoll(ctx, protocol.PollContext{BaseURL: input.Config.BaseURL, Model: request.Model, Request: request, TaskID: taskID})
if err != nil {
return videoPollOutcome{}, err
}
body, err := executeProtocolRequest(withProviderRequestKind(ctx, "poll"), input.Config, spec)
if err != nil {
return videoPollOutcome{}, err
}
state, err := adapter.ParsePoll(ctx, protocol.PollContext{BaseURL: input.Config.BaseURL, Model: request.Model, Request: request, TaskID: taskID}, body)
if err != nil {
return videoPollOutcome{}, err
}
if state.TaskID != "" {
taskID = state.TaskID
}
switch state.Status {
case protocol.StatusSucceeded:
result, err := finishProtocolAdapterResult(ctx, input, adapter, request, taskID, state.Result, policy)
return videoPollOutcome{Done: err == nil, Result: result}, err
case protocol.StatusFailed, protocol.StatusCancelled:
return videoPollOutcome{}, protocolResultError(state.Message, taskID)
}
return videoPollOutcome{}, nil
})
}
func extractProviderTaskID(body []byte) (string, error) {
var payload map[string]interface{}
if json.Unmarshal(body, &payload) != nil {
return "", nil
}
id, err := firstJSONString(payload, "id", "task_id", "taskId", "request_id", "name")
if id != "" {
return id, nil
}
if data, ok := payload["data"].(map[string]interface{}); ok {
nested, nestedErr := firstJSONString(data, "id", "task_id", "taskId", "request_id")
if nested != "" {
return nested, nil
}
if nestedErr != nil {
return "", nestedErr
}
}
return "", err
}
// queryProtocolAdapterVideoTask 只读取一次已有的声明式 Provider 任务。
// 人工恢复走此路径,因此检查本地失败任务时绝不会创建第二个计费任务。
func queryProtocolAdapterVideoTask(ctx context.Context, input canvasGenerationInput, adapter protocol.Adapter, taskID string) (map[string]interface{}, string, error) {
request := protocolRequestFromInput(input)
pollContext := protocol.PollContext{BaseURL: input.Config.BaseURL, Model: request.Model, Request: request, TaskID: taskID}
spec, err := adapter.BuildPoll(ctx, pollContext)
if err != nil {
return nil, "", err
}
body, err := executeProtocolRequest(withProviderRequestKind(ctx, "poll"), input.Config, spec)
if err != nil {
return nil, "", err
}
state, err := adapter.ParsePoll(ctx, pollContext, body)
if err != nil {
return nil, "", err
}
providerStatus := string(state.Status)
switch state.Status {
case protocol.StatusSucceeded:
result, err := finishProtocolAdapterResult(ctx, input, adapter, request, taskID, state.Result, defaultVideoPollPolicy())
return result, providerStatus, err
case protocol.StatusFailed, protocol.StatusCancelled:
return nil, providerStatus, protocolResultError(state.Message, taskID)
case protocol.StatusPending, protocol.StatusProcessing:
return nil, providerStatus, nil
default:
return nil, providerStatus, fmt.Errorf("声明式协议任务 %s 返回未知状态:%s", taskID, providerStatus)
}
}
func validateGenerationInterface(mode string, interfaceType string) error {
// Standalone callers (including legacy tests and migration tools) may not
// have a Service/plugin runtime. Use the shipped declarative catalog first;
// fall back to host builtins only when the package catalog is unavailable.
registry := loadOfficialFallbackRegistry()
if registry == nil {
registry = protocol.Builtins()
} else if _, ok := registry.Resolve(strings.TrimSpace(interfaceType)); !ok {
// A deployment may ship only a subset of the optional plugin catalog.
// Preserve the host-backed builtins for standalone validation instead of
// reporting a false "not installed" error for those interfaces.
if _, builtinOK := protocol.Builtins().Resolve(strings.TrimSpace(interfaceType)); builtinOK {
registry = protocol.Builtins()
}
}
return validateGenerationInterfaceWithRegistry(registry, mode, interfaceType)
}
func (s *Service) validateGenerationInterface(mode string, interfaceType string) error {
return validateGenerationInterfaceWithRegistry(s.protocolRegistry(), mode, interfaceType)
}
func validateGenerationInterfaceWithRegistry(registry *protocol.Registry, mode string, interfaceType string) error {
interfaceType = strings.TrimSpace(interfaceType)
if interfaceType == "" {
return nil
}
adapter, ok := registry.Resolve(interfaceType)
if !ok {
return fmt.Errorf("接口类型 %s 未安装", interfaceType)
}
metadata := adapter.Metadata()
if !metadata.Enabled || metadata.UnavailableReason != "" {
return fmt.Errorf("接口类型 %s 当前不可用:%s", interfaceType, metadata.UnavailableReason)
}
if mode != "" && !protocolCapabilityMatches(metadata, protocol.Capability(mode)) {
return fmt.Errorf("接口类型 %s 不支持%s生成", interfaceType, mode)
}
return nil
}
func protocolCapabilityMatches(metadata protocol.Metadata, capability protocol.Capability) bool {
for _, item := range metadata.Categories {
if item == capability {
return true
}
}
return false
}