Skip to content

Commit 775cc6a

Browse files
MingTianSangHongzhi Wenclaude
authored
为订阅内容卡片增加“加入角色卡”按钮,支持从单个 Steam 创意工坊订阅中手动恢复/添加角色卡 (#1523)
* 为订阅内容卡片增加“加入角色卡”按钮,支持从单个 Steam 创意工坊订阅中手动恢复/添加角色卡 * _sync_result() 现在无论是否 target sync 都会带上 code loadCharacterCards() 刷新失败单独提示“刷新列表失败”,不再误报为“加入角色卡失败” * 已存在但仍带 tombstone 的角色,现在手动“加入角色卡”会清理 tombstone,并作为恢复成功返回,不再误走 409 * restored_existing_candidates 不再只按名字清 tombstone;现在只清理已确认属于当前 Workshop item 的同名角色 * fix(workshop): _is_matching_workshop_character 也按 avatar 绑定判定归属 恢复路径的归属判定原本只看 character_origin.source_id,但退订确认路径 (_is_confirmed_workshop_character) 已同时把 avatar.asset_source_id 当作 Workshop 归属依据。旧数据/半迁移数据可能只有 avatar 绑定(live2d_item_id 迁移只写 avatar.asset_source_id;用户在模型设置里手动绑定 Workshop 模型时也只写 avatar.*), 这类角色会被退订按 avatar 命中删除并打 tombstone,却无法被恢复路径识别,导致 tombstone 永远清不掉、/sync-character/{item_id} 一直回 409。两边判定收口为一致。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(workshop): 同步内部异常显式回 500,不再伪装成业务态 sync_workshop_character_cards 的外层 except 原本只 log + error_count+=1 后 返回普通 _sync_result()(无 code),下游 /sync-character/{item_id} 与 /sync-characters 只按业务 code 分支,导致真实后端异常被误判成 WORKSHOP_CHARACTER_NOT_FOUND / NOT_ADDED,前端把服务端故障当成 "此订阅里没有角色卡"。 外层 except 改为返回专属 code WORKSHOP_SYNC_FAILED(区别于逐角色的部分错误, 后者仍走原 error_count 累加),两个入口都补 500 映射。前端 !response.ok 分支 会把后端 error 串接进通用失败文案,无需新增 i18n key。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(workshop): 恢复成功也回写 added_character_names,前端提示不丢名字 /sync-character/{item_id} 成功分支用 successful_names(added + restored 去重) 拼 message,但响应只 **result,added_character_names 在“仅清 tombstone 恢复成功” 场景仍为空。前端成功提示只读 added_character_names,会被 formatWorkshopCharacterNameList 回退成“未知角色卡”。把 successful_names 回写到 added_character_names,restored_deleted_names 仍保留。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Hongzhi Wen <cartabio.coder1@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a568b28 commit 775cc6a

13 files changed

Lines changed: 1226 additions & 50 deletions

main_routers/workshop_router.py

Lines changed: 386 additions & 42 deletions
Large diffs are not rendered by default.

static/css/character_card_manager.css

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2675,11 +2675,13 @@ line-height: 1.4;
26752675

26762676
/* 卡片操作按钮 - workshop-card专用(按钮区域) */
26772677
.workshop-card .card-actions {
2678-
background-color: #bcf1ff;
2679-
padding: 6px;
2680-
border-radius: 50px;
2678+
background: transparent;
2679+
padding: 0;
2680+
border-radius: 0;
26812681
margin: auto 8px 0 8px;
2682-
display: block;
2682+
display: flex;
2683+
flex-direction: column;
2684+
gap: 18px;
26832685
}
26842686

26852687
.workshop-card .card-actions button,
@@ -2693,7 +2695,9 @@ line-height: 1.4;
26932695
font-weight: 900;
26942696
letter-spacing: 2px;
26952697
padding: 14px 0;
2698+
margin: 0 !important;
26962699
cursor: pointer;
2700+
box-shadow: 0 0 0 6px #bcf1ff;
26972701
text-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1);
26982702
transition: filter 0.2s;
26992703
}
@@ -2702,7 +2706,7 @@ line-height: 1.4;
27022706
.workshop-card .card-actions .button:hover {
27032707
filter: brightness(1.05);
27042708
transform: none;
2705-
box-shadow: none;
2709+
box-shadow: 0 0 0 6px #bcf1ff;
27062710
}
27072711

