소스 검색

feat(volcengine): add asset library support for official DoubaoVideo channels

Register a DoubaoVideo asset adapter that forwards the Ark-compatible
asset API (POST /api/v1/volcengine/asset?Action=xx) to the official
/openApi/portrait endpoint with a Volcengine V4 HMAC-SHA256 signature
(canonical request over content-type/host/x-content-sha256/x-date,
date/cn-beijing/ark/request scope). The AK/SK pair and optional
project code are stored in channel_asset_credentials (pool_id holds
the project code), so asset credentials stay separate from the video
Bearer key; the channel form now accepts them for type 54.

DoubaoVideo joins the seedance asset family so a user's asset upload
channel and video task channel stay the same (asset:// references
pass through to the upstream). Both resolution chains (asset proxy
resolver and video asset family matcher) now require the asset
credential to be configured before a type-54 channel can serve,
bind, or be auto-matched for assets, falling through to compatible
channels otherwise. The /api/v3 native-path whitelist simplifies
back to the family list now that 54 is a member.

Verified end-to-end against the real upstream: CreateAssetGroup,
CreateAsset (real video upload, Processing -> Active moderation),
ListAssets/GetAsset, and a video generation task referencing the
asset via asset://video reference (succeeded, 432900 tokens).

Co-Authored-By: ZCode <noreply@anthropic.com>
master
fengsilin 6 일 전
부모
커밋
b9edaa1ad3
11개의 변경된 파일363개의 추가작업 그리고 28개의 파일을 삭제
  1. +12
    -2
      controller/channel.go
  2. +7
    -4
      controller/channel_test_tianyiyun_test.go
  3. +1
    -2
      controller/relay.go
  4. +1
    -5
      middleware/distributor.go
  5. +7
    -4
      middleware/doubao_asset_binding_test.go
  6. +1
    -0
      service/asset_adapter.go
  7. +190
    -0
      service/asset_doubao.go
  8. +93
    -0
      service/asset_doubao_test.go
  9. +21
    -2
      service/asset_resolver.go
  10. +10
    -0
      service/video_asset_channel.go
  11. +20
    -9
      web/src/components/table/channels/modals/EditChannelModal.jsx

+ 12
- 2
controller/channel.go 파일 보기

@@ -726,13 +726,23 @@ type ChannelAssetCredentialInput struct {
}

