Compare commits
2 Commits
6d4142fff0
...
969283690f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
969283690f | ||
|
|
0a1a3a266c |
@ -2726,10 +2726,8 @@ def _assets_api_call(func, *args, **kwargs):
|
|||||||
return result, None
|
return result, None
|
||||||
except AssetsAPIError as e:
|
except AssetsAPIError as e:
|
||||||
logger.warning('Assets API error: %s', e)
|
logger.warning('Assets API error: %s', e)
|
||||||
# Surface readable message for known parameter errors
|
|
||||||
msg = e.api_message if hasattr(e, 'api_message') else str(e)
|
|
||||||
return None, Response(
|
return None, Response(
|
||||||
{'error': 'assets_api_error', 'message': msg},
|
{'error': 'assets_api_error', 'message': e.user_message},
|
||||||
status=status.HTTP_502_BAD_GATEWAY,
|
status=status.HTTP_502_BAD_GATEWAY,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -2750,18 +2748,35 @@ def asset_groups_view(request):
|
|||||||
team = request.user.team
|
team = request.user.team
|
||||||
|
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
groups = (
|
groups = list(
|
||||||
AssetGroup.objects
|
AssetGroup.objects
|
||||||
.filter(team=team)
|
.filter(team=team)
|
||||||
.annotate(asset_count=Count('assets'))
|
.annotate(asset_count=Count('assets'))
|
||||||
.order_by('-created_at')
|
.order_by('-created_at')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 从火山同步素材组名字(一次 API 调用)
|
||||||
|
remote_ids = [g.remote_group_id for g in groups if g.remote_group_id]
|
||||||
|
if remote_ids:
|
||||||
|
try:
|
||||||
|
from utils.assets_client import list_asset_groups
|
||||||
|
remote_items, _ = list_asset_groups(page=1, page_size=100)
|
||||||
|
remote_name_map = {item['Id']: item.get('Name', '') for item in remote_items if 'Id' in item}
|
||||||
|
for g in groups:
|
||||||
|
if g.remote_group_id and g.remote_group_id in remote_name_map:
|
||||||
|
remote_name = remote_name_map[g.remote_group_id]
|
||||||
|
if remote_name and remote_name != g.name:
|
||||||
|
g.name = remote_name
|
||||||
|
g.save(update_fields=['name'])
|
||||||
|
except Exception:
|
||||||
|
pass # 同步失败不影响列表展示
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
for g in groups:
|
for g in groups:
|
||||||
results.append({
|
results.append({
|
||||||
'id': g.id,
|
'id': g.id,
|
||||||
'name': g.name,
|
'name': g.name,
|
||||||
'thumbnail_url': g.thumbnail_url,
|
'thumbnail_url': g.thumbnail_url if g.asset_count > 0 else '',
|
||||||
'asset_count': g.asset_count,
|
'asset_count': g.asset_count,
|
||||||
'remote_group_id': g.remote_group_id,
|
'remote_group_id': g.remote_group_id,
|
||||||
'created_at': g.created_at.isoformat(),
|
'created_at': g.created_at.isoformat(),
|
||||||
@ -2784,12 +2799,12 @@ def asset_groups_view(request):
|
|||||||
w, h = img.size
|
w, h = img.size
|
||||||
if w < 300 or h < 300:
|
if w < 300 or h < 300:
|
||||||
return Response(
|
return Response(
|
||||||
{'error': f'图片尺寸太小({w}x{h}),宽高均需 ≥ 300px'},
|
{'error': f'图片太小了,请上传更大的图片(当前 {w}x{h},最小要求 300x300)'},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
if w > 6000 or h > 6000:
|
if w > 6000 or h > 6000:
|
||||||
return Response(
|
return Response(
|
||||||
{'error': f'图片尺寸太大({w}x{h}),宽高均需 ≤ 6000px'},
|
{'error': f'图片太大了,请压缩后重试(当前 {w}x{h},最大支持 6000x6000)'},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
file.seek(0) # Reset after PIL read
|
file.seek(0) # Reset after PIL read
|
||||||
@ -2868,6 +2883,18 @@ def asset_group_detail_view(request, group_id):
|
|||||||
return Response({'error': '素材组不存在'}, status=status.HTTP_404_NOT_FOUND)
|
return Response({'error': '素材组不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
|
# 同步火山端的素材组名字
|
||||||
|
if group.remote_group_id:
|
||||||
|
try:
|
||||||
|
from utils.assets_client import get_asset_group
|
||||||
|
remote = get_asset_group(group.remote_group_id)
|
||||||
|
remote_name = remote.get('Name', '')
|
||||||
|
if remote_name and remote_name != group.name:
|
||||||
|
group.name = remote_name
|
||||||
|
group.save(update_fields=['name'])
|
||||||
|
except Exception:
|
||||||
|
pass # 查不到就用本地名字
|
||||||
|
|
||||||
assets_qs = Asset.objects.filter(group=group).order_by('-created_at')
|
assets_qs = Asset.objects.filter(group=group).order_by('-created_at')
|
||||||
asset_list = []
|
asset_list = []
|
||||||
for a in assets_qs:
|
for a in assets_qs:
|
||||||
@ -2947,12 +2974,12 @@ def asset_group_add_asset_view(request, group_id):
|
|||||||
w, h = img.size
|
w, h = img.size
|
||||||
if w < 300 or h < 300:
|
if w < 300 or h < 300:
|
||||||
return Response(
|
return Response(
|
||||||
{'error': f'图片尺寸太小({w}x{h}),宽高均需 ≥ 300px'},
|
{'error': f'图片太小了,请上传更大的图片(当前 {w}x{h},最小要求 300x300)'},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
if w > 6000 or h > 6000:
|
if w > 6000 or h > 6000:
|
||||||
return Response(
|
return Response(
|
||||||
{'error': f'图片尺寸太大({w}x{h}),宽高均需 ≤ 6000px'},
|
{'error': f'图片太大了,请压缩后重试(当前 {w}x{h},最大支持 6000x6000)'},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
file.seek(0)
|
file.seek(0)
|
||||||
@ -3053,6 +3080,7 @@ def asset_search_view(request):
|
|||||||
groups = (
|
groups = (
|
||||||
AssetGroup.objects
|
AssetGroup.objects
|
||||||
.filter(team=team, name__icontains=q)
|
.filter(team=team, name__icontains=q)
|
||||||
|
.annotate(asset_count=Count('assets'))
|
||||||
.order_by('-created_at')[:20]
|
.order_by('-created_at')[:20]
|
||||||
)
|
)
|
||||||
results = []
|
results = []
|
||||||
@ -3060,7 +3088,8 @@ def asset_search_view(request):
|
|||||||
results.append({
|
results.append({
|
||||||
'id': g.id,
|
'id': g.id,
|
||||||
'name': g.name,
|
'name': g.name,
|
||||||
'thumbnail_url': g.thumbnail_url,
|
'thumbnail_url': g.thumbnail_url if g.asset_count > 0 else '',
|
||||||
|
'asset_count': g.asset_count,
|
||||||
'remote_group_id': g.remote_group_id,
|
'remote_group_id': g.remote_group_id,
|
||||||
})
|
})
|
||||||
return Response({'results': results})
|
return Response({'results': results})
|
||||||
|
|||||||
@ -21,12 +21,41 @@ HOST = 'open.volcengineapi.com'
|
|||||||
PROJECT_NAME = 'int_dev_Airlabs'
|
PROJECT_NAME = 'int_dev_Airlabs'
|
||||||
|
|
||||||
|
|
||||||
|
_ASSETS_ERROR_MESSAGES = {
|
||||||
|
'ConfigError': '素材服务未配置,请联系管理员',
|
||||||
|
'RequestError': '素材服务暂时不可用,请稍后重试',
|
||||||
|
'InvalidParameter': '素材参数无效,请检查输入',
|
||||||
|
'NotFound': '素材不存在或已被删除',
|
||||||
|
'NotExist': '素材不存在或已被删除',
|
||||||
|
'InternalError': '素材服务异常,请稍后重试',
|
||||||
|
'Forbidden': '没有权限操作该素材',
|
||||||
|
'RateLimitExceeded': '操作过于频繁,请稍后重试',
|
||||||
|
}
|
||||||
|
|
||||||
|
_ASSETS_MESSAGE_KEYWORDS = {
|
||||||
|
'dimension': '图片尺寸不符合要求(宽高需在 300~6000 像素之间)',
|
||||||
|
'size': '文件大小超出限制',
|
||||||
|
'format': '不支持的文件格式',
|
||||||
|
'not found': '素材不存在或已被删除',
|
||||||
|
'permission': '没有权限操作该素材',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class AssetsAPIError(Exception):
|
class AssetsAPIError(Exception):
|
||||||
"""Raised when the Assets API returns an error."""
|
"""Raised when the Assets API returns an error."""
|
||||||
def __init__(self, code, message, status_code=400):
|
def __init__(self, code, message, status_code=400):
|
||||||
self.code = code
|
self.code = code
|
||||||
self.api_message = message
|
self.api_message = message
|
||||||
self.status_code = status_code
|
self.status_code = status_code
|
||||||
|
# 中文友好提示
|
||||||
|
friendly = _ASSETS_ERROR_MESSAGES.get(code)
|
||||||
|
if not friendly:
|
||||||
|
msg_lower = (message or '').lower()
|
||||||
|
for keyword, hint in _ASSETS_MESSAGE_KEYWORDS.items():
|
||||||
|
if keyword in msg_lower:
|
||||||
|
friendly = hint
|
||||||
|
break
|
||||||
|
self.user_message = friendly or '素材操作失败,请稍后重试'
|
||||||
super().__init__(f'[{code}] {message}')
|
super().__init__(f'[{code}] {message}')
|
||||||
|
|
||||||
|
|
||||||
@ -167,6 +196,12 @@ def get_asset(asset_id: str) -> dict:
|
|||||||
return _do_request('GetAsset', body)
|
return _do_request('GetAsset', body)
|
||||||
|
|
||||||
|
|
||||||
|
def get_asset_group(group_id: str) -> dict:
|
||||||
|
"""Get single asset group details."""
|
||||||
|
body = {'Id': group_id, 'ProjectName': PROJECT_NAME}
|
||||||
|
return _do_request('GetAssetGroup', body)
|
||||||
|
|
||||||
|
|
||||||
def update_asset_group(group_id: str, name: str = None, description: str = None):
|
def update_asset_group(group_id: str, name: str = None, description: str = None):
|
||||||
"""Update an asset group's name and/or description."""
|
"""Update an asset group's name and/or description."""
|
||||||
body = {'Id': group_id, 'ProjectName': PROJECT_NAME}
|
body = {'Id': group_id, 'ProjectName': PROJECT_NAME}
|
||||||
|
|||||||
@ -211,7 +211,11 @@ export function AssetLibraryModal({ open, onClose }: Props) {
|
|||||||
<div className={styles.grid}>
|
<div className={styles.grid}>
|
||||||
{groups.map((group) => (
|
{groups.map((group) => (
|
||||||
<div key={group.id} className={styles.card} onClick={() => handleGroupClick(group)}>
|
<div key={group.id} className={styles.card} onClick={() => handleGroupClick(group)}>
|
||||||
|
{group.asset_count === 0 ? (
|
||||||
|
<div className={styles.cardThumb} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--color-text-disabled)', fontSize: 12 }}>暂无图片</div>
|
||||||
|
) : (
|
||||||
<img src={tosThumb(group.thumbnail_url, 300)} alt={group.name} className={styles.cardThumb} />
|
<img src={tosThumb(group.thumbnail_url, 300)} alt={group.name} className={styles.cardThumb} />
|
||||||
|
)}
|
||||||
<div className={styles.cardInfo}>
|
<div className={styles.cardInfo}>
|
||||||
{editingName && editingName.id === group.id ? (
|
{editingName && editingName.id === group.id ? (
|
||||||
<div className={styles.inlineEditWrap} onClick={(e) => e.stopPropagation()}>
|
<div className={styles.inlineEditWrap} onClick={(e) => e.stopPropagation()}>
|
||||||
@ -406,6 +410,7 @@ export function AssetLibraryModal({ open, onClose }: Props) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className={styles.dropZoneWarning}>⚠️ 素材上传后无法删除,请确认后再上传</div>
|
<div className={styles.dropZoneWarning}>⚠️ 素材上传后无法删除,请确认后再上传</div>
|
||||||
|
<div className={styles.dropZoneWarning}>⚠️ 图片尺寸要求:宽高均需在 300~6000 像素之间</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
|
|||||||
@ -125,6 +125,9 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: #2a2a3a;
|
background: #2a2a3a;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbMedia {
|
.thumbMedia {
|
||||||
|
|||||||
@ -271,8 +271,8 @@ export function PromptInput() {
|
|||||||
typedAtRef.current = true;
|
typedAtRef.current = true;
|
||||||
setMentionMode('references');
|
setMentionMode('references');
|
||||||
openMentionPopup();
|
openMentionPopup();
|
||||||
} else if (/[\u4e00-\u9fff]+/.test(textAfterAt) && !textAfterAt.includes(' ')) {
|
} else if (textAfterAt.length > 0 && !textAfterAt.includes(' ')) {
|
||||||
// Chinese text after @, search assets
|
// Text after @, search assets (Chinese + English)
|
||||||
if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
|
if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
|
||||||
searchTimerRef.current = setTimeout(() => {
|
searchTimerRef.current = setTimeout(() => {
|
||||||
assetsApi.search(textAfterAt).then((res) => {
|
assetsApi.search(textAfterAt).then((res) => {
|
||||||
@ -641,7 +641,11 @@ export function PromptInput() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={styles.mentionThumb}>
|
<div className={styles.mentionThumb}>
|
||||||
|
{group.thumbnail_url ? (
|
||||||
<img src={tosThumb(group.thumbnail_url, 72)} alt="" className={styles.thumbMedia} />
|
<img src={tosThumb(group.thumbnail_url, 72)} alt="" className={styles.thumbMedia} />
|
||||||
|
) : (
|
||||||
|
<span style={{ fontSize: 9, color: 'var(--color-text-disabled)' }}>无图</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className={styles.mentionLabel}>{group.name}</span>
|
<span className={styles.mentionLabel}>{group.name}</span>
|
||||||
<span className={styles.mentionType}>人像</span>
|
<span className={styles.mentionType}>人像</span>
|
||||||
|
|||||||
@ -3,6 +3,8 @@
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 24px 20px 60px;
|
padding: 24px 20px 60px;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
height: 100vh;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header */
|
/* Header */
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user