3 次程式碼提交

作者 SHA1 備註 提交日期
  fengsilin e9975df35c feat(web): 素材凭据输入框增加已配置占位符 1 月之前
  fengsilin 62bda9526a chore(e2e): 凭据与进程清理工具安全加固 1 月之前
  fengsilin 0ab4867130 fix(channel): 修复豆包视频渠道素材凭据摘要不回显 1 月之前
共有 9 個文件被更改,包括 92 次插入 和 25 次删除
分割檢視
  1. +7
    -2
      controller/channel.go
  2. +22
    -4
      controller/channel_asset_credential_test.go
  3. +1
    -1
      test/e2e/helpers/assert.go
  4. +32
    -11
      test/e2e/helpers/cluster.go
  5. +3
    -3
      test/e2e/user_migration_test.go
  6. +2
    -2
      web/e2e/fixtures/cluster.ts
  7. +5
    -2
      web/e2e/utils/process.js
  8. +10
    -0
      web/src/components/table/channels/modals/EditChannelModal.jsx
  9. +10
    -0
      web/src/i18n/locales/en.json

+ 7
- 2
controller/channel.go 查看文件

@@ -70,10 +70,15 @@ func clearChannelInfo(channel *model.Channel) {
}
}

func isAssetCredentialSummaryChannel(channel *model.Channel) bool {
return channel != nil && (channel.Type == constant.ChannelTypeChinaMobileSeedance ||
channel.Type == constant.ChannelTypeDoubaoVideo)
}