func channelAssetCredentialFromInput(channelType int, input *ChannelAssetCredentialInput) (*model.ChannelAssetCredential, error) {
if channelType != constant.ChannelTypeChinaMobileSeedance || input == nil {
if input == nil {
return nil, nil
}
var label string
switch channelType {
case constant.ChannelTypeChinaMobileSeedance:
label = "移动云素材"
case constant.ChannelTypeDoubaoVideo:
label = "火山素材"
default:
// Other channel types do not use separate asset credentials.
return nil, nil
}
ak := strings.TrimSpace(input.AccessKey)
sk := strings.TrimSpace(input.SecretKey)
if ak == "" || sk == "" {
return nil, errors.New("移动云素材 AccessKey 和 SecretKey 必须同时填写")
return nil, errors.New(label + " AccessKey 和 SecretKey 必须同时填写")
}
return &model.ChannelAssetCredential{AccessKey: ak, SecretKey: sk, PoolID: strings.TrimSpace(input.PoolID)}, nil
}


+ 7
- 4
controller/channel_test_tianyiyun_test.go 파일 보기

@@ -29,11 +29,14 @@ func TestRequiredTaskChannelTypeForTianyiYunSeedanceModelUsesAllowedFamily(t *te
c := newControllerJSONContext(t, "/api/v3/contents/generations/tasks", `{"model":"Doubao-Seedance-2.0"}`)

require.Equal(t, 0, requiredTaskChannelTypeForRequest(c))
// The native /api/v3 path also accepts official Volcengine (DoubaoVideo)
// channels alongside the seedance asset family.
require.ElementsMatch(t,
append(service.VideoAssetChannelTypesForFamily(service.VideoAssetFamilySeedance),
constant.ChannelTypeDoubaoVideo),
service.VideoAssetChannelTypesForFamily(service.VideoAssetFamilySeedance),
allowedTaskChannelTypesForRequest(c),
)
// The seedance family must include the official Volcengine channel so the
// native /api/v3 path can route to it.
require.Contains(t,
allowedTaskChannelTypesForRequest(c),
constant.ChannelTypeDoubaoVideo,
)
}

+ 1
- 2
controller/relay.go 파일 보기

@@ -368,8 +368,7 @@ func requiredTaskChannelTypeForRequest(c *gin.Context) int {
func allowedTaskChannelTypesForRequest(c *gin.Context) []int {
if strings.HasPrefix(c.Request.URL.Path, "/api/v3/contents/generations/tasks") {
// Keep in sync with allowedChannelTypesForRequest in middleware/distributor.go
return append(service.VideoAssetChannelTypesForFamily(service.VideoAssetFamilySeedance),
constant.ChannelTypeDoubaoVideo)
return service.VideoAssetChannelTypesForFamily(service.VideoAssetFamilySeedance)
}
if isKlingAipingNativePath(c.Request.URL.Path) {
return service.VideoAssetChannelTypesForFamily(service.VideoAssetFamilyKling)


+ 1
- 5
middleware/distributor.go 파일 보기

@@ -345,11 +345,7 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {

func allowedChannelTypesForRequest(c *gin.Context, modelName string) []int {
if strings.HasPrefix(c.Request.URL.Path, "/api/v3/contents/generations/tasks") {
// DoubaoVideo channels (official Volcengine Ark) also serve the native
// /api/v3 path; they are appended here instead of joining the seedance
// asset family so they stay out of the user asset binding system.
return append(service.VideoAssetChannelTypesForFamily(service.VideoAssetFamilySeedance),
constant.ChannelTypeDoubaoVideo)
return service.VideoAssetChannelTypesForFamily(service.VideoAssetFamilySeedance)
}
return nil
}


+ 7
- 4
middleware/doubao_asset_binding_test.go 파일 보기

@@ -212,13 +212,16 @@ func TestSeedanceTasksUseAllowedFamilyTypesInsteadOfModelPrefix(t *testing.T) {
c.Request = httptest.NewRequest(http.MethodPost, "/api/v3/contents/generations/tasks", strings.NewReader(`{}`))

require.Equal(t, 0, requiredChannelTypeForRequest(c, "Doubao-Seedance-2.0"))
// The native /api/v3 path also accepts official Volcengine (DoubaoVideo)
// channels alongside the seedance asset family.
require.ElementsMatch(t,
append(service.VideoAssetChannelTypesForFamily(service.VideoAssetFamilySeedance),
constant.ChannelTypeDoubaoVideo),
service.VideoAssetChannelTypesForFamily(service.VideoAssetFamilySeedance),
allowedChannelTypesForRequest(c, "Doubao-Seedance-2.0"),
)
// The seedance family must include the official Volcengine channel so the
// native /api/v3 path can route to it.
require.Contains(t,
allowedChannelTypesForRequest(c, "Doubao-Seedance-2.0"),
constant.ChannelTypeDoubaoVideo,
)
}

func TestShouldPersistDoubaoVideoBindingRequiresVideoSubmitRelayMode(t *testing.T) {


+ 1
- 0
service/asset_adapter.go 파일 보기

@@ -60,6 +60,7 @@ func init() {

func registerDefaultAssetAdapters() {
assetAdapters[constant.ChannelTypeChinaMobileSeedance] = NewChinaMobileAssetAdapter()
assetAdapters[constant.ChannelTypeDoubaoVideo] = NewDoubaoVideoAssetAdapter()
assetAdapters[constant.ChannelTypeDoubaoVideoCompatibleAiping] = NewCompatibleAssetAdapter("aiping_asset", []AssetOperation{
AssetOperationAssetCreate,
AssetOperationAssetList,


+ 190
- 0
service/asset_doubao.go 파일 보기

@@ -0,0 +1,190 @@
package service

import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/setting/system_setting"
)

// DoubaoVideoAssetAdapter serves the Ark-compatible asset API on official
// Volcengine (DoubaoVideo) channels. Unlike the aiping adapter (Bearer key),
// the official endpoint authenticates every request with a Volcengine V4
// HMAC-SHA256 signature built from an AccessKey/SecretKey pair stored in
// channel_asset_credentials (PoolID holds the optional project code).
type DoubaoVideoAssetAdapter struct {
operation map[AssetOperation]struct{}
}

func NewDoubaoVideoAssetAdapter() AssetAdapter {
operations := []AssetOperation{
AssetOperationAssetCreate,
AssetOperationAssetList,
AssetOperationAssetGet,
AssetOperationAssetUpdate,
AssetOperationAssetDelete,
AssetOperationAssetGroupCreate,
AssetOperationAssetGroupList,
AssetOperationAssetGroupGet,
AssetOperationAssetGroupUpdate,
AssetOperationAssetGroupDelete,
}
supported := make(map[AssetOperation]struct{}, len(operations))
for _, op := range operations {
supported[op] = struct{}{}
}
return &DoubaoVideoAssetAdapter{operation: supported}
}

func (a *DoubaoVideoAssetAdapter) Name() string {
return "doubao_video_asset"
}

func (a *DoubaoVideoAssetAdapter) Supports(operation AssetOperation) bool {
_, ok := a.operation[operation]
return ok
}

func (a *DoubaoVideoAssetAdapter) DoAssetRequest(ctx context.Context, channel *model.Channel, req AssetRequest) (*AssetUpstreamResponse, *AssetError) {
if !a.Supports(req.Action.Operation) {
return nil, newAssetError(AssetErrorOperationNotSupported, fmt.Sprintf("asset operation %s is not supported", req.Action.Operation), http.StatusBadRequest)
}
credential, err := model.GetChannelAssetCredential(channel.Id)
if err != nil {
return nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
}
if credential == nil || strings.TrimSpace(credential.AccessKey) == "" || strings.TrimSpace(credential.SecretKey) == "" {
return nil, newAssetError(AssetErrorServer, "Volcengine asset AccessKey and SecretKey are required (configure the channel asset credential)", http.StatusBadRequest)
}

upstreamURL, rawQuery, err := buildDoubaoVideoAssetURL(channel, req.Action.Action, req.Version)
if err != nil {
return nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
}
fetchSetting := system_setting.GetFetchSetting()
if err := common.ValidateURLWithFetchSetting(upstreamURL, fetchSetting.EnableSSRFProtection, fetchSetting.AllowPrivateIp, fetchSetting.DomainFilterMode, fetchSetting.IpFilterMode, fetchSetting.DomainList, fetchSetting.IpList, fetchSetting.AllowedPorts, fetchSetting.ApplyIPFilterForDomain); err != nil {
return nil, newAssetError(AssetErrorServer, fmt.Sprintf("request blocked: %v", err), http.StatusForbidden)
}

httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, upstreamURL, bytes.NewReader(req.RawBody))
if err != nil {
return nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
}
signDoubaoVideoAssetRequest(httpReq, credential.AccessKey, credential.SecretKey, req.RawBody, rawQuery)
if projectCode := strings.TrimSpace(credential.PoolID); projectCode != "" {
httpReq.Header.Set("X-Project-Code", projectCode)
}

client, err := GetHttpClientWithProxy(channel.GetSetting().Proxy)
if err != nil {
return nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
}
if client == nil {
client = http.DefaultClient
}
resp, err := client.Do(httpReq)
if err != nil {
return nil, newAssetError(AssetErrorUpstream, err.Error(), http.StatusBadGateway)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, newAssetError(AssetErrorUpstream, err.Error(), http.StatusBadGateway)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, newAssetError(AssetErrorUpstream, string(data), http.StatusBadGateway)
}
return &AssetUpstreamResponse{StatusCode: resp.StatusCode, Header: resp.Header, Body: data}, nil
}

func buildDoubaoVideoAssetURL(channel *model.Channel, action string, version string) (string, string, error) {
baseURL := strings.TrimSpace(channel.GetBaseURL())
if baseURL == "" {
baseURL = "https://ark.cn-beijing.volces.com"
}
u, err := url.Parse(baseURL)
if err != nil {
return "", "", err
}
u.Path = strings.TrimRight(u.Path, "/") + "/openApi/portrait"
if strings.TrimSpace(version) == "" {
version = "2024-01-01"
}
// Action < Version lexicographically, so this raw query is already the
// sorted canonical form required by the V4 signature.
rawQuery := "Action=" + url.QueryEscape(action) + "&Version=" + url.QueryEscape(version)
u.RawQuery = rawQuery
return u.String(), rawQuery, nil
}

// signDoubaoVideoAssetRequest applies the Volcengine V4 HMAC-SHA256 signature
// to the request: canonical request over (method, path, sorted query,
// content-type/host/x-content-sha256/x-date headers, body hash) -> string to
// sign with the {date}/{region}/ark/request scope -> chained HMAC keys.
func signDoubaoVideoAssetRequest(req *http.Request, accessKey string, secretKey string, body []byte, rawQuery string) {
now := time.Now().UTC()
xDate := now.Format("20060102T150405Z")
shortDate := now.Format("20060102")
payloadHash := sha256.Sum256(body)
payloadHashHex := hex.EncodeToString(payloadHash[:])

canonicalHeaders := fmt.Sprintf(
"content-type:application/json\nhost:%s\nx-content-sha256:%s\nx-date:%s\n",
req.URL.Host, payloadHashHex, xDate,
)
signedHeaders := "content-type;host;x-content-sha256;x-date"
canonicalRequest := strings.Join([]string{
req.Method,
req.URL.EscapedPath(),
rawQuery,
canonicalHeaders,
signedHeaders,
payloadHashHex,
}, "\n")

scope := shortDate + "/cn-beijing/ark/request"
canonicalHash := sha256.Sum256([]byte(canonicalRequest))
stringToSign := strings.Join([]string{
"HMAC-SHA256",
xDate,
scope,
hex.EncodeToString(canonicalHash[:]),
}, "\n")

signingKey := volcengineSigningKey(secretKey, shortDate)
signature := hmac.New(sha256.New, signingKey)
signature.Write([]byte(stringToSign))

req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("X-Date", xDate)
req.Header.Set("X-Content-Sha256", payloadHashHex)
req.Header.Set("Authorization", fmt.Sprintf(
"HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s",
accessKey, scope, signedHeaders, hex.EncodeToString(signature.Sum(nil)),
))
}

func volcengineSigningKey(secretKey string, shortDate string) []byte {
key := hmacSHA256([]byte(secretKey), shortDate)
key = hmacSHA256(key, "cn-beijing")
key = hmacSHA256(key, "ark")
return hmacSHA256(key, "request")
}

func hmacSHA256(key []byte, message string) []byte {
mac := hmac.New(sha256.New, key)
mac.Write([]byte(message))
return mac.Sum(nil)
}

+ 93
- 0
service/asset_doubao_test.go 파일 보기

@@ -0,0 +1,93 @@
package service

import (
"net/http"
"net/url"
"strings"
"testing"

"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/model"
"github.com/stretchr/testify/require"
)

func TestDoubaoVideoAssetAdapter_SupportsAllOperations(t *testing.T) {
adapter := NewDoubaoVideoAssetAdapter()
require.Equal(t, "doubao_video_asset", adapter.Name())
for _, op := range []AssetOperation{
AssetOperationAssetCreate,
AssetOperationAssetList,
AssetOperationAssetGet,
AssetOperationAssetUpdate,
AssetOperationAssetDelete,
AssetOperationAssetGroupCreate,
AssetOperationAssetGroupList,
AssetOperationAssetGroupGet,
AssetOperationAssetGroupUpdate,
AssetOperationAssetGroupDelete,
} {
require.True(t, adapter.Supports(op), "operation %s should be supported", op)
}
}

func TestBuildDoubaoVideoAssetURL(t *testing.T) {
baseURL := "http://14.103.147.238:19220"
channel := &model.Channel{Type: constant.ChannelTypeDoubaoVideo, BaseURL: &baseURL}
rawURL, rawQuery, err := buildDoubaoVideoAssetURL(channel, "ListAssets", "")
require.NoError(t, err)
require.Equal(t, "Action=ListAssets&Version=2024-01-01", rawQuery)
parsed, err := url.Parse(rawURL)
require.NoError(t, err)
require.Equal(t, "/openApi/portrait", parsed.Path)
require.Equal(t, "Action=ListAssets&Version=2024-01-01", parsed.RawQuery)
}

func TestBuildDoubaoVideoAssetURL_DefaultBaseAndTrailingSlash(t *testing.T) {
baseURL := "https://ark.cn-beijing.volces.com/"
channel := &model.Channel{Type: constant.ChannelTypeDoubaoVideo, BaseURL: &baseURL}
rawURL, _, err := buildDoubaoVideoAssetURL(channel, "CreateAsset", "2024-01-01")
require.NoError(t, err)
require.Equal(t, "https://ark.cn-beijing.volces.com/openApi/portrait?Action=CreateAsset&Version=2024-01-01", rawURL)
}

func TestSignDoubaoVideoAssetRequest_HeaderShape(t *testing.T) {
req, err := http.NewRequest(http.MethodPost, "http://14.103.147.238:19220/openApi/portrait?Action=ListAssets&Version=2024-01-01", nil)
require.NoError(t, err)
signDoubaoVideoAssetRequest(req, "AK-test", "SK-test", []byte(`{"Filter":{"GroupType":"AIGC"}}`), "Action=ListAssets&Version=2024-01-01")

auth := req.Header.Get("Authorization")
require.True(t, strings.HasPrefix(auth, "HMAC-SHA256 Credential=AK-test/"), "got %s", auth)
require.Contains(t, auth, "/cn-beijing/ark/request, SignedHeaders=content-type;host;x-content-sha256;x-date, Signature=")
require.NotEmpty(t, req.Header.Get("X-Date"))
require.Len(t, req.Header.Get("X-Date"), 16)
require.Equal(t, "application/json", req.Header.Get("Content-Type"))
// body hash header must be the hex sha256 of the raw body
require.Regexp(t, `^[0-9a-f]{64}$`, req.Header.Get("X-Content-Sha256"))
}

// The signature must change when any signed input changes (body, key, date).
func TestSignDoubaoVideoAssetRequest_DeterministicPerInputs(t *testing.T) {
build := func(body string) string {
req, _ := http.NewRequest(http.MethodPost, "http://h/openApi/portrait?Action=A&Version=2024-01-01", nil)
signDoubaoVideoAssetRequest(req, "AK", "SK", []byte(body), "Action=A&Version=2024-01-01")
return req.Header.Get("Authorization")
}
sig1 := build(`{"a":1}`)
sig2 := build(`{"a":1}`)
sig3 := build(`{"a":2}`)
require.Equal(t, sig1[strings.Index(sig1, "Signature="):], sig2[strings.Index(sig2, "Signature="):])
require.NotEqual(t, sig1[strings.Index(sig1, "Signature="):], sig3[strings.Index(sig3, "Signature="):])
}

func TestDoubaoVideoAssetAdapter_MissingCredential(t *testing.T) {
db := setupDoubaoAssetChannelDB(t)
require.NoError(t, db.AutoMigrate(&model.ChannelAssetCredential{}))
adapter := NewDoubaoVideoAssetAdapter()
channel := &model.Channel{Id: 999999, Type: constant.ChannelTypeDoubaoVideo}
_, assetErr := adapter.DoAssetRequest(t.Context(), channel, AssetRequest{
Action: AssetActionSpec{Action: "ListAssets", Operation: AssetOperationAssetList},
RawBody: []byte(`{}`),
})
require.NotNil(t, assetErr)
require.Contains(t, assetErr.Message, "AccessKey and SecretKey are required")
}

+ 21
- 2
service/asset_resolver.go 파일 보기

@@ -4,12 +4,31 @@ import (
"errors"
"fmt"
"net/http"
"strings"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/model"
"gorm.io/gorm"
)

// assetCredentialConfigured guards channel types whose asset adapter signs
// requests with a dedicated AK/SK pair stored in channel_asset_credentials
// (currently only official Volcengine DoubaoVideo). Without the credential
// the channel must not serve, be bound, or be auto-matched for assets; a DB
// error is treated as not configured so auto-match falls through to other
// candidate channels.
func assetCredentialConfigured(channel *model.Channel) bool {
if channel.Type != constant.ChannelTypeDoubaoVideo {
return true
}
credential, err := model.GetChannelAssetCredential(channel.Id)
if err != nil || credential == nil {
return false
}
return strings.TrimSpace(credential.AccessKey) != "" && strings.TrimSpace(credential.SecretKey) != ""
}

func ResolveAssetChannelForOperation(userID int, tokenGroup string, operation AssetOperation) (*model.Channel, AssetAdapter, *AssetError) {
bindings, err := model.GetUserAssetChannelsByTypes(userID, RegisteredAssetChannelTypes(), tokenGroup)
if err != nil {
@@ -25,7 +44,7 @@ func ResolveAssetChannelForOperation(userID int, tokenGroup string, operation As
return nil, nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
}
adapter, ok := GetAssetAdapter(channel.Type)
if channel.Status != common.ChannelStatusEnabled || !assetChannelHasKey(channel) || !MatchDoubaoAssetGroup(channel.GetGroups(), tokenGroup) || !ok {
if channel.Status != common.ChannelStatusEnabled || !assetChannelHasKey(channel) || !MatchDoubaoAssetGroup(channel.GetGroups(), tokenGroup) || !ok || !assetCredentialConfigured(channel) {
return nil, nil, newAssetError(AssetErrorBindingInvalid, "bound asset channel is not available for asset library", http.StatusBadGateway)
}
if !adapter.Supports(operation) {
@@ -66,7 +85,7 @@ func autoMatchAssetChannelForOperation(tokenGroup string, operation AssetOperati
}
return nil, nil, err
}
if channel.Status == common.ChannelStatusEnabled && assetChannelHasKey(channel) && MatchDoubaoAssetGroup(channel.GetGroups(), tokenGroup) {
if channel.Status == common.ChannelStatusEnabled && assetChannelHasKey(channel) && MatchDoubaoAssetGroup(channel.GetGroups(), tokenGroup) && assetCredentialConfigured(channel) {
return channel, adapter, nil
}
}


+ 10
- 0
service/video_asset_channel.go 파일 보기

@@ -25,7 +25,11 @@ func VideoAssetFamilies() []VideoAssetFamily {
func VideoAssetChannelTypesForFamily(family VideoAssetFamily) []int {
switch family {
case VideoAssetFamilySeedance:
// DoubaoVideo (official Volcengine Ark) joins the family so a user's
// asset upload channel and video task channel stay the same; assets
// are referenced by asset:// ids passed through to the upstream.
return []int{
constant.ChannelTypeDoubaoVideo,
constant.ChannelTypeDoubaoVideoCompatibleAiping,
constant.ChannelTypeDoubaoVideoCompatibleTianyiYun,
constant.ChannelTypeChinaMobileSeedance,
@@ -218,6 +222,12 @@ func IsUsableVideoAssetChannelForFamily(channel *model.Channel, tokenGroup strin
if !MatchDoubaoAssetGroup(channel.GetGroups(), tokenGroup) {
return false
}
// Official Volcengine channels additionally need the asset AK/SK
// credential before they can serve the asset family; without it they are
// skipped so binding/auto-match falls through to compatible channels.
if !assetCredentialConfigured(channel) {
return false
}
modelName = strings.TrimSpace(modelName)
if modelName == "" {
return true


+ 20
- 9
web/src/components/table/channels/modals/EditChannelModal.jsx 파일 보기

@@ -1365,7 +1365,7 @@ const EditChannelModal = (props) => {
if (isEdit && (!localInputs.key || localInputs.key.trim() === '')) {
delete localInputs.key;
}
if (localInputs.type === 61) {
if ([54, 61].includes(localInputs.type)) {
const credential = localInputs.asset_credential || {};
const accessKey = String(credential.access_key || '').trim();
const secretKey = String(credential.secret_key || '').trim();
@@ -2538,9 +2538,11 @@ const EditChannelModal = (props) => {
</>
)}

{inputs.type === 61 && (
{[54, 61].includes(inputs.type) && (
<Card className='mb-4' shadows='always'>
<Text strong>{t('移动云素材凭证')}</Text>
<Text strong>
{t(inputs.type === 61 ? '移动云素材凭证' : '火山素材凭证')}
</Text>
<Text type='tertiary' size='small' className='block mb-3'>
{inputs.asset_credential_configured
? t('已配置素材凭证;留空不会覆盖现有凭证。')
@@ -2558,12 +2560,21 @@ const EditChannelModal = (props) => {
mode='password'
autoComplete='new-password'
/>
<Form.Input
field='asset_credential.pool_id'
label='PoolID'
placeholder='CIDC-CORE-00'
extraText={t('留空使用默认 PoolID:CIDC-CORE-00')}
/>
{inputs.type === 61 ? (
<Form.Input
field='asset_credential.pool_id'
label='PoolID'
placeholder='CIDC-CORE-00'
extraText={t('留空使用默认 PoolID:CIDC-CORE-00')}
/>
) : (
<Form.Input
field='asset_credential.pool_id'
label={t('项目编码 ProjectCode')}
placeholder='bTrHbDj6TB6ZhF6O'
extraText={t('火山项目编码,可选')}
/>
)}
</Card>
)}



불러오는 중...
취소
저장