27082712
/* 已发布/禁用按钮 - 非交互外观 */
@@ -2712,13 +2716,14 @@ line-height: 1.4;
27122716
color: rgba(255, 255, 255, 0.8);
27132717
cursor: not-allowed;
27142718
opacity: 0.7;
2719+
box-shadow: 0 0 0 6px #bcf1ff;
27152720
}
27162721

27172722
.workshop-card .card-actions .button-disabled:hover,
27182723
.workshop-card .card-actions button[disabled]:hover {
27192724
filter: none;
27202725
transform: none;
2721-
box-shadow: none;
2726+
box-shadow: 0 0 0 6px #bcf1ff;
27222727
}
27232728

27242729
/* 卡片操作按钮 */
@@ -7043,12 +7048,28 @@ margin-top: 10px;
70437048
color: #93c5fd;
70447049
}
70457050

7046-
[data-theme="dark"] .workshop-card .card-path,
7047-
[data-theme="dark"] .workshop-card .card-actions {
7051+
[data-theme="dark"] .workshop-card .card-path {
70487052
background: rgba(15, 23, 42, 0.42);
70497053
border: 1px solid rgba(125, 211, 252, 0.18);
70507054
}
70517055

7056+
/* 订阅卡操作区保持透明,按钮各自保留独立底板。 */
7057+
[data-theme="dark"] .workshop-card .card-actions {
7058+
background: transparent;
7059+
border: 0;
7060+
}
7061+
7062+
[data-theme="dark"] .workshop-card .card-actions button,
7063+
[data-theme="dark"] .workshop-card .card-actions .button,
7064+
[data-theme="dark"] .workshop-card .card-actions button:hover,
7065+
[data-theme="dark"] .workshop-card .card-actions .button:hover,
7066+
[data-theme="dark"] .workshop-card .card-actions .button-disabled,
7067+
[data-theme="dark"] .workshop-card .card-actions button[disabled],
7068+
[data-theme="dark"] .workshop-card .card-actions .button-disabled:hover,
7069+
[data-theme="dark"] .workshop-card .card-actions button[disabled]:hover {
7070+
box-shadow: 0 0 0 6px rgba(15, 23, 42, 0.42);
7071+
}
7072+
70527073
[data-theme="dark"] .card-info-body,
70537074
[data-theme="dark"] #card-info-preview,
70547075
[data-theme="dark"] #card-info-dynamic-content,

static/js/character_card_manager.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2064,6 +2064,7 @@ function renderSubscriptionsPage() {
20642064
<button class="button button-primary" onclick="openWorkshopVoiceClone('${formattedItem.id}')" title="${formattedItem.voiceReferenceDisplayName || ''}" style="margin-bottom: 8px;">
20652065
${window.t ? window.t('steam.openVoiceClone') : '在语音克隆页打开'}
20662066
</button>` : ''}
2067+
<button class="button button-primary" data-item-id="${formattedItem.id}" data-item-name="${formattedItem.name}" onclick="addWorkshopCharacterCardFromSubscription(this)" style="margin-bottom: 8px;">${window.t ? window.t('steam.workshopAddCharacterCard') : '加入角色卡'}</button>
20672068
<button class="button button-danger" data-item-id="${formattedItem.id}" data-item-name="${formattedItem.name}" onclick="unsubscribeItem(this.dataset.itemId, this.dataset.itemName)">${window.t ? window.t('steam.unsubscribe') : '取消订阅'}</button>
20682069
</div>
20692070
</div>
@@ -2072,6 +2073,94 @@ function renderSubscriptionsPage() {
20722073
}).join('');
20732074
}
20742075

2076+
function formatWorkshopCharacterNameList(names) {
2077+
const list = Array.isArray(names)
2078+
? names.map(name => String(name || '').trim()).filter(Boolean)
2079+
: [];
2080+
return list.length > 0 ? list.join('、') : (window.t ? window.t('steam.unknownCharacterCard') : '未知角色卡');
2081+
}
2082+
2083+
async function showWorkshopCharacterAddAlert(message, type = 'info') {
2084+
if (typeof showAlertDialog === 'function') {
2085+
const title = type === 'info'
2086+
? (window.t ? window.t('steam.characterCardAlreadyExistsTitle') : '角色卡已存在')
2087+
: (window.t ? window.t('common.warning') : '提示');
2088+
await showAlertDialog(message, {
2089+
type,
2090+
title,
2091+
});
2092+
return;
2093+
}
2094+
window.alert(message);
2095+
}
2096+
2097+
async function addWorkshopCharacterCardFromSubscription(button) {
2098+
const itemId = button?.dataset?.itemId || '';
2099+
if (!itemId) return;
2100+
2101+
const originalText = button.textContent;
2102+
button.disabled = true;
2103+
button.textContent = window.t ? window.t('steam.workshopAddingCharacterCard') : '正在加入...';
2104+
2105+
try {
2106+
const response = await fetch(`/api/steam/workshop/sync-character/${encodeURIComponent(itemId)}`, {
2107+
method: 'POST',
2108+
});
2109+
let data = {};
2110+
try {
2111+
data = await response.json();
2112+
} catch (_) {
2113+
data = {};
2114+
}
2115+
2116+
if (data.code === 'WORKSHOP_CHARACTER_ALREADY_EXISTS') {
2117+
const namesText = formatWorkshopCharacterNameList(data.existing_character_names);
2118+
const message = window.t
2119+
? window.t('steam.characterCardAlreadyExistsMessage', { names: namesText })
2120+
: `角色卡已存在:${namesText}`;
2121+
await showWorkshopCharacterAddAlert(message, 'info');
2122+
return;
2123+
}
2124+
2125+
if (!response.ok || !data.success) {
2126+
const fallbackError = data.error || data.message || (window.t ? window.t('common.unknownError') : 'Unknown error');
2127+
const key = data.code === 'WORKSHOP_CHARACTER_NOT_FOUND'
2128+
? 'steam.workshopCharacterNotFound'
2129+
: 'steam.workshopCharacterAddFailed';
2130+
const message = window.t
2131+
? window.t(key, { error: fallbackError })
2132+
: (data.code === 'WORKSHOP_CHARACTER_NOT_FOUND'
2133+
? '此订阅内容中未找到可加入的角色卡,请确认内容已下载完成。'
2134+
: `加入角色卡失败: ${fallbackError}`);
2135+
await showWorkshopCharacterAddAlert(message, 'warning');
2136+
return;
2137+
}
2138+
2139+
const namesText = formatWorkshopCharacterNameList(data.added_character_names);
2140+
const successMessage = window.t
2141+
? window.t('steam.workshopCharacterAdded', { names: namesText })
2142+
: `已加入角色卡:${namesText}`;
2143+
showMessage(successMessage, 'success');
2144+
try {
2145+
await loadCharacterCards();
2146+
} catch (refreshError) {
2147+
console.warn('刷新角色卡列表失败:', refreshError);
2148+
const refreshMessage = window.t
2149+
? window.t('steam.characterCardsRefreshFailed', { error: refreshError.message })
2150+
: `刷新列表失败: ${refreshError.message}`;
2151+
showMessage(refreshMessage, 'warning');
2152+
}
2153+
} catch (error) {
2154+
const message = window.t
2155+
? window.t('steam.workshopCharacterAddFailed', { error: error.message })
2156+
: `加入角色卡失败: ${error.message}`;
2157+
showMessage(message, 'error');
2158+
} finally {
2159+
button.disabled = false;
2160+
button.textContent = originalText;
2161+
}
2162+
}
2163+
20752164
// 更新分页控件
20762165
function updatePagination() {
20772166
const pagination = document.querySelector('.pagination');

static/locales/en.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,14 @@
208208
"uploadReady": "Ready to upload {{itemName}}",
209209
"pageDescription": "From this page you can browse, edit, upload, import and export character cards, and manage Live2D models and voices hosted on the Steam Workshop.",
210210
"openVoiceClone": "Open in Voice Clone",
211+
"workshopAddCharacterCard": "Add Character Card",
212+
"workshopAddingCharacterCard": "Adding...",
213+
"unknownCharacterCard": "Unknown character card",
214+
"characterCardAlreadyExistsTitle": "Character Card Exists",
215+
"characterCardAlreadyExistsMessage": "Character card already exists: {{names}}",
216+
"workshopCharacterAdded": "Character card added: {{names}}",
217+
"workshopCharacterNotFound": "No character card was found in this subscription. Make sure the content has finished downloading.",
218+
"workshopCharacterAddFailed": "Failed to add character card: {{error}}",
211219
"configurePath": "Configure Path",
212220
"localModPathConfig": "Local Mod Path Configuration",
213221
"localModFolder": "Local Mod Folder Path",
@@ -411,6 +419,7 @@
411419
"loadingItemDetailsById": "Loading details for item ID: {{id}}...",
412420
"characterCardsRefreshed": "Character cards list refreshed, {{count}} character cards in total",
413421
"characterCardsRefreshedEmpty": "Character cards list refreshed, no character cards available",
422+
"characterCardsRefreshFailed": "Failed to refresh list: {{error}}",
414423
"requiredFieldsMissing": "Please fill in the following required fields: {{fields}}",
415424
"refreshLive2DPreview": "Refresh Live2D Preview",
416425
"removeTag": "Remove Tag",

static/locales/es.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,14 @@
208208
"uploadReady": "Listo para cargar {{itemName}}",
209209
"pageDescription": "Gestiona tarjetas de personaje: explora, edita, importa/exporta tarjetas y administra modelos y voces (incluyendo Steam Workshop como fuente opcional)",
210210
"openVoiceClone": "Abrir en clon de voz",
211+
"workshopAddCharacterCard": "Añadir tarjeta",
212+
"workshopAddingCharacterCard": "Añadiendo...",
213+
"unknownCharacterCard": "Tarjeta de personaje desconocida",
214+
"characterCardAlreadyExistsTitle": "La tarjeta ya existe",
215+
"characterCardAlreadyExistsMessage": "La tarjeta de personaje ya existe: {{names}}",
216+
"workshopCharacterAdded": "Tarjeta de personaje añadida: {{names}}",
217+
"workshopCharacterNotFound": "No se encontró una tarjeta de personaje en esta suscripción. Asegúrate de que el contenido haya terminado de descargarse.",
218+
"workshopCharacterAddFailed": "No se pudo añadir la tarjeta de personaje: {{error}}",
211219
"configurePath": "Configurar ruta",
212220
"localModPathConfig": "Configuración de ruta de modificación local",
213221
"localModFolder": "Ruta de la carpeta de modificación local",
@@ -411,6 +419,7 @@
411419
"loadingItemDetailsById": "Cargando detalles para el ID del artículo: {{id}}...",
412420
"characterCardsRefreshed": "Lista de tarjetas de personaje actualizada, {{count}} tarjetas de personaje en total",
413421
"characterCardsRefreshedEmpty": "Lista de cartas de personaje actualizada, no hay cartas de personaje disponibles",
422+
"characterCardsRefreshFailed": "Error al actualizar la lista: {{error}}",
414423
"requiredFieldsMissing": "Por favor complete los siguientes campos obligatorios: {{fields}}",
415424
"refreshLive2DPreview": "Actualizar vista previa de Live2D",
416425
"removeTag": "Quitar etiqueta",

static/locales/ja.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,14 @@
208208
"uploadReady": "{{itemName}}のアップロード準備完了",
209209
"pageDescription": "このページではキャラクターカードの閲覧・編集・アップロード・インポート/エクスポート、およびSteamワークショップ内のLive2Dモデルとボイスの管理が行えます",
210210
"openVoiceClone": "ボイスクローンで開く",
211+
"workshopAddCharacterCard": "キャラクターカードを追加",
212+
"workshopAddingCharacterCard": "追加中...",
213+
"unknownCharacterCard": "不明なキャラクターカード",
214+
"characterCardAlreadyExistsTitle": "キャラクターカードは既に存在します",
215+
"characterCardAlreadyExistsMessage": "キャラクターカードは既に存在します:{{names}}",
216+
"workshopCharacterAdded": "キャラクターカードを追加しました:{{names}}",
217+
"workshopCharacterNotFound": "このサブスクリプション内に追加できるキャラクターカードが見つかりません。コンテンツのダウンロード完了を確認してください。",
218+
"workshopCharacterAddFailed": "キャラクターカードの追加に失敗しました: {{error}}",
211219
"configurePath": "パス設定",
212220
"localModPathConfig": "ローカルMODパス設定",
213221
"localModFolder": "ローカルMODフォルダパス",
@@ -411,6 +419,7 @@
411419
"loadingItemDetailsById": "アイテムID: {{id}} の詳細を読み込み中...",
412420
"characterCardsRefreshed": "キャラクターカードリストを更新しました。合計 {{count}} 枚",
413421
"characterCardsRefreshedEmpty": "キャラクターカードリストを更新しました。利用可能なカードはありません",
422+
"characterCardsRefreshFailed": "リストの更新に失敗しました: {{error}}",
414423
"requiredFieldsMissing": "以下の必須項目を入力してください: {{fields}}",
415424
"refreshLive2DPreview": "Live2Dプレビューを更新",
416425
"removeTag": "タグを削除",

static/locales/ko.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,14 @@
208208
"uploadReady": "{{itemName}} 업로드 준비 중",
209209
"pageDescription": "이 페이지에서 캐릭터 카드를 검색·편집·업로드·가져오기/내보내기 하고 Steam 창작마당의 Live2D 모델과 음성을 관리할 수 있습니다",
210210
"openVoiceClone": "음성 복제에서 열기",
211+
"workshopAddCharacterCard": "캐릭터 카드 추가",
212+
"workshopAddingCharacterCard": "추가 중...",
213+
"unknownCharacterCard": "알 수 없는 캐릭터 카드",
214+
"characterCardAlreadyExistsTitle": "캐릭터 카드가 이미 존재함",
215+
"characterCardAlreadyExistsMessage": "캐릭터 카드가 이미 존재합니다: {{names}}",
216+
"workshopCharacterAdded": "캐릭터 카드가 추가되었습니다: {{names}}",
217+
"workshopCharacterNotFound": "이 구독 항목에서 추가할 수 있는 캐릭터 카드를 찾을 수 없습니다. 콘텐츠 다운로드가 완료되었는지 확인해 주세요.",
218+
"workshopCharacterAddFailed": "캐릭터 카드 추가 실패: {{error}}",
211219
"configurePath": "경로 구성",
212220
"localModPathConfig": "로컬 Mod 경로 구성",
213221
"localModFolder": "로컬 Mod 폴더 경로",
@@ -411,6 +419,7 @@
411419
"loadingItemDetailsById": "항목 ID: {{id}}에 대한 세부정보 로드 중...",
412420
"characterCardsRefreshed": "캐릭터 카드 목록이 새로 고쳐졌습니다. 총 {{count}} 캐릭터 카드입니다.",
413421
"characterCardsRefreshedEmpty": "캐릭터 카드 목록이 새로 고쳐졌고 사용 가능한 캐릭터 카드가 없습니다.",
422+
"characterCardsRefreshFailed": "목록 새로 고침 실패: {{error}}",
414423
"requiredFieldsMissing": "다음 필수 필드를 작성하십시오: {{fields}}",
415424
"refreshLive2DPreview": "Live2D 미리보기 새로 고침",
416425
"removeTag": "태그 제거",

static/locales/pt.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,14 @@
208208
"uploadReady": "Pronto para carregar {{itemName}}",
209209
"pageDescription": "Navegue, assine, baixe e gerencie modelos e vozes Live2D do Steam Workshop",
210210
"openVoiceClone": "Abrir no clone de voz",
211+
"workshopAddCharacterCard": "Adicionar cartão",
212+
"workshopAddingCharacterCard": "Adicionando...",
213+
"unknownCharacterCard": "Cartão de personagem desconhecido",
214+
"characterCardAlreadyExistsTitle": "Cartão já existe",
215+
"characterCardAlreadyExistsMessage": "O cartão de personagem já existe: {{names}}",
216+
"workshopCharacterAdded": "Cartão de personagem adicionado: {{names}}",
217+
"workshopCharacterNotFound": "Nenhum cartão de personagem foi encontrado nesta inscrição. Verifique se o conteúdo terminou de baixar.",
218+
"workshopCharacterAddFailed": "Falha ao adicionar cartão de personagem: {{error}}",
211219
"configurePath": "Configurar caminho",
212220
"localModPathConfig": "Configuração do caminho do mod local",
213221
"localModFolder": "Caminho da pasta Mod local",
@@ -411,6 +419,7 @@
411419
"loadingItemDetailsById": "Carregando detalhes do ID do item: {{id}}...",
412420
"characterCardsRefreshed": "Lista de cartas de personagem atualizada, {{count}} cartas de personagem no total",
413421
"characterCardsRefreshedEmpty": "Lista de cartas de personagem atualizada, nenhuma carta de personagem disponível",
422+
"characterCardsRefreshFailed": "Falha ao atualizar a lista: {{error}}",
414423
"requiredFieldsMissing": "Preencha os seguintes campos obrigatórios: {{fields}}",
415424
"refreshLive2DPreview": "Atualizar visualização do Live2D",
416425
"removeTag": "Remover etiqueta",

static/locales/ru.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,14 @@
208208
"uploadReady": "Готово к загрузке {{itemName}}",
209209
"pageDescription": "На этой странице можно просматривать, редактировать, загружать, импортировать и экспортировать карточки персонажей, а также управлять моделями Live2D и голосами в Мастерской Steam",
210210
"openVoiceClone": "Открыть в клонировании голоса",
211+
"workshopAddCharacterCard": "Добавить карточку",
212+
"workshopAddingCharacterCard": "Добавление...",
213+
"unknownCharacterCard": "Неизвестная карточка персонажа",
214+
"characterCardAlreadyExistsTitle": "Карточка уже существует",
215+
"characterCardAlreadyExistsMessage": "Карточка персонажа уже существует: {{names}}",
216+
"workshopCharacterAdded": "Карточка персонажа добавлена: {{names}}",
217+
"workshopCharacterNotFound": "В этой подписке не найдена карточка персонажа. Убедитесь, что загрузка содержимого завершена.",
218+
"workshopCharacterAddFailed": "Не удалось добавить карточку персонажа: {{error}}",
211219
"configurePath": "Настроить путь",
212220
"localModPathConfig": "Настройка пути к локальным модам",
213221
"localModFolder": "Путь к папке локальных модов",
@@ -411,6 +419,7 @@
411419
"loadingItemDetailsById": "Загрузка деталей предмета ID: {{id}}...",
412420
"characterCardsRefreshed": "Список карточек обновлён, всего {{count}} карточек",
413421
"characterCardsRefreshedEmpty": "Список карточек обновлён, карточки отсутствуют",
422+
"characterCardsRefreshFailed": "Не удалось обновить список: {{error}}",
414423
"requiredFieldsMissing": "Заполните следующие обязательные поля: {{fields}}",
415424
"refreshLive2DPreview": "Обновить предпросмотр Live2D",
416425
"removeTag": "Удалить тег",

0 commit comments

Comments
 (0)