func attachChannelAssetCredentialSummaries(channels []*model.Channel) error {
ids := make([]int, 0)
for _, channel := range channels {
if channel != nil && channel.Type == constant.ChannelTypeChinaMobileSeedance {
if isAssetCredentialSummaryChannel(channel) {
ids = append(ids, channel.Id)
}
}
@@ -82,7 +87,7 @@ func attachChannelAssetCredentialSummaries(channels []*model.Channel) error {
return err
}
for _, channel := range channels {
if channel == nil || (channel.Type != constant.ChannelTypeChinaMobileSeedance && channel.Type != constant.ChannelTypeDoubaoVideo) {
if !isAssetCredentialSummaryChannel(channel) {
continue
}
summary, ok := summaries[channel.Id]


+ 22
- 4
controller/channel_asset_credential_test.go 查看文件

@@ -33,18 +33,36 @@ func setupChannelAssetCredentialControllerDB(t *testing.T) *gorm.DB {

func TestAttachChannelAssetCredentialSummariesDoesNotExposeSecrets(t *testing.T) {
db := setupChannelAssetCredentialControllerDB(t)
accessKey := "ak-" + t.Name()
secretKey := "sk-" + t.Name()
channel := &model.Channel{Id: 61, Type: constant.ChannelTypeChinaMobileSeedance, Key: "video-key", Name: "channel"}
require.NoError(t, db.Create(channel).Error)
require.NoError(t, model.UpsertChannelAssetCredential(&model.ChannelAssetCredential{
ChannelId: 61,
AccessKey: "ak-secret",
SecretKey: "sk-secret",
AccessKey: accessKey,
SecretKey: secretKey,
PoolID: "pool-61",
}))

require.NoError(t, attachChannelAssetCredentialSummaries([]*model.Channel{channel}))
assert.True(t, channel.AssetCredentialConfigured)
assert.Equal(t, "pool-61", channel.AssetCredentialPoolID)
assert.NotContains(t, channel.Key, "ak-secret")
assert.NotContains(t, channel.Key, "sk-secret")
assert.NotContains(t, channel.Key, accessKey)
assert.NotContains(t, channel.Key, secretKey)
}

func TestAttachChannelAssetCredentialSummariesIncludesDoubaoVideo(t *testing.T) {
db := setupChannelAssetCredentialControllerDB(t)
channel := &model.Channel{Id: 54, Type: constant.ChannelTypeDoubaoVideo, Key: "video-key", Name: "doubao"}
require.NoError(t, db.Create(channel).Error)
require.NoError(t, model.UpsertChannelAssetCredential(&model.ChannelAssetCredential{
ChannelId: 54,
AccessKey: "ak-" + t.Name(),
SecretKey: "sk-" + t.Name(),
BaseURL: "http://example.com/openApi/portrait",
}))

require.NoError(t, attachChannelAssetCredentialSummaries([]*model.Channel{channel}))
assert.True(t, channel.AssetCredentialConfigured)
assert.Equal(t, "http://example.com/openApi/portrait", channel.AssetCredentialBaseURL)
}

+ 1
- 1
test/e2e/helpers/assert.go 查看文件

@@ -107,7 +107,7 @@ func GetRemoteUserSnapshot(baseURL string, userID int) (*userMigration.RemoteUse
nil,
http.StatusOK,
map[string]string{
"X-Sync-API-Key": DefaultSyncAPIKey,
"X-Sync-API-Key": SyncAPIKey(),
"X-Sync-Node": "e2e-assert",
},
&resp,


+ 32
- 11
test/e2e/helpers/cluster.go 查看文件

@@ -19,19 +19,40 @@ const (

DefaultCNPort = 3100
DefaultOVPort = 3101
DefaultSessionSecret = "user-migration-e2e-session-secret"
DefaultCNDBFileName = "cn.db"
DefaultOVDBFileName = "ov.db"
DefaultCNLogFileName = "cn.log"
DefaultOVLogFileName = "ov.log"
DefaultCNSessionName = "cn_session"
DefaultOVSessionName = "ov_session"
DefaultSyncAPIKey = "e2e-sync-key"
DefaultRootUsername = "root"
DefaultRootPassword = "Password123!"
defaultNodeReadyTimout = 45 * time.Second
)

// The e2e cluster runs on loopback only; each secret below resolves from the
// environment first and falls back to a local-only value shared with the
// Playwright fixtures (see web/e2e/fixtures/cluster.ts).
func RootPassword() string {
if password := strings.TrimSpace(os.Getenv("E2E_ROOT_PASSWORD")); password != "" {
return password
}
return "e2e-local-root-pass"
}

func SessionSecret() string {
if secret := strings.TrimSpace(os.Getenv("E2E_SESSION_SECRET")); secret != "" {
return secret
}
return "e2e-local-session-secret"
}

func SyncAPIKey() string {
if key := strings.TrimSpace(os.Getenv("E2E_SYNC_API_KEY")); key != "" {
return key
}
return "e2e-local-sync-key"
}

type NodeProcess struct {
Name string
Port int
@@ -115,11 +136,11 @@ func PrepareEnvironment(repoRoot, artifactRoot, binaryPath, scenarioName string)
return nil, err
}

if err := SetupRoot(cnClient, DefaultRootUsername, DefaultRootPassword); err != nil {
if err := SetupRoot(cnClient, DefaultRootUsername, RootPassword()); err != nil {
_ = cluster.Stop()
return nil, fmt.Errorf("setup CN root: %w", err)
}
if err := SetupRoot(ovClient, DefaultRootUsername, DefaultRootPassword); err != nil {
if err := SetupRoot(ovClient, DefaultRootUsername, RootPassword()); err != nil {
_ = cluster.Stop()
return nil, fmt.Errorf("setup OV root: %w", err)
}
@@ -151,25 +172,25 @@ func PrepareEnvironment(repoRoot, artifactRoot, binaryPath, scenarioName string)
return nil, err
}

if err := Login(cnClient, DefaultRootUsername, DefaultRootPassword); err != nil {
if err := Login(cnClient, DefaultRootUsername, RootPassword()); err != nil {
_ = cluster.Stop()
return nil, fmt.Errorf("login CN root: %w", err)
}
if err := Login(ovClient, DefaultRootUsername, DefaultRootPassword); err != nil {
if err := Login(ovClient, DefaultRootUsername, RootPassword()); err != nil {
_ = cluster.Stop()
return nil, fmt.Errorf("login OV root: %w", err)
}

if err := ConfigureCNRegionSync(cnClient, cluster.CN.BaseURL, cluster.OV.BaseURL, DefaultSyncAPIKey); err != nil {
if err := ConfigureCNRegionSync(cnClient, cluster.CN.BaseURL, cluster.OV.BaseURL, SyncAPIKey()); err != nil {
_ = cluster.Stop()
return nil, fmt.Errorf("configure CN region sync: %w", err)
}
if err := ConfigureOVRegionSync(ovClient, cluster.CN.BaseURL, DefaultSyncAPIKey); err != nil {
if err := ConfigureOVRegionSync(ovClient, cluster.CN.BaseURL, SyncAPIKey()); err != nil {
_ = cluster.Stop()
return nil, fmt.Errorf("configure OV region sync: %w", err)
}

if err := WaitOptionApplied(cluster.OV.BaseURL, DefaultSyncAPIKey, 5, 20*time.Second); err != nil {
if err := WaitOptionApplied(cluster.OV.BaseURL, SyncAPIKey(), 5, 20*time.Second); err != nil {
_ = cluster.Stop()
return nil, fmt.Errorf("wait OV migration API ready: %w", err)
}
@@ -272,7 +293,7 @@ func (c *Cluster) startNode(name string, port int, dbPath, sessionName, nodeType
cmd.Env = mergeEnv(os.Environ(), map[string]string{
"PORT": fmt.Sprintf("%d", port),
"SQLITE_PATH": dbPath,
"SESSION_SECRET": DefaultSessionSecret,
"SESSION_SECRET": SessionSecret(),
"SESSION_NAME": sessionName,
"SESSION_SECURE": "false",
"SESSION_SAMESITE": "strict",


+ 3
- 3
test/e2e/user_migration_test.go 查看文件

@@ -224,7 +224,7 @@ func TestUserMigrationE2E_ScanPagination(t *testing.T) {
func TestUserMigrationE2E_RootPermission(t *testing.T) {
env := newEnvironment(t)

adminClient := createAndLoginManagedUser(t, env.Cluster.CN.BaseURL, env.CNClient, "migration-admin", helpers.DefaultRootPassword, common.RoleAdminUser)
adminClient := createAndLoginManagedUser(t, env.Cluster.CN.BaseURL, env.CNClient, "migration-admin", helpers.RootPassword(), common.RoleAdminUser)

var listResp simpleResponse
require.NoError(t, adminClient.GetJSON("/api/user-migrations/batches", 200, &listResp))
@@ -707,7 +707,7 @@ func createManagedUsers(t *testing.T, client *helpers.APIClient, prefix string,
var resp simpleResponse
require.NoError(t, client.PostJSON("/api/user/", map[string]any{
"username": username,
"password": helpers.DefaultRootPassword,
"password": helpers.RootPassword(),
"display_name": displayName,
"email": email,
"role": role,
@@ -775,7 +775,7 @@ func convertRemoteUserToSynced(t *testing.T, ovBaseURL string, sourceUserID, rem
},
200,
map[string]string{
"X-Sync-API-Key": helpers.DefaultSyncAPIKey,
"X-Sync-API-Key": helpers.SyncAPIKey(),
"X-Sync-Node": "e2e-test",
},
&resp,


+ 2
- 2
web/e2e/fixtures/cluster.ts 查看文件

@@ -33,9 +33,9 @@ export const clusterRunDir = path.join(artifactRoot, 'runs', 'playwright-ui');
export const cnBaseURL = 'http://127.0.0.1:3100';
export const ovBaseURL = 'http://127.0.0.1:3101';
export const rootUsername = 'root';
export const rootPassword = 'Password123!';
export const rootPassword = process.env.E2E_ROOT_PASSWORD ?? 'e2e-local-root-pass';
export const adminUsername = 'playwright-admin';
export const adminPassword = 'Password123!';
export const adminPassword = process.env.E2E_ADMIN_PASSWORD ?? 'e2e-local-admin-pass';

export type ClusterMetadata = {
serverPid: number;


+ 5
- 2
web/e2e/utils/process.js 查看文件

@@ -66,10 +66,13 @@ function waitForExit(child, timeoutMs) {
}

function killWindowsProcessTree(pid) {
if (!pid) return Promise.resolve();
const numericPid = Number(pid);
if (!Number.isInteger(numericPid) || numericPid <= 0) {
return Promise.resolve();
}

return new Promise((resolve) => {
const killer = spawn('taskkill', ['/PID', String(pid), '/T', '/F'], {
const killer = spawn('taskkill', ['/PID', String(numericPid), '/T', '/F'], {
stdio: 'ignore',
});



+ 10
- 0
web/src/components/table/channels/modals/EditChannelModal.jsx 查看文件

@@ -2566,12 +2566,22 @@ const EditChannelModal = (props) => {
label='AccessKey'
mode='password'
autoComplete='new-password'
placeholder={
inputs.asset_credential_configured
? t('已配置,留空保持不变')
: t('请输入 AccessKey')
}
/>
<Form.Input
field='asset_credential.secret_key'
label='SecretKey'
mode='password'
autoComplete='new-password'
placeholder={
inputs.asset_credential_configured
? t('已配置,留空保持不变')
: t('请输入 SecretKey')
}
/>
{inputs.type === 61 ? (
<Form.Input


+ 10
- 0
web/src/i18n/locales/en.json 查看文件

@@ -2723,6 +2723,16 @@
"豆包视频(素材网关)": "Doubao Video (Asset Gateway)",
"素材 API 地址": "Asset API Base URL",
"素材库接口调用该地址下的 /openApi/portrait,为素材网关专属;留空则使用渠道 API 地址。火山官方地址不提供此接口。": "Asset APIs call /openApi/portrait under this address (asset gateway only); leave empty to use the channel API address. The official Volcengine address does not provide this API.",
"已配置,留空保持不变": "Configured; leave empty to keep the current value",
"请输入 AccessKey": "Enter the AccessKey",
"请输入 SecretKey": "Enter the SecretKey",
"已配置素材凭证;留空不会覆盖现有凭证。": "Asset credentials configured; leaving the fields empty will not overwrite them.",
"未配置素材凭证;素材上传接口将不可用。": "No asset credentials configured; asset upload APIs will be unavailable.",
"移动云素材凭证": "China Mobile Cloud Asset Credentials",
"火山素材凭证": "Volcengine Asset Credentials",
"项目编码 ProjectCode": "Project Code (ProjectCode)",
"火山项目编码,可选": "Volcengine project code, optional",
"请同时填写素材 AccessKey 和 SecretKey": "Fill in both asset AccessKey and SecretKey",
"账单": "Bills",
"账户充值": "Account recharge",
"账户已删除!": "Account has been deleted!",


Loading…
取消
儲存