Compare commits

..

2 Commits

Author SHA1 Message Date
old burden a347873a4e Merge remote-tracking branch 'origin/seedance' into seedance 2026-04-01 17:41:35 +08:00
old burden 2a91a33825 fix: 对接素材管理 2026-04-01 17:40:40 +08:00
6 changed files with 206 additions and 47 deletions

View File

@ -58,7 +58,15 @@
</div> </div>
</div> </div>
<div class="vg-asset-controls"> <div class="vg-asset-controls">
<input v-model.trim="assetGroupId" class="vg-asset-group-input" placeholder="请输入 GroupId" /> <select v-model="assetGroupId" class="vg-asset-group-input">
<option value="">请选择素材组</option>
<option v-for="g in assetGroups" :key="g.Id || g.id" :value="g.Id || g.id">
{{ g.Name || g.name || g.Id || g.id }}
</option>
</select>
<mf-button class="vg-compose-left-upload" size="small" @click="loadAssetGroups" :loading="groupLoading">
刷新分组
</mf-button>
<mf-button class="vg-compose-left-upload" size="small" @click="loadAssetsByGroup" :loading="assetLoading"> <mf-button class="vg-compose-left-upload" size="small" @click="loadAssetsByGroup" :loading="assetLoading">
查询资产 查询资产
</mf-button> </mf-button>
@ -66,6 +74,12 @@
上传资产 上传资产
</mf-button> </mf-button>
</div> </div>
<div class="vg-asset-controls">
<input v-model.trim="newGroupName" class="vg-asset-group-input" placeholder="输入新素材组名称" />
<mf-button class="vg-compose-left-upload" size="small" type="primary" @click="createAssetGroup" :loading="groupLoading">
新建素材组
</mf-button>
</div>
<div v-if="mediaList.length === 0" class="vg-compose-empty" @click="openFilePicker"> <div v-if="mediaList.length === 0" class="vg-compose-empty" @click="openFilePicker">
<div class="vg-compose-empty-icon" aria-hidden="true">+</div> <div class="vg-compose-empty-icon" aria-hidden="true">+</div>
@ -207,6 +221,9 @@ const mentionVisible = ref(false)
const mentionActiveIndex = ref(-1) const mentionActiveIndex = ref(-1)
const assetGroupId = ref('') const assetGroupId = ref('')
const assetLoading = ref(false) const assetLoading = ref(false)
const groupLoading = ref(false)
const assetGroups = ref([])
const newGroupName = ref('')
watch( watch(
() => props.modelValue, () => props.modelValue,
@ -244,6 +261,9 @@ onMounted(() => {
if (editorRef.value && localPrompt.value) { if (editorRef.value && localPrompt.value) {
editorRef.value.innerText = localPrompt.value editorRef.value.innerText = localPrompt.value
} }
if (isReference.value) {
loadAssetGroups()
}
}) })
const mediaList = computed(() => internalMediaList.value) const mediaList = computed(() => internalMediaList.value)
@ -368,6 +388,62 @@ const mergeAssetsToMediaList = (assets) => {
setMediaList(next.slice(0, props.maxMediaCount)) setMediaList(next.slice(0, props.maxMediaCount))
} }
const loadAssetGroups = async () => {
if (!proxy?.$axios) return
groupLoading.value = true
try {
const res = await proxy.$axios({
url: 'api/byteAssetGroup/listAssetGroups',
method: 'POST',
data: {
Filter: { GroupType: 'AIGC' },
PageNumber: 1,
PageSize: 100,
SortBy: 'CreateTime',
SortOrder: 'Desc'
}
})
const rows = Array.isArray(res?.data?.Items) ? res.data.Items : []
assetGroups.value = rows
if (!assetGroupId.value && rows.length) {
assetGroupId.value = rows[0]?.Id || rows[0]?.id || ''
}
} catch (err) {
Message.error(err?.message || '加载素材组失败')
} finally {
groupLoading.value = false
}
}
const createAssetGroup = async () => {
const name = String(newGroupName.value || '').trim()
if (!name) {
Message.warning('请输入素材组名称')
return
}
if (!proxy?.$axios) return
groupLoading.value = true
try {
const res = await proxy.$axios({
url: 'api/byteAssetGroup/createAssetGroup',
method: 'POST',
data: {
Name: name,
ProjectName: 'default'
}
})
const gid = res?.data?.Id || res?.data?.id || ''
newGroupName.value = ''
await loadAssetGroups()
if (gid) assetGroupId.value = gid
Message.success('素材组创建成功')
} catch (err) {
Message.error(err?.message || '创建素材组失败')
} finally {
groupLoading.value = false
}
}
const loadAssetsByGroup = async () => { const loadAssetsByGroup = async () => {
const gid = String(assetGroupId.value || '').trim() const gid = String(assetGroupId.value || '').trim()
if (!gid) { if (!gid) {
@ -381,7 +457,7 @@ const loadAssetsByGroup = async () => {
assetLoading.value = true assetLoading.value = true
try { try {
const res = await proxy.$axios({ const res = await proxy.$axios({
url: 'api/portal/asset/listAssets', url: 'api/byteAsset/listAssets',
method: 'POST', method: 'POST',
data: { data: {
Filter: { Filter: {
@ -509,15 +585,15 @@ const loadAssetsByGroup = async () => {
const mt = entry.mediaType || 'image' const mt = entry.mediaType || 'image'
const assetType = const assetType =
mt === 'video' ? 'Video' : mt === 'audio' ? 'Audio' : 'Image' mt === 'video' ? 'Video' : mt === 'audio' ? 'Audio' : 'Image'
const fd = new FormData()
fd.append('file', entry._fileRef)
fd.append('groupId', gid)
fd.append('assetType', assetType)
fd.append('name', entry?.name || '')
const createRes = await proxy.$axios({ const createRes = await proxy.$axios({
url: 'api/portal/asset/createAsset', url: 'api/byteAsset/createAsset',
method: 'POST', method: 'POST',
data: { data: fd
GroupId: gid,
URL: url,
Name: entry?.name || '',
AssetType: assetType
}
}) })
assetId = createRes?.data?.Id || createRes?.data?.id || '' assetId = createRes?.data?.Id || createRes?.data?.id || ''
if (!assetId) throw new Error(createRes?.msg || '创建素材失败未返回资产ID') if (!assetId) throw new Error(createRes?.msg || '创建素材失败未返回资产ID')

View File

@ -176,12 +176,13 @@ export default {
this.createLoading = true this.createLoading = true
try { try {
const res = await this.$axios({ const res = await this.$axios({
url: 'api/portal/asset/post', url: 'api/byteAssetGroup/createAssetGroup',
method: 'POST', method: 'POST',
data: { data: {
Name: name, Name: name,
Description: String(this.createForm.description || '').trim(), Description: String(this.createForm.description || '').trim(),
GroupType: 'AIGC' GroupType: 'AIGC',
ProjectName: 'default'
} }
}) })
if (res.code === 200) { if (res.code === 200) {
@ -217,7 +218,7 @@ export default {
if (ids.length) payload.Filter.GroupIds = ids if (ids.length) payload.Filter.GroupIds = ids
const res = await this.$axios({ const res = await this.$axios({
url: 'api/portal/asset/list', url: 'api/byteAssetGroup/listAssetGroups',
method: 'POST', method: 'POST',
data: payload data: payload
}) })
@ -255,7 +256,7 @@ export default {
this.detailLoadingId = id this.detailLoadingId = id
try { try {
const res = await this.$axios({ const res = await this.$axios({
url: 'api/portal/asset/get', url: 'api/byteAssetGroup/getAssetGroup',
method: 'POST', method: 'POST',
data: { Id: id } data: { Id: id }
}) })

View File

@ -3,6 +3,16 @@
<section class="asset-left"> <section class="asset-left">
<div class="panel-title">素材组树</div> <div class="panel-title">素材组树</div>
<a-button size="mini" type="outline" :loading="groupLoading" @click="loadGroups">刷新分组</a-button> <a-button size="mini" type="outline" :loading="groupLoading" @click="loadGroups">刷新分组</a-button>
<div class="form-grid mtop">
<div class="field">
<label>素材组名称</label>
<a-input v-model="groupForm.name" placeholder="输入分组名称" />
</div>
<div class="field actions">
<a-button type="primary" size="mini" @click="createGroup">新建分组</a-button>
<a-button size="mini" @click="updateGroup" :disabled="!groupForm.id">更新分组</a-button>
</div>
</div>
<div class="group-tree"> <div class="group-tree">
<div <div
v-for="g in groups" v-for="g in groups"
@ -27,8 +37,9 @@
<a-input v-model="createForm.groupId" placeholder="请选择左侧分组或手动输入" /> <a-input v-model="createForm.groupId" placeholder="请选择左侧分组或手动输入" />
</div> </div>
<div class="field"> <div class="field">
<label>URL</label> <label>文件</label>
<a-input v-model="createForm.url" placeholder="素材公网 URLhttp/https" /> <input type="file" @change="onFileChange" />
<div class="group-id">{{ createForm.fileName || '未选择文件' }}</div>
</div> </div>
<div class="field"> <div class="field">
<label>Name</label> <label>Name</label>
@ -143,11 +154,14 @@
</template> </template>
<script> <script>
const GROUP_LIST_API = 'api/portal/asset/list' const GROUP_LIST_API = 'api/byteAssetGroup/listAssetGroups'
const ASSET_CREATE_API = 'api/portal/asset/createAsset' const GROUP_CREATE_API = 'api/byteAssetGroup/createAssetGroup'
const ASSET_LIST_API = 'api/portal/asset/listAssets' const GROUP_GET_API = 'api/byteAssetGroup/getAssetGroup'
const ASSET_GET_API = 'api/portal/asset/getAsset' const GROUP_UPDATE_API = 'api/byteAssetGroup/updateAssetGroup'
const ASSET_DELETE_API = 'api/portal/asset/deleteAsset' const ASSET_CREATE_API = 'api/byteAsset/createAsset'
const ASSET_LIST_API = 'api/byteAsset/listAssets'
const ASSET_GET_API = 'api/byteAsset/getAsset'
const ASSET_DELETE_API = 'api/byteAsset/deleteAsset'
export default { export default {
name: 'AssetManage', name: 'AssetManage',
@ -160,10 +174,15 @@ export default {
selectedGroupId: '', selectedGroupId: '',
createForm: { createForm: {
groupId: '', groupId: '',
url: '', file: null,
fileName: '',
name: '', name: '',
assetType: 'Image' assetType: 'Image'
}, },
groupForm: {
name: '',
id: ''
},
filters: { filters: {
groupId: '', groupId: '',
name: '', name: '',
@ -206,6 +225,8 @@ export default {
if (!this.selectedGroupId && this.groups.length) { if (!this.selectedGroupId && this.groups.length) {
const gid = this.groups[0].Id || this.groups[0].id const gid = this.groups[0].Id || this.groups[0].id
this.selectedGroupId = gid this.selectedGroupId = gid
this.groupForm.id = gid
this.groupForm.name = this.groups[0].Name || this.groups[0].name || ''
this.createForm.groupId = gid this.createForm.groupId = gid
this.filters.groupId = gid this.filters.groupId = gid
this.searchAssets(1) this.searchAssets(1)
@ -219,30 +240,89 @@ export default {
selectGroup(g) { selectGroup(g) {
const gid = g?.Id || g?.id const gid = g?.Id || g?.id
this.selectedGroupId = gid this.selectedGroupId = gid
this.groupForm.id = gid
this.groupForm.name = g?.Name || g?.name || ''
this.createForm.groupId = gid this.createForm.groupId = gid
this.filters.groupId = gid this.filters.groupId = gid
this.searchAssets(1) this.searchAssets(1)
}, },
async createGroup() {
const name = String(this.groupForm.name || '').trim()
if (!name) return this.$message.error('请输入素材组名称')
try {
const res = await this.$axios({
url: GROUP_CREATE_API,
method: 'POST',
data: { Name: name, ProjectName: 'default' }
})
if (res.code === 200) {
this.$message.success('创建素材组成功')
await this.loadGroups()
} else {
this.$message.error(res.msg || '创建素材组失败')
}
} catch (e) {
this.$message.error(e?.message || '创建素材组失败')
}
},
async updateGroup() {
const id = String(this.groupForm.id || '').trim()
const name = String(this.groupForm.name || '').trim()
if (!id) return this.$message.error('请先选择素材组')
if (!name) return this.$message.error('请输入素材组名称')
try {
const detailRes = await this.$axios({
url: GROUP_GET_API,
method: 'POST',
data: { Id: id }
})
const detail = detailRes?.data || {}
const payload = {
Id: id,
Name: name,
GroupType: detail.GroupType || detail.groupType || 'AIGC',
ProjectName: detail.ProjectName || detail.projectName || 'default'
}
const res = await this.$axios({
url: GROUP_UPDATE_API,
method: 'POST',
data: payload
})
if (res.code === 200) {
this.$message.success('更新素材组成功')
await this.loadGroups()
} else {
this.$message.error(res.msg || '更新素材组失败')
}
} catch (e) {
this.$message.error(e?.message || '更新素材组失败')
}
},
onFileChange(e) {
const file = e?.target?.files?.[0]
this.createForm.file = file || null
this.createForm.fileName = file?.name || ''
},
async createAsset() { async createAsset() {
const groupId = String(this.createForm.groupId || '').trim() const groupId = String(this.createForm.groupId || '').trim()
const url = String(this.createForm.url || '').trim()
if (!groupId) return this.$message.error('请填写 GroupId') if (!groupId) return this.$message.error('请填写 GroupId')
if (!/^https?:\/\//i.test(url)) return this.$message.error('URL 必须是 http(s) 地址') if (!this.createForm.file) return this.$message.error('请选择上传文件')
this.createLoading = true this.createLoading = true
try { try {
const fd = new FormData()
fd.append('file', this.createForm.file)
fd.append('groupId', groupId)
fd.append('assetType', this.createForm.assetType)
fd.append('name', String(this.createForm.name || '').trim())
const res = await this.$axios({ const res = await this.$axios({
url: ASSET_CREATE_API, url: ASSET_CREATE_API,
method: 'POST', method: 'POST',
data: { data: fd
GroupId: groupId,
URL: url,
Name: String(this.createForm.name || '').trim(),
AssetType: this.createForm.assetType
}
}) })
if (res.code === 200) { if (res.code === 200) {
this.$message.success('新增素材成功') this.$message.success('新增素材成功')
this.createForm.url = '' this.createForm.file = null
this.createForm.fileName = ''
this.createForm.name = '' this.createForm.name = ''
this.searchAssets(1) this.searchAssets(1)
} else { } else {

View File

@ -44,8 +44,6 @@ public class ByteApiController extends BaseController {
private String url; private String url;
// 火山引擎配置 // 火山引擎配置
@Value("${volcengine.ark.apiKey}")
private String volcApiKey;
@Value("${volcengine.ark.baseUrl}") @Value("${volcengine.ark.baseUrl}")
private String volcBaseUrl; private String volcBaseUrl;
@Value("${volcengine.ark.callbackUrl}") @Value("${volcengine.ark.callbackUrl}")

View File

@ -224,16 +224,16 @@ tencentCos:
# domain: https://images.iqyjsnwv.com/ # domain: https://images.iqyjsnwv.com/
byteapi: byteapi:
url: https://ark.ap-southeast.bytepluses.com/api/v3 url:
apiKey: 3e33e034-7e25-4228-8864-b51b2a7a8f97 apiKey:
callBackUrl: https://undressing.top callBackUrl:
# 火山引擎 Ark API (Seedance 2.0) # 火山引擎 Ark API (Seedance 2.0)
volcengine: volcengine:
ark: ark:
baseUrl: https://ark.cn-beijing.volces.com baseUrl:
apiKey: 3e33e034-7e25-4228-8864-b51b2a7a8f97 apiKey:
callbackUrl: https://undressing.top/api/ai/volcCallback callbackUrl:
# 门户视频生成页:模型 / 比例 / 时长 / 分辨率均由此处维护,前后端不写死业务枚举 # 门户视频生成页:模型 / 比例 / 时长 / 分辨率均由此处维护,前后端不写死业务枚举
portal: portal:

View File

@ -5,12 +5,15 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.ai.domain.ByteBodyReq; import com.ruoyi.ai.domain.ByteBodyReq;
import com.ruoyi.ai.domain.ByteBodyRes; import com.ruoyi.ai.domain.ByteBodyRes;
import com.ruoyi.ai.service.IByteDeptApiKeyService;
import com.ruoyi.ai.service.IByteService; import com.ruoyi.ai.service.IByteService;
import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.http.OkHttpUtils; import com.ruoyi.common.utils.http.OkHttpUtils;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
import okhttp3.*; import okhttp3.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -29,15 +32,12 @@ public class ByteService implements IByteService {
@Value("${byteapi.url}") @Value("${byteapi.url}")
private String API_URL; private String API_URL;
@Value("${byteapi.apiKey}")
private String apiKey;
// 火山引擎配置 // 火山引擎配置
@Value("${volcengine.ark.baseUrl:https://ark.cn-beijing.volces.com}") @Value("${volcengine.ark.baseUrl:https://ark.cn-beijing.volces.com}")
private String volcBaseUrl; private String volcBaseUrl;
@Value("${volcengine.ark.apiKey}") @Autowired
private String volcApiKey; private IByteDeptApiKeyService byteDeptApiKeyService;
@Override @Override
public ByteBodyRes promptToImg(ByteBodyReq req) throws Exception { public ByteBodyRes promptToImg(ByteBodyReq req) throws Exception {
@ -59,7 +59,7 @@ public class ByteService implements IByteService {
Request request = new Request.Builder() Request request = new Request.Builder()
.url(API_URL + "/images/generations") .url(API_URL + "/images/generations")
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.header("Authorization", "Bearer " + apiKey) .header("Authorization", "Bearer " + resolveCurrentAiUserApiKey())
// .proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("192.168.1.1", 8080))) // .proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("192.168.1.1", 8080)))
.post(RequestBody.create( .post(RequestBody.create(
MediaType.parse("application/json"), MediaType.parse("application/json"),
@ -84,7 +84,7 @@ public class ByteService implements IByteService {
@Override @Override
public ByteBodyRes imgToVideo(ByteBodyReq req) throws Exception { public ByteBodyRes imgToVideo(ByteBodyReq req) throws Exception {
return imgToVideo(req, volcApiKey); return imgToVideo(req, resolveCurrentAiUserApiKey());
} }
@Override @Override
@ -125,7 +125,7 @@ public class ByteService implements IByteService {
@Override @Override
public ByteBodyRes uploadVideo(String id) throws Exception { public ByteBodyRes uploadVideo(String id) throws Exception {
return uploadVideo(id, volcApiKey); return uploadVideo(id, resolveCurrentAiUserApiKey());
} }
@Override @Override
@ -161,7 +161,7 @@ public class ByteService implements IByteService {
@Override @Override
public AjaxResult cancelVideoTask(String id) throws Exception { public AjaxResult cancelVideoTask(String id) throws Exception {
return cancelVideoTask(id, volcApiKey); return cancelVideoTask(id, resolveCurrentAiUserApiKey());
} }
@Override @Override
@ -228,4 +228,8 @@ public class ByteService implements IByteService {
} }
return body; return body;
} }
private String resolveCurrentAiUserApiKey() {
return byteDeptApiKeyService.resolveVolcApiKey(SecurityUtils.getAiUserId());
}
} }