feat: v0.13.0 主副管理员 + 素材引用 bug 修复 + admin 保护
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 17m15s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 17m15s
【主副管理员】
①User 加 is_team_owner 字段,现有团管自动升为主管
②主管可指定/取消副管理员,副管不能再指定别人
③副管不能禁用/修改其他管理员
④超管团队详情支持三种角色显示和切换
【素材引用 bug 修复】
⑤span.replaceWith('') → span.remove(),删除引用后标签真正移除
⑥switchMode 时清空 assetMentions,切换模式不带旧素材
⑦fallback 只在纯文本时生效,用户删标签后不再偷偷加回
⑧后端跳过未解析的 asset:// URL,不发给火山 API
【admin 保护】
⑨admin 账号不可被任何人禁用
⑩admin 密码不可被其他超管重置
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f4255a04ee
commit
727be720b4
18
backend/apps/accounts/migrations/0013_user_is_team_owner.py
Normal file
18
backend/apps/accounts/migrations/0013_user_is_team_owner.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.29 on 2026-03-24 03:34
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0012_user_last_read_announcement'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='is_team_owner',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='团队主管理员'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 4.2.29 on 2026-03-24 03:34
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def set_admins_as_owners(apps, schema_editor):
|
||||||
|
User = apps.get_model('accounts', 'User')
|
||||||
|
User.objects.filter(is_team_admin=True).update(is_team_owner=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0013_user_is_team_owner'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(set_admins_as_owners, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
@ -51,6 +51,7 @@ class User(AbstractUser):
|
|||||||
verbose_name='所属团队',
|
verbose_name='所属团队',
|
||||||
)
|
)
|
||||||
is_team_admin = models.BooleanField(default=False, verbose_name='团队管理员')
|
is_team_admin = models.BooleanField(default=False, verbose_name='团队管理员')
|
||||||
|
is_team_owner = models.BooleanField(default=False, verbose_name='团队主管理员')
|
||||||
daily_seconds_limit = models.IntegerField(default=600, verbose_name='每日秒数上限')
|
daily_seconds_limit = models.IntegerField(default=600, verbose_name='每日秒数上限')
|
||||||
monthly_seconds_limit = models.IntegerField(default=6000, verbose_name='每月秒数上限')
|
monthly_seconds_limit = models.IntegerField(default=6000, verbose_name='每月秒数上限')
|
||||||
# ── 次数限额(v0.10.0 新增) ──
|
# ── 次数限额(v0.10.0 新增) ──
|
||||||
|
|||||||
@ -11,7 +11,7 @@ class UserSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
fields = ('id', 'username', 'email', 'is_staff', 'is_team_admin', 'role', 'team_name', 'must_change_password')
|
fields = ('id', 'username', 'email', 'is_staff', 'is_team_admin', 'is_team_owner', 'role', 'team_name', 'must_change_password')
|
||||||
|
|
||||||
|
|
||||||
class RegisterSerializer(serializers.Serializer):
|
class RegisterSerializer(serializers.Serializer):
|
||||||
|
|||||||
@ -61,6 +61,7 @@ urlpatterns = [
|
|||||||
path('team/members/<int:member_id>', views.team_member_detail_view, name='team_member_detail'),
|
path('team/members/<int:member_id>', views.team_member_detail_view, name='team_member_detail'),
|
||||||
path('team/members/<int:member_id>/quota', views.team_member_quota_view, name='team_member_quota'),
|
path('team/members/<int:member_id>/quota', views.team_member_quota_view, name='team_member_quota'),
|
||||||
path('team/members/<int:member_id>/status', views.team_member_status_view, name='team_member_status'),
|
path('team/members/<int:member_id>/status', views.team_member_status_view, name='team_member_status'),
|
||||||
|
path('team/members/<int:member_id>/role', views.team_member_role_view, name='team_member_role'),
|
||||||
|
|
||||||
# ── Team Admin: Consumption Records ──
|
# ── Team Admin: Consumption Records ──
|
||||||
path('team/records', views.team_records_view, name='team_records'),
|
path('team/records', views.team_records_view, name='team_records'),
|
||||||
|
|||||||
@ -299,6 +299,11 @@ def video_generate_view(request):
|
|||||||
except (ValueError, Exception) as e:
|
except (ValueError, Exception) as e:
|
||||||
logger.warning('Failed to resolve asset group URL %s: %s', url, e)
|
logger.warning('Failed to resolve asset group URL %s: %s', url, e)
|
||||||
|
|
||||||
|
# 未解析成功的 asset URL 不发给火山 API(会导致 InvalidParameter)
|
||||||
|
if resolved_url.startswith('asset://'):
|
||||||
|
logger.warning('Skipping unresolved asset URL: %s', resolved_url)
|
||||||
|
continue
|
||||||
|
|
||||||
if ref_type == 'image':
|
if ref_type == 'image':
|
||||||
item = {'type': 'image_url', 'image_url': {'url': resolved_url}}
|
item = {'type': 'image_url', 'image_url': {'url': resolved_url}}
|
||||||
if role:
|
if role:
|
||||||
@ -1001,6 +1006,7 @@ def admin_team_detail_view(request, team_id):
|
|||||||
'username': m.username,
|
'username': m.username,
|
||||||
'email': m.email,
|
'email': m.email,
|
||||||
'is_team_admin': m.is_team_admin,
|
'is_team_admin': m.is_team_admin,
|
||||||
|
'is_team_owner': m.is_team_owner,
|
||||||
'is_active': m.is_active,
|
'is_active': m.is_active,
|
||||||
'disabled_by': m.disabled_by,
|
'disabled_by': m.disabled_by,
|
||||||
'daily_seconds_limit': m.daily_seconds_limit,
|
'daily_seconds_limit': m.daily_seconds_limit,
|
||||||
@ -1033,19 +1039,35 @@ def admin_team_member_role_view(request, team_id, member_id):
|
|||||||
return Response({'error': '成员不存在'}, status=status.HTTP_404_NOT_FOUND)
|
return Response({'error': '成员不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
is_admin = request.data.get('is_team_admin')
|
is_admin = request.data.get('is_team_admin')
|
||||||
if is_admin is None:
|
is_owner = request.data.get('is_team_owner')
|
||||||
return Response({'error': '请提供 is_team_admin 参数'}, status=status.HTTP_400_BAD_REQUEST)
|
if is_admin is None and is_owner is None:
|
||||||
|
return Response({'error': '请提供 is_team_admin 或 is_team_owner 参数'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
before = {'is_team_admin': member.is_team_admin}
|
before = {'is_team_admin': member.is_team_admin, 'is_team_owner': member.is_team_owner}
|
||||||
member.is_team_admin = bool(is_admin)
|
update_fields = []
|
||||||
member.save(update_fields=['is_team_admin'])
|
if is_admin is not None:
|
||||||
|
member.is_team_admin = bool(is_admin)
|
||||||
|
update_fields.append('is_team_admin')
|
||||||
|
# 取消管理员时同时取消主管
|
||||||
|
if not bool(is_admin):
|
||||||
|
member.is_team_owner = False
|
||||||
|
update_fields.append('is_team_owner')
|
||||||
|
if is_owner is not None:
|
||||||
|
member.is_team_owner = bool(is_owner)
|
||||||
|
if bool(is_owner):
|
||||||
|
member.is_team_admin = True # 主管一定是管理员
|
||||||
|
update_fields.append('is_team_admin')
|
||||||
|
if 'is_team_owner' not in update_fields:
|
||||||
|
update_fields.append('is_team_owner')
|
||||||
|
member.save(update_fields=update_fields)
|
||||||
log_admin_action(request, 'team_update', 'user', target_id=member.id, target_name=member.username,
|
log_admin_action(request, 'team_update', 'user', target_id=member.id, target_name=member.username,
|
||||||
before=before, after={'is_team_admin': member.is_team_admin})
|
before=before, after={'is_team_admin': member.is_team_admin, 'is_team_owner': member.is_team_owner})
|
||||||
|
|
||||||
return Response({
|
return Response({
|
||||||
'user_id': member.id,
|
'user_id': member.id,
|
||||||
'username': member.username,
|
'username': member.username,
|
||||||
'is_team_admin': member.is_team_admin,
|
'is_team_admin': member.is_team_admin,
|
||||||
|
'is_team_owner': member.is_team_owner,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -1195,6 +1217,7 @@ def admin_team_create_admin_view(request, team_id):
|
|||||||
password=serializer.validated_data['password'],
|
password=serializer.validated_data['password'],
|
||||||
team=team,
|
team=team,
|
||||||
is_team_admin=True,
|
is_team_admin=True,
|
||||||
|
is_team_owner=True,
|
||||||
daily_seconds_limit=team.daily_member_limit_default,
|
daily_seconds_limit=team.daily_member_limit_default,
|
||||||
monthly_seconds_limit=-1, # Team admin unlimited by default
|
monthly_seconds_limit=-1, # Team admin unlimited by default
|
||||||
daily_generation_limit=-1, # Team admin unlimited by default
|
daily_generation_limit=-1, # Team admin unlimited by default
|
||||||
@ -2116,6 +2139,7 @@ def team_members_list_view(request):
|
|||||||
'username': m.username,
|
'username': m.username,
|
||||||
'email': m.email,
|
'email': m.email,
|
||||||
'is_team_admin': m.is_team_admin,
|
'is_team_admin': m.is_team_admin,
|
||||||
|
'is_team_owner': m.is_team_owner,
|
||||||
'is_active': m.is_active,
|
'is_active': m.is_active,
|
||||||
'daily_seconds_limit': m.daily_seconds_limit,
|
'daily_seconds_limit': m.daily_seconds_limit,
|
||||||
'monthly_seconds_limit': m.monthly_seconds_limit,
|
'monthly_seconds_limit': m.monthly_seconds_limit,
|
||||||
@ -2268,6 +2292,10 @@ def team_member_quota_view(request, member_id):
|
|||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
return Response({'error': '成员不存在'}, status=status.HTTP_404_NOT_FOUND)
|
return Response({'error': '成员不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
# 副管不能修改主管或其他副管的额度
|
||||||
|
if not request.user.is_team_owner and member.is_team_admin:
|
||||||
|
return Response({'error': '副管理员不能修改其他管理员的额度'}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
serializer = MemberQuotaSerializer(data=request.data)
|
serializer = MemberQuotaSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
@ -2312,9 +2340,15 @@ def team_member_status_view(request, member_id):
|
|||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
return Response({'error': '成员不存在'}, status=status.HTTP_404_NOT_FOUND)
|
return Response({'error': '成员不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
# Cannot disable yourself or other team admins
|
# Cannot disable yourself
|
||||||
if member.id == request.user.id:
|
if member.id == request.user.id:
|
||||||
return Response({'error': '不能停用自己的账号'}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'error': '不能停用自己的账号'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
# 副管不能禁用主管或其他副管
|
||||||
|
if not request.user.is_team_owner and member.is_team_admin:
|
||||||
|
return Response({'error': '副管理员不能操作其他管理员'}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
# 主管不能被副管禁用
|
||||||
|
if member.is_team_owner and not request.user.is_team_owner:
|
||||||
|
return Response({'error': '不能停用主管理员'}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
serializer = UserStatusSerializer(data=request.data)
|
serializer = UserStatusSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
@ -2332,6 +2366,41 @@ def team_member_status_view(request, member_id):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(['PATCH'])
|
||||||
|
@permission_classes([IsTeamAdmin])
|
||||||
|
def team_member_role_view(request, member_id):
|
||||||
|
"""PATCH /api/v1/team/members/<id>/role — Owner toggles vice admin."""
|
||||||
|
if not request.user.is_team_owner:
|
||||||
|
return Response({'error': '只有主管理员可以设置副管理员'}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
team = request.user.team
|
||||||
|
try:
|
||||||
|
member = team.members.get(id=member_id)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
return Response({'error': '成员不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
if member.id == request.user.id:
|
||||||
|
return Response({'error': '不能修改自己的角色'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
if member.is_team_owner:
|
||||||
|
return Response({'error': '不能修改主管理员的角色'}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
is_admin = request.data.get('is_team_admin')
|
||||||
|
if is_admin is None:
|
||||||
|
return Response({'error': '请提供 is_team_admin 参数'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
before = {'is_team_admin': member.is_team_admin}
|
||||||
|
member.is_team_admin = bool(is_admin)
|
||||||
|
member.save(update_fields=['is_team_admin'])
|
||||||
|
log_admin_action(request, 'member_quota_update', 'user', target_id=member.id, target_name=member.username,
|
||||||
|
before=before, after={'is_team_admin': member.is_team_admin})
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'user_id': member.id,
|
||||||
|
'username': member.username,
|
||||||
|
'is_team_admin': member.is_team_admin,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
# Profile: User's own consumption data
|
# Profile: User's own consumption data
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
|
|||||||
@ -230,7 +230,7 @@ export function PromptInput() {
|
|||||||
spans.forEach((span) => {
|
spans.forEach((span) => {
|
||||||
if (span.dataset.refType === 'asset') return; // skip asset mentions
|
if (span.dataset.refType === 'asset') return; // skip asset mentions
|
||||||
if (!refIds.has(span.dataset.refId!)) {
|
if (!refIds.has(span.dataset.refId!)) {
|
||||||
span.replaceWith('');
|
span.remove();
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -353,6 +353,9 @@ export const teamApi = {
|
|||||||
updateMemberStatus: (memberId: number, isActive: boolean) =>
|
updateMemberStatus: (memberId: number, isActive: boolean) =>
|
||||||
api.patch(`/team/members/${memberId}/status`, { is_active: isActive }),
|
api.patch(`/team/members/${memberId}/status`, { is_active: isActive }),
|
||||||
|
|
||||||
|
setMemberRole: (memberId: number, isTeamAdmin: boolean) =>
|
||||||
|
api.patch(`/team/members/${memberId}/role`, { is_team_admin: isTeamAdmin }),
|
||||||
|
|
||||||
// Content Assets
|
// Content Assets
|
||||||
getAssetsOverview: () =>
|
getAssetsOverview: () =>
|
||||||
api.get<{
|
api.get<{
|
||||||
|
|||||||
@ -3,9 +3,11 @@ import { teamApi } from '../lib/api';
|
|||||||
import type { TeamMember } from '../types';
|
import type { TeamMember } from '../types';
|
||||||
import { showToast } from '../components/Toast';
|
import { showToast } from '../components/Toast';
|
||||||
import { ConfirmModal } from '../components/ConfirmModal';
|
import { ConfirmModal } from '../components/ConfirmModal';
|
||||||
|
import { useAuthStore } from '../store/auth';
|
||||||
import styles from './UsersPage.module.css';
|
import styles from './UsersPage.module.css';
|
||||||
|
|
||||||
export function TeamMembersPage() {
|
export function TeamMembersPage() {
|
||||||
|
const currentUser = useAuthStore((s) => s.user);
|
||||||
const [members, setMembers] = useState<TeamMember[]>([]);
|
const [members, setMembers] = useState<TeamMember[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
@ -150,8 +152,10 @@ export function TeamMembersPage() {
|
|||||||
{m.username}
|
{m.username}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{m.is_team_admin ? (
|
{m.is_team_owner ? (
|
||||||
<span className={styles.statusBadge} style={{ background: 'rgba(108, 99, 255, 0.15)', color: '#6c63ff' }}>管理员</span>
|
<span className={styles.statusBadge} style={{ background: 'rgba(0, 184, 230, 0.15)', color: '#00b8e6' }}>主管理员</span>
|
||||||
|
) : m.is_team_admin ? (
|
||||||
|
<span className={styles.statusBadge} style={{ background: 'rgba(167, 139, 250, 0.15)', color: '#a78bfa' }}>副管理员</span>
|
||||||
) : (
|
) : (
|
||||||
<span style={{ color: 'var(--color-text-secondary)', fontSize: 12 }}>成员</span>
|
<span style={{ color: 'var(--color-text-secondary)', fontSize: 12 }}>成员</span>
|
||||||
)}
|
)}
|
||||||
@ -169,6 +173,17 @@ export function TeamMembersPage() {
|
|||||||
<td>
|
<td>
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
<button className={styles.editBtn} onClick={() => openEditModal(m)}>编辑配额</button>
|
<button className={styles.editBtn} onClick={() => openEditModal(m)}>编辑配额</button>
|
||||||
|
{currentUser?.is_team_owner && !m.is_team_owner && (
|
||||||
|
m.is_team_admin ? (
|
||||||
|
<button className={styles.editBtn} onClick={async () => {
|
||||||
|
try { await teamApi.setMemberRole(m.id, false); showToast('已取消副管理员'); fetchMembers(); } catch { showToast('操作失败'); }
|
||||||
|
}}>取消副管理员</button>
|
||||||
|
) : (
|
||||||
|
<button className={styles.editBtn} onClick={async () => {
|
||||||
|
try { await teamApi.setMemberRole(m.id, true); showToast('已设为副管理员'); fetchMembers(); } catch { showToast('操作失败'); }
|
||||||
|
}}>设为副管理员</button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
className={`${styles.toggleBtn} ${m.is_active ? styles.disableBtn : styles.enableBtn}`}
|
className={`${styles.toggleBtn} ${m.is_active ? styles.disableBtn : styles.enableBtn}`}
|
||||||
onClick={() => setConfirmMember(m)}
|
onClick={() => setConfirmMember(m)}
|
||||||
|
|||||||
@ -257,6 +257,15 @@
|
|||||||
background: rgba(255, 255, 255, 0.04);
|
background: rgba(255, 255, 255, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ownerBadge {
|
||||||
|
background: rgba(0, 184, 230, 0.15);
|
||||||
|
color: var(--color-primary, #00b8e6);
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.adminBadge {
|
.adminBadge {
|
||||||
background: rgba(167, 139, 250, 0.15);
|
background: rgba(167, 139, 250, 0.15);
|
||||||
color: #a78bfa;
|
color: #a78bfa;
|
||||||
|
|||||||
@ -822,19 +822,21 @@ export function TeamsPage() {
|
|||||||
</td>
|
</td>
|
||||||
<td>{m.email}</td>
|
<td>{m.email}</td>
|
||||||
<td>
|
<td>
|
||||||
{m.is_team_admin ? (
|
{m.is_team_owner ? (
|
||||||
<span className={styles.adminBadge} style={{ cursor: 'pointer' }} title="点击取消管理员" onClick={async () => {
|
<span className={styles.ownerBadge}>主管理员</span>
|
||||||
|
) : m.is_team_admin ? (
|
||||||
|
<span className={styles.adminBadge} style={{ cursor: 'pointer' }} title="点击取消副管理员" onClick={async () => {
|
||||||
try {
|
try {
|
||||||
await adminApi.setMemberRole(detailTeam!.id, m.id, false);
|
await adminApi.setMemberRole(detailTeam!.id, m.id, false);
|
||||||
showToast('已取消管理员');
|
showToast('已取消副管理员');
|
||||||
const { data: refreshed } = await adminApi.getTeamDetail(detailTeam!.id); setDetailTeam(refreshed);
|
const { data: refreshed } = await adminApi.getTeamDetail(detailTeam!.id); setDetailTeam(refreshed);
|
||||||
} catch { showToast('操作失败'); }
|
} catch { showToast('操作失败'); }
|
||||||
}}>管理员</span>
|
}}>副管理员</span>
|
||||||
) : (
|
) : (
|
||||||
<span style={{ cursor: 'pointer', color: 'var(--color-text-secondary)' }} title="点击设为管理员" onClick={async () => {
|
<span style={{ cursor: 'pointer', color: 'var(--color-text-secondary)' }} title="点击设为副管理员" onClick={async () => {
|
||||||
try {
|
try {
|
||||||
await adminApi.setMemberRole(detailTeam!.id, m.id, true);
|
await adminApi.setMemberRole(detailTeam!.id, m.id, true);
|
||||||
showToast('已设为管理员');
|
showToast('已设为副管理员');
|
||||||
const { data: refreshed } = await adminApi.getTeamDetail(detailTeam!.id); setDetailTeam(refreshed);
|
const { data: refreshed } = await adminApi.getTeamDetail(detailTeam!.id); setDetailTeam(refreshed);
|
||||||
} catch { showToast('操作失败'); }
|
} catch { showToast('操作失败'); }
|
||||||
}}>成员</span>
|
}}>成员</span>
|
||||||
|
|||||||
@ -429,18 +429,23 @@ export const useGenerationStore = create<GenerationState>((set, get) => ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: also add from inputBar assetMentions (for regenerate scenario)
|
// Fallback: only use inputBar assetMentions when editorHtml has NO asset spans
|
||||||
const inputAssetMentions = input.assetMentions || [];
|
// (regenerate scenario where editorHtml is plain text)
|
||||||
for (const am of inputAssetMentions) {
|
// If user edited the HTML and removed some asset tags, respect that — don't re-add from store
|
||||||
if (am.groupId && !seenGroupIds.has(am.groupId)) {
|
const htmlHadAssetSpans = input.editorHtml?.includes('data-ref-type="asset"');
|
||||||
seenGroupIds.add(am.groupId);
|
if (!htmlHadAssetSpans) {
|
||||||
uploadedRefs.push({
|
const inputAssetMentions = input.assetMentions || [];
|
||||||
url: `asset://group-${am.groupId}`,
|
for (const am of inputAssetMentions) {
|
||||||
type: 'image',
|
if (am.groupId && !seenGroupIds.has(am.groupId)) {
|
||||||
role: 'reference_image',
|
seenGroupIds.add(am.groupId);
|
||||||
label: am.label,
|
uploadedRefs.push({
|
||||||
thumb_url: am.thumbUrl || '',
|
url: `asset://group-${am.groupId}`,
|
||||||
});
|
type: 'image',
|
||||||
|
role: 'reference_image',
|
||||||
|
label: am.label,
|
||||||
|
thumb_url: am.thumbUrl || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -237,6 +237,7 @@ export const useInputBarStore = create<InputBarState>((set, get) => ({
|
|||||||
mode,
|
mode,
|
||||||
prevReferences: state.references,
|
prevReferences: state.references,
|
||||||
references: [],
|
references: [],
|
||||||
|
assetMentions: [],
|
||||||
aspectRatio: '16:9',
|
aspectRatio: '16:9',
|
||||||
duration: 5,
|
duration: 5,
|
||||||
});
|
});
|
||||||
@ -246,6 +247,7 @@ export const useInputBarStore = create<InputBarState>((set, get) => ({
|
|||||||
if (state.lastFrame) URL.revokeObjectURL(state.lastFrame.previewUrl);
|
if (state.lastFrame) URL.revokeObjectURL(state.lastFrame.previewUrl);
|
||||||
set({
|
set({
|
||||||
mode,
|
mode,
|
||||||
|
assetMentions: [],
|
||||||
firstFrame: null,
|
firstFrame: null,
|
||||||
lastFrame: null,
|
lastFrame: null,
|
||||||
references: state.prevReferences,
|
references: state.prevReferences,
|
||||||
|
|||||||
@ -82,6 +82,7 @@ export interface User {
|
|||||||
email: string;
|
email: string;
|
||||||
is_staff: boolean;
|
is_staff: boolean;
|
||||||
is_team_admin: boolean;
|
is_team_admin: boolean;
|
||||||
|
is_team_owner?: boolean;
|
||||||
role: UserRole;
|
role: UserRole;
|
||||||
team_name: string | null;
|
team_name: string | null;
|
||||||
must_change_password: boolean;
|
must_change_password: boolean;
|
||||||
@ -153,6 +154,7 @@ export interface AdminUser {
|
|||||||
disabled_by: string;
|
disabled_by: string;
|
||||||
is_staff: boolean;
|
is_staff: boolean;
|
||||||
is_team_admin: boolean;
|
is_team_admin: boolean;
|
||||||
|
is_team_owner?: boolean;
|
||||||
team_id: number | null;
|
team_id: number | null;
|
||||||
team_name: string | null;
|
team_name: string | null;
|
||||||
date_joined: string;
|
date_joined: string;
|
||||||
@ -312,6 +314,7 @@ export interface TeamMember {
|
|||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
is_team_admin: boolean;
|
is_team_admin: boolean;
|
||||||
|
is_team_owner?: boolean;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
disabled_by: string;
|
disabled_by: string;
|
||||||
daily_seconds_limit: number;
|
daily_seconds_limit: number;
|
||||||
@ -369,6 +372,7 @@ export interface AssetMemberSummary {
|
|||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
is_team_admin: boolean;
|
is_team_admin: boolean;
|
||||||
|
is_team_owner?: boolean;
|
||||||
video_count: number;
|
video_count: number;
|
||||||
seconds_consumed: number;
|
seconds_consumed: number;
|
||||||
cost_consumed?: number;
|
cost_consumed?: number;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user