diff --git a/admin-ui/src/views/ai/order/index.vue b/admin-ui/src/views/ai/order/index.vue index 96efc8b..77085bb 100644 --- a/admin-ui/src/views/ai/order/index.vue +++ b/admin-ui/src/views/ai/order/index.vue @@ -123,7 +123,21 @@ @click="playVideo(scope.row.result)" /> - + + + {{ scope.row.result }} @@ -309,6 +323,39 @@ export default { const value = String(str || "").trim(); return /^https?:\/\//i.test(value); }, + /** 已知火山错误码后附中文说明(括号) */ + volcFailureCodeWithHint(code) { + const c = String(code || "").trim(); + if (!c) return ""; + const hints = { + OutputVideoSensitiveContentDetected: "输出视频可能包含敏感信息", + InvalidParameter: "请求参数无效" + }; + const hint = hints[c]; + return hint ? `${c}(${hint})` : c; + }, + /** result 为火山回调失败写入的 VideoTaskError JSON 时解析为 { code, message } */ + parseVolcTaskErrorJson(str) { + const s = String(str || "").trim(); + if (!s || s[0] !== "{") return null; + try { + const o = JSON.parse(s); + if ( + o && + typeof o === "object" && + !Array.isArray(o) && + ("code" in o || "message" in o) + ) { + return { + code: o.code != null ? String(o.code) : "", + message: o.message != null ? String(o.message) : "" + }; + } + } catch (_) { + /* ignore */ + } + return null; + }, // 判断是否为图片结果(与 portal-ui GeneratedAssets 一致) isImage(url) { const value = String(url || "").trim(); @@ -333,6 +380,7 @@ export default { }, // 非链接内容点击事件 async handleOtherEvent(row) { + if (this.parseVolcTaskErrorJson(row.result)) return; // 防止重复点击 if (row.isDownloading) return; const originalResult = row.result; @@ -509,6 +557,19 @@ export default { height: auto; } +.order-result-error { + text-align: left; + max-width: 280px; + margin: 0 auto; + font-size: 12px; + line-height: 1.45; + color: #c45656; +} + +.order-result-error-line { + word-break: break-word; +} + .text-ellipsis-two-lines { /* 必须设置宽度(继承表格列宽,也可手动指定) */ width: 100%; diff --git a/portal-ui/src/views/VideoGen.vue b/portal-ui/src/views/VideoGen.vue index 0432086..601d169 100644 --- a/portal-ui/src/views/VideoGen.vue +++ b/portal-ui/src/views/VideoGen.vue @@ -126,6 +126,19 @@ {{ taskStatusText(row) }} + @@ -573,6 +586,51 @@ export default { el.scrollTop = el.scrollHeight }, + /** 解析失败订单存库的 VideoTaskError JSON(兼容旧数据、非 JSON 的 result) */ + parseTaskResultErrorJson(result) { + const s = String(result ?? '').trim() + if (!s || s[0] !== '{') return null + try { + const o = JSON.parse(s) + if ( + o && + typeof o === 'object' && + !Array.isArray(o) && + ('code' in o || 'message' in o) + ) { + return { + code: o.code != null ? String(o.code) : '', + message: o.message != null ? String(o.message) : '' + } + } + } catch (_) { + /* ignore */ + } + return null + }, + + /** status=2 且 result 为错误 JSON 时返回明细,否则 null */ + taskRowFailureError(row) { + const st = row?.status + if (st !== 2 && st !== '2') return null + const parsed = this.parseTaskResultErrorJson(row?.result) + if (!parsed) return null + if (!parsed.code && !parsed.message) return null + return parsed + }, + + /** 已知火山错误码后附中文说明(括号) */ + volcFailureCodeWithHint(code) { + const c = String(code || '').trim() + if (!c) return '' + const hints = { + OutputVideoSensitiveContentDetected: '输出视频可能包含敏感信息', + InvalidParameter: '请求参数无效' + } + const hint = hints[c] + return hint ? `${c}(${hint})` : c + }, + taskRowResultTrim(row) { let r = String(row?.result ?? '').trim() if (r) return r @@ -1969,11 +2027,26 @@ export default { .vg-chat-user-row { display: flex; + flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: 12px; } +.vg-chat-failure-detail { + flex: 1 1 100%; + margin-top: 4px; + padding-top: 8px; + border-top: 1px solid rgba(255, 255, 255, 0.08); + font-size: 12px; + line-height: 1.45; + color: rgba(255, 180, 180, 0.92); +} + +.vg-chat-failure-line { + word-break: break-word; +} + .vg-chat-user-col-main { flex: 1; min-width: 0; diff --git a/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/ByteApiController.java b/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/ByteApiController.java index 5446a50..f1f1dcc 100644 --- a/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/ByteApiController.java +++ b/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/ByteApiController.java @@ -8,8 +8,10 @@ import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.request.video.dto.VideoTaskCallBackRequest; import com.ruoyi.common.core.response.video.GetVideoGenerationTaskResponse; +import com.ruoyi.common.core.response.video.dto.VideoTaskError; import com.ruoyi.common.enums.AiOrderStatusType; import com.ruoyi.common.enums.VideoTaskStatusType; +import com.ruoyi.common.utils.JsonUtils; import com.ruoyi.common.utils.RandomStringUtil; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.TencentCosUtil; @@ -398,6 +400,9 @@ public class ByteApiController extends BaseController { return aiOrderService.volcCallbackSuccessProcess(request, taskResp, order); } else { // 前面已判断过status的合法性,并处理了三种非失败的状态,所以可以确定是取消、失败、超时 + if (taskResp.getError() != null) { + order.setResult(JsonUtils.toJson(taskResp.getError())); + } aiOrderService.orderFailure(order); return AjaxResult.success(); }