From c042ae25fb06f44008f201ef80e7634af164c98f Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Thu, 11 Jun 2026 16:29:07 +0800 Subject: [PATCH 1/7] 1 --- video-gen-api/app/services/payment.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/video-gen-api/app/services/payment.py b/video-gen-api/app/services/payment.py index db4ddbb6..57cbca9c 100644 --- a/video-gen-api/app/services/payment.py +++ b/video-gen-api/app/services/payment.py @@ -2,6 +2,14 @@ import logging import os from datetime import datetime, timedelta +# 尝试设置 SSL 证书路径 +try: + import certifi + os.environ["SSL_CERT_FILE"] = certifi.where() + os.environ["REQUESTS_CA_BUNDLE"] = certifi.where() +except ImportError: + pass + from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession From 2a37adea0ac97d5520f254ba3e5f35d07864bff9 Mon Sep 17 00:00:00 2001 From: Lrd <13001933075@sina.cn> Date: Thu, 11 Jun 2026 16:54:41 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=E6=8E=88=E6=9D=83=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=AF=A6=E6=83=85=EF=BC=8C=E7=BC=96=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E5=88=A0=E9=99=A4=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{index-BBz1j0L-.js => index-BH90EezD.js} | 2 +- video-gen-admin/dist/index.html | 27 +- video-gen-admin/src/App.tsx | 2 + video-gen-admin/src/api/index.ts | 37 ++ .../src/pages/AdminOauthAppList.tsx | 406 ++++++++++++++++++ video-gen-admin/tsconfig.tsbuildinfo | 2 +- video-gen-app/src/api/index.ts | 91 ++-- video-gen-app/src/pages/AuthorizationPage.tsx | 180 +++++--- 8 files changed, 602 insertions(+), 145 deletions(-) rename video-gen-admin/dist/assets/{index-BBz1j0L-.js => index-BH90EezD.js} (96%) create mode 100644 video-gen-admin/src/pages/AdminOauthAppList.tsx diff --git a/video-gen-admin/dist/assets/index-BBz1j0L-.js b/video-gen-admin/dist/assets/index-BH90EezD.js similarity index 96% rename from video-gen-admin/dist/assets/index-BBz1j0L-.js rename to video-gen-admin/dist/assets/index-BH90EezD.js index 80342d54..99657087 100644 --- a/video-gen-admin/dist/assets/index-BBz1j0L-.js +++ b/video-gen-admin/dist/assets/index-BH90EezD.js @@ -326,4 +326,4 @@ html body { ${n}-delete `]:{zIndex:10,width:r,margin:`0 ${J(e.marginXXS)}`,fontSize:r,cursor:`pointer`,transition:`all ${e.motionDurationSlow}`,color:i,"&:hover":{color:i},svg:{verticalAlign:`baseline`}}},[`${s}-thumbnail, ${s}-thumbnail img`]:{position:`static`,display:`block`,width:`100%`,height:`100%`,objectFit:`contain`},[`${s}-name`]:{display:`none`,textAlign:`center`},[`${s}-file + ${s}-name`]:{position:`absolute`,bottom:e.margin,display:`block`,width:`calc(100% - ${J(a(e.paddingXS).mul(2).equal())})`},[`${s}-uploading`]:{[`&${s}`]:{backgroundColor:e.colorFillAlter},[`&::before, ${n}-eye, ${n}-download, ${n}-delete`]:{display:`none`}},[`${s}-progress`]:{bottom:e.marginXL,width:`calc(100% - ${J(a(e.paddingXS).mul(2).equal())})`,paddingInlineStart:0}}},[`${t}-wrapper${t}-picture-circle-wrapper`]:{[`${t}${t}-select`]:{borderRadius:`50%`}}}},HV=e=>{let{componentCls:t}=e;return{[`${t}-rtl`]:{direction:`rtl`}}},UV=e=>{let{componentCls:t,colorTextDisabled:n}=e;return{[`${t}-wrapper`]:{...qc(e),[t]:{outline:0,"input[type='file']":{cursor:`pointer`}},[`${t}-select`]:{display:`inline-block`},[`${t}-hidden`]:{display:`none`},[`${t}-disabled`]:{color:n,cursor:`not-allowed`}}}},WV=nl(`Upload`,e=>{let{fontSizeHeading3:t,marginXS:n,lineWidth:r,pictureCardSize:i,calc:a}=e,o=hs(e,{uploadThumbnailSize:a(t).mul(2).equal(),uploadProgressOffset:a(a(n).div(2)).add(r).equal(),uploadPicCardSize:i});return[UV(o),LV(o),BV(o),VV(o),RV(o),zV(o),HV(o),fh(o)]},e=>({actionsColor:e.colorIcon,pictureCardSize:e.controlHeightLG*2.55})),GV={icon:function(e,t){return{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M534 352V136H232v752h560V394H576a42 42 0 01-42-42z`,fill:t}},{tag:`path`,attrs:{d:`M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM602 137.8L790.2 326H602V137.8zM792 888H232V136h302v216a42 42 0 0042 42h216v494z`,fill:e}}]}},name:`file`,theme:`twotone`};function KV(){return KV=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,KV({},e,{ref:t,icon:GV}))),JV={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M779.3 196.6c-94.2-94.2-247.6-94.2-341.7 0l-261 260.8c-1.7 1.7-2.6 4-2.6 6.4s.9 4.7 2.6 6.4l36.9 36.9a9 9 0 0012.7 0l261-260.8c32.4-32.4 75.5-50.2 121.3-50.2s88.9 17.8 121.2 50.2c32.4 32.4 50.2 75.5 50.2 121.2 0 45.8-17.8 88.8-50.2 121.2l-266 265.9-43.1 43.1c-40.3 40.3-105.8 40.3-146.1 0-19.5-19.5-30.2-45.4-30.2-73s10.7-53.5 30.2-73l263.9-263.8c6.7-6.6 15.5-10.3 24.9-10.3h.1c9.4 0 18.1 3.7 24.7 10.3 6.7 6.7 10.3 15.5 10.3 24.9 0 9.3-3.7 18.1-10.3 24.7L372.4 653c-1.7 1.7-2.6 4-2.6 6.4s.9 4.7 2.6 6.4l36.9 36.9a9 9 0 0012.7 0l215.6-215.6c19.9-19.9 30.8-46.3 30.8-74.4s-11-54.6-30.8-74.4c-41.1-41.1-107.9-41-149 0L463 364 224.8 602.1A172.22 172.22 0 00174 724.8c0 46.3 18.1 89.8 50.8 122.5 33.9 33.8 78.3 50.7 122.7 50.7 44.4 0 88.8-16.9 122.6-50.7l309.2-309C824.8 492.7 850 432 850 367.5c.1-64.6-25.1-125.3-70.7-170.9z`}}]},name:`paper-clip`,theme:`outlined`};function YV(){return YV=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,YV({},e,{ref:t,icon:JV}))),ZV={icon:function(e,t){return{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 632H136v-39.9l138.5-164.3 150.1 178L658.1 489 888 761.6V792zm0-129.8L664.2 396.8c-3.2-3.8-9-3.8-12.2 0L424.6 666.4l-144-170.7c-3.2-3.8-9-3.8-12.2 0L136 652.7V232h752v430.2z`,fill:e}},{tag:`path`,attrs:{d:`M424.6 765.8l-150.1-178L136 752.1V792h752v-30.4L658.1 489z`,fill:t}},{tag:`path`,attrs:{d:`M136 652.7l132.4-157c3.2-3.8 9-3.8 12.2 0l144 170.7L652 396.8c3.2-3.8 9-3.8 12.2 0L888 662.2V232H136v420.7zM304 280a88 88 0 110 176 88 88 0 010-176z`,fill:t}},{tag:`path`,attrs:{d:`M276 368a28 28 0 1056 0 28 28 0 10-56 0z`,fill:t}},{tag:`path`,attrs:{d:`M304 456a88 88 0 100-176 88 88 0 000 176zm0-116c15.5 0 28 12.5 28 28s-12.5 28-28 28-28-12.5-28-28 12.5-28 28-28z`,fill:e}}]}},name:`picture`,theme:`twotone`};function QV(){return QV=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,QV({},e,{ref:t,icon:ZV})));function eH(e){return{...e,lastModified:e.lastModified,lastModifiedDate:e.lastModifiedDate,name:e.name,size:e.size,type:e.type,uid:e.uid,percent:0,originFileObj:e}}function tH(e,t){let n=Xr(t),r=n.findIndex(({uid:t})=>t===e.uid);return r===-1?n.push(e):n[r]=e,n}function nH(e,t){let n=e.uid===void 0?`name`:`uid`;return t.filter(t=>t[n]===e[n])[0]}function rH(e,t){let n=e.uid===void 0?`name`:`uid`,r=t.filter(t=>t[n]!==e[n]);return r.length===t.length?null:r}var iH=(e=``)=>{let t=e.split(`/`),n=t[t.length-1].split(/#|\?/)[0];return(/\.[^./\\]*$/.exec(n)||[``])[0]},aH=e=>e.indexOf(`image/`)===0,oH=e=>{if(e.type&&!e.thumbUrl)return aH(e.type);let t=e.thumbUrl||e.url||``,n=iH(t);return/^data:image\//.test(t)||/(webp|svg|png|gif|jpg|jpeg|jfif|bmp|dpg|ico|heic|heif)$/i.test(n)?!0:!(/^data:/.test(t)||n)},sH=200;function cH(e){return new Promise(t=>{if(!e.type||!aH(e.type)){t(``);return}let n=document.createElement(`canvas`);n.width=sH,n.height=sH,n.style.cssText=`position: fixed; left: 0; top: 0; width: ${sH}px; height: ${sH}px; z-index: 9999; display: none;`,document.body.appendChild(n);let r=n.getContext(`2d`),i=new Image;if(i.onload=()=>{let{width:e,height:a}=i,o=sH,s=sH,c=0,l=0;e>a?(s=sH/e*a,l=-(s-o)/2):(o=sH/a*e,c=-(o-s)/2),r.drawImage(i,c,l,o,s);let u=n.toDataURL();document.body.removeChild(n),window.URL.revokeObjectURL(i.src),t(u)},i.crossOrigin=`anonymous`,e.type.startsWith(`image/svg+xml`)){let t=new FileReader;t.onload=()=>{t.result&&typeof t.result==`string`&&(i.src=t.result)},t.readAsDataURL(e)}else if(e.type.startsWith(`image/gif`)){let n=new FileReader;n.onload=()=>{n.result&&t(n.result)},n.readAsDataURL(e)}else i.src=window.URL.createObjectURL(e)})}var lH={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M505.7 661a8 8 0 0012.6 0l112-141.7c4.1-5.2.4-12.9-6.3-12.9h-74.1V168c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v338.3H400c-6.7 0-10.4 7.7-6.3 12.9l112 141.8zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8z`}}]},name:`download`,theme:`outlined`};function uH(){return uH=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,uH({},e,{ref:t,icon:lH}))),fH=x.forwardRef(({prefixCls:e,className:t,style:n,classNames:r,styles:i,locale:a,listType:o,file:s,items:c,progress:l,iconRender:u,actionIconRender:d,itemRender:f,isImgUrl:p,showPreviewIcon:m,showRemoveIcon:h,showDownloadIcon:g,previewIcon:_,removeIcon:v,downloadIcon:y,extra:b,onPreview:S,onDownload:C,onClose:w},T)=>{let{status:E}=s,[D,O]=x.useState(E);x.useEffect(()=>{E!==`removed`&&O(E)},[E]);let[k,A]=x.useState(!1);x.useEffect(()=>{let e=setTimeout(()=>{A(!0)},300);return()=>{clearTimeout(e)}},[]);let j=u(s),M=x.createElement(`div`,{className:`${e}-icon`},j);if(o===`picture`||o===`picture-card`||o===`picture-circle`)if(D===`uploading`||!s.thumbUrl&&!s.url){let t=q(`${e}-list-item-thumbnail`,{[`${e}-list-item-file`]:D!==`uploading`});M=x.createElement(`div`,{className:t},j)}else{let t=p?.(s)?x.createElement(`img`,{src:s.thumbUrl||s.url,alt:s.name,className:`${e}-list-item-image`,crossOrigin:s.crossOrigin}):j,n=q(`${e}-list-item-thumbnail`,{[`${e}-list-item-file`]:p&&!p(s)});M=x.createElement(`a`,{className:n,onClick:e=>S(s,e),href:s.url||s.thumbUrl,target:`_blank`,rel:`noopener noreferrer`},t)}let N=q(`${e}-list-item`,`${e}-list-item-${D}`,r?.item),P=typeof s.linkProps==`string`?JSON.parse(s.linkProps):s.linkProps,F=(typeof h==`function`?h(s):h)?d((typeof v==`function`?v(s):v)||x.createElement(EB,null),()=>w(s),e,a.removeFile,!0):null,I=(typeof g==`function`?g(s):g)&&D===`done`?d((typeof y==`function`?y(s):y)||x.createElement(dH,null),()=>C(s),e,a.downloadFile):null,L=o!==`picture-card`&&o!==`picture-circle`&&x.createElement(`span`,{key:`download-delete`,className:q(`${e}-list-item-actions`,{picture:o===`picture`})},I,F),R=typeof b==`function`?b(s):b,z=R&&x.createElement(`span`,{className:`${e}-list-item-extra`},R),B=q(`${e}-list-item-name`),V=s.url?x.createElement(`a`,{key:`view`,target:`_blank`,rel:`noopener noreferrer`,className:B,title:s.name,...P,href:s.url,onClick:e=>S(s,e)},s.name,z):x.createElement(`span`,{key:`view`,className:B,onClick:e=>S(s,e),title:s.name},s.name,z),H=(typeof m==`function`?m(s):m)&&(s.url||s.thumbUrl)?x.createElement(`a`,{href:s.url||s.thumbUrl,target:`_blank`,rel:`noopener noreferrer`,onClick:e=>S(s,e),title:a.previewFile},typeof _==`function`?_(s):_||x.createElement(AM,null)):null,U=(o===`picture-card`||o===`picture-circle`)&&D!==`uploading`&&x.createElement(`span`,{className:`${e}-list-item-actions`},H,D===`done`&&I,F),{getPrefixCls:W}=x.useContext(ai),ee=W(),te=x.createElement(`div`,{className:N,style:i?.item},M,V,L,U,k&&x.createElement(Pu,{motionName:`${ee}-fade`,visible:D===`uploading`,motionDeadline:2e3},({className:t})=>{let n=`percent`in s?x.createElement(oF,{type:`line`,percent:s.percent,"aria-label":s[`aria-label`],"aria-labelledby":s[`aria-labelledby`],...l}):null;return x.createElement(`div`,{className:q(`${e}-list-item-progress`,t)},n)})),ne=s.response&&typeof s.response==`string`?s.response:s.error?.statusText||s.error?.message||a.uploadError,re=D===`error`?x.createElement(Bw,{title:ne,getPopupContainer:e=>e.parentNode},te):te;return x.createElement(`div`,{className:q(`${e}-list-item-container`,t),style:n,ref:T},f?f(re,s,c,{download:C.bind(null,s),preview:S.bind(null,s),remove:w.bind(null,s)}):re)}),pH=x.forwardRef((e,t)=>{let{listType:n=`text`,previewFile:r=cH,onPreview:i,onDownload:a,onRemove:o,locale:s,iconRender:c,isImageUrl:l=oH,prefixCls:u,items:d=[],showPreviewIcon:f=!0,showRemoveIcon:p=!0,showDownloadIcon:m=!1,removeIcon:h,previewIcon:g,downloadIcon:_,extra:v,progress:y={size:[-1,2],showInfo:!1},appendAction:b,appendActionVisible:S=!0,itemRender:C,disabled:w,classNames:T,styles:E}=e,[,D]=wd(),[O,k]=x.useState(!1),A=[`picture-card`,`picture-circle`].includes(n);x.useEffect(()=>{n.startsWith(`picture`)&&(d||[]).forEach(e=>{!(e.originFileObj instanceof File||e.originFileObj instanceof Blob)||e.thumbUrl!==void 0||(e.thumbUrl=``,r?.(e.originFileObj).then(t=>{e.thumbUrl=t||``,D()}))})},[n,d,r]),x.useEffect(()=>{k(!0)},[]);let j=(e,t)=>{if(i)return t?.preventDefault(),i(e)},M=e=>{typeof a==`function`?a(e):e.url&&window.open(e.url)},N=e=>{o?.(e)},P=e=>{if(c)return c(e,n);let t=e.status===`uploading`;if(n.startsWith(`picture`)){let r=n===`picture`?x.createElement(um,null):s.uploading,i=l?.(e)?x.createElement($V,null):x.createElement(qV,null);return t?r:i}return t?x.createElement(um,null):x.createElement(XV,null)},F=(e,t,n,r,i)=>{let a={type:`text`,size:`small`,title:r,onClick:n=>{t(),x.isValidElement(e)&&e.props.onClick?.(n)},className:`${n}-list-item-action`,disabled:i?w:!1};return x.isValidElement(e)?x.createElement(Mg,{...a,icon:xp(e,{...e.props,onClick:()=>{}})}):x.createElement(Mg,{...a},x.createElement(`span`,null,e))};x.useImperativeHandle(t,()=>({handlePreview:j,handleDownload:M}));let{getPrefixCls:I}=x.useContext(ai),L=I(`upload`,u),R=I(),z=q(`${L}-list`,`${L}-list-${n}`,T?.list),B=x.useMemo(()=>xr(Fm(R),[`onAppearEnd`,`onEnterEnd`,`onLeaveEnd`]),[R]),V={...A?{}:B,motionDeadline:2e3,motionName:`${L}-${A?`animate-inline`:`animate`}`,keys:Xr(d.map(e=>({key:e.uid,file:e}))),motionAppear:O};return x.createElement(`div`,{className:z,style:E?.list},x.createElement(Nu,{...V,component:!1},({key:e,file:t,className:r,style:i})=>x.createElement(fH,{key:e,locale:s,prefixCls:L,className:r,style:i,classNames:T,styles:E,file:t,items:d,progress:y,listType:n,isImgUrl:l,showPreviewIcon:f,showRemoveIcon:p,showDownloadIcon:m,removeIcon:h,previewIcon:g,downloadIcon:_,extra:v,iconRender:P,actionIconRender:F,itemRender:C,onPreview:j,onDownload:M,onClose:N})),b&&x.createElement(Pu,{...V,visible:S,forceRender:!0},({className:e,style:t})=>xp(b,n=>({className:q(n.className,e),style:{...t,pointerEvents:e?`none`:void 0,...n.style}}))))}),mH=`__LIST_IGNORE_${Date.now()}__`,hH=x.forwardRef((e,t)=>{let n=ci(`upload`),{fileList:r,defaultFileList:i,onRemove:a,showUploadList:o=!0,listType:s=`text`,onPreview:c,onDownload:l,onChange:u,onDrop:d,previewFile:f,disabled:p,locale:m,iconRender:h,isImageUrl:g,progress:_,prefixCls:v,className:y,type:b=`select`,children:S,style:C,itemRender:w,maxCount:T,data:E={},multiple:D=!1,hasControlInside:O=!0,action:k=``,accept:A=``,supportServerRender:j=!0,rootClassName:M,styles:N,classNames:P}=e,F=x.useContext(Ep),I=p??F,L=e.customRequest||n.customRequest,[R,z]=Vn(i,r),B=R||[],[V,H]=x.useState(`drop`),U=x.useRef(null),W=x.useRef(null);x.useMemo(()=>{let e=Date.now();(r||[]).forEach((t,n)=>{!t.uid&&!Object.isFrozen(t)&&(t.uid=`__AUTO__${e}_${n}__`)})},[r]);let ee=(e,t,n)=>{let r=Xr(t),i=!1;T===1?r=r.slice(-1):T&&(i=r.length>T,r=r.slice(0,T)),(0,Ff.flushSync)(()=>{z(r)});let a={file:e,fileList:r};n&&(a.event=n),(!i||e.status===`removed`||r.some(t=>t.uid===e.uid))&&(0,Ff.flushSync)(()=>{u?.(a)})},te=async(t,n)=>{let{beforeUpload:r}=e,i=t;if(r){let e=await r(t,n);if(e===!1)return!1;if(delete t[mH],e===mH)return Object.defineProperty(t,mH,{value:!0,configurable:!0}),!1;pd(e)&&(i=e)}return i},ne=e=>{let t=e.filter(e=>!e.file[mH]);if(!t.length)return;let n=t.map(e=>eH(e.file)),r=Xr(B);n.forEach(e=>{r=tH(e,r)}),n.forEach((e,n)=>{let i=e;if(t[n].parsedFile)e.status=`uploading`;else{let{originFileObj:t}=e,n;try{n=new File([t],t.name,{type:t.type})}catch{n=new Blob([t],{type:t.type}),n.name=t.name,n.lastModifiedDate=new Date,n.lastModified=new Date().getTime()}n.uid=e.uid,i=n}ee(i,r)})},re=(e,t,n)=>{try{typeof e==`string`&&(e=JSON.parse(e))}catch{}if(!nH(t,B))return;let r=eH(t);r.status=`done`,r.percent=100,r.response=e,r.xhr=n,ee(r,tH(r,B))},G=(e,t)=>{if(!nH(t,B))return;let n=eH(t);n.status=`uploading`,n.percent=e.percent,ee(n,tH(n,B),e)},ie=(e,t,n)=>{if(!nH(n,B))return;let r=eH(n);r.error=e,r.response=t,r.status=`error`,ee(r,tH(r,B))},ae=e=>{let t;Promise.resolve(typeof a==`function`?a(e):a).then(n=>{if(n===!1)return;let r=rH(e,B);r&&(t={...e,status:`removed`},B?.forEach(e=>{let n=t.uid===void 0?`name`:`uid`;e[n]===t[n]&&!Object.isFrozen(e)&&(e.status=`removed`)}),U.current?.abort(t),ee(t,r))})},oe=e=>{H(e.type),e.type===`drop`&&d?.(e)};x.useImperativeHandle(t,()=>({onBatchStart:ne,onSuccess:re,onProgress:G,onError:ie,fileList:B,upload:U.current,nativeElement:W.current}));let{getPrefixCls:K,direction:se,className:ce,style:le,classNames:ue,styles:de}=ci(`upload`),fe=K(`upload`,v),pe={...e,listType:s,showUploadList:o,type:b,multiple:D,hasControlInside:O,supportServerRender:j,disabled:I},[me,he]=Nd([ue,P],[de,N],{props:pe}),ge={onBatchStart:ne,onError:ie,onProgress:G,onSuccess:re,...e,customRequest:L,data:E,multiple:D,action:k,accept:A,supportServerRender:j,prefixCls:fe,disabled:I,beforeUpload:te,onChange:void 0,hasControlInside:O};delete ge.className,delete ge.style,(!S||I)&&delete ge.id;let _e=`${fe}-wrapper`,[ve,ye]=WV(fe,_e),[be]=od(`Upload`,$u.Upload),{showRemoveIcon:xe,showPreviewIcon:Se,showDownloadIcon:Ce,removeIcon:we,previewIcon:Te,downloadIcon:Ee,extra:De}=typeof o==`boolean`?{}:o,Oe=xe===void 0?!I:xe,ke=(e,t)=>o?x.createElement(pH,{classNames:me,styles:he,prefixCls:fe,listType:s,items:B,previewFile:f,onPreview:c,onDownload:l,onRemove:ae,showRemoveIcon:Oe,showPreviewIcon:Se,showDownloadIcon:Ce,removeIcon:we,previewIcon:Te,downloadIcon:Ee,iconRender:h,extra:De,locale:{...be,...m},isImageUrl:g,progress:_,appendAction:e,appendActionVisible:t,itemRender:w,disabled:I}):e,Ae=q(_e,y,M,ve,ye,ce,me.root,{[`${fe}-rtl`]:se===`rtl`,[`${fe}-picture-card-wrapper`]:s===`picture-card`,[`${fe}-picture-circle-wrapper`]:s===`picture-circle`}),je={...he.root},Me={...le,...C};if(b===`drag`){let e=q(ve,fe,`${fe}-drag`,{[`${fe}-drag-uploading`]:B.some(e=>e.status===`uploading`),[`${fe}-drag-hover`]:V===`dragover`,[`${fe}-disabled`]:I,[`${fe}-rtl`]:se===`rtl`},me.trigger);return x.createElement(`span`,{className:Ae,ref:W,style:je},x.createElement(`div`,{className:e,style:{...Me,...he.trigger},onDrop:oe,onDragOver:oe,onDragLeave:oe},x.createElement(IV,{...ge,ref:U,className:`${fe}-btn`},x.createElement(`div`,{className:`${fe}-drag-container`},S))),ke())}let Ne=q(fe,`${fe}-select`,{[`${fe}-disabled`]:I,[`${fe}-hidden`]:!S},me.trigger),Pe=x.createElement(`div`,{className:Ne,style:{...Me,...he.trigger}},x.createElement(IV,{...ge,ref:U}));return s===`picture-card`||s===`picture-circle`?x.createElement(`span`,{className:Ae,ref:W,style:je},ke(Pe,!!S)):x.createElement(`span`,{className:Ae,ref:W,style:je},Pe,ke())}),gH=x.forwardRef((e,t)=>{let{style:n,height:r,hasControlInside:i=!1,children:a,...o}=e,s={...n,height:r};return x.createElement(hH,{ref:t,hasControlInside:i,...o,style:s,type:`drag`},a)}),_H=hH;_H.Dragger=gH,_H.LIST_IGNORE=mH;var vH=o(((e,t)=>{function n(e){return e&&e.__esModule?e:{default:e}}t.exports=n,t.exports.__esModule=!0,t.exports.default=t.exports})),yH=o((e=>{Object.defineProperty(e,`__esModule`,{value:!0}),e.default=void 0,e.default={items_per_page:`条/页`,jump_to:`跳至`,jump_to_confirm:`确定`,page:`页`,prev_page:`上一页`,next_page:`下一页`,prev_5:`向前 5 页`,next_5:`向后 5 页`,prev_3:`向前 3 页`,next_3:`向后 3 页`,page_size:`页码`}})),bH=o((e=>{Object.defineProperty(e,`__esModule`,{value:!0}),e.commonLocale=void 0,e.commonLocale={yearFormat:`YYYY`,dayFormat:`D`,cellMeridiemFormat:`A`,monthBeforeYear:!0}})),xH=o((e=>{Object.defineProperty(e,`__esModule`,{value:!0}),e.default=void 0;var t=bH();function n(e){"@babel/helpers - typeof";return n=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},n(e)}function r(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),n.push.apply(n,r)}return n}function i(e){for(var t=1;t{Object.defineProperty(e,`__esModule`,{value:!0}),e.default=void 0,e.default={placeholder:`请选择时间`,rangePlaceholder:[`开始时间`,`结束时间`]}})),CH=o((e=>{var t=vH().default;Object.defineProperty(e,`__esModule`,{value:!0}),e.default=void 0;var n=t(xH()),r=t(SH()),i={lang:{placeholder:`请选择日期`,yearPlaceholder:`请选择年份`,quarterPlaceholder:`请选择季度`,monthPlaceholder:`请选择月份`,weekPlaceholder:`请选择周`,rangePlaceholder:[`开始日期`,`结束日期`],rangeYearPlaceholder:[`开始年份`,`结束年份`],rangeMonthPlaceholder:[`开始月份`,`结束月份`],rangeQuarterPlaceholder:[`开始季度`,`结束季度`],rangeWeekPlaceholder:[`开始周`,`结束周`],...n.default},timePickerLocale:{...r.default}};i.lang.ok=`确定`,e.default=i})),wH=o((e=>{var t=vH().default;Object.defineProperty(e,`__esModule`,{value:!0}),e.default=void 0,e.default=t(CH()).default})),TH=o((e=>{var t=vH().default;Object.defineProperty(e,`__esModule`,{value:!0}),e.default=void 0;var n=t(yH()),r=t(wH()),i=t(CH()),a=t(SH()),o="${label}不是一个有效的${type}";e.default={locale:`zh-cn`,Pagination:n.default,DatePicker:i.default,TimePicker:a.default,Calendar:r.default,global:{placeholder:`请选择`,close:`关闭`,sortable:`可排序`},Table:{filterTitle:`筛选`,filterConfirm:`确定`,filterReset:`重置`,filterEmptyText:`无筛选项`,filterCheckAll:`全选`,filterSearchPlaceholder:`在筛选项中搜索`,emptyText:`暂无数据`,selectAll:`全选当页`,selectInvert:`反选当页`,selectNone:`清空所有`,selectionAll:`全选所有`,sortTitle:`排序`,expand:`展开行`,collapse:`关闭行`,triggerDesc:`点击降序`,triggerAsc:`点击升序`,cancelSort:`取消排序`},Modal:{okText:`确定`,cancelText:`取消`,justOkText:`知道了`},Tour:{Next:`下一步`,Previous:`上一步`,Finish:`结束导览`},Popconfirm:{cancelText:`取消`,okText:`确定`},Transfer:{titles:[``,``],searchPlaceholder:`请输入搜索内容`,itemUnit:`项`,itemsUnit:`项`,remove:`删除`,selectCurrent:`全选当页`,removeCurrent:`删除当页`,selectAll:`全选所有`,deselectAll:`取消全选`,removeAll:`删除全部`,selectInvert:`反选当页`},Upload:{uploading:`文件上传中`,removeFile:`删除文件`,uploadError:`上传错误`,previewFile:`预览文件`,downloadFile:`下载文件`},Empty:{description:`暂无数据`},Icon:{icon:`图标`},Text:{edit:`编辑`,copy:`复制`,copied:`复制成功`,expand:`展开`,collapse:`收起`},Form:{optional:`(可选)`,defaultValidateMessages:{default:"字段验证错误${label}",required:"请输入${label}",enum:"${label}必须是其中一个[${enum}]",whitespace:"${label}不能为空字符",date:{format:"${label}日期格式无效",parse:"${label}不能转换为日期",invalid:"${label}是一个无效日期"},types:{string:o,method:o,array:o,object:o,number:o,date:o,boolean:o,integer:o,float:o,regexp:o,email:o,url:o,hex:o},string:{len:"${label}须为${len}个字符",min:"${label}最少${min}个字符",max:"${label}最多${max}个字符",range:"${label}须在${min}-${max}字符之间"},number:{len:"${label}必须等于${len}",min:"${label}最小值为${min}",max:"${label}最大值为${max}",range:"${label}须在${min}-${max}之间"},array:{len:"须为${len}个${label}",min:"最少${min}个${label}",max:"最多${max}个${label}",range:"${label}数量须在${min}-${max}之间"},pattern:{mismatch:"${label}与模式不匹配${pattern}"}}},QRCode:{expired:`二维码过期`,refresh:`点击刷新`,scanned:`已扫描`},ColorPicker:{presetEmpty:`暂无`,transparent:`无色`,singleColor:`单色`,gradientColor:`渐变色`}}})),EH=o(((e,t)=>{t.exports=TH()})),DH={icon:{tag:`svg`,attrs:{"fill-rule":`evenodd`,viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M512 64c247.42 0 448 200.58 448 448S759.42 960 512 960 64 759.42 64 512 264.58 64 512 64m32.5 168c-69.67 0-86.06 16.84-86.72 39.08l-.02 1.43v46.62H291.45c-9.92 0-14.28 23.05-14.27 39.3 0 2.7 2.08 4.93 4.77 4.93h175.81v58.3h-116.5c-9.96 0-14.3 23.76-14.27 39.47a4.77 4.77 0 004.77 4.76h233.45c-4.53 41.06-15.43 77.59-30.72 109.32l-1.22 2.5-.32-.28c-60.24-28.47-120.43-52.57-194.4-52.57l-2.62.01c-84.98 1.11-144.71 56.5-145.91 127.04l-.02 1.22.02 2.13c1.24 70.4 63.56 126.45 148.52 126.45 61.25 0 116.38-16.85 163.46-45.02a138.58 138.58 0 0014.07-7.96 345.6 345.6 0 0050.3-41.16l9.45 6.35 12.46 8.32c57.53 38.26 113.76 72.62 169.86 79.27a142.62 142.62 0 0018.31 1.16c43.02 0 55-52.68 57.39-95.51l.14-2.84c.4-8.46-6.2-15.6-14.65-15.86-75.46-2.37-136.45-22.05-192-46.11l-6.27-2.75c35.15-56.8 56.66-121.81 57.15-186.66l.09-1.08c.4-5.51-4-10.2-9.52-10.2H549.33v-58.3h165.73c9.92 0 14.28-22.12 14.27-39.31a4.85 4.85 0 00-4.78-4.92H549.32v-82.35a4.8 4.8 0 00-4.83-4.78M328 583.85c54.63 0 107.08 22.41 158.1 52.19l5.76 3.4c-103.57 119.84-247.17 95.9-261.72 26.37a66.89 66.89 0 01-1.14-9.83l-.06-2.34.02-.9c.97-40.12 45.33-68.9 99.04-68.9`}}]},name:`alipay-circle`,theme:`outlined`};function OH(){return OH=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,OH({},e,{ref:t,icon:DH}))),AH={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M917.7 148.8l-42.4-42.4c-1.6-1.6-3.6-2.3-5.7-2.3s-4.1.8-5.7 2.3l-76.1 76.1a199.27 199.27 0 00-112.1-34.3c-51.2 0-102.4 19.5-141.5 58.6L432.3 308.7a8.03 8.03 0 000 11.3L704 591.7c1.6 1.6 3.6 2.3 5.7 2.3 2 0 4.1-.8 5.7-2.3l101.9-101.9c68.9-69 77-175.7 24.3-253.5l76.1-76.1c3.1-3.2 3.1-8.3 0-11.4zM769.1 441.7l-59.4 59.4-186.8-186.8 59.4-59.4c24.9-24.9 58.1-38.7 93.4-38.7 35.3 0 68.4 13.7 93.4 38.7 24.9 24.9 38.7 58.1 38.7 93.4 0 35.3-13.8 68.4-38.7 93.4zm-190.2 105a8.03 8.03 0 00-11.3 0L501 613.3 410.7 523l66.7-66.7c3.1-3.1 3.1-8.2 0-11.3L441 408.6a8.03 8.03 0 00-11.3 0L363 475.3l-43-43a7.85 7.85 0 00-5.7-2.3c-2 0-4.1.8-5.7 2.3L206.8 534.2c-68.9 69-77 175.7-24.3 253.5l-76.1 76.1a8.03 8.03 0 000 11.3l42.4 42.4c1.6 1.6 3.6 2.3 5.7 2.3s4.1-.8 5.7-2.3l76.1-76.1c33.7 22.9 72.9 34.3 112.1 34.3 51.2 0 102.4-19.5 141.5-58.6l101.9-101.9c3.1-3.1 3.1-8.2 0-11.3l-43-43 66.7-66.7c3.1-3.1 3.1-8.2 0-11.3l-36.6-36.2zM441.7 769.1a131.32 131.32 0 01-93.4 38.7c-35.3 0-68.4-13.7-93.4-38.7a131.32 131.32 0 01-38.7-93.4c0-35.3 13.7-68.4 38.7-93.4l59.4-59.4 186.8 186.8-59.4 59.4z`}}]},name:`api`,theme:`outlined`};function jH(){return jH=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,jH({},e,{ref:t,icon:AH}))),NH={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H212V212h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H612V212h200v200zM464 544H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H212V612h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H612V612h200v200z`}}]},name:`appstore`,theme:`outlined`};function PH(){return PH=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,PH({},e,{ref:t,icon:NH}))),IH={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M862 465.3h-81c-4.6 0-9 2-12.1 5.5L550 723.1V160c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v563.1L255.1 470.8c-3-3.5-7.4-5.5-12.1-5.5h-81c-6.8 0-10.5 8.1-6 13.2L487.9 861a31.96 31.96 0 0048.3 0L868 478.5c4.5-5.2.8-13.2-6-13.2z`}}]},name:`arrow-down`,theme:`outlined`};function LH(){return LH=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,LH({},e,{ref:t,icon:IH}))),zH={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M868 545.5L536.1 163a31.96 31.96 0 00-48.3 0L156 545.5a7.97 7.97 0 006 13.2h81c4.6 0 9-2 12.1-5.5L474 300.9V864c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V300.9l218.9 252.3c3 3.5 7.4 5.5 12.1 5.5h81c6.8 0 10.5-8 6-13.2z`}}]},name:`arrow-up`,theme:`outlined`};function BH(){return BH=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,BH({},e,{ref:t,icon:zH}))),HH={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M842 454c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8 0 140.3-113.7 254-254 254S258 594.3 258 454c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8 0 168.7 126.6 307.9 290 327.6V884H326.7c-13.7 0-24.7 14.3-24.7 32v36c0 4.4 2.8 8 6.2 8h407.6c3.4 0 6.2-3.6 6.2-8v-36c0-17.7-11-32-24.7-32H548V782.1c165.3-18 294-158 294-328.1zM512 624c93.9 0 170-75.2 170-168V232c0-92.8-76.1-168-170-168s-170 75.2-170 168v224c0 92.8 76.1 168 170 168zm-94-392c0-50.6 41.9-92 94-92s94 41.4 94 92v224c0 50.6-41.9 92-94 92s-94-41.4-94-92V232z`}}]},name:`audio`,theme:`outlined`};function UH(){return UH=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,UH({},e,{ref:t,icon:HH}))),GH={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M894 462c30.9 0 43.8-39.7 18.7-58L530.8 126.2a31.81 31.81 0 00-37.6 0L111.3 404c-25.1 18.2-12.2 58 18.8 58H192v374h-72c-4.4 0-8 3.6-8 8v52c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-52c0-4.4-3.6-8-8-8h-72V462h62zM512 196.7l271.1 197.2H240.9L512 196.7zM264 462h117v374H264V462zm189 0h117v374H453V462zm307 374H642V462h118v374z`}}]},name:`bank`,theme:`outlined`};function KH(){return KH=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,KH({},e,{ref:t,icon:GH}))),JH={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M888 792H200V168c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v688c0 4.4 3.6 8 8 8h752c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-600-80h56c4.4 0 8-3.6 8-8V560c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v144c0 4.4 3.6 8 8 8zm152 0h56c4.4 0 8-3.6 8-8V384c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v320c0 4.4 3.6 8 8 8zm152 0h56c4.4 0 8-3.6 8-8V462c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v242c0 4.4 3.6 8 8 8zm152 0h56c4.4 0 8-3.6 8-8V304c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v400c0 4.4 3.6 8 8 8z`}}]},name:`bar-chart`,theme:`outlined`};function YH(){return YH=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,YH({},e,{ref:t,icon:JH}))),ZH={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M816 768h-24V428c0-141.1-104.3-257.7-240-277.1V112c0-22.1-17.9-40-40-40s-40 17.9-40 40v38.9c-135.7 19.4-240 136-240 277.1v340h-24c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h216c0 61.8 50.2 112 112 112s112-50.2 112-112h216c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zM512 888c-26.5 0-48-21.5-48-48h96c0 26.5-21.5 48-48 48zM304 768V428c0-55.6 21.6-107.8 60.9-147.1S456.4 220 512 220c55.6 0 107.8 21.6 147.1 60.9S720 372.4 720 428v340H304z`}}]},name:`bell`,theme:`outlined`};function QH(){return QH=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,QH({},e,{ref:t,icon:ZH}))),eU={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32zm-260 72h96v209.9L621.5 312 572 347.4V136zm220 752H232V136h280v296.9c0 3.3 1 6.6 3 9.3a15.9 15.9 0 0022.3 3.7l83.8-59.9 81.4 59.4c2.7 2 6 3.1 9.4 3.1 8.8 0 16-7.2 16-16V136h64v752z`}}]},name:`book`,theme:`outlined`};function tU(){return tU=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,tU({},e,{ref:t,icon:eU}))),rU={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M916 210H376c-17.7 0-32 14.3-32 32v236H108c-17.7 0-32 14.3-32 32v272c0 17.7 14.3 32 32 32h540c17.7 0 32-14.3 32-32V546h236c17.7 0 32-14.3 32-32V242c0-17.7-14.3-32-32-32zm-504 68h200v200H412V278zm-68 468H144V546h200v200zm268 0H412V546h200v200zm268-268H680V278h200v200z`}}]},name:`build`,theme:`outlined`};function iU(){return iU=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,iU({},e,{ref:t,icon:rU}))),oU={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M632 888H392c-4.4 0-8 3.6-8 8v32c0 17.7 14.3 32 32 32h192c17.7 0 32-14.3 32-32v-32c0-4.4-3.6-8-8-8zM512 64c-181.1 0-328 146.9-328 328 0 121.4 66 227.4 164 284.1V792c0 17.7 14.3 32 32 32h264c17.7 0 32-14.3 32-32V676.1c98-56.7 164-162.7 164-284.1 0-181.1-146.9-328-328-328zm127.9 549.8L604 634.6V752H420V634.6l-35.9-20.8C305.4 568.3 256 484.5 256 392c0-141.4 114.6-256 256-256s256 114.6 256 256c0 92.5-49.4 176.3-128.1 221.8z`}}]},name:`bulb`,theme:`outlined`};function sU(){return sU=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,sU({},e,{ref:t,icon:oU}))),lU={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M251.2 387H320v68.8c0 1.8 1.8 3.2 4 3.2h48c2.2 0 4-1.4 4-3.3V387h68.8c1.8 0 3.2-1.8 3.2-4v-48c0-2.2-1.4-4-3.3-4H376v-68.8c0-1.8-1.8-3.2-4-3.2h-48c-2.2 0-4 1.4-4 3.2V331h-68.8c-1.8 0-3.2 1.8-3.2 4v48c0 2.2 1.4 4 3.2 4zm328 0h193.6c1.8 0 3.2-1.8 3.2-4v-48c0-2.2-1.4-4-3.3-4H579.2c-1.8 0-3.2 1.8-3.2 4v48c0 2.2 1.4 4 3.2 4zm0 265h193.6c1.8 0 3.2-1.8 3.2-4v-48c0-2.2-1.4-4-3.3-4H579.2c-1.8 0-3.2 1.8-3.2 4v48c0 2.2 1.4 4 3.2 4zm0 104h193.6c1.8 0 3.2-1.8 3.2-4v-48c0-2.2-1.4-4-3.3-4H579.2c-1.8 0-3.2 1.8-3.2 4v48c0 2.2 1.4 4 3.2 4zm-195.7-81l61.2-74.9c4.3-5.2.7-13.1-5.9-13.1H388c-2.3 0-4.5 1-5.9 2.9l-34 41.6-34-41.6a7.85 7.85 0 00-5.9-2.9h-50.9c-6.6 0-10.2 7.9-5.9 13.1l61.2 74.9-62.7 76.8c-4.4 5.2-.8 13.1 5.8 13.1h50.8c2.3 0 4.5-1 5.9-2.9l35.5-43.5 35.5 43.5c1.5 1.8 3.7 2.9 5.9 2.9h50.8c6.6 0 10.2-7.9 5.9-13.1L383.5 675zM880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zm-36 732H180V180h664v664z`}}]},name:`calculator`,theme:`outlined`};function uU(){return uU=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,uU({},e,{ref:t,icon:lU}))),fU={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M864 248H728l-32.4-90.8a32.07 32.07 0 00-30.2-21.2H358.6c-13.5 0-25.6 8.5-30.1 21.2L296 248H160c-44.2 0-80 35.8-80 80v456c0 44.2 35.8 80 80 80h704c44.2 0 80-35.8 80-80V328c0-44.2-35.8-80-80-80zm8 536c0 4.4-3.6 8-8 8H160c-4.4 0-8-3.6-8-8V328c0-4.4 3.6-8 8-8h186.7l17.1-47.8 22.9-64.2h250.5l22.9 64.2 17.1 47.8H864c4.4 0 8 3.6 8 8v456zM512 384c-88.4 0-160 71.6-160 160s71.6 160 160 160 160-71.6 160-160-71.6-160-160-160zm0 256c-53 0-96-43-96-96s43-96 96-96 96 43 96 96-43 96-96 96z`}}]},name:`camera`,theme:`outlined`};function pU(){return pU=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,pU({},e,{ref:t,icon:fU}))),hU={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M380 704h264c4.4 0 8-3.6 8-8v-84c0-4.4-3.6-8-8-8h-40c-4.4 0-8 3.6-8 8v36H428v-36c0-4.4-3.6-8-8-8h-40c-4.4 0-8 3.6-8 8v84c0 4.4 3.6 8 8 8zm340-123a40 40 0 1080 0 40 40 0 10-80 0zm239-167.6L935.3 372a8 8 0 00-10.9-2.9l-50.7 29.6-78.3-216.2a63.9 63.9 0 00-60.9-44.4H301.2c-34.7 0-65.5 22.4-76.2 55.5l-74.6 205.2-50.8-29.6a8 8 0 00-10.9 2.9L65 413.4c-2.2 3.8-.9 8.6 2.9 10.8l60.4 35.2-14.5 40c-1.2 3.2-1.8 6.6-1.8 10v348.2c0 15.7 11.8 28.4 26.3 28.4h67.6c12.3 0 23-9.3 25.6-22.3l7.7-37.7h545.6l7.7 37.7c2.7 13 13.3 22.3 25.6 22.3h67.6c14.5 0 26.3-12.7 26.3-28.4V509.4c0-3.4-.6-6.8-1.8-10l-14.5-40 60.3-35.2a8 8 0 003-10.8zM840 517v237H184V517l15.6-43h624.8l15.6 43zM292.7 218.1l.5-1.3.4-1.3c1.1-3.3 4.1-5.5 7.6-5.5h427.6l75.4 208H220l72.7-199.9zM224 581a40 40 0 1080 0 40 40 0 10-80 0z`}}]},name:`car`,theme:`outlined`};function gU(){return gU=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,gU({},e,{ref:t,icon:hU}))),vU={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M699 353h-46.9c-10.2 0-19.9 4.9-25.9 13.3L469 584.3l-71.2-98.8c-6-8.3-15.6-13.3-25.9-13.3H325c-6.5 0-10.3 7.4-6.5 12.7l124.6 172.8a31.8 31.8 0 0051.7 0l210.6-292c3.9-5.3.1-12.7-6.4-12.7z`}},{tag:`path`,attrs:{d:`M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z`}}]},name:`check-circle`,theme:`outlined`};function yU(){return yU=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,yU({},e,{ref:t,icon:vU}))),xU={icon:{tag:`svg`,attrs:{"fill-rule":`evenodd`,viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm0 76c-205.4 0-372 166.6-372 372s166.6 372 372 372 372-166.6 372-372-166.6-372-372-372zm128.01 198.83c.03 0 .05.01.09.06l45.02 45.01a.2.2 0 01.05.09.12.12 0 010 .07c0 .02-.01.04-.05.08L557.25 512l127.87 127.86a.27.27 0 01.05.06v.02a.12.12 0 010 .07c0 .03-.01.05-.05.09l-45.02 45.02a.2.2 0 01-.09.05.12.12 0 01-.07 0c-.02 0-.04-.01-.08-.05L512 557.25 384.14 685.12c-.04.04-.06.05-.08.05a.12.12 0 01-.07 0c-.03 0-.05-.01-.09-.05l-45.02-45.02a.2.2 0 01-.05-.09.12.12 0 010-.07c0-.02.01-.04.06-.08L466.75 512 338.88 384.14a.27.27 0 01-.05-.06l-.01-.02a.12.12 0 010-.07c0-.03.01-.05.05-.09l45.02-45.02a.2.2 0 01.09-.05.12.12 0 01.07 0c.02 0 .04.01.08.06L512 466.75l127.86-127.86c.04-.05.06-.06.08-.06a.12.12 0 01.07 0z`}}]},name:`close-circle`,theme:`outlined`};function SU(){return SU=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,SU({},e,{ref:t,icon:xU}))),wU={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M811.4 418.7C765.6 297.9 648.9 212 512.2 212S258.8 297.8 213 418.6C127.3 441.1 64 519.1 64 612c0 110.5 89.5 200 199.9 200h496.2C870.5 812 960 722.5 960 612c0-92.7-63.1-170.7-148.6-193.3zm36.3 281a123.07 123.07 0 01-87.6 36.3H263.9c-33.1 0-64.2-12.9-87.6-36.3A123.3 123.3 0 01140 612c0-28 9.1-54.3 26.2-76.3a125.7 125.7 0 0166.1-43.7l37.9-9.9 13.9-36.6c8.6-22.8 20.6-44.1 35.7-63.4a245.6 245.6 0 0152.4-49.9c41.1-28.9 89.5-44.2 140-44.2s98.9 15.3 140 44.2c19.9 14 37.5 30.8 52.4 49.9 15.1 19.3 27.1 40.7 35.7 63.4l13.8 36.5 37.8 10c54.3 14.5 92.1 63.8 92.1 120 0 33.1-12.9 64.3-36.3 87.7z`}}]},name:`cloud`,theme:`outlined`};function TU(){return TU=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,TU({},e,{ref:t,icon:wU}))),DU={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M704 446H320c-4.4 0-8 3.6-8 8v402c0 4.4 3.6 8 8 8h384c4.4 0 8-3.6 8-8V454c0-4.4-3.6-8-8-8zm-328 64h272v117H376V510zm272 290H376V683h272v117z`}},{tag:`path`,attrs:{d:`M424 748a32 32 0 1064 0 32 32 0 10-64 0zm0-178a32 32 0 1064 0 32 32 0 10-64 0z`}},{tag:`path`,attrs:{d:`M811.4 368.9C765.6 248 648.9 162 512.2 162S258.8 247.9 213 368.8C126.9 391.5 63.5 470.2 64 563.6 64.6 668 145.6 752.9 247.6 762c4.7.4 8.7-3.3 8.7-8v-60.4c0-4-3-7.4-7-7.9-27-3.4-52.5-15.2-72.1-34.5-24-23.5-37.2-55.1-37.2-88.6 0-28 9.1-54.4 26.2-76.4 16.7-21.4 40.2-36.9 66.1-43.7l37.9-10 13.9-36.7c8.6-22.8 20.6-44.2 35.7-63.5 14.9-19.2 32.6-36 52.4-50 41.1-28.9 89.5-44.2 140-44.2s98.9 15.3 140 44.3c19.9 14 37.5 30.8 52.4 50 15.1 19.3 27.1 40.7 35.7 63.5l13.8 36.6 37.8 10c54.2 14.4 92.1 63.7 92.1 120 0 33.6-13.2 65.1-37.2 88.6-19.5 19.2-44.9 31.1-71.9 34.5-4 .5-6.9 3.9-6.9 7.9V754c0 4.7 4.1 8.4 8.8 8 101.7-9.2 182.5-94 183.2-198.2.6-93.4-62.7-172.1-148.6-194.9z`}}]},name:`cloud-server`,theme:`outlined`};function OU(){return OU=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,OU({},e,{ref:t,icon:DU}))),AU={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M516 673c0 4.4 3.4 8 7.5 8h185c4.1 0 7.5-3.6 7.5-8v-48c0-4.4-3.4-8-7.5-8h-185c-4.1 0-7.5 3.6-7.5 8v48zm-194.9 6.1l192-161c3.8-3.2 3.8-9.1 0-12.3l-192-160.9A7.95 7.95 0 00308 351v62.7c0 2.4 1 4.6 2.9 6.1L420.7 512l-109.8 92.2a8.1 8.1 0 00-2.9 6.1V673c0 6.8 7.9 10.5 13.1 6.1zM880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zm-40 728H184V184h656v656z`}}]},name:`code`,theme:`outlined`};function jU(){return jU=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,jU({},e,{ref:t,icon:AU}))),NU={icon:{tag:`svg`,attrs:{viewBox:`0 0 1024 1024`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M275 281c19.9 0 36-16.1 36-36V36c0-19.9-16.1-36-36-36s-36 16.1-36 36v209c0 19.9 16.1 36 36 36zm613 144H768c0-39.8-32.2-72-72-72H200c-39.8 0-72 32.2-72 72v248c0 3.4.2 6.7.7 9.9-.5 7-.7 14-.7 21.1 0 176.7 143.3 320 320 320 160.1 0 292.7-117.5 316.3-271H888c39.8 0 72-32.2 72-72V497c0-39.8-32.2-72-72-72zM696 681h-1.1c.7 7.6 1.1 15.2 1.1 23 0 137-111 248-248 248S200 841 200 704c0-7.8.4-15.4 1.1-23H200V425h496v256zm192-8H776V497h112v176zM613 281c19.9 0 36-16.1 36-36V36c0-19.9-16.1-36-36-36s-36 16.1-36 36v209c0 19.9 16.1 36 36 36zm-170 0c19.9 0 36-16.1 36-36V36c0-19.9-16.1-36-36-36s-36 16.1-36 36v209c0 19.9 16.1 36 36 36z`}}]},name:`coffee`,theme:`outlined`};function PU(){return PU=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,PU({},e,{ref:t,icon:NU}))),IU={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372zm198.4-588.1a32 32 0 00-24.5.5L414.9 415 296.4 686c-3.6 8.2-3.6 17.5 0 25.7 3.4 7.8 9.7 13.9 17.7 17 3.8 1.5 7.7 2.2 11.7 2.2 4.4 0 8.7-.9 12.8-2.7l271-118.6 118.5-271a32.06 32.06 0 00-17.7-42.7zM576.8 534.4l26.2 26.2-42.4 42.4-26.2-26.2L380 644.4 447.5 490 422 464.4l42.4-42.4 25.5 25.5L644.4 380l-67.6 154.4zM464.4 422L422 464.4l25.5 25.6 86.9 86.8 26.2 26.2 42.4-42.4-26.2-26.2-86.8-86.9z`}}]},name:`compass`,theme:`outlined`};function LU(){return LU=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,LU({},e,{ref:t,icon:IU}))),zU={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M899.6 276.5L705 396.4 518.4 147.5a8.06 8.06 0 00-12.9 0L319 396.4 124.3 276.5c-5.7-3.5-13.1 1.2-12.2 7.9L188.5 865c1.1 7.9 7.9 14 16 14h615.1c8 0 14.9-6 15.9-14l76.4-580.6c.8-6.7-6.5-11.4-12.3-7.9zm-126 534.1H250.3l-53.8-409.4 139.8 86.1L512 252.9l175.7 234.4 139.8-86.1-53.9 409.4zM512 509c-62.1 0-112.6 50.5-112.6 112.6S449.9 734.2 512 734.2s112.6-50.5 112.6-112.6S574.1 509 512 509zm0 160.9c-26.6 0-48.2-21.6-48.2-48.3 0-26.6 21.6-48.3 48.2-48.3s48.2 21.6 48.2 48.3c0 26.6-21.6 48.3-48.2 48.3z`}}]},name:`crown`,theme:`outlined`};function BU(){return BU=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,BU({},e,{ref:t,icon:zU}))),HU={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M924.8 385.6a446.7 446.7 0 00-96-142.4 446.7 446.7 0 00-142.4-96C631.1 123.8 572.5 112 512 112s-119.1 11.8-174.4 35.2a446.7 446.7 0 00-142.4 96 446.7 446.7 0 00-96 142.4C75.8 440.9 64 499.5 64 560c0 132.7 58.3 257.7 159.9 343.1l1.7 1.4c5.8 4.8 13.1 7.5 20.6 7.5h531.7c7.5 0 14.8-2.7 20.6-7.5l1.7-1.4C901.7 817.7 960 692.7 960 560c0-60.5-11.9-119.1-35.2-174.4zM761.4 836H262.6A371.12 371.12 0 01140 560c0-99.4 38.7-192.8 109-263 70.3-70.3 163.7-109 263-109 99.4 0 192.8 38.7 263 109 70.3 70.3 109 163.7 109 263 0 105.6-44.5 205.5-122.6 276zM623.5 421.5a8.03 8.03 0 00-11.3 0L527.7 506c-18.7-5-39.4-.2-54.1 14.5a55.95 55.95 0 000 79.2 55.95 55.95 0 0079.2 0 55.87 55.87 0 0014.5-54.1l84.5-84.5c3.1-3.1 3.1-8.2 0-11.3l-28.3-28.3zM490 320h44c4.4 0 8-3.6 8-8v-80c0-4.4-3.6-8-8-8h-44c-4.4 0-8 3.6-8 8v80c0 4.4 3.6 8 8 8zm260 218v44c0 4.4 3.6 8 8 8h80c4.4 0 8-3.6 8-8v-44c0-4.4-3.6-8-8-8h-80c-4.4 0-8 3.6-8 8zm12.7-197.2l-31.1-31.1a8.03 8.03 0 00-11.3 0l-56.6 56.6a8.03 8.03 0 000 11.3l31.1 31.1c3.1 3.1 8.2 3.1 11.3 0l56.6-56.6c3.1-3.1 3.1-8.2 0-11.3zm-458.6-31.1a8.03 8.03 0 00-11.3 0l-31.1 31.1a8.03 8.03 0 000 11.3l56.6 56.6c3.1 3.1 8.2 3.1 11.3 0l31.1-31.1c3.1-3.1 3.1-8.2 0-11.3l-56.6-56.6zM262 530h-80c-4.4 0-8 3.6-8 8v44c0 4.4 3.6 8 8 8h80c4.4 0 8-3.6 8-8v-44c0-4.4-3.6-8-8-8z`}}]},name:`dashboard`,theme:`outlined`};function UU(){return UU=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,UU({},e,{ref:t,icon:HU}))),GU={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32zm-600 72h560v208H232V136zm560 480H232V408h560v208zm0 272H232V680h560v208zM304 240a40 40 0 1080 0 40 40 0 10-80 0zm0 272a40 40 0 1080 0 40 40 0 10-80 0zm0 272a40 40 0 1080 0 40 40 0 10-80 0z`}}]},name:`database`,theme:`outlined`};function KU(){return KU=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,KU({},e,{ref:t,icon:GU}))),JU={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372zm47.7-395.2l-25.4-5.9V348.6c38 5.2 61.5 29 65.5 58.2.5 4 3.9 6.9 7.9 6.9h44.9c4.7 0 8.4-4.1 8-8.8-6.1-62.3-57.4-102.3-125.9-109.2V263c0-4.4-3.6-8-8-8h-28.1c-4.4 0-8 3.6-8 8v33c-70.8 6.9-126.2 46-126.2 119 0 67.6 49.8 100.2 102.1 112.7l24.7 6.3v142.7c-44.2-5.9-69-29.5-74.1-61.3-.6-3.8-4-6.6-7.9-6.6H363c-4.7 0-8.4 4-8 8.7 4.5 55 46.2 105.6 135.2 112.1V761c0 4.4 3.6 8 8 8h28.4c4.4 0 8-3.6 8-8.1l-.2-31.7c78.3-6.9 134.3-48.8 134.3-124-.1-69.4-44.2-100.4-109-116.4zm-68.6-16.2c-5.6-1.6-10.3-3.1-15-5-33.8-12.2-49.5-31.9-49.5-57.3 0-36.3 27.5-57 64.5-61.7v124zM534.3 677V543.3c3.1.9 5.9 1.6 8.8 2.2 47.3 14.4 63.2 34.4 63.2 65.1 0 39.1-29.4 62.6-72 66.4z`}}]},name:`dollar`,theme:`outlined`};function YU(){return YU=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,YU({},e,{ref:t,icon:JU}))),ZU={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z`}},{tag:`path`,attrs:{d:`M464 688a48 48 0 1096 0 48 48 0 10-96 0zm24-112h48c4.4 0 8-3.6 8-8V296c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8z`}}]},name:`exclamation-circle`,theme:`outlined`};function QU(){return QU=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,QU({},e,{ref:t,icon:ZU}))),eW={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M512 472a40 40 0 1080 0 40 40 0 10-80 0zm367 352.9L696.3 352V178H768v-68H256v68h71.7v174L145 824.9c-2.8 7.4-4.3 15.2-4.3 23.1 0 35.3 28.7 64 64 64h614.6c7.9 0 15.7-1.5 23.1-4.3 33-12.7 49.4-49.8 36.6-82.8zM395.7 364.7V180h232.6v184.7L719.2 600c-20.7-5.3-42.1-8-63.9-8-61.2 0-119.2 21.5-165.3 60a188.78 188.78 0 01-121.3 43.9c-32.7 0-64.1-8.3-91.8-23.7l118.8-307.5zM210.5 844l41.7-107.8c35.7 18.1 75.4 27.8 116.6 27.8 61.2 0 119.2-21.5 165.3-60 33.9-28.2 76.3-43.9 121.3-43.9 35 0 68.4 9.5 97.6 27.1L813.5 844h-603z`}}]},name:`experiment`,theme:`outlined`};function tW(){return tW=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,tW({},e,{ref:t,icon:eW}))),rW={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M553.1 509.1l-77.8 99.2-41.1-52.4a8 8 0 00-12.6 0l-99.8 127.2a7.98 7.98 0 006.3 12.9H696c6.7 0 10.4-7.7 6.3-12.9l-136.5-174a8.1 8.1 0 00-12.7 0zM360 442a40 40 0 1080 0 40 40 0 10-80 0zm494.6-153.4L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216a42 42 0 0042 42h216v494z`}}]},name:`file-image`,theme:`outlined`};function iW(){return iW=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,iW({},e,{ref:t,icon:rW}))),oW={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M531.3 574.4l.3-1.4c5.8-23.9 13.1-53.7 7.4-80.7-3.8-21.3-19.5-29.6-32.9-30.2-15.8-.7-29.9 8.3-33.4 21.4-6.6 24-.7 56.8 10.1 98.6-13.6 32.4-35.3 79.5-51.2 107.5-29.6 15.3-69.3 38.9-75.2 68.7-1.2 5.5.2 12.5 3.5 18.8 3.7 7 9.6 12.4 16.5 15 3 1.1 6.6 2 10.8 2 17.6 0 46.1-14.2 84.1-79.4 5.8-1.9 11.8-3.9 17.6-5.9 27.2-9.2 55.4-18.8 80.9-23.1 28.2 15.1 60.3 24.8 82.1 24.8 21.6 0 30.1-12.8 33.3-20.5 5.6-13.5 2.9-30.5-6.2-39.6-13.2-13-45.3-16.4-95.3-10.2-24.6-15-40.7-35.4-52.4-65.8zM421.6 726.3c-13.9 20.2-24.4 30.3-30.1 34.7 6.7-12.3 19.8-25.3 30.1-34.7zm87.6-235.5c5.2 8.9 4.5 35.8.5 49.4-4.9-19.9-5.6-48.1-2.7-51.4.8.1 1.5.7 2.2 2zm-1.6 120.5c10.7 18.5 24.2 34.4 39.1 46.2-21.6 4.9-41.3 13-58.9 20.2-4.2 1.7-8.3 3.4-12.3 5 13.3-24.1 24.4-51.4 32.1-71.4zm155.6 65.5c.1.2.2.5-.4.9h-.2l-.2.3c-.8.5-9 5.3-44.3-8.6 40.6-1.9 45 7.3 45.1 7.4zm191.4-388.2L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216a42 42 0 0042 42h216v494z`}}]},name:`file-pdf`,theme:`outlined`};function sW(){return sW=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,sW({},e,{ref:t,icon:oW}))),lW={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M834.1 469.2A347.49 347.49 0 00751.2 354l-29.1-26.7a8.09 8.09 0 00-13 3.3l-13 37.3c-8.1 23.4-23 47.3-44.1 70.8-1.4 1.5-3 1.9-4.1 2-1.1.1-2.8-.1-4.3-1.5-1.4-1.2-2.1-3-2-4.8 3.7-60.2-14.3-128.1-53.7-202C555.3 171 510 123.1 453.4 89.7l-41.3-24.3c-5.4-3.2-12.3 1-12 7.3l2.2 48c1.5 32.8-2.3 61.8-11.3 85.9-11 29.5-26.8 56.9-47 81.5a295.64 295.64 0 01-47.5 46.1 352.6 352.6 0 00-100.3 121.5A347.75 347.75 0 00160 610c0 47.2 9.3 92.9 27.7 136a349.4 349.4 0 0075.5 110.9c32.4 32 70 57.2 111.9 74.7C418.5 949.8 464.5 959 512 959s93.5-9.2 136.9-27.3A348.6 348.6 0 00760.8 857c32.4-32 57.8-69.4 75.5-110.9a344.2 344.2 0 0027.7-136c0-48.8-10-96.2-29.9-140.9zM713 808.5c-53.7 53.2-125 82.4-201 82.4s-147.3-29.2-201-82.4c-53.5-53.1-83-123.5-83-198.4 0-43.5 9.8-85.2 29.1-124 18.8-37.9 46.8-71.8 80.8-97.9a349.6 349.6 0 0058.6-56.8c25-30.5 44.6-64.5 58.2-101a240 240 0 0012.1-46.5c24.1 22.2 44.3 49 61.2 80.4 33.4 62.6 48.8 118.3 45.8 165.7a74.01 74.01 0 0024.4 59.8 73.36 73.36 0 0053.4 18.8c19.7-1 37.8-9.7 51-24.4 13.3-14.9 24.8-30.1 34.4-45.6 14 17.9 25.7 37.4 35 58.4 15.9 35.8 24 73.9 24 113.1 0 74.9-29.5 145.4-83 198.4z`}}]},name:`fire`,theme:`outlined`};function uW(){return uW=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,uW({},e,{ref:t,icon:lW}))),fW={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M880 305H624V192c0-17.7-14.3-32-32-32H184v-40c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v784c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V640h248v113c0 17.7 14.3 32 32 32h416c17.7 0 32-14.3 32-32V337c0-17.7-14.3-32-32-32zM184 568V232h368v336H184zm656 145H504v-73h112c4.4 0 8-3.6 8-8V377h216v336z`}}]},name:`flag`,theme:`outlined`};function pW(){return pW=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,pW({},e,{ref:t,icon:fW}))),hW={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M920 416H616c-4.4 0-8 3.6-8 8v112c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-56h60v320h-46c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h164c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8h-46V480h60v56c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V424c0-4.4-3.6-8-8-8zM656 296V168c0-4.4-3.6-8-8-8H104c-4.4 0-8 3.6-8 8v128c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-64h168v560h-92c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h264c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-92V232h168v64c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8z`}}]},name:`font-size`,theme:`outlined`};function gW(){return gW=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,gW({},e,{ref:t,icon:hW}))),vW={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M880 310H732.4c13.6-21.4 21.6-46.8 21.6-74 0-76.1-61.9-138-138-138-41.4 0-78.7 18.4-104 47.4-25.3-29-62.6-47.4-104-47.4-76.1 0-138 61.9-138 138 0 27.2 7.9 52.6 21.6 74H144c-17.7 0-32 14.3-32 32v200c0 4.4 3.6 8 8 8h40v344c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V550h40c4.4 0 8-3.6 8-8V342c0-17.7-14.3-32-32-32zm-334-74c0-38.6 31.4-70 70-70s70 31.4 70 70-31.4 70-70 70h-70v-70zm-138-70c38.6 0 70 31.4 70 70v70h-70c-38.6 0-70-31.4-70-70s31.4-70 70-70zM180 482V378h298v104H180zm48 68h250v308H228V550zm568 308H546V550h250v308zm48-376H546V378h298v104z`}}]},name:`gift`,theme:`outlined`};function yW(){return yW=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,yW({},e,{ref:t,icon:vW}))),xW={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M854.4 800.9c.2-.3.5-.6.7-.9C920.6 722.1 960 621.7 960 512s-39.4-210.1-104.8-288c-.2-.3-.5-.5-.7-.8-1.1-1.3-2.1-2.5-3.2-3.7-.4-.5-.8-.9-1.2-1.4l-4.1-4.7-.1-.1c-1.5-1.7-3.1-3.4-4.6-5.1l-.1-.1c-3.2-3.4-6.4-6.8-9.7-10.1l-.1-.1-4.8-4.8-.3-.3c-1.5-1.5-3-2.9-4.5-4.3-.5-.5-1-1-1.6-1.5-1-1-2-1.9-3-2.8-.3-.3-.7-.6-1-1C736.4 109.2 629.5 64 512 64s-224.4 45.2-304.3 119.2c-.3.3-.7.6-1 1-1 .9-2 1.9-3 2.9-.5.5-1 1-1.6 1.5-1.5 1.4-3 2.9-4.5 4.3l-.3.3-4.8 4.8-.1.1c-3.3 3.3-6.5 6.7-9.7 10.1l-.1.1c-1.6 1.7-3.1 3.4-4.6 5.1l-.1.1c-1.4 1.5-2.8 3.1-4.1 4.7-.4.5-.8.9-1.2 1.4-1.1 1.2-2.1 2.5-3.2 3.7-.2.3-.5.5-.7.8C103.4 301.9 64 402.3 64 512s39.4 210.1 104.8 288c.2.3.5.6.7.9l3.1 3.7c.4.5.8.9 1.2 1.4l4.1 4.7c0 .1.1.1.1.2 1.5 1.7 3 3.4 4.6 5l.1.1c3.2 3.4 6.4 6.8 9.6 10.1l.1.1c1.6 1.6 3.1 3.2 4.7 4.7l.3.3c3.3 3.3 6.7 6.5 10.1 9.6 80.1 74 187 119.2 304.5 119.2s224.4-45.2 304.3-119.2a300 300 0 0010-9.6l.3-.3c1.6-1.6 3.2-3.1 4.7-4.7l.1-.1c3.3-3.3 6.5-6.7 9.6-10.1l.1-.1c1.5-1.7 3.1-3.3 4.6-5 0-.1.1-.1.1-.2 1.4-1.5 2.8-3.1 4.1-4.7.4-.5.8-.9 1.2-1.4a99 99 0 003.3-3.7zm4.1-142.6c-13.8 32.6-32 62.8-54.2 90.2a444.07 444.07 0 00-81.5-55.9c11.6-46.9 18.8-98.4 20.7-152.6H887c-3 40.9-12.6 80.6-28.5 118.3zM887 484H743.5c-1.9-54.2-9.1-105.7-20.7-152.6 29.3-15.6 56.6-34.4 81.5-55.9A373.86 373.86 0 01887 484zM658.3 165.5c39.7 16.8 75.8 40 107.6 69.2a394.72 394.72 0 01-59.4 41.8c-15.7-45-35.8-84.1-59.2-115.4 3.7 1.4 7.4 2.9 11 4.4zm-90.6 700.6c-9.2 7.2-18.4 12.7-27.7 16.4V697a389.1 389.1 0 01115.7 26.2c-8.3 24.6-17.9 47.3-29 67.8-17.4 32.4-37.8 58.3-59 75.1zm59-633.1c11 20.6 20.7 43.3 29 67.8A389.1 389.1 0 01540 327V141.6c9.2 3.7 18.5 9.1 27.7 16.4 21.2 16.7 41.6 42.6 59 75zM540 640.9V540h147.5c-1.6 44.2-7.1 87.1-16.3 127.8l-.3 1.2A445.02 445.02 0 00540 640.9zm0-156.9V383.1c45.8-2.8 89.8-12.5 130.9-28.1l.3 1.2c9.2 40.7 14.7 83.5 16.3 127.8H540zm-56 56v100.9c-45.8 2.8-89.8 12.5-130.9 28.1l-.3-1.2c-9.2-40.7-14.7-83.5-16.3-127.8H484zm-147.5-56c1.6-44.2 7.1-87.1 16.3-127.8l.3-1.2c41.1 15.6 85 25.3 130.9 28.1V484H336.5zM484 697v185.4c-9.2-3.7-18.5-9.1-27.7-16.4-21.2-16.7-41.7-42.7-59.1-75.1-11-20.6-20.7-43.3-29-67.8 37.2-14.6 75.9-23.3 115.8-26.1zm0-370a389.1 389.1 0 01-115.7-26.2c8.3-24.6 17.9-47.3 29-67.8 17.4-32.4 37.8-58.4 59.1-75.1 9.2-7.2 18.4-12.7 27.7-16.4V327zM365.7 165.5c3.7-1.5 7.3-3 11-4.4-23.4 31.3-43.5 70.4-59.2 115.4-21-12-40.9-26-59.4-41.8 31.8-29.2 67.9-52.4 107.6-69.2zM165.5 365.7c13.8-32.6 32-62.8 54.2-90.2 24.9 21.5 52.2 40.3 81.5 55.9-11.6 46.9-18.8 98.4-20.7 152.6H137c3-40.9 12.6-80.6 28.5-118.3zM137 540h143.5c1.9 54.2 9.1 105.7 20.7 152.6a444.07 444.07 0 00-81.5 55.9A373.86 373.86 0 01137 540zm228.7 318.5c-39.7-16.8-75.8-40-107.6-69.2 18.5-15.8 38.4-29.7 59.4-41.8 15.7 45 35.8 84.1 59.2 115.4-3.7-1.4-7.4-2.9-11-4.4zm292.6 0c-3.7 1.5-7.3 3-11 4.4 23.4-31.3 43.5-70.4 59.2-115.4 21 12 40.9 26 59.4 41.8a373.81 373.81 0 01-107.6 69.2z`}}]},name:`global`,theme:`outlined`};function SW(){return SW=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,SW({},e,{ref:t,icon:xW}))),wW={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M923 283.6a260.04 260.04 0 00-56.9-82.8 264.4 264.4 0 00-84-55.5A265.34 265.34 0 00679.7 125c-49.3 0-97.4 13.5-139.2 39-10 6.1-19.5 12.8-28.5 20.1-9-7.3-18.5-14-28.5-20.1-41.8-25.5-89.9-39-139.2-39-35.5 0-69.9 6.8-102.4 20.3-31.4 13-59.7 31.7-84 55.5a258.44 258.44 0 00-56.9 82.8c-13.9 32.3-21 66.6-21 101.9 0 33.3 6.8 68 20.3 103.3 11.3 29.5 27.5 60.1 48.2 91 32.8 48.9 77.9 99.9 133.9 151.6 92.8 85.7 184.7 144.9 188.6 147.3l23.7 15.2c10.5 6.7 24 6.7 34.5 0l23.7-15.2c3.9-2.5 95.7-61.6 188.6-147.3 56-51.7 101.1-102.7 133.9-151.6 20.7-30.9 37-61.5 48.2-91 13.5-35.3 20.3-70 20.3-103.3.1-35.3-7-69.6-20.9-101.9zM512 814.8S156 586.7 156 385.5C156 283.6 240.3 201 344.3 201c73.1 0 136.5 40.8 167.7 100.4C543.2 241.8 606.6 201 679.7 201c104 0 188.3 82.6 188.3 184.5 0 201.2-356 429.3-356 429.3z`}}]},name:`heart`,theme:`outlined`};function TW(){return TW=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,TW({},e,{ref:t,icon:wW}))),DW={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M957.6 507.4L603.2 158.2a7.9 7.9 0 00-11.2 0L353.3 393.4a8.03 8.03 0 00-.1 11.3l.1.1 40 39.4-117.2 115.3a8.03 8.03 0 00-.1 11.3l.1.1 39.5 38.9-189.1 187H72.1c-4.4 0-8.1 3.6-8.1 8V860c0 4.4 3.6 8 8 8h344.9c2.1 0 4.1-.8 5.6-2.3l76.1-75.6 40.4 39.8a7.9 7.9 0 0011.2 0l117.1-115.6 40.1 39.5a7.9 7.9 0 0011.2 0l238.7-235.2c3.4-3 3.4-8 .3-11.2zM389.8 796.2H229.6l134.4-133 80.1 78.9-54.3 54.1zm154.8-62.1L373.2 565.2l68.6-67.6 171.4 168.9-68.6 67.6zM713.1 658L450.3 399.1 597.6 254l262.8 259-147.3 145z`}}]},name:`highlight`,theme:`outlined`};function OW(){return OW=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,OW({},e,{ref:t,icon:DW}))),AW={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M536.1 273H488c-4.4 0-8 3.6-8 8v275.3c0 2.6 1.2 5 3.3 6.5l165.3 120.7c3.6 2.6 8.6 1.9 11.2-1.7l28.6-39c2.7-3.7 1.9-8.7-1.7-11.2L544.1 528.5V281c0-4.4-3.6-8-8-8zm219.8 75.2l156.8 38.3c5 1.2 9.9-2.6 9.9-7.7l.8-161.5c0-6.7-7.7-10.5-12.9-6.3L752.9 334.1a8 8 0 003 14.1zm167.7 301.1l-56.7-19.5a8 8 0 00-10.1 4.8c-1.9 5.1-3.9 10.1-6 15.1-17.8 42.1-43.3 80-75.9 112.5a353 353 0 01-112.5 75.9 352.18 352.18 0 01-137.7 27.8c-47.8 0-94.1-9.3-137.7-27.8a353 353 0 01-112.5-75.9c-32.5-32.5-58-70.4-75.9-112.5A353.44 353.44 0 01171 512c0-47.8 9.3-94.2 27.8-137.8 17.8-42.1 43.3-80 75.9-112.5a353 353 0 01112.5-75.9C430.6 167.3 477 158 524.8 158s94.1 9.3 137.7 27.8A353 353 0 01775 261.7c10.2 10.3 19.8 21 28.6 32.3l59.8-46.8C784.7 146.6 662.2 81.9 524.6 82 285 82.1 92.6 276.7 95 516.4 97.4 751.9 288.9 942 524.8 942c185.5 0 343.5-117.6 403.7-282.3 1.5-4.2-.7-8.9-4.9-10.4z`}}]},name:`history`,theme:`outlined`};function jW(){return jW=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,jW({},e,{ref:t,icon:AW}))),NW={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M946.5 505L560.1 118.8l-25.9-25.9a31.5 31.5 0 00-44.4 0L77.5 505a63.9 63.9 0 00-18.8 46c.4 35.2 29.7 63.3 64.9 63.3h42.5V940h691.8V614.3h43.4c17.1 0 33.2-6.7 45.3-18.8a63.6 63.6 0 0018.7-45.3c0-17-6.7-33.1-18.8-45.2zM568 868H456V664h112v204zm217.9-325.7V868H632V640c0-22.1-17.9-40-40-40H432c-22.1 0-40 17.9-40 40v228H238.1V542.3h-96l370-369.7 23.1 23.1L882 542.3h-96.1z`}}]},name:`home`,theme:`outlined`};function PW(){return PW=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,PW({},e,{ref:t,icon:NW}))),IW={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 632H136V232h752v560zM610.3 476h123.4c1.3 0 2.3-3.6 2.3-8v-48c0-4.4-1-8-2.3-8H610.3c-1.3 0-2.3 3.6-2.3 8v48c0 4.4 1 8 2.3 8zm4.8 144h185.7c3.9 0 7.1-3.6 7.1-8v-48c0-4.4-3.2-8-7.1-8H615.1c-3.9 0-7.1 3.6-7.1 8v48c0 4.4 3.2 8 7.1 8zM224 673h43.9c4.2 0 7.6-3.3 7.9-7.5 3.8-50.5 46-90.5 97.2-90.5s93.4 40 97.2 90.5c.3 4.2 3.7 7.5 7.9 7.5H522a8 8 0 008-8.4c-2.8-53.3-32-99.7-74.6-126.1a111.8 111.8 0 0029.1-75.5c0-61.9-49.9-112-111.4-112s-111.4 50.1-111.4 112c0 29.1 11 55.5 29.1 75.5a158.09 158.09 0 00-74.6 126.1c-.4 4.6 3.2 8.4 7.8 8.4zm149-262c28.5 0 51.7 23.3 51.7 52s-23.2 52-51.7 52-51.7-23.3-51.7-52 23.2-52 51.7-52z`}}]},name:`idcard`,theme:`outlined`};function LW(){return LW=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,LW({},e,{ref:t,icon:IW}))),zW={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M956.9 845.1L896.4 632V168c0-17.7-14.3-32-32-32h-704c-17.7 0-32 14.3-32 32v464L67.9 845.1C60.4 866 75.8 888 98 888h828.8c22.2 0 37.6-22 30.1-42.9zM200.4 208h624v395h-624V208zm228.3 608l8.1-37h150.3l8.1 37H428.7zm224 0l-19.1-86.7c-.8-3.7-4.1-6.3-7.8-6.3H398.2c-3.8 0-7 2.6-7.8 6.3L371.3 816H151l42.3-149h638.2l42.3 149H652.7z`}}]},name:`laptop`,theme:`outlined`};function BW(){return BW=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,BW({},e,{ref:t,icon:zW}))),HW={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M888 792H200V168c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v688c0 4.4 3.6 8 8 8h752c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM305.8 637.7c3.1 3.1 8.1 3.1 11.3 0l138.3-137.6L583 628.5c3.1 3.1 8.2 3.1 11.3 0l275.4-275.3c3.1-3.1 3.1-8.2 0-11.3l-39.6-39.6a8.03 8.03 0 00-11.3 0l-230 229.9L461.4 404a8.03 8.03 0 00-11.3 0L266.3 586.7a8.03 8.03 0 000 11.3l39.5 39.7z`}}]},name:`line-chart`,theme:`outlined`};function UW(){return UW=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,UW({},e,{ref:t,icon:HW}))),GW={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M832 464h-68V240c0-70.7-57.3-128-128-128H388c-70.7 0-128 57.3-128 128v224h-68c-17.7 0-32 14.3-32 32v384c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V496c0-17.7-14.3-32-32-32zM332 240c0-30.9 25.1-56 56-56h248c30.9 0 56 25.1 56 56v224H332V240zm460 600H232V536h560v304zM484 701v53c0 4.4 3.6 8 8 8h40c4.4 0 8-3.6 8-8v-53a48.01 48.01 0 10-56 0z`}}]},name:`lock`,theme:`outlined`};function KW(){return KW=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,KW({},e,{ref:t,icon:GW}))),JW={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M868 732h-70.3c-4.8 0-9.3 2.1-12.3 5.8-7 8.5-14.5 16.7-22.4 24.5a353.84 353.84 0 01-112.7 75.9A352.8 352.8 0 01512.4 866c-47.9 0-94.3-9.4-137.9-27.8a353.84 353.84 0 01-112.7-75.9 353.28 353.28 0 01-76-112.5C167.3 606.2 158 559.9 158 512s9.4-94.2 27.8-137.8c17.8-42.1 43.4-80 76-112.5s70.5-58.1 112.7-75.9c43.6-18.4 90-27.8 137.9-27.8 47.9 0 94.3 9.3 137.9 27.8 42.2 17.8 80.1 43.4 112.7 75.9 7.9 7.9 15.3 16.1 22.4 24.5 3 3.7 7.6 5.8 12.3 5.8H868c6.3 0 10.2-7 6.7-12.3C798 160.5 663.8 81.6 511.3 82 271.7 82.6 79.6 277.1 82 516.4 84.4 751.9 276.2 942 512.4 942c152.1 0 285.7-78.8 362.3-197.7 3.4-5.3-.4-12.3-6.7-12.3zm88.9-226.3L815 393.7c-5.3-4.2-13-.4-13 6.3v76H488c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h314v76c0 6.7 7.8 10.5 13 6.3l141.9-112a8 8 0 000-12.6z`}}]},name:`logout`,theme:`outlined`};function YW(){return YW=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,YW({},e,{ref:t,icon:JW}))),ZW={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 110.8V792H136V270.8l-27.6-21.5 39.3-50.5 42.8 33.3h643.1l42.8-33.3 39.3 50.5-27.7 21.5zM833.6 232L512 482 190.4 232l-42.8-33.3-39.3 50.5 27.6 21.5 341.6 265.6a55.99 55.99 0 0068.7 0L888 270.8l27.6-21.5-39.3-50.5-42.7 33.2z`}}]},name:`mail`,theme:`outlined`};function QW(){return QW=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,QW({},e,{ref:t,icon:ZW}))),eG={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M839.2 278.1a32 32 0 00-30.4-22.1H736V144c0-17.7-14.3-32-32-32H320c-17.7 0-32 14.3-32 32v112h-72.8a31.9 31.9 0 00-30.4 22.1L112 502v378c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V502l-72.8-223.9zM360 184h304v72H360v-72zm480 656H184V513.4L244.3 328h535.4L840 513.4V840zM652 572H544V464c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v108H372c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h108v108c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V636h108c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z`}}]},name:`medicine-box`,theme:`outlined`};function tG(){return tG=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,tG({},e,{ref:t,icon:eG}))),rG={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M904 160H120c-4.4 0-8 3.6-8 8v64c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-64c0-4.4-3.6-8-8-8zm0 624H120c-4.4 0-8 3.6-8 8v64c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-64c0-4.4-3.6-8-8-8zm0-312H120c-4.4 0-8 3.6-8 8v64c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-64c0-4.4-3.6-8-8-8z`}}]},name:`menu`,theme:`outlined`};function iG(){return iG=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,iG({},e,{ref:t,icon:rG}))),oG={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M696 480H328c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h368c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z`}},{tag:`path`,attrs:{d:`M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z`}}]},name:`minus-circle`,theme:`outlined`};function sG(){return sG=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,sG({},e,{ref:t,icon:oG}))),lG={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M744 62H280c-35.3 0-64 28.7-64 64v768c0 35.3 28.7 64 64 64h464c35.3 0 64-28.7 64-64V126c0-35.3-28.7-64-64-64zm-8 824H288V134h448v752zM472 784a40 40 0 1080 0 40 40 0 10-80 0z`}}]},name:`mobile`,theme:`outlined`};function uG(){return uG=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,uG({},e,{ref:t,icon:lG}))),fG={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M692.8 412.7l.2-.2-34.6-44.3a7.97 7.97 0 00-11.2-1.4l-50.4 39.3-70.5-90.1a7.97 7.97 0 00-11.2-1.4l-37.9 29.7a7.97 7.97 0 00-1.4 11.2l70.5 90.2-.2.1 34.6 44.3c2.7 3.5 7.7 4.1 11.2 1.4l50.4-39.3 64.1 82c2.7 3.5 7.7 4.1 11.2 1.4l37.9-29.6c3.5-2.7 4.1-7.7 1.4-11.2l-64.1-82.1zM608 112c-167.9 0-304 136.1-304 304 0 70.3 23.9 135 63.9 186.5L114.3 856.1a8.03 8.03 0 000 11.3l42.3 42.3c3.1 3.1 8.2 3.1 11.3 0l253.6-253.6C473 696.1 537.7 720 608 720c167.9 0 304-136.1 304-304S775.9 112 608 112zm161.2 465.2C726.2 620.3 668.9 644 608 644s-118.2-23.7-161.2-66.8C403.7 534.2 380 476.9 380 416s23.7-118.2 66.8-161.2c43-43.1 100.3-66.8 161.2-66.8s118.2 23.7 161.2 66.8c43.1 43 66.8 100.3 66.8 161.2s-23.7 118.2-66.8 161.2z`}}]},name:`monitor`,theme:`outlined`};function pG(){return pG=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,pG({},e,{ref:t,icon:fG}))),hG={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372zm159.6-585h-59.5c-3 0-5.8 1.7-7.1 4.4l-90.6 180H511l-90.6-180a8 8 0 00-7.1-4.4h-60.7c-1.3 0-2.6.3-3.8 1-3.9 2.1-5.3 7-3.2 10.9L457 515.7h-61.4c-4.4 0-8 3.6-8 8v29.9c0 4.4 3.6 8 8 8h81.7V603h-81.7c-4.4 0-8 3.6-8 8v29.9c0 4.4 3.6 8 8 8h81.7V717c0 4.4 3.6 8 8 8h54.3c4.4 0 8-3.6 8-8v-68.1h82c4.4 0 8-3.6 8-8V611c0-4.4-3.6-8-8-8h-82v-41.5h82c4.4 0 8-3.6 8-8v-29.9c0-4.4-3.6-8-8-8h-62l111.1-204.8c.6-1.2 1-2.5 1-3.8-.1-4.4-3.7-8-8.1-8z`}}]},name:`pay-circle`,theme:`outlined`};function gG(){return gG=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,gG({},e,{ref:t,icon:hG}))),vG={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M877.1 238.7L770.6 132.3c-13-13-30.4-20.3-48.8-20.3s-35.8 7.2-48.8 20.3L558.3 246.8c-13 13-20.3 30.5-20.3 48.9 0 18.5 7.2 35.8 20.3 48.9l89.6 89.7a405.46 405.46 0 01-86.4 127.3c-36.7 36.9-79.6 66-127.2 86.6l-89.6-89.7c-13-13-30.4-20.3-48.8-20.3a68.2 68.2 0 00-48.8 20.3L132.3 673c-13 13-20.3 30.5-20.3 48.9 0 18.5 7.2 35.8 20.3 48.9l106.4 106.4c22.2 22.2 52.8 34.9 84.2 34.9 6.5 0 12.8-.5 19.2-1.6 132.4-21.8 263.8-92.3 369.9-198.3C818 606 888.4 474.6 910.4 342.1c6.3-37.6-6.3-76.3-33.3-103.4zm-37.6 91.5c-19.5 117.9-82.9 235.5-178.4 331s-213 158.9-330.9 178.4c-14.8 2.5-30-2.5-40.8-13.2L184.9 721.9 295.7 611l119.8 120 .9.9 21.6-8a481.29 481.29 0 00285.7-285.8l8-21.6-120.8-120.7 110.8-110.9 104.5 104.5c10.8 10.8 15.8 26 13.3 40.8z`}}]},name:`phone`,theme:`outlined`};function yG(){return yG=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,yG({},e,{ref:t,icon:vG}))),xG={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 632H136v-39.9l138.5-164.3 150.1 178L658.1 489 888 761.6V792zm0-129.8L664.2 396.8c-3.2-3.8-9-3.8-12.2 0L424.6 666.4l-144-170.7c-3.2-3.8-9-3.8-12.2 0L136 652.7V232h752v430.2zM304 456a88 88 0 100-176 88 88 0 000 176zm0-116c15.5 0 28 12.5 28 28s-12.5 28-28 28-28-12.5-28-28 12.5-28 28-28z`}}]},name:`picture`,theme:`outlined`};function SG(){return SG=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,SG({},e,{ref:t,icon:xG}))),wG={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M864 518H506V160c0-4.4-3.6-8-8-8h-26a398.46 398.46 0 00-282.8 117.1 398.19 398.19 0 00-85.7 127.1A397.61 397.61 0 0072 552a398.46 398.46 0 00117.1 282.8c36.7 36.7 79.5 65.6 127.1 85.7A397.61 397.61 0 00472 952a398.46 398.46 0 00282.8-117.1c36.7-36.7 65.6-79.5 85.7-127.1A397.61 397.61 0 00872 552v-26c0-4.4-3.6-8-8-8zM705.7 787.8A331.59 331.59 0 01470.4 884c-88.1-.4-170.9-34.9-233.2-97.2C174.5 724.1 140 640.7 140 552c0-88.7 34.5-172.1 97.2-234.8 54.6-54.6 124.9-87.9 200.8-95.5V586h364.3c-7.7 76.3-41.3 147-96.6 201.8zM952 462.4l-2.6-28.2c-8.5-92.1-49.4-179-115.2-244.6A399.4 399.4 0 00589 74.6L560.7 72c-4.7-.4-8.7 3.2-8.7 7.9V464c0 4.4 3.6 8 8 8l384-1c4.7 0 8.4-4 8-8.6zm-332.2-58.2V147.6a332.24 332.24 0 01166.4 89.8c45.7 45.6 77 103.6 90 166.1l-256.4.7z`}}]},name:`pie-chart`,theme:`outlined`};function TG(){return TG=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,TG({},e,{ref:t,icon:wG}))),DG={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z`}},{tag:`path`,attrs:{d:`M719.4 499.1l-296.1-215A15.9 15.9 0 00398 297v430c0 13.1 14.8 20.5 25.3 12.9l296.1-215a15.9 15.9 0 000-25.8zm-257.6 134V390.9L628.5 512 461.8 633.1z`}}]},name:`play-circle`,theme:`outlined`};function OG(){return OG=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,OG({},e,{ref:t,icon:DG}))),AG={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M280 752h80c4.4 0 8-3.6 8-8V280c0-4.4-3.6-8-8-8h-80c-4.4 0-8 3.6-8 8v464c0 4.4 3.6 8 8 8zm192-280h80c4.4 0 8-3.6 8-8V280c0-4.4-3.6-8-8-8h-80c-4.4 0-8 3.6-8 8v184c0 4.4 3.6 8 8 8zm192 72h80c4.4 0 8-3.6 8-8V280c0-4.4-3.6-8-8-8h-80c-4.4 0-8 3.6-8 8v256c0 4.4 3.6 8 8 8zm216-432H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zm-40 728H184V184h656v656z`}}]},name:`project`,theme:`outlined`};function jG(){return jG=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,jG({},e,{ref:t,icon:AG}))),NG={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M928 161H699.2c-49.1 0-97.1 14.1-138.4 40.7L512 233l-48.8-31.3A255.2 255.2 0 00324.8 161H96c-17.7 0-32 14.3-32 32v568c0 17.7 14.3 32 32 32h228.8c49.1 0 97.1 14.1 138.4 40.7l44.4 28.6c1.3.8 2.8 1.3 4.3 1.3s3-.4 4.3-1.3l44.4-28.6C602 807.1 650.1 793 699.2 793H928c17.7 0 32-14.3 32-32V193c0-17.7-14.3-32-32-32zM324.8 721H136V233h188.8c35.4 0 69.8 10.1 99.5 29.2l48.8 31.3 6.9 4.5v462c-47.6-25.6-100.8-39-155.2-39zm563.2 0H699.2c-54.4 0-107.6 13.4-155.2 39V298l6.9-4.5 48.8-31.3c29.7-19.1 64.1-29.2 99.5-29.2H888v488zM396.9 361H211.1c-3.9 0-7.1 3.4-7.1 7.5v45c0 4.1 3.2 7.5 7.1 7.5h185.7c3.9 0 7.1-3.4 7.1-7.5v-45c.1-4.1-3.1-7.5-7-7.5zm223.1 7.5v45c0 4.1 3.2 7.5 7.1 7.5h185.7c3.9 0 7.1-3.4 7.1-7.5v-45c0-4.1-3.2-7.5-7.1-7.5H627.1c-3.9 0-7.1 3.4-7.1 7.5zM396.9 501H211.1c-3.9 0-7.1 3.4-7.1 7.5v45c0 4.1 3.2 7.5 7.1 7.5h185.7c3.9 0 7.1-3.4 7.1-7.5v-45c.1-4.1-3.1-7.5-7-7.5zm416 0H627.1c-3.9 0-7.1 3.4-7.1 7.5v45c0 4.1 3.2 7.5 7.1 7.5h185.7c3.9 0 7.1-3.4 7.1-7.5v-45c.1-4.1-3.1-7.5-7-7.5z`}}]},name:`read`,theme:`outlined`};function PG(){return PG=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,PG({},e,{ref:t,icon:NG}))),IG={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`defs`,attrs:{},children:[{tag:`style`,attrs:{}}]},{tag:`path`,attrs:{d:`M508 704c79.5 0 144-64.5 144-144s-64.5-144-144-144-144 64.5-144 144 64.5 144 144 144zm0-224c44.2 0 80 35.8 80 80s-35.8 80-80 80-80-35.8-80-80 35.8-80 80-80z`}},{tag:`path`,attrs:{d:`M832 256h-28.1l-35.7-120.9c-4-13.7-16.5-23.1-30.7-23.1h-451c-14.3 0-26.8 9.4-30.7 23.1L220.1 256H192c-17.7 0-32 14.3-32 32v28c0 4.4 3.6 8 8 8h45.8l47.7 558.7a32 32 0 0031.9 29.3h429.2a32 32 0 0031.9-29.3L802.2 324H856c4.4 0 8-3.6 8-8v-28c0-17.7-14.3-32-32-32zm-518.6-76h397.2l22.4 76H291l22.4-76zm376.2 664H326.4L282 324h451.9l-44.3 520z`}}]},name:`rest`,theme:`outlined`};function LG(){return LG=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,LG({},e,{ref:t,icon:IG}))),zG={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M300 328a60 60 0 10120 0 60 60 0 10-120 0zM852 64H172c-17.7 0-32 14.3-32 32v660c0 17.7 14.3 32 32 32h680c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32zm-32 660H204V128h616v596zM604 328a60 60 0 10120 0 60 60 0 10-120 0zm250.2 556H169.8c-16.5 0-29.8 14.3-29.8 32v36c0 4.4 3.3 8 7.4 8h729.1c4.1 0 7.4-3.6 7.4-8v-36c.1-17.7-13.2-32-29.7-32zM664 508H360c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h304c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8z`}}]},name:`robot`,theme:`outlined`};function BG(){return BG=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,BG({},e,{ref:t,icon:zG}))),HG={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M864 736c0-111.6-65.4-208-160-252.9V317.3c0-15.1-5.3-29.7-15.1-41.2L536.5 95.4C530.1 87.8 521 84 512 84s-18.1 3.8-24.5 11.4L335.1 276.1a63.97 63.97 0 00-15.1 41.2v165.8C225.4 528 160 624.4 160 736h156.5c-2.3 7.2-3.5 15-3.5 23.8 0 22.1 7.6 43.7 21.4 60.8a97.2 97.2 0 0043.1 30.6c23.1 54 75.6 88.8 134.5 88.8 29.1 0 57.3-8.6 81.4-24.8 23.6-15.8 41.9-37.9 53-64a97 97 0 0043.1-30.5 97.52 97.52 0 0021.4-60.8c0-8.4-1.1-16.4-3.1-23.8H864zM762.3 621.4c9.4 14.6 17 30.3 22.5 46.6H700V558.7a211.6 211.6 0 0162.3 62.7zM388 483.1V318.8l124-147 124 147V668H388V483.1zM239.2 668c5.5-16.3 13.1-32 22.5-46.6 16.3-25.2 37.5-46.5 62.3-62.7V668h-84.8zm388.9 116.2c-5.2 3-11.2 4.2-17.1 3.4l-19.5-2.4-2.8 19.4c-5.4 37.9-38.4 66.5-76.7 66.5-38.3 0-71.3-28.6-76.7-66.5l-2.8-19.5-19.5 2.5a27.7 27.7 0 01-17.1-3.5c-8.7-5-14.1-14.3-14.1-24.4 0-10.6 5.9-19.4 14.6-23.8h231.3c8.8 4.5 14.6 13.3 14.6 23.8-.1 10.2-5.5 19.6-14.2 24.5zM464 400a48 48 0 1096 0 48 48 0 10-96 0z`}}]},name:`rocket`,theme:`outlined`};function UG(){return UG=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,UG({},e,{ref:t,icon:HG}))),GG={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M793 242H366v-74c0-6.7-7.7-10.4-12.9-6.3l-142 112a8 8 0 000 12.6l142 112c5.2 4.1 12.9.4 12.9-6.3v-74h415v470H175c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h618c35.3 0 64-28.7 64-64V306c0-35.3-28.7-64-64-64z`}}]},name:`rollback`,theme:`outlined`};function KG(){return KG=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,KG({},e,{ref:t,icon:GG}))),JG={icon:{tag:`svg`,attrs:{viewBox:`0 0 1024 1024`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M512 64L128 192v384c0 212.1 171.9 384 384 384s384-171.9 384-384V192L512 64zm312 512c0 172.3-139.7 312-312 312S200 748.3 200 576V246l312-110 312 110v330z`}},{tag:`path`,attrs:{d:`M378.4 475.1a35.91 35.91 0 00-50.9 0 35.91 35.91 0 000 50.9l129.4 129.4 2.1 2.1a33.98 33.98 0 0048.1 0L730.6 434a33.98 33.98 0 000-48.1l-2.8-2.8a33.98 33.98 0 00-48.1 0L483 579.7 378.4 475.1z`}}]},name:`safety`,theme:`outlined`};function YG(){return YG=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,YG({},e,{ref:t,icon:JG}))),ZG={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M893.3 293.3L730.7 130.7c-7.5-7.5-16.7-13-26.7-16V112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V338.5c0-17-6.7-33.2-18.7-45.2zM384 184h256v104H384V184zm456 656H184V184h136v136c0 17.7 14.3 32 32 32h320c17.7 0 32-14.3 32-32V205.8l136 136V840zM512 442c-79.5 0-144 64.5-144 144s64.5 144 144 144 144-64.5 144-144-64.5-144-144-144zm0 224c-44.2 0-80-35.8-80-80s35.8-80 80-80 80 35.8 80 80-35.8 80-80 80z`}}]},name:`save`,theme:`outlined`};function QG(){return QG=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,QG({},e,{ref:t,icon:ZG}))),eK={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M866.9 169.9L527.1 54.1C523 52.7 517.5 52 512 52s-11 .7-15.1 2.1L157.1 169.9c-8.3 2.8-15.1 12.4-15.1 21.2v482.4c0 8.8 5.7 20.4 12.6 25.9L499.3 968c3.5 2.7 8 4.1 12.6 4.1s9.2-1.4 12.6-4.1l344.7-268.6c6.9-5.4 12.6-17 12.6-25.9V191.1c.2-8.8-6.6-18.3-14.9-21.2zM810 654.3L512 886.5 214 654.3V226.7l298-101.6 298 101.6v427.6zM402.9 528.8l-77.5 77.5a8.03 8.03 0 000 11.3l34 34c3.1 3.1 8.2 3.1 11.3 0l77.5-77.5c55.7 35.1 130.1 28.4 178.6-20.1 56.3-56.3 56.3-147.5 0-203.8-56.3-56.3-147.5-56.3-203.8 0-48.5 48.5-55.2 123-20.1 178.6zm65.4-133.3c31.3-31.3 82-31.3 113.2 0 31.3 31.3 31.3 82 0 113.2-31.3 31.3-82 31.3-113.2 0s-31.3-81.9 0-113.2z`}}]},name:`security-scan`,theme:`outlined`};function tK(){return tK=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,tK({},e,{ref:t,icon:eK}))),rK={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`defs`,attrs:{},children:[{tag:`style`,attrs:{}}]},{tag:`path`,attrs:{d:`M931.4 498.9L94.9 79.5c-3.4-1.7-7.3-2.1-11-1.2a15.99 15.99 0 00-11.7 19.3l86.2 352.2c1.3 5.3 5.2 9.6 10.4 11.3l147.7 50.7-147.6 50.7c-5.2 1.8-9.1 6-10.3 11.3L72.2 926.5c-.9 3.7-.5 7.6 1.2 10.9 3.9 7.9 13.5 11.1 21.5 7.2l836.5-417c3.1-1.5 5.6-4.1 7.2-7.1 3.9-8 .7-17.6-7.2-21.6zM170.8 826.3l50.3-205.6 295.2-101.3c2.3-.8 4.2-2.6 5-5 1.4-4.2-.8-8.7-5-10.2L221.1 403 171 198.2l628 314.9-628.2 313.2z`}}]},name:`send`,theme:`outlined`};function iK(){return iK=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,iK({},e,{ref:t,icon:rK}))),oK={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 009.3-35.2l-.9-2.6a443.74 443.74 0 00-79.7-137.9l-1.8-2.1a32.12 32.12 0 00-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 00-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 00-25.8 25.7l-15.8 85.4a351.86 351.86 0 00-99 57.4l-81.9-29.1a32 32 0 00-35.1 9.5l-1.8 2.1a446.02 446.02 0 00-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 00-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0035.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0025.8 25.7l2.7.5a449.4 449.4 0 00159 0l2.7-.5a32.05 32.05 0 0025.8-25.7l15.7-85a350 350 0 0099.7-57.6l81.3 28.9a32 32 0 0035.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 01-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 01-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 01624 502c0 29.9-11.7 58-32.8 79.2z`}}]},name:`setting`,theme:`outlined`};function sK(){return sK=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,sK({},e,{ref:t,icon:oK}))),lK={icon:{tag:`svg`,attrs:{viewBox:`0 0 1024 1024`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M922.9 701.9H327.4l29.9-60.9 496.8-.9c16.8 0 31.2-12 34.2-28.6l68.8-385.1c1.8-10.1-.9-20.5-7.5-28.4a34.99 34.99 0 00-26.6-12.5l-632-2.1-5.4-25.4c-3.4-16.2-18-28-34.6-28H96.5a35.3 35.3 0 100 70.6h125.9L246 312.8l58.1 281.3-74.8 122.1a34.96 34.96 0 00-3 36.8c6 11.9 18.1 19.4 31.5 19.4h62.8a102.43 102.43 0 00-20.6 61.7c0 56.6 46 102.6 102.6 102.6s102.6-46 102.6-102.6c0-22.3-7.4-44-20.6-61.7h161.1a102.43 102.43 0 00-20.6 61.7c0 56.6 46 102.6 102.6 102.6s102.6-46 102.6-102.6c0-22.3-7.4-44-20.6-61.7H923c19.4 0 35.3-15.8 35.3-35.3a35.42 35.42 0 00-35.4-35.2zM305.7 253l575.8 1.9-56.4 315.8-452.3.8L305.7 253zm96.9 612.7c-17.4 0-31.6-14.2-31.6-31.6 0-17.4 14.2-31.6 31.6-31.6s31.6 14.2 31.6 31.6a31.6 31.6 0 01-31.6 31.6zm325.1 0c-17.4 0-31.6-14.2-31.6-31.6 0-17.4 14.2-31.6 31.6-31.6s31.6 14.2 31.6 31.6a31.6 31.6 0 01-31.6 31.6z`}}]},name:`shopping-cart`,theme:`outlined`};function uK(){return uK=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,uK({},e,{ref:t,icon:lK}))),fK={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M832 312H696v-16c0-101.6-82.4-184-184-184s-184 82.4-184 184v16H192c-17.7 0-32 14.3-32 32v536c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V344c0-17.7-14.3-32-32-32zm-432-16c0-61.9 50.1-112 112-112s112 50.1 112 112v16H400v-16zm392 544H232V384h96v88c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-88h224v88c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-88h96v456z`}}]},name:`shopping`,theme:`outlined`};function pK(){return pK=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,pK({},e,{ref:t,icon:fK}))),hK={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M925.6 405.1l-203-253.7a6.5 6.5 0 00-5-2.4H306.4c-1.9 0-3.8.9-5 2.4l-203 253.7a6.5 6.5 0 00.2 8.3l408.6 459.5c1.2 1.4 3 2.1 4.8 2.1 1.8 0 3.5-.8 4.8-2.1l408.6-459.5a6.5 6.5 0 00.2-8.3zM645.2 206.4l34.4 133.9-132.5-133.9h98.1zm8.2 178.5H370.6L512 242l141.4 142.9zM378.8 206.4h98.1L344.3 340.3l34.5-133.9zm-53.4 7l-44.1 171.5h-93.1l137.2-171.5zM194.6 434.9H289l125.8 247.7-220.2-247.7zM512 763.4L345.1 434.9h333.7L512 763.4zm97.1-80.8L735 434.9h94.4L609.1 682.6zm133.6-297.7l-44.1-171.5 137.2 171.5h-93.1z`}}]},name:`sketch`,theme:`outlined`};function gK(){return gK=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,gK({},e,{ref:t,icon:hK}))),vK={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M870 126H663.8c-17.4 0-32.9 11.9-37 29.3C614.3 208.1 567 246 512 246s-102.3-37.9-114.8-90.7a37.93 37.93 0 00-37-29.3H154a44 44 0 00-44 44v252a44 44 0 0044 44h75v388a44 44 0 0044 44h478a44 44 0 0044-44V466h75a44 44 0 0044-44V170a44 44 0 00-44-44zm-28 268H723v432H301V394H182V198h153.3c28.2 71.2 97.5 120 176.7 120s148.5-48.8 176.7-120H842v196z`}}]},name:`skin`,theme:`outlined`};function yK(){return yK=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,yK({},e,{ref:t,icon:vK}))),xK={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M288 421a48 48 0 1096 0 48 48 0 10-96 0zm352 0a48 48 0 1096 0 48 48 0 10-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 01248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 01249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 01775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 01775 775zM664 533h-48.1c-4.2 0-7.8 3.2-8.1 7.4C604 589.9 562.5 629 512 629s-92.1-39.1-95.8-88.6c-.3-4.2-3.9-7.4-8.1-7.4H360a8 8 0 00-8 8.4c4.4 84.3 74.5 151.6 160 151.6s155.6-67.3 160-151.6a8 8 0 00-8-8.4z`}}]},name:`smile`,theme:`outlined`};function SK(){return SK=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,SK({},e,{ref:t,icon:xK}))),wK={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M688 264c0-4.4-3.6-8-8-8H296c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h384c4.4 0 8-3.6 8-8v-48zm-8 136H296c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h384c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zM480 544H296c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zm-48 308H208V148h560v344c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V108c0-17.7-14.3-32-32-32H168c-17.7 0-32 14.3-32 32v784c0 17.7 14.3 32 32 32h264c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm356.8-74.4c29-26.3 47.2-64.3 47.2-106.6 0-79.5-64.5-144-144-144s-144 64.5-144 144c0 42.3 18.2 80.3 47.2 106.6-57 32.5-96.2 92.7-99.2 162.1-.2 4.5 3.5 8.3 8 8.3h48.1c4.2 0 7.7-3.3 8-7.6C564 871.2 621.7 816 692 816s128 55.2 131.9 124.4c.2 4.2 3.7 7.6 8 7.6H880c4.6 0 8.2-3.8 8-8.3-2.9-69.5-42.2-129.6-99.2-162.1zM692 591c44.2 0 80 35.8 80 80s-35.8 80-80 80-80-35.8-80-80 35.8-80 80-80z`}}]},name:`solution`,theme:`outlined`};function TK(){return TK=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,TK({},e,{ref:t,icon:wK}))),DK={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M625.9 115c-5.9 0-11.9 1.6-17.4 5.3L254 352H90c-8.8 0-16 7.2-16 16v288c0 8.8 7.2 16 16 16h164l354.5 231.7c5.5 3.6 11.6 5.3 17.4 5.3 16.7 0 32.1-13.3 32.1-32.1V147.1c0-18.8-15.4-32.1-32.1-32.1zM586 803L293.4 611.7l-18-11.7H146V424h129.4l17.9-11.7L586 221v582zm348-327H806c-8.8 0-16 7.2-16 16v40c0 8.8 7.2 16 16 16h128c8.8 0 16-7.2 16-16v-40c0-8.8-7.2-16-16-16zm-41.9 261.8l-110.3-63.7a15.9 15.9 0 00-21.7 5.9l-19.9 34.5c-4.4 7.6-1.8 17.4 5.8 21.8L856.3 800a15.9 15.9 0 0021.7-5.9l19.9-34.5c4.4-7.6 1.7-17.4-5.8-21.8zM760 344a15.9 15.9 0 0021.7 5.9L892 286.2c7.6-4.4 10.2-14.2 5.8-21.8L878 230a15.9 15.9 0 00-21.7-5.9L746 287.8a15.99 15.99 0 00-5.8 21.8L760 344z`}}]},name:`sound`,theme:`outlined`};function OK(){return OK=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,OK({},e,{ref:t,icon:DK}))),AK={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M908.1 353.1l-253.9-36.9L540.7 86.1c-3.1-6.3-8.2-11.4-14.5-14.5-15.8-7.8-35-1.3-42.9 14.5L369.8 316.2l-253.9 36.9c-7 1-13.4 4.3-18.3 9.3a32.05 32.05 0 00.6 45.3l183.7 179.1-43.4 252.9a31.95 31.95 0 0046.4 33.7L512 754l227.1 119.4c6.2 3.3 13.4 4.4 20.3 3.2 17.4-3 29.1-19.5 26.1-36.9l-43.4-252.9 183.7-179.1c5-4.9 8.3-11.3 9.3-18.3 2.7-17.5-9.5-33.7-27-36.3zM664.8 561.6l36.1 210.3L512 672.7 323.1 772l36.1-210.3-152.8-149L417.6 382 512 190.7 606.4 382l211.2 30.7-152.8 148.9z`}}]},name:`star`,theme:`outlined`};function jK(){return jK=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,jK({},e,{ref:t,icon:AK}))),NK={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372 0-89 31.3-170.8 83.5-234.8l523.3 523.3C682.8 852.7 601 884 512 884zm288.5-137.2L277.2 223.5C341.2 171.3 423 140 512 140c205.4 0 372 166.6 372 372 0 89-31.3 170.8-83.5 234.8z`}}]},name:`stop`,theme:`outlined`};function PK(){return PK=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,PK({},e,{ref:t,icon:NK}))),IK={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M938 458.8l-29.6-312.6c-1.5-16.2-14.4-29-30.6-30.6L565.2 86h-.4c-3.2 0-5.7 1-7.6 2.9L88.9 557.2a9.96 9.96 0 000 14.1l363.8 363.8c1.9 1.9 4.4 2.9 7.1 2.9s5.2-1 7.1-2.9l468.3-468.3c2-2.1 3-5 2.8-8zM459.7 834.7L189.3 564.3 589 164.6 836 188l23.4 247-399.7 399.7zM680 256c-48.5 0-88 39.5-88 88s39.5 88 88 88 88-39.5 88-88-39.5-88-88-88zm0 120c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z`}}]},name:`tag`,theme:`outlined`};function LK(){return LK=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,LK({},e,{ref:t,icon:IK}))),zK={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M824.2 699.9a301.55 301.55 0 00-86.4-60.4C783.1 602.8 812 546.8 812 484c0-110.8-92.4-201.7-203.2-200-109.1 1.7-197 90.6-197 200 0 62.8 29 118.8 74.2 155.5a300.95 300.95 0 00-86.4 60.4C345 754.6 314 826.8 312 903.8a8 8 0 008 8.2h56c4.3 0 7.9-3.4 8-7.7 1.9-58 25.4-112.3 66.7-153.5A226.62 226.62 0 01612 684c60.9 0 118.2 23.7 161.3 66.8C814.5 792 838 846.3 840 904.3c.1 4.3 3.7 7.7 8 7.7h56a8 8 0 008-8.2c-2-77-33-149.2-87.8-203.9zM612 612c-34.2 0-66.4-13.3-90.5-37.5a126.86 126.86 0 01-37.5-91.8c.3-32.8 13.4-64.5 36.3-88 24-24.6 56.1-38.3 90.4-38.7 33.9-.3 66.8 12.9 91 36.6 24.8 24.3 38.4 56.8 38.4 91.4 0 34.2-13.3 66.3-37.5 90.5A127.3 127.3 0 01612 612zM361.5 510.4c-.9-8.7-1.4-17.5-1.4-26.4 0-15.9 1.5-31.4 4.3-46.5.7-3.6-1.2-7.3-4.5-8.8-13.6-6.1-26.1-14.5-36.9-25.1a127.54 127.54 0 01-38.7-95.4c.9-32.1 13.8-62.6 36.3-85.6 24.7-25.3 57.9-39.1 93.2-38.7 31.9.3 62.7 12.6 86 34.4 7.9 7.4 14.7 15.6 20.4 24.4 2 3.1 5.9 4.4 9.3 3.2 17.6-6.1 36.2-10.4 55.3-12.4 5.6-.6 8.8-6.6 6.3-11.6-32.5-64.3-98.9-108.7-175.7-109.9-110.9-1.7-203.3 89.2-203.3 199.9 0 62.8 28.9 118.8 74.2 155.5-31.8 14.7-61.1 35-86.5 60.4-54.8 54.7-85.8 126.9-87.8 204a8 8 0 008 8.2h56.1c4.3 0 7.9-3.4 8-7.7 1.9-58 25.4-112.3 66.7-153.5 29.4-29.4 65.4-49.8 104.7-59.7 3.9-1 6.5-4.7 6-8.7z`}}]},name:`team`,theme:`outlined`};function BK(){return BK=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,BK({},e,{ref:t,icon:zK}))),HK={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M848 359.3H627.7L825.8 109c4.1-5.3.4-13-6.3-13H436c-2.8 0-5.5 1.5-6.9 4L170 547.5c-3.1 5.3.7 12 6.9 12h174.4l-89.4 357.6c-1.9 7.8 7.5 13.3 13.3 7.7L853.5 373c5.2-4.9 1.7-13.7-5.5-13.7zM378.2 732.5l60.3-241H281.1l189.6-327.4h224.6L487 427.4h211L378.2 732.5z`}}]},name:`thunderbolt`,theme:`outlined`};function UK(){return UK=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,UK({},e,{ref:t,icon:HK}))),GK={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M876.6 239.5c-.5-.9-1.2-1.8-2-2.5-5-5-13.1-5-18.1 0L684.2 409.3l-67.9-67.9L788.7 169c.8-.8 1.4-1.6 2-2.5 3.6-6.1 1.6-13.9-4.5-17.5-98.2-58-226.8-44.7-311.3 39.7-67 67-89.2 162-66.5 247.4l-293 293c-3 3-2.8 7.9.3 11l169.7 169.7c3.1 3.1 8.1 3.3 11 .3l292.9-292.9c85.5 22.8 180.5.7 247.6-66.4 84.4-84.5 97.7-213.1 39.7-311.3zM786 499.8c-58.1 58.1-145.3 69.3-214.6 33.6l-8.8 8.8-.1-.1-274 274.1-79.2-79.2 230.1-230.1s0 .1.1.1l52.8-52.8c-35.7-69.3-24.5-156.5 33.6-214.6a184.2 184.2 0 01144-53.5L537 318.9a32.05 32.05 0 000 45.3l124.5 124.5a32.05 32.05 0 0045.3 0l132.8-132.8c3.7 51.8-14.4 104.8-53.6 143.9z`}}]},name:`tool`,theme:`outlined`};function KK(){return KK=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,KK({},e,{ref:t,icon:GK}))),JK={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M868 160h-92v-40c0-4.4-3.6-8-8-8H256c-4.4 0-8 3.6-8 8v40h-92a44 44 0 00-44 44v148c0 81.7 60 149.6 138.2 162C265.7 630.2 359 721.7 476 734.5v105.2H280c-17.7 0-32 14.3-32 32V904c0 4.4 3.6 8 8 8h512c4.4 0 8-3.6 8-8v-32.3c0-17.7-14.3-32-32-32H548V734.5C665 721.7 758.3 630.2 773.8 514 852 501.6 912 433.7 912 352V204a44 44 0 00-44-44zM184 352V232h64v207.6a91.99 91.99 0 01-64-87.6zm520 128c0 49.1-19.1 95.4-53.9 130.1-34.8 34.8-81 53.9-130.1 53.9h-16c-49.1 0-95.4-19.1-130.1-53.9-34.8-34.8-53.9-81-53.9-130.1V184h384v296zm136-128c0 41-26.9 75.8-64 87.6V232h64v120z`}}]},name:`trophy`,theme:`outlined`};function YK(){return YK=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,YK({},e,{ref:t,icon:JK}))),ZK={icon:{tag:`svg`,attrs:{"fill-rule":`evenodd`,viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M608 192a32 32 0 0132 32v160h174.81a32 32 0 0126.68 14.33l113.19 170.84a32 32 0 015.32 17.68V672a32 32 0 01-32 32h-96c0 70.7-57.3 128-128 128s-128-57.3-128-128H384c0 70.7-57.3 128-128 128s-128-57.3-128-128H96a32 32 0 01-32-32V224a32 32 0 0132-32zM256 640a64 64 0 000 128h1.06A64 64 0 00256 640m448 0a64 64 0 000 128h1.06A64 64 0 00704 640M576 256H128v384h17.12c22.13-38.26 63.5-64 110.88-64 47.38 0 88.75 25.74 110.88 64H576zm221.63 192H640v145.12A127.43 127.43 0 01704 576c47.38 0 88.75 25.74 110.88 64H896v-43.52zM500 448a12 12 0 0112 12v40a12 12 0 01-12 12H332a12 12 0 01-12-12v-40a12 12 0 0112-12zM308 320a12 12 0 0112 12v40a12 12 0 01-12 12H204a12 12 0 01-12-12v-40a12 12 0 0112-12z`}}]},name:`truck`,theme:`outlined`};function QK(){return QK=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,QK({},e,{ref:t,icon:ZK}))),eq={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M400 317.7h73.9V656c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V317.7H624c6.7 0 10.4-7.7 6.3-12.9L518.3 163a8 8 0 00-12.6 0l-112 141.7c-4.1 5.3-.4 13 6.3 13zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8z`}}]},name:`upload`,theme:`outlined`};function tq(){return tq=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,tq({},e,{ref:t,icon:eq}))),rq={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M858.5 763.6a374 374 0 00-80.6-119.5 375.63 375.63 0 00-119.5-80.6c-.4-.2-.8-.3-1.2-.5C719.5 518 760 444.7 760 362c0-137-111-248-248-248S264 225 264 362c0 82.7 40.5 156 102.8 201.1-.4.2-.8.3-1.2.5-44.8 18.9-85 46-119.5 80.6a375.63 375.63 0 00-80.6 119.5A371.7 371.7 0 00136 901.8a8 8 0 008 8.2h60c4.4 0 7.9-3.5 8-7.8 2-77.2 33-149.5 87.8-204.3 56.7-56.7 132-87.9 212.2-87.9s155.5 31.2 212.2 87.9C779 752.7 810 825 812 902.2c.1 4.4 3.6 7.8 8 7.8h60a8 8 0 008-8.2c-1-47.8-10.9-94.3-29.5-138.2zM512 534c-45.9 0-89.1-17.9-121.6-50.4S340 407.9 340 362c0-45.9 17.9-89.1 50.4-121.6S466.1 190 512 190s89.1 17.9 121.6 50.4S684 316.1 684 362c0 45.9-17.9 89.1-50.4 121.6S557.9 534 512 534z`}}]},name:`user`,theme:`outlined`};function iq(){return iq=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,iq({},e,{ref:t,icon:rq}))),oq={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M912 302.3L784 376V224c0-35.3-28.7-64-64-64H128c-35.3 0-64 28.7-64 64v576c0 35.3 28.7 64 64 64h592c35.3 0 64-28.7 64-64V648l128 73.7c21.3 12.3 48-3.1 48-27.6V330c0-24.6-26.7-40-48-27.7zM712 792H136V232h576v560zm176-167l-104-59.8V458.9L888 399v226zM208 360h112c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H208c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8z`}}]},name:`video-camera`,theme:`outlined`};function sq(){return sq=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,sq({},e,{ref:t,icon:oq}))),lq={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zm-40 464H528V448h312v128zm0 264H184V184h656v200H496c-17.7 0-32 14.3-32 32v192c0 17.7 14.3 32 32 32h344v200zM580 512a40 40 0 1080 0 40 40 0 10-80 0z`}}]},name:`wallet`,theme:`outlined`};function uq(){return uq=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,uq({},e,{ref:t,icon:lq}))),fq={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M690.1 377.4c5.9 0 11.8.2 17.6.5-24.4-128.7-158.3-227.1-319.9-227.1C209 150.8 64 271.4 64 420.2c0 81.1 43.6 154.2 111.9 203.6a21.5 21.5 0 019.1 17.6c0 2.4-.5 4.6-1.1 6.9-5.5 20.3-14.2 52.8-14.6 54.3-.7 2.6-1.7 5.2-1.7 7.9 0 5.9 4.8 10.8 10.8 10.8 2.3 0 4.2-.9 6.2-2l70.9-40.9c5.3-3.1 11-5 17.2-5 3.2 0 6.4.5 9.5 1.4 33.1 9.5 68.8 14.8 105.7 14.8 6 0 11.9-.1 17.8-.4-7.1-21-10.9-43.1-10.9-66 0-135.8 132.2-245.8 295.3-245.8zm-194.3-86.5c23.8 0 43.2 19.3 43.2 43.1s-19.3 43.1-43.2 43.1c-23.8 0-43.2-19.3-43.2-43.1s19.4-43.1 43.2-43.1zm-215.9 86.2c-23.8 0-43.2-19.3-43.2-43.1s19.3-43.1 43.2-43.1 43.2 19.3 43.2 43.1-19.4 43.1-43.2 43.1zm586.8 415.6c56.9-41.2 93.2-102 93.2-169.7 0-124-120.8-224.5-269.9-224.5-149 0-269.9 100.5-269.9 224.5S540.9 847.5 690 847.5c30.8 0 60.6-4.4 88.1-12.3 2.6-.8 5.2-1.2 7.9-1.2 5.2 0 9.9 1.6 14.3 4.1l59.1 34c1.7 1 3.3 1.7 5.2 1.7a9 9 0 006.4-2.6 9 9 0 002.6-6.4c0-2.2-.9-4.4-1.4-6.6-.3-1.2-7.6-28.3-12.2-45.3-.5-1.9-.9-3.8-.9-5.7.1-5.9 3.1-11.2 7.6-14.5zM600.2 587.2c-19.9 0-36-16.1-36-35.9 0-19.8 16.1-35.9 36-35.9s36 16.1 36 35.9c0 19.8-16.2 35.9-36 35.9zm179.9 0c-19.9 0-36-16.1-36-35.9 0-19.8 16.1-35.9 36-35.9s36 16.1 36 35.9a36.08 36.08 0 01-36 35.9z`}}]},name:`wechat`,theme:`outlined`};function pq(){return pq=Object.assign?Object.assign.bind():function(e){for(var t=1;tx.createElement(Y,pq({},e,{ref:t,icon:fq}))),hq=e=>{let t,n=new Set,r=(e,r)=>{let i=typeof e==`function`?e(t):e;if(!Object.is(i,t)){let e=t;t=r??(typeof i!=`object`||!i)?i:Object.assign({},t,i),n.forEach(n=>n(t,e))}},i=()=>t,a={setState:r,getState:i,getInitialState:()=>o,subscribe:e=>(n.add(e),()=>n.delete(e))},o=t=e(r,i,a);return a},gq=(e=>e?hq(e):hq),_q=e=>e;function vq(e,t=_q){let n=x.useSyncExternalStore(e.subscribe,x.useCallback(()=>t(e.getState()),[e,t]),x.useCallback(()=>t(e.getInitialState()),[e,t]));return x.useDebugValue(n),n}var yq=e=>{let t=gq(e),n=e=>vq(t,e);return Object.assign(n,t),n},bq=(e=>e?yq(e):yq),xq=c(EH(),1),Sq=`AES-GCM`,Cq=12,wq=128;async function Tq(){throw Error(`VITE_ENCRYPTION_KEY not configured`)}async function Eq(e){let t=await Tq(),n=crypto.getRandomValues(new Uint8Array(Cq)),r=new TextEncoder().encode(e),i=await crypto.subtle.encrypt({name:Sq,iv:n,tagLength:wq},t,r),a=new Uint8Array(i),o=new Uint8Array(n.length+a.length);return o.set(n),o.set(a,n.length),btoa(String.fromCharCode(...o))}async function Dq(e){let t=await Tq(),n=Uint8Array.from(atob(e),e=>e.charCodeAt(0)),r=n.slice(0,Cq),i=n.slice(Cq),a=await crypto.subtle.decrypt({name:Sq,iv:r,tagLength:wq},t,i);return new TextDecoder().decode(a)}var Oq=`http://ceshi.apiforeign.minzhong.cn`,kq=!1;function Aq(e){return e.replace(/_([a-z])/g,(e,t)=>t.toUpperCase())}function jq(e){return Array.isArray(e)?e.map(jq):typeof e==`object`&&e?Object.fromEntries(Object.entries(e).map(([e,t])=>[Aq(e),jq(t)])):e}function Mq(){return localStorage.getItem(`auth_token`)}function Nq(e){localStorage.setItem(`auth_token`,e)}function Pq(){localStorage.removeItem(`auth_token`)}async function Fq(e){try{return await Dq(e)}catch{return null}}async function Iq(e,t={}){let{method:n=`GET`,body:r,auth:i=!0,encryptBody:a=kq}=t,o={"Content-Type":`application/json`};if(i){let e=Mq();e&&(o.Authorization=`Bearer ${e}`)}let s;if(r!==void 0){let e=JSON.stringify(r);a?(o[`X-Encrypted`]=`true`,s=JSON.stringify({data:await Eq(e)})):s=e}let c=await fetch(`${Oq}/api${e}`,{method:n,headers:o,body:s});if(c.status===204)return;let l=await c.text();if(!l)return;let u;try{u=JSON.parse(l)}catch{throw Error(`响应解析失败 (${c.status})`)}if(u&&typeof u.data==`string`&&a){let e=await Fq(u.data);e!==null&&(u=JSON.parse(e))}if(!c.ok){let e=u?.detail||`请求失败 (${c.status})`;throw c.status===401&&(Pq(),window.location.href=`/login`),Error(e)}return jq(u)}var Lq={get:(e,t=!0)=>Iq(e,{auth:t}),post:(e,t,n=!0)=>Iq(e,{method:`POST`,body:t,auth:n}),put:(e,t,n=!0)=>Iq(e,{method:`PUT`,body:t,auth:n}),delete:(e,t=!0)=>Iq(e,{method:`DELETE`,auth:t})};async function Rq(e,t,n,r){let i=await Lq.post(`/auth/admin-login`,{username:e,password:t,captcha_token:n,remember_me:r||!1},!1);return Nq(i.accessToken),i.user}async function zq(){await Lq.post(`/auth/logout`),Pq()}async function Bq(){try{return await Lq.get(`/auth/me`)}catch{return null}}async function Vq(){return Lq.get(`/admin/stats`)}async function Hq(e){let t=e?`?search=${encodeURIComponent(e)}`:``;return Lq.get(`/admin/users${t}`)}async function Uq(e,t,n){await Lq.post(`/admin/users/${e}/credits`,{amount:t,description:n})}async function Wq(e,t){await Lq.put(`/admin/users/${e}/status`,{is_active:t})}async function Gq(){return Lq.get(`/admin/model-configs`)}async function Kq(e){return e.id?Lq.put(`/admin/model-configs/${e.id}`,e):Lq.post(`/admin/model-configs`,e)}async function qq(e){await Lq.delete(`/admin/model-configs/${e}`)}async function Jq(){return Lq.get(`/admin/system-configs`)}async function Yq(e,t){await Lq.put(`/admin/system-configs/${e}`,{value:t})}async function Xq(e,t){let n=new FormData;n.append(`file`,e),n.append(`config_key`,t);let r=localStorage.getItem(`auth_token`),i=await fetch(`http://localhost:8000/api/admin/upload-pdf`,{method:`POST`,headers:r?{Authorization:`Bearer ${r}`}:{},body:n});if(!i.ok)throw Error(`上传失败`);return i.json()}async function Zq(e){let t=new URLSearchParams;e?.user_id&&t.set(`user_id`,e.user_id),e?.type&&t.set(`type`,e.type);let n=t.toString()?`?${t}`:``;return Lq.get(`/admin/credit-records${n}`)}async function Qq(){return Lq.get(`/admin/industry-configs`)}async function $q(e){return e.id?Lq.put(`/admin/industry-configs/${e.id}`,e):Lq.post(`/admin/industry-configs`,e)}async function eJ(e){await Lq.delete(`/admin/industry-configs/${e}`)}async function tJ(){return Lq.get(`/admin/video-engines`)}async function nJ(e){return e.id?Lq.put(`/admin/video-engines/${e.id}`,e):Lq.post(`/admin/video-engines`,e)}async function rJ(e){await Lq.delete(`/admin/video-engines/${e}`)}async function iJ(){return Lq.get(`/admin/image-engines`)}async function aJ(e){return e.id?Lq.put(`/admin/image-engines/${e.id}`,e):Lq.post(`/admin/image-engines`,e)}async function oJ(e){await Lq.delete(`/admin/image-engines/${e}`)}async function sJ(){return Lq.get(`/admin/credit-ratios`)}async function cJ(e){return e.id?Lq.put(`/admin/credit-ratios/${e.id}`,e):Lq.post(`/admin/credit-ratios`,e)}async function lJ(e){await Lq.delete(`/admin/credit-ratios/${e}`)}async function uJ(){return Lq.get(`/admin/payment-configs`)}async function dJ(e,t){await Lq.put(`/admin/payment-configs/${e}`,{value:t})}async function fJ(){return Lq.get(`/admin/notifications`)}async function pJ(e){await Lq.post(`/admin/notifications`,e)}async function mJ(e){await Lq.delete(`/admin/notifications/${e}`)}async function hJ(e){return Lq.get(`/admin/notifications/${e}/read-users`)}async function gJ(){return Lq.get(`/admin/menu-configs`)}async function _J(e){return e.id?Lq.put(`/admin/menu-configs/${e.id}`,e):Lq.post(`/admin/menu-configs`,e)}async function vJ(e){await Lq.delete(`/admin/menu-configs/${e}`)}async function yJ(e){return Lq.post(`/admin/users`,e)}async function bJ(e,t){await Lq.put(`/admin/users/${e}/menus`,{allowed_menus:t})}async function xJ(e,t){await Lq.put(`/admin/users/${e}/reset-password`,{new_password:t})}async function SJ(e,t){await Lq.post(`/admin/change-password`,{old_password:e,new_password:t})}async function CJ(){return Lq.get(`/admin/recharge-packages`)}async function wJ(e){return e.id?Lq.put(`/admin/recharge-packages/${e.id}`,e):Lq.post(`/admin/recharge-packages`,e)}async function TJ(e){await Lq.delete(`/admin/recharge-packages/${e}`)}async function EJ(e){let t=e?`?page=${e}`:``;return Lq.get(`/admin/operation-logs${t}`)}async function DJ(e){let t=new URLSearchParams;e?.userId&&t.set(`user_id`,e.userId),e?.status&&t.set(`status`,e.status),e?.page&&t.set(`page`,String(e.page)),e?.pageSize&&t.set(`page_size`,String(e.pageSize));let n=t.toString();return Lq.get(`/admin/generation-records${n?`?${n}`:``}`)}async function OJ(e,t,n){await Lq.put(`/admin/generation-records/${e}/status`,{status:t,video_url:n})}async function kJ(e,t,n,r){await Lq.post(`/admin/generation-records/${e}/generate`,{aspect_ratio:t,resolution:n,image_size:r})}async function AJ(){return Lq.get(`/generation-ai/engines`)}async function jJ(e){let t=new URLSearchParams;e?.genType&&t.set(`gen_type`,e.genType),e?.status&&t.set(`status`,e.status),e?.page&&t.set(`page`,String(e.page)),e?.pageSize&&t.set(`page_size`,String(e.pageSize)),e?.userId&&t.set(`user_id`,e.userId),e?.userName&&t.set(`user_name`,e.userName);let n=t.toString();return Lq.get(`/generation-ai/tasks${n?`?${n}`:``}`)}var MJ=bq(e=>({user:null,loading:!0,login:async(t,n,r)=>{e({user:await Rq(t,n,void 0,r)})},logout:async()=>{await zq(),e({user:null})},checkAuth:async()=>{try{if(!localStorage.getItem(`auth_token`)){e({user:null,loading:!1});return}e({user:await Bq(),loading:!1})}catch(t){(t?.message?.includes(`401`)||t?.message?.includes(`Unauthorized`))&&localStorage.removeItem(`auth_token`),e({user:null,loading:!1})}}})),NJ=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.fragment`);function r(e,n,r){var i=null;if(r!==void 0&&(i=``+r),n.key!==void 0&&(i=``+n.key),`key`in n)for(var a in r={},n)a!==`key`&&(r[a]=n[a]);else r=n;return n=r.ref,{$$typeof:t,type:e,key:i,ref:n===void 0?null:n,props:r}}e.Fragment=n,e.jsx=r,e.jsxs=r})),$=o(((e,t)=>{t.exports=NJ()}))(),{Sider:PJ,Content:FJ}=sN,IJ={HomeOutlined:(0,$.jsx)(FW,{}),PlayCircleOutlined:(0,$.jsx)(kG,{}),WalletOutlined:(0,$.jsx)(dq,{}),RobotOutlined:(0,$.jsx)(VG,{}),SettingOutlined:(0,$.jsx)(cK,{}),BellOutlined:(0,$.jsx)($H,{}),UserOutlined:(0,$.jsx)(aq,{}),AppstoreOutlined:(0,$.jsx)(FH,{}),FileTextOutlined:(0,$.jsx)(kj,{}),StarOutlined:(0,$.jsx)(MK,{}),HeartOutlined:(0,$.jsx)(EW,{}),CameraOutlined:(0,$.jsx)(mU,{}),DashboardOutlined:(0,$.jsx)(WU,{}),CalculatorOutlined:(0,$.jsx)(dU,{}),DollarOutlined:(0,$.jsx)(XU,{}),GiftOutlined:(0,$.jsx)(bW,{}),ThunderboltOutlined:(0,$.jsx)(WK,{}),FireOutlined:(0,$.jsx)(dW,{}),CloudOutlined:(0,$.jsx)(EU,{}),SmileOutlined:(0,$.jsx)(CK,{}),TrophyOutlined:(0,$.jsx)(XK,{}),RocketOutlined:(0,$.jsx)(WG,{}),BulbOutlined:(0,$.jsx)(cU,{}),CodeOutlined:(0,$.jsx)(MU,{}),PictureOutlined:(0,$.jsx)(CG,{}),VideoCameraOutlined:(0,$.jsx)(cq,{}),AudioOutlined:(0,$.jsx)(WH,{}),MailOutlined:(0,$.jsx)($W,{}),PhoneOutlined:(0,$.jsx)(bG,{}),GlobalOutlined:(0,$.jsx)(CW,{}),ShoppingCartOutlined:(0,$.jsx)(dK,{}),TeamOutlined:(0,$.jsx)(VK,{}),BarChartOutlined:(0,$.jsx)(XH,{}),PieChartOutlined:(0,$.jsx)(EG,{}),LineChartOutlined:(0,$.jsx)(WW,{}),SecurityScanOutlined:(0,$.jsx)(nK,{}),ApiOutlined:(0,$.jsx)(MH,{}),DatabaseOutlined:(0,$.jsx)(qU,{}),CloudServerOutlined:(0,$.jsx)(kU,{}),MenuOutlined:(0,$.jsx)(aG,{}),PlusOutlined:(0,$.jsx)(_O,{}),EditOutlined:(0,$.jsx)(kB,{}),DeleteOutlined:(0,$.jsx)(EB,{}),LockOutlined:(0,$.jsx)(qW,{}),LogoutOutlined:(0,$.jsx)(XW,{})},LJ=()=>{let e=Ye(),t=Ke(),{user:n,loading:r,logout:i}=MJ(),[a,o]=(0,x.useState)(!1),[s,c]=(0,x.useState)([]),[l,u]=(0,x.useState)(!1),[d]=Z.useForm();if((0,x.useEffect)(()=>{gJ().then(e=>{let t=e.filter(e=>{let t=e.menu_target??e.menuTarget??`admin`;return(e.is_active??e.isActive??!0)&&(t===`admin`||t===`both`)}),r=!!(n?.isAdmin??n?.is_admin);if(n&&!r){let e=n.allowedMenus??n?.allowed_menus;if(e&&Array.isArray(e)&&e.length>0){let n=new Set(e);t=t.filter(e=>(e.menu_type??e.menuType)===`group`?t.some(t=>(t.parent_id??t.parentId)===e.id&&n.has(t.path)):n.has(e.path))}else t=[]}c(t)}).catch(()=>{})},[n]),r)return(0,$.jsx)(`div`,{style:{display:`flex`,justifyContent:`center`,alignItems:`center`,height:`100vh`},children:(0,$.jsx)(aP,{size:`large`})});if(!n)return(0,$.jsx)(St,{to:`/admin/login`,replace:!0});let f={};s.filter(e=>(e.menu_type??e.menuType)!==`group`&&(e.parent_id??e.parentId)).forEach(e=>{let t=e.parent_id??e.parentId;f[t]||(f[t]=[]),f[t].push(e)});let p=[...s].sort((e,t)=>(e.sort_order??e.sortOrder??0)-(t.sort_order??t.sortOrder??0)),m=[];p.forEach(e=>{let t=e.menu_type??e.menuType,n=e.parent_id??e.parentId;if(t===`group`){let t=(f[e.id]||[]).sort((e,t)=>(e.sort_order??e.sortOrder??0)-(t.sort_order??t.sortOrder??0)).map(e=>({key:e.path,icon:IJ[e.icon]||void 0,label:e.label}));t.length>0&&m.push({key:`group-${e.id}`,icon:IJ[e.icon]||void 0,label:e.label,children:t})}else n||m.push({key:e.path,icon:IJ[e.icon]||void 0,label:e.label})});let h=!!(n?.isAdmin??n?.is_admin);m.length===0&&h&&m.push({key:`/`,icon:(0,$.jsx)(WU,{}),label:`数据概览`},{key:`/users`,icon:(0,$.jsx)(aq,{}),label:`用户管理`});let g=t.pathname,_=[];m.forEach(e=>{e.children?e.children.forEach(e=>_.push(e.key)):_.push(e.key)}),_.includes(g)||(g=_.find(e=>g.startsWith(e))||`/`);let v=[];return m.forEach(e=>{e.children&&e.children.some(e=>e.key===g)&&v.push(e.key)}),(0,$.jsxs)(sN,{style:{minHeight:`100vh`},children:[(0,$.jsxs)(PJ,{collapsible:!0,collapsed:a,onCollapse:o,width:220,theme:`dark`,style:{background:`linear-gradient(180deg, #0f0f23 0%, #1a1a35 100%)`},children:[(0,$.jsxs)(`div`,{style:{height:64,display:`flex`,alignItems:`center`,justifyContent:`center`,gap:10,borderBottom:`1px solid rgba(255,255,255,0.06)`},children:[(0,$.jsx)(`div`,{style:{width:32,height:32,borderRadius:8,background:`linear-gradient(135deg, #6366f1, #8b5cf6)`,display:`flex`,alignItems:`center`,justifyContent:`center`},children:(0,$.jsx)(WK,{style:{fontSize:16,color:`#fff`}})}),!a&&(0,$.jsx)(`span`,{style:{color:`#f1f5f9`,fontSize:15,fontWeight:700},children:`管理后台`})]}),(0,$.jsx)(cD,{mode:`inline`,selectedKeys:[g],defaultOpenKeys:v,items:m,onClick:({key:t})=>{t.startsWith(`group-`)||e(t)},style:{background:`transparent`,borderRight:0,marginTop:8},theme:`dark`})]}),(0,$.jsxs)(sN,{children:[(0,$.jsxs)(`div`,{style:{height:56,background:`#fff`,borderBottom:`1px solid #f0f0f5`,display:`flex`,alignItems:`center`,justifyContent:`space-between`,padding:`0 24px`},children:[(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:16},children:m.find(e=>e.key===g)?.label||m.flatMap(e=>e.children||[]).find(e=>e.key===g)?.label||`管理后台`}),(0,$.jsx)(Ej,{menu:{items:[{key:`user`,icon:(0,$.jsx)(aq,{}),label:n?.username,disabled:!0},{type:`divider`},{key:`changePwd`,icon:(0,$.jsx)(qW,{}),label:`修改密码`},{key:`logout`,icon:(0,$.jsx)(XW,{}),label:`退出登录`,danger:!0}],onClick:({key:t})=>{t===`logout`&&(i(),e(`/login`)),t===`changePwd`&&(u(!0),d.resetFields())}},placement:`bottomRight`,arrow:!0,children:(0,$.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:8,cursor:`pointer`,padding:`4px 8px`,borderRadius:8,transition:`background 0.2s`},children:[(0,$.jsx)(Zw,{size:28,icon:(0,$.jsx)(aq,{}),style:{background:`linear-gradient(135deg, #6366f1, #8b5cf6)`}}),(0,$.jsx)(Q.Text,{style:{fontSize:13,fontWeight:500},children:n?.username})]})})]}),(0,$.jsx)(FJ,{style:{padding:24,background:`#f5f6fa`,overflow:`auto`},children:m.length===0&&!h?(0,$.jsxs)(`div`,{style:{display:`flex`,flexDirection:`column`,alignItems:`center`,justifyContent:`center`,height:`60vh`,color:`#94a3b8`},children:[(0,$.jsx)(qW,{style:{fontSize:48,marginBottom:16,color:`#cbd5e1`}}),(0,$.jsx)(`div`,{style:{fontSize:16,fontWeight:600,color:`#64748b`},children:`暂无任何权限`}),(0,$.jsx)(`div`,{style:{fontSize:13,marginTop:8},children:`请联系管理员配置菜单权限`})]}):(0,$.jsx)(Ct,{})})]}),(0,$.jsx)(CP,{title:(0,$.jsxs)(wj,{children:[(0,$.jsx)(qW,{}),`修改密码`]}),open:l,onOk:async()=>{try{let e=await d.validateFields();if(e.newPassword!==e.confirmPassword){bP.error(`两次输入的密码不一致`);return}await SJ(e.oldPassword,e.newPassword),bP.success(`密码修改成功`),u(!1),d.resetFields()}catch(e){if(e?.errorFields)return;bP.error(e?.message||`修改失败`)}},onCancel:()=>{u(!1),d.resetFields()},okText:`确认修改`,cancelText:`取消`,width:420,children:(0,$.jsxs)(Z,{form:d,layout:`vertical`,style:{marginTop:16},children:[(0,$.jsx)(Z.Item,{name:`oldPassword`,label:`原密码`,rules:[{required:!0,message:`请输入原密码`}],children:(0,$.jsx)(QM.Password,{placeholder:`请输入原密码`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`newPassword`,label:`新密码`,rules:[{required:!0,min:6,message:`密码至少6位`}],children:(0,$.jsx)(QM.Password,{placeholder:`请输入新密码(至少6位)`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`confirmPassword`,label:`确认新密码`,rules:[{required:!0,message:`请再次输入新密码`}],children:(0,$.jsx)(QM.Password,{placeholder:`请再次输入新密码`,size:`large`})})]})})]})},RJ=()=>{let e=Ye(),{login:t}=MJ(),[n,r]=(0,x.useState)(!1);return(0,$.jsx)(`div`,{style:{minHeight:`100vh`,display:`flex`,alignItems:`center`,justifyContent:`center`,background:`linear-gradient(135deg, #0f0f23 0%, #1a1a35 50%, #0f0f23 100%)`},children:(0,$.jsxs)(Mk,{bordered:!1,style:{width:420,borderRadius:16,boxShadow:`0 20px 60px rgba(0,0,0,0.3)`},children:[(0,$.jsxs)(`div`,{style:{textAlign:`center`,marginBottom:32},children:[(0,$.jsx)(`div`,{style:{width:56,height:56,borderRadius:14,margin:`0 auto 16px`,background:`linear-gradient(135deg, #6366f1, #8b5cf6)`,display:`flex`,alignItems:`center`,justifyContent:`center`,boxShadow:`0 8px 24px rgba(99,102,241,0.3)`},children:(0,$.jsx)(WK,{style:{fontSize:26,color:`#fff`}})}),(0,$.jsxs)(Q.Title,{level:3,style:{margin:0},children:[`VideoGen`,(0,$.jsx)(`span`,{style:{color:`#6366f1`},children:`.AI`})]}),(0,$.jsx)(Q.Text,{type:`secondary`,children:`管理后台`})]}),(0,$.jsxs)(Z,{onFinish:async n=>{r(!0);try{await t(n.username,n.password,n.rememberMe),bP.success(`登录成功`),e(`/`)}catch{bP.error(`登录失败`)}finally{r(!1)}},layout:`vertical`,children:[(0,$.jsx)(Z.Item,{name:`username`,rules:[{required:!0,message:`请输入用户名`}],children:(0,$.jsx)(QM,{placeholder:`用户名`,size:`large`,prefix:(0,$.jsx)(aq,{style:{color:`#94a3b8`,marginRight:8}})})}),(0,$.jsx)(Z.Item,{name:`password`,rules:[{required:!0,message:`请输入密码`}],children:(0,$.jsx)(QM.Password,{placeholder:`密码`,size:`large`,prefix:(0,$.jsx)(qW,{style:{color:`#94a3b8`,marginRight:8}})})}),(0,$.jsx)(Z.Item,{name:`rememberMe`,valuePropName:`checked`,children:(0,$.jsx)(iA,{children:`记住我的登录状态`})}),(0,$.jsx)(Z.Item,{style:{marginBottom:8},children:(0,$.jsx)(mD,{type:`primary`,htmlType:`submit`,loading:n,block:!0,size:`large`,style:{borderRadius:10,fontWeight:600,height:44,background:`linear-gradient(135deg, #6366f1, #8b5cf6)`,border:`none`},children:`登录管理后台`})})]})]})})},zJ=()=>{let[e,t]=(0,x.useState)(null),[n,r]=(0,x.useState)(!0);return(0,x.useEffect)(()=>{(async()=>{r(!0);try{t(await Vq())}catch{}r(!1)})()},[]),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(uF,{gutter:[16,16],children:(e?[{title:`总用户数`,value:e.totalUsers,icon:(0,$.jsx)(aq,{}),color:`#6366f1`,bg:`rgba(99,102,241,0.08)`},{title:`总项目数`,value:e.totalProjects,icon:(0,$.jsx)(MG,{}),color:`#06b6d4`,bg:`rgba(6,182,212,0.08)`},{title:`总生成次数`,value:e.totalGenerations,icon:(0,$.jsx)(kG,{}),color:`#10b981`,bg:`rgba(16,185,129,0.08)`},{title:`总收入(元)`,value:e.totalRevenue,icon:(0,$.jsx)(XU,{}),color:`#f59e0b`,bg:`rgba(245,158,11,0.08)`,prefix:`¥`},{title:`今日消耗积分`,value:e.creditsConsumedToday,icon:(0,$.jsx)(WK,{}),color:`#ef4444`,bg:`rgba(239,68,68,0.08)`}]:[]).map((e,t)=>(0,$.jsx)(dA,{xs:12,sm:8,lg:t<4?6:24,children:(0,$.jsx)(Mk,{bordered:!1,loading:n,style:{borderRadius:12,border:`1px solid #f0f0f5`},children:(0,$.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:14},children:[(0,$.jsx)(`div`,{style:{width:44,height:44,borderRadius:10,background:e.bg,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:20,color:e.color,flexShrink:0},children:e.icon}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(`div`,{style:{color:`#94a3b8`,fontSize:12,marginBottom:2},children:e.title}),(0,$.jsxs)(`div`,{style:{fontSize:22,fontWeight:800,color:`#1a1a2e`},children:[e.prefix,typeof e.value==`number`?e.value.toLocaleString():e.value]})]})]})})},e.title))}),(0,$.jsx)(uF,{gutter:[16,16],style:{marginTop:16},children:(0,$.jsx)(dA,{xs:24,lg:12,children:(0,$.jsx)(Mk,{title:`系统信息`,bordered:!1,style:{borderRadius:12,border:`1px solid #f0f0f5`},children:(0,$.jsx)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:12},children:[{label:`平台名称`,value:`VideoGen.AI`},{label:`API版本`,value:`v1.0.0`},{label:`数据库`,value:`PostgreSQL`},{label:`视频引擎`,value:`Seedance 2.0`}].map(e=>(0,$.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,padding:`8px 0`,borderBottom:`1px solid #f5f6fa`},children:[(0,$.jsx)(Q.Text,{type:`secondary`,children:e.label}),(0,$.jsx)(Q.Text,{strong:!0,children:e.value})]},e.label))})})})})]})};function BJ(e){if(!e)return`-`;let t=e.trim();t.includes(`T`)||(t=t.replace(` `,`T`));let n=t.indexOf(`.`);return n>0&&(t=t.slice(0,n)),t=t.replace(/[+-]\d{2}:?\d{0,2}$/,``).replace(/Z$/,``),t.replace(`T`,` `).slice(0,16)}var VJ=()=>{let[e,t]=(0,x.useState)([]),[n,r]=(0,x.useState)(!0),[i,a]=(0,x.useState)(``),[o,s]=(0,x.useState)(`frontend`),[c,l]=(0,x.useState)({open:!1,user:null}),[u,d]=(0,x.useState)(!1),[f,p]=(0,x.useState)(`frontend`),[m,h]=(0,x.useState)({open:!1,user:null}),[g,_]=(0,x.useState)([]),[v,y]=(0,x.useState)([]),[b,S]=(0,x.useState)({open:!1,user:null}),[C]=Z.useForm(),[w]=Z.useForm(),[T]=Z.useForm(),E=async()=>{r(!0);try{t(await Hq(i||void 0))}catch{}r(!1)};(0,x.useEffect)(()=>{E()},[]);let D=()=>E(),O=async()=>{try{let e=await C.validateFields(),{user:t}=c;if(!t)return;await Uq(t.id,e.amount,e.description),bP.success(`已${e.amount>0?`增加`:`扣除`} ${Math.abs(e.amount)} 积分`),l({open:!1,user:null}),C.resetFields(),E()}catch{}},k=async e=>{await Wq(e.id,!e.isActive),bP.success(e.isActive?`已禁用该用户`:`已启用该用户`),E()},A=async()=>{try{let e=await w.validateFields(),t=e.user_type||`frontend`;await yJ({username:t===`admin`?e.username:void 0,password:e.password,email:e.email||void 0,phone:t===`frontend`?e.phone:e.phone||void 0,credits:e.credits||0,user_type:t}),bP.success(`用户创建成功`),d(!1),w.resetFields(),p(`frontend`),E()}catch{}},j=async e=>{try{let t=await gJ(),n=e.userType===`admin`;_(t.filter(e=>{let t=e.menu_target??e.menuTarget??`frontend`;return n?t===`admin`||t===`both`:t===`frontend`||t===`both`})),y(e.allowedMenus||[]),h({open:!0,user:e})}catch{bP.error(`加载菜单失败`)}},M=g.filter(e=>(e.menu_type??e.menuType)===`group`),N=g.filter(e=>(e.menu_type??e.menuType)!==`group`),P={};N.filter(e=>e.parent_id??e.parentId).forEach(e=>{let t=e.parent_id??e.parentId;P[t]||(P[t]=[]),P[t].push(e)});let F=N.filter(e=>!(e.parent_id??e.parentId)),I=async()=>{let{user:e}=m;if(e)try{await bJ(e.id,v.length>0?v:null),bP.success(`菜单权限已更新`),h({open:!1,user:null}),E()}catch(e){bP.error(e?.message||`保存失败`)}},L=async()=>{try{let e=await T.validateFields(),{user:t}=b;if(!t)return;await xJ(t.id,e.newPassword),bP.success(`已重置 ${t.username} 的密码`),S({open:!1,user:null}),T.resetFields()}catch{}},R=e.filter(e=>e.userType===o),z=o===`admin`,B=[{title:`用户`,key:`user`,width:200,render:(e,t)=>(0,$.jsxs)(wj,{children:[(0,$.jsx)(`div`,{style:{width:32,height:32,borderRadius:8,background:t.isAdmin?`linear-gradient(135deg, #f59e0b, #f97316)`:`linear-gradient(135deg, #6366f1, #8b5cf6)`,display:`flex`,alignItems:`center`,justifyContent:`center`,color:`#fff`,fontSize:13,fontWeight:700},children:t.username.charAt(0).toUpperCase()}),(0,$.jsxs)(`div`,{children:[(0,$.jsxs)(`div`,{style:{fontWeight:600},children:[t.username,t.isAdmin&&(0,$.jsx)(CB,{color:`orange`,style:{marginLeft:6,fontSize:10},children:`管理员`})]}),(0,$.jsx)(`div`,{style:{color:`#94a3b8`,fontSize:12},children:t.email})]})]})},...z?[]:[{title:`积分余额`,dataIndex:`credits`,width:120,sorter:(e,t)=>e.credits-t.credits,render:e=>(0,$.jsx)(Q.Text,{strong:!0,style:{color:e>0?`#10b981`:`#ef4444`,fontSize:15},children:e.toLocaleString()})}],{title:`手机号`,dataIndex:`phone`,width:130,render:e=>(0,$.jsx)(Q.Text,{type:`secondary`,children:e||`-`})},{title:`状态`,dataIndex:`isActive`,width:80,render:e=>(0,$.jsx)(CB,{color:e?`green`:`red`,children:e?`正常`:`禁用`})},{title:`注册时间`,dataIndex:`createdAt`,width:120,render:e=>(0,$.jsx)(Q.Text,{type:`secondary`,style:{fontSize:12},children:BJ(e)})},{title:`最后登录`,dataIndex:`lastLoginAt`,width:140,render:e=>(0,$.jsx)(Q.Text,{type:`secondary`,style:{fontSize:12},children:BJ(e)})},{title:`操作`,key:`action`,width:320,fixed:`right`,render:(e,t)=>(0,$.jsxs)(wj,{size:4,children:[!z&&(0,$.jsx)(mD,{type:`link`,size:`small`,icon:(0,$.jsx)(dq,{}),onClick:()=>{l({open:!0,user:t}),C.resetFields()},children:`调整积分`}),(0,$.jsx)(mD,{type:`link`,size:`small`,icon:(0,$.jsx)(aG,{}),onClick:()=>j(t),children:`菜单权限`}),(0,$.jsx)(mD,{type:`link`,size:`small`,icon:(0,$.jsx)(qW,{}),onClick:()=>{S({open:!0,user:t}),T.resetFields()},children:`重置密码`}),(0,$.jsx)(DP,{title:t.isActive?`确定禁用该用户?`:`确定启用该用户?`,onConfirm:()=>k(t),children:(0,$.jsx)(mD,{type:`link`,size:`small`,danger:t.isActive,icon:t.isActive?(0,$.jsx)(FK,{}):(0,$.jsx)(bU,{}),children:t.isActive?`禁用`:`启用`})})]})}];return(0,$.jsxs)(`div`,{children:[(0,$.jsxs)(Mk,{bordered:!1,style:{borderRadius:12,border:`1px solid #f0f0f5`},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,marginBottom:16},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,gap:12},children:[(0,$.jsx)(QM,{placeholder:`搜索用户名或手机号`,prefix:(0,$.jsx)(KC,{style:{color:`#94a3b8`}}),value:i,onChange:e=>a(e.target.value),onPressEnter:D,style:{width:280,borderRadius:8},allowClear:!0}),(0,$.jsx)(mD,{type:`primary`,onClick:D,style:{borderRadius:8},children:`搜索`})]}),(0,$.jsx)(mD,{type:`primary`,icon:(0,$.jsx)(_O,{}),onClick:()=>{p(o),w.setFieldsValue({user_type:o}),d(!0)},style:{borderRadius:8},children:`创建用户`})]}),(0,$.jsx)(vk,{activeKey:o,onChange:s,items:[{key:`frontend`,label:`前台用户`},{key:`admin`,label:`后台用户`}]}),(0,$.jsx)(uB,{columns:B,dataSource:R,rowKey:`id`,loading:n,pagination:{pageSize:10,showTotal:e=>`共 ${e} 个用户`},scroll:{x:1e3}})]}),(0,$.jsxs)(CP,{title:(0,$.jsxs)(wj,{children:[(0,$.jsx)(dq,{}),`调整积分 - `,c.user?.username]}),open:c.open,onOk:O,onCancel:()=>{l({open:!1,user:null}),C.resetFields()},okText:`确认`,cancelText:`取消`,width:440,children:[(0,$.jsxs)(`div`,{style:{marginBottom:16,padding:`12px 16px`,background:`#f8fafc`,borderRadius:8},children:[(0,$.jsx)(`span`,{style:{color:`#64748b`},children:`当前积分:`}),(0,$.jsx)(`span`,{style:{fontWeight:800,fontSize:18,color:`#6366f1`},children:c.user?.credits.toLocaleString()})]}),(0,$.jsxs)(Z,{form:C,layout:`vertical`,children:[(0,$.jsx)(Z.Item,{name:`amount`,label:`积分变动`,rules:[{required:!0,message:`请输入积分数量`}],children:(0,$.jsx)($A,{style:{width:`100%`},size:`large`,placeholder:`正数增加,负数扣除`,formatter:e=>`${e}`.replace(/\B(?=(\d{3})+(?!\d))/g,`,`)})}),(0,$.jsx)(Z.Item,{name:`description`,label:`原因`,rules:[{required:!0,message:`请输入调整原因`}],children:(0,$.jsx)(QM.TextArea,{rows:2,placeholder:`请输入调整原因`,size:`large`})})]})]}),(0,$.jsx)(CP,{title:(0,$.jsxs)(wj,{children:[(0,$.jsx)(aq,{}),`创建用户`]}),open:u,onOk:A,onCancel:()=>{d(!1),w.resetFields(),p(`frontend`)},okText:`创建`,cancelText:`取消`,width:480,children:(0,$.jsxs)(Z,{form:w,layout:`vertical`,style:{marginTop:16},onValuesChange:e=>{e.user_type&&p(e.user_type)},children:[(0,$.jsx)(Z.Item,{name:`user_type`,label:`用户类型`,initialValue:`frontend`,rules:[{required:!0}],children:(0,$.jsx)(ZC,{size:`large`,options:[{value:`frontend`,label:`前端用户`},{value:`admin`,label:`后台管理员`}]})}),f===`frontend`?(0,$.jsx)(Z.Item,{name:`phone`,label:`手机号`,rules:[{required:!0,message:`请输入手机号`},{pattern:/^1\d{10}$/,message:`请输入正确的手机号`}],children:(0,$.jsx)(QM,{placeholder:`请输入手机号`,maxLength:11,size:`large`})}):(0,$.jsx)(Z.Item,{name:`username`,label:`用户名`,rules:[{required:!0,message:`请输入用户名`}],children:(0,$.jsx)(QM,{placeholder:`请输入用户名`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`password`,label:`密码`,rules:[{required:!0,min:6,message:`密码至少6位`}],children:(0,$.jsx)(QM.Password,{placeholder:`请输入密码(至少6位)`,size:`large`})}),f===`frontend`&&(0,$.jsx)(Z.Item,{name:`credits`,label:`初始积分`,initialValue:0,children:(0,$.jsx)($A,{min:0,style:{width:`100%`},size:`large`})}),(0,$.jsx)(Z.Item,{name:`email`,label:`邮箱`,children:(0,$.jsx)(QM,{placeholder:`选填`,size:`large`})})]})}),(0,$.jsxs)(CP,{title:(0,$.jsxs)(wj,{children:[(0,$.jsx)(aG,{}),`菜单权限 - `,m.user?.username,` (`,m.user?.userType===`admin`?`后台菜单`:`前台菜单`,`)`]}),open:m.open,onOk:I,onCancel:()=>{h({open:!1,user:null})},okText:`保存`,cancelText:`取消`,width:520,children:[(0,$.jsx)(`div`,{style:{marginBottom:12},children:(0,$.jsx)(Q.Text,{type:`secondary`,style:{fontSize:13},children:`勾选该用户可访问的菜单,不勾选则显示全部菜单`})}),(0,$.jsx)(`div`,{style:{padding:`12px 16px`,background:`#f8fafc`,borderRadius:8,maxHeight:400,overflow:`auto`},children:(0,$.jsx)(iA.Group,{value:v,onChange:e=>y(e),children:(0,$.jsxs)(wj,{direction:`vertical`,size:8,style:{width:`100%`},children:[F.map(e=>(0,$.jsxs)(iA,{value:e.path,style:{width:`100%`},children:[e.label,(0,$.jsx)(Q.Text,{type:`secondary`,style:{fontSize:12,marginLeft:8},children:e.path})]},e.path)),M.map(e=>{let t=P[e.id]||[];return t.length===0?null:(0,$.jsxs)(`div`,{children:[(0,$.jsx)(`div`,{style:{fontWeight:600,fontSize:13,color:`#6366f1`,marginBottom:4,marginTop:4},children:e.label}),(0,$.jsx)(wj,{direction:`vertical`,size:4,style:{paddingLeft:12,width:`100%`},children:t.map(e=>(0,$.jsxs)(iA,{value:e.path,style:{width:`100%`},children:[e.label,(0,$.jsx)(Q.Text,{type:`secondary`,style:{fontSize:12,marginLeft:8},children:e.path})]},e.path))})]},e.id)})]})})})]}),(0,$.jsx)(CP,{title:(0,$.jsxs)(wj,{children:[(0,$.jsx)(qW,{}),`重置密码 - `,b.user?.username]}),open:b.open,onOk:L,onCancel:()=>{S({open:!1,user:null}),T.resetFields()},okText:`确认重置`,cancelText:`取消`,width:420,children:(0,$.jsx)(Z,{form:T,layout:`vertical`,style:{marginTop:16},children:(0,$.jsx)(Z.Item,{name:`newPassword`,label:`新密码`,rules:[{required:!0,min:6,message:`密码至少6位`}],children:(0,$.jsx)(QM.Password,{placeholder:`请输入新密码(至少6位)`,size:`large`})})})})]})},HJ=()=>{let[e,t]=(0,x.useState)([]),[n,r]=(0,x.useState)(!0),[i,a]=(0,x.useState)({open:!1,model:null}),[o]=Z.useForm(),s=async()=>{r(!0);try{t(await Gq())}catch{}r(!1)};(0,x.useEffect)(()=>{s()},[]);let c=async()=>{try{let e=await o.validateFields(),t={name:e.name,provider:e.provider,model_name:e.modelName,api_base:e.apiBase,api_key:e.apiKey,weight:e.weight,max_tokens:e.maxTokens,temperature:e.temperature,is_active:e.isActive??!0,priority:e.priority??0};i.model?.id?await Kq({id:i.model.id,...t}):await Kq(t),bP.success(i.model?.id?`模型配置已更新`:`模型配置已添加`),a({open:!1,model:null}),o.resetFields(),s()}catch(e){if(e?.errorFields)return;bP.error(e?.message||`保存失败`)}},l=async e=>{try{await qq(e),bP.success(`模型配置已删除`),s()}catch(e){bP.error(e?.message||`删除失败`)}},u=e=>{a({open:!0,model:e||null}),e?o.setFieldsValue(e):(o.resetFields(),o.setFieldsValue({provider:`sdk`,weight:1,maxTokens:4096,temperature:.7,isActive:!0,priority:0}))};return(0,$.jsxs)(`div`,{children:[(0,$.jsxs)(Mk,{bordered:!1,style:{borderRadius:12,border:`1px solid #f0f0f5`},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,marginBottom:16},children:[(0,$.jsxs)(Q.Text,{type:`secondary`,children:[`共 `,e.length,` 个模型配置,按权重进行加权随机调度`]}),(0,$.jsx)(mD,{type:`primary`,icon:(0,$.jsx)(_O,{}),onClick:()=>u(),style:{borderRadius:8},children:`添加模型`})]}),(0,$.jsx)(uB,{columns:[{title:`模型名称`,dataIndex:`name`,width:150,render:(e,t)=>(0,$.jsxs)(wj,{children:[(0,$.jsx)(`div`,{style:{width:32,height:32,borderRadius:8,background:t.isActive?`linear-gradient(135deg, #6366f1, #8b5cf6)`:`linear-gradient(135deg, #94a3b8, #cbd5e1)`,display:`flex`,alignItems:`center`,justifyContent:`center`,color:`#fff`,fontSize:14},children:(0,$.jsx)(VG,{})}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(`div`,{style:{fontWeight:600},children:e}),(0,$.jsx)(`div`,{style:{color:`#94a3b8`,fontSize:12},children:t.modelName})]})]})},{title:`提供商`,dataIndex:`provider`,width:140,render:e=>(0,$.jsx)(CB,{color:e===`mock`?`default`:`blue`,children:{sdk:`SDK模式`,openai_compatible:`OpenAI兼容`,mock:`Mock模式`}[e]||e})},{title:`API地址`,dataIndex:`apiBase`,width:200,render:e=>(0,$.jsx)(Q.Text,{type:`secondary`,style:{fontSize:12},ellipsis:!0,children:e||`-`})},{title:`权重`,dataIndex:`weight`,width:80,sorter:(e,t)=>e.weight-t.weight},{title:`Max Tokens`,dataIndex:`maxTokens`,width:100},{title:`Temperature`,dataIndex:`temperature`,width:100,render:e=>e.toFixed(1)},{title:`状态`,dataIndex:`isActive`,width:80,render:e=>(0,$.jsx)(CB,{color:e?`green`:`default`,children:e?`启用`:`停用`})},{title:`操作`,key:`action`,width:150,fixed:`right`,render:(e,t)=>(0,$.jsxs)(wj,{size:4,children:[(0,$.jsx)(mD,{type:`link`,size:`small`,icon:(0,$.jsx)(kB,{}),onClick:()=>u(t),children:`编辑`}),(0,$.jsx)(DP,{title:`确定删除该模型配置?`,onConfirm:()=>l(t.id),children:(0,$.jsx)(mD,{type:`link`,size:`small`,danger:!0,icon:(0,$.jsx)(EB,{}),children:`删除`})})]})}],dataSource:e,rowKey:`id`,loading:n,pagination:!1,scroll:{x:900}})]}),(0,$.jsx)(CP,{title:(0,$.jsxs)(wj,{children:[(0,$.jsx)(VG,{}),i.model?.id?`编辑模型`:`添加模型`]}),open:i.open,onOk:c,onCancel:()=>{a({open:!1,model:null}),o.resetFields()},okText:`确认`,cancelText:`取消`,width:560,children:(0,$.jsxs)(Z,{form:o,layout:`vertical`,children:[(0,$.jsx)(Z.Item,{name:`name`,label:`显示名称`,rules:[{required:!0,message:`请输入模型名称`}],children:(0,$.jsx)(QM,{placeholder:`例如:GPT-4o`,size:`large`})}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`provider`,label:`提供商`,style:{flex:1},rules:[{required:!0}],children:(0,$.jsx)(ZC,{size:`large`,options:[{value:`sdk`,label:`SDK模式`},{value:`openai_compatible`,label:`OpenAI兼容`},{value:`mock`,label:`Mock模式`}]})}),(0,$.jsx)(Z.Item,{name:`modelName`,label:`模型标识`,style:{flex:1},rules:[{required:!0,message:`请输入模型标识`}],children:(0,$.jsx)(QM,{placeholder:`例如:gpt-4o`,size:`large`})})]}),(0,$.jsx)(Z.Item,{name:`apiBase`,label:`API地址`,children:(0,$.jsx)(QM,{placeholder:`https://api.openai.com/v1`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`apiKey`,label:`API Key`,children:(0,$.jsx)(QM.Password,{placeholder:`sk-****`,size:`large`})}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`weight`,label:`权重`,style:{flex:1},rules:[{required:!0}],children:(0,$.jsx)($A,{min:0,max:10,style:{width:`100%`},size:`large`})}),(0,$.jsx)(Z.Item,{name:`maxTokens`,label:`Max Tokens`,style:{flex:1},rules:[{required:!0}],children:(0,$.jsx)($A,{min:256,max:128e3,style:{width:`100%`},size:`large`})}),(0,$.jsx)(Z.Item,{name:`temperature`,label:`Temperature`,style:{flex:1},rules:[{required:!0}],children:(0,$.jsx)($A,{min:0,max:2,step:.1,style:{width:`100%`},size:`large`})})]}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`priority`,label:`优先级`,style:{flex:1},rules:[{required:!0}],children:(0,$.jsx)($A,{min:0,max:10,style:{width:`100%`},size:`large`})}),(0,$.jsx)(Z.Item,{name:`isActive`,label:`启用`,valuePropName:`checked`,style:{flex:1,paddingTop:30},children:(0,$.jsx)(yF,{})})]})]})})]})},UJ=()=>{let[e,t]=(0,x.useState)([]),[n,r]=(0,x.useState)(!0),[i,a]=(0,x.useState)(!1),[o,s]=(0,x.useState)(``),[c]=Z.useForm();(0,x.useEffect)(()=>{l()},[]);let l=async()=>{r(!0);let e=await Jq();t(e);let n={};e.forEach(e=>{n[e.key]=e.value}),c.setFieldsValue(n),r(!1)},u=async()=>{try{let n=await c.validateFields();a(!0);for(let t of e){let e=n[t.key];e!==void 0&&e!==t.value&&await Yq(t.id,e??``)}bP.success(`系统配置已保存`),t(await Jq()),a(!1)}catch(e){a(!1),bP.error(e?.message||`保存失败`)}},d=async(e,n)=>{s(n);try{let r=await Xq(e,n);t(e=>e.map(e=>e.key===n?{...e,value:r.url}:e)),c.setFieldsValue({[n]:r.url}),bP.success(`PDF上传成功`)}catch{bP.error(`上传失败`)}finally{s(``)}return!1},f={站点信息:e.filter(e=>e.key.startsWith(`site_`)),协议配置:e.filter(e=>e.key===`user_agreement_url`||e.key===`privacy_policy_url`),"SEO 设置":e.filter(e=>e.key.startsWith(`seo_`))},p=e=>({site_name:`平台显示名称,将展示在页面标题和导航栏`,site_logo:`平台Logo图片URL,建议尺寸 200x40px`,user_agreement_url:`用户注册/登录时需同意的用户协议PDF文件`,privacy_policy_url:`用户注册/登录时需同意的隐私政策PDF文件`,seo_title:`搜索引擎结果中显示的标题`,seo_description:`搜索引擎结果中显示的描述文字,建议150字以内`,seo_keywords:`用逗号分隔的关键词列表`})[e.key]||e.description||``,m=e=>e.key===`seo_description`?(0,$.jsx)(QM.TextArea,{rows:3,placeholder:e.description,size:`large`}):e.key===`seo_keywords`?(0,$.jsx)(QM,{placeholder:`关键词1, 关键词2, 关键词3`,size:`large`}):(0,$.jsx)(QM,{placeholder:e.description,size:`large`}),h=({config:e})=>{let t=e.key===`user_agreement_url`?`用户协议`:`隐私政策`,n=e.value&&e.value.startsWith(`/uploads/`);return(0,$.jsxs)(`div`,{style:{padding:`16px`,borderRadius:10,border:`1px solid #f0f0f5`,background:`#fafbfc`,marginBottom:12},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,alignItems:`center`,marginBottom:8},children:[(0,$.jsxs)(wj,{children:[(0,$.jsx)(cW,{style:{color:`#ef4444`,fontSize:18}}),(0,$.jsx)(Q.Text,{strong:!0,children:t})]}),(0,$.jsxs)(wj,{children:[n&&(0,$.jsx)(mD,{size:`small`,icon:(0,$.jsx)(AM,{}),onClick:()=>window.open(`http://localhost:8000${e.value}`,`_blank`),children:`预览`}),(0,$.jsx)(_H,{accept:`.pdf`,showUploadList:!1,beforeUpload:t=>d(t,e.key),children:(0,$.jsx)(mD,{size:`small`,type:`primary`,icon:(0,$.jsx)(nq,{}),loading:o===e.key,children:n?`重新上传`:`上传PDF`})})]})]}),(0,$.jsx)(Q.Text,{type:`secondary`,style:{fontSize:12},children:n?`已上传: ${e.value}`:`尚未上传,前台将不显示对应链接`})]})};return n?(0,$.jsx)(Mk,{loading:!0,bordered:!1,style:{borderRadius:12}}):(0,$.jsxs)(`div`,{style:{maxWidth:720},children:[(0,$.jsxs)(Mk,{bordered:!1,style:{borderRadius:12,border:`1px solid #f0f0f5`,marginBottom:16},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:12,marginBottom:24},children:[(0,$.jsx)(`div`,{style:{width:44,height:44,borderRadius:10,background:`rgba(99,102,241,0.08)`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:20,color:`#6366f1`},children:(0,$.jsx)(cK,{})}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Title,{level:4,style:{margin:0},children:`系统设置`}),(0,$.jsx)(Q.Text,{type:`secondary`,children:`管理站点基础信息、协议文件和SEO配置`})]})]}),(0,$.jsx)(Z,{form:c,layout:`vertical`,children:Object.entries(f).map(([e,t])=>(0,$.jsxs)(`div`,{style:{marginBottom:24},children:[(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:14,display:`block`,marginBottom:12,paddingBottom:8,borderBottom:`1px solid #f0f0f5`},children:e}),e===`协议配置`?t.map(e=>(0,$.jsx)(h,{config:e},e.id)):t.map(e=>(0,$.jsx)(Z.Item,{name:e.key,label:(0,$.jsx)(`span`,{style:{fontWeight:500},children:e.description}),extra:p(e),children:m(e)},e.id))]},e))})]}),(0,$.jsx)(`div`,{style:{display:`flex`,justifyContent:`flex-end`},children:(0,$.jsx)(mD,{type:`primary`,icon:(0,$.jsx)($G,{}),onClick:u,loading:i,size:`large`,style:{borderRadius:8,minWidth:140},children:`保存配置`})})]})},WJ=()=>{let[e,t]=(0,x.useState)([]),[n,r]=(0,x.useState)(!1),[i,a]=(0,x.useState)([]),[o,s]=(0,x.useState)(!1),[c]=Z.useForm(),[l,u]=(0,x.useState)({open:!1,notifId:``,title:``}),[d,f]=(0,x.useState)([]),[p,m]=(0,x.useState)(!1),h=async()=>{r(!0);try{let e=await fJ(),n=await Hq(),r={};n.forEach(e=>{r[e.id]=e.username}),t((e.items||[]).map(e=>({id:e.id,title:e.title,content:e.content,type:e.type,targetUserId:e.userId||null,target:e.userId?r[e.userId]||e.userId:`全部用户`,createdAt:e.createdAt}))),a(n.map(e=>({id:e.id,username:e.username})))}catch{bP.error(`加载通知列表失败`)}finally{r(!1)}};(0,x.useEffect)(()=>{h()},[]);let g=async()=>{try{let e=await c.validateFields();await pJ({title:e.title,content:e.content,type:e.type||`system`,target_user_id:e.target_user_id||void 0}),bP.success(`消息已发送`),s(!1),c.resetFields(),h()}catch{}},_=async e=>{try{await mJ(e),bP.success(`已删除`),h()}catch(e){bP.error(e?.message||`删除失败`)}},v=async(e,t)=>{u({open:!0,notifId:e,title:t}),m(!0);try{f((await hJ(e)).items||[])}catch{bP.error(`加载已读列表失败`)}finally{m(!1)}},y=e=>{switch(e){case`system`:return`blue`;case`credit`:return`orange`;case`promo`:return`purple`;default:return`default`}};return(0,$.jsxs)(`div`,{children:[(0,$.jsxs)(Mk,{bordered:!1,style:{borderRadius:12,border:`1px solid #f0f0f5`},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,marginBottom:16},children:[(0,$.jsxs)(wj,{children:[(0,$.jsx)($H,{style:{fontSize:18,color:`#6366f1`}}),(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:16},children:`消息推送管理`}),(0,$.jsxs)(CB,{color:`purple`,children:[e.length,` 条消息`]})]}),(0,$.jsx)(mD,{type:`primary`,icon:(0,$.jsx)(_O,{}),onClick:()=>s(!0),style:{borderRadius:8},children:`发送新消息`})]}),(0,$.jsx)(uB,{columns:[{title:`标题`,dataIndex:`title`,width:200,render:e=>(0,$.jsx)(Q.Text,{strong:!0,children:e})},{title:`内容`,dataIndex:`content`,ellipsis:!0},{title:`类型`,dataIndex:`type`,width:80,render:e=>(0,$.jsx)(CB,{color:y(e),children:{system:`系统`,credit:`积分`,promo:`活动`}[e]||e})},{title:`发送目标`,dataIndex:`target`,width:120,render:e=>(0,$.jsx)(CB,{color:e===`全部用户`?`green`:`blue`,children:e})},{title:`发送时间`,dataIndex:`createdAt`,width:160,render:e=>BJ(e)},{title:`操作`,key:`action`,width:180,render:(e,t)=>(0,$.jsxs)(wj,{size:4,children:[(0,$.jsx)(mD,{type:`link`,size:`small`,icon:(0,$.jsx)(AM,{}),onClick:()=>v(t.id,t.title),children:`已读`}),(0,$.jsx)(DP,{title:`确定删除该消息?`,onConfirm:()=>_(t.id),children:(0,$.jsx)(mD,{type:`link`,danger:!0,size:`small`,icon:(0,$.jsx)(EB,{}),children:`删除`})})]})}],dataSource:e,rowKey:`id`,loading:n,pagination:{pageSize:10,showTotal:e=>`共 ${e} 条消息`},scroll:{x:900}})]}),(0,$.jsx)(CP,{title:(0,$.jsxs)(wj,{children:[(0,$.jsx)(aK,{}),`发送消息`]}),open:o,onOk:g,onCancel:()=>{s(!1),c.resetFields()},okText:`发送`,cancelText:`取消`,width:520,children:(0,$.jsxs)(Z,{form:c,layout:`vertical`,style:{marginTop:16},children:[(0,$.jsx)(Z.Item,{name:`title`,label:`消息标题`,rules:[{required:!0,message:`请输入标题`}],children:(0,$.jsx)(QM,{placeholder:`请输入消息标题`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`content`,label:`消息内容`,rules:[{required:!0,message:`请输入内容`}],children:(0,$.jsx)(QM.TextArea,{rows:4,placeholder:`请输入消息内容`,size:`large`})}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`type`,label:`消息类型`,style:{flex:1},initialValue:`system`,rules:[{required:!0}],children:(0,$.jsx)(ZC,{size:`large`,options:[{value:`system`,label:`系统通知`},{value:`credit`,label:`积分通知`},{value:`promo`,label:`活动通知`}]})}),(0,$.jsx)(Z.Item,{name:`target_user_id`,label:`发送目标`,style:{flex:1},extra:`留空则发送给全部用户`,children:(0,$.jsx)(ZC,{size:`large`,allowClear:!0,placeholder:`全部用户`,options:i.map(e=>({value:e.id,label:e.username}))})})]})]})}),(0,$.jsx)(CP,{title:(0,$.jsxs)(wj,{children:[(0,$.jsx)(VK,{}),`已读用户 — `,l.title]}),open:l.open,onCancel:()=>{u({open:!1,notifId:``,title:``}),f([])},footer:null,width:480,children:p?(0,$.jsx)(`div`,{style:{textAlign:`center`,padding:40},children:`加载中...`}):d.length===0?(0,$.jsx)(xC,{description:`暂无用户已读`,style:{padding:`40px 0`}}):(0,$.jsxs)(`div`,{children:[(0,$.jsxs)(`div`,{style:{marginBottom:12,color:`#64748b`,fontSize:13},children:[`共 `,d.length,` 人已读`]}),(0,$.jsx)(uB,{dataSource:d,rowKey:`userId`,pagination:!1,size:`small`,columns:[{title:`用户名`,dataIndex:`username`,render:e=>(0,$.jsx)(Q.Text,{strong:!0,children:e})},{title:`已读时间`,dataIndex:`readAt`,width:180,render:e=>BJ(e)}]})]})})]})},GJ={recharge:{text:`充值`,color:`green`,icon:(0,$.jsx)(VH,{})},consume:{text:`消费`,color:`red`,icon:(0,$.jsx)(RH,{})},refund:{text:`退回`,color:`blue`,icon:(0,$.jsx)(qG,{})}},KJ=()=>{let[e,t]=(0,x.useState)([]),[n,r]=(0,x.useState)(0),[i,a]=(0,x.useState)(!1),[o,s]=(0,x.useState)(``),c=async e=>{a(!0);try{let n=await Zq(e?{type:e}:void 0);t(n.items||[]),r(n.total||0)}catch{bP.error(`加载积分记录失败`)}finally{a(!1)}};(0,x.useEffect)(()=>{c()},[]);let l=e=>{s(e),c(e||void 0)},u=e.filter(e=>e.type===`recharge`).reduce((e,t)=>e+t.amount,0),d=e.filter(e=>e.type===`consume`).reduce((e,t)=>e+Math.abs(t.amount),0);return(0,$.jsxs)(`div`,{children:[(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,marginBottom:16},children:[(0,$.jsx)(Mk,{bordered:!1,style:{flex:1,borderRadius:12,border:`1px solid #f0f0f5`},children:(0,$.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:12},children:[(0,$.jsx)(`div`,{style:{width:44,height:44,borderRadius:10,background:`rgba(16,185,129,0.08)`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:20,color:`#10b981`},children:(0,$.jsx)(VH,{})}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(`div`,{style:{color:`#94a3b8`,fontSize:12},children:`总充值`}),(0,$.jsxs)(`div`,{style:{fontSize:22,fontWeight:800,color:`#10b981`},children:[`+`,u.toLocaleString()]})]})]})}),(0,$.jsx)(Mk,{bordered:!1,style:{flex:1,borderRadius:12,border:`1px solid #f0f0f5`},children:(0,$.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:12},children:[(0,$.jsx)(`div`,{style:{width:44,height:44,borderRadius:10,background:`rgba(239,68,68,0.08)`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:20,color:`#ef4444`},children:(0,$.jsx)(RH,{})}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(`div`,{style:{color:`#94a3b8`,fontSize:12},children:`总消费`}),(0,$.jsxs)(`div`,{style:{fontSize:22,fontWeight:800,color:`#ef4444`},children:[`-`,d.toLocaleString()]})]})]})}),(0,$.jsx)(Mk,{bordered:!1,style:{flex:1,borderRadius:12,border:`1px solid #f0f0f5`},children:(0,$.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:12},children:[(0,$.jsx)(`div`,{style:{width:44,height:44,borderRadius:10,background:`rgba(99,102,241,0.08)`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:20,color:`#6366f1`},children:(0,$.jsx)(dq,{})}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(`div`,{style:{color:`#94a3b8`,fontSize:12},children:`交易笔数`}),(0,$.jsx)(`div`,{style:{fontSize:22,fontWeight:800,color:`#1a1a2e`},children:n})]})]})})]}),(0,$.jsxs)(Mk,{bordered:!1,style:{borderRadius:12,border:`1px solid #f0f0f5`},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,marginBottom:16},children:[(0,$.jsx)(wj,{children:(0,$.jsx)(ZC,{value:o,onChange:l,style:{width:120},options:[{value:``,label:`全部类型`},{value:`recharge`,label:`充值`},{value:`consume`,label:`消费`},{value:`refund`,label:`退回`}]})}),(0,$.jsx)(mD,{icon:(0,$.jsx)(lF,{}),onClick:()=>c(o||void 0),children:`刷新`})]}),(0,$.jsx)(uB,{columns:[{title:`用户`,dataIndex:`username`,width:120,render:e=>(0,$.jsx)(Q.Text,{strong:!0,children:e})},{title:`类型`,dataIndex:`type`,width:100,render:e=>{let t=GJ[e]||{text:e||`-`,color:`default`,icon:null};return(0,$.jsx)(CB,{color:t.color,icon:t.icon,children:t.text})}},{title:`变动积分`,dataIndex:`amount`,width:120,sorter:(e,t)=>e.amount-t.amount,render:e=>(0,$.jsxs)(Q.Text,{strong:!0,style:{color:e>0?`#10b981`:`#ef4444`,fontSize:15},children:[e>0?`+`:``,e.toLocaleString()]})},{title:`变动后余额`,dataIndex:`balanceAfter`,width:120,render:e=>(0,$.jsx)(Q.Text,{type:`secondary`,children:e.toLocaleString()})},{title:`说明`,dataIndex:`description`,ellipsis:!0},{title:`时间`,dataIndex:`createdAt`,width:160,render:e=>(0,$.jsx)(Q.Text,{type:`secondary`,style:{fontSize:12},children:BJ(e)})}],dataSource:e,rowKey:`id`,loading:i,pagination:{pageSize:10,showTotal:e=>`共 ${e} 条记录`},scroll:{x:800}})]})]})},qJ=()=>{let[e,t]=(0,x.useState)(!1),[n,r]=(0,x.useState)([]),[i,a]=(0,x.useState)(!1),[o,s]=(0,x.useState)(!1),[c]=Z.useForm(),l=async()=>{try{let e=await uJ();r(e);let t={};e.forEach(e=>{t[e.key]=e.value}),c.setFieldsValue({wechat_mch_id:t.payment_wechat_mch_id||``,wechat_api_key:t.payment_wechat_api_key||``,wechat_cert_path:t.payment_wechat_cert_path||``,wechat_notify_url:t.payment_wechat_notify_url||``,alipay_app_id:t.payment_alipay_app_id||``,alipay_private_key:t.payment_alipay_private_key||``,alipay_public_key:t.payment_alipay_public_key||``,alipay_notify_url:t.payment_alipay_notify_url||``}),a(t.payment_wechat_enabled===`true`),s(t.payment_alipay_enabled===`true`)}catch{bP.error(`加载支付配置失败`)}};return(0,x.useEffect)(()=>{l()},[]),(0,$.jsxs)(`div`,{style:{maxWidth:720},children:[(0,$.jsxs)(Mk,{bordered:!1,style:{borderRadius:12,border:`1px solid #f0f0f5`,marginBottom:16},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,justifyContent:`space-between`,marginBottom:20},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:12},children:[(0,$.jsx)(`div`,{style:{width:44,height:44,borderRadius:10,background:`rgba(7,193,96,0.08)`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:22,color:`#07c160`},children:(0,$.jsx)(mq,{})}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Title,{level:5,style:{margin:0},children:`微信支付`}),(0,$.jsx)(Q.Text,{type:`secondary`,style:{fontSize:12},children:`微信商户号支付配置`})]})]}),(0,$.jsx)(yF,{checked:i,onChange:a,checkedChildren:`已启用`,unCheckedChildren:`未启用`})]}),(0,$.jsxs)(Z,{form:c,layout:`vertical`,children:[(0,$.jsx)(Z.Item,{name:`wechat_mch_id`,label:`商户号 (MchID)`,children:(0,$.jsx)(QM,{placeholder:`微信支付商户号`,size:`large`,disabled:!i})}),(0,$.jsx)(Z.Item,{name:`wechat_api_key`,label:`API密钥`,children:(0,$.jsx)(QM.Password,{placeholder:`微信支付API密钥`,size:`large`,disabled:!i})}),(0,$.jsx)(Z.Item,{name:`wechat_cert_path`,label:`证书路径`,children:(0,$.jsx)(QM,{placeholder:`apiclient_cert.pem 路径`,size:`large`,disabled:!i})}),(0,$.jsx)(Z.Item,{name:`wechat_notify_url`,label:`回调地址`,children:(0,$.jsx)(QM,{placeholder:`https://yourdomain.com/api/payments/wechat/callback`,size:`large`,disabled:!i})})]})]}),(0,$.jsxs)(Mk,{bordered:!1,style:{borderRadius:12,border:`1px solid #f0f0f5`,marginBottom:16},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,justifyContent:`space-between`,marginBottom:20},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:12},children:[(0,$.jsx)(`div`,{style:{width:44,height:44,borderRadius:10,background:`rgba(0,122,255,0.08)`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:22,color:`#007aff`},children:(0,$.jsx)(kH,{})}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Title,{level:5,style:{margin:0},children:`支付宝`}),(0,$.jsx)(Q.Text,{type:`secondary`,style:{fontSize:12},children:`支付宝应用支付配置`})]})]}),(0,$.jsx)(yF,{checked:o,onChange:s,checkedChildren:`已启用`,unCheckedChildren:`未启用`})]}),(0,$.jsxs)(Z,{form:c,layout:`vertical`,children:[(0,$.jsx)(Z.Item,{name:`alipay_app_id`,label:`AppID`,children:(0,$.jsx)(QM,{placeholder:`支付宝应用AppID`,size:`large`,disabled:!o})}),(0,$.jsx)(Z.Item,{name:`alipay_private_key`,label:`应用私钥`,children:(0,$.jsx)(QM.TextArea,{rows:3,placeholder:`支付宝应用私钥 (PKCS8格式)`,disabled:!o})}),(0,$.jsx)(Z.Item,{name:`alipay_public_key`,label:`支付宝公钥`,children:(0,$.jsx)(QM.TextArea,{rows:3,placeholder:`支付宝公钥`,disabled:!o})}),(0,$.jsx)(Z.Item,{name:`alipay_notify_url`,label:`回调地址`,children:(0,$.jsx)(QM,{placeholder:`https://yourdomain.com/api/payments/alipay/callback`,size:`large`,disabled:!o})})]})]}),(0,$.jsx)(`div`,{style:{display:`flex`,justifyContent:`flex-end`},children:(0,$.jsx)(mD,{type:`primary`,icon:(0,$.jsx)($G,{}),onClick:async()=>{try{let e=await c.validateFields();t(!0);let r=[[`payment_wechat_enabled`,String(i)],[`payment_wechat_mch_id`,e.wechat_mch_id||``],[`payment_wechat_api_key`,e.wechat_api_key||``],[`payment_wechat_cert_path`,e.wechat_cert_path||``],[`payment_wechat_notify_url`,e.wechat_notify_url||``],[`payment_alipay_enabled`,String(o)],[`payment_alipay_app_id`,e.alipay_app_id||``],[`payment_alipay_private_key`,e.alipay_private_key||``],[`payment_alipay_public_key`,e.alipay_public_key||``],[`payment_alipay_notify_url`,e.alipay_notify_url||``]];for(let[e,t]of r){let r=n.find(t=>t.key===e);r&&await dJ(r.id,t)}bP.success(`支付配置已保存`),l()}catch{bP.error(`保存失败`)}finally{t(!1)}},loading:e,size:`large`,style:{borderRadius:8,minWidth:140},children:`保存配置`})})]})},JJ=[{name:`ShoppingCartOutlined`,label:`购物`,component:(0,$.jsx)(dK,{})},{name:`BookOutlined`,label:`书籍`,component:(0,$.jsx)(nU,{})},{name:`HomeOutlined`,label:`房产`,component:(0,$.jsx)(FW,{})},{name:`FireOutlined`,label:`火`,component:(0,$.jsx)(dW,{})},{name:`RocketOutlined`,label:`科技`,component:(0,$.jsx)(WG,{})},{name:`SkinOutlined`,label:`时尚`,component:(0,$.jsx)(bK,{})},{name:`CompassOutlined`,label:`指南`,component:(0,$.jsx)(RU,{})},{name:`HeartOutlined`,label:`健康`,component:(0,$.jsx)(EW,{})},{name:`CarOutlined`,label:`汽车`,component:(0,$.jsx)(_U,{})},{name:`CameraOutlined`,label:`相机`,component:(0,$.jsx)(mU,{})},{name:`CloudOutlined`,label:`云`,component:(0,$.jsx)(EU,{})},{name:`StarOutlined`,label:`星`,component:(0,$.jsx)(MK,{})},{name:`TrophyOutlined`,label:`奖杯`,component:(0,$.jsx)(XK,{})},{name:`ThunderboltOutlined`,label:`闪电`,component:(0,$.jsx)(WK,{})},{name:`BulbOutlined`,label:`灯泡`,component:(0,$.jsx)(cU,{})},{name:`CoffeeOutlined`,label:`咖啡`,component:(0,$.jsx)(FU,{})},{name:`CrownOutlined`,label:`皇冠`,component:(0,$.jsx)(VU,{})},{name:`DashboardOutlined`,label:`仪表`,component:(0,$.jsx)(WU,{})},{name:`FlagOutlined`,label:`旗帜`,component:(0,$.jsx)(mW,{})},{name:`GlobalOutlined`,label:`全球`,component:(0,$.jsx)(CW,{})},{name:`GiftOutlined`,label:`礼物`,component:(0,$.jsx)(bW,{})},{name:`LaptopOutlined`,label:`笔记本`,component:(0,$.jsx)(VW,{})},{name:`MobileOutlined`,label:`手机`,component:(0,$.jsx)(dG,{})},{name:`MonitorOutlined`,label:`显示器`,component:(0,$.jsx)(mG,{})},{name:`PayCircleOutlined`,label:`支付`,component:(0,$.jsx)(_G,{})},{name:`PictureOutlined`,label:`图片`,component:(0,$.jsx)(CG,{})},{name:`PlayCircleOutlined`,label:`播放`,component:(0,$.jsx)(kG,{})},{name:`SafetyOutlined`,label:`安全`,component:(0,$.jsx)(XG,{})},{name:`ShoppingOutlined`,label:`商店`,component:(0,$.jsx)(mK,{})},{name:`SmileOutlined`,label:`笑脸`,component:(0,$.jsx)(CK,{})},{name:`SoundOutlined`,label:`声音`,component:(0,$.jsx)(kK,{})},{name:`TagOutlined`,label:`标签`,component:(0,$.jsx)(RK,{})},{name:`TeamOutlined`,label:`团队`,component:(0,$.jsx)(VK,{})},{name:`ToolOutlined`,label:`工具`,component:(0,$.jsx)(qK,{})},{name:`TruckOutlined`,label:`物流`,component:(0,$.jsx)($K,{})},{name:`VideoCameraOutlined`,label:`视频`,component:(0,$.jsx)(cq,{})},{name:`WalletOutlined`,label:`钱包`,component:(0,$.jsx)(dq,{})},{name:`BankOutlined`,label:`银行`,component:(0,$.jsx)(qH,{})},{name:`BuildOutlined`,label:`建筑`,component:(0,$.jsx)(aU,{})},{name:`ExperimentOutlined`,label:`实验`,component:(0,$.jsx)(nW,{})},{name:`HighlightOutlined`,label:`高亮`,component:(0,$.jsx)(kW,{})},{name:`IdcardOutlined`,label:`名片`,component:(0,$.jsx)(RW,{})},{name:`MedicineBoxOutlined`,label:`医药`,component:(0,$.jsx)(nG,{})},{name:`ReadOutlined`,label:`阅读`,component:(0,$.jsx)(FG,{})},{name:`RestOutlined`,label:`休息`,component:(0,$.jsx)(RG,{})},{name:`SketchOutlined`,label:`钻石`,component:(0,$.jsx)(_K,{})},{name:`SolutionOutlined`,label:`方案`,component:(0,$.jsx)(EK,{})}],YJ=Object.fromEntries(JJ.map(e=>[e.name,e.component]));function XJ(e){return YJ[e]||(0,$.jsx)(FH,{})}function ZJ(e){try{let t=typeof e==`string`?JSON.parse(e):e;if(!Array.isArray(t))return{skillItems:[],optionGroups:[]};let n=[],r=[];for(let e of t)e&&e.type===`option_group`&&e.name&&Array.isArray(e.options)?r.push({name:e.name,options:e.options}):e&&e.key&&e.label&&n.push({key:e.key,label:e.label});return{skillItems:n,optionGroups:r}}catch{return{skillItems:[],optionGroups:[]}}}var QJ=({value:e,onChange:t})=>{let n=JJ.find(t=>t.name===e);return(0,$.jsx)(Ej,{trigger:[`click`],dropdownRender:()=>(0,$.jsx)(`div`,{style:{background:`#fff`,borderRadius:12,padding:12,width:360,boxShadow:`0 12px 40px rgba(0,0,0,0.15)`,border:`1px solid #f0f0f5`},children:(0,$.jsx)(`div`,{style:{display:`grid`,gridTemplateColumns:`repeat(8, 1fr)`,gap:4},children:JJ.map(n=>(0,$.jsx)(`div`,{onMouseDown:e=>{e.preventDefault(),e.stopPropagation(),t?.(n.name)},title:n.label,style:{width:40,height:40,borderRadius:8,display:`flex`,alignItems:`center`,justifyContent:`center`,cursor:`pointer`,fontSize:18,transition:`all 0.15s`,background:e===n.name?`#6366f1`:`transparent`,color:e===n.name?`#fff`:`#64748b`,border:e===n.name?`none`:`1px solid transparent`},onMouseEnter:t=>{e!==n.name&&(t.currentTarget.style.background=`#f1f5f9`)},onMouseLeave:t=>{e!==n.name&&(t.currentTarget.style.background=`transparent`)},children:n.component},n.name))})}),children:(0,$.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:8,padding:`8px 12px`,borderRadius:8,border:`1px solid #d9d9d9`,cursor:`pointer`,height:40},children:[e?(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`span`,{style:{fontSize:18,color:`#6366f1`},children:XJ(e)}),(0,$.jsx)(`span`,{style:{color:`#64748b`,fontSize:13},children:n?.label||e})]}):(0,$.jsx)(`span`,{style:{color:`#bfbfbf`,fontSize:13},children:`选择图标`}),(0,$.jsx)(UC,{style:{fontSize:10,color:`#bfbfbf`,marginLeft:`auto`}})]})})},$J=()=>{let[e,t]=(0,x.useState)([]),[n,r]=(0,x.useState)(!1),[i,a]=(0,x.useState)(!1),[o,s]=(0,x.useState)({open:!1,item:null}),[c]=Z.useForm(),l=async()=>{r(!0);try{t((await Qq()).map(e=>{let{skillItems:t,optionGroups:n}=ZJ(e.skills);return{id:e.id,key:e.key,label:e.label,icon:e.icon||``,description:e.description||``,skills:JSON.stringify(t),optionGroups:n,isActive:e.is_active??e.isActive??!0,sortOrder:e.sort_order??e.sortOrder??0}}))}catch{bP.error(`加载行业配置失败`)}finally{r(!1)}};(0,x.useEffect)(()=>{l()},[]);let u=async()=>{try{let e=await c.validateFields();a(!0);let t=[];e.skills_wentutujie?.trim()&&t.push({type:`skill`,key:`文图理解生成视频提示词`,label:e.skills_wentutujie.trim()}),e.skills_wentutujie_image?.trim()&&t.push({type:`skill`,key:`文图理解生成图片提示词`,label:e.skills_wentutujie_image.trim()});let n=(e.optionGroups||[]).filter(e=>e?.name?.trim()).map(e=>({name:e.name.trim(),options:(e.options||[]).filter(e=>e?.trim())})).filter(e=>e.options.length>0);for(let e of n)t.push({type:`option_group`,name:e.name,options:e.options});let r={key:e.key,label:e.label,icon:e.icon||``,description:e.description||``,skills:t,is_active:e.is_active??!0,sort_order:e.sort_order??0};o.item?.id?(await $q({id:o.item.id,...r}),bP.success(`已更新`)):(await $q(r),bP.success(`已添加`)),s({open:!1,item:null}),c.resetFields(),l()}catch(e){if(e?.errorFields)return;bP.error(e?.message||`保存失败`)}finally{a(!1)}},d=async e=>{try{await eJ(e),bP.success(`已删除`),l()}catch(e){bP.error(e?.message||`删除失败`)}},f=e=>{s({open:!0,item:e||null});let t=``,n=``,r=[];if(e){try{let r=JSON.parse(e.skills);if(Array.isArray(r))for(let e of r)(e.key===`文图理解生成视频提示词`||e.key===`文图理解`)&&(t=e.label||``),e.key===`文图理解生成图片提示词`&&(n=e.label||``)}catch{}r=e.optionGroups.length>0?e.optionGroups:[{name:``,options:[]}]}e?c.setFieldsValue({key:e.key,label:e.label,icon:e.icon||``,description:e.description,skills_wentutujie:t,skills_wentutujie_image:n,optionGroups:r,is_active:e.isActive,sort_order:e.sortOrder}):(c.resetFields(),c.setFieldsValue({is_active:!0,sort_order:0,optionGroups:[{name:``,options:[]}]}))};return(0,$.jsxs)(`div`,{children:[(0,$.jsxs)(Mk,{style:{borderRadius:12,border:`1px solid #f0f0f5`},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,marginBottom:16},children:[(0,$.jsxs)(wj,{children:[(0,$.jsx)(FH,{style:{fontSize:18,color:`#6366f1`}}),(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:16},children:`行业与技能配置`}),(0,$.jsxs)(CB,{color:`purple`,children:[e.length,` 个行业`]})]}),(0,$.jsx)(mD,{type:`primary`,icon:(0,$.jsx)(_O,{}),onClick:()=>f(),style:{borderRadius:8},children:`添加行业`})]}),(0,$.jsx)(uB,{columns:[{title:`行业`,key:`industry`,width:180,render:(e,t)=>(0,$.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:10},children:[(0,$.jsx)(`div`,{style:{width:36,height:36,borderRadius:10,background:`#f1f5f9`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:18,color:`#6366f1`,flexShrink:0},children:XJ(t.icon)}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{strong:!0,children:t.label}),(0,$.jsx)(`div`,{style:{color:`#94a3b8`,fontSize:12},children:t.key})]})]})},{title:`描述`,dataIndex:`description`,ellipsis:!0},{title:`选项配置`,key:`optionGroups`,width:260,render:(e,t)=>!t.optionGroups||t.optionGroups.length===0?(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#cbd5e1`},children:`未配置`}):(0,$.jsx)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:4},children:t.optionGroups.map((e,t)=>(0,$.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:6},children:[(0,$.jsx)(CB,{color:`blue`,style:{margin:0,fontSize:11},children:e.name}),(0,$.jsxs)(Q.Text,{style:{fontSize:11,color:`#64748b`},children:[e.options.slice(0,3).join(`、`),e.options.length>3?`...${e.options.length}项`:``]})]},t))})},{title:`文图理解生成视频提示词`,dataIndex:`skills`,ellipsis:!0,render:e=>{try{let t=JSON.parse(e);if(Array.isArray(t)){let e=t.find(e=>e.key===`文图理解生成视频提示词`||e.key===`文图理解`);if(e)return(0,$.jsx)(Q.Text,{ellipsis:!0,style:{fontSize:12},children:e.label})}}catch{}return(0,$.jsx)(`span`,{style:{color:`#bfbfbf`},children:`-`})}},{title:`文图理解生成图片提示词`,dataIndex:`skills`,ellipsis:!0,render:e=>{try{let t=JSON.parse(e);if(Array.isArray(t)){let e=t.find(e=>e.key===`文图理解生成图片提示词`);if(e)return(0,$.jsx)(Q.Text,{ellipsis:!0,style:{fontSize:12},children:e.label})}}catch{}return(0,$.jsx)(`span`,{style:{color:`#bfbfbf`},children:`-`})}},{title:`状态`,dataIndex:`isActive`,width:80,render:e=>(0,$.jsx)(CB,{color:e?`green`:`default`,children:e?`启用`:`停用`})},{title:`操作`,key:`action`,width:150,fixed:`right`,render:(e,t)=>(0,$.jsxs)(wj,{size:4,children:[(0,$.jsx)(mD,{type:`link`,size:`small`,icon:(0,$.jsx)(kB,{}),onClick:()=>f(t),children:`编辑`}),(0,$.jsx)(DP,{title:`确定删除该行业?`,onConfirm:()=>d(t.id),children:(0,$.jsx)(mD,{type:`link`,size:`small`,danger:!0,icon:(0,$.jsx)(EB,{}),children:`删除`})})]})}],dataSource:e,rowKey:`id`,loading:n,pagination:!1,scroll:{x:900}})]}),(0,$.jsx)(CP,{title:(0,$.jsxs)(wj,{children:[(0,$.jsx)(FH,{}),o.item?`编辑行业`:`添加行业`]}),open:o.open,onOk:u,onCancel:()=>{s({open:!1,item:null}),c.resetFields()},okText:`保存`,cancelText:`取消`,width:640,confirmLoading:i,children:(0,$.jsxs)(Z,{form:c,layout:`vertical`,style:{marginTop:16},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`key`,label:`行业标识`,style:{flex:1},rules:[{required:!0,message:`请输入标识`}],children:(0,$.jsx)(QM,{placeholder:`例如:ecommerce`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`label`,label:`行业名称`,style:{flex:1},rules:[{required:!0,message:`请输入名称`}],children:(0,$.jsx)(QM,{placeholder:`例如:电商`,size:`large`})})]}),(0,$.jsx)(Z.Item,{name:`icon`,label:`行业图标`,children:(0,$.jsx)(QJ,{})}),(0,$.jsx)(Z.Item,{name:`description`,label:`行业描述`,children:(0,$.jsx)(QM.TextArea,{rows:2,placeholder:`行业描述`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`skills_wentutujie`,label:`文图理解生成视频提示词`,extra:`用于LLM优化视频提示词的系统指令,根据行业特点引导AI理解文案与画面的关系`,children:(0,$.jsx)(QM.TextArea,{rows:3,placeholder:`请输入文图理解生成视频提示词,例如: 你是一位专业的电商视频文案专家,擅长将产品卖点转化为视觉语言`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`skills_wentutujie_image`,label:`文图理解生成图片提示词`,extra:`用于LLM优化图片提示词的系统指令,根据行业特点引导AI理解文案与画面的关系`,children:(0,$.jsx)(QM.TextArea,{rows:3,placeholder:`请输入文图理解生成图片提示词,例如: -你是一位专业的电商图片文案专家,擅长将产品卖点转化为图片生成提示词`,size:`large`})}),(0,$.jsxs)(`div`,{style:{marginBottom:8},children:[(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:13},children:`行业选项配置`}),(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#94a3b8`,marginLeft:8},children:`添加选项组,每组包含名称和多个选项,前台将显示为下拉选择`})]}),(0,$.jsx)(Z.List,{name:`optionGroups`,children:(e,{add:t,remove:n})=>(0,$.jsxs)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:8,marginBottom:16},children:[e.map(({key:e,name:t,...r})=>(0,$.jsxs)(`div`,{style:{display:`flex`,gap:8,alignItems:`flex-start`,padding:`10px 12px`,borderRadius:10,background:`#f8f9fc`,border:`1px solid #f0f0f5`},children:[(0,$.jsxs)(`div`,{style:{flex:1,display:`flex`,flexDirection:`column`,gap:8},children:[(0,$.jsx)(Z.Item,{...r,name:[t,`name`],label:`选项名称`,style:{marginBottom:0},rules:[{required:!0,message:`请输入选项名称`}],children:(0,$.jsx)(QM,{placeholder:`例如:视频风格`,size:`middle`,style:{borderRadius:8}})}),(0,$.jsx)(Z.Item,{...r,name:[t,`options`],label:`选项内容`,style:{marginBottom:0},children:(0,$.jsx)(ZC,{mode:`tags`,size:`middle`,placeholder:`输入选项后回车添加`,style:{borderRadius:8},tokenSeparators:[`,`,`,`,`、`]})})]}),(0,$.jsx)(cG,{onClick:()=>n(t),style:{color:`#ef4444`,fontSize:16,marginTop:34,cursor:`pointer`,flexShrink:0}})]},e)),(0,$.jsx)(mD,{type:`dashed`,onClick:()=>t(),block:!0,icon:(0,$.jsx)(_O,{}),style:{borderRadius:8,height:36},children:`添加选项组`})]})}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`is_active`,label:`启用状态`,valuePropName:`checked`,initialValue:!0,style:{flex:1},children:(0,$.jsx)(yF,{})}),(0,$.jsx)(Z.Item,{name:`sort_order`,label:`排序`,initialValue:0,style:{flex:1},children:(0,$.jsx)(QM,{type:`number`,size:`large`})})]})]})})]})};function eY(e){if(Array.isArray(e))return e;if(typeof e==`string`)try{return JSON.parse(e)}catch{return[]}return[]}var tY=()=>{let[e,t]=(0,x.useState)([]),[n,r]=(0,x.useState)(!1),[i,a]=(0,x.useState)({open:!1,engine:null}),[o]=Z.useForm(),s=async()=>{r(!0);try{t((await tJ()).map(e=>({...e,supportedRatios:eY(e.supportedRatios),supportedResolutions:eY(e.supportedResolutions),supportedDurations:eY(e.supportedDurations)})))}catch{bP.error(`加载视频引擎失败`)}finally{r(!1)}};(0,x.useEffect)(()=>{s()},[]);let c=async()=>{try{let e=await o.validateFields(),t={name:e.name,provider:e.provider,api_base:e.apiBase,api_key:e.apiKey,model_name:e.modelName,supported_ratios:JSON.stringify(e.supportedRatios||[]),supported_resolutions:JSON.stringify(e.supportedResolutions||[]),supported_durations:JSON.stringify(e.supportedDurations||[]),is_active:e.isActive??!0,priority:e.priority??0};i.engine?(await nJ({id:i.engine.id,...t}),bP.success(`已更新`)):(await nJ(t),bP.success(`已添加`)),a({open:!1,engine:null}),o.resetFields(),s()}catch(e){if(e?.errorFields)return;bP.error(e?.message||`保存失败`)}},l=async e=>{try{await rJ(e),bP.success(`已删除`),s()}catch{bP.error(`删除失败`)}},u=e=>{a({open:!0,engine:e||null}),e?o.setFieldsValue(e):(o.resetFields(),o.setFieldsValue({isActive:!0,priority:0,supportedRatios:[`16:9`,`4:3`,`1:1`,`3:4`,`9:16`,`21:9`],supportedResolutions:[`480p`,`720p`,`1080p`],supportedDurations:[4,5,6,7,8,9,10,11,12,13,14,15]}))};return(0,$.jsxs)(`div`,{children:[(0,$.jsxs)(Mk,{bordered:!1,style:{borderRadius:12,border:`1px solid #f0f0f5`},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,marginBottom:16},children:[(0,$.jsxs)(wj,{children:[(0,$.jsx)(kG,{style:{fontSize:18,color:`#6366f1`}}),(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:16},children:`视频引擎配置`}),(0,$.jsxs)(CB,{color:`purple`,children:[e.length,` 个引擎`]})]}),(0,$.jsx)(mD,{type:`primary`,icon:(0,$.jsx)(_O,{}),onClick:()=>u(),style:{borderRadius:8},children:`添加引擎`})]}),(0,$.jsx)(uB,{columns:[{title:`引擎名称`,key:`name`,width:180,render:(e,t)=>(0,$.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:10},children:[(0,$.jsx)(`div`,{style:{width:36,height:36,borderRadius:8,background:t.isActive?`linear-gradient(135deg, #6366f1, #8b5cf6)`:`linear-gradient(135deg, #94a3b8, #cbd5e1)`,display:`flex`,alignItems:`center`,justifyContent:`center`,color:`#fff`,fontSize:16},children:(0,$.jsx)(kG,{})}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{strong:!0,children:t.name}),(0,$.jsx)(`div`,{style:{color:`#94a3b8`,fontSize:12},children:t.provider})]})]})},{title:`支持比例`,dataIndex:`supportedRatios`,width:200,render:e=>(0,$.jsx)(wj,{size:2,wrap:!0,children:e.map(e=>(0,$.jsx)(CB,{children:e},e))})},{title:`支持分辨率`,dataIndex:`supportedResolutions`,width:150,render:e=>(0,$.jsx)(wj,{size:2,wrap:!0,children:e.map(e=>(0,$.jsx)(CB,{color:`blue`,children:e},e))})},{title:`支持时长`,dataIndex:`supportedDurations`,width:120,render:e=>(0,$.jsx)(CB,{color:`orange`,children:e?.length?`${Math.min(...e)}-${Math.max(...e)}s`:`-`})},{title:`状态`,dataIndex:`isActive`,width:80,render:e=>(0,$.jsx)(CB,{color:e?`green`:`default`,children:e?`启用`:`停用`})},{title:`操作`,key:`action`,width:150,fixed:`right`,render:(e,t)=>(0,$.jsxs)(wj,{size:4,children:[(0,$.jsx)(mD,{type:`link`,size:`small`,icon:(0,$.jsx)(kB,{}),onClick:()=>u(t),children:`编辑`}),(0,$.jsx)(DP,{title:`确定删除?`,onConfirm:()=>l(t.id),children:(0,$.jsx)(mD,{type:`link`,size:`small`,danger:!0,icon:(0,$.jsx)(EB,{}),children:`删除`})})]})}],dataSource:e,rowKey:`id`,loading:n,pagination:!1,scroll:{x:900}})]}),(0,$.jsx)(CP,{title:(0,$.jsxs)(wj,{children:[(0,$.jsx)(kG,{}),i.engine?`编辑引擎`:`添加引擎`]}),open:i.open,onOk:c,onCancel:()=>{a({open:!1,engine:null}),o.resetFields()},okText:`保存`,cancelText:`取消`,width:620,children:(0,$.jsxs)(Z,{form:o,layout:`vertical`,style:{marginTop:16},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`name`,label:`引擎名称`,style:{flex:1},rules:[{required:!0}],children:(0,$.jsx)(QM,{placeholder:`Seedance 2.0`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`provider`,label:`提供商`,style:{flex:1},rules:[{required:!0}],children:(0,$.jsx)(ZC,{size:`large`,options:[{value:`ark`,label:`火山引擎 (Ark)`}]})})]}),(0,$.jsx)(Z.Item,{name:`apiBase`,label:`API基础地址`,rules:[{required:!0}],children:(0,$.jsx)(QM,{placeholder:`https://ark.cn-beijing.volces.com/api/v3`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`apiKey`,label:`API Key`,children:(0,$.jsx)(QM.Password,{placeholder:`sk-****`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`modelName`,label:`模型名称`,children:(0,$.jsx)(QM,{placeholder:`doubao-seedance-2-0-260128`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`supportedRatios`,label:`支持比例`,children:(0,$.jsx)(ZC,{mode:`multiple`,size:`large`,options:[{value:`16:9`,label:`16:9 (横屏)`},{value:`4:3`,label:`4:3 (标准)`},{value:`1:1`,label:`1:1 (方形)`},{value:`3:4`,label:`3:4 (竖版)`},{value:`9:16`,label:`9:16 (竖屏)`},{value:`21:9`,label:`21:9 (超宽)`}]})}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`supportedResolutions`,label:`支持分辨率`,style:{flex:1},children:(0,$.jsx)(ZC,{mode:`multiple`,size:`large`,options:[{value:`480p`},{value:`720p`},{value:`1080p`}]})}),(0,$.jsx)(Z.Item,{name:`supportedDurations`,label:`支持时长(秒)`,style:{flex:1},children:(0,$.jsx)(ZC,{mode:`multiple`,size:`large`,options:Array.from({length:12},(e,t)=>({value:t+4,label:`${t+4}秒`}))})})]}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`priority`,label:`优先级`,children:(0,$.jsx)(ZC,{size:`large`,options:[{value:0,label:`0 (默认)`},{value:1,label:`1`},{value:2,label:`2`},{value:3,label:`3`},{value:5,label:`5`},{value:10,label:`10 (最高)`}]})}),(0,$.jsx)(Z.Item,{name:`isActive`,label:`启用状态`,valuePropName:`checked`,style:{paddingTop:30},children:(0,$.jsx)(yF,{})})]})]})})]})};function nY(e){if(Array.isArray(e))return e;if(typeof e==`string`)try{return JSON.parse(e)}catch{return[]}return[]}function rY(e){if(e&&typeof e==`object`&&!Array.isArray(e))return e;if(typeof e==`string`)try{let t=JSON.parse(e);return t&&typeof t==`object`&&!Array.isArray(t)?t:{}}catch{return{}}return{}}var iY={"2K":{"1:1":`2048×2048`,"4:3":`2304×1728`,"3:4":`1728×2304`,"16:9":`2560×1440`,"9:16":`1600×2848`,"3:2":`2496×1664`,"2:3":`1664×2496`,"21:9":`3024×1296`},"4K":{"1:1":`4096×4096`,"4:3":`4608×3456`,"3:4":`3520×4704`,"16:9":`5404×3040`,"9:16":`3040×5504`,"3:2":`4992×3328`,"2:3":`3328×4992`,"21:9":`6197×2656`}},aY=[`1:1`,`4:3`,`3:4`,`16:9`,`9:16`,`3:2`,`2:3`,`21:9`],oY=()=>{let[e,t]=(0,x.useState)([]),[n,r]=(0,x.useState)(!1),[i,a]=(0,x.useState)({open:!1,engine:null}),[o]=Z.useForm(),s=async()=>{r(!0);try{t((await iJ()).map(e=>({...e,supportedModels:nY(e.supportedModels),supportedSizes:rY(e.supportedSizes)})))}catch{bP.error(`加载图片引擎失败`)}finally{r(!1)}};(0,x.useEffect)(()=>{s()},[]);let c=async()=>{try{let e=await o.validateFields(),t={};for(let n of[`2K`,`4K`]){let r=e[`size_${n}`]||[];if(r.length>0){t[n]={};for(let e of r)t[n][e]=iY[n]?.[e]||e}}let n={name:e.name,provider:e.provider,api_base:e.apiBase,api_key:e.apiKey,model_name:e.modelName,supported_models:JSON.stringify(e.supportedModels||[]),supported_sizes:JSON.stringify(t),default_size:e.defaultSize||`2K`,generate_url:e.generateUrl||``,is_active:e.isActive??!0,priority:e.priority??0};i.engine?(await aJ({id:i.engine.id,...n}),bP.success(`已更新`)):(await aJ(n),bP.success(`已添加`)),a({open:!1,engine:null}),o.resetFields(),s()}catch(e){if(e?.errorFields)return;bP.error(e?.message||`保存失败`)}},l=async e=>{try{await oJ(e),bP.success(`已删除`),s()}catch{bP.error(`删除失败`)}},u=e=>{if(a({open:!0,engine:e||null}),e){let t={};for(let n of[`2K`,`4K`])t[`size_${n}`]=Object.keys(e.supportedSizes?.[n]||{});o.setFieldsValue({...e,...t})}else o.resetFields(),o.setFieldsValue({isActive:!0,priority:0,supportedModels:[`doubao-seedream-5-0-260128`],defaultSize:`2K`,size_2K:aY,size_4K:aY})};return(0,$.jsxs)(`div`,{children:[(0,$.jsxs)(Mk,{style:{borderRadius:12,border:`1px solid #f0f0f5`},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,marginBottom:16},children:[(0,$.jsxs)(wj,{children:[(0,$.jsx)(CG,{style:{fontSize:18,color:`#10b981`}}),(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:16},children:`图片引擎配置`}),(0,$.jsxs)(CB,{color:`green`,children:[e.length,` 个引擎`]})]}),(0,$.jsx)(mD,{type:`primary`,icon:(0,$.jsx)(_O,{}),onClick:()=>u(),style:{borderRadius:8},children:`添加引擎`})]}),(0,$.jsx)(uB,{columns:[{title:`引擎名称`,key:`name`,width:180,render:(e,t)=>(0,$.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:10},children:[(0,$.jsx)(`div`,{style:{width:36,height:36,borderRadius:8,background:t.isActive?`linear-gradient(135deg, #10b981, #059669)`:`linear-gradient(135deg, #94a3b8, #cbd5e1)`,display:`flex`,alignItems:`center`,justifyContent:`center`,color:`#fff`,fontSize:16},children:(0,$.jsx)(CG,{})}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{strong:!0,children:t.name}),(0,$.jsx)(`div`,{style:{color:`#94a3b8`,fontSize:12},children:t.modelName})]})]})},{title:`2K 支持比例`,key:`sizes_2k`,width:260,render:(e,t)=>{let n=Object.keys(t.supportedSizes?.[`2K`]||{});return n.length===0?(0,$.jsx)(`span`,{style:{color:`#bfbfbf`},children:`-`}):(0,$.jsx)(wj,{size:2,wrap:!0,children:n.map(e=>(0,$.jsxs)(CB,{color:`blue`,children:[e,` `,t.supportedSizes[`2K`][e]]},e))})}},{title:`4K 支持比例`,key:`sizes_4k`,width:260,render:(e,t)=>{let n=Object.keys(t.supportedSizes?.[`4K`]||{});return n.length===0?(0,$.jsx)(`span`,{style:{color:`#bfbfbf`},children:`-`}):(0,$.jsx)(wj,{size:2,wrap:!0,children:n.map(e=>(0,$.jsxs)(CB,{color:`purple`,children:[e,` `,t.supportedSizes[`4K`][e]]},e))})}},{title:`状态`,dataIndex:`isActive`,width:80,render:e=>(0,$.jsx)(CB,{color:e?`green`:`default`,children:e?`启用`:`停用`})},{title:`操作`,key:`action`,width:150,fixed:`right`,render:(e,t)=>(0,$.jsxs)(wj,{size:4,children:[(0,$.jsx)(mD,{type:`link`,size:`small`,icon:(0,$.jsx)(kB,{}),onClick:()=>u(t),children:`编辑`}),(0,$.jsx)(DP,{title:`确定删除?`,onConfirm:()=>l(t.id),children:(0,$.jsx)(mD,{type:`link`,size:`small`,danger:!0,icon:(0,$.jsx)(EB,{}),children:`删除`})})]})}],dataSource:e,rowKey:`id`,loading:n,pagination:!1,scroll:{x:1e3}})]}),(0,$.jsx)(CP,{title:(0,$.jsxs)(wj,{children:[(0,$.jsx)(CG,{}),i.engine?`编辑引擎`:`添加引擎`]}),open:i.open,onOk:c,onCancel:()=>{a({open:!1,engine:null}),o.resetFields()},okText:`保存`,cancelText:`取消`,width:680,children:(0,$.jsxs)(Z,{form:o,layout:`vertical`,style:{marginTop:16},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`name`,label:`引擎名称`,style:{flex:1},rules:[{required:!0}],children:(0,$.jsx)(QM,{placeholder:`豆包文生图`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`provider`,label:`提供商`,style:{flex:1},rules:[{required:!0}],children:(0,$.jsx)(ZC,{size:`large`,options:[{value:`ark`,label:`火山引擎 (Ark)`}]})})]}),(0,$.jsx)(Z.Item,{name:`apiBase`,label:`API基础地址`,rules:[{required:!0}],children:(0,$.jsx)(QM,{placeholder:`https://ark.cn-beijing.volces.com/api/v3`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`apiKey`,label:`API Key`,children:(0,$.jsx)(QM.Password,{placeholder:`sk-****`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`modelName`,label:`默认模型`,children:(0,$.jsx)(QM,{placeholder:`doubao-seedream-5-0-260128`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`supportedModels`,label:`支持模型列表`,children:(0,$.jsx)(ZC,{mode:`tags`,size:`large`,placeholder:`输入模型ID后回车添加`,tokenSeparators:[`,`,`,`],options:[{value:`doubao-seedream-5-0-260128`}]})}),(0,$.jsxs)(`div`,{style:{background:`#f8f9fc`,borderRadius:10,padding:16,marginBottom:8},children:[(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:14},children:`尺寸配置`}),(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#94a3b8`,marginLeft:8},children:`勾选每个档位支持的比例,前台选择后传对应像素值给SDK`})]}),[`2K`,`4K`].map(e=>(0,$.jsxs)(`div`,{style:{background:`#fafbfc`,borderRadius:10,padding:`12px 16px`,marginBottom:12,border:`1px solid #f0f0f5`},children:[(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:13,color:e===`2K`?`#3b82f6`:`#8b5cf6`},children:e}),(0,$.jsx)(Z.Item,{name:`size_${e}`,style:{marginTop:8,marginBottom:0},children:(0,$.jsx)(iA.Group,{style:{width:`100%`},children:(0,$.jsx)(`div`,{style:{display:`grid`,gridTemplateColumns:`repeat(4, 1fr)`,gap:`6px 0`},children:aY.map(t=>(0,$.jsxs)(iA,{value:t,style:{fontSize:12},children:[t,` `,(0,$.jsx)(`span`,{style:{color:`#94a3b8`,fontSize:11},children:iY[e]?.[t]})]},t))})})})]},e)),(0,$.jsx)(Z.Item,{name:`defaultSize`,label:`默认尺寸档位`,children:(0,$.jsx)(ZC,{size:`large`,options:[{value:`2K`,label:`2K`},{value:`4K`,label:`4K`}]})}),(0,$.jsx)(Z.Item,{name:`generateUrl`,label:`生成接口地址`,children:(0,$.jsx)(QM,{placeholder:`https://ark.cn-beijing.volces.com/api/v3/images/generations`,size:`large`})}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`priority`,label:`优先级`,children:(0,$.jsx)(ZC,{size:`large`,options:[{value:0,label:`0 (默认)`},{value:1,label:`1`},{value:2,label:`2`},{value:3,label:`3`},{value:5,label:`5`},{value:10,label:`10 (最高)`}]})}),(0,$.jsx)(Z.Item,{name:`isActive`,label:`启用状态`,valuePropName:`checked`,style:{paddingTop:30},children:(0,$.jsx)(yF,{})})]})]})})]})},sY=[`2K`,`4K`],cY=[`480p`,`720p`,`1080p`],lY=()=>{let[e,t]=(0,x.useState)([]),[n,r]=(0,x.useState)([]),[i,a]=(0,x.useState)(!1),[o,s]=(0,x.useState)({open:!1,ratio:null}),[c]=Z.useForm(),[l,u]=(0,x.useState)(10),[d,f]=(0,x.useState)(null),[p,m]=(0,x.useState)(!1),h=Z.useWatch(`genType`,c)||`video`,g=Z.useWatch(`modelConfigId`,c),_=async()=>{a(!0);try{let[e,n,i]=await Promise.all([sJ(),Jq(),AJ()]),a=(i?.engine?.image||[]).map(e=>({...e,genType:`image`})),o=(i?.engine?.video||[]).map(e=>({...e,genType:`video`}));t(e),r([...o,...a]);let s=n.find(e=>e.key===`text_credits_per_1000_tokens`);s&&(u(Number(s.value)||10),f({id:s.id}))}catch{bP.error(`加载积分比例失败`)}finally{a(!1)}};(0,x.useEffect)(()=>{_()},[]);let v=(0,x.useMemo)(()=>n.filter(e=>e.genType===h).map(e=>({value:e.id,label:`${e.name}${e.modelName?`(${e.modelName})`:``}`})),[n,h]),y=(0,x.useMemo)(()=>n.find(e=>e.id===g&&e.genType===h),[n,h,g]),b=(0,x.useMemo)(()=>{if(h===`image`){let e=y?.supportedSizes?Object.keys(y.supportedSizes):[];return(e.length?e:sY).map(e=>({value:e,label:e}))}let e=y?.supportedResolutions||[];return(e.length?e:cY).map(e=>({value:e,label:e}))},[h,y]),S=(e,t)=>n.find(n=>n.id===e&&(!t||n.genType===t))?.name||e,C=async()=>{try{let e=await c.validateFields(),t={model_config_id:e.modelConfigId,gen_type:e.genType,resolution:e.resolution,ratio:e.ratio,base_credits:e.baseCredits,per_second_credits:e.genType===`image`?0:e.perSecondCredits||0};o.ratio?(await cJ({id:o.ratio.id,...t}),bP.success(`已更新`)):(await cJ(t),bP.success(`已添加`)),s({open:!1,ratio:null}),c.resetFields(),_()}catch(e){if(e?.errorFields)return;bP.error(e?.message||`保存失败`)}},w=async e=>{try{await lJ(e),bP.success(`已删除`),_()}catch{bP.error(`删除失败`)}},T=async()=>{if(d){m(!0);try{await Yq(d.id,String(l)),bP.success(`文字积分费率已更新`)}catch(e){bP.error(e?.message||`保存失败`)}finally{m(!1)}}},E=e=>{s({open:!0,ratio:e||null}),e?c.setFieldsValue({modelConfigId:e.modelConfigId,genType:e.genType===`image`?`image`:`video`,resolution:e.resolution,ratio:e.ratio,baseCredits:e.baseCredits,perSecondCredits:e.perSecondCredits}):(c.resetFields(),c.setFieldsValue({genType:`video`,ratio:1,baseCredits:60,perSecondCredits:2}))};return(0,$.jsxs)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:16},children:[(0,$.jsxs)(Mk,{bordered:!1,style:{borderRadius:12,border:`1px solid #f0f0f5`},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,alignItems:`center`,marginBottom:16},children:[(0,$.jsxs)(wj,{children:[(0,$.jsx)(_W,{style:{fontSize:18,color:`#f59e0b`}}),(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:16},children:`文字积分费率`})]}),(0,$.jsx)(mD,{type:`primary`,loading:p,onClick:T,style:{borderRadius:8},children:`保存`})]}),(0,$.jsx)(Q.Text,{type:`secondary`,style:{display:`block`,marginBottom:16,fontSize:13},children:`文字积分计算公式:ceil(总token数 x 费率 / 1000),最低1积分`}),(0,$.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:16},children:[(0,$.jsx)(Q.Text,{children:`每1000 token消耗积分:`}),(0,$.jsx)($A,{min:0,max:1e3,step:.01,value:l,onChange:e=>u(e||0),size:`large`,style:{width:160},addonAfter:`积分`}),(0,$.jsxs)(Q.Text,{type:`secondary`,style:{fontSize:12},children:[`示例:1000 token = `,l,` 积分,500 token = `,(500*l/1e3).toFixed(4),` 积分`]})]})]}),(0,$.jsxs)(Mk,{bordered:!1,style:{borderRadius:12,border:`1px solid #f0f0f5`},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,marginBottom:16},children:[(0,$.jsxs)(wj,{children:[(0,$.jsx)(dU,{style:{fontSize:18,color:`#6366f1`}}),(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:16},children:`积分比例配置`}),(0,$.jsxs)(CB,{color:`purple`,children:[e.length,` 条规则`]})]}),(0,$.jsx)(mD,{type:`primary`,icon:(0,$.jsx)(_O,{}),onClick:()=>E(),style:{borderRadius:8},children:`添加比例`})]}),(0,$.jsx)(Q.Text,{type:`secondary`,style:{display:`block`,marginBottom:16,fontSize:13},children:`视频积分公式:(基础积分 + 每秒积分 x 视频时长) x 模型倍率;图片积分公式:基础积分 x 模型倍率`}),(0,$.jsx)(uB,{columns:[{title:`类型`,dataIndex:`genType`,width:80,render:e=>(0,$.jsx)(CB,{color:e===`image`?`cyan`:`orange`,children:e===`image`?`图片`:`视频`})},{title:`引擎`,dataIndex:`modelConfigId`,width:180,render:(e,t)=>(0,$.jsx)(CB,{color:t.genType===`image`?`cyan`:`purple`,children:S(e,t.genType)})},{title:`分辨率/尺寸`,dataIndex:`resolution`,width:110,render:e=>(0,$.jsx)(CB,{color:{"480p":`blue`,"720p":`blue`,"1080p":`blue`,"4K":`green`,"2K":`green`}[e]||`default`,children:e})},{title:`倍率`,dataIndex:`ratio`,width:100,sorter:(e,t)=>e.ratio-t.ratio,render:e=>(0,$.jsxs)(Q.Text,{strong:!0,style:{color:e>=2?`#ef4444`:e>=1.5?`#f59e0b`:`#10b981`},children:[`x`,e]})},{title:`基础积分`,dataIndex:`baseCredits`,width:100,render:e=>(0,$.jsxs)(Q.Text,{children:[e,` 积分`]})},{title:`每秒积分`,dataIndex:`perSecondCredits`,width:100,render:(e,t)=>(0,$.jsx)(Q.Text,{children:t.genType===`image`?`-`:`${e} 积分/秒`})},{title:`示例计算`,key:`example`,width:120,render:(e,t)=>{let n;return n=t.genType===`image`?Math.round(t.baseCredits*t.ratio):Math.round((t.baseCredits+t.perSecondCredits*15)*t.ratio),(0,$.jsxs)(Q.Text,{strong:!0,style:{color:`#6366f1`},children:[n,` 积分`]})}},{title:`操作`,key:`action`,width:150,fixed:`right`,render:(e,t)=>(0,$.jsxs)(wj,{size:4,children:[(0,$.jsx)(mD,{type:`link`,size:`small`,icon:(0,$.jsx)(kB,{}),onClick:()=>E(t),children:`编辑`}),(0,$.jsx)(DP,{title:`确定删除?`,onConfirm:()=>w(t.id),children:(0,$.jsx)(mD,{type:`link`,size:`small`,danger:!0,icon:(0,$.jsx)(EB,{}),children:`删除`})})]})}],dataSource:e,rowKey:`id`,loading:i,pagination:!1,scroll:{x:860}})]}),(0,$.jsx)(CP,{title:(0,$.jsxs)(wj,{children:[(0,$.jsx)(dU,{}),o.ratio?`编辑比例`:`添加比例`]}),open:o.open,onOk:C,onCancel:()=>{s({open:!1,ratio:null}),c.resetFields()},okText:`保存`,cancelText:`取消`,width:520,children:(0,$.jsxs)(Z,{form:c,layout:`vertical`,style:{marginTop:16},children:[(0,$.jsx)(Z.Item,{name:`genType`,label:`生成类型`,rules:[{required:!0,message:`请选择生成类型`}],children:(0,$.jsx)(ZC,{size:`large`,options:[{value:`video`,label:`视频`},{value:`image`,label:`图片`}],onChange:()=>{c.setFieldsValue({modelConfigId:void 0,resolution:void 0})}})}),(0,$.jsx)(Z.Item,{name:`modelConfigId`,label:`引擎`,rules:[{required:!0,message:`请选择引擎`}],children:(0,$.jsx)(ZC,{size:`large`,placeholder:`请选择引擎`,options:v,showSearch:!0,optionFilterProp:`label`,onChange:()=>c.setFieldsValue({resolution:void 0})})}),(0,$.jsx)(Z.Item,{name:`resolution`,label:h===`image`?`图片尺寸`:`分辨率`,rules:[{required:!0,message:h===`image`?`请选择图片尺寸`:`请选择分辨率`}],children:(0,$.jsx)(ZC,{size:`large`,placeholder:h===`image`?`请选择图片尺寸`:`请选择分辨率`,options:b})}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`ratio`,label:`倍率`,style:{flex:1},rules:[{required:!0,message:`请输入倍率`}],children:(0,$.jsx)($A,{min:.1,max:10,step:.1,style:{width:`100%`},size:`large`})}),(0,$.jsx)(Z.Item,{name:`baseCredits`,label:`基础积分`,style:{flex:1},rules:[{required:!0,message:`请输入基础积分`}],children:(0,$.jsx)($A,{min:0,max:1e3,style:{width:`100%`},size:`large`})}),h!==`image`&&(0,$.jsx)(Z.Item,{name:`perSecondCredits`,label:`每秒积分`,style:{flex:1},rules:[{required:!0,message:`请输入每秒积分`}],children:(0,$.jsx)($A,{min:0,max:100,style:{width:`100%`},size:`large`})})]})]})})]})},uY={HomeOutlined:(0,$.jsx)(FW,{}),PlayCircleOutlined:(0,$.jsx)(kG,{}),WalletOutlined:(0,$.jsx)(dq,{}),RobotOutlined:(0,$.jsx)(VG,{}),SettingOutlined:(0,$.jsx)(cK,{}),BellOutlined:(0,$.jsx)($H,{}),UserOutlined:(0,$.jsx)(aq,{}),AppstoreOutlined:(0,$.jsx)(FH,{}),FileTextOutlined:(0,$.jsx)(kj,{}),StarOutlined:(0,$.jsx)(MK,{}),HeartOutlined:(0,$.jsx)(EW,{}),CameraOutlined:(0,$.jsx)(mU,{}),DashboardOutlined:(0,$.jsx)(WU,{}),CalculatorOutlined:(0,$.jsx)(dU,{}),DollarOutlined:(0,$.jsx)(XU,{}),GiftOutlined:(0,$.jsx)(bW,{}),ThunderboltOutlined:(0,$.jsx)(WK,{}),FireOutlined:(0,$.jsx)(dW,{}),CloudOutlined:(0,$.jsx)(EU,{}),SmileOutlined:(0,$.jsx)(CK,{}),TrophyOutlined:(0,$.jsx)(XK,{}),RocketOutlined:(0,$.jsx)(WG,{}),BulbOutlined:(0,$.jsx)(cU,{}),CodeOutlined:(0,$.jsx)(MU,{}),PictureOutlined:(0,$.jsx)(CG,{}),VideoCameraOutlined:(0,$.jsx)(cq,{}),AudioOutlined:(0,$.jsx)(WH,{}),MailOutlined:(0,$.jsx)($W,{}),PhoneOutlined:(0,$.jsx)(bG,{}),GlobalOutlined:(0,$.jsx)(CW,{}),ShoppingCartOutlined:(0,$.jsx)(dK,{}),TeamOutlined:(0,$.jsx)(VK,{}),BarChartOutlined:(0,$.jsx)(XH,{}),PieChartOutlined:(0,$.jsx)(EG,{}),LineChartOutlined:(0,$.jsx)(WW,{}),SecurityScanOutlined:(0,$.jsx)(nK,{}),ApiOutlined:(0,$.jsx)(MH,{}),DatabaseOutlined:(0,$.jsx)(qU,{}),CloudServerOutlined:(0,$.jsx)(kU,{}),MenuOutlined:(0,$.jsx)(aG,{})},dY=Object.keys(uY).map(e=>({value:e,label:(0,$.jsxs)(`span`,{style:{display:`flex`,alignItems:`center`,gap:8},children:[uY[e],` `,e.replace(`Outlined`,``)]})})),fY={page:`blue`,group:`purple`},pY={page:`页面`,group:`分组`},mY=()=>{let[e,t]=(0,x.useState)([]),[n,r]=(0,x.useState)(!0),[i,a]=(0,x.useState)(`frontend`),[o,s]=(0,x.useState)({open:!1,menu:null}),[c]=Z.useForm(),l=async()=>{r(!0);try{t(await gJ())}catch{}r(!1)};(0,x.useEffect)(()=>{l()},[]);let u=async()=>{try{let e=await c.validateFields(),t={label:e.label,path:e.path||``,icon:e.icon||``,sort_order:e.sortOrder??0,is_active:e.isActive??!0,parent_id:e.parentId||null,menu_type:e.menuType||`page`,menu_target:e.menuTarget||`frontend`,is_default:e.isDefault??!1};o.menu?.id?await _J({...t,id:o.menu.id}):await _J(t),bP.success(o.menu?.id?`菜单已更新`:`菜单已添加`),s({open:!1,menu:null}),c.resetFields(),l()}catch{}},d=async e=>{try{await vJ(e),bP.success(`菜单已删除`),l()}catch(e){bP.error(e?.message||`删除失败`)}},f=t=>{s({open:!0,menu:t||null}),t?c.setFieldsValue({label:t.label,path:t.path,icon:t.icon,sortOrder:t.sortOrder??0,isActive:t.isActive??!0,parentId:t.parentId??``,menuType:t.menuType??`page`,menuTarget:t.menuTarget??`frontend`,isDefault:t.isDefault??!1}):(c.resetFields(),c.setFieldsValue({icon:`HomeOutlined`,sortOrder:e.length,isActive:!0,menuType:`page`,menuTarget:i,isDefault:!1}))},p=e.filter(e=>{let t=e.menuTarget??`frontend`;return t===i||t===`both`}),m=[{value:``,label:`顶级菜单`},...p.filter(e=>e.menuType===`group`).map(e=>({value:e.id,label:e.label}))],h=[],g=p.filter(e=>!e.parentId),_={};p.filter(e=>e.parentId).forEach(e=>{let t=e.parentId;_[t]||(_[t]=[]),_[t].push(e)}),g.sort((e,t)=>(e.sortOrder??0)-(t.sortOrder??0)).forEach(e=>{h.push({...e,_depth:0}),(_[e.id]||[]).sort((e,t)=>(e.sortOrder??0)-(t.sortOrder??0)).forEach(e=>{h.push({...e,_depth:1})})});let v=[{title:`排序`,dataIndex:`sortOrder`,width:60},{title:`菜单名称`,key:`label`,width:180,render:(e,t)=>(0,$.jsxs)(`span`,{style:{paddingLeft:t._depth*20,fontWeight:t._depth===0?600:400},children:[t._depth===1&&(0,$.jsx)(`span`,{style:{color:`#cbd5e1`,marginRight:4},children:`└`}),t.label]})},{title:`路由路径`,dataIndex:`path`,width:160,render:e=>e||(0,$.jsx)(Q.Text,{type:`secondary`,children:`-`})},{title:`图标`,dataIndex:`icon`,width:100,render:e=>e&&uY[e]?(0,$.jsx)(`span`,{style:{fontSize:16,color:`#6366f1`},children:uY[e]}):`-`},{title:`类型`,dataIndex:`menuType`,width:80,render:e=>(0,$.jsx)(CB,{color:fY[e]||`default`,children:pY[e]||e})},{title:`状态`,dataIndex:`isActive`,width:70,render:e=>(0,$.jsx)(`span`,{style:{color:e?`#22c55e`:`#94a3b8`},children:e?`启用`:`停用`})},...i===`frontend`?[{title:`默认显示`,dataIndex:`isDefault`,width:80,render:e=>e?(0,$.jsx)(CB,{color:`green`,children:`默认`}):(0,$.jsx)(Q.Text,{type:`secondary`,children:`-`})}]:[],{title:`操作`,key:`action`,width:150,render:(e,t)=>(0,$.jsxs)(wj,{size:4,children:[(0,$.jsx)(mD,{type:`link`,size:`small`,icon:(0,$.jsx)(kB,{}),onClick:()=>f(t),children:`编辑`}),(0,$.jsx)(DP,{title:`确定删除该菜单?`,onConfirm:()=>d(t.id),children:(0,$.jsx)(mD,{type:`link`,size:`small`,danger:!0,icon:(0,$.jsx)(EB,{}),children:`删除`})})]})}];return(0,$.jsxs)(`div`,{children:[(0,$.jsxs)(Mk,{bordered:!1,style:{borderRadius:12,border:`1px solid #f0f0f5`},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,marginBottom:16},children:[(0,$.jsxs)(wj,{children:[(0,$.jsx)(aG,{style:{fontSize:18,color:`#6366f1`}}),(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:16},children:`菜单配置`})]}),(0,$.jsx)(mD,{type:`primary`,icon:(0,$.jsx)(_O,{}),onClick:()=>f(),style:{borderRadius:8},children:`添加菜单`})]}),(0,$.jsx)(vk,{activeKey:i,onChange:a,items:[{key:`frontend`,label:`前台菜单`},{key:`admin`,label:`后台菜单`}]}),(0,$.jsx)(uB,{columns:v,dataSource:h,rowKey:`id`,loading:n,pagination:!1,scroll:{x:900}})]}),(0,$.jsx)(CP,{title:(0,$.jsxs)(wj,{children:[(0,$.jsx)(aG,{}),o.menu?.id?`编辑菜单`:`添加菜单`]}),open:o.open,onOk:u,onCancel:()=>{s({open:!1,menu:null}),c.resetFields()},okText:`确认`,cancelText:`取消`,width:520,children:(0,$.jsxs)(Z,{form:c,layout:`vertical`,children:[(0,$.jsx)(Z.Item,{name:`label`,label:`菜单名称`,rules:[{required:!0,message:`请输入菜单名称`}],children:(0,$.jsx)(QM,{placeholder:`例如:我的项目`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`path`,label:`路由路径`,tooltip:`分组类型可留空`,children:(0,$.jsx)(QM,{placeholder:`例如:/projects(分组可留空)`,size:`large`})}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`menuType`,label:`菜单类型`,style:{flex:1},rules:[{required:!0}],children:(0,$.jsx)(ZC,{size:`large`,options:[{value:`page`,label:`页面`},{value:`group`,label:`分组`}]})}),(0,$.jsx)(Z.Item,{name:`menuTarget`,label:`适用端`,style:{flex:1},rules:[{required:!0}],children:(0,$.jsx)(ZC,{size:`large`,options:[{value:`frontend`,label:`前台`},{value:`admin`,label:`后台`},{value:`both`,label:`两者`}]})})]}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`icon`,label:`图标`,style:{flex:1},rules:[{required:!0}],children:(0,$.jsx)(ZC,{size:`large`,options:dY})}),(0,$.jsx)(Z.Item,{name:`sortOrder`,label:`排序`,style:{flex:1},children:(0,$.jsx)($A,{min:0,max:100,style:{width:`100%`},size:`large`,placeholder:`默认0`})})]}),(0,$.jsx)(Z.Item,{name:`parentId`,label:`上级菜单`,children:(0,$.jsx)(ZC,{size:`large`,options:m,allowClear:!0,placeholder:`顶级菜单`})}),(0,$.jsx)(Z.Item,{name:`isActive`,label:`启用`,valuePropName:`checked`,children:(0,$.jsx)(yF,{})}),(0,$.jsx)(Z.Item,{noStyle:!0,shouldUpdate:(e,t)=>e.menuTarget!==t.menuTarget,children:({getFieldValue:e})=>e(`menuTarget`)===`admin`?null:(0,$.jsx)(Z.Item,{name:`isDefault`,label:`新用户默认显示`,valuePropName:`checked`,tooltip:`开启后,新注册用户默认显示此菜单`,children:(0,$.jsx)(yF,{})})})]})})]})},hY={normal:`blue`,gift:`green`,promo:`purple`},gY={normal:`常规`,gift:`赠送`,promo:`促销`},_Y=()=>{let[e,t]=(0,x.useState)([]),[n,r]=(0,x.useState)(!1),[i,a]=(0,x.useState)({open:!1,item:null}),[o]=Z.useForm(),s=async()=>{r(!0);try{t((await CJ()).map(e=>({id:e.id,name:e.name,credits:e.credits,price:e.price,bonusCredits:e.bonus_credits??e.bonusCredits??0,totalCredits:e.total_credits??e.totalCredits??e.credits,description:e.description,packageType:e.package_type??e.packageType??`normal`,isGift:e.is_gift??e.isGift??!1,isActive:e.is_active??e.isActive??!0,sortOrder:e.sort_order??e.sortOrder??0})))}catch{bP.error(`加载充值套餐失败`)}finally{r(!1)}};(0,x.useEffect)(()=>{s()},[]);let c=async()=>{try{let e=await o.validateFields(),t={name:e.name,credits:e.credits,price:e.price,bonus_credits:e.bonusCredits||0,description:e.description||null,package_type:e.packageType||`normal`,is_gift:e.isGift||!1,is_active:e.isActive??!0,sort_order:e.sortOrder??0};i.item?.id?(await wJ({id:i.item.id,...t}),bP.success(`已更新`)):(await wJ(t),bP.success(`已添加`)),a({open:!1,item:null}),o.resetFields(),s()}catch{}},l=async e=>{try{await TJ(e),bP.success(`已删除`),s()}catch(e){bP.error(e?.message||`删除失败`)}},u=e=>{a({open:!0,item:e||null}),e?o.setFieldsValue({name:e.name,credits:e.credits,price:e.price,bonusCredits:e.bonusCredits,description:e.description,packageType:e.packageType,isGift:e.isGift,isActive:e.isActive,sortOrder:e.sortOrder}):(o.resetFields(),o.setFieldsValue({isActive:!0,sortOrder:0,packageType:`normal`,bonusCredits:0,isGift:!1}))};return(0,$.jsxs)(`div`,{children:[(0,$.jsxs)(Mk,{bordered:!1,style:{borderRadius:12,border:`1px solid #f0f0f5`},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,marginBottom:16},children:[(0,$.jsxs)(wj,{children:[(0,$.jsx)(bW,{style:{fontSize:18,color:`#6366f1`}}),(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:16},children:`充值套餐管理`}),(0,$.jsxs)(CB,{color:`purple`,children:[e.length,` 个套餐`]})]}),(0,$.jsx)(mD,{type:`primary`,icon:(0,$.jsx)(_O,{}),onClick:()=>u(),style:{borderRadius:8},children:`添加套餐`})]}),(0,$.jsx)(uB,{columns:[{title:`套餐名称`,key:`name`,width:160,render:(e,t)=>(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{strong:!0,children:t.name}),t.description&&(0,$.jsx)(`div`,{style:{color:`#94a3b8`,fontSize:12},children:t.description})]})},{title:`基础积分`,dataIndex:`credits`,width:100,render:e=>(0,$.jsx)(Q.Text,{children:e.toLocaleString()})},{title:`赠送积分`,dataIndex:`bonusCredits`,width:100,render:e=>e>0?(0,$.jsxs)(CB,{color:`green`,children:[`+`,e.toLocaleString()]}):(0,$.jsx)(Q.Text,{type:`secondary`,children:`-`})},{title:`总积分`,key:`total`,width:100,render:(e,t)=>(0,$.jsx)(Q.Text,{strong:!0,style:{color:`#6366f1`},children:(t.credits+t.bonusCredits).toLocaleString()})},{title:`价格(元)`,dataIndex:`price`,width:100,render:e=>(0,$.jsxs)(Q.Text,{strong:!0,children:[`¥`,e]})},{title:`类型`,dataIndex:`packageType`,width:80,render:e=>(0,$.jsx)(CB,{color:hY[e]||`default`,children:gY[e]||e})},{title:`状态`,dataIndex:`isActive`,width:80,render:e=>(0,$.jsx)(CB,{color:e?`green`:`default`,children:e?`启用`:`停用`})},{title:`操作`,key:`action`,width:150,fixed:`right`,render:(e,t)=>(0,$.jsxs)(wj,{size:4,children:[(0,$.jsx)(mD,{type:`link`,size:`small`,icon:(0,$.jsx)(kB,{}),onClick:()=>u(t),children:`编辑`}),(0,$.jsx)(DP,{title:`确定删除?`,onConfirm:()=>l(t.id),children:(0,$.jsx)(mD,{type:`link`,size:`small`,danger:!0,icon:(0,$.jsx)(EB,{}),children:`删除`})})]})}],dataSource:e,rowKey:`id`,loading:n,pagination:!1,scroll:{x:900}})]}),(0,$.jsx)(CP,{title:(0,$.jsxs)(wj,{children:[(0,$.jsx)(bW,{}),i.item?`编辑套餐`:`添加套餐`]}),open:i.open,onOk:c,onCancel:()=>{a({open:!1,item:null}),o.resetFields()},okText:`保存`,cancelText:`取消`,width:520,children:(0,$.jsxs)(Z,{form:o,layout:`vertical`,style:{marginTop:16},children:[(0,$.jsx)(Z.Item,{name:`name`,label:`套餐名称`,rules:[{required:!0,message:`请输入套餐名称`}],children:(0,$.jsx)(QM,{placeholder:`例如:进阶包`,size:`large`})}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`credits`,label:`基础积分`,rules:[{required:!0,message:`请输入积分`}],style:{flex:1},children:(0,$.jsx)($A,{min:1,placeholder:`2000`,size:`large`,style:{width:`100%`}})}),(0,$.jsx)(Z.Item,{name:`price`,label:`价格(元)`,rules:[{required:!0,message:`请输入价格`}],style:{flex:1},children:(0,$.jsx)($A,{min:.01,step:1,placeholder:`168`,size:`large`,style:{width:`100%`}})})]}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`bonusCredits`,label:`赠送积分`,initialValue:0,style:{flex:1},children:(0,$.jsx)($A,{min:0,placeholder:`0`,size:`large`,style:{width:`100%`}})}),(0,$.jsx)(Z.Item,{name:`packageType`,label:`套餐类型`,initialValue:`normal`,style:{flex:1},children:(0,$.jsx)(ZC,{size:`large`,options:[{value:`normal`,label:`常规`},{value:`gift`,label:`赠送`},{value:`promo`,label:`促销`}]})})]}),(0,$.jsx)(Z.Item,{name:`description`,label:`描述`,children:(0,$.jsx)(QM,{placeholder:`套餐描述(可选)`,size:`large`})}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`isActive`,label:`启用状态`,valuePropName:`checked`,initialValue:!0,style:{flex:1},children:(0,$.jsx)(yF,{})}),(0,$.jsx)(Z.Item,{name:`isGift`,label:`是否赠送`,valuePropName:`checked`,initialValue:!1,style:{flex:1},children:(0,$.jsx)(yF,{})}),(0,$.jsx)(Z.Item,{name:`sortOrder`,label:`排序`,initialValue:0,style:{flex:1},children:(0,$.jsx)($A,{size:`large`,style:{width:`100%`}})})]})]})})]})},vY={POST:`green`,PUT:`blue`,DELETE:`red`},yY=()=>{let[e,t]=(0,x.useState)([]),[n,r]=(0,x.useState)(0),[i,a]=(0,x.useState)(!1),[o,s]=(0,x.useState)(1),c=async e=>{a(!0);try{let n=await EJ(e||o);t(n.items||[]),r(n.total||0)}catch{bP.error(`加载操作日志失败`)}finally{a(!1)}};return(0,x.useEffect)(()=>{c()},[]),(0,$.jsx)(`div`,{children:(0,$.jsxs)(Mk,{bordered:!1,style:{borderRadius:12,border:`1px solid #f0f0f5`},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,marginBottom:16},children:[(0,$.jsxs)(wj,{children:[(0,$.jsx)(MW,{style:{fontSize:18,color:`#6366f1`}}),(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:16},children:`操作日志`})]}),(0,$.jsx)(mD,{icon:(0,$.jsx)(lF,{}),onClick:()=>c(),children:`刷新`})]}),(0,$.jsx)(uB,{columns:[{title:`操作人`,dataIndex:`username`,width:120,render:e=>(0,$.jsx)(Q.Text,{strong:!0,children:e})},{title:`操作`,dataIndex:`action`,width:160,render:e=>(0,$.jsx)(Q.Text,{children:e})},{title:`方法`,dataIndex:`method`,width:80,render:e=>(0,$.jsx)(CB,{color:vY[e]||`default`,children:e})},{title:`路径`,dataIndex:`path`,width:220,ellipsis:!0,render:e=>(0,$.jsx)(Q.Text,{type:`secondary`,style:{fontSize:12},children:e})},{title:`时间`,dataIndex:`createdAt`,width:160,render:e=>(0,$.jsx)(Q.Text,{type:`secondary`,style:{fontSize:12},children:BJ(e)})}],dataSource:e,rowKey:`id`,loading:i,pagination:{current:o,pageSize:20,total:n,showTotal:e=>`共 ${e} 条记录`,onChange:e=>{s(e),c(e)}},scroll:{x:800}})]})})},bY=`http://ceshi.apiforeign.minzhong.cn`.replace(/\/api\/?$/i,``).replace(/\/$/,``),xY={image:`empty`,video:`empty`,videoCover:`empty`,references:{}},SY={optimizing:{color:`processing`,text:`优化中`,icon:(0,$.jsx)(um,{spin:!0})},prompt_optimized:{color:`processing`,text:`待生成`,icon:(0,$.jsx)(hj,{})},generating:{color:`warning`,text:`生成中`,icon:(0,$.jsx)(um,{spin:!0})},completed:{color:`success`,text:`已完成`,icon:(0,$.jsx)(bU,{})},failed:{color:`error`,text:`失败`,icon:(0,$.jsx)(CU,{})}},CY={image:{text:`图片`,color:`purple`,icon:(0,$.jsx)(aW,{})},video:{text:`视频`,color:`geekblue`,icon:(0,$.jsx)(cq,{})}},wY=e=>/^(https?:)?\/\//i.test(e)||/^(blob|data):/i.test(e),TY=e=>!!e&&/^blob:/i.test(e.trim()),EY=e=>{if(!e)return``;let t=String(e).trim();return t?wY(t)?t:bY?`${bY}${t.startsWith(`/`)?t:`/${t}`}`:t.startsWith(`/`)?t:`/${t}`:``},DY=e=>e?e.length>12?`${e.slice(0,8)}...`:e:`-`,OY=e=>e?BJ(e):`-`,kY=e=>e==null||e===``,AY=e=>{if(!e||TY(e))return!1;try{let t=new URL(EY(e),window.location.origin),n=t.searchParams.get(`exp`)||t.searchParams.get(`expires`)||t.searchParams.get(`expire`)||t.searchParams.get(`expires_at`)||t.searchParams.get(`x-expires`);if(!n)return!1;let r=Number(n);if(!Number.isFinite(r))return!1;let i=r>1e10?r:r*1e3;return Date.now()>=i}catch{return!1}},jY=e=>e?TY(e)||AY(e)?`invalid`:`checking`:`empty`,MY=e=>{let t=e.url||e.mediaUrl||e.fileUrl;return typeof t==`string`&&t.trim()?t.trim():void 0},NY=e=>{let t=String(e.type||e.mediaType||e.mimeType||``).toLowerCase(),n=MY(e)?.toLowerCase()||``;return t.includes(`video`)||/\.(mp4|mov|webm|m4v)(\?|$)/i.test(n)?`video`:t.includes(`image`)||/\.(png|jpe?g|webp|gif|bmp|svg)(\?|$)/i.test(n)?`image`:t||`unknown`},PY=(e,t)=>`${t}-${MY(e)||`empty`}`,FY=(e,t=`资源`)=>TY(e)?`本地临时素材已失效`:AY(e)?`${t}链接已超时,请刷新列表或重新搜索后再查看`:`${t}加载失败,请刷新列表或重新搜索后再查看`,IY=({text:e,minHeight:t=240,compact:n=!1,action:r})=>(0,$.jsxs)(`div`,{style:{width:`100%`,minHeight:n?void 0:t,height:n?`100%`:void 0,borderRadius:n?10:12,background:`#f8f9fc`,border:`1px dashed #cbd5e1`,color:`#64748b`,display:`flex`,alignItems:`center`,justifyContent:`center`,flexDirection:`column`,gap:n?4:10,textAlign:`center`},children:[(0,$.jsx)(Q.Text,{style:{color:`#64748b`,fontSize:n?11:13},children:e}),r]}),LY=({label:e,value:t})=>(0,$.jsxs)(`div`,{style:{flex:1,minWidth:120},children:[(0,$.jsx)(Q.Text,{style:{fontSize:11,color:`#94a3b8`,display:`block`},children:e}),(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:14},children:kY(t)?`-`:t})]}),RY=()=>{let[e,t]=(0,x.useState)([]),[n,r]=(0,x.useState)(0),[i,a]=(0,x.useState)(!1),[o,s]=(0,x.useState)(1),[c]=(0,x.useState)(20),[l,u]=(0,x.useState)(``),[d,f]=(0,x.useState)(``),[p,m]=(0,x.useState)(0),[h,g]=(0,x.useState)(null),[_,v]=(0,x.useState)(xY),[y,b]=(0,x.useState)(!1),S=(0,x.useRef)(null),[C,w]=(0,x.useState)(null),[T,E]=(0,x.useState)(null),D=(0,x.useCallback)(async()=>{a(!0);try{let e=await DJ({userId:d.trim()||void 0,status:l||void 0,page:o,pageSize:c});t((e.items||[]).map(e=>({id:e.id,userId:e.userId,username:e.username,projectId:e.projectId,projectName:e.projectName,originalPrompt:e.originalPrompt,optimizedPrompt:e.optimizedPrompt,duration:e.duration,aspectRatio:e.aspectRatio,resolution:e.resolution,status:e.status,videoUrl:e.videoUrl,videoCoverUrl:e.videoCoverUrl,references:e.references,creditsCost:e.creditsCost||0,textCreditsCost:e.textCreditsCost||0,textTokensUsed:e.textTokensUsed||0,videoTokensUsed:e.videoTokensUsed||0,errorMessage:e.errorMessage,createdAt:e.createdAt,generatedAt:e.generatedAt,genType:e.genType,imageSize:e.imageSize,imageUrl:e.imageUrl,imageTokensUsed:e.imageTokensUsed||0,imageProportion:e.imageProportion,imagePx:e.imagePx}))),r(e.total||0)}catch{bP.error(`加载记录失败`)}finally{a(!1)}},[l,d,o,c]);(0,x.useEffect)(()=>{D()},[D,p]),(0,x.useEffect)(()=>{if(!h){v(xY);return}let e=(h.references||[]).reduce((e,t,n)=>{let r=MY(t);return e[PY(t,n)]=jY(r),e},{});S.current&&(S.current.pause(),S.current.currentTime=0),b(!1),v({image:jY(h.imageUrl),video:jY(h.videoUrl),videoCover:jY(h.videoCoverUrl),references:e})},[h]);let O=()=>{s(1),m(e=>e+1)},k=(0,x.useCallback)(e=>{g(e)},[]),A=()=>{S.current&&S.current.pause(),b(!1),g(null)},j=e=>{v(t=>({...t,...e}))},M=(e,t)=>{v(n=>({...n,references:{...n.references,[e]:t}}))},N=(e,t=`素材`)=>{if(!e){bP.warning(`${t}链接为空,暂无法查看`);return}if(TY(e)){bP.warning(`本地临时素材已失效,暂无法查看`);return}if(AY(e)){bP.warning(`${t}链接已超时,请刷新列表或重新搜索后再查看`);return}window.open(EY(e),`_blank`,`noopener,noreferrer`)},P=()=>{if(h?.videoUrl){if(AY(h.videoUrl)){j({video:`invalid`}),bP.warning(`视频链接已超时,请刷新列表或重新搜索后再查看`);return}b(!0),window.setTimeout(()=>{S.current?.play().catch(()=>{b(!1),j({video:`invalid`}),bP.warning(`视频播放失败,请确认资源链接是否仍然有效`)})},0)}},F=async(e,t,n)=>{w(e);try{await OJ(e,t,n),bP.success(`状态已更新`),D()}catch(e){bP.error(e?.message||`更新失败`)}finally{w(null)}},I=async()=>{if(T){w(T.record.id);try{await kJ(T.record.id,T.ratio,T.resolution,T.image_size),bP.success(`已提交${T.record.genType===`video`?`视频`:`图片`}生成`),E(null),D()}catch(e){bP.error(e?.message||`生成失败`)}finally{w(null)}}},L=(0,x.useMemo)(()=>[{title:`用户`,key:`user`,width:120,render:(e,t)=>(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:13},children:t.username||`未知用户`}),(0,$.jsx)(`div`,{style:{fontSize:11,color:`#94a3b8`},children:DY(t.userId)})]})},{title:`项目`,dataIndex:`projectName`,width:120,ellipsis:!0,render:e=>(0,$.jsx)(Q.Text,{style:{fontSize:13},children:e||`-`})},{title:`类型`,dataIndex:`genType`,width:90,ellipsis:!0,render:e=>{let t=CY[e]||{text:e||`-`,color:`default`,icon:null};return(0,$.jsx)(CB,{color:t.color,icon:t.icon,children:t.text})}},{title:`提示词`,key:`prompt`,ellipsis:!0,render:(e,t)=>(0,$.jsx)(Bw,{title:t.originalPrompt,placement:`topLeft`,children:(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#475569`},ellipsis:!0,children:t.originalPrompt||`-`})})},{title:`参数`,key:`params`,width:160,render:(e,t)=>t.genType===`video`?t.duration||t.aspectRatio||t.resolution?(0,$.jsxs)(wj,{size:4,wrap:!0,children:[t.duration?(0,$.jsxs)(CB,{children:[t.duration,`s`]}):null,t.aspectRatio?(0,$.jsx)(CB,{children:t.aspectRatio}):null,t.resolution?(0,$.jsx)(CB,{children:t.resolution}):null]}):(0,$.jsx)(CB,{color:`default`,children:`待配置`}):t.imageSize||t.imageProportion||t.imagePx?(0,$.jsxs)(wj,{size:4,wrap:!0,children:[t.imageSize?(0,$.jsx)(CB,{children:t.imageSize}):null,t.imageProportion?(0,$.jsx)(CB,{children:t.imageProportion}):null,t.imagePx?(0,$.jsx)(CB,{children:t.imagePx}):null]}):(0,$.jsx)(CB,{color:`default`,children:`待配置`})},{title:`积分`,key:`credits`,width:120,render:(e,t)=>(0,$.jsxs)(`div`,{style:{fontSize:12},children:[t.textCreditsCost>0?(0,$.jsxs)(`div`,{style:{color:`#f59e0b`},children:[`文字: `,t.textCreditsCost]}):null,t.creditsCost>0?(0,$.jsxs)(`div`,{style:{color:`#6366f1`},children:[t.genType===`video`?`视频`:`图片`,`: `,t.creditsCost]}):null,t.textCreditsCost===0&&t.creditsCost===0?(0,$.jsx)(Q.Text,{style:{color:`#94a3b8`},children:`0`}):null]})},{title:`状态`,dataIndex:`status`,width:90,render:e=>{let t=SY[e]||{color:`default`,text:e||`-`,icon:null};return(0,$.jsx)(CB,{color:t.color,icon:t.icon,children:t.text})}},{title:`时间`,key:`time`,width:150,render:(e,t)=>(0,$.jsxs)(`div`,{style:{fontSize:12,color:`#94a3b8`},children:[(0,$.jsx)(`div`,{children:OY(t.createdAt)}),t.generatedAt?(0,$.jsxs)(`div`,{style:{color:`#10b981`},children:[`生成: `,OY(t.generatedAt)]}):null]})},{title:`操作`,key:`action`,width:150,fixed:`right`,render:(e,t)=>(0,$.jsxs)(wj,{size:4,wrap:!0,children:[(0,$.jsx)(mD,{size:`small`,icon:(0,$.jsx)(AM,{}),onClick:()=>k(t),children:`详情`}),t.status===`generating`?(0,$.jsx)(mD,{size:`small`,danger:!0,loading:C===t.id,onClick:()=>{CP.confirm({title:`确认操作`,icon:(0,$.jsx)($U,{}),content:`确定将此记录标记为失败?`,onOk:()=>F(t.id,`failed`)})},children:`标记失败`}):null,t.status===`failed`?(0,$.jsx)(mD,{size:`small`,type:`primary`,danger:!0,loading:C===t.id,onClick:()=>E({record:t,ratio:t.aspectRatio||`16:9`,resolution:t.resolution||`720p`,image_size:t.imageSize||`2K`}),children:`重试生成`}):null,t.status===`prompt_optimized`?(0,$.jsxs)($.Fragment,{children:[(0,$.jsxs)(mD,{size:`small`,type:`primary`,loading:C===t.id,onClick:()=>E({record:t,ratio:t.aspectRatio||`16:9`,resolution:t.resolution||`720p`,image_size:t.imageSize||`2K`}),style:{background:`#6366f1`,border:`none`},children:[`生成`,t.genType===`video`?`视频`:`图片`]}),(0,$.jsx)(mD,{size:`small`,danger:!0,loading:C===t.id,onClick:()=>{CP.confirm({title:`确认操作`,icon:(0,$.jsx)($U,{}),content:`确定将此记录标记为失败?`,onOk:()=>F(t.id,`failed`)})},children:`标记失败`})]}):null]})}],[k,C]),R=h?CY[h.genType||``]||{text:h.genType||`-`,color:`default`,icon:null}:null,z=h?SY[h.status]||{color:`default`,text:h.status||`-`,icon:null}:null,B=()=>{if(!h||h.genType!==`image`||h.status!==`completed`)return null;if(!h.imageUrl)return(0,$.jsx)(IY,{text:`此图片任务暂无结果图片`,minHeight:260});if(_.image===`invalid`)return(0,$.jsx)(IY,{text:FY(h.imageUrl,`图片`),minHeight:260});let e=EY(h.imageUrl);return(0,$.jsxs)(`div`,{title:`点击新页面查看图片`,onClick:()=>N(h.imageUrl,`生成图片`),style:{position:`relative`,width:`100%`,minHeight:260,borderRadius:12,background:`#f8f9fc`,border:`1px solid #e2e8f0`,overflow:`hidden`,display:`flex`,alignItems:`center`,justifyContent:`center`,cursor:`pointer`},children:[(0,$.jsx)(`img`,{src:e,alt:`生成图片`,onLoad:()=>j({image:`valid`}),onError:()=>j({image:`invalid`}),style:{display:_.image===`valid`?`block`:`none`,width:`100%`,maxHeight:560,objectFit:`contain`,background:`#fff`}},`${h.id}-${e}`),_.image===`checking`?(0,$.jsx)(IY,{text:`图片加载检测中...`,minHeight:260}):null]})},V=()=>{if(!h||y||_.video===`invalid`)return null;let e=h.videoUrl?(0,$.jsx)(mD,{type:`primary`,shape:`circle`,size:`large`,icon:(0,$.jsx)(kG,{}),onClick:P,style:{boxShadow:`0 8px 20px rgba(15,23,42,0.25)`}}):null;if(!h.videoCoverUrl)return(0,$.jsx)(`div`,{style:{position:`absolute`,inset:0,borderRadius:12,overflow:`hidden`,background:`#f8f9fc`,display:`flex`,alignItems:`center`,justifyContent:`center`},children:(0,$.jsx)(IY,{text:`此视频无封面`,minHeight:340,action:e})});if(_.videoCover===`invalid`)return(0,$.jsx)(IY,{text:FY(h.videoCoverUrl,`视频封面`),minHeight:340,action:e});let t=EY(h.videoCoverUrl);return(0,$.jsxs)(`div`,{style:{position:`absolute`,inset:0,borderRadius:12,overflow:`hidden`,background:`#f8f9fc`,border:`1px solid #e2e8f0`,display:`flex`,alignItems:`center`,justifyContent:`center`},children:[(0,$.jsx)(`img`,{src:t,alt:`视频封面`,onLoad:()=>j({videoCover:`valid`}),onError:()=>j({videoCover:`invalid`}),style:{display:_.videoCover===`valid`?`block`:`none`,width:`100%`,height:`100%`,objectFit:`contain`,background:`#000`}},`${h.id}-cover-${t}`),_.videoCover===`checking`?(0,$.jsx)(IY,{text:`视频封面加载检测中...`,minHeight:340,action:e}):null,_.videoCover===`valid`?(0,$.jsx)(`div`,{style:{position:`absolute`,inset:0,display:`flex`,alignItems:`center`,justifyContent:`center`,background:`rgba(15,23,42,0.18)`},children:e}):null]})},H=()=>{if(!h||h.genType!==`video`||h.status!==`completed`)return null;if(!h.videoUrl)return(0,$.jsx)(IY,{text:`此视频任务暂无结果视频`,minHeight:340});if(_.video===`invalid`)return(0,$.jsx)(IY,{text:FY(h.videoUrl,`视频`),minHeight:340});let e=EY(h.videoUrl);return(0,$.jsxs)(`div`,{style:{position:`relative`,width:`100%`,height:340,borderRadius:12,background:`#000`,overflow:`hidden`,border:`1px solid #e2e8f0`},children:[(0,$.jsx)(`video`,{ref:S,src:e,preload:`metadata`,controls:y,onLoadedMetadata:()=>j({video:`valid`}),onCanPlay:()=>j({video:`valid`}),onPlaying:()=>{b(!0),j({video:`valid`})},onPause:()=>b(!1),onEnded:()=>b(!1),onError:()=>{b(!1),j({video:`invalid`})},style:{width:`100%`,height:`100%`,objectFit:`contain`,background:`#000`,opacity:+!!y,pointerEvents:y?`auto`:`none`}},`${h.id}-${e}`),V()]})},U=(e,t)=>{let n=MY(e),r=NY(e),i=PY(e,t),a=_.references[i]||jY(n),o=r===`image`,s=r===`video`,c=typeof e.name==`string`&&e.name?e.name:`参考素材 ${t+1}`;if(!n)return(0,$.jsxs)(`div`,{style:{width:94},children:[(0,$.jsx)(`div`,{style:{width:94,height:94},children:(0,$.jsx)(IY,{text:`无链接`,compact:!0})}),(0,$.jsx)(Q.Text,{ellipsis:!0,style:{display:`block`,marginTop:4,fontSize:11,color:`#94a3b8`},children:c})]},i);if(a===`invalid`)return(0,$.jsxs)(`div`,{style:{width:94},children:[(0,$.jsx)(`div`,{style:{width:94,height:94},children:(0,$.jsx)(IY,{text:TY(n)?`本地临时素材已失效`:`素材不可访问`,compact:!0})}),(0,$.jsx)(Bw,{title:c,children:(0,$.jsx)(Q.Text,{ellipsis:!0,style:{display:`block`,marginTop:4,fontSize:11,color:`#94a3b8`},children:c})})]},i);let l=EY(n);return(0,$.jsxs)(`div`,{style:{width:94},children:[(0,$.jsxs)(`div`,{role:`button`,tabIndex:0,title:`点击新页面查看素材`,onClick:()=>N(n,s?`视频素材`:o?`图片素材`:`素材`),onKeyDown:e=>{e.key===`Enter`&&N(n,s?`视频素材`:o?`图片素材`:`素材`)},style:{width:94,height:94,borderRadius:10,overflow:`hidden`,border:`1px solid #e2e8f0`,background:`#f8f9fc`,position:`relative`,display:`flex`,alignItems:`center`,justifyContent:`center`,cursor:`pointer`},children:[o?(0,$.jsx)(`img`,{src:l,alt:c,onLoad:()=>M(i,`valid`),onError:()=>M(i,`invalid`),style:{display:a===`valid`?`block`:`none`,width:`100%`,height:`100%`,objectFit:`cover`}}):null,s?(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`video`,{src:l,preload:`metadata`,onLoadedMetadata:()=>M(i,`valid`),onError:()=>M(i,`invalid`),style:{display:`none`}}),(0,$.jsxs)(`div`,{style:{color:`#64748b`,fontSize:12,textAlign:`center`},children:[(0,$.jsx)(kG,{style:{fontSize:18}}),(0,$.jsx)(`div`,{children:`视频素材`}),(0,$.jsx)(`div`,{style:{fontSize:10,color:`#94a3b8`,marginTop:2},children:`点击查看`})]})]}):null,!o&&!s?(0,$.jsxs)(`div`,{style:{color:`#64748b`,fontSize:12,textAlign:`center`},children:[`文件素材`,(0,$.jsx)(`div`,{style:{fontSize:10,color:`#94a3b8`,marginTop:2},children:`点击查看`})]}):null,a===`checking`&&o?(0,$.jsx)(`div`,{style:{position:`absolute`,inset:0,display:`flex`,alignItems:`center`,justifyContent:`center`,background:`#f8f9fc`},children:(0,$.jsx)(um,{style:{color:`#6366f1`}})}):null]}),(0,$.jsx)(Bw,{title:c,children:(0,$.jsx)(Q.Text,{ellipsis:!0,style:{display:`block`,marginTop:4,fontSize:11,color:`#94a3b8`},children:c})})]},i)};return(0,$.jsxs)(`div`,{children:[(0,$.jsxs)(Mk,{bordered:!1,style:{borderRadius:12,border:`1px solid #f0f0f5`},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,marginBottom:16,flexWrap:`wrap`,gap:12},children:[(0,$.jsxs)(wj,{children:[(0,$.jsx)(cq,{style:{fontSize:18,color:`#6366f1`}}),(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:16},children:`生成记录管理`}),(0,$.jsxs)(CB,{color:`purple`,children:[n,` 条记录`]})]}),(0,$.jsxs)(wj,{children:[(0,$.jsx)(ZC,{placeholder:`状态筛选`,allowClear:!0,style:{width:120},value:l||void 0,onChange:e=>{u(e||``),s(1)},options:[{value:`optimizing`,label:`优化中`},{value:`prompt_optimized`,label:`待生成`},{value:`generating`,label:`生成中`},{value:`completed`,label:`已完成`},{value:`failed`,label:`失败`}]}),(0,$.jsx)(QM,{placeholder:`用户ID搜索`,prefix:(0,$.jsx)(KC,{style:{color:`#94a3b8`}}),style:{width:200},value:d,onChange:e=>f(e.target.value),onPressEnter:O,allowClear:!0}),(0,$.jsx)(mD,{type:`primary`,onClick:O,style:{borderRadius:8},children:`搜索`})]})]}),(0,$.jsx)(uB,{columns:L,dataSource:e,rowKey:`id`,loading:i,scroll:{x:1120},pagination:{current:o,pageSize:c,total:n,onChange:s,showSizeChanger:!1,showTotal:e=>`共 ${e} 条`}})]}),(0,$.jsx)(CP,{title:(0,$.jsxs)(wj,{children:[h?.genType===`video`?(0,$.jsx)(cq,{}):(0,$.jsx)(aW,{}),(0,$.jsx)(`span`,{children:`生成记录详情`})]}),open:!!h,onCancel:A,footer:null,width:900,destroyOnClose:!0,children:h?(0,$.jsxs)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:16,marginTop:16},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,flexWrap:`wrap`},children:[(0,$.jsxs)(`div`,{style:{flex:1,minWidth:150,padding:12,borderRadius:10,background:`#f8f9fc`},children:[(0,$.jsx)(Q.Text,{style:{fontSize:11,color:`#94a3b8`,display:`block`},children:`用户`}),(0,$.jsx)(Q.Text,{strong:!0,children:h.username||`-`})]}),(0,$.jsxs)(`div`,{style:{flex:1,minWidth:150,padding:12,borderRadius:10,background:`#f8f9fc`},children:[(0,$.jsx)(Q.Text,{style:{fontSize:11,color:`#94a3b8`,display:`block`},children:`项目`}),(0,$.jsx)(Q.Text,{strong:!0,children:h.projectName||`-`})]}),(0,$.jsxs)(`div`,{style:{flex:1,minWidth:150,padding:12,borderRadius:10,background:`#f8f9fc`},children:[(0,$.jsx)(Q.Text,{style:{fontSize:11,color:`#94a3b8`,display:`block`},children:`类型 / 状态`}),(0,$.jsxs)(wj,{size:4,wrap:!0,children:[R?(0,$.jsx)(CB,{color:R.color,icon:R.icon,children:R.text}):null,z?(0,$.jsx)(CB,{color:z.color,icon:z.icon,children:z.text}):null]})]})]}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#94a3b8`,display:`block`,marginBottom:6},children:`原始提示词`}),(0,$.jsx)(`div`,{style:{padding:12,borderRadius:10,background:`#f8f9fc`,border:`1px solid #f0f0f5`},children:(0,$.jsx)(Q.Text,{style:{fontSize:13,color:`#475569`,lineHeight:1.7},children:h.originalPrompt||`-`})})]}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#94a3b8`,display:`block`,marginBottom:6},children:`优化后提示词`}),(0,$.jsx)(`div`,{style:{padding:12,borderRadius:10,background:`rgba(99,102,241,0.02)`,border:`1px solid rgba(99,102,241,0.1)`},children:(0,$.jsx)(Q.Text,{style:{fontSize:13,color:`#1a1a2e`,lineHeight:1.7},children:h.optimizedPrompt||`-`})})]}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,padding:12,borderRadius:10,background:`#f8f9fc`,flexWrap:`wrap`},children:[(0,$.jsx)(LY,{label:`文字积分`,value:(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(Q.Text,{strong:!0,style:{color:`#f59e0b`},children:h.textCreditsCost||0}),(0,$.jsxs)(Q.Text,{style:{fontSize:11,color:`#94a3b8`},children:[` (`,h.textTokensUsed||0,` tokens)`]})]})}),(0,$.jsx)(LY,{label:`${h.genType===`image`?`图片`:`视频`}积分`,value:(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(Q.Text,{strong:!0,style:{color:`#6366f1`},children:h.creditsCost||0}),h.genType===`video`&&h.videoTokensUsed?(0,$.jsxs)(Q.Text,{style:{fontSize:11,color:`#94a3b8`},children:[` (`,h.videoTokensUsed,` tokens)`]}):null,h.genType===`image`&&h.imageTokensUsed?(0,$.jsxs)(Q.Text,{style:{fontSize:11,color:`#94a3b8`},children:[` (`,h.imageTokensUsed,` tokens)`]}):null]})}),(0,$.jsx)(LY,{label:`总积分`,value:(h.textCreditsCost||0)+(h.creditsCost||0)})]}),h.genType===`video`?h.duration||h.aspectRatio||h.resolution?(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,padding:12,borderRadius:10,background:`#f8f9fc`,flexWrap:`wrap`},children:[(0,$.jsx)(LY,{label:`时长`,value:h.duration?`${h.duration}秒`:`-`}),(0,$.jsx)(LY,{label:`比例`,value:h.aspectRatio||`-`}),(0,$.jsx)(LY,{label:`分辨率`,value:h.resolution||`-`})]}):(0,$.jsx)(`div`,{style:{padding:12,borderRadius:10,background:`#f8f9fc`,textAlign:`center`},children:(0,$.jsx)(CB,{color:`default`,children:`视频参数待用户配置`})}):null,h.genType===`image`?h.imageSize||h.imageProportion||h.imagePx?(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,padding:12,borderRadius:10,background:`#f8f9fc`,flexWrap:`wrap`},children:[(0,$.jsx)(LY,{label:`尺寸`,value:h.imagePx||`-`}),(0,$.jsx)(LY,{label:`比例`,value:h.imageProportion||`-`}),(0,$.jsx)(LY,{label:`分辨率`,value:h.imageSize||`-`})]}):(0,$.jsx)(`div`,{style:{padding:12,borderRadius:10,background:`#f8f9fc`,textAlign:`center`},children:(0,$.jsx)(CB,{color:`default`,children:`图片参数待用户配置`})}):null,!h?.references||h.references.length===0?null:(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#94a3b8`,display:`block`,marginBottom:6},children:`参考内容`}),(0,$.jsx)(`div`,{style:{display:`flex`,gap:10,flexWrap:`wrap`},children:h.references.map((e,t)=>U(e,t))})]}),h.status===`completed`?(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#94a3b8`,display:`block`,marginBottom:6},children:h.genType===`video`?`生成视频`:`生成图片`}),h.genType===`video`?H():B()]}):null,h.status===`failed`&&h.errorMessage?(0,$.jsx)(`div`,{style:{padding:12,borderRadius:10,background:`rgba(239,68,68,0.04)`,border:`1px solid rgba(239,68,68,0.15)`},children:(0,$.jsxs)(Q.Text,{style:{fontSize:12,color:`#ef4444`},children:[`错误信息: `,h.errorMessage]})}):null,(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,fontSize:12,color:`#94a3b8`,flexWrap:`wrap`},children:[(0,$.jsxs)(`span`,{children:[`创建: `,OY(h.createdAt)]}),(0,$.jsxs)(`span`,{children:[`生成: `,OY(h.generatedAt)]})]})]}):(0,$.jsx)(xC,{description:`暂无详情`})}),(0,$.jsx)(CP,{title:(0,$.jsxs)(wj,{children:[T&&(T.record.genType===`video`?(0,$.jsx)(kG,{}):(0,$.jsx)(aW,{})),T&&(T.record.genType===`video`?`生成视频`:`生成图片`)]}),open:!!T,onCancel:()=>E(null),onOk:I,okText:`提交生成`,cancelText:`取消`,confirmLoading:T?C===T.record.id:!1,width:420,children:T?(0,$.jsx)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:16,marginTop:16},children:T.record.genType===`video`?(0,$.jsxs)($.Fragment,{children:[(0,$.jsxs)(`div`,{style:{padding:12,borderRadius:10,background:`#f8f9fc`},children:[(0,$.jsx)(Q.Text,{style:{fontSize:11,color:`#94a3b8`,display:`block`},children:`时长`}),(0,$.jsxs)(Q.Text,{strong:!0,children:[T.record.duration||5,`s`]})]}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#64748b`,display:`block`,marginBottom:6},children:`画面比例`}),(0,$.jsx)(ZC,{value:T.ratio,onChange:e=>E(t=>t?{...t,ratio:e}:null),style:{width:`100%`},options:[`16:9`,`4:3`,`1:1`,`3:4`,`9:16`,`21:9`].map(e=>({value:e,label:e}))})]}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#64748b`,display:`block`,marginBottom:6},children:`分辨率`}),(0,$.jsx)(ZC,{value:T.resolution,onChange:e=>E(t=>t?{...t,resolution:e}:null),style:{width:`100%`},options:[`480p`,`720p`,`1080p`].map(e=>({value:e,label:e}))})]})]}):(0,$.jsxs)($.Fragment,{children:[(0,$.jsxs)(`div`,{style:{padding:12,borderRadius:10,background:`#f8f9fc`},children:[(0,$.jsx)(Q.Text,{style:{fontSize:11,color:`#94a3b8`,display:`block`},children:`尺寸`}),(0,$.jsx)(Q.Text,{strong:!0,children:T.record.imagePx||`-`})]}),(0,$.jsxs)(`div`,{style:{padding:12,borderRadius:10,background:`#f8f9fc`},children:[(0,$.jsx)(Q.Text,{style:{fontSize:11,color:`#94a3b8`,display:`block`},children:`比例`}),(0,$.jsx)(Q.Text,{strong:!0,children:T.record.imageProportion||`-`})]}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#64748b`,display:`block`,marginBottom:6},children:`分辨率`}),(0,$.jsx)(ZC,{value:T.image_size,onChange:e=>E(t=>t?{...t,image_size:e}:null),style:{width:`100%`},options:[`2K`,`4K`].map(e=>({value:e,label:e}))})]})]})}):null})]})},zY=`http://ceshi.apiforeign.minzhong.cn`.replace(/\/api\/?$/i,``).replace(/\/$/,``),BY=20,VY={image:`empty`,video:`empty`,videoCover:`empty`,references:{}},HY={pending:{color:`default`,text:`待处理`,icon:(0,$.jsx)(hj,{})},generating:{color:`warning`,text:`生成中`,icon:(0,$.jsx)(um,{spin:!0})},completed:{color:`success`,text:`已完成`,icon:(0,$.jsx)(bU,{})},failed:{color:`error`,text:`失败`,icon:(0,$.jsx)(CU,{})}},UY={timeout:`任务超时`,queued:`已入队`,preparing:`准备中`,creating_provider_task:`创建任务中`,waiting_remote:`等待生成`,result_ready:`结果就绪`,downloading:`下载中`,done:`完成`,download_failed:`下载失败`,polling:`轮询中`,failed:`失败`},WY={image:{text:`图片`,color:`purple`,icon:(0,$.jsx)(aW,{})},video:{text:`视频`,color:`geekblue`,icon:(0,$.jsx)(cq,{})}},GY=e=>/^(https?:)?\/\//i.test(e)||/^(blob|data):/i.test(e),KY=e=>!!e&&/^blob:/i.test(e.trim()),qY=e=>{if(!e)return``;let t=String(e).trim();return t?GY(t)?t:zY?`${zY}${t.startsWith(`/`)?t:`/${t}`}`:t.startsWith(`/`)?t:`/${t}`:``},JY=e=>e?e.length>12?`${e.slice(0,8)}...`:e:`-`,YY=e=>e?BJ(e):`-`,XY=e=>e==null||e===``,ZY=e=>{if(!e||KY(e))return!1;try{let t=new URL(qY(e),window.location.origin),n=t.searchParams.get(`exp`)||t.searchParams.get(`expires`)||t.searchParams.get(`expire`)||t.searchParams.get(`expires_at`)||t.searchParams.get(`x-expires`);if(!n)return!1;let r=Number(n);if(!Number.isFinite(r))return!1;let i=r>1e10?r:r*1e3;return Date.now()>=i}catch{return!1}},QY=e=>e?KY(e)||ZY(e)?`invalid`:`checking`:`empty`,$Y=e=>{let t=e.url||e.mediaUrl||e.fileUrl;return typeof t==`string`&&t.trim()?t.trim():void 0},eX=e=>{let t=String(e.type||e.mediaType||e.mimeType||``).toLowerCase(),n=$Y(e)?.toLowerCase()||``;return t.includes(`video`)||/\.(mp4|mov|webm|m4v)(\?|$)/i.test(n)?`video`:t.includes(`image`)||/\.(png|jpe?g|webp|gif|bmp|svg)(\?|$)/i.test(n)?`image`:t||`unknown`},tX=(e,t)=>`${t}-${$Y(e)||`empty`}`,nX=(e,t=`资源`)=>KY(e)?`本地临时素材已失效`:ZY(e)?`${t}链接已超时,请刷新列表或重新搜索后再查看`:`${t}加载失败,请刷新列表或重新搜索后再查看`,rX=({text:e,minHeight:t=240,compact:n=!1,action:r})=>(0,$.jsxs)(`div`,{style:{width:`100%`,minHeight:n?void 0:t,height:n?`100%`:void 0,borderRadius:n?10:12,background:`#f8f9fc`,border:`1px dashed #cbd5e1`,color:`#64748b`,display:`flex`,alignItems:`center`,justifyContent:`center`,flexDirection:`column`,gap:n?4:10,textAlign:`center`},children:[(0,$.jsx)(Q.Text,{style:{color:`#64748b`,fontSize:n?11:13},children:e}),r]}),iX=({label:e,value:t})=>(0,$.jsxs)(`div`,{style:{flex:1,minWidth:120},children:[(0,$.jsx)(Q.Text,{style:{fontSize:11,color:`#94a3b8`,display:`block`},children:e}),(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:14},children:XY(t)?`-`:t})]}),aX=()=>{let[e,t]=(0,x.useState)([]),[n,r]=(0,x.useState)(0),[i,a]=(0,x.useState)(!1),[o,s]=(0,x.useState)(1),[c,l]=(0,x.useState)(``),[u,d]=(0,x.useState)(``),[f,p]=(0,x.useState)(``),[m,h]=(0,x.useState)(``),[g,_]=(0,x.useState)(``),[v,y]=(0,x.useState)(``),[b,S]=(0,x.useState)(0),[C,w]=(0,x.useState)(null),[T,E]=(0,x.useState)(VY),[D,O]=(0,x.useState)(!1),k=(0,x.useRef)(null),A=(0,x.useCallback)(async()=>{a(!0);try{let e=await jJ({genType:u||void 0,status:c||void 0,userId:g||void 0,userName:v||void 0,page:o,pageSize:BY});t(e.items||[]),r(e.total||0)}catch(e){bP.error(e?.message||`加载创作记录失败`)}finally{a(!1)}},[u,c,o,g,v]);(0,x.useEffect)(()=>{A()},[A,b]),(0,x.useEffect)(()=>{if(!C){E(VY);return}let e=(C.mediaReferences||[]).reduce((e,t,n)=>{let r=$Y(t);return e[tX(t,n)]=QY(r),e},{});k.current&&(k.current.pause(),k.current.currentTime=0),O(!1),E({image:QY(C.imageUrl),video:QY(C.videoUrl),videoCover:QY(C.videoCoverUrl),references:e})},[C]);let j=()=>{s(1),_(f.trim()),y(m.trim()),S(e=>e+1)},M=(0,x.useCallback)(e=>{w(e)},[]),N=()=>{k.current&&k.current.pause(),O(!1),w(null)},P=e=>{E(t=>({...t,...e}))},F=(e,t)=>{E(n=>({...n,references:{...n.references,[e]:t}}))},I=()=>{if(C?.videoUrl){if(ZY(C.videoUrl)){P({video:`invalid`}),bP.warning(`视频链接已超时,请刷新列表或重新搜索后再查看`);return}O(!0),window.setTimeout(()=>{k.current?.play().catch(()=>{O(!1),P({video:`invalid`}),bP.warning(`视频播放失败,请确认资源链接是否仍然有效`)})},0)}},L=(e,t=`素材`)=>{if(!e){bP.warning(`${t}链接为空,暂无法查看`);return}if(KY(e)){bP.warning(`本地临时素材已失效,暂无法查看`);return}if(ZY(e)){bP.warning(`${t}链接已超时,请刷新列表或重新搜索后再查看`);return}window.open(qY(e),`_blank`,`noopener,noreferrer`)},R=(0,x.useMemo)(()=>[{title:`用户`,key:`user`,width:150,render:(e,t)=>(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:13},children:t.userName||`未知用户`}),(0,$.jsx)(`div`,{style:{fontSize:11,color:`#94a3b8`},children:JY(t.userId)})]})},{title:`类型`,dataIndex:`genType`,width:90,render:e=>{let t=WY[e]||{text:e||`-`,color:`default`,icon:null};return(0,$.jsx)(CB,{color:t.color,icon:t.icon,children:t.text})}},{title:`提示词`,key:`prompt`,ellipsis:!0,render:(e,t)=>(0,$.jsx)(Bw,{title:t.originalPrompt,placement:`topLeft`,children:(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#475569`},ellipsis:!0,children:t.originalPrompt||`-`})})},{title:`参数`,key:`params`,width:180,render:(e,t)=>t.genType===`video`?t.duration||t.aspectRatio||t.resolution?(0,$.jsxs)(wj,{size:4,wrap:!0,children:[t.duration?(0,$.jsxs)(CB,{children:[t.duration,`s`]}):null,t.aspectRatio?(0,$.jsx)(CB,{children:t.aspectRatio}):null,t.resolution?(0,$.jsx)(CB,{children:t.resolution}):null]}):(0,$.jsx)(CB,{color:`default`,children:`无参数`}):t.imageSize||t.imageProportion||t.imagePx?(0,$.jsxs)(wj,{size:4,wrap:!0,children:[t.imageSize?(0,$.jsx)(CB,{children:t.imageSize}):null,t.imageProportion?(0,$.jsx)(CB,{children:t.imageProportion}):null,t.imagePx?(0,$.jsx)(CB,{children:t.imagePx}):null]}):(0,$.jsx)(CB,{color:`default`,children:`无参数`})},{title:`积分`,key:`credits`,width:130,render:(e,t)=>(0,$.jsxs)(`div`,{style:{fontSize:12},children:[(0,$.jsxs)(`div`,{style:{color:`#6366f1`},children:[`总: `,t.creditsCost||0]}),t.textCreditsCost>0?(0,$.jsxs)(`div`,{style:{color:`#f59e0b`},children:[`文字: `,t.textCreditsCost]}):null]})},{title:`状态`,dataIndex:`status`,width:100,render:e=>{let t=HY[e]||{color:`default`,text:e||`-`,icon:null};return(0,$.jsx)(CB,{color:t.color,icon:t.icon,children:t.text})}},{title:`阶段`,dataIndex:`pipelineStage`,width:120,render:e=>(0,$.jsx)(CB,{color:`blue`,children:UY[e]||e||`-`})},{title:`时间`,key:`time`,width:170,render:(e,t)=>(0,$.jsxs)(`div`,{style:{fontSize:12,color:`#94a3b8`},children:[(0,$.jsx)(`div`,{children:YY(t.createdAt)}),t.generatedAt?(0,$.jsxs)(`div`,{style:{color:`#10b981`},children:[`生成: `,YY(t.generatedAt)]}):null]})},{title:`操作`,key:`action`,width:90,fixed:`right`,render:(e,t)=>(0,$.jsx)(mD,{size:`small`,icon:(0,$.jsx)(AM,{}),onClick:()=>M(t),children:`详情`})}],[M]),z=C?WY[C.genType]||{text:C.genType||`-`,color:`default`,icon:null}:null,B=C?HY[C.status]||{color:`default`,text:C.status||`-`,icon:null}:null,V=()=>{if(!C||C.genType!==`image`||C.status!==`completed`)return null;if(!C.imageUrl)return(0,$.jsx)(rX,{text:`此图片任务暂无结果图片`,minHeight:260});if(T.image===`invalid`)return(0,$.jsx)(rX,{text:nX(C.imageUrl,`图片`),minHeight:260});let e=qY(C.imageUrl);return(0,$.jsxs)(`div`,{style:{position:`relative`,width:`100%`,minHeight:260,borderRadius:12,background:`#f8f9fc`,border:`1px solid #e2e8f0`,overflow:`hidden`,display:`flex`,alignItems:`center`,justifyContent:`center`},children:[(0,$.jsx)(`img`,{src:e,alt:`生成图片`,onLoad:()=>P({image:`valid`}),onError:()=>P({image:`invalid`}),style:{display:T.image===`valid`?`block`:`none`,width:`100%`,maxHeight:560,objectFit:`contain`,background:`#fff`}},`${C.id}-${e}`),T.image===`checking`?(0,$.jsx)(rX,{text:`图片加载检测中...`,minHeight:260}):null]})},H=()=>{if(!C||D||T.video===`invalid`)return null;let e=C.videoUrl?(0,$.jsx)(mD,{type:`primary`,shape:`circle`,size:`large`,icon:(0,$.jsx)(kG,{}),onClick:I,style:{boxShadow:`0 8px 20px rgba(15,23,42,0.25)`}}):null;if(!C.videoCoverUrl)return(0,$.jsx)(`div`,{style:{position:`absolute`,inset:0,borderRadius:12,overflow:`hidden`,background:`#f8f9fc`,display:`flex`,alignItems:`center`,justifyContent:`center`},children:(0,$.jsx)(rX,{text:`此视频无封面`,minHeight:340,action:e})});if(T.videoCover===`invalid`)return(0,$.jsx)(rX,{text:nX(C.videoCoverUrl,`视频封面`),minHeight:340,action:e});let t=qY(C.videoCoverUrl);return(0,$.jsxs)(`div`,{style:{position:`absolute`,inset:0,borderRadius:12,overflow:`hidden`,background:`#f8f9fc`,border:`1px solid #e2e8f0`,display:`flex`,alignItems:`center`,justifyContent:`center`},children:[(0,$.jsx)(`img`,{src:t,alt:`视频封面`,onLoad:()=>P({videoCover:`valid`}),onError:()=>P({videoCover:`invalid`}),style:{display:T.videoCover===`valid`?`block`:`none`,width:`100%`,height:`100%`,objectFit:`contain`,background:`#000`}},`${C.id}-cover-${t}`),T.videoCover===`checking`?(0,$.jsx)(rX,{text:`视频封面加载检测中...`,minHeight:340,action:e}):null,T.videoCover===`valid`?(0,$.jsx)(`div`,{style:{position:`absolute`,inset:0,display:`flex`,alignItems:`center`,justifyContent:`center`,background:`rgba(15,23,42,0.18)`},children:e}):null]})},U=()=>{if(!C||C.genType!==`video`||C.status!==`completed`)return null;if(!C.videoUrl)return(0,$.jsx)(rX,{text:`此视频任务暂无结果视频`,minHeight:340});if(T.video===`invalid`)return(0,$.jsx)(rX,{text:nX(C.videoUrl,`视频`),minHeight:340});let e=qY(C.videoUrl);return(0,$.jsxs)(`div`,{style:{position:`relative`,width:`100%`,height:340,borderRadius:12,background:`#000`,overflow:`hidden`,border:`1px solid #e2e8f0`},children:[(0,$.jsx)(`video`,{ref:k,src:e,preload:`metadata`,controls:D,onLoadedMetadata:()=>P({video:`valid`}),onCanPlay:()=>P({video:`valid`}),onPlaying:()=>{O(!0),P({video:`valid`})},onPause:()=>O(!1),onEnded:()=>O(!1),onError:()=>{O(!1),P({video:`invalid`})},style:{width:`100%`,height:`100%`,objectFit:`contain`,background:`#000`,opacity:+!!D,pointerEvents:D?`auto`:`none`}},`${C.id}-${e}`),H()]})},W=(e,t)=>{let n=$Y(e),r=eX(e),i=tX(e,t),a=T.references[i]||QY(n),o=r===`image`,s=r===`video`,c=typeof e.name==`string`&&e.name?e.name:`参考素材 ${t+1}`;if(!n)return(0,$.jsxs)(`div`,{style:{width:94},children:[(0,$.jsx)(`div`,{style:{width:94,height:94},children:(0,$.jsx)(rX,{text:`无链接`,compact:!0})}),(0,$.jsx)(Q.Text,{ellipsis:!0,style:{display:`block`,marginTop:4,fontSize:11,color:`#94a3b8`},children:c})]},i);if(a===`invalid`)return(0,$.jsxs)(`div`,{style:{width:94},children:[(0,$.jsx)(`div`,{style:{width:94,height:94},children:(0,$.jsx)(rX,{text:KY(n)?`本地临时素材已失效`:`素材不可访问`,compact:!0})}),(0,$.jsx)(Bw,{title:c,children:(0,$.jsx)(Q.Text,{ellipsis:!0,style:{display:`block`,marginTop:4,fontSize:11,color:`#94a3b8`},children:c})})]},i);let l=qY(n);return(0,$.jsxs)(`div`,{style:{width:94},children:[(0,$.jsxs)(`div`,{role:`button`,tabIndex:0,title:`点击新页面查看素材`,onClick:()=>L(n,s?`视频素材`:o?`图片素材`:`素材`),onKeyDown:e=>{e.key===`Enter`&&L(n,s?`视频素材`:o?`图片素材`:`素材`)},style:{width:94,height:94,borderRadius:10,overflow:`hidden`,border:`1px solid #e2e8f0`,background:`#f8f9fc`,position:`relative`,display:`flex`,alignItems:`center`,justifyContent:`center`,cursor:`pointer`},children:[o?(0,$.jsx)(`img`,{src:l,alt:c,onLoad:()=>F(i,`valid`),onError:()=>F(i,`invalid`),style:{display:a===`valid`?`block`:`none`,width:`100%`,height:`100%`,objectFit:`cover`}}):null,s?(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`video`,{src:l,preload:`metadata`,onLoadedMetadata:()=>F(i,`valid`),onError:()=>F(i,`invalid`),style:{display:`none`}}),(0,$.jsxs)(`div`,{style:{color:`#64748b`,fontSize:12,textAlign:`center`},children:[(0,$.jsx)(kG,{style:{fontSize:18}}),(0,$.jsx)(`div`,{children:`视频素材`}),(0,$.jsx)(`div`,{style:{fontSize:10,color:`#94a3b8`,marginTop:2},children:`点击查看`})]})]}):null,!o&&!s?(0,$.jsxs)(`div`,{style:{color:`#64748b`,fontSize:12,textAlign:`center`},children:[`文件素材`,(0,$.jsx)(`div`,{style:{fontSize:10,color:`#94a3b8`,marginTop:2},children:`点击查看`})]}):null,a===`checking`&&o?(0,$.jsx)(`div`,{style:{position:`absolute`,inset:0,display:`flex`,alignItems:`center`,justifyContent:`center`,background:`#f8f9fc`},children:(0,$.jsx)(um,{style:{color:`#6366f1`}})}):null]}),(0,$.jsx)(Bw,{title:c,children:(0,$.jsx)(Q.Text,{ellipsis:!0,style:{display:`block`,marginTop:4,fontSize:11,color:`#94a3b8`},children:c})})]},i)};return(0,$.jsxs)(`div`,{style:{padding:24},children:[(0,$.jsx)(Mk,{title:(0,$.jsxs)(wj,{children:[(0,$.jsx)(aW,{}),(0,$.jsx)(`span`,{children:`创作记录管理`})]}),extra:(0,$.jsxs)(wj,{wrap:!0,children:[(0,$.jsx)(ZC,{allowClear:!0,placeholder:`状态筛选`,value:c||void 0,style:{width:140},onChange:e=>{l(e||``),s(1)},options:[{value:`generating`,label:`生成中`},{value:`completed`,label:`已完成`},{value:`failed`,label:`失败`}]}),(0,$.jsx)(ZC,{allowClear:!0,placeholder:`类型筛选`,value:u||void 0,style:{width:120},onChange:e=>{d(e||``),s(1)},options:[{value:`image`,label:`图片`},{value:`video`,label:`视频`}]}),(0,$.jsx)(QM,{placeholder:`用户ID`,value:f,onChange:e=>p(e.target.value),onPressEnter:j,style:{width:180},allowClear:!0}),(0,$.jsx)(QM,{placeholder:`用户名`,value:m,onChange:e=>h(e.target.value),onPressEnter:j,style:{width:160},allowClear:!0}),(0,$.jsx)(mD,{icon:(0,$.jsx)(KC,{}),onClick:j,children:`搜索`})]}),bordered:!1,style:{borderRadius:16},children:(0,$.jsx)(uB,{rowKey:`id`,loading:i,columns:R,dataSource:e,scroll:{x:1180},pagination:{current:o,pageSize:BY,total:n,showSizeChanger:!1,showTotal:e=>`共 ${e} 条`,onChange:e=>s(e)}})}),(0,$.jsx)(CP,{title:(0,$.jsxs)(wj,{children:[C?.genType===`video`?(0,$.jsx)(cq,{}):(0,$.jsx)(aW,{}),(0,$.jsx)(`span`,{children:`创作记录详情`})]}),open:!!C,onCancel:N,footer:null,width:900,destroyOnClose:!0,children:C?(0,$.jsxs)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:16,marginTop:12},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,gap:12,flexWrap:`wrap`},children:[z?(0,$.jsx)(CB,{color:z.color,icon:z.icon,children:z.text}):null,B?(0,$.jsx)(CB,{color:B.color,icon:B.icon,children:B.text}):null,C.pipelineStage?(0,$.jsx)(CB,{color:`blue`,children:UY[C.pipelineStage]||C.pipelineStage}):null]}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,padding:12,borderRadius:10,background:`#f8f9fc`,flexWrap:`wrap`},children:[(0,$.jsx)(iX,{label:`用户名称`,value:C.userName||`未知用户`}),(0,$.jsx)(iX,{label:`用户ID`,value:C.userId||`-`}),(0,$.jsx)(iX,{label:`任务ID`,value:C.id})]}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#94a3b8`,display:`block`,marginBottom:6},children:`原始提示词`}),(0,$.jsx)(`div`,{style:{padding:12,borderRadius:10,background:`#f8f9fc`,lineHeight:1.6},children:C.originalPrompt||`-`})]}),C.genType===`video`?(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,padding:12,borderRadius:10,background:`#f8f9fc`,flexWrap:`wrap`},children:[(0,$.jsx)(iX,{label:`时长`,value:C.duration?`${C.duration}秒`:`-`}),(0,$.jsx)(iX,{label:`画面比例`,value:C.aspectRatio||`-`}),(0,$.jsx)(iX,{label:`分辨率`,value:C.resolution||`-`})]}):(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,padding:12,borderRadius:10,background:`#f8f9fc`,flexWrap:`wrap`},children:[(0,$.jsx)(iX,{label:`图片档位`,value:C.imageSize||`-`}),(0,$.jsx)(iX,{label:`图片比例`,value:C.imageProportion||`-`}),(0,$.jsx)(iX,{label:`像素尺寸`,value:C.imagePx||`-`})]}),C.engineSnapshot?(0,$.jsxs)(`div`,{style:{padding:12,borderRadius:10,background:`#f8f9fc`},children:[(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#94a3b8`,display:`block`,marginBottom:8},children:`引擎快照`}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,flexWrap:`wrap`},children:[(0,$.jsx)(iX,{label:`引擎名称`,value:C.engineSnapshot.name||`-`}),(0,$.jsx)(iX,{label:`服务商`,value:C.engineSnapshot.provider||`-`}),(0,$.jsx)(iX,{label:`模型`,value:C.engineSnapshot.modelName||`-`}),(0,$.jsx)(iX,{label:`引擎ID`,value:C.engineSnapshot.id||C.engineId||`-`})]})]}):null,(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,padding:12,borderRadius:10,background:`#f8f9fc`,flexWrap:`wrap`},children:[(0,$.jsx)(iX,{label:`总积分`,value:(0,$.jsx)(Q.Text,{strong:!0,style:{color:`#6366f1`},children:C.creditsCost||0})}),(0,$.jsx)(iX,{label:`文字积分`,value:`${C.textCreditsCost||0} (${C.textTokensUsed||0} tokens)`}),(0,$.jsx)(iX,{label:`图片 tokens`,value:C.imageTokensUsed??0}),(0,$.jsx)(iX,{label:`视频 tokens`,value:C.videoTokensUsed??0})]}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,padding:12,borderRadius:10,background:`#f8f9fc`,flexWrap:`wrap`},children:[(0,$.jsx)(iX,{label:`第三方任务ID`,value:C.providerTaskId||C.seedanceTaskId||`-`}),(0,$.jsx)(iX,{label:`轮询次数`,value:C.pollCount??0}),(0,$.jsx)(iX,{label:`重试次数`,value:C.retryCount??0})]}),!C?.mediaReferences||C.mediaReferences.length===0?null:(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#94a3b8`,display:`block`,marginBottom:6},children:`参考内容`}),(0,$.jsx)(`div`,{style:{display:`flex`,gap:10,flexWrap:`wrap`},children:C.mediaReferences.map((e,t)=>W(e,t))})]}),C.status===`completed`?(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#94a3b8`,display:`block`,marginBottom:6},children:C.genType===`video`?`生成视频`:`生成图片`}),C.genType===`video`?U():V()]}):null,C.status===`failed`&&C.errorMessage?(0,$.jsx)(`div`,{style:{padding:12,borderRadius:10,background:`rgba(239,68,68,0.04)`,border:`1px solid rgba(239,68,68,0.15)`},children:(0,$.jsxs)(Q.Text,{style:{fontSize:12,color:`#ef4444`},children:[`错误信息: `,C.errorMessage]})}):null,(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,fontSize:12,color:`#94a3b8`,flexWrap:`wrap`},children:[(0,$.jsxs)(`span`,{children:[`创建: `,YY(C.createdAt)]}),(0,$.jsxs)(`span`,{children:[`生成: `,YY(C.generatedAt)]})]})]}):(0,$.jsx)(xC,{description:`暂无详情`})})]})},oX=({children:e})=>{let{user:t,loading:n,checkAuth:r}=MJ();return(0,x.useEffect)(()=>{!localStorage.getItem(`auth_token`)&&!n&&!t&&(window.location.href=`/login`)},[t,n]),n?(0,$.jsx)(`div`,{style:{display:`flex`,justifyContent:`center`,alignItems:`center`,height:`100vh`},children:(0,$.jsx)(aP,{size:`large`})}):t?(0,$.jsx)($.Fragment,{children:e}):(window.location.href=`/login`,null)};(0,Em.createRoot)(document.getElementById(`root`)).render((0,$.jsx)(()=>{let{checkAuth:e}=MJ();return(0,x.useEffect)(()=>{e()},[]),(0,$.jsx)(Kp,{locale:xq.default,theme:{token:{colorPrimary:`#6366f1`,borderRadius:8},components:{Button:{controlHeight:36,controlHeightLG:44},Card:{boxShadow:`0 1px 3px rgba(0,0,0,0.04)`},Table:{headerBg:`#fafbfc`}}},children:(0,$.jsx)(fx,{children:(0,$.jsx)(mn,{children:(0,$.jsxs)(Et,{children:[(0,$.jsx)(wt,{path:`/login`,element:(0,$.jsx)(RJ,{})}),(0,$.jsxs)(wt,{path:`/`,element:(0,$.jsx)(oX,{children:(0,$.jsx)(LJ,{})}),children:[(0,$.jsx)(wt,{index:!0,element:(0,$.jsx)(zJ,{})}),(0,$.jsx)(wt,{path:`users`,element:(0,$.jsx)(VJ,{})}),(0,$.jsx)(wt,{path:`credit-records`,element:(0,$.jsx)(KJ,{})}),(0,$.jsx)(wt,{path:`models`,element:(0,$.jsx)(HJ,{})}),(0,$.jsx)(wt,{path:`credit-ratios`,element:(0,$.jsx)(lY,{})}),(0,$.jsx)(wt,{path:`video-engines`,element:(0,$.jsx)(tY,{})}),(0,$.jsx)(wt,{path:`image-engines`,element:(0,$.jsx)(oY,{})}),(0,$.jsx)(wt,{path:`industries`,element:(0,$.jsx)($J,{})}),(0,$.jsx)(wt,{path:`menu-configs`,element:(0,$.jsx)(mY,{})}),(0,$.jsx)(wt,{path:`recharge-packages`,element:(0,$.jsx)(_Y,{})}),(0,$.jsx)(wt,{path:`payment`,element:(0,$.jsx)(qJ,{})}),(0,$.jsx)(wt,{path:`settings`,element:(0,$.jsx)(UJ,{})}),(0,$.jsx)(wt,{path:`notifications`,element:(0,$.jsx)(WJ,{})}),(0,$.jsx)(wt,{path:`operation-logs`,element:(0,$.jsx)(yY,{})}),(0,$.jsx)(wt,{path:`generation-records`,element:(0,$.jsx)(RY,{})}),(0,$.jsx)(wt,{path:`generation-ai`,element:(0,$.jsx)(aX,{})})]}),(0,$.jsx)(wt,{path:`*`,element:(0,$.jsx)(St,{to:`/`,replace:!0})})]})})})})},{})); \ No newline at end of file +你是一位专业的电商图片文案专家,擅长将产品卖点转化为图片生成提示词`,size:`large`})}),(0,$.jsxs)(`div`,{style:{marginBottom:8},children:[(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:13},children:`行业选项配置`}),(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#94a3b8`,marginLeft:8},children:`添加选项组,每组包含名称和多个选项,前台将显示为下拉选择`})]}),(0,$.jsx)(Z.List,{name:`optionGroups`,children:(e,{add:t,remove:n})=>(0,$.jsxs)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:8,marginBottom:16},children:[e.map(({key:e,name:t,...r})=>(0,$.jsxs)(`div`,{style:{display:`flex`,gap:8,alignItems:`flex-start`,padding:`10px 12px`,borderRadius:10,background:`#f8f9fc`,border:`1px solid #f0f0f5`},children:[(0,$.jsxs)(`div`,{style:{flex:1,display:`flex`,flexDirection:`column`,gap:8},children:[(0,$.jsx)(Z.Item,{...r,name:[t,`name`],label:`选项名称`,style:{marginBottom:0},rules:[{required:!0,message:`请输入选项名称`}],children:(0,$.jsx)(QM,{placeholder:`例如:视频风格`,size:`middle`,style:{borderRadius:8}})}),(0,$.jsx)(Z.Item,{...r,name:[t,`options`],label:`选项内容`,style:{marginBottom:0},children:(0,$.jsx)(ZC,{mode:`tags`,size:`middle`,placeholder:`输入选项后回车添加`,style:{borderRadius:8},tokenSeparators:[`,`,`,`,`、`]})})]}),(0,$.jsx)(cG,{onClick:()=>n(t),style:{color:`#ef4444`,fontSize:16,marginTop:34,cursor:`pointer`,flexShrink:0}})]},e)),(0,$.jsx)(mD,{type:`dashed`,onClick:()=>t(),block:!0,icon:(0,$.jsx)(_O,{}),style:{borderRadius:8,height:36},children:`添加选项组`})]})}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`is_active`,label:`启用状态`,valuePropName:`checked`,initialValue:!0,style:{flex:1},children:(0,$.jsx)(yF,{})}),(0,$.jsx)(Z.Item,{name:`sort_order`,label:`排序`,initialValue:0,style:{flex:1},children:(0,$.jsx)(QM,{type:`number`,size:`large`})})]})]})})]})};function eY(e){if(Array.isArray(e))return e;if(typeof e==`string`)try{return JSON.parse(e)}catch{return[]}return[]}var tY=()=>{let[e,t]=(0,x.useState)([]),[n,r]=(0,x.useState)(!1),[i,a]=(0,x.useState)({open:!1,engine:null}),[o]=Z.useForm(),s=async()=>{r(!0);try{t((await tJ()).map(e=>({...e,supportedRatios:eY(e.supportedRatios),supportedResolutions:eY(e.supportedResolutions),supportedDurations:eY(e.supportedDurations)})))}catch{bP.error(`加载视频引擎失败`)}finally{r(!1)}};(0,x.useEffect)(()=>{s()},[]);let c=async()=>{try{let e=await o.validateFields(),t={name:e.name,provider:e.provider,api_base:e.apiBase,api_key:e.apiKey,model_name:e.modelName,supported_ratios:JSON.stringify(e.supportedRatios||[]),supported_resolutions:JSON.stringify(e.supportedResolutions||[]),supported_durations:JSON.stringify(e.supportedDurations||[]),is_active:e.isActive??!0,priority:e.priority??0};i.engine?(await nJ({id:i.engine.id,...t}),bP.success(`已更新`)):(await nJ(t),bP.success(`已添加`)),a({open:!1,engine:null}),o.resetFields(),s()}catch(e){if(e?.errorFields)return;bP.error(e?.message||`保存失败`)}},l=async e=>{try{await rJ(e),bP.success(`已删除`),s()}catch{bP.error(`删除失败`)}},u=e=>{a({open:!0,engine:e||null}),e?o.setFieldsValue(e):(o.resetFields(),o.setFieldsValue({isActive:!0,priority:0,supportedRatios:[`16:9`,`4:3`,`1:1`,`3:4`,`9:16`,`21:9`],supportedResolutions:[`480p`,`720p`,`1080p`],supportedDurations:[4,5,6,7,8,9,10,11,12,13,14,15]}))};return(0,$.jsxs)(`div`,{children:[(0,$.jsxs)(Mk,{bordered:!1,style:{borderRadius:12,border:`1px solid #f0f0f5`},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,marginBottom:16},children:[(0,$.jsxs)(wj,{children:[(0,$.jsx)(kG,{style:{fontSize:18,color:`#6366f1`}}),(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:16},children:`视频引擎配置`}),(0,$.jsxs)(CB,{color:`purple`,children:[e.length,` 个引擎`]})]}),(0,$.jsx)(mD,{type:`primary`,icon:(0,$.jsx)(_O,{}),onClick:()=>u(),style:{borderRadius:8},children:`添加引擎`})]}),(0,$.jsx)(uB,{columns:[{title:`引擎名称`,key:`name`,width:180,render:(e,t)=>(0,$.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:10},children:[(0,$.jsx)(`div`,{style:{width:36,height:36,borderRadius:8,background:t.isActive?`linear-gradient(135deg, #6366f1, #8b5cf6)`:`linear-gradient(135deg, #94a3b8, #cbd5e1)`,display:`flex`,alignItems:`center`,justifyContent:`center`,color:`#fff`,fontSize:16},children:(0,$.jsx)(kG,{})}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{strong:!0,children:t.name}),(0,$.jsx)(`div`,{style:{color:`#94a3b8`,fontSize:12},children:t.provider})]})]})},{title:`支持比例`,dataIndex:`supportedRatios`,width:200,render:e=>(0,$.jsx)(wj,{size:2,wrap:!0,children:e.map(e=>(0,$.jsx)(CB,{children:e},e))})},{title:`支持分辨率`,dataIndex:`supportedResolutions`,width:150,render:e=>(0,$.jsx)(wj,{size:2,wrap:!0,children:e.map(e=>(0,$.jsx)(CB,{color:`blue`,children:e},e))})},{title:`支持时长`,dataIndex:`supportedDurations`,width:120,render:e=>(0,$.jsx)(CB,{color:`orange`,children:e?.length?`${Math.min(...e)}-${Math.max(...e)}s`:`-`})},{title:`状态`,dataIndex:`isActive`,width:80,render:e=>(0,$.jsx)(CB,{color:e?`green`:`default`,children:e?`启用`:`停用`})},{title:`操作`,key:`action`,width:150,fixed:`right`,render:(e,t)=>(0,$.jsxs)(wj,{size:4,children:[(0,$.jsx)(mD,{type:`link`,size:`small`,icon:(0,$.jsx)(kB,{}),onClick:()=>u(t),children:`编辑`}),(0,$.jsx)(DP,{title:`确定删除?`,onConfirm:()=>l(t.id),children:(0,$.jsx)(mD,{type:`link`,size:`small`,danger:!0,icon:(0,$.jsx)(EB,{}),children:`删除`})})]})}],dataSource:e,rowKey:`id`,loading:n,pagination:!1,scroll:{x:900}})]}),(0,$.jsx)(CP,{title:(0,$.jsxs)(wj,{children:[(0,$.jsx)(kG,{}),i.engine?`编辑引擎`:`添加引擎`]}),open:i.open,onOk:c,onCancel:()=>{a({open:!1,engine:null}),o.resetFields()},okText:`保存`,cancelText:`取消`,width:620,children:(0,$.jsxs)(Z,{form:o,layout:`vertical`,style:{marginTop:16},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`name`,label:`引擎名称`,style:{flex:1},rules:[{required:!0}],children:(0,$.jsx)(QM,{placeholder:`Seedance 2.0`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`provider`,label:`提供商`,style:{flex:1},rules:[{required:!0}],children:(0,$.jsx)(ZC,{size:`large`,options:[{value:`ark`,label:`火山引擎 (Ark)`}]})})]}),(0,$.jsx)(Z.Item,{name:`apiBase`,label:`API基础地址`,rules:[{required:!0}],children:(0,$.jsx)(QM,{placeholder:`https://ark.cn-beijing.volces.com/api/v3`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`apiKey`,label:`API Key`,children:(0,$.jsx)(QM.Password,{placeholder:`sk-****`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`modelName`,label:`模型名称`,children:(0,$.jsx)(QM,{placeholder:`doubao-seedance-2-0-260128`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`supportedRatios`,label:`支持比例`,children:(0,$.jsx)(ZC,{mode:`multiple`,size:`large`,options:[{value:`16:9`,label:`16:9 (横屏)`},{value:`4:3`,label:`4:3 (标准)`},{value:`1:1`,label:`1:1 (方形)`},{value:`3:4`,label:`3:4 (竖版)`},{value:`9:16`,label:`9:16 (竖屏)`},{value:`21:9`,label:`21:9 (超宽)`}]})}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`supportedResolutions`,label:`支持分辨率`,style:{flex:1},children:(0,$.jsx)(ZC,{mode:`multiple`,size:`large`,options:[{value:`480p`},{value:`720p`},{value:`1080p`}]})}),(0,$.jsx)(Z.Item,{name:`supportedDurations`,label:`支持时长(秒)`,style:{flex:1},children:(0,$.jsx)(ZC,{mode:`multiple`,size:`large`,options:Array.from({length:12},(e,t)=>({value:t+4,label:`${t+4}秒`}))})})]}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`priority`,label:`优先级`,children:(0,$.jsx)(ZC,{size:`large`,options:[{value:0,label:`0 (默认)`},{value:1,label:`1`},{value:2,label:`2`},{value:3,label:`3`},{value:5,label:`5`},{value:10,label:`10 (最高)`}]})}),(0,$.jsx)(Z.Item,{name:`isActive`,label:`启用状态`,valuePropName:`checked`,style:{paddingTop:30},children:(0,$.jsx)(yF,{})})]})]})})]})};function nY(e){if(Array.isArray(e))return e;if(typeof e==`string`)try{return JSON.parse(e)}catch{return[]}return[]}function rY(e){if(e&&typeof e==`object`&&!Array.isArray(e))return e;if(typeof e==`string`)try{let t=JSON.parse(e);return t&&typeof t==`object`&&!Array.isArray(t)?t:{}}catch{return{}}return{}}var iY={"2K":{"1:1":`2048×2048`,"4:3":`2304×1728`,"3:4":`1728×2304`,"16:9":`2560×1440`,"9:16":`1600×2848`,"3:2":`2496×1664`,"2:3":`1664×2496`,"21:9":`3024×1296`},"4K":{"1:1":`4096×4096`,"4:3":`4608×3456`,"3:4":`3520×4704`,"16:9":`5404×3040`,"9:16":`3040×5504`,"3:2":`4992×3328`,"2:3":`3328×4992`,"21:9":`6197×2656`}},aY=[`1:1`,`4:3`,`3:4`,`16:9`,`9:16`,`3:2`,`2:3`,`21:9`],oY=()=>{let[e,t]=(0,x.useState)([]),[n,r]=(0,x.useState)(!1),[i,a]=(0,x.useState)({open:!1,engine:null}),[o]=Z.useForm(),s=async()=>{r(!0);try{t((await iJ()).map(e=>({...e,supportedModels:nY(e.supportedModels),supportedSizes:rY(e.supportedSizes)})))}catch{bP.error(`加载图片引擎失败`)}finally{r(!1)}};(0,x.useEffect)(()=>{s()},[]);let c=async()=>{try{let e=await o.validateFields(),t={};for(let n of[`2K`,`4K`]){let r=e[`size_${n}`]||[];if(r.length>0){t[n]={};for(let e of r)t[n][e]=iY[n]?.[e]||e}}let n={name:e.name,provider:e.provider,api_base:e.apiBase,api_key:e.apiKey,model_name:e.modelName,supported_models:JSON.stringify(e.supportedModels||[]),supported_sizes:JSON.stringify(t),default_size:e.defaultSize||`2K`,generate_url:e.generateUrl||``,is_active:e.isActive??!0,priority:e.priority??0};i.engine?(await aJ({id:i.engine.id,...n}),bP.success(`已更新`)):(await aJ(n),bP.success(`已添加`)),a({open:!1,engine:null}),o.resetFields(),s()}catch(e){if(e?.errorFields)return;bP.error(e?.message||`保存失败`)}},l=async e=>{try{await oJ(e),bP.success(`已删除`),s()}catch{bP.error(`删除失败`)}},u=e=>{if(a({open:!0,engine:e||null}),e){let t={};for(let n of[`2K`,`4K`])t[`size_${n}`]=Object.keys(e.supportedSizes?.[n]||{});o.setFieldsValue({...e,...t})}else o.resetFields(),o.setFieldsValue({isActive:!0,priority:0,supportedModels:[`doubao-seedream-5-0-260128`],defaultSize:`2K`,size_2K:aY,size_4K:aY})};return(0,$.jsxs)(`div`,{children:[(0,$.jsxs)(Mk,{style:{borderRadius:12,border:`1px solid #f0f0f5`},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,marginBottom:16},children:[(0,$.jsxs)(wj,{children:[(0,$.jsx)(CG,{style:{fontSize:18,color:`#10b981`}}),(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:16},children:`图片引擎配置`}),(0,$.jsxs)(CB,{color:`green`,children:[e.length,` 个引擎`]})]}),(0,$.jsx)(mD,{type:`primary`,icon:(0,$.jsx)(_O,{}),onClick:()=>u(),style:{borderRadius:8},children:`添加引擎`})]}),(0,$.jsx)(uB,{columns:[{title:`引擎名称`,key:`name`,width:180,render:(e,t)=>(0,$.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:10},children:[(0,$.jsx)(`div`,{style:{width:36,height:36,borderRadius:8,background:t.isActive?`linear-gradient(135deg, #10b981, #059669)`:`linear-gradient(135deg, #94a3b8, #cbd5e1)`,display:`flex`,alignItems:`center`,justifyContent:`center`,color:`#fff`,fontSize:16},children:(0,$.jsx)(CG,{})}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{strong:!0,children:t.name}),(0,$.jsx)(`div`,{style:{color:`#94a3b8`,fontSize:12},children:t.modelName})]})]})},{title:`2K 支持比例`,key:`sizes_2k`,width:260,render:(e,t)=>{let n=Object.keys(t.supportedSizes?.[`2K`]||{});return n.length===0?(0,$.jsx)(`span`,{style:{color:`#bfbfbf`},children:`-`}):(0,$.jsx)(wj,{size:2,wrap:!0,children:n.map(e=>(0,$.jsxs)(CB,{color:`blue`,children:[e,` `,t.supportedSizes[`2K`][e]]},e))})}},{title:`4K 支持比例`,key:`sizes_4k`,width:260,render:(e,t)=>{let n=Object.keys(t.supportedSizes?.[`4K`]||{});return n.length===0?(0,$.jsx)(`span`,{style:{color:`#bfbfbf`},children:`-`}):(0,$.jsx)(wj,{size:2,wrap:!0,children:n.map(e=>(0,$.jsxs)(CB,{color:`purple`,children:[e,` `,t.supportedSizes[`4K`][e]]},e))})}},{title:`状态`,dataIndex:`isActive`,width:80,render:e=>(0,$.jsx)(CB,{color:e?`green`:`default`,children:e?`启用`:`停用`})},{title:`操作`,key:`action`,width:150,fixed:`right`,render:(e,t)=>(0,$.jsxs)(wj,{size:4,children:[(0,$.jsx)(mD,{type:`link`,size:`small`,icon:(0,$.jsx)(kB,{}),onClick:()=>u(t),children:`编辑`}),(0,$.jsx)(DP,{title:`确定删除?`,onConfirm:()=>l(t.id),children:(0,$.jsx)(mD,{type:`link`,size:`small`,danger:!0,icon:(0,$.jsx)(EB,{}),children:`删除`})})]})}],dataSource:e,rowKey:`id`,loading:n,pagination:!1,scroll:{x:1e3}})]}),(0,$.jsx)(CP,{title:(0,$.jsxs)(wj,{children:[(0,$.jsx)(CG,{}),i.engine?`编辑引擎`:`添加引擎`]}),open:i.open,onOk:c,onCancel:()=>{a({open:!1,engine:null}),o.resetFields()},okText:`保存`,cancelText:`取消`,width:680,children:(0,$.jsxs)(Z,{form:o,layout:`vertical`,style:{marginTop:16},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`name`,label:`引擎名称`,style:{flex:1},rules:[{required:!0}],children:(0,$.jsx)(QM,{placeholder:`豆包文生图`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`provider`,label:`提供商`,style:{flex:1},rules:[{required:!0}],children:(0,$.jsx)(ZC,{size:`large`,options:[{value:`ark`,label:`火山引擎 (Ark)`}]})})]}),(0,$.jsx)(Z.Item,{name:`apiBase`,label:`API基础地址`,rules:[{required:!0}],children:(0,$.jsx)(QM,{placeholder:`https://ark.cn-beijing.volces.com/api/v3`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`apiKey`,label:`API Key`,children:(0,$.jsx)(QM.Password,{placeholder:`sk-****`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`modelName`,label:`默认模型`,children:(0,$.jsx)(QM,{placeholder:`doubao-seedream-5-0-260128`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`supportedModels`,label:`支持模型列表`,children:(0,$.jsx)(ZC,{mode:`tags`,size:`large`,placeholder:`输入模型ID后回车添加`,tokenSeparators:[`,`,`,`],options:[{value:`doubao-seedream-5-0-260128`}]})}),(0,$.jsxs)(`div`,{style:{background:`#f8f9fc`,borderRadius:10,padding:16,marginBottom:8},children:[(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:14},children:`尺寸配置`}),(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#94a3b8`,marginLeft:8},children:`勾选每个档位支持的比例,前台选择后传对应像素值给SDK`})]}),[`2K`,`4K`].map(e=>(0,$.jsxs)(`div`,{style:{background:`#fafbfc`,borderRadius:10,padding:`12px 16px`,marginBottom:12,border:`1px solid #f0f0f5`},children:[(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:13,color:e===`2K`?`#3b82f6`:`#8b5cf6`},children:e}),(0,$.jsx)(Z.Item,{name:`size_${e}`,style:{marginTop:8,marginBottom:0},children:(0,$.jsx)(iA.Group,{style:{width:`100%`},children:(0,$.jsx)(`div`,{style:{display:`grid`,gridTemplateColumns:`repeat(4, 1fr)`,gap:`6px 0`},children:aY.map(t=>(0,$.jsxs)(iA,{value:t,style:{fontSize:12},children:[t,` `,(0,$.jsx)(`span`,{style:{color:`#94a3b8`,fontSize:11},children:iY[e]?.[t]})]},t))})})})]},e)),(0,$.jsx)(Z.Item,{name:`defaultSize`,label:`默认尺寸档位`,children:(0,$.jsx)(ZC,{size:`large`,options:[{value:`2K`,label:`2K`},{value:`4K`,label:`4K`}]})}),(0,$.jsx)(Z.Item,{name:`generateUrl`,label:`生成接口地址`,children:(0,$.jsx)(QM,{placeholder:`https://ark.cn-beijing.volces.com/api/v3/images/generations`,size:`large`})}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`priority`,label:`优先级`,children:(0,$.jsx)(ZC,{size:`large`,options:[{value:0,label:`0 (默认)`},{value:1,label:`1`},{value:2,label:`2`},{value:3,label:`3`},{value:5,label:`5`},{value:10,label:`10 (最高)`}]})}),(0,$.jsx)(Z.Item,{name:`isActive`,label:`启用状态`,valuePropName:`checked`,style:{paddingTop:30},children:(0,$.jsx)(yF,{})})]})]})})]})},sY=[`2K`,`4K`],cY=[`480p`,`720p`,`1080p`],lY=()=>{let[e,t]=(0,x.useState)([]),[n,r]=(0,x.useState)([]),[i,a]=(0,x.useState)(!1),[o,s]=(0,x.useState)({open:!1,ratio:null}),[c]=Z.useForm(),[l,u]=(0,x.useState)(10),[d,f]=(0,x.useState)(null),[p,m]=(0,x.useState)(!1),h=Z.useWatch(`genType`,c)||`video`,g=Z.useWatch(`modelConfigId`,c),_=async()=>{a(!0);try{let[e,n,i]=await Promise.all([sJ(),Jq(),AJ()]),a=(i?.engine?.image||[]).map(e=>({...e,genType:`image`})),o=(i?.engine?.video||[]).map(e=>({...e,genType:`video`}));t(e),r([...o,...a]);let s=n.find(e=>e.key===`text_credits_per_1000_tokens`);s&&(u(Number(s.value)||10),f({id:s.id}))}catch{bP.error(`加载积分比例失败`)}finally{a(!1)}};(0,x.useEffect)(()=>{_()},[]);let v=(0,x.useMemo)(()=>n.filter(e=>e.genType===h).map(e=>({value:e.id,label:`${e.name}${e.modelName?`(${e.modelName})`:``}`})),[n,h]),y=(0,x.useMemo)(()=>n.find(e=>e.id===g&&e.genType===h),[n,h,g]),b=(0,x.useMemo)(()=>{if(h===`image`){let e=y?.supportedSizes?Object.keys(y.supportedSizes):[];return(e.length?e:sY).map(e=>({value:e,label:e}))}let e=y?.supportedResolutions||[];return(e.length?e:cY).map(e=>({value:e,label:e}))},[h,y]),S=(e,t)=>n.find(n=>n.id===e&&(!t||n.genType===t))?.name||e,C=async()=>{try{let e=await c.validateFields(),t={model_config_id:e.modelConfigId,gen_type:e.genType,resolution:e.resolution,ratio:e.ratio,base_credits:e.baseCredits,per_second_credits:e.genType===`image`?0:e.perSecondCredits||0};o.ratio?(await cJ({id:o.ratio.id,...t}),bP.success(`已更新`)):(await cJ(t),bP.success(`已添加`)),s({open:!1,ratio:null}),c.resetFields(),_()}catch(e){if(e?.errorFields)return;bP.error(e?.message||`保存失败`)}},w=async e=>{try{await lJ(e),bP.success(`已删除`),_()}catch{bP.error(`删除失败`)}},T=async()=>{if(d){m(!0);try{await Yq(d.id,String(l)),bP.success(`文字积分费率已更新`)}catch(e){bP.error(e?.message||`保存失败`)}finally{m(!1)}}},E=e=>{s({open:!0,ratio:e||null}),e?c.setFieldsValue({modelConfigId:e.modelConfigId,genType:e.genType===`image`?`image`:`video`,resolution:e.resolution,ratio:e.ratio,baseCredits:e.baseCredits,perSecondCredits:e.perSecondCredits}):(c.resetFields(),c.setFieldsValue({genType:`video`,ratio:1,baseCredits:60,perSecondCredits:2}))};return(0,$.jsxs)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:16},children:[(0,$.jsxs)(Mk,{bordered:!1,style:{borderRadius:12,border:`1px solid #f0f0f5`},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,alignItems:`center`,marginBottom:16},children:[(0,$.jsxs)(wj,{children:[(0,$.jsx)(_W,{style:{fontSize:18,color:`#f59e0b`}}),(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:16},children:`文字积分费率`})]}),(0,$.jsx)(mD,{type:`primary`,loading:p,onClick:T,style:{borderRadius:8},children:`保存`})]}),(0,$.jsx)(Q.Text,{type:`secondary`,style:{display:`block`,marginBottom:16,fontSize:13},children:`文字积分计算公式:ceil(总token数 x 费率 / 1000),最低1积分`}),(0,$.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:16},children:[(0,$.jsx)(Q.Text,{children:`每1000 token消耗积分:`}),(0,$.jsx)($A,{min:0,max:1e3,step:.01,value:l,onChange:e=>u(e||0),size:`large`,style:{width:160},addonAfter:`积分`}),(0,$.jsxs)(Q.Text,{type:`secondary`,style:{fontSize:12},children:[`示例:1000 token = `,l,` 积分,500 token = `,(500*l/1e3).toFixed(4),` 积分`]})]})]}),(0,$.jsxs)(Mk,{bordered:!1,style:{borderRadius:12,border:`1px solid #f0f0f5`},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,marginBottom:16},children:[(0,$.jsxs)(wj,{children:[(0,$.jsx)(dU,{style:{fontSize:18,color:`#6366f1`}}),(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:16},children:`积分比例配置`}),(0,$.jsxs)(CB,{color:`purple`,children:[e.length,` 条规则`]})]}),(0,$.jsx)(mD,{type:`primary`,icon:(0,$.jsx)(_O,{}),onClick:()=>E(),style:{borderRadius:8},children:`添加比例`})]}),(0,$.jsx)(Q.Text,{type:`secondary`,style:{display:`block`,marginBottom:16,fontSize:13},children:`视频积分公式:(基础积分 + 每秒积分 x 视频时长) x 模型倍率;图片积分公式:基础积分 x 模型倍率`}),(0,$.jsx)(uB,{columns:[{title:`类型`,dataIndex:`genType`,width:80,render:e=>(0,$.jsx)(CB,{color:e===`image`?`cyan`:`orange`,children:e===`image`?`图片`:`视频`})},{title:`引擎`,dataIndex:`modelConfigId`,width:180,render:(e,t)=>(0,$.jsx)(CB,{color:t.genType===`image`?`cyan`:`purple`,children:S(e,t.genType)})},{title:`分辨率/尺寸`,dataIndex:`resolution`,width:110,render:e=>(0,$.jsx)(CB,{color:{"480p":`blue`,"720p":`blue`,"1080p":`blue`,"4K":`green`,"2K":`green`}[e]||`default`,children:e})},{title:`倍率`,dataIndex:`ratio`,width:100,sorter:(e,t)=>e.ratio-t.ratio,render:e=>(0,$.jsxs)(Q.Text,{strong:!0,style:{color:e>=2?`#ef4444`:e>=1.5?`#f59e0b`:`#10b981`},children:[`x`,e]})},{title:`基础积分`,dataIndex:`baseCredits`,width:100,render:e=>(0,$.jsxs)(Q.Text,{children:[e,` 积分`]})},{title:`每秒积分`,dataIndex:`perSecondCredits`,width:100,render:(e,t)=>(0,$.jsx)(Q.Text,{children:t.genType===`image`?`-`:`${e} 积分/秒`})},{title:`示例计算`,key:`example`,width:120,render:(e,t)=>{let n;return n=t.genType===`image`?Math.round(t.baseCredits*t.ratio):Math.round((t.baseCredits+t.perSecondCredits*15)*t.ratio),(0,$.jsxs)(Q.Text,{strong:!0,style:{color:`#6366f1`},children:[n,` 积分`]})}},{title:`操作`,key:`action`,width:150,fixed:`right`,render:(e,t)=>(0,$.jsxs)(wj,{size:4,children:[(0,$.jsx)(mD,{type:`link`,size:`small`,icon:(0,$.jsx)(kB,{}),onClick:()=>E(t),children:`编辑`}),(0,$.jsx)(DP,{title:`确定删除?`,onConfirm:()=>w(t.id),children:(0,$.jsx)(mD,{type:`link`,size:`small`,danger:!0,icon:(0,$.jsx)(EB,{}),children:`删除`})})]})}],dataSource:e,rowKey:`id`,loading:i,pagination:!1,scroll:{x:860}})]}),(0,$.jsx)(CP,{title:(0,$.jsxs)(wj,{children:[(0,$.jsx)(dU,{}),o.ratio?`编辑比例`:`添加比例`]}),open:o.open,onOk:C,onCancel:()=>{s({open:!1,ratio:null}),c.resetFields()},okText:`保存`,cancelText:`取消`,width:520,children:(0,$.jsxs)(Z,{form:c,layout:`vertical`,style:{marginTop:16},children:[(0,$.jsx)(Z.Item,{name:`genType`,label:`生成类型`,rules:[{required:!0,message:`请选择生成类型`}],children:(0,$.jsx)(ZC,{size:`large`,options:[{value:`video`,label:`视频`},{value:`image`,label:`图片`}],onChange:()=>{c.setFieldsValue({modelConfigId:void 0,resolution:void 0})}})}),(0,$.jsx)(Z.Item,{name:`modelConfigId`,label:`引擎`,rules:[{required:!0,message:`请选择引擎`}],children:(0,$.jsx)(ZC,{size:`large`,placeholder:`请选择引擎`,options:v,showSearch:!0,optionFilterProp:`label`,onChange:()=>c.setFieldsValue({resolution:void 0})})}),(0,$.jsx)(Z.Item,{name:`resolution`,label:h===`image`?`图片尺寸`:`分辨率`,rules:[{required:!0,message:h===`image`?`请选择图片尺寸`:`请选择分辨率`}],children:(0,$.jsx)(ZC,{size:`large`,placeholder:h===`image`?`请选择图片尺寸`:`请选择分辨率`,options:b})}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`ratio`,label:`倍率`,style:{flex:1},rules:[{required:!0,message:`请输入倍率`}],children:(0,$.jsx)($A,{min:.1,max:10,step:.1,style:{width:`100%`},size:`large`})}),(0,$.jsx)(Z.Item,{name:`baseCredits`,label:`基础积分`,style:{flex:1},rules:[{required:!0,message:`请输入基础积分`}],children:(0,$.jsx)($A,{min:0,max:1e3,style:{width:`100%`},size:`large`})}),h!==`image`&&(0,$.jsx)(Z.Item,{name:`perSecondCredits`,label:`每秒积分`,style:{flex:1},rules:[{required:!0,message:`请输入每秒积分`}],children:(0,$.jsx)($A,{min:0,max:100,style:{width:`100%`},size:`large`})})]})]})})]})},uY={HomeOutlined:(0,$.jsx)(FW,{}),PlayCircleOutlined:(0,$.jsx)(kG,{}),WalletOutlined:(0,$.jsx)(dq,{}),RobotOutlined:(0,$.jsx)(VG,{}),SettingOutlined:(0,$.jsx)(cK,{}),BellOutlined:(0,$.jsx)($H,{}),UserOutlined:(0,$.jsx)(aq,{}),AppstoreOutlined:(0,$.jsx)(FH,{}),FileTextOutlined:(0,$.jsx)(kj,{}),StarOutlined:(0,$.jsx)(MK,{}),HeartOutlined:(0,$.jsx)(EW,{}),CameraOutlined:(0,$.jsx)(mU,{}),DashboardOutlined:(0,$.jsx)(WU,{}),CalculatorOutlined:(0,$.jsx)(dU,{}),DollarOutlined:(0,$.jsx)(XU,{}),GiftOutlined:(0,$.jsx)(bW,{}),ThunderboltOutlined:(0,$.jsx)(WK,{}),FireOutlined:(0,$.jsx)(dW,{}),CloudOutlined:(0,$.jsx)(EU,{}),SmileOutlined:(0,$.jsx)(CK,{}),TrophyOutlined:(0,$.jsx)(XK,{}),RocketOutlined:(0,$.jsx)(WG,{}),BulbOutlined:(0,$.jsx)(cU,{}),CodeOutlined:(0,$.jsx)(MU,{}),PictureOutlined:(0,$.jsx)(CG,{}),VideoCameraOutlined:(0,$.jsx)(cq,{}),AudioOutlined:(0,$.jsx)(WH,{}),MailOutlined:(0,$.jsx)($W,{}),PhoneOutlined:(0,$.jsx)(bG,{}),GlobalOutlined:(0,$.jsx)(CW,{}),ShoppingCartOutlined:(0,$.jsx)(dK,{}),TeamOutlined:(0,$.jsx)(VK,{}),BarChartOutlined:(0,$.jsx)(XH,{}),PieChartOutlined:(0,$.jsx)(EG,{}),LineChartOutlined:(0,$.jsx)(WW,{}),SecurityScanOutlined:(0,$.jsx)(nK,{}),ApiOutlined:(0,$.jsx)(MH,{}),DatabaseOutlined:(0,$.jsx)(qU,{}),CloudServerOutlined:(0,$.jsx)(kU,{}),MenuOutlined:(0,$.jsx)(aG,{})},dY=Object.keys(uY).map(e=>({value:e,label:(0,$.jsxs)(`span`,{style:{display:`flex`,alignItems:`center`,gap:8},children:[uY[e],` `,e.replace(`Outlined`,``)]})})),fY={page:`blue`,group:`purple`},pY={page:`页面`,group:`分组`},mY=()=>{let[e,t]=(0,x.useState)([]),[n,r]=(0,x.useState)(!0),[i,a]=(0,x.useState)(`frontend`),[o,s]=(0,x.useState)({open:!1,menu:null}),[c]=Z.useForm(),l=async()=>{r(!0);try{t(await gJ())}catch{}r(!1)};(0,x.useEffect)(()=>{l()},[]);let u=async()=>{try{let e=await c.validateFields(),t={label:e.label,path:e.path||``,icon:e.icon||``,sort_order:e.sortOrder??0,is_active:e.isActive??!0,parent_id:e.parentId||null,menu_type:e.menuType||`page`,menu_target:e.menuTarget||`frontend`,is_default:e.isDefault??!1};o.menu?.id?await _J({...t,id:o.menu.id}):await _J(t),bP.success(o.menu?.id?`菜单已更新`:`菜单已添加`),s({open:!1,menu:null}),c.resetFields(),l()}catch{}},d=async e=>{try{await vJ(e),bP.success(`菜单已删除`),l()}catch(e){bP.error(e?.message||`删除失败`)}},f=t=>{s({open:!0,menu:t||null}),t?c.setFieldsValue({label:t.label,path:t.path,icon:t.icon,sortOrder:t.sortOrder??0,isActive:t.isActive??!0,parentId:t.parentId??``,menuType:t.menuType??`page`,menuTarget:t.menuTarget??`frontend`,isDefault:t.isDefault??!1}):(c.resetFields(),c.setFieldsValue({icon:`HomeOutlined`,sortOrder:e.length,isActive:!0,menuType:`page`,menuTarget:i,isDefault:!1}))},p=e.filter(e=>{let t=e.menuTarget??`frontend`;return t===i||t===`both`}),m=[{value:``,label:`顶级菜单`},...p.filter(e=>e.menuType===`group`).map(e=>({value:e.id,label:e.label}))],h=[],g=p.filter(e=>!e.parentId),_={};p.filter(e=>e.parentId).forEach(e=>{let t=e.parentId;_[t]||(_[t]=[]),_[t].push(e)}),g.sort((e,t)=>(e.sortOrder??0)-(t.sortOrder??0)).forEach(e=>{h.push({...e,_depth:0}),(_[e.id]||[]).sort((e,t)=>(e.sortOrder??0)-(t.sortOrder??0)).forEach(e=>{h.push({...e,_depth:1})})});let v=[{title:`排序`,dataIndex:`sortOrder`,width:60},{title:`菜单名称`,key:`label`,width:180,render:(e,t)=>(0,$.jsxs)(`span`,{style:{paddingLeft:t._depth*20,fontWeight:t._depth===0?600:400},children:[t._depth===1&&(0,$.jsx)(`span`,{style:{color:`#cbd5e1`,marginRight:4},children:`└`}),t.label]})},{title:`路由路径`,dataIndex:`path`,width:160,render:e=>e||(0,$.jsx)(Q.Text,{type:`secondary`,children:`-`})},{title:`图标`,dataIndex:`icon`,width:100,render:e=>e&&uY[e]?(0,$.jsx)(`span`,{style:{fontSize:16,color:`#6366f1`},children:uY[e]}):`-`},{title:`类型`,dataIndex:`menuType`,width:80,render:e=>(0,$.jsx)(CB,{color:fY[e]||`default`,children:pY[e]||e})},{title:`状态`,dataIndex:`isActive`,width:70,render:e=>(0,$.jsx)(`span`,{style:{color:e?`#22c55e`:`#94a3b8`},children:e?`启用`:`停用`})},...i===`frontend`?[{title:`默认显示`,dataIndex:`isDefault`,width:80,render:e=>e?(0,$.jsx)(CB,{color:`green`,children:`默认`}):(0,$.jsx)(Q.Text,{type:`secondary`,children:`-`})}]:[],{title:`操作`,key:`action`,width:150,render:(e,t)=>(0,$.jsxs)(wj,{size:4,children:[(0,$.jsx)(mD,{type:`link`,size:`small`,icon:(0,$.jsx)(kB,{}),onClick:()=>f(t),children:`编辑`}),(0,$.jsx)(DP,{title:`确定删除该菜单?`,onConfirm:()=>d(t.id),children:(0,$.jsx)(mD,{type:`link`,size:`small`,danger:!0,icon:(0,$.jsx)(EB,{}),children:`删除`})})]})}];return(0,$.jsxs)(`div`,{children:[(0,$.jsxs)(Mk,{bordered:!1,style:{borderRadius:12,border:`1px solid #f0f0f5`},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,marginBottom:16},children:[(0,$.jsxs)(wj,{children:[(0,$.jsx)(aG,{style:{fontSize:18,color:`#6366f1`}}),(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:16},children:`菜单配置`})]}),(0,$.jsx)(mD,{type:`primary`,icon:(0,$.jsx)(_O,{}),onClick:()=>f(),style:{borderRadius:8},children:`添加菜单`})]}),(0,$.jsx)(vk,{activeKey:i,onChange:a,items:[{key:`frontend`,label:`前台菜单`},{key:`admin`,label:`后台菜单`}]}),(0,$.jsx)(uB,{columns:v,dataSource:h,rowKey:`id`,loading:n,pagination:!1,scroll:{x:900}})]}),(0,$.jsx)(CP,{title:(0,$.jsxs)(wj,{children:[(0,$.jsx)(aG,{}),o.menu?.id?`编辑菜单`:`添加菜单`]}),open:o.open,onOk:u,onCancel:()=>{s({open:!1,menu:null}),c.resetFields()},okText:`确认`,cancelText:`取消`,width:520,children:(0,$.jsxs)(Z,{form:c,layout:`vertical`,children:[(0,$.jsx)(Z.Item,{name:`label`,label:`菜单名称`,rules:[{required:!0,message:`请输入菜单名称`}],children:(0,$.jsx)(QM,{placeholder:`例如:我的项目`,size:`large`})}),(0,$.jsx)(Z.Item,{name:`path`,label:`路由路径`,tooltip:`分组类型可留空`,children:(0,$.jsx)(QM,{placeholder:`例如:/projects(分组可留空)`,size:`large`})}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`menuType`,label:`菜单类型`,style:{flex:1},rules:[{required:!0}],children:(0,$.jsx)(ZC,{size:`large`,options:[{value:`page`,label:`页面`},{value:`group`,label:`分组`}]})}),(0,$.jsx)(Z.Item,{name:`menuTarget`,label:`适用端`,style:{flex:1},rules:[{required:!0}],children:(0,$.jsx)(ZC,{size:`large`,options:[{value:`frontend`,label:`前台`},{value:`admin`,label:`后台`},{value:`both`,label:`两者`}]})})]}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`icon`,label:`图标`,style:{flex:1},rules:[{required:!0}],children:(0,$.jsx)(ZC,{size:`large`,options:dY})}),(0,$.jsx)(Z.Item,{name:`sortOrder`,label:`排序`,style:{flex:1},children:(0,$.jsx)($A,{min:0,max:100,style:{width:`100%`},size:`large`,placeholder:`默认0`})})]}),(0,$.jsx)(Z.Item,{name:`parentId`,label:`上级菜单`,children:(0,$.jsx)(ZC,{size:`large`,options:m,allowClear:!0,placeholder:`顶级菜单`})}),(0,$.jsx)(Z.Item,{name:`isActive`,label:`启用`,valuePropName:`checked`,children:(0,$.jsx)(yF,{})}),(0,$.jsx)(Z.Item,{noStyle:!0,shouldUpdate:(e,t)=>e.menuTarget!==t.menuTarget,children:({getFieldValue:e})=>e(`menuTarget`)===`admin`?null:(0,$.jsx)(Z.Item,{name:`isDefault`,label:`新用户默认显示`,valuePropName:`checked`,tooltip:`开启后,新注册用户默认显示此菜单`,children:(0,$.jsx)(yF,{})})})]})})]})},hY={normal:`blue`,gift:`green`,promo:`purple`},gY={normal:`常规`,gift:`赠送`,promo:`促销`},_Y=()=>{let[e,t]=(0,x.useState)([]),[n,r]=(0,x.useState)(!1),[i,a]=(0,x.useState)({open:!1,item:null}),[o]=Z.useForm(),s=async()=>{r(!0);try{t((await CJ()).map(e=>({id:e.id,name:e.name,credits:e.credits,price:e.price,bonusCredits:e.bonus_credits??e.bonusCredits??0,totalCredits:e.total_credits??e.totalCredits??e.credits,description:e.description,packageType:e.package_type??e.packageType??`normal`,isGift:e.is_gift??e.isGift??!1,isActive:e.is_active??e.isActive??!0,sortOrder:e.sort_order??e.sortOrder??0})))}catch{bP.error(`加载充值套餐失败`)}finally{r(!1)}};(0,x.useEffect)(()=>{s()},[]);let c=async()=>{try{let e=await o.validateFields(),t={name:e.name,credits:e.credits,price:e.price,bonus_credits:e.bonusCredits||0,description:e.description||null,package_type:e.packageType||`normal`,is_gift:e.isGift||!1,is_active:e.isActive??!0,sort_order:e.sortOrder??0};i.item?.id?(await wJ({id:i.item.id,...t}),bP.success(`已更新`)):(await wJ(t),bP.success(`已添加`)),a({open:!1,item:null}),o.resetFields(),s()}catch{}},l=async e=>{try{await TJ(e),bP.success(`已删除`),s()}catch(e){bP.error(e?.message||`删除失败`)}},u=e=>{a({open:!0,item:e||null}),e?o.setFieldsValue({name:e.name,credits:e.credits,price:e.price,bonusCredits:e.bonusCredits,description:e.description,packageType:e.packageType,isGift:e.isGift,isActive:e.isActive,sortOrder:e.sortOrder}):(o.resetFields(),o.setFieldsValue({isActive:!0,sortOrder:0,packageType:`normal`,bonusCredits:0,isGift:!1}))};return(0,$.jsxs)(`div`,{children:[(0,$.jsxs)(Mk,{bordered:!1,style:{borderRadius:12,border:`1px solid #f0f0f5`},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,marginBottom:16},children:[(0,$.jsxs)(wj,{children:[(0,$.jsx)(bW,{style:{fontSize:18,color:`#6366f1`}}),(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:16},children:`充值套餐管理`}),(0,$.jsxs)(CB,{color:`purple`,children:[e.length,` 个套餐`]})]}),(0,$.jsx)(mD,{type:`primary`,icon:(0,$.jsx)(_O,{}),onClick:()=>u(),style:{borderRadius:8},children:`添加套餐`})]}),(0,$.jsx)(uB,{columns:[{title:`套餐名称`,key:`name`,width:160,render:(e,t)=>(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{strong:!0,children:t.name}),t.description&&(0,$.jsx)(`div`,{style:{color:`#94a3b8`,fontSize:12},children:t.description})]})},{title:`基础积分`,dataIndex:`credits`,width:100,render:e=>(0,$.jsx)(Q.Text,{children:e.toLocaleString()})},{title:`赠送积分`,dataIndex:`bonusCredits`,width:100,render:e=>e>0?(0,$.jsxs)(CB,{color:`green`,children:[`+`,e.toLocaleString()]}):(0,$.jsx)(Q.Text,{type:`secondary`,children:`-`})},{title:`总积分`,key:`total`,width:100,render:(e,t)=>(0,$.jsx)(Q.Text,{strong:!0,style:{color:`#6366f1`},children:(t.credits+t.bonusCredits).toLocaleString()})},{title:`价格(元)`,dataIndex:`price`,width:100,render:e=>(0,$.jsxs)(Q.Text,{strong:!0,children:[`¥`,e]})},{title:`类型`,dataIndex:`packageType`,width:80,render:e=>(0,$.jsx)(CB,{color:hY[e]||`default`,children:gY[e]||e})},{title:`状态`,dataIndex:`isActive`,width:80,render:e=>(0,$.jsx)(CB,{color:e?`green`:`default`,children:e?`启用`:`停用`})},{title:`操作`,key:`action`,width:150,fixed:`right`,render:(e,t)=>(0,$.jsxs)(wj,{size:4,children:[(0,$.jsx)(mD,{type:`link`,size:`small`,icon:(0,$.jsx)(kB,{}),onClick:()=>u(t),children:`编辑`}),(0,$.jsx)(DP,{title:`确定删除?`,onConfirm:()=>l(t.id),children:(0,$.jsx)(mD,{type:`link`,size:`small`,danger:!0,icon:(0,$.jsx)(EB,{}),children:`删除`})})]})}],dataSource:e,rowKey:`id`,loading:n,pagination:!1,scroll:{x:900}})]}),(0,$.jsx)(CP,{title:(0,$.jsxs)(wj,{children:[(0,$.jsx)(bW,{}),i.item?`编辑套餐`:`添加套餐`]}),open:i.open,onOk:c,onCancel:()=>{a({open:!1,item:null}),o.resetFields()},okText:`保存`,cancelText:`取消`,width:520,children:(0,$.jsxs)(Z,{form:o,layout:`vertical`,style:{marginTop:16},children:[(0,$.jsx)(Z.Item,{name:`name`,label:`套餐名称`,rules:[{required:!0,message:`请输入套餐名称`}],children:(0,$.jsx)(QM,{placeholder:`例如:进阶包`,size:`large`})}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`credits`,label:`基础积分`,rules:[{required:!0,message:`请输入积分`}],style:{flex:1},children:(0,$.jsx)($A,{min:1,placeholder:`2000`,size:`large`,style:{width:`100%`}})}),(0,$.jsx)(Z.Item,{name:`price`,label:`价格(元)`,rules:[{required:!0,message:`请输入价格`}],style:{flex:1},children:(0,$.jsx)($A,{min:.01,step:1,placeholder:`168`,size:`large`,style:{width:`100%`}})})]}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`bonusCredits`,label:`赠送积分`,initialValue:0,style:{flex:1},children:(0,$.jsx)($A,{min:0,placeholder:`0`,size:`large`,style:{width:`100%`}})}),(0,$.jsx)(Z.Item,{name:`packageType`,label:`套餐类型`,initialValue:`normal`,style:{flex:1},children:(0,$.jsx)(ZC,{size:`large`,options:[{value:`normal`,label:`常规`},{value:`gift`,label:`赠送`},{value:`promo`,label:`促销`}]})})]}),(0,$.jsx)(Z.Item,{name:`description`,label:`描述`,children:(0,$.jsx)(QM,{placeholder:`套餐描述(可选)`,size:`large`})}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16},children:[(0,$.jsx)(Z.Item,{name:`isActive`,label:`启用状态`,valuePropName:`checked`,initialValue:!0,style:{flex:1},children:(0,$.jsx)(yF,{})}),(0,$.jsx)(Z.Item,{name:`isGift`,label:`是否赠送`,valuePropName:`checked`,initialValue:!1,style:{flex:1},children:(0,$.jsx)(yF,{})}),(0,$.jsx)(Z.Item,{name:`sortOrder`,label:`排序`,initialValue:0,style:{flex:1},children:(0,$.jsx)($A,{size:`large`,style:{width:`100%`}})})]})]})})]})},vY={POST:`green`,PUT:`blue`,DELETE:`red`},yY=()=>{let[e,t]=(0,x.useState)([]),[n,r]=(0,x.useState)(0),[i,a]=(0,x.useState)(!1),[o,s]=(0,x.useState)(1),c=async e=>{a(!0);try{let n=await EJ(e||o);t(n.items||[]),r(n.total||0)}catch{bP.error(`加载操作日志失败`)}finally{a(!1)}};return(0,x.useEffect)(()=>{c()},[]),(0,$.jsx)(`div`,{children:(0,$.jsxs)(Mk,{bordered:!1,style:{borderRadius:12,border:`1px solid #f0f0f5`},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,marginBottom:16},children:[(0,$.jsxs)(wj,{children:[(0,$.jsx)(MW,{style:{fontSize:18,color:`#6366f1`}}),(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:16},children:`操作日志`})]}),(0,$.jsx)(mD,{icon:(0,$.jsx)(lF,{}),onClick:()=>c(),children:`刷新`})]}),(0,$.jsx)(uB,{columns:[{title:`操作人`,dataIndex:`username`,width:120,render:e=>(0,$.jsx)(Q.Text,{strong:!0,children:e})},{title:`操作`,dataIndex:`action`,width:160,render:e=>(0,$.jsx)(Q.Text,{children:e})},{title:`方法`,dataIndex:`method`,width:80,render:e=>(0,$.jsx)(CB,{color:vY[e]||`default`,children:e})},{title:`路径`,dataIndex:`path`,width:220,ellipsis:!0,render:e=>(0,$.jsx)(Q.Text,{type:`secondary`,style:{fontSize:12},children:e})},{title:`时间`,dataIndex:`createdAt`,width:160,render:e=>(0,$.jsx)(Q.Text,{type:`secondary`,style:{fontSize:12},children:BJ(e)})}],dataSource:e,rowKey:`id`,loading:i,pagination:{current:o,pageSize:20,total:n,showTotal:e=>`共 ${e} 条记录`,onChange:e=>{s(e),c(e)}},scroll:{x:800}})]})})},bY={POST:`green`,PUT:`blue`,DELETE:`red`},xY=()=>{let[e,t]=(0,x.useState)([]),[n,r]=(0,x.useState)(0),[i,a]=(0,x.useState)(!1),[o,s]=(0,x.useState)(1),c=async e=>{a(!0);try{let n=await EJ(e||o);t(n.items||[]),r(n.total||0)}catch{bP.error(`加载操作日志失败`)}finally{a(!1)}};return(0,x.useEffect)(()=>{c()},[]),(0,$.jsx)(`div`,{children:(0,$.jsxs)(Mk,{bordered:!1,style:{borderRadius:12,border:`1px solid #f0f0f5`},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,marginBottom:16},children:[(0,$.jsxs)(wj,{children:[(0,$.jsx)(MW,{style:{fontSize:18,color:`#6366f1`}}),(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:16},children:`操作日志`})]}),(0,$.jsx)(mD,{icon:(0,$.jsx)(lF,{}),onClick:()=>c(),children:`刷新`})]}),(0,$.jsx)(uB,{columns:[{title:`操作人`,dataIndex:`username`,width:120,render:e=>(0,$.jsx)(Q.Text,{strong:!0,children:e})},{title:`操作`,dataIndex:`action`,width:160,render:e=>(0,$.jsx)(Q.Text,{children:e})},{title:`方法`,dataIndex:`method`,width:80,render:e=>(0,$.jsx)(CB,{color:bY[e]||`default`,children:e})},{title:`路径`,dataIndex:`path`,width:220,ellipsis:!0,render:e=>(0,$.jsx)(Q.Text,{type:`secondary`,style:{fontSize:12},children:e})},{title:`时间`,dataIndex:`createdAt`,width:160,render:e=>(0,$.jsx)(Q.Text,{type:`secondary`,style:{fontSize:12},children:BJ(e)})}],dataSource:e,rowKey:`id`,loading:i,pagination:{current:o,pageSize:20,total:n,showTotal:e=>`共 ${e} 条记录`,onChange:e=>{s(e),c(e)}},scroll:{x:800}})]})})},SY=`http://ceshi.apiforeign.minzhong.cn`.replace(/\/api\/?$/i,``).replace(/\/$/,``),CY={image:`empty`,video:`empty`,videoCover:`empty`,references:{}},wY={optimizing:{color:`processing`,text:`优化中`,icon:(0,$.jsx)(um,{spin:!0})},prompt_optimized:{color:`processing`,text:`待生成`,icon:(0,$.jsx)(hj,{})},generating:{color:`warning`,text:`生成中`,icon:(0,$.jsx)(um,{spin:!0})},completed:{color:`success`,text:`已完成`,icon:(0,$.jsx)(bU,{})},failed:{color:`error`,text:`失败`,icon:(0,$.jsx)(CU,{})}},TY={image:{text:`图片`,color:`purple`,icon:(0,$.jsx)(aW,{})},video:{text:`视频`,color:`geekblue`,icon:(0,$.jsx)(cq,{})}},EY=e=>/^(https?:)?\/\//i.test(e)||/^(blob|data):/i.test(e),DY=e=>!!e&&/^blob:/i.test(e.trim()),OY=e=>{if(!e)return``;let t=String(e).trim();return t?EY(t)?t:SY?`${SY}${t.startsWith(`/`)?t:`/${t}`}`:t.startsWith(`/`)?t:`/${t}`:``},kY=e=>e?e.length>12?`${e.slice(0,8)}...`:e:`-`,AY=e=>e?BJ(e):`-`,jY=e=>e==null||e===``,MY=e=>{if(!e||DY(e))return!1;try{let t=new URL(OY(e),window.location.origin),n=t.searchParams.get(`exp`)||t.searchParams.get(`expires`)||t.searchParams.get(`expire`)||t.searchParams.get(`expires_at`)||t.searchParams.get(`x-expires`);if(!n)return!1;let r=Number(n);if(!Number.isFinite(r))return!1;let i=r>1e10?r:r*1e3;return Date.now()>=i}catch{return!1}},NY=e=>e?DY(e)||MY(e)?`invalid`:`checking`:`empty`,PY=e=>{let t=e.url||e.mediaUrl||e.fileUrl;return typeof t==`string`&&t.trim()?t.trim():void 0},FY=e=>{let t=String(e.type||e.mediaType||e.mimeType||``).toLowerCase(),n=PY(e)?.toLowerCase()||``;return t.includes(`video`)||/\.(mp4|mov|webm|m4v)(\?|$)/i.test(n)?`video`:t.includes(`image`)||/\.(png|jpe?g|webp|gif|bmp|svg)(\?|$)/i.test(n)?`image`:t||`unknown`},IY=(e,t)=>`${t}-${PY(e)||`empty`}`,LY=(e,t=`资源`)=>DY(e)?`本地临时素材已失效`:MY(e)?`${t}链接已超时,请刷新列表或重新搜索后再查看`:`${t}加载失败,请刷新列表或重新搜索后再查看`,RY=({text:e,minHeight:t=240,compact:n=!1,action:r})=>(0,$.jsxs)(`div`,{style:{width:`100%`,minHeight:n?void 0:t,height:n?`100%`:void 0,borderRadius:n?10:12,background:`#f8f9fc`,border:`1px dashed #cbd5e1`,color:`#64748b`,display:`flex`,alignItems:`center`,justifyContent:`center`,flexDirection:`column`,gap:n?4:10,textAlign:`center`},children:[(0,$.jsx)(Q.Text,{style:{color:`#64748b`,fontSize:n?11:13},children:e}),r]}),zY=({label:e,value:t})=>(0,$.jsxs)(`div`,{style:{flex:1,minWidth:120},children:[(0,$.jsx)(Q.Text,{style:{fontSize:11,color:`#94a3b8`,display:`block`},children:e}),(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:14},children:jY(t)?`-`:t})]}),BY=()=>{let[e,t]=(0,x.useState)([]),[n,r]=(0,x.useState)(0),[i,a]=(0,x.useState)(!1),[o,s]=(0,x.useState)(1),[c]=(0,x.useState)(20),[l,u]=(0,x.useState)(``),[d,f]=(0,x.useState)(``),[p,m]=(0,x.useState)(0),[h,g]=(0,x.useState)(null),[_,v]=(0,x.useState)(CY),[y,b]=(0,x.useState)(!1),S=(0,x.useRef)(null),[C,w]=(0,x.useState)(null),[T,E]=(0,x.useState)(null),D=(0,x.useCallback)(async()=>{a(!0);try{let e=await DJ({userId:d.trim()||void 0,status:l||void 0,page:o,pageSize:c});t((e.items||[]).map(e=>({id:e.id,userId:e.userId,username:e.username,projectId:e.projectId,projectName:e.projectName,originalPrompt:e.originalPrompt,optimizedPrompt:e.optimizedPrompt,duration:e.duration,aspectRatio:e.aspectRatio,resolution:e.resolution,status:e.status,videoUrl:e.videoUrl,videoCoverUrl:e.videoCoverUrl,references:e.references,creditsCost:e.creditsCost||0,textCreditsCost:e.textCreditsCost||0,textTokensUsed:e.textTokensUsed||0,videoTokensUsed:e.videoTokensUsed||0,errorMessage:e.errorMessage,createdAt:e.createdAt,generatedAt:e.generatedAt,genType:e.genType,imageSize:e.imageSize,imageUrl:e.imageUrl,imageTokensUsed:e.imageTokensUsed||0,imageProportion:e.imageProportion,imagePx:e.imagePx}))),r(e.total||0)}catch{bP.error(`加载记录失败`)}finally{a(!1)}},[l,d,o,c]);(0,x.useEffect)(()=>{D()},[D,p]),(0,x.useEffect)(()=>{if(!h){v(CY);return}let e=(h.references||[]).reduce((e,t,n)=>{let r=PY(t);return e[IY(t,n)]=NY(r),e},{});S.current&&(S.current.pause(),S.current.currentTime=0),b(!1),v({image:NY(h.imageUrl),video:NY(h.videoUrl),videoCover:NY(h.videoCoverUrl),references:e})},[h]);let O=()=>{s(1),m(e=>e+1)},k=(0,x.useCallback)(e=>{g(e)},[]),A=()=>{S.current&&S.current.pause(),b(!1),g(null)},j=e=>{v(t=>({...t,...e}))},M=(e,t)=>{v(n=>({...n,references:{...n.references,[e]:t}}))},N=(e,t=`素材`)=>{if(!e){bP.warning(`${t}链接为空,暂无法查看`);return}if(DY(e)){bP.warning(`本地临时素材已失效,暂无法查看`);return}if(MY(e)){bP.warning(`${t}链接已超时,请刷新列表或重新搜索后再查看`);return}window.open(OY(e),`_blank`,`noopener,noreferrer`)},P=()=>{if(h?.videoUrl){if(MY(h.videoUrl)){j({video:`invalid`}),bP.warning(`视频链接已超时,请刷新列表或重新搜索后再查看`);return}b(!0),window.setTimeout(()=>{S.current?.play().catch(()=>{b(!1),j({video:`invalid`}),bP.warning(`视频播放失败,请确认资源链接是否仍然有效`)})},0)}},F=async(e,t,n)=>{w(e);try{await OJ(e,t,n),bP.success(`状态已更新`),D()}catch(e){bP.error(e?.message||`更新失败`)}finally{w(null)}},I=async()=>{if(T){w(T.record.id);try{await kJ(T.record.id,T.ratio,T.resolution,T.image_size),bP.success(`已提交${T.record.genType===`video`?`视频`:`图片`}生成`),E(null),D()}catch(e){bP.error(e?.message||`生成失败`)}finally{w(null)}}},L=(0,x.useMemo)(()=>[{title:`用户`,key:`user`,width:120,render:(e,t)=>(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:13},children:t.username||`未知用户`}),(0,$.jsx)(`div`,{style:{fontSize:11,color:`#94a3b8`},children:kY(t.userId)})]})},{title:`项目`,dataIndex:`projectName`,width:120,ellipsis:!0,render:e=>(0,$.jsx)(Q.Text,{style:{fontSize:13},children:e||`-`})},{title:`类型`,dataIndex:`genType`,width:90,ellipsis:!0,render:e=>{let t=TY[e]||{text:e||`-`,color:`default`,icon:null};return(0,$.jsx)(CB,{color:t.color,icon:t.icon,children:t.text})}},{title:`提示词`,key:`prompt`,ellipsis:!0,render:(e,t)=>(0,$.jsx)(Bw,{title:t.originalPrompt,placement:`topLeft`,children:(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#475569`},ellipsis:!0,children:t.originalPrompt||`-`})})},{title:`参数`,key:`params`,width:160,render:(e,t)=>t.genType===`video`?t.duration||t.aspectRatio||t.resolution?(0,$.jsxs)(wj,{size:4,wrap:!0,children:[t.duration?(0,$.jsxs)(CB,{children:[t.duration,`s`]}):null,t.aspectRatio?(0,$.jsx)(CB,{children:t.aspectRatio}):null,t.resolution?(0,$.jsx)(CB,{children:t.resolution}):null]}):(0,$.jsx)(CB,{color:`default`,children:`待配置`}):t.imageSize||t.imageProportion||t.imagePx?(0,$.jsxs)(wj,{size:4,wrap:!0,children:[t.imageSize?(0,$.jsx)(CB,{children:t.imageSize}):null,t.imageProportion?(0,$.jsx)(CB,{children:t.imageProportion}):null,t.imagePx?(0,$.jsx)(CB,{children:t.imagePx}):null]}):(0,$.jsx)(CB,{color:`default`,children:`待配置`})},{title:`积分`,key:`credits`,width:120,render:(e,t)=>(0,$.jsxs)(`div`,{style:{fontSize:12},children:[t.textCreditsCost>0?(0,$.jsxs)(`div`,{style:{color:`#f59e0b`},children:[`文字: `,t.textCreditsCost]}):null,t.creditsCost>0?(0,$.jsxs)(`div`,{style:{color:`#6366f1`},children:[t.genType===`video`?`视频`:`图片`,`: `,t.creditsCost]}):null,t.textCreditsCost===0&&t.creditsCost===0?(0,$.jsx)(Q.Text,{style:{color:`#94a3b8`},children:`0`}):null]})},{title:`状态`,dataIndex:`status`,width:90,render:e=>{let t=wY[e]||{color:`default`,text:e||`-`,icon:null};return(0,$.jsx)(CB,{color:t.color,icon:t.icon,children:t.text})}},{title:`时间`,key:`time`,width:150,render:(e,t)=>(0,$.jsxs)(`div`,{style:{fontSize:12,color:`#94a3b8`},children:[(0,$.jsx)(`div`,{children:AY(t.createdAt)}),t.generatedAt?(0,$.jsxs)(`div`,{style:{color:`#10b981`},children:[`生成: `,AY(t.generatedAt)]}):null]})},{title:`操作`,key:`action`,width:150,fixed:`right`,render:(e,t)=>(0,$.jsxs)(wj,{size:4,wrap:!0,children:[(0,$.jsx)(mD,{size:`small`,icon:(0,$.jsx)(AM,{}),onClick:()=>k(t),children:`详情`}),t.status===`generating`?(0,$.jsx)(mD,{size:`small`,danger:!0,loading:C===t.id,onClick:()=>{CP.confirm({title:`确认操作`,icon:(0,$.jsx)($U,{}),content:`确定将此记录标记为失败?`,onOk:()=>F(t.id,`failed`)})},children:`标记失败`}):null,t.status===`failed`?(0,$.jsx)(mD,{size:`small`,type:`primary`,danger:!0,loading:C===t.id,onClick:()=>E({record:t,ratio:t.aspectRatio||`16:9`,resolution:t.resolution||`720p`,image_size:t.imageSize||`2K`}),children:`重试生成`}):null,t.status===`prompt_optimized`?(0,$.jsxs)($.Fragment,{children:[(0,$.jsxs)(mD,{size:`small`,type:`primary`,loading:C===t.id,onClick:()=>E({record:t,ratio:t.aspectRatio||`16:9`,resolution:t.resolution||`720p`,image_size:t.imageSize||`2K`}),style:{background:`#6366f1`,border:`none`},children:[`生成`,t.genType===`video`?`视频`:`图片`]}),(0,$.jsx)(mD,{size:`small`,danger:!0,loading:C===t.id,onClick:()=>{CP.confirm({title:`确认操作`,icon:(0,$.jsx)($U,{}),content:`确定将此记录标记为失败?`,onOk:()=>F(t.id,`failed`)})},children:`标记失败`})]}):null]})}],[k,C]),R=h?TY[h.genType||``]||{text:h.genType||`-`,color:`default`,icon:null}:null,z=h?wY[h.status]||{color:`default`,text:h.status||`-`,icon:null}:null,B=()=>{if(!h||h.genType!==`image`||h.status!==`completed`)return null;if(!h.imageUrl)return(0,$.jsx)(RY,{text:`此图片任务暂无结果图片`,minHeight:260});if(_.image===`invalid`)return(0,$.jsx)(RY,{text:LY(h.imageUrl,`图片`),minHeight:260});let e=OY(h.imageUrl);return(0,$.jsxs)(`div`,{title:`点击新页面查看图片`,onClick:()=>N(h.imageUrl,`生成图片`),style:{position:`relative`,width:`100%`,minHeight:260,borderRadius:12,background:`#f8f9fc`,border:`1px solid #e2e8f0`,overflow:`hidden`,display:`flex`,alignItems:`center`,justifyContent:`center`,cursor:`pointer`},children:[(0,$.jsx)(`img`,{src:e,alt:`生成图片`,onLoad:()=>j({image:`valid`}),onError:()=>j({image:`invalid`}),style:{display:_.image===`valid`?`block`:`none`,width:`100%`,maxHeight:560,objectFit:`contain`,background:`#fff`}},`${h.id}-${e}`),_.image===`checking`?(0,$.jsx)(RY,{text:`图片加载检测中...`,minHeight:260}):null]})},V=()=>{if(!h||y||_.video===`invalid`)return null;let e=h.videoUrl?(0,$.jsx)(mD,{type:`primary`,shape:`circle`,size:`large`,icon:(0,$.jsx)(kG,{}),onClick:P,style:{boxShadow:`0 8px 20px rgba(15,23,42,0.25)`}}):null;if(!h.videoCoverUrl)return(0,$.jsx)(`div`,{style:{position:`absolute`,inset:0,borderRadius:12,overflow:`hidden`,background:`#f8f9fc`,display:`flex`,alignItems:`center`,justifyContent:`center`},children:(0,$.jsx)(RY,{text:`此视频无封面`,minHeight:340,action:e})});if(_.videoCover===`invalid`)return(0,$.jsx)(RY,{text:LY(h.videoCoverUrl,`视频封面`),minHeight:340,action:e});let t=OY(h.videoCoverUrl);return(0,$.jsxs)(`div`,{style:{position:`absolute`,inset:0,borderRadius:12,overflow:`hidden`,background:`#f8f9fc`,border:`1px solid #e2e8f0`,display:`flex`,alignItems:`center`,justifyContent:`center`},children:[(0,$.jsx)(`img`,{src:t,alt:`视频封面`,onLoad:()=>j({videoCover:`valid`}),onError:()=>j({videoCover:`invalid`}),style:{display:_.videoCover===`valid`?`block`:`none`,width:`100%`,height:`100%`,objectFit:`contain`,background:`#000`}},`${h.id}-cover-${t}`),_.videoCover===`checking`?(0,$.jsx)(RY,{text:`视频封面加载检测中...`,minHeight:340,action:e}):null,_.videoCover===`valid`?(0,$.jsx)(`div`,{style:{position:`absolute`,inset:0,display:`flex`,alignItems:`center`,justifyContent:`center`,background:`rgba(15,23,42,0.18)`},children:e}):null]})},H=()=>{if(!h||h.genType!==`video`||h.status!==`completed`)return null;if(!h.videoUrl)return(0,$.jsx)(RY,{text:`此视频任务暂无结果视频`,minHeight:340});if(_.video===`invalid`)return(0,$.jsx)(RY,{text:LY(h.videoUrl,`视频`),minHeight:340});let e=OY(h.videoUrl);return(0,$.jsxs)(`div`,{style:{position:`relative`,width:`100%`,height:340,borderRadius:12,background:`#000`,overflow:`hidden`,border:`1px solid #e2e8f0`},children:[(0,$.jsx)(`video`,{ref:S,src:e,preload:`metadata`,controls:y,onLoadedMetadata:()=>j({video:`valid`}),onCanPlay:()=>j({video:`valid`}),onPlaying:()=>{b(!0),j({video:`valid`})},onPause:()=>b(!1),onEnded:()=>b(!1),onError:()=>{b(!1),j({video:`invalid`})},style:{width:`100%`,height:`100%`,objectFit:`contain`,background:`#000`,opacity:+!!y,pointerEvents:y?`auto`:`none`}},`${h.id}-${e}`),V()]})},U=(e,t)=>{let n=PY(e),r=FY(e),i=IY(e,t),a=_.references[i]||NY(n),o=r===`image`,s=r===`video`,c=typeof e.name==`string`&&e.name?e.name:`参考素材 ${t+1}`;if(!n)return(0,$.jsxs)(`div`,{style:{width:94},children:[(0,$.jsx)(`div`,{style:{width:94,height:94},children:(0,$.jsx)(RY,{text:`无链接`,compact:!0})}),(0,$.jsx)(Q.Text,{ellipsis:!0,style:{display:`block`,marginTop:4,fontSize:11,color:`#94a3b8`},children:c})]},i);if(a===`invalid`)return(0,$.jsxs)(`div`,{style:{width:94},children:[(0,$.jsx)(`div`,{style:{width:94,height:94},children:(0,$.jsx)(RY,{text:DY(n)?`本地临时素材已失效`:`素材不可访问`,compact:!0})}),(0,$.jsx)(Bw,{title:c,children:(0,$.jsx)(Q.Text,{ellipsis:!0,style:{display:`block`,marginTop:4,fontSize:11,color:`#94a3b8`},children:c})})]},i);let l=OY(n);return(0,$.jsxs)(`div`,{style:{width:94},children:[(0,$.jsxs)(`div`,{role:`button`,tabIndex:0,title:`点击新页面查看素材`,onClick:()=>N(n,s?`视频素材`:o?`图片素材`:`素材`),onKeyDown:e=>{e.key===`Enter`&&N(n,s?`视频素材`:o?`图片素材`:`素材`)},style:{width:94,height:94,borderRadius:10,overflow:`hidden`,border:`1px solid #e2e8f0`,background:`#f8f9fc`,position:`relative`,display:`flex`,alignItems:`center`,justifyContent:`center`,cursor:`pointer`},children:[o?(0,$.jsx)(`img`,{src:l,alt:c,onLoad:()=>M(i,`valid`),onError:()=>M(i,`invalid`),style:{display:a===`valid`?`block`:`none`,width:`100%`,height:`100%`,objectFit:`cover`}}):null,s?(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`video`,{src:l,preload:`metadata`,onLoadedMetadata:()=>M(i,`valid`),onError:()=>M(i,`invalid`),style:{display:`none`}}),(0,$.jsxs)(`div`,{style:{color:`#64748b`,fontSize:12,textAlign:`center`},children:[(0,$.jsx)(kG,{style:{fontSize:18}}),(0,$.jsx)(`div`,{children:`视频素材`}),(0,$.jsx)(`div`,{style:{fontSize:10,color:`#94a3b8`,marginTop:2},children:`点击查看`})]})]}):null,!o&&!s?(0,$.jsxs)(`div`,{style:{color:`#64748b`,fontSize:12,textAlign:`center`},children:[`文件素材`,(0,$.jsx)(`div`,{style:{fontSize:10,color:`#94a3b8`,marginTop:2},children:`点击查看`})]}):null,a===`checking`&&o?(0,$.jsx)(`div`,{style:{position:`absolute`,inset:0,display:`flex`,alignItems:`center`,justifyContent:`center`,background:`#f8f9fc`},children:(0,$.jsx)(um,{style:{color:`#6366f1`}})}):null]}),(0,$.jsx)(Bw,{title:c,children:(0,$.jsx)(Q.Text,{ellipsis:!0,style:{display:`block`,marginTop:4,fontSize:11,color:`#94a3b8`},children:c})})]},i)};return(0,$.jsxs)(`div`,{children:[(0,$.jsxs)(Mk,{bordered:!1,style:{borderRadius:12,border:`1px solid #f0f0f5`},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,marginBottom:16,flexWrap:`wrap`,gap:12},children:[(0,$.jsxs)(wj,{children:[(0,$.jsx)(cq,{style:{fontSize:18,color:`#6366f1`}}),(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:16},children:`生成记录管理`}),(0,$.jsxs)(CB,{color:`purple`,children:[n,` 条记录`]})]}),(0,$.jsxs)(wj,{children:[(0,$.jsx)(ZC,{placeholder:`状态筛选`,allowClear:!0,style:{width:120},value:l||void 0,onChange:e=>{u(e||``),s(1)},options:[{value:`optimizing`,label:`优化中`},{value:`prompt_optimized`,label:`待生成`},{value:`generating`,label:`生成中`},{value:`completed`,label:`已完成`},{value:`failed`,label:`失败`}]}),(0,$.jsx)(QM,{placeholder:`用户ID搜索`,prefix:(0,$.jsx)(KC,{style:{color:`#94a3b8`}}),style:{width:200},value:d,onChange:e=>f(e.target.value),onPressEnter:O,allowClear:!0}),(0,$.jsx)(mD,{type:`primary`,onClick:O,style:{borderRadius:8},children:`搜索`})]})]}),(0,$.jsx)(uB,{columns:L,dataSource:e,rowKey:`id`,loading:i,scroll:{x:1120},pagination:{current:o,pageSize:c,total:n,onChange:s,showSizeChanger:!1,showTotal:e=>`共 ${e} 条`}})]}),(0,$.jsx)(CP,{title:(0,$.jsxs)(wj,{children:[h?.genType===`video`?(0,$.jsx)(cq,{}):(0,$.jsx)(aW,{}),(0,$.jsx)(`span`,{children:`生成记录详情`})]}),open:!!h,onCancel:A,footer:null,width:900,destroyOnClose:!0,children:h?(0,$.jsxs)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:16,marginTop:16},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,flexWrap:`wrap`},children:[(0,$.jsxs)(`div`,{style:{flex:1,minWidth:150,padding:12,borderRadius:10,background:`#f8f9fc`},children:[(0,$.jsx)(Q.Text,{style:{fontSize:11,color:`#94a3b8`,display:`block`},children:`用户`}),(0,$.jsx)(Q.Text,{strong:!0,children:h.username||`-`})]}),(0,$.jsxs)(`div`,{style:{flex:1,minWidth:150,padding:12,borderRadius:10,background:`#f8f9fc`},children:[(0,$.jsx)(Q.Text,{style:{fontSize:11,color:`#94a3b8`,display:`block`},children:`项目`}),(0,$.jsx)(Q.Text,{strong:!0,children:h.projectName||`-`})]}),(0,$.jsxs)(`div`,{style:{flex:1,minWidth:150,padding:12,borderRadius:10,background:`#f8f9fc`},children:[(0,$.jsx)(Q.Text,{style:{fontSize:11,color:`#94a3b8`,display:`block`},children:`类型 / 状态`}),(0,$.jsxs)(wj,{size:4,wrap:!0,children:[R?(0,$.jsx)(CB,{color:R.color,icon:R.icon,children:R.text}):null,z?(0,$.jsx)(CB,{color:z.color,icon:z.icon,children:z.text}):null]})]})]}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#94a3b8`,display:`block`,marginBottom:6},children:`原始提示词`}),(0,$.jsx)(`div`,{style:{padding:12,borderRadius:10,background:`#f8f9fc`,border:`1px solid #f0f0f5`},children:(0,$.jsx)(Q.Text,{style:{fontSize:13,color:`#475569`,lineHeight:1.7},children:h.originalPrompt||`-`})})]}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#94a3b8`,display:`block`,marginBottom:6},children:`优化后提示词`}),(0,$.jsx)(`div`,{style:{padding:12,borderRadius:10,background:`rgba(99,102,241,0.02)`,border:`1px solid rgba(99,102,241,0.1)`},children:(0,$.jsx)(Q.Text,{style:{fontSize:13,color:`#1a1a2e`,lineHeight:1.7},children:h.optimizedPrompt||`-`})})]}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,padding:12,borderRadius:10,background:`#f8f9fc`,flexWrap:`wrap`},children:[(0,$.jsx)(zY,{label:`文字积分`,value:(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(Q.Text,{strong:!0,style:{color:`#f59e0b`},children:h.textCreditsCost||0}),(0,$.jsxs)(Q.Text,{style:{fontSize:11,color:`#94a3b8`},children:[` (`,h.textTokensUsed||0,` tokens)`]})]})}),(0,$.jsx)(zY,{label:`${h.genType===`image`?`图片`:`视频`}积分`,value:(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(Q.Text,{strong:!0,style:{color:`#6366f1`},children:h.creditsCost||0}),h.genType===`video`&&h.videoTokensUsed?(0,$.jsxs)(Q.Text,{style:{fontSize:11,color:`#94a3b8`},children:[` (`,h.videoTokensUsed,` tokens)`]}):null,h.genType===`image`&&h.imageTokensUsed?(0,$.jsxs)(Q.Text,{style:{fontSize:11,color:`#94a3b8`},children:[` (`,h.imageTokensUsed,` tokens)`]}):null]})}),(0,$.jsx)(zY,{label:`总积分`,value:(h.textCreditsCost||0)+(h.creditsCost||0)})]}),h.genType===`video`?h.duration||h.aspectRatio||h.resolution?(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,padding:12,borderRadius:10,background:`#f8f9fc`,flexWrap:`wrap`},children:[(0,$.jsx)(zY,{label:`时长`,value:h.duration?`${h.duration}秒`:`-`}),(0,$.jsx)(zY,{label:`比例`,value:h.aspectRatio||`-`}),(0,$.jsx)(zY,{label:`分辨率`,value:h.resolution||`-`})]}):(0,$.jsx)(`div`,{style:{padding:12,borderRadius:10,background:`#f8f9fc`,textAlign:`center`},children:(0,$.jsx)(CB,{color:`default`,children:`视频参数待用户配置`})}):null,h.genType===`image`?h.imageSize||h.imageProportion||h.imagePx?(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,padding:12,borderRadius:10,background:`#f8f9fc`,flexWrap:`wrap`},children:[(0,$.jsx)(zY,{label:`尺寸`,value:h.imagePx||`-`}),(0,$.jsx)(zY,{label:`比例`,value:h.imageProportion||`-`}),(0,$.jsx)(zY,{label:`分辨率`,value:h.imageSize||`-`})]}):(0,$.jsx)(`div`,{style:{padding:12,borderRadius:10,background:`#f8f9fc`,textAlign:`center`},children:(0,$.jsx)(CB,{color:`default`,children:`图片参数待用户配置`})}):null,!h?.references||h.references.length===0?null:(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#94a3b8`,display:`block`,marginBottom:6},children:`参考内容`}),(0,$.jsx)(`div`,{style:{display:`flex`,gap:10,flexWrap:`wrap`},children:h.references.map((e,t)=>U(e,t))})]}),h.status===`completed`?(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#94a3b8`,display:`block`,marginBottom:6},children:h.genType===`video`?`生成视频`:`生成图片`}),h.genType===`video`?H():B()]}):null,h.status===`failed`&&h.errorMessage?(0,$.jsx)(`div`,{style:{padding:12,borderRadius:10,background:`rgba(239,68,68,0.04)`,border:`1px solid rgba(239,68,68,0.15)`},children:(0,$.jsxs)(Q.Text,{style:{fontSize:12,color:`#ef4444`},children:[`错误信息: `,h.errorMessage]})}):null,(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,fontSize:12,color:`#94a3b8`,flexWrap:`wrap`},children:[(0,$.jsxs)(`span`,{children:[`创建: `,AY(h.createdAt)]}),(0,$.jsxs)(`span`,{children:[`生成: `,AY(h.generatedAt)]})]})]}):(0,$.jsx)(xC,{description:`暂无详情`})}),(0,$.jsx)(CP,{title:(0,$.jsxs)(wj,{children:[T&&(T.record.genType===`video`?(0,$.jsx)(kG,{}):(0,$.jsx)(aW,{})),T&&(T.record.genType===`video`?`生成视频`:`生成图片`)]}),open:!!T,onCancel:()=>E(null),onOk:I,okText:`提交生成`,cancelText:`取消`,confirmLoading:T?C===T.record.id:!1,width:420,children:T?(0,$.jsx)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:16,marginTop:16},children:T.record.genType===`video`?(0,$.jsxs)($.Fragment,{children:[(0,$.jsxs)(`div`,{style:{padding:12,borderRadius:10,background:`#f8f9fc`},children:[(0,$.jsx)(Q.Text,{style:{fontSize:11,color:`#94a3b8`,display:`block`},children:`时长`}),(0,$.jsxs)(Q.Text,{strong:!0,children:[T.record.duration||5,`s`]})]}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#64748b`,display:`block`,marginBottom:6},children:`画面比例`}),(0,$.jsx)(ZC,{value:T.ratio,onChange:e=>E(t=>t?{...t,ratio:e}:null),style:{width:`100%`},options:[`16:9`,`4:3`,`1:1`,`3:4`,`9:16`,`21:9`].map(e=>({value:e,label:e}))})]}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#64748b`,display:`block`,marginBottom:6},children:`分辨率`}),(0,$.jsx)(ZC,{value:T.resolution,onChange:e=>E(t=>t?{...t,resolution:e}:null),style:{width:`100%`},options:[`480p`,`720p`,`1080p`].map(e=>({value:e,label:e}))})]})]}):(0,$.jsxs)($.Fragment,{children:[(0,$.jsxs)(`div`,{style:{padding:12,borderRadius:10,background:`#f8f9fc`},children:[(0,$.jsx)(Q.Text,{style:{fontSize:11,color:`#94a3b8`,display:`block`},children:`尺寸`}),(0,$.jsx)(Q.Text,{strong:!0,children:T.record.imagePx||`-`})]}),(0,$.jsxs)(`div`,{style:{padding:12,borderRadius:10,background:`#f8f9fc`},children:[(0,$.jsx)(Q.Text,{style:{fontSize:11,color:`#94a3b8`,display:`block`},children:`比例`}),(0,$.jsx)(Q.Text,{strong:!0,children:T.record.imageProportion||`-`})]}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#64748b`,display:`block`,marginBottom:6},children:`分辨率`}),(0,$.jsx)(ZC,{value:T.image_size,onChange:e=>E(t=>t?{...t,image_size:e}:null),style:{width:`100%`},options:[`2K`,`4K`].map(e=>({value:e,label:e}))})]})]})}):null})]})},VY=`http://ceshi.apiforeign.minzhong.cn`.replace(/\/api\/?$/i,``).replace(/\/$/,``),HY=20,UY={image:`empty`,video:`empty`,videoCover:`empty`,references:{}},WY={pending:{color:`default`,text:`待处理`,icon:(0,$.jsx)(hj,{})},generating:{color:`warning`,text:`生成中`,icon:(0,$.jsx)(um,{spin:!0})},completed:{color:`success`,text:`已完成`,icon:(0,$.jsx)(bU,{})},failed:{color:`error`,text:`失败`,icon:(0,$.jsx)(CU,{})}},GY={timeout:`任务超时`,queued:`已入队`,preparing:`准备中`,creating_provider_task:`创建任务中`,waiting_remote:`等待生成`,result_ready:`结果就绪`,downloading:`下载中`,done:`完成`,download_failed:`下载失败`,polling:`轮询中`,failed:`失败`},KY={image:{text:`图片`,color:`purple`,icon:(0,$.jsx)(aW,{})},video:{text:`视频`,color:`geekblue`,icon:(0,$.jsx)(cq,{})}},qY=e=>/^(https?:)?\/\//i.test(e)||/^(blob|data):/i.test(e),JY=e=>!!e&&/^blob:/i.test(e.trim()),YY=e=>{if(!e)return``;let t=String(e).trim();return t?qY(t)?t:VY?`${VY}${t.startsWith(`/`)?t:`/${t}`}`:t.startsWith(`/`)?t:`/${t}`:``},XY=e=>e?e.length>12?`${e.slice(0,8)}...`:e:`-`,ZY=e=>e?BJ(e):`-`,QY=e=>e==null||e===``,$Y=e=>{if(!e||JY(e))return!1;try{let t=new URL(YY(e),window.location.origin),n=t.searchParams.get(`exp`)||t.searchParams.get(`expires`)||t.searchParams.get(`expire`)||t.searchParams.get(`expires_at`)||t.searchParams.get(`x-expires`);if(!n)return!1;let r=Number(n);if(!Number.isFinite(r))return!1;let i=r>1e10?r:r*1e3;return Date.now()>=i}catch{return!1}},eX=e=>e?JY(e)||$Y(e)?`invalid`:`checking`:`empty`,tX=e=>{let t=e.url||e.mediaUrl||e.fileUrl;return typeof t==`string`&&t.trim()?t.trim():void 0},nX=e=>{let t=String(e.type||e.mediaType||e.mimeType||``).toLowerCase(),n=tX(e)?.toLowerCase()||``;return t.includes(`video`)||/\.(mp4|mov|webm|m4v)(\?|$)/i.test(n)?`video`:t.includes(`image`)||/\.(png|jpe?g|webp|gif|bmp|svg)(\?|$)/i.test(n)?`image`:t||`unknown`},rX=(e,t)=>`${t}-${tX(e)||`empty`}`,iX=(e,t=`资源`)=>JY(e)?`本地临时素材已失效`:$Y(e)?`${t}链接已超时,请刷新列表或重新搜索后再查看`:`${t}加载失败,请刷新列表或重新搜索后再查看`,aX=({text:e,minHeight:t=240,compact:n=!1,action:r})=>(0,$.jsxs)(`div`,{style:{width:`100%`,minHeight:n?void 0:t,height:n?`100%`:void 0,borderRadius:n?10:12,background:`#f8f9fc`,border:`1px dashed #cbd5e1`,color:`#64748b`,display:`flex`,alignItems:`center`,justifyContent:`center`,flexDirection:`column`,gap:n?4:10,textAlign:`center`},children:[(0,$.jsx)(Q.Text,{style:{color:`#64748b`,fontSize:n?11:13},children:e}),r]}),oX=({label:e,value:t})=>(0,$.jsxs)(`div`,{style:{flex:1,minWidth:120},children:[(0,$.jsx)(Q.Text,{style:{fontSize:11,color:`#94a3b8`,display:`block`},children:e}),(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:14},children:QY(t)?`-`:t})]}),sX=()=>{let[e,t]=(0,x.useState)([]),[n,r]=(0,x.useState)(0),[i,a]=(0,x.useState)(!1),[o,s]=(0,x.useState)(1),[c,l]=(0,x.useState)(``),[u,d]=(0,x.useState)(``),[f,p]=(0,x.useState)(``),[m,h]=(0,x.useState)(``),[g,_]=(0,x.useState)(``),[v,y]=(0,x.useState)(``),[b,S]=(0,x.useState)(0),[C,w]=(0,x.useState)(null),[T,E]=(0,x.useState)(UY),[D,O]=(0,x.useState)(!1),k=(0,x.useRef)(null),A=(0,x.useCallback)(async()=>{a(!0);try{let e=await jJ({genType:u||void 0,status:c||void 0,userId:g||void 0,userName:v||void 0,page:o,pageSize:HY});t(e.items||[]),r(e.total||0)}catch(e){bP.error(e?.message||`加载创作记录失败`)}finally{a(!1)}},[u,c,o,g,v]);(0,x.useEffect)(()=>{A()},[A,b]),(0,x.useEffect)(()=>{if(!C){E(UY);return}let e=(C.mediaReferences||[]).reduce((e,t,n)=>{let r=tX(t);return e[rX(t,n)]=eX(r),e},{});k.current&&(k.current.pause(),k.current.currentTime=0),O(!1),E({image:eX(C.imageUrl),video:eX(C.videoUrl),videoCover:eX(C.videoCoverUrl),references:e})},[C]);let j=()=>{s(1),_(f.trim()),y(m.trim()),S(e=>e+1)},M=(0,x.useCallback)(e=>{w(e)},[]),N=()=>{k.current&&k.current.pause(),O(!1),w(null)},P=e=>{E(t=>({...t,...e}))},F=(e,t)=>{E(n=>({...n,references:{...n.references,[e]:t}}))},I=()=>{if(C?.videoUrl){if($Y(C.videoUrl)){P({video:`invalid`}),bP.warning(`视频链接已超时,请刷新列表或重新搜索后再查看`);return}O(!0),window.setTimeout(()=>{k.current?.play().catch(()=>{O(!1),P({video:`invalid`}),bP.warning(`视频播放失败,请确认资源链接是否仍然有效`)})},0)}},L=(e,t=`素材`)=>{if(!e){bP.warning(`${t}链接为空,暂无法查看`);return}if(JY(e)){bP.warning(`本地临时素材已失效,暂无法查看`);return}if($Y(e)){bP.warning(`${t}链接已超时,请刷新列表或重新搜索后再查看`);return}window.open(YY(e),`_blank`,`noopener,noreferrer`)},R=(0,x.useMemo)(()=>[{title:`用户`,key:`user`,width:150,render:(e,t)=>(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{strong:!0,style:{fontSize:13},children:t.userName||`未知用户`}),(0,$.jsx)(`div`,{style:{fontSize:11,color:`#94a3b8`},children:XY(t.userId)})]})},{title:`类型`,dataIndex:`genType`,width:90,render:e=>{let t=KY[e]||{text:e||`-`,color:`default`,icon:null};return(0,$.jsx)(CB,{color:t.color,icon:t.icon,children:t.text})}},{title:`提示词`,key:`prompt`,ellipsis:!0,render:(e,t)=>(0,$.jsx)(Bw,{title:t.originalPrompt,placement:`topLeft`,children:(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#475569`},ellipsis:!0,children:t.originalPrompt||`-`})})},{title:`参数`,key:`params`,width:180,render:(e,t)=>t.genType===`video`?t.duration||t.aspectRatio||t.resolution?(0,$.jsxs)(wj,{size:4,wrap:!0,children:[t.duration?(0,$.jsxs)(CB,{children:[t.duration,`s`]}):null,t.aspectRatio?(0,$.jsx)(CB,{children:t.aspectRatio}):null,t.resolution?(0,$.jsx)(CB,{children:t.resolution}):null]}):(0,$.jsx)(CB,{color:`default`,children:`无参数`}):t.imageSize||t.imageProportion||t.imagePx?(0,$.jsxs)(wj,{size:4,wrap:!0,children:[t.imageSize?(0,$.jsx)(CB,{children:t.imageSize}):null,t.imageProportion?(0,$.jsx)(CB,{children:t.imageProportion}):null,t.imagePx?(0,$.jsx)(CB,{children:t.imagePx}):null]}):(0,$.jsx)(CB,{color:`default`,children:`无参数`})},{title:`积分`,key:`credits`,width:130,render:(e,t)=>(0,$.jsxs)(`div`,{style:{fontSize:12},children:[(0,$.jsxs)(`div`,{style:{color:`#6366f1`},children:[`总: `,t.creditsCost||0]}),t.textCreditsCost>0?(0,$.jsxs)(`div`,{style:{color:`#f59e0b`},children:[`文字: `,t.textCreditsCost]}):null]})},{title:`状态`,dataIndex:`status`,width:100,render:e=>{let t=WY[e]||{color:`default`,text:e||`-`,icon:null};return(0,$.jsx)(CB,{color:t.color,icon:t.icon,children:t.text})}},{title:`阶段`,dataIndex:`pipelineStage`,width:120,render:e=>(0,$.jsx)(CB,{color:`blue`,children:GY[e]||e||`-`})},{title:`时间`,key:`time`,width:170,render:(e,t)=>(0,$.jsxs)(`div`,{style:{fontSize:12,color:`#94a3b8`},children:[(0,$.jsx)(`div`,{children:ZY(t.createdAt)}),t.generatedAt?(0,$.jsxs)(`div`,{style:{color:`#10b981`},children:[`生成: `,ZY(t.generatedAt)]}):null]})},{title:`操作`,key:`action`,width:90,fixed:`right`,render:(e,t)=>(0,$.jsx)(mD,{size:`small`,icon:(0,$.jsx)(AM,{}),onClick:()=>M(t),children:`详情`})}],[M]),z=C?KY[C.genType]||{text:C.genType||`-`,color:`default`,icon:null}:null,B=C?WY[C.status]||{color:`default`,text:C.status||`-`,icon:null}:null,V=()=>{if(!C||C.genType!==`image`||C.status!==`completed`)return null;if(!C.imageUrl)return(0,$.jsx)(aX,{text:`此图片任务暂无结果图片`,minHeight:260});if(T.image===`invalid`)return(0,$.jsx)(aX,{text:iX(C.imageUrl,`图片`),minHeight:260});let e=YY(C.imageUrl);return(0,$.jsxs)(`div`,{style:{position:`relative`,width:`100%`,minHeight:260,borderRadius:12,background:`#f8f9fc`,border:`1px solid #e2e8f0`,overflow:`hidden`,display:`flex`,alignItems:`center`,justifyContent:`center`},children:[(0,$.jsx)(`img`,{src:e,alt:`生成图片`,onLoad:()=>P({image:`valid`}),onError:()=>P({image:`invalid`}),style:{display:T.image===`valid`?`block`:`none`,width:`100%`,maxHeight:560,objectFit:`contain`,background:`#fff`}},`${C.id}-${e}`),T.image===`checking`?(0,$.jsx)(aX,{text:`图片加载检测中...`,minHeight:260}):null]})},H=()=>{if(!C||D||T.video===`invalid`)return null;let e=C.videoUrl?(0,$.jsx)(mD,{type:`primary`,shape:`circle`,size:`large`,icon:(0,$.jsx)(kG,{}),onClick:I,style:{boxShadow:`0 8px 20px rgba(15,23,42,0.25)`}}):null;if(!C.videoCoverUrl)return(0,$.jsx)(`div`,{style:{position:`absolute`,inset:0,borderRadius:12,overflow:`hidden`,background:`#f8f9fc`,display:`flex`,alignItems:`center`,justifyContent:`center`},children:(0,$.jsx)(aX,{text:`此视频无封面`,minHeight:340,action:e})});if(T.videoCover===`invalid`)return(0,$.jsx)(aX,{text:iX(C.videoCoverUrl,`视频封面`),minHeight:340,action:e});let t=YY(C.videoCoverUrl);return(0,$.jsxs)(`div`,{style:{position:`absolute`,inset:0,borderRadius:12,overflow:`hidden`,background:`#f8f9fc`,border:`1px solid #e2e8f0`,display:`flex`,alignItems:`center`,justifyContent:`center`},children:[(0,$.jsx)(`img`,{src:t,alt:`视频封面`,onLoad:()=>P({videoCover:`valid`}),onError:()=>P({videoCover:`invalid`}),style:{display:T.videoCover===`valid`?`block`:`none`,width:`100%`,height:`100%`,objectFit:`contain`,background:`#000`}},`${C.id}-cover-${t}`),T.videoCover===`checking`?(0,$.jsx)(aX,{text:`视频封面加载检测中...`,minHeight:340,action:e}):null,T.videoCover===`valid`?(0,$.jsx)(`div`,{style:{position:`absolute`,inset:0,display:`flex`,alignItems:`center`,justifyContent:`center`,background:`rgba(15,23,42,0.18)`},children:e}):null]})},U=()=>{if(!C||C.genType!==`video`||C.status!==`completed`)return null;if(!C.videoUrl)return(0,$.jsx)(aX,{text:`此视频任务暂无结果视频`,minHeight:340});if(T.video===`invalid`)return(0,$.jsx)(aX,{text:iX(C.videoUrl,`视频`),minHeight:340});let e=YY(C.videoUrl);return(0,$.jsxs)(`div`,{style:{position:`relative`,width:`100%`,height:340,borderRadius:12,background:`#000`,overflow:`hidden`,border:`1px solid #e2e8f0`},children:[(0,$.jsx)(`video`,{ref:k,src:e,preload:`metadata`,controls:D,onLoadedMetadata:()=>P({video:`valid`}),onCanPlay:()=>P({video:`valid`}),onPlaying:()=>{O(!0),P({video:`valid`})},onPause:()=>O(!1),onEnded:()=>O(!1),onError:()=>{O(!1),P({video:`invalid`})},style:{width:`100%`,height:`100%`,objectFit:`contain`,background:`#000`,opacity:+!!D,pointerEvents:D?`auto`:`none`}},`${C.id}-${e}`),H()]})},W=(e,t)=>{let n=tX(e),r=nX(e),i=rX(e,t),a=T.references[i]||eX(n),o=r===`image`,s=r===`video`,c=typeof e.name==`string`&&e.name?e.name:`参考素材 ${t+1}`;if(!n)return(0,$.jsxs)(`div`,{style:{width:94},children:[(0,$.jsx)(`div`,{style:{width:94,height:94},children:(0,$.jsx)(aX,{text:`无链接`,compact:!0})}),(0,$.jsx)(Q.Text,{ellipsis:!0,style:{display:`block`,marginTop:4,fontSize:11,color:`#94a3b8`},children:c})]},i);if(a===`invalid`)return(0,$.jsxs)(`div`,{style:{width:94},children:[(0,$.jsx)(`div`,{style:{width:94,height:94},children:(0,$.jsx)(aX,{text:JY(n)?`本地临时素材已失效`:`素材不可访问`,compact:!0})}),(0,$.jsx)(Bw,{title:c,children:(0,$.jsx)(Q.Text,{ellipsis:!0,style:{display:`block`,marginTop:4,fontSize:11,color:`#94a3b8`},children:c})})]},i);let l=YY(n);return(0,$.jsxs)(`div`,{style:{width:94},children:[(0,$.jsxs)(`div`,{role:`button`,tabIndex:0,title:`点击新页面查看素材`,onClick:()=>L(n,s?`视频素材`:o?`图片素材`:`素材`),onKeyDown:e=>{e.key===`Enter`&&L(n,s?`视频素材`:o?`图片素材`:`素材`)},style:{width:94,height:94,borderRadius:10,overflow:`hidden`,border:`1px solid #e2e8f0`,background:`#f8f9fc`,position:`relative`,display:`flex`,alignItems:`center`,justifyContent:`center`,cursor:`pointer`},children:[o?(0,$.jsx)(`img`,{src:l,alt:c,onLoad:()=>F(i,`valid`),onError:()=>F(i,`invalid`),style:{display:a===`valid`?`block`:`none`,width:`100%`,height:`100%`,objectFit:`cover`}}):null,s?(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`video`,{src:l,preload:`metadata`,onLoadedMetadata:()=>F(i,`valid`),onError:()=>F(i,`invalid`),style:{display:`none`}}),(0,$.jsxs)(`div`,{style:{color:`#64748b`,fontSize:12,textAlign:`center`},children:[(0,$.jsx)(kG,{style:{fontSize:18}}),(0,$.jsx)(`div`,{children:`视频素材`}),(0,$.jsx)(`div`,{style:{fontSize:10,color:`#94a3b8`,marginTop:2},children:`点击查看`})]})]}):null,!o&&!s?(0,$.jsxs)(`div`,{style:{color:`#64748b`,fontSize:12,textAlign:`center`},children:[`文件素材`,(0,$.jsx)(`div`,{style:{fontSize:10,color:`#94a3b8`,marginTop:2},children:`点击查看`})]}):null,a===`checking`&&o?(0,$.jsx)(`div`,{style:{position:`absolute`,inset:0,display:`flex`,alignItems:`center`,justifyContent:`center`,background:`#f8f9fc`},children:(0,$.jsx)(um,{style:{color:`#6366f1`}})}):null]}),(0,$.jsx)(Bw,{title:c,children:(0,$.jsx)(Q.Text,{ellipsis:!0,style:{display:`block`,marginTop:4,fontSize:11,color:`#94a3b8`},children:c})})]},i)};return(0,$.jsxs)(`div`,{style:{padding:24},children:[(0,$.jsx)(Mk,{title:(0,$.jsxs)(wj,{children:[(0,$.jsx)(aW,{}),(0,$.jsx)(`span`,{children:`创作记录管理`})]}),extra:(0,$.jsxs)(wj,{wrap:!0,children:[(0,$.jsx)(ZC,{allowClear:!0,placeholder:`状态筛选`,value:c||void 0,style:{width:140},onChange:e=>{l(e||``),s(1)},options:[{value:`generating`,label:`生成中`},{value:`completed`,label:`已完成`},{value:`failed`,label:`失败`}]}),(0,$.jsx)(ZC,{allowClear:!0,placeholder:`类型筛选`,value:u||void 0,style:{width:120},onChange:e=>{d(e||``),s(1)},options:[{value:`image`,label:`图片`},{value:`video`,label:`视频`}]}),(0,$.jsx)(QM,{placeholder:`用户ID`,value:f,onChange:e=>p(e.target.value),onPressEnter:j,style:{width:180},allowClear:!0}),(0,$.jsx)(QM,{placeholder:`用户名`,value:m,onChange:e=>h(e.target.value),onPressEnter:j,style:{width:160},allowClear:!0}),(0,$.jsx)(mD,{icon:(0,$.jsx)(KC,{}),onClick:j,children:`搜索`})]}),bordered:!1,style:{borderRadius:16},children:(0,$.jsx)(uB,{rowKey:`id`,loading:i,columns:R,dataSource:e,scroll:{x:1180},pagination:{current:o,pageSize:HY,total:n,showSizeChanger:!1,showTotal:e=>`共 ${e} 条`,onChange:e=>s(e)}})}),(0,$.jsx)(CP,{title:(0,$.jsxs)(wj,{children:[C?.genType===`video`?(0,$.jsx)(cq,{}):(0,$.jsx)(aW,{}),(0,$.jsx)(`span`,{children:`创作记录详情`})]}),open:!!C,onCancel:N,footer:null,width:900,destroyOnClose:!0,children:C?(0,$.jsxs)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:16,marginTop:12},children:[(0,$.jsxs)(`div`,{style:{display:`flex`,gap:12,flexWrap:`wrap`},children:[z?(0,$.jsx)(CB,{color:z.color,icon:z.icon,children:z.text}):null,B?(0,$.jsx)(CB,{color:B.color,icon:B.icon,children:B.text}):null,C.pipelineStage?(0,$.jsx)(CB,{color:`blue`,children:GY[C.pipelineStage]||C.pipelineStage}):null]}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,padding:12,borderRadius:10,background:`#f8f9fc`,flexWrap:`wrap`},children:[(0,$.jsx)(oX,{label:`用户名称`,value:C.userName||`未知用户`}),(0,$.jsx)(oX,{label:`用户ID`,value:C.userId||`-`}),(0,$.jsx)(oX,{label:`任务ID`,value:C.id})]}),(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#94a3b8`,display:`block`,marginBottom:6},children:`原始提示词`}),(0,$.jsx)(`div`,{style:{padding:12,borderRadius:10,background:`#f8f9fc`,lineHeight:1.6},children:C.originalPrompt||`-`})]}),C.genType===`video`?(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,padding:12,borderRadius:10,background:`#f8f9fc`,flexWrap:`wrap`},children:[(0,$.jsx)(oX,{label:`时长`,value:C.duration?`${C.duration}秒`:`-`}),(0,$.jsx)(oX,{label:`画面比例`,value:C.aspectRatio||`-`}),(0,$.jsx)(oX,{label:`分辨率`,value:C.resolution||`-`})]}):(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,padding:12,borderRadius:10,background:`#f8f9fc`,flexWrap:`wrap`},children:[(0,$.jsx)(oX,{label:`图片档位`,value:C.imageSize||`-`}),(0,$.jsx)(oX,{label:`图片比例`,value:C.imageProportion||`-`}),(0,$.jsx)(oX,{label:`像素尺寸`,value:C.imagePx||`-`})]}),C.engineSnapshot?(0,$.jsxs)(`div`,{style:{padding:12,borderRadius:10,background:`#f8f9fc`},children:[(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#94a3b8`,display:`block`,marginBottom:8},children:`引擎快照`}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,flexWrap:`wrap`},children:[(0,$.jsx)(oX,{label:`引擎名称`,value:C.engineSnapshot.name||`-`}),(0,$.jsx)(oX,{label:`服务商`,value:C.engineSnapshot.provider||`-`}),(0,$.jsx)(oX,{label:`模型`,value:C.engineSnapshot.modelName||`-`}),(0,$.jsx)(oX,{label:`引擎ID`,value:C.engineSnapshot.id||C.engineId||`-`})]})]}):null,(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,padding:12,borderRadius:10,background:`#f8f9fc`,flexWrap:`wrap`},children:[(0,$.jsx)(oX,{label:`总积分`,value:(0,$.jsx)(Q.Text,{strong:!0,style:{color:`#6366f1`},children:C.creditsCost||0})}),(0,$.jsx)(oX,{label:`文字积分`,value:`${C.textCreditsCost||0} (${C.textTokensUsed||0} tokens)`}),(0,$.jsx)(oX,{label:`图片 tokens`,value:C.imageTokensUsed??0}),(0,$.jsx)(oX,{label:`视频 tokens`,value:C.videoTokensUsed??0})]}),(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,padding:12,borderRadius:10,background:`#f8f9fc`,flexWrap:`wrap`},children:[(0,$.jsx)(oX,{label:`第三方任务ID`,value:C.providerTaskId||C.seedanceTaskId||`-`}),(0,$.jsx)(oX,{label:`轮询次数`,value:C.pollCount??0}),(0,$.jsx)(oX,{label:`重试次数`,value:C.retryCount??0})]}),!C?.mediaReferences||C.mediaReferences.length===0?null:(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#94a3b8`,display:`block`,marginBottom:6},children:`参考内容`}),(0,$.jsx)(`div`,{style:{display:`flex`,gap:10,flexWrap:`wrap`},children:C.mediaReferences.map((e,t)=>W(e,t))})]}),C.status===`completed`?(0,$.jsxs)(`div`,{children:[(0,$.jsx)(Q.Text,{style:{fontSize:12,color:`#94a3b8`,display:`block`,marginBottom:6},children:C.genType===`video`?`生成视频`:`生成图片`}),C.genType===`video`?U():V()]}):null,C.status===`failed`&&C.errorMessage?(0,$.jsx)(`div`,{style:{padding:12,borderRadius:10,background:`rgba(239,68,68,0.04)`,border:`1px solid rgba(239,68,68,0.15)`},children:(0,$.jsxs)(Q.Text,{style:{fontSize:12,color:`#ef4444`},children:[`错误信息: `,C.errorMessage]})}):null,(0,$.jsxs)(`div`,{style:{display:`flex`,gap:16,fontSize:12,color:`#94a3b8`,flexWrap:`wrap`},children:[(0,$.jsxs)(`span`,{children:[`创建: `,ZY(C.createdAt)]}),(0,$.jsxs)(`span`,{children:[`生成: `,ZY(C.generatedAt)]})]})]}):(0,$.jsx)(xC,{description:`暂无详情`})})]})},cX=({children:e})=>{let{user:t,loading:n,checkAuth:r}=MJ();return(0,x.useEffect)(()=>{!localStorage.getItem(`auth_token`)&&!n&&!t&&(window.location.href=`/login`)},[t,n]),n?(0,$.jsx)(`div`,{style:{display:`flex`,justifyContent:`center`,alignItems:`center`,height:`100vh`},children:(0,$.jsx)(aP,{size:`large`})}):t?(0,$.jsx)($.Fragment,{children:e}):(window.location.href=`/login`,null)};(0,Em.createRoot)(document.getElementById(`root`)).render((0,$.jsx)(()=>{let{checkAuth:e}=MJ();return(0,x.useEffect)(()=>{e()},[]),(0,$.jsx)(Kp,{locale:xq.default,theme:{token:{colorPrimary:`#6366f1`,borderRadius:8},components:{Button:{controlHeight:36,controlHeightLG:44},Card:{boxShadow:`0 1px 3px rgba(0,0,0,0.04)`},Table:{headerBg:`#fafbfc`}}},children:(0,$.jsx)(fx,{children:(0,$.jsx)(mn,{children:(0,$.jsxs)(Et,{children:[(0,$.jsx)(wt,{path:`/login`,element:(0,$.jsx)(RJ,{})}),(0,$.jsxs)(wt,{path:`/`,element:(0,$.jsx)(cX,{children:(0,$.jsx)(LJ,{})}),children:[(0,$.jsx)(wt,{index:!0,element:(0,$.jsx)(zJ,{})}),(0,$.jsx)(wt,{path:`users`,element:(0,$.jsx)(VJ,{})}),(0,$.jsx)(wt,{path:`credit-records`,element:(0,$.jsx)(KJ,{})}),(0,$.jsx)(wt,{path:`models`,element:(0,$.jsx)(HJ,{})}),(0,$.jsx)(wt,{path:`credit-ratios`,element:(0,$.jsx)(lY,{})}),(0,$.jsx)(wt,{path:`video-engines`,element:(0,$.jsx)(tY,{})}),(0,$.jsx)(wt,{path:`image-engines`,element:(0,$.jsx)(oY,{})}),(0,$.jsx)(wt,{path:`industries`,element:(0,$.jsx)($J,{})}),(0,$.jsx)(wt,{path:`menu-configs`,element:(0,$.jsx)(mY,{})}),(0,$.jsx)(wt,{path:`recharge-packages`,element:(0,$.jsx)(_Y,{})}),(0,$.jsx)(wt,{path:`payment`,element:(0,$.jsx)(qJ,{})}),(0,$.jsx)(wt,{path:`settings`,element:(0,$.jsx)(UJ,{})}),(0,$.jsx)(wt,{path:`notifications`,element:(0,$.jsx)(WJ,{})}),(0,$.jsx)(wt,{path:`oauthapp-list`,element:(0,$.jsx)(xY,{})}),(0,$.jsx)(wt,{path:`operation-logs`,element:(0,$.jsx)(yY,{})}),(0,$.jsx)(wt,{path:`generation-records`,element:(0,$.jsx)(BY,{})}),(0,$.jsx)(wt,{path:`generation-ai`,element:(0,$.jsx)(sX,{})})]}),(0,$.jsx)(wt,{path:`*`,element:(0,$.jsx)(St,{to:`/`,replace:!0})})]})})})})},{})); \ No newline at end of file diff --git a/video-gen-admin/dist/index.html b/video-gen-admin/dist/index.html index 3fe48f30..c44185f8 100644 --- a/video-gen-admin/dist/index.html +++ b/video-gen-admin/dist/index.html @@ -1,14 +1,13 @@ - - - - - - - VideoGen.AI 管理后台 - - - -
- - - + + + + + + + VideoGen.AI 管理后台 + + + +
+ + diff --git a/video-gen-admin/src/App.tsx b/video-gen-admin/src/App.tsx index cbf94893..3f35093b 100644 --- a/video-gen-admin/src/App.tsx +++ b/video-gen-admin/src/App.tsx @@ -18,6 +18,7 @@ import AdminCreditRatios from './pages/AdminCreditRatios'; import AdminMenuConfig from './pages/AdminMenuConfig'; import AdminRechargePackages from './pages/AdminRechargePackages'; import AdminOperationLogs from './pages/AdminOperationLogs'; +import AdminOauthAppList from './pages/AdminOauthAppList'; import AdminGenerationRecords from './pages/AdminGenerationRecords'; import AdminGenerationAiRecords from './pages/AdminGenerationAiRecords'; import { useAdminStore } from './store'; @@ -77,6 +78,7 @@ const App = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/video-gen-admin/src/api/index.ts b/video-gen-admin/src/api/index.ts index 649dc3cb..e325c04d 100644 --- a/video-gen-admin/src/api/index.ts +++ b/video-gen-admin/src/api/index.ts @@ -278,6 +278,43 @@ export async function getOperationLogs(page?: number): Promise<{ total: number; return api.get(`/admin/operation-logs${q}`); } +// ── oauthapp List ────────────────────────────────────── + +export async function getOauthAppList(page?: number): Promise<{ total: number; items: any[] }> { + const q = page ? `?page=${page}` : ''; + return api.get(`/admin/user-oauth-apps/list${q}`); +} + +export async function createOauthApp(data: { + app_id: string; + secret: string; + open_type: number; + count?: number; + auth_url?: string; + company?: string; +}): Promise { + return api.post('/admin/user-oauth-apps/create', data); +} + +export async function getOauthApp(id: string): Promise { + return api.get(`/admin/user-oauth-apps/read/${id}`); +} + +export async function updateOauthApp(id: string, data: { + app_id?: string; + secret?: string; + open_type?: number; + count?: number; + auth_url?: string; + company?: string; +}): Promise { + return api.post(`/admin/user-oauth-apps/update/${id}`, data); +} + +export async function deleteOauthApp(id: string): Promise { + await api.get(`/admin/user-oauth-apps/delete/${id}`); +} + // ── Generation Records (Admin) ───────────────────────────── export async function getAdminGenerationRecords(params?: { diff --git a/video-gen-admin/src/pages/AdminOauthAppList.tsx b/video-gen-admin/src/pages/AdminOauthAppList.tsx new file mode 100644 index 00000000..5e79d181 --- /dev/null +++ b/video-gen-admin/src/pages/AdminOauthAppList.tsx @@ -0,0 +1,406 @@ +import React, { useEffect, useState } from 'react'; +import { + Button, Card, Space, Table, Tag, Typography, message, Modal, Form, Input, Select, InputNumber, +} from 'antd'; +import { + HistoryOutlined, ReloadOutlined, PlusOutlined, EyeOutlined, EditOutlined, DeleteOutlined, +} from '@ant-design/icons'; +import { getOauthAppList, createOauthApp, getOauthApp, updateOauthApp, deleteOauthApp } from '../api'; +import { formatDate } from '../utils/formatDate'; + +interface OAuthApp { + id: string; + appId: string; + secret: string; + status: number; + count: number; + openType: number; + authUrl?: string; + company?: string; + createBy: string; + createdAt: string; + updatedAt: string; +} + +const AdminOauthAppList: React.FC = () => { + const [apps, setApps] = useState([]); + const [total, setTotal] = useState(0); + const [loading, setLoading] = useState(false); + const [page, setPage] = useState(1); + const [createModalVisible, setCreateModalVisible] = useState(false); + const [detailModalVisible, setDetailModalVisible] = useState(false); + const [updateModalVisible, setUpdateModalVisible] = useState(false); + const [currentApp, setCurrentApp] = useState(null); + const [form] = Form.useForm(); + const [updateForm] = Form.useForm(); + + const load = async (p?: number) => { + setLoading(true); + try { + const res = await getOauthAppList(p || page); + setApps(res.items || []); + setTotal(res.total || 0); + } catch { + message.error('加载授权应用列表失败'); + } finally { + setLoading(false); + } + }; + + const handleCreate = async () => { + try { + const values = await form.validateFields(); + await createOauthApp({ + app_id: values.app_id, + secret: values.secret, + open_type: values.open_type, + count: values.count, + auth_url: values.auth_url, + company: values.company, + }); + message.success('创建成功'); + setCreateModalVisible(false); + form.resetFields(); + load(); + } catch (e: any) { + message.error(e?.message || '创建失败'); + } + }; + + const handleDetail = async (id: string) => { + try { + const app = await getOauthApp(id); + setCurrentApp(app); + setDetailModalVisible(true); + } catch (e: any) { + message.error(e?.message || '获取详情失败'); + } + }; + + const handleUpdate = async (id: string) => { + try { + const app = await getOauthApp(id); + setCurrentApp(app); + updateForm.setFieldsValue({ + app_id: app.appId, + secret: app.secret, + open_type: app.openType, + count: app.count, + auth_url: app.authUrl, + company: app.company, + }); + setUpdateModalVisible(true); + } catch (e: any) { + message.error(e?.message || '获取详情失败'); + } + }; + + const handleSaveUpdate = async () => { + if (!currentApp) return; + try { + const values = await updateForm.validateFields(); + await updateOauthApp(currentApp.id, { + app_id: values.app_id, + secret: values.secret, + open_type: values.open_type, + count: values.count, + auth_url: values.auth_url, + company: values.company, + }); + message.success('更新成功'); + setUpdateModalVisible(false); + updateForm.resetFields(); + load(); + } catch (e: any) { + message.error(e?.message || '更新失败'); + } + }; + + const handleDelete = (id: string) => { + Modal.confirm({ + title: '确认删除', + content: '确定要删除这个授权应用吗?', + okText: '删除', + okType: 'danger', + cancelText: '取消', + onOk: async () => { + try { + await deleteOauthApp(id); + message.success('删除成功'); + load(); + } catch (e: any) { + message.error(e?.message || '删除失败'); + } + }, + }); + }; + + useEffect(() => { load(); }, []); + const columns = [ + { title: 'ID', dataIndex: 'id', + render: (v: string) => {v}, + }, + { title: '应用ID', dataIndex: 'appId', + render: (v: string) => {v}, + }, + { title: '应用密钥', dataIndex: 'secret', + render: (v: string) => {v}, + }, + { title: '开户方式', dataIndex: 'openType', + render: (v: number) => { + const typeMap: Record = { + 1: '千川', 2: '广告', 3: '本地推', 4: '星图', 5: '快手代理商', + 6: '巨量星图', 7: '巨量服务单', 8: '腾讯服务单', 9: '腾讯营销K2', 10: '腾讯营销K3' + }; + return {typeMap[v] || v}; + }, + }, + { title: '归属公司', dataIndex: 'company', + render: (v: string) => {v}, + }, + + { title: '授权次数', dataIndex: 'count', + render: (v: number) => {v}, + }, + { title: '状态', dataIndex: 'status', + render: (v: number) => {v === 1 ? '正常' : '禁用'}, + }, + { title: '授权URL', dataIndex: 'authUrl', + render: (v: string) => {v}, + }, + { title: '创建人', dataIndex: 'createBy', + render: (v: string) => {v}, + }, + { title: '创建时间', dataIndex: 'createdAt', + render: (v: string) => {formatDate(v)}, + }, + { title: '更新时间', dataIndex: 'updatedAt', + render: (v: string) => {formatDate(v)}, + }, + { title: '操作', + render: (_: any, record: OAuthApp) => ( + + + + + + ), + }, + ]; + + return ( +
+ +
+ + + 授权应用列表 + + + + + +
+ `共 ${t} 条记录`, + onChange: (p) => { setPage(p); load(p); }, + }} + scroll={{ x: 800 }} + /> + + + { + setCreateModalVisible(false); + form.resetFields(); + }} + okText="创建" + cancelText="取消" + width={600} + > +
+ + + + + + + + + + + + + + + + + + + +
+ + { + setDetailModalVisible(false); + setCurrentApp(null); + }} + okText="关闭" + cancelText="取消" + width={600} + > + {currentApp && ( +
+

ID: {currentApp.id}

+

应用ID: {currentApp.appId}

+

应用密钥: {currentApp.secret}

+

开户方式: {(() => { + const typeMap: Record = { + 1: '千川', 2: '广告', 3: '本地推', 4: '星图', 5: '快手代理商', + 6: '巨量星图', 7: '巨量服务单', 8: '腾讯服务单', 9: '腾讯营销K2', 10: '腾讯营销K3' + }; + return typeMap[currentApp.openType] || currentApp.openType; + })()}

+

归属公司: {currentApp.company || '-'}

+

授权次数: {currentApp.count}

+

状态: {currentApp.status === 1 ? '正常' : '禁用'}

+

授权URL: {currentApp.authUrl || '-'}

+

创建人: {currentApp.createBy}

+

创建时间: {formatDate(currentApp.createdAt)}

+

更新时间: {formatDate(currentApp.updatedAt)}

+
+ )} +
+ + { + setUpdateModalVisible(false); + updateForm.resetFields(); + setCurrentApp(null); + }} + okText="更新" + cancelText="取消" + width={600} + > +
+ + + + + + + + + + + + + + + + + + + +
+ + ); +}; + +export default AdminOauthAppList; \ No newline at end of file diff --git a/video-gen-admin/tsconfig.tsbuildinfo b/video-gen-admin/tsconfig.tsbuildinfo index 6d63e8b8..a2a9b11e 100644 --- a/video-gen-admin/tsconfig.tsbuildinfo +++ b/video-gen-admin/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/env.d.ts","./src/main.tsx","./src/api/client.ts","./src/api/crypto.ts","./src/api/index.ts","./src/pages/admincreditratios.tsx","./src/pages/admincreditrecords.tsx","./src/pages/admindashboard.tsx","./src/pages/admingenerationrecords.tsx","./src/pages/adminimageengines.tsx","./src/pages/adminindustries.tsx","./src/pages/adminlayout.tsx","./src/pages/adminloginpage.tsx","./src/pages/adminmenuconfig.tsx","./src/pages/adminmodels.tsx","./src/pages/adminnotificationmanager.tsx","./src/pages/adminoperationlogs.tsx","./src/pages/adminpaymentconfig.tsx","./src/pages/adminrechargepackages.tsx","./src/pages/adminsettings.tsx","./src/pages/adminusers.tsx","./src/pages/adminvideoengines.tsx","./src/store/index.ts","./src/types/index.ts","./src/utils/formatdate.ts"],"version":"6.0.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/env.d.ts","./src/main.tsx","./src/api/client.ts","./src/api/crypto.ts","./src/api/index.ts","./src/pages/admincreditratios.tsx","./src/pages/admincreditrecords.tsx","./src/pages/admindashboard.tsx","./src/pages/admingenerationairecords.tsx","./src/pages/admingenerationrecords.tsx","./src/pages/adminimageengines.tsx","./src/pages/adminindustries.tsx","./src/pages/adminlayout.tsx","./src/pages/adminloginpage.tsx","./src/pages/adminmenuconfig.tsx","./src/pages/adminmodels.tsx","./src/pages/adminnotificationmanager.tsx","./src/pages/adminoauthapplist.tsx","./src/pages/adminoperationlogs.tsx","./src/pages/adminpaymentconfig.tsx","./src/pages/adminrechargepackages.tsx","./src/pages/adminsettings.tsx","./src/pages/adminusers.tsx","./src/pages/adminvideoengines.tsx","./src/store/index.ts","./src/types/index.ts","./src/utils/formatdate.ts"],"version":"6.0.3"} \ No newline at end of file diff --git a/video-gen-app/src/api/index.ts b/video-gen-app/src/api/index.ts index 762389f1..627c4f06 100644 --- a/video-gen-app/src/api/index.ts +++ b/video-gen-app/src/api/index.ts @@ -2,18 +2,14 @@ * API abstraction layer. * Switches between mock data and real backend based on VITE_USE_MOCK env var. */ - import { api, setToken, clearToken } from './client'; import * as mock from './mock'; import type { User, CreditRecord, Project, GenerationRecord, OptimizeParams, GenerateParams, OptimizeResult, Industry, IndustryConfig, AdminUser, AdminStats, ModelConfig, SystemConfig, AdminNotification, } from '../types'; - const USE_MOCK = import.meta.env.VITE_USE_MOCK === 'true'; - // ── Auth ────────────────────────────────────────────────── - export async function login(username: string, password: string, captchaToken?: string, rememberMe?: boolean): Promise { if (USE_MOCK) return mock.mockLogin({ username, password }); const res = await api.post<{ accessToken: string; user: User }>('/auth/login', { username, password, captcha_token: captchaToken, remember_me: rememberMe || false }, false); @@ -26,19 +22,16 @@ export async function phonelogin(phone: string, code: string): Promise { setToken(res.accessToken); return res.user; } - export async function register(phone: string, code: string, password: string): Promise { const res = await api.post<{ accessToken: string; user: User }>('/auth/register', { phone, code, password }, false); setToken(res.accessToken); return res.user; } - export async function logout(): Promise { if (USE_MOCK) return mock.mockLogout(); await api.post('/auth/logout'); clearToken(); } - export async function getUser(): Promise { if (USE_MOCK) return mock.mockGetUser(); try { @@ -47,38 +40,30 @@ export async function getUser(): Promise { return null; } } - export async function changePassword(oldPwd: string, newPwd: string): Promise { if (USE_MOCK) return; await api.post('/auth/change-password', { old_password: oldPwd, new_password: newPwd }); } - // ── Projects ────────────────────────────────────────────── - export async function getProjects(): Promise { if (USE_MOCK) return mock.mockGetProjects(); return api.get('/projects'); } - export async function createProject(name: string, industry: Industry): Promise { if (USE_MOCK) return mock.mockCreateProject(name, industry); return api.post('/projects', { name, industry }); } - export async function deleteProject(id: string): Promise { if (USE_MOCK) return mock.mockDeleteProject(id); await api.delete(`/projects/${id}`); } - // ── Generation ──────────────────────────────────────────── - export interface GenerationRecordPageListOut { page: number; pageSize: number; total: number; items: GenerationRecord[]; } - export interface GetRecordsPageParams { projectId?: string; status?: string; @@ -86,7 +71,6 @@ export interface GetRecordsPageParams { pageSize?: number; signal?: AbortSignal; } - export async function getRecordsPage(params: GetRecordsPageParams = {}): Promise { const page = params.page && params.page > 0 ? params.page : 1; const pageSize = params.pageSize && params.pageSize > 0 ? params.pageSize : 10; @@ -105,7 +89,6 @@ export async function getRecordsPage(params: GetRecordsPageParams = {}): Promise items: filtered.slice(start, start + pageSize), }; } - const query = new URLSearchParams(); if (params.projectId) query.set('project_id', params.projectId); if (params.status) query.set('status', params.status); @@ -114,7 +97,6 @@ export async function getRecordsPage(params: GetRecordsPageParams = {}): Promise return api.get(`/generation-records?${query.toString()}`, { signal: params.signal }); } - export async function optimizePrompt( projectId: string, params: OptimizeParams ): Promise { @@ -131,7 +113,6 @@ export async function optimizePrompt( image_px: params.image_px || null, }); } - export async function uploadImage(file: File): Promise<{ url: string; filename: string }> { const form = new FormData(); form.append('file', file); @@ -145,7 +126,6 @@ export async function uploadImage(file: File): Promise<{ url: string; filename: const data = await res.json(); return { url: data.url, filename: data.filename }; } - export async function uploadVideo(file: File): Promise<{ url: string; filename: string }> { const form = new FormData(); form.append('file', file); @@ -159,15 +139,12 @@ export async function uploadVideo(file: File): Promise<{ url: string; filename: const data = await res.json(); return { url: data.url, filename: data.filename }; } - export async function deleteUpload(url: string): Promise { await api.post(`/generation-records/delete-file?url=${encodeURIComponent(url)}`); } - export async function updateRecordPrompt(recordId: string, optimizedPrompt: string): Promise { await api.put(`/generation-records/${recordId}/prompt`, { optimized_prompt: optimizedPrompt }); } - export async function generateVideo(recordId: string, params: GenerateParams): Promise { if (USE_MOCK) return mock.mockGenerateVideo(recordId); return api.post(`/generation-records/${recordId}/generate`, { @@ -175,36 +152,27 @@ export async function generateVideo(recordId: string, params: GenerateParams): P resolution: params.resolution, }); } - // ── Credits ─────────────────────────────────────────────── - export async function getCredits(): Promise<{ credits: number; records: CreditRecord[] }> { if (USE_MOCK) return mock.mockGetCredits(); return api.get('/credits'); } - // ── Captcha ─────────────────────────────────────────────── - export async function getSliderCaptcha(): Promise<{ captcha_id: string; bg_image: string; slider_image: string }> { if (USE_MOCK) return { captcha_id: 'mock', bg_image: '', slider_image: '' }; return api.get('/captcha/slider', false); } - export async function verifyCaptcha(captchaId: string, x: number): Promise { if (USE_MOCK) return 'mock-token'; const res = await api.post<{ token: string }>('/captcha/verify', { captcha_id: captchaId, x_offset: x }, false); return res.token; } - // ── Site Info ───────────────────────────────────────────── - export async function getSiteInfo(): Promise<{ siteName: string; siteLogo: string; userAgreementUrl: string; privacyPolicyUrl: string }> { if (USE_MOCK) return { siteName: 'VideoGen.AI', siteLogo: '', userAgreementUrl: '', privacyPolicyUrl: '' }; return api.get('/auth/site-info', false); } - // ── Video Engines ───────────────────────────────────────── - export async function getVideoEngines(): Promise<{ items: { id: string; name: string; provider: string; supportedRatios: string[]; supportedResolutions: string[]; supportedDurations: number[] }[] }> { if (USE_MOCK) return { items: [{ id: 'mock', name: 'Seedance', provider: 'seedance', supportedRatios: ['16:9', '9:16', '1:1', '4:3', '3:4', '21:9'], supportedResolutions: ['480p', '720p', '1080p'], supportedDurations: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] }] }; return api.get('/video-engines'); @@ -213,89 +181,69 @@ export async function getVideoEngines(): Promise<{ items: { id: string; name: st export async function getParameters(): Promise { return api.get('/image-engines'); } - - // ── SMS ─────────────────────────────────────────────────── - export async function sendSms(phone: string, scene: string): Promise { if (USE_MOCK) return; await api.post('/sms/send', { phone, scene: scene }, false); } - export async function verifySms(phone: string, code: string): Promise<{ token: string }> { if (USE_MOCK) return { token: 'mock-sms-token' }; return api.post('/sms/verify', { phone, code }, false); } - // ── Notifications ───────────────────────────────────────── - export async function getNotifications(): Promise { if (USE_MOCK) return mock.mockGetAdminNotifications(); return api.get('/notifications'); } - export async function getUnreadCount(): Promise { if (USE_MOCK) return mock.mockGetAdminNotifications().then(n => n.filter(x => !x.isRead).length); const res = await api.get<{ count: number }>('/notifications/unread-count'); return res.count; } - export async function markNotificationRead(id: string): Promise { if (USE_MOCK) return; await api.put(`/notifications/${id}/read`); } - // ── Admin ───────────────────────────────────────────────── - export async function getAdminStats(): Promise { if (USE_MOCK) return mock.mockGetAdminStats(); return api.get('/admin/stats'); } - export async function getAdminUsers(search?: string): Promise { if (USE_MOCK) return mock.mockGetAdminUsers(search); const q = search ? `?search=${encodeURIComponent(search)}` : ''; return api.get(`/admin/users${q}`); } - export async function adjustCredits(userId: string, amount: number, description: string): Promise { if (USE_MOCK) return mock.mockAdjustCredits(userId, amount, description); await api.post(`/admin/users/${userId}/credits`, { amount, description }); } - export async function toggleUserStatus(userId: string, isActive: boolean): Promise { if (USE_MOCK) return mock.mockToggleUserStatus(userId, isActive); await api.put(`/admin/users/${userId}/status`, { is_active: isActive }); } - export async function getModelConfigs(): Promise { if (USE_MOCK) return mock.mockGetModelConfigs(); return api.get('/admin/model-configs'); } - export async function saveModelConfig(config: Partial & { id?: string }): Promise { if (USE_MOCK) return mock.mockSaveModelConfig(config as any); if (config.id) return api.put(`/admin/model-configs/${config.id}`, config); return api.post('/admin/model-configs', config); } - export async function deleteModelConfig(id: string): Promise { if (USE_MOCK) return mock.mockDeleteModelConfig(id); await api.delete(`/admin/model-configs/${id}`); } - export async function getSystemConfigs(): Promise { if (USE_MOCK) return mock.mockGetSystemConfigs(); return api.get('/admin/system-configs'); } - export async function updateSystemConfig(id: string, value: string): Promise { if (USE_MOCK) return mock.mockUpdateSystemConfig(id, value); await api.put(`/admin/system-configs/${id}`, { value }); } - // ── Industries ───────────────────────────────────────────── - export async function getIndustries(): Promise { const data = await api.get('/industries'); return data.map((item: any) => { @@ -306,20 +254,14 @@ export async function getIndustries(): Promise { return { ...item, optionGroups }; }); } - // ── Menu Config ──────────────────────────────────────────── - export async function getMenuConfigs(): Promise { return api.get('/menu-configs'); } - // ── Recharge Packages ────────────────────────────────────── - export async function getRechargePackages(): Promise { return api.get('/recharge-packages'); } - - export async function getCreditRatios(): Promise { return api.get('/credits/ratios'); } @@ -327,10 +269,7 @@ export async function getCreditRatios(): Promise { export async function getEngine(): Promise { return api.get('/generation-ai/engines'); } - // ── Generation AI Tasks ──────────────────────────────────── - - // 创建ai生成任务 export async function createGenerationTask(params: any): Promise { return api.post('/generation-ai/tasks', params); @@ -347,20 +286,38 @@ export async function gethistory(Pagebreak: any): Promise { export async function gethistoryItems(Pagebreak: any): Promise { return api.get('/generation-ai/history/'+Pagebreak); } - // 删除ai对话历史记录 export async function deleteHistory(id: string): Promise { await api.delete(`/generation-ai/tasks/${id}`); } - - export async function calculateCredits(): Promise { return api.get('/credits/credit-ratios'); } - - - // 获取验证码 export async function getSendcode(phone: string): Promise { return api.post('/sms/send', { phone }); } +export interface OAuthAppParam { + page: number; + pageSize: number; + open_type?: string; + status?: string; + app_id?: string; +} +export interface OAuthAppList { + page: number; + pageSize: number; + total: number; + data: any[]; +} +// 获取用户列表 +export async function getAuthorizationList(params: OAuthAppParam): Promise { + const query = new URLSearchParams(); + query.set('page', String(params.page)); + query.set('page_size', String(params.pageSize)); + if (params.open_type) query.set('open_type', params.open_type); + if (params.status) query.set('status', params.status); + if (params.app_id) query.set('app_id', params.app_id); + + return api.get(`/admin/user-oauth-apps/list?${query.toString()}`); +} diff --git a/video-gen-app/src/pages/AuthorizationPage.tsx b/video-gen-app/src/pages/AuthorizationPage.tsx index 7580b700..2fd48c79 100644 --- a/video-gen-app/src/pages/AuthorizationPage.tsx +++ b/video-gen-app/src/pages/AuthorizationPage.tsx @@ -1,12 +1,16 @@ import React, { useEffect, useState, useLayoutEffect, useRef, useCallback } from 'react'; import { Button, Table, Checkbox, Tag, Space, message, Modal } from 'antd'; import { PlusOutlined, CheckCircleOutlined, ClockCircleOutlined, CiCircleOutlined, EyeOutlined, XOutlined } from '@ant-design/icons'; -// 模拟授权数据 -const mockAuthorizations = [ - { id: '1867060028363785', status: 'active', description: '用户张三的API授权' }, - { id: '1867059757929740', status: 'pending', description: '用户李四的API授权' }, - { id: '1867059808785418', status: 'active', description: '用户王五的API授权' }, -]; +import type { OAuthAppParam } from '../api/index'; +import { getAuthorizationList } from '../api/index'; + +// 授权数据类型 +interface AuthorizationData { + id: string; + status: string; + description: string; +} + // 状态配置 const statusConfig = { active: { label: '已授权', color: 'green', icon: CheckCircleOutlined }, @@ -30,12 +34,60 @@ const consumptionTypeConfig = { audio: { label: '音频转换', color: 'purple' }, image: { label: '图片处理', color: 'green' }, }; + +// 模拟表头接口返回数据 +const mockTableHeaderResponse = { + code: 200, + message: 'success', + data: [ + { title: '序号', dataIndex: 'index', key: 'index', width: 80, fixed: 'left' }, + { title: '消耗ID', dataIndex: 'id', key: 'id', ellipsis: true }, + { title: '授权ID', dataIndex: 'authorizationId', key: 'authorizationId', ellipsis: true }, + { title: '消耗类型', dataIndex: 'type', key: 'type', width: 120 }, + { title: '消耗金额', dataIndex: 'amount', key: 'amount', width: 120 }, + { title: '消耗描述', dataIndex: 'description', key: 'description', ellipsis: true }, + { title: '消耗时间', dataIndex: 'createdAt', key: 'createdAt', width: 160, fixed: 'right' }, + ], +}; + +// 模拟获取表头接口 +const fetchTableHeader = () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(mockTableHeaderResponse); + }, 500); + }); +}; + const AuthorizationPage: React.FC = () => { - const [authorizations, setAuthorizations] = useState(mockAuthorizations); + const [authorizations, setAuthorizations] = useState([]); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [loading, setLoading] = useState(false); + const [listLoading, setListLoading] = useState(false); const [showConsumptionModal, setShowConsumptionModal] = useState(false); const [consumptionRecords, setConsumptionRecords] = useState(mockConsumptionRecords); + const [consumptionColumns, setConsumptionColumns] = useState([]); + const [headerLoading, setHeaderLoading] = useState(false); + + // 页面初始化时获取授权列表 + useEffect(() => { + const loadData = async () => { + setListLoading(true); + try { + const params: OAuthAppParam = { + page: 1, + pageSize: 10, + }; + const response = await getAuthorizationList(params); + setAuthorizations(response.data || []); + } catch (error) { + message.error('获取授权列表失败'); + } finally { + setListLoading(false); + } + }; + loadData(); + }, []); // 状态标签渲染 const renderStatus = (status: string) => { @@ -97,7 +149,7 @@ const AuthorizationPage: React.FC = () => { dataIndex: 'operation', key: 'operation', width: 120, - render: (_: any, record: typeof mockAuthorizations[0]) => ( + render: (_: any, record: AuthorizationData) => ( @@ -114,6 +166,60 @@ const AuthorizationPage: React.FC = () => { } ]; + // 获取表头数据 + const handleFetchHeader = async () => { + setHeaderLoading(true); + try { + const response = await fetchTableHeader(); + if (response.code === 200) { + // 对特定列添加render函数 + const columnsWithRender = response.data.map(col => { + if (col.dataIndex === 'index') { + return { + ...col, + render: (text: number) => {text}, + }; + } + if (col.dataIndex === 'id') { + return { + ...col, + render: (text: string) => {text}, + }; + } + if (col.dataIndex === 'type') { + return { + ...col, + render: (text: string) => { + const config = consumptionTypeConfig[text as keyof typeof consumptionTypeConfig]; + return {config?.label}; + }, + }; + } + if (col.dataIndex === 'amount') { + return { + ...col, + render: (text: number) => {text} 元, + }; + } + return col; + }); + setConsumptionColumns(columnsWithRender); + } else { + message.error(response.message); + } + } catch (error) { + message.error('获取表头失败'); + } finally { + setHeaderLoading(false); + } + }; + + // 打开消耗弹窗 + const handleOpenConsumptionModal = () => { + setShowConsumptionModal(true); + handleFetchHeader(); + }; + // 处理点击授权按钮 const handleAuthorize = () => { if (selectedRowKeys.length === 0) { @@ -134,14 +240,12 @@ const AuthorizationPage: React.FC = () => { }, 800); }; - // 准备表格数据(添加序号) const tableData = authorizations.map((item, index) => ({ ...item, index: index + 1, key: item.id, })); - // 准备消耗记录表格数据(添加序号) const consumptionTableData = consumptionRecords.map((item, index) => ({ ...item, index: index + 1, @@ -184,6 +288,7 @@ const AuthorizationPage: React.FC = () => {
{
{text}, - }, - { - title: '消耗ID', - dataIndex: 'id', - key: 'id', - ellipsis: true, - render: (text: string) => {text}, - }, - { - title: '授权ID', - dataIndex: 'authorizationId', - key: 'authorizationId', - ellipsis: true, - }, - { - title: '消耗类型', - dataIndex: 'type', - key: 'type', - width: 120, - render: (text: string) => { - const config = consumptionTypeConfig[text as keyof typeof consumptionTypeConfig]; - return {config?.label}; - }, - }, - { - title: '消耗金额', - dataIndex: 'amount', - key: 'amount', - width: 120, - render: (text: number) => {text} 元, - }, - { - title: '消耗描述', - dataIndex: 'description', - key: 'description', - ellipsis: true, - }, - { - title: '消耗时间', - dataIndex: 'createdAt', - key: 'createdAt', - width: 160, - }, - ]} + columns={consumptionColumns} + loading={headerLoading} pagination={{ pageSize: 10, showSizeChanger: true, From 74266a126c90f8a10b36fdcf19793d2097172ccd Mon Sep 17 00:00:00 2001 From: GinHa <15201596918@163.com> Date: Thu, 11 Jun 2026 17:54:40 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=E6=8B=86=E9=95=9C=E5=A4=8D=E5=88=BB?= =?UTF-8?q?=E5=BC=80=E5=8F=91=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dbb11c0b5a0a_add_shot_replicate_tables.py | 161 ++ video-gen-api/app/api/v1/__init__.py | 2 + .../app/api/v1/hot_opening_replicate.py | 148 +- video-gen-api/app/api/v1/shot_replicate.py | 668 +++++++ video-gen-api/app/config.py | 56 + video-gen-api/app/enums/__init__.py | 1 + video-gen-api/app/enums/shot_replicate.py | 92 + video-gen-api/app/models/__init__.py | 3 + video-gen-api/app/models/base.py | 21 +- .../app/models/shot_replicate_segment.py | 80 + .../app/models/shot_replicate_task_set.py | 60 + video-gen-api/app/schemas/shot_replicate.py | 734 +++++++ .../celery_download_recovery_service.py | 319 +-- .../generation_module_hook_service.py | 12 + .../services/generation_recovery_service.py | 494 ++++- .../app/services/generation_refund_service.py | 2 +- .../services/hot_opening_replicate_service.py | 56 +- .../hot_opening_video_prompt_service.py | 2 +- .../services/module_generation_log_service.py | 37 +- .../app/services/redis_registry_service.py | 354 ++++ .../services/shot_replicate_flow_service.py | 1747 +++++++++++++++++ .../shot_replicate_recovery_service.py | 128 ++ .../shot_replicate_taskset_service.py | 529 +++++ .../services/shot_video_analysis_service.py | 521 +++++ .../app/services/shot_video_split_service.py | 173 ++ .../services/upload_video_asset_service.py | 211 ++ video-gen-api/app/tasks/__init__.py | 4 +- video-gen-api/app/tasks/async_runner.py | 126 +- video-gen-api/app/tasks/celery_app.py | 44 +- .../app/tasks/generation_create_tasks.py | 14 +- .../app/tasks/generation_download_tasks.py | 2 +- .../app/tasks/generation_poll_tasks.py | 227 ++- .../app/tasks/shot_replicate_flow_tasks.py | 42 + .../app/tasks/shot_replicate_tasks.py | 505 +++++ 34 files changed, 7105 insertions(+), 470 deletions(-) create mode 100644 video-gen-api/alembic/versions/dbb11c0b5a0a_add_shot_replicate_tables.py create mode 100644 video-gen-api/app/api/v1/shot_replicate.py create mode 100644 video-gen-api/app/enums/shot_replicate.py create mode 100644 video-gen-api/app/models/shot_replicate_segment.py create mode 100644 video-gen-api/app/models/shot_replicate_task_set.py create mode 100644 video-gen-api/app/schemas/shot_replicate.py create mode 100644 video-gen-api/app/services/redis_registry_service.py create mode 100644 video-gen-api/app/services/shot_replicate_flow_service.py create mode 100644 video-gen-api/app/services/shot_replicate_recovery_service.py create mode 100644 video-gen-api/app/services/shot_replicate_taskset_service.py create mode 100644 video-gen-api/app/services/shot_video_analysis_service.py create mode 100644 video-gen-api/app/services/shot_video_split_service.py create mode 100644 video-gen-api/app/services/upload_video_asset_service.py create mode 100644 video-gen-api/app/tasks/shot_replicate_flow_tasks.py create mode 100644 video-gen-api/app/tasks/shot_replicate_tasks.py diff --git a/video-gen-api/alembic/versions/dbb11c0b5a0a_add_shot_replicate_tables.py b/video-gen-api/alembic/versions/dbb11c0b5a0a_add_shot_replicate_tables.py new file mode 100644 index 00000000..8ef2b4b3 --- /dev/null +++ b/video-gen-api/alembic/versions/dbb11c0b5a0a_add_shot_replicate_tables.py @@ -0,0 +1,161 @@ +"""add shot replicate tables + +Revision ID: dbb11c0b5a0a +Revises: 9ac2212e1b8e +Create Date: 2026-06-11 14:59:26.871902 +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = 'dbb11c0b5a0a' +down_revision: Union[str, None] = '9ac2212e1b8e' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('shot_replicate_task_sets', + sa.Column('id', sa.String(length=32), nullable=False), + sa.Column('user_id', sa.String(length=32), nullable=False), + sa.Column('title', sa.String(length=160), nullable=True), + sa.Column('video_url', sa.String(length=512), nullable=False), + sa.Column('video_path', sa.String(length=512), nullable=False), + sa.Column('video_duration_seconds', sa.Float(), nullable=False), + sa.Column('status', sa.String(length=32), nullable=False), + sa.Column('analysis_status', sa.String(length=32), nullable=False), + sa.Column('split_status', sa.String(length=32), nullable=False), + sa.Column('original_video_content', sa.Text(), nullable=True), + sa.Column('original_video_category', sa.String(length=160), nullable=True), + sa.Column('original_video_audience', sa.Text(), nullable=True), + sa.Column('ai_suggestion_json', sa.JSON().with_variant(postgresql.JSONB(astext_type=sa.Text()), 'postgresql'), nullable=True), + sa.Column('analysis_raw_json', sa.JSON().with_variant(postgresql.JSONB(astext_type=sa.Text()), 'postgresql'), nullable=True), + sa.Column('analysis_result_json', sa.JSON().with_variant(postgresql.JSONB(astext_type=sa.Text()), 'postgresql'), nullable=True), + sa.Column('segment_count', sa.Integer(), nullable=False), + sa.Column('completed_segment_count', sa.Integer(), nullable=False), + sa.Column('failed_segment_count', sa.Integer(), nullable=False), + sa.Column('analysis_error_message', sa.Text(), nullable=True), + sa.Column('split_error_message', sa.Text(), nullable=True), + sa.Column('idempotency_key', sa.String(length=64), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_shot_replicate_task_sets_analysis_status', 'shot_replicate_task_sets', ['analysis_status'], unique=False) + op.create_index('idx_shot_replicate_task_sets_split_status', 'shot_replicate_task_sets', ['split_status'], unique=False) + op.create_index('idx_shot_replicate_task_sets_status', 'shot_replicate_task_sets', ['status'], unique=False) + op.create_index('idx_shot_replicate_task_sets_user_created', 'shot_replicate_task_sets', ['user_id', 'created_at'], unique=False) + op.create_index(op.f('ix_shot_replicate_task_sets_analysis_status'), 'shot_replicate_task_sets', ['analysis_status'], unique=False) + op.create_index(op.f('ix_shot_replicate_task_sets_deleted_at'), 'shot_replicate_task_sets', ['deleted_at'], unique=False) + op.create_index(op.f('ix_shot_replicate_task_sets_idempotency_key'), 'shot_replicate_task_sets', ['idempotency_key'], unique=False) + op.create_index(op.f('ix_shot_replicate_task_sets_split_status'), 'shot_replicate_task_sets', ['split_status'], unique=False) + op.create_index(op.f('ix_shot_replicate_task_sets_status'), 'shot_replicate_task_sets', ['status'], unique=False) + op.create_index(op.f('ix_shot_replicate_task_sets_user_id'), 'shot_replicate_task_sets', ['user_id'], unique=False) + op.create_index('uq_shot_replicate_task_sets_user_idempotency', 'shot_replicate_task_sets', ['user_id', 'idempotency_key'], unique=True, postgresql_where=sa.text('deleted_at IS NULL AND idempotency_key IS NOT NULL')) + op.create_table('shot_replicate_segments', + sa.Column('id', sa.String(length=32), nullable=False), + sa.Column('task_set_id', sa.String(length=32), nullable=False), + sa.Column('user_id', sa.String(length=32), nullable=False), + sa.Column('segment_index', sa.Integer(), nullable=False), + sa.Column('source_mode', sa.String(length=32), nullable=False), + sa.Column('start_second', sa.Float(), nullable=False), + sa.Column('end_second', sa.Float(), nullable=False), + sa.Column('duration_seconds', sa.Float(), nullable=False), + sa.Column('time_node', sa.String(length=64), nullable=False), + sa.Column('split_status', sa.String(length=32), nullable=False), + sa.Column('analysis_status', sa.String(length=32), nullable=False), + sa.Column('replicate_status', sa.String(length=32), nullable=False), + sa.Column('segment_video_url', sa.String(length=512), nullable=True), + sa.Column('segment_video_path', sa.String(length=512), nullable=True), + sa.Column('original_video_content', sa.Text(), nullable=True), + sa.Column('original_video_category', sa.String(length=160), nullable=True), + sa.Column('original_video_audience', sa.Text(), nullable=True), + sa.Column('segment_content', sa.Text(), nullable=True), + sa.Column('segment_category', sa.String(length=160), nullable=True), + sa.Column('segment_audience', sa.Text(), nullable=True), + sa.Column('analysis_json', sa.JSON().with_variant(postgresql.JSONB(astext_type=sa.Text()), 'postgresql'), nullable=True), + sa.Column('ai_suggestion_json', sa.JSON().with_variant(postgresql.JSONB(astext_type=sa.Text()), 'postgresql'), nullable=True), + sa.Column('module_project_id', sa.String(length=32), nullable=True), + sa.Column('split_celery_task_id', sa.String(length=160), nullable=True), + sa.Column('split_enqueued_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('split_started_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('split_lease_until', sa.DateTime(timezone=True), nullable=True), + sa.Column('split_next_retry_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('split_retry_count', sa.Integer(), nullable=False), + sa.Column('split_last_error', sa.Text(), nullable=True), + sa.Column('split_completed_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('analysis_error_message', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['task_set_id'], ['shot_replicate_task_sets.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_shot_replicate_segments_analysis_status', 'shot_replicate_segments', ['analysis_status'], unique=False) + op.create_index('idx_shot_replicate_segments_project', 'shot_replicate_segments', ['module_project_id'], unique=False) + op.create_index('idx_shot_replicate_segments_replicate_status', 'shot_replicate_segments', ['replicate_status'], unique=False) + op.create_index('idx_shot_replicate_segments_source_mode', 'shot_replicate_segments', ['source_mode'], unique=False) + op.create_index('idx_shot_replicate_segments_split_status', 'shot_replicate_segments', ['split_status'], unique=False) + op.create_index('idx_shot_replicate_segments_task_set', 'shot_replicate_segments', ['task_set_id', 'segment_index'], unique=False) + op.create_index('idx_shot_replicate_segments_user_created', 'shot_replicate_segments', ['user_id', 'created_at'], unique=False) + op.create_index(op.f('ix_shot_replicate_segments_analysis_status'), 'shot_replicate_segments', ['analysis_status'], unique=False) + op.create_index(op.f('ix_shot_replicate_segments_deleted_at'), 'shot_replicate_segments', ['deleted_at'], unique=False) + op.create_index(op.f('ix_shot_replicate_segments_module_project_id'), 'shot_replicate_segments', ['module_project_id'], unique=False) + op.create_index(op.f('ix_shot_replicate_segments_replicate_status'), 'shot_replicate_segments', ['replicate_status'], unique=False) + op.create_index(op.f('ix_shot_replicate_segments_segment_index'), 'shot_replicate_segments', ['segment_index'], unique=False) + op.create_index(op.f('ix_shot_replicate_segments_source_mode'), 'shot_replicate_segments', ['source_mode'], unique=False) + op.create_index(op.f('ix_shot_replicate_segments_split_celery_task_id'), 'shot_replicate_segments', ['split_celery_task_id'], unique=False) + op.create_index(op.f('ix_shot_replicate_segments_split_lease_until'), 'shot_replicate_segments', ['split_lease_until'], unique=False) + op.create_index(op.f('ix_shot_replicate_segments_split_next_retry_at'), 'shot_replicate_segments', ['split_next_retry_at'], unique=False) + op.create_index(op.f('ix_shot_replicate_segments_split_status'), 'shot_replicate_segments', ['split_status'], unique=False) + op.create_index(op.f('ix_shot_replicate_segments_task_set_id'), 'shot_replicate_segments', ['task_set_id'], unique=False) + op.create_index(op.f('ix_shot_replicate_segments_user_id'), 'shot_replicate_segments', ['user_id'], unique=False) + op.create_index('uq_shot_replicate_segments_task_set_index_active', 'shot_replicate_segments', ['task_set_id', 'segment_index'], unique=True, postgresql_where=sa.text('deleted_at IS NULL')) + op.drop_column('user_oauth_app', 'count') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('user_oauth_app', sa.Column('count', sa.BIGINT(), autoincrement=False, nullable=False, comment='应用最大可以授权多少个用户')) + op.drop_index('uq_shot_replicate_segments_task_set_index_active', table_name='shot_replicate_segments', postgresql_where=sa.text('deleted_at IS NULL')) + op.drop_index(op.f('ix_shot_replicate_segments_user_id'), table_name='shot_replicate_segments') + op.drop_index(op.f('ix_shot_replicate_segments_task_set_id'), table_name='shot_replicate_segments') + op.drop_index(op.f('ix_shot_replicate_segments_split_status'), table_name='shot_replicate_segments') + op.drop_index(op.f('ix_shot_replicate_segments_split_next_retry_at'), table_name='shot_replicate_segments') + op.drop_index(op.f('ix_shot_replicate_segments_split_lease_until'), table_name='shot_replicate_segments') + op.drop_index(op.f('ix_shot_replicate_segments_split_celery_task_id'), table_name='shot_replicate_segments') + op.drop_index(op.f('ix_shot_replicate_segments_source_mode'), table_name='shot_replicate_segments') + op.drop_index(op.f('ix_shot_replicate_segments_segment_index'), table_name='shot_replicate_segments') + op.drop_index(op.f('ix_shot_replicate_segments_replicate_status'), table_name='shot_replicate_segments') + op.drop_index(op.f('ix_shot_replicate_segments_module_project_id'), table_name='shot_replicate_segments') + op.drop_index(op.f('ix_shot_replicate_segments_deleted_at'), table_name='shot_replicate_segments') + op.drop_index(op.f('ix_shot_replicate_segments_analysis_status'), table_name='shot_replicate_segments') + op.drop_index('idx_shot_replicate_segments_user_created', table_name='shot_replicate_segments') + op.drop_index('idx_shot_replicate_segments_task_set', table_name='shot_replicate_segments') + op.drop_index('idx_shot_replicate_segments_split_status', table_name='shot_replicate_segments') + op.drop_index('idx_shot_replicate_segments_source_mode', table_name='shot_replicate_segments') + op.drop_index('idx_shot_replicate_segments_replicate_status', table_name='shot_replicate_segments') + op.drop_index('idx_shot_replicate_segments_project', table_name='shot_replicate_segments') + op.drop_index('idx_shot_replicate_segments_analysis_status', table_name='shot_replicate_segments') + op.drop_table('shot_replicate_segments') + op.drop_index('uq_shot_replicate_task_sets_user_idempotency', table_name='shot_replicate_task_sets', postgresql_where=sa.text('deleted_at IS NULL AND idempotency_key IS NOT NULL')) + op.drop_index(op.f('ix_shot_replicate_task_sets_user_id'), table_name='shot_replicate_task_sets') + op.drop_index(op.f('ix_shot_replicate_task_sets_status'), table_name='shot_replicate_task_sets') + op.drop_index(op.f('ix_shot_replicate_task_sets_split_status'), table_name='shot_replicate_task_sets') + op.drop_index(op.f('ix_shot_replicate_task_sets_idempotency_key'), table_name='shot_replicate_task_sets') + op.drop_index(op.f('ix_shot_replicate_task_sets_deleted_at'), table_name='shot_replicate_task_sets') + op.drop_index(op.f('ix_shot_replicate_task_sets_analysis_status'), table_name='shot_replicate_task_sets') + op.drop_index('idx_shot_replicate_task_sets_user_created', table_name='shot_replicate_task_sets') + op.drop_index('idx_shot_replicate_task_sets_status', table_name='shot_replicate_task_sets') + op.drop_index('idx_shot_replicate_task_sets_split_status', table_name='shot_replicate_task_sets') + op.drop_index('idx_shot_replicate_task_sets_analysis_status', table_name='shot_replicate_task_sets') + op.drop_table('shot_replicate_task_sets') + # ### end Alembic commands ### diff --git a/video-gen-api/app/api/v1/__init__.py b/video-gen-api/app/api/v1/__init__.py index e80ee5b1..a46e5c52 100644 --- a/video-gen-api/app/api/v1/__init__.py +++ b/video-gen-api/app/api/v1/__init__.py @@ -16,6 +16,7 @@ from app.api.v1.video_engines import router as video_engines_router from app.api.v1.image_engines import router as image_engines_router from app.api.v1.generation_ai import router as generation_ai_router from app.api.v1.hot_opening_replicate import router as hot_opening_replicate_router +from app.api.v1.shot_replicate import router as shot_replicate_router from app.api.v1.test import router as test_router from app.api.v1.user_oauth import router as user_oauth_router from app.api.v1.user_oauth_app import router as user_oauth_app_router @@ -37,6 +38,7 @@ api_router.include_router(video_engines_router) api_router.include_router(image_engines_router) api_router.include_router(generation_ai_router) api_router.include_router(hot_opening_replicate_router) +api_router.include_router(shot_replicate_router) api_router.include_router(test_router) api_router.include_router(user_oauth_router) api_router.include_router(user_oauth_app_router) diff --git a/video-gen-api/app/api/v1/hot_opening_replicate.py b/video-gen-api/app/api/v1/hot_opening_replicate.py index e53bc8d4..5704d42d 100644 --- a/video-gen-api/app/api/v1/hot_opening_replicate.py +++ b/video-gen-api/app/api/v1/hot_opening_replicate.py @@ -1,10 +1,14 @@ from __future__ import annotations +from types import SimpleNamespace + from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query +from sqlalchemy import inspect as sa_inspect from sqlalchemy.ext.asyncio import AsyncSession from app.dependencies import get_current_user, get_db from app.models.user import User +from app.enums.hot_opening_replicate import ModuleCodeEnum from app.schemas.hot_opening_replicate import ( HotOpeningActionOut, HotOpeningDeleteOut, @@ -35,14 +39,94 @@ from app.services.hot_opening_replicate_service import ( update_hot_opening_material_input, update_hot_opening_video_prompt_schema, ) +from app.services.module_generation_log_service import log_module_error from app.tasks.celery_app import celery_app +MODULE = ModuleCodeEnum.HOT_OPENING_REPLICATE.value + + + +def _safe_user_id(user: object | None) -> str | None: + """从 ORM 对象中安全取用户ID,避免 rollback/commit 后访问过期属性触发 MissingGreenlet。""" + if user is None: + return None + try: + value = getattr(user, "__dict__", {}).get("id") + if value is not None: + return str(value) + except Exception: + pass + try: + identity = sa_inspect(user).identity + if identity: + return str(identity[0]) + except Exception: + pass + return None + + +def _safe_user_is_admin(user: object | None) -> bool: + """安全判断管理员身份;如果对象属性已过期,保守按普通用户处理。""" + if user is None: + return False + try: + data = getattr(user, "__dict__", {}) + if "is_admin" in data: + return bool(data.get("is_admin")) + except Exception: + pass + return False + + +def _user_context(user: object | None) -> SimpleNamespace: + return SimpleNamespace(id=_safe_user_id(user), is_admin=_safe_user_is_admin(user)) + router = APIRouter( prefix="/hot-opening-replications", tags=["hot-opening-replications"], ) + + +def _log_api_error( + *, + event_type: str, + current_user: User | None = None, + project_id: str | None = None, + step_id: str | None = None, + message: str | None = None, + exc: BaseException | None = None, + detail: dict | None = None, +) -> None: + log_module_error( + module=MODULE, + event_type=event_type, + project_id=project_id, + step_id=step_id, + user_id=_safe_user_id(current_user), + message=message, + detail=detail, + exc=exc, + ) + + +def _log_api_exception_from_locals(exc: BaseException, local_values: dict, message: str) -> None: + current_user = local_values.get("current_user") + project_id = local_values.get("project_id_value") or local_values.get("project_id") + step_id = local_values.get("step_id_value") or local_values.get("step_id") + req = local_values.get("req") + detail = {"request": req.model_dump() if hasattr(req, "model_dump") else str(req) if req is not None else None} + _log_api_error( + event_type="API_REQUEST_FAILED", + current_user=current_user if isinstance(current_user, User) else None, + project_id=str(project_id) if project_id else None, + step_id=str(step_id) if step_id else None, + message=message, + detail=detail, + exc=exc, + ) + async def _reload_project_detail( db: AsyncSession, current_user: User, @@ -52,7 +136,7 @@ async def _reload_project_detail( project = await _get_project_for_user( db, project_id=project_id, - user=current_user, + user=_user_context(current_user), for_update=False, populate_existing=True, ) @@ -72,14 +156,33 @@ async def _mark_dispatch_failed_and_raise( try: await mark_hot_opening_step_dispatch_failed( db, - current_user=current_user, + current_user=_user_context(current_user), project_id=project_id, step_id=step_id, error_message=message, ) await db.commit() - except Exception: + except Exception as exc: await db.rollback() + _log_api_error( + event_type="CELERY_DISPATCH_MARK_FAILED", + current_user=current_user, + project_id=project_id, + step_id=step_id, + message="Celery 投递失败后标记步骤失败也失败", + detail={"dispatch_error": message}, + exc=exc, + ) + log_module_error( + module=MODULE, + event_type="CELERY_DISPATCH_FAILED", + project_id=project_id, + step_id=step_id, + user_id=_safe_user_id(current_user), + message=message, + detail={"reason": "celery_dispatch_failed"}, + error=message, + ) raise HTTPException(status_code=503, detail=message) @@ -118,6 +221,7 @@ async def create_task( raise except Exception as exc: await db.rollback() + _log_api_exception_from_locals(exc, locals(), f"创建爆款开头复刻项目失败: {exc}") raise HTTPException(status_code=500, detail=f"创建爆款开头复刻项目失败: {exc}") return await _reload_project_detail(db, current_user, project_id_value) @@ -185,6 +289,7 @@ async def update_material( raise except Exception as exc: await db.rollback() + _log_api_exception_from_locals(exc, locals(), f"修改素材输入失败: {exc}") raise HTTPException(status_code=500, detail=f"修改素材输入失败: {exc}") return HotOpeningActionOut( @@ -228,6 +333,7 @@ async def update_image_prompt( raise except Exception as exc: await db.rollback() + _log_api_exception_from_locals(exc, locals(), f"修改图片 AI 提词失败: {exc}") raise HTTPException(status_code=500, detail=f"修改图片 AI 提词失败: {exc}") return HotOpeningActionOut( @@ -300,6 +406,14 @@ async def generate_image_prompt( ): _ = req if celery_app is None: + _log_api_error( + event_type="CELERY_DISABLED", + current_user=current_user, + project_id=project_id, + step_id=step_id, + message="Celery未启用:请配置 REDIS_URL 或 CELERY_BROKER_URL 后启动 worker", + detail={"api": "hot_opening_replicate"}, + ) raise HTTPException(status_code=503, detail="Celery未启用:请配置 REDIS_URL 或 CELERY_BROKER_URL 后启动 worker") try: @@ -312,6 +426,7 @@ async def generate_image_prompt( raise except Exception as exc: await db.rollback() + _log_api_exception_from_locals(exc, locals(), f"图片提词任务创建失败: {exc}") raise HTTPException(status_code=500, detail=f"图片提词任务创建失败: {exc}") from app.tasks.hot_opening_replicate_tasks import start_image_prompt_optimize @@ -354,6 +469,14 @@ async def generate_image( db: AsyncSession = Depends(get_db), ): if celery_app is None: + _log_api_error( + event_type="CELERY_DISABLED", + current_user=current_user, + project_id=project_id, + step_id=step_id, + message="Celery未启用:请配置 REDIS_URL 或 CELERY_BROKER_URL 后启动 worker", + detail={"api": "hot_opening_replicate"}, + ) raise HTTPException(status_code=503, detail="Celery未启用:请配置 REDIS_URL 或 CELERY_BROKER_URL 后启动 worker") try: @@ -369,6 +492,7 @@ async def generate_image( raise except Exception as exc: await db.rollback() + _log_api_exception_from_locals(exc, locals(), f"图片生成任务创建失败: {exc}") raise HTTPException(status_code=500, detail=f"图片生成任务创建失败: {exc}") from app.tasks.generation_create_tasks import chatapi_create_generation_task @@ -411,6 +535,14 @@ async def generate_video_prompt( db: AsyncSession = Depends(get_db), ): if celery_app is None: + _log_api_error( + event_type="CELERY_DISABLED", + current_user=current_user, + project_id=project_id, + step_id=step_id, + message="Celery未启用:请配置 REDIS_URL 或 CELERY_BROKER_URL 后启动 worker", + detail={"api": "hot_opening_replicate"}, + ) raise HTTPException(status_code=503, detail="Celery未启用:请配置 REDIS_URL 或 CELERY_BROKER_URL 后启动 worker") try: @@ -423,6 +555,7 @@ async def generate_video_prompt( raise except Exception as exc: await db.rollback() + _log_api_exception_from_locals(exc, locals(), f"视频提词任务创建失败: {exc}") raise HTTPException(status_code=500, detail=f"视频提词任务创建失败: {exc}") from app.tasks.hot_opening_replicate_tasks import start_video_prompt_optimize @@ -466,6 +599,14 @@ async def generate_video( db: AsyncSession = Depends(get_db), ): if celery_app is None: + _log_api_error( + event_type="CELERY_DISABLED", + current_user=current_user, + project_id=project_id, + step_id=step_id, + message="Celery未启用:请配置 REDIS_URL 或 CELERY_BROKER_URL 后启动 worker", + detail={"api": "hot_opening_replicate"}, + ) raise HTTPException(status_code=503, detail="Celery未启用:请配置 REDIS_URL 或 CELERY_BROKER_URL 后启动 worker") try: @@ -481,6 +622,7 @@ async def generate_video( raise except Exception as exc: await db.rollback() + _log_api_exception_from_locals(exc, locals(), f"视频生成任务创建失败: {exc}") raise HTTPException(status_code=500, detail=f"视频生成任务创建失败: {exc}") from app.tasks.generation_create_tasks import chatapi_create_generation_task diff --git a/video-gen-api/app/api/v1/shot_replicate.py b/video-gen-api/app/api/v1/shot_replicate.py new file mode 100644 index 00000000..f54095ed --- /dev/null +++ b/video-gen-api/app/api/v1/shot_replicate.py @@ -0,0 +1,668 @@ +from __future__ import annotations + +from types import SimpleNamespace + +from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query +from sqlalchemy import inspect as sa_inspect +from sqlalchemy.ext.asyncio import AsyncSession + +from app.dependencies import get_current_user, get_db +from app.models.user import User +from app.enums.shot_replicate import ModuleCodeEnum +from app.schemas.shot_replicate import ( + ShotReplicateActionOut, + ShotReplicateDeleteOut, + ShotReplicateGenerateImagePromptRequest, + ShotReplicateGenerateImageRequest, + ShotReplicateGenerateVideoPromptRequest, + ShotReplicateGenerateVideoRequest, + ShotReplicateImagePromptUpdateRequest, + ShotReplicateMaterialUpdateRequest, + ShotReplicateSpecOut, + ShotReplicateTaskDetailOut, + ShotReplicateVideoPromptSchemaUpdateRequest, + ShotSegmentDetailOut, + ShotSegmentListOut, + ShotSegmentReplicationCreateRequest, + ShotSplitByAIOut, + ShotSplitByAIRequest, + ShotSplitCustomOut, + ShotSplitCustomRequest, + ShotTaskSetCreate, + ShotTaskSetDetailOut, + ShotTaskSetListOut, +) +from app.services.shot_replicate_flow_service import ( + _get_project_for_user, + create_shot_replicate_project_from_segment, + delete_shot_replicate_project, + generate_image_from_prompt, + generate_video_from_prompt, + mark_shot_replicate_step_dispatch_failed, + project_to_detail_out, + submit_image_prompt_optimize, + submit_video_prompt_optimize, + update_shot_replicate_image_prompt, + update_shot_replicate_material_input, + update_shot_replicate_video_prompt_schema, +) +from app.services.shot_replicate_taskset_service import ( + create_custom_segment, + create_segments_by_ai, + create_task_set, + get_segment_for_user, + list_segments, + list_task_sets, + segment_detail, + task_set_detail, +) +from app.services.module_generation_log_service import log_module_error, log_module_event_file +from app.tasks.celery_app import celery_app + +MODULE = ModuleCodeEnum.SHOT_REPLICATE.value + + + +def _safe_user_id(user: object | None) -> str | None: + """从 ORM 对象中安全取用户ID,避免 rollback/commit 后访问过期属性触发 MissingGreenlet。""" + if user is None: + return None + try: + value = getattr(user, "__dict__", {}).get("id") + if value is not None: + return str(value) + except Exception: + pass + try: + identity = sa_inspect(user).identity + if identity: + return str(identity[0]) + except Exception: + pass + return None + + +def _safe_user_is_admin(user: object | None) -> bool: + """安全判断管理员身份;如果对象属性已过期,保守按普通用户处理。""" + if user is None: + return False + try: + data = getattr(user, "__dict__", {}) + if "is_admin" in data: + return bool(data.get("is_admin")) + except Exception: + pass + return False + + +def _user_context(user: object | None) -> SimpleNamespace: + return SimpleNamespace(id=_safe_user_id(user), is_admin=_safe_user_is_admin(user)) + +router = APIRouter(prefix="/shot-replications", tags=["shot-replications"]) + + + + +def _log_api_error( + *, + event_type: str, + current_user: User | None = None, + project_id: str | None = None, + step_id: str | None = None, + message: str | None = None, + exc: BaseException | None = None, + detail: dict | None = None, +) -> None: + log_module_error( + module=MODULE, + event_type=event_type, + project_id=project_id, + step_id=step_id, + user_id=_safe_user_id(current_user), + message=message, + detail=detail, + exc=exc, + ) + + +def _log_api_exception_from_locals(exc: BaseException, local_values: dict, message: str) -> None: + current_user = local_values.get("current_user") + project_id = local_values.get("project_id_value") or local_values.get("project_id") or local_values.get("task_set_id") or local_values.get("task_set_id_value") + step_id = local_values.get("step_id_value") or local_values.get("step_id") or local_values.get("segment_id") or local_values.get("segment_id_value") + req = local_values.get("req") + detail = {"api": local_values.get("__name__"), "request": req.model_dump() if hasattr(req, "model_dump") else str(req) if req is not None else None} + _log_api_error( + event_type="API_REQUEST_FAILED", + current_user=current_user if isinstance(current_user, User) else None, + project_id=str(project_id) if project_id else None, + step_id=str(step_id) if step_id else None, + message=message, + detail=detail, + exc=exc, + ) + +async def _reload_project_detail(db: AsyncSession, current_user: User, project_id: str) -> ShotReplicateTaskDetailOut: + project = await _get_project_for_user( + db, + project_id=project_id, + user=_user_context(current_user), + for_update=False, + populate_existing=True, + ) + return await project_to_detail_out(db, project) + + +async def _mark_dispatch_failed_and_raise( + db: AsyncSession, + *, + current_user: User, + project_id: str, + step_id: str | None, + message: str, +) -> None: + if step_id: + try: + await mark_shot_replicate_step_dispatch_failed( + db, + current_user=_user_context(current_user), + project_id=project_id, + step_id=step_id, + error_message=message, + ) + await db.commit() + except Exception as exc: + await db.rollback() + _log_api_error( + event_type="CELERY_DISPATCH_MARK_FAILED", + current_user=current_user, + project_id=project_id, + step_id=step_id, + message="Celery 投递失败后标记步骤失败也失败", + detail={"dispatch_error": message}, + exc=exc, + ) + log_module_error( + module=MODULE, + event_type="CELERY_DISPATCH_FAILED", + project_id=project_id, + step_id=step_id, + user_id=_safe_user_id(current_user), + message=message, + detail={"reason": "celery_dispatch_failed"}, + error=message, + ) + raise HTTPException(status_code=503, detail=message) + + +@router.get( + "/spec", + response_model=ShotReplicateSpecOut, + summary="查询拆镜复刻模块状态枚举和步骤 JSON 结构说明", +) +async def get_spec(): + return ShotReplicateSpecOut() + + +@router.post( + "/task-sets", + response_model=ShotTaskSetDetailOut, + summary="创建拆镜总任务集并异步分析原视频", +) +async def create_shot_task_set( + req: ShotTaskSetCreate = Body(...), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + try: + task_set = await create_task_set(db, current_user=current_user, req=req) + task_set_id = task_set.id + await db.commit() + except HTTPException: + await db.rollback() + raise + except Exception as exc: + await db.rollback() + _log_api_exception_from_locals(exc, locals(), f"创建拆镜总任务集失败: {exc}") + raise HTTPException(status_code=500, detail=f"创建拆镜总任务集失败: {exc}") + + if celery_app: + try: + from app.tasks.shot_replicate_tasks import analyze_original_video + + analyze_original_video.apply_async(args=[task_set_id], queue="gen_chatapi_create", countdown=0) + except Exception as exc: + # 分析任务投递失败时保留总任务,前端可稍后通过恢复/重试处理。 + _log_api_error( + event_type="CELERY_DISPATCH_FAILED", + current_user=current_user, + project_id=task_set_id, + message=f"拆镜分析任务投递失败: {exc}", + detail={"task_set_id": task_set_id, "task": "analyze_original_video"}, + exc=exc, + ) + raise HTTPException(status_code=503, detail=f"拆镜分析任务投递失败: {exc}") + + return await task_set_detail(db, current_user=_user_context(current_user), task_set_id=task_set_id) + + +@router.get( + "/task-sets", + response_model=ShotTaskSetListOut, + summary="查询拆镜总任务集列表", +) +async def list_shot_task_sets( + status: str | None = Query(None, description="总任务状态,见 ShotTaskSetStatusEnum"), + analysis_status: str | None = Query(None, description="分析状态,见 ShotAnalysisStatusEnum"), + split_status: str | None = Query(None, description="拆镜状态,见 ShotSplitStatusEnum"), + keyword: str | None = Query(None, description="标题/内容关键词"), + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=100), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + return await list_task_sets( + db, + current_user=current_user, + status=status, + analysis_status=analysis_status, + split_status=split_status, + keyword=keyword, + page=page, + page_size=page_size, + ) + + +@router.get( + "/task-sets/{task_set_id}", + response_model=ShotTaskSetDetailOut, + summary="获取拆镜总任务集详情", +) +async def get_shot_task_set( + task_set_id: str = Path(...), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + return await task_set_detail(db, current_user=_user_context(current_user), task_set_id=task_set_id) + + +@router.post( + "/task-sets/{task_set_id}/split-by-ai", + response_model=ShotSplitByAIOut, + summary="按 AI 建议方案异步拆镜", +) +async def split_by_ai( + task_set_id: str = Path(...), + req: ShotSplitByAIRequest = Body(default_factory=ShotSplitByAIRequest), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + try: + out = await create_segments_by_ai(db, current_user=current_user, task_set_id=task_set_id, req=req) + segment_ids = [item.id for item in out.segments] + await db.commit() + except HTTPException: + await db.rollback() + raise + except Exception as exc: + await db.rollback() + _log_api_exception_from_locals(exc, locals(), f"按 AI 建议拆镜失败: {exc}") + raise HTTPException(status_code=500, detail=f"按 AI 建议拆镜失败: {exc}") + + if celery_app: + from app.tasks.shot_replicate_tasks import split_one_segment + + for segment_id in segment_ids: + split_one_segment.apply_async(args=[segment_id], queue="gen_result_download", countdown=0) + return out + + +@router.post( + "/task-sets/{task_set_id}/split-custom", + response_model=ShotSplitCustomOut, + summary="按用户自定义开始/结束秒异步拆单条片段", +) +async def split_custom( + task_set_id: str = Path(...), + req: ShotSplitCustomRequest = Body(...), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + try: + out = await create_custom_segment(db, current_user=current_user, task_set_id=task_set_id, req=req) + segment_id = out.segment.id + await db.commit() + except HTTPException: + await db.rollback() + raise + except Exception as exc: + await db.rollback() + _log_api_exception_from_locals(exc, locals(), f"自定义拆镜失败: {exc}") + raise HTTPException(status_code=500, detail=f"自定义拆镜失败: {exc}") + + if celery_app: + from app.tasks.shot_replicate_tasks import split_one_segment + + split_one_segment.apply_async(args=[segment_id], queue="gen_result_download", countdown=0) + return out + + +@router.get( + "/task-sets/{task_set_id}/segments", + response_model=ShotSegmentListOut, + summary="查询拆镜片段列表", +) +async def list_task_set_segments( + task_set_id: str = Path(...), + source_mode: str | None = Query(None, description="ai_suggestion/custom"), + split_status: str | None = Query(None, description="拆镜状态"), + analysis_status: str | None = Query(None, description="片段分析状态"), + replicate_status: str | None = Query(None, description="复刻状态"), + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=100), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + return await list_segments( + db, + current_user=current_user, + task_set_id=task_set_id, + source_mode=source_mode, + split_status=split_status, + analysis_status=analysis_status, + replicate_status=replicate_status, + page=page, + page_size=page_size, + ) + + +@router.get( + "/segments/{segment_id}", + response_model=ShotSegmentDetailOut, + summary="获取拆镜片段详情", +) +async def get_segment( + segment_id: str = Path(...), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + return await segment_detail(db, current_user=current_user, segment_id=segment_id) + + +@router.post( + "/segments/{segment_id}/replication-projects", + response_model=ShotReplicateActionOut, + summary="将拆镜片段创建为拆镜复刻项目", +) +async def create_replication_project_from_segment( + segment_id: str = Path(...), + req: ShotSegmentReplicationCreateRequest = Body(...), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + try: + segment = await get_segment_for_user(db, segment_id=segment_id, user=current_user, for_update=True) + project = await create_shot_replicate_project_from_segment(db, current_user=current_user, segment=segment, req=req) + project_id = project.id + await db.commit() + except HTTPException: + await db.rollback() + raise + except Exception as exc: + await db.rollback() + _log_api_exception_from_locals(exc, locals(), f"创建拆镜复刻项目失败: {exc}") + raise HTTPException(status_code=500, detail=f"创建拆镜复刻项目失败: {exc}") + + return ShotReplicateActionOut( + message="已从拆镜片段创建复刻项目,素材视频已锁定", + project_id=project_id, + step_id=None, + detail=await _reload_project_detail(db, current_user, project_id), + ) + + +@router.get( + "/projects/{project_id}", + response_model=ShotReplicateTaskDetailOut, + summary="获取拆镜复刻项目详情", +) +async def get_project( + project_id: str = Path(...), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + return await _reload_project_detail(db, current_user, project_id) + + +@router.put( + "/projects/{project_id}/material", + response_model=ShotReplicateActionOut, + summary="修改拆镜复刻素材信息,素材视频不允许修改", +) +async def update_material( + project_id: str = Path(...), + req: ShotReplicateMaterialUpdateRequest = Body(...), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + try: + project_id_value, step_id_value = await update_shot_replicate_material_input(db, current_user=current_user, project_id=project_id, req=req) + await db.commit() + except HTTPException: + await db.rollback() + raise + except Exception as exc: + await db.rollback() + _log_api_exception_from_locals(exc, locals(), f"修改素材输入失败: {exc}") + raise HTTPException(status_code=500, detail=f"修改素材输入失败: {exc}") + return ShotReplicateActionOut(message="素材输入已修改,素材视频保持锁定", project_id=project_id_value, step_id=step_id_value, detail=await _reload_project_detail(db, current_user, project_id_value)) + + +@router.put( + "/projects/{project_id}/steps/{step_id}/image-prompt", + response_model=ShotReplicateActionOut, + summary="直接修改图片 AI 优化提词", +) +async def update_image_prompt( + project_id: str = Path(...), + step_id: str = Path(...), + req: ShotReplicateImagePromptUpdateRequest = Body(...), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + try: + project, step = await update_shot_replicate_image_prompt(db, current_user=current_user, project_id=project_id, step_id=step_id, req=req) + project_id_value, step_id_value = project.id, step.id + await db.commit() + except HTTPException: + await db.rollback() + raise + except Exception as exc: + await db.rollback() + _log_api_exception_from_locals(exc, locals(), f"修改图片 AI 提词失败: {exc}") + raise HTTPException(status_code=500, detail=f"修改图片 AI 提词失败: {exc}") + return ShotReplicateActionOut(message="图片 AI 提词已修改,后续步骤已软删除", project_id=project_id_value, step_id=step_id_value, detail=await _reload_project_detail(db, current_user, project_id_value)) + + +@router.put( + "/projects/{project_id}/steps/{step_id}/video-prompt-schema", + response_model=ShotReplicateActionOut, + summary="修改视频 AI 提词 JSON schema", +) +async def update_video_prompt_schema( + project_id: str = Path(...), + step_id: str = Path(...), + req: ShotReplicateVideoPromptSchemaUpdateRequest = Body(...), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + try: + project, step = await update_shot_replicate_video_prompt_schema(db, current_user=current_user, project_id=project_id, step_id=step_id, req=req) + project_id_value, step_id_value = project.id, step.id + await db.commit() + except HTTPException: + await db.rollback() + raise + except Exception as exc: + await db.rollback() + _log_api_exception_from_locals(exc, locals(), f"修改视频 AI 提词失败: {exc}") + raise HTTPException(status_code=500, detail=f"修改视频 AI 提词失败: {exc}") + return ShotReplicateActionOut(message="视频 AI 提词 schema 已修改,第5步视频生成已软删除", project_id=project_id_value, step_id=step_id_value, detail=await _reload_project_detail(db, current_user, project_id_value)) + + +@router.post( + "/projects/{project_id}/generate-image-prompt", + response_model=ShotReplicateActionOut, + summary="生成图片 AI 提词", +) +async def generate_image_prompt( + project_id: str = Path(...), + req: ShotReplicateGenerateImagePromptRequest = Body(default_factory=ShotReplicateGenerateImagePromptRequest), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + try: + project, step = await submit_image_prompt_optimize(db, current_user=current_user, project_id=project_id, req=req) + project_id_value, step_id_value = project.id, step.id + await db.commit() + except HTTPException: + await db.rollback() + raise + except Exception as exc: + await db.rollback() + _log_api_exception_from_locals(exc, locals(), f"提交图片 AI 提词失败: {exc}") + raise HTTPException(status_code=500, detail=f"提交图片 AI 提词失败: {exc}") + + try: + if celery_app: + from app.tasks.shot_replicate_flow_tasks import start_image_prompt_optimize + + start_image_prompt_optimize.apply_async(args=[project_id_value, step_id_value], queue="gen_chatapi_create", countdown=0) + except Exception as exc: + await _mark_dispatch_failed_and_raise(db, current_user=current_user, project_id=project_id_value, step_id=step_id_value, message=f"图片 AI 提词任务投递失败: {exc}") + + return ShotReplicateActionOut(message="图片 AI 提词任务已提交", project_id=project_id_value, step_id=step_id_value, detail=await _reload_project_detail(db, current_user, project_id_value)) + + +@router.post( + "/projects/{project_id}/generate-image", + response_model=ShotReplicateActionOut, + summary="根据图片 AI 提词生成图片", +) +async def generate_image( + project_id: str = Path(...), + req: ShotReplicateGenerateImageRequest = Body(...), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + try: + project, step, chat_task = await generate_image_from_prompt(db, current_user=current_user, project_id=project_id, req=req) + project_id_value, step_id_value, chat_task_id_value = project.id, step.id, chat_task.id + await db.commit() + except HTTPException: + await db.rollback() + raise + except Exception as exc: + await db.rollback() + _log_api_exception_from_locals(exc, locals(), f"提交图片生成失败: {exc}") + raise HTTPException(status_code=500, detail=f"提交图片生成失败: {exc}") + + try: + if celery_app: + from app.tasks.generation_create_tasks import chatapi_create_generation_task + + chatapi_create_generation_task.apply_async(args=[chat_task_id_value], queue="gen_chatapi_create", countdown=0) + except Exception as exc: + await _mark_dispatch_failed_and_raise(db, current_user=current_user, project_id=project_id_value, step_id=step_id_value, message=f"图片生成任务投递失败: {exc}") + + return ShotReplicateActionOut(message="图片生成任务已提交", project_id=project_id_value, step_id=step_id_value, detail=await _reload_project_detail(db, current_user, project_id_value)) + + +@router.post( + "/projects/{project_id}/generate-video-prompt", + response_model=ShotReplicateActionOut, + summary="生成视频 AI 提词 JSON schema", +) +async def generate_video_prompt( + project_id: str = Path(...), + req: ShotReplicateGenerateVideoPromptRequest = Body(...), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + try: + project, step = await submit_video_prompt_optimize(db, current_user=current_user, project_id=project_id, req=req) + project_id_value, step_id_value = project.id, step.id + await db.commit() + except HTTPException: + await db.rollback() + raise + except Exception as exc: + await db.rollback() + _log_api_exception_from_locals(exc, locals(), f"提交视频 AI 提词失败: {exc}") + raise HTTPException(status_code=500, detail=f"提交视频 AI 提词失败: {exc}") + + try: + if celery_app: + from app.tasks.shot_replicate_flow_tasks import start_video_prompt_optimize + + start_video_prompt_optimize.apply_async(args=[project_id_value, step_id_value], queue="gen_chatapi_create", countdown=0) + except Exception as exc: + await _mark_dispatch_failed_and_raise(db, current_user=current_user, project_id=project_id_value, step_id=step_id_value, message=f"视频 AI 提词任务投递失败: {exc}") + + return ShotReplicateActionOut(message="视频 AI 提词任务已提交", project_id=project_id_value, step_id=step_id_value, detail=await _reload_project_detail(db, current_user, project_id_value)) + + +@router.post( + "/projects/{project_id}/generate-video", + response_model=ShotReplicateActionOut, + summary="根据视频 AI 提词生成视频", +) +async def generate_video( + project_id: str = Path(...), + req: ShotReplicateGenerateVideoRequest = Body(...), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + try: + project, step, chat_task = await generate_video_from_prompt(db, current_user=current_user, project_id=project_id, req=req) + project_id_value, step_id_value, chat_task_id_value = project.id, step.id, chat_task.id + await db.commit() + except HTTPException: + await db.rollback() + raise + except Exception as exc: + await db.rollback() + _log_api_exception_from_locals(exc, locals(), f"提交视频生成失败: {exc}") + raise HTTPException(status_code=500, detail=f"提交视频生成失败: {exc}") + + try: + if celery_app: + from app.tasks.generation_create_tasks import chatapi_create_generation_task + + chatapi_create_generation_task.apply_async(args=[chat_task_id_value], queue="gen_chatapi_create", countdown=0) + except Exception as exc: + await _mark_dispatch_failed_and_raise(db, current_user=current_user, project_id=project_id_value, step_id=step_id_value, message=f"视频生成任务投递失败: {exc}") + + return ShotReplicateActionOut(message="视频生成任务已提交", project_id=project_id_value, step_id=step_id_value, detail=await _reload_project_detail(db, current_user, project_id_value)) + + +@router.delete( + "/projects/{project_id}", + response_model=ShotReplicateDeleteOut, + summary="软删除拆镜复刻项目", +) +async def delete_project( + project_id: str = Path(...), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + try: + out = await delete_shot_replicate_project(db, current_user=current_user, project_id=project_id) + await db.commit() + return out + except HTTPException: + await db.rollback() + raise + except Exception as exc: + await db.rollback() + _log_api_exception_from_locals(exc, locals(), f"删除拆镜复刻项目失败: {exc}") + raise HTTPException(status_code=500, detail=f"删除拆镜复刻项目失败: {exc}") diff --git a/video-gen-api/app/config.py b/video-gen-api/app/config.py index 334175a3..066c5528 100644 --- a/video-gen-api/app/config.py +++ b/video-gen-api/app/config.py @@ -87,6 +87,13 @@ class Settings(BaseSettings): # ChatAPI async generation pipeline settings CELERY_BROKER_URL: str = "" CELERY_RESULT_BACKEND: str = "" + # Celery async 兼容配置。 + # single_loop:每个 Celery 子进程一个专用 event loop,推荐线上/本地统一使用。 + # direct:旧版线程本地 loop 降级模式,建议配合 CELERY_DB_USE_NULLPOOL=true。 + CELERY_ASYNC_RUNNER_MODE: str = "single_loop" + CELERY_DB_USE_NULLPOOL: bool = False + CELERY_STARTUP_RECOVERY_ENABLED: bool = True + CHATAPI_REQUEST_TIMEOUT_SECONDS: int = 180 CHATAPI_VIDEO_FPS: float = 0.5 CHATAPI_ASYNC_MAX_RETRIES: int = 3 @@ -123,6 +130,22 @@ class Settings(BaseSettings): DOWNLOAD_ACTIVE_REDIS_HASH_KEY: str = "vg:celery:download:active" DOWNLOAD_ACTIVE_REDIS_ZSET_KEY: str = "vg:celery:download:active_index" + # Celery 生成链路 / provider poll 容灾配置。 + # 说明: + # - 不新增 Celery worker;恢复任务仍投递到 gen_result_download。 + # - worker_ready 每个 worker 都会尝试抢启动恢复锁,只有抢到锁的 worker 投递恢复任务。 + # - poll active 使用独立 Redis key,避免影响稳定的下载 active 注册表。 + GENERATION_RECOVERY_BATCH_SIZE: int = 100 + GENERATION_RECOVERY_MAX_ROUNDS: int = 5 + POLL_RECOVERY_BATCH_SIZE: int = 100 + POLL_TASK_LEASE_SECONDS: int = 5 * 60 + POLL_TASK_QUEUE_TIMEOUT_SECONDS: int = 2 * 60 + POLL_ACTIVE_REDIS_HASH_KEY: str = "vg:celery:poll:active" + POLL_ACTIVE_REDIS_ZSET_KEY: str = "vg:celery:poll:active_index" + CELERY_STARTUP_RECOVERY_LOCK_KEY: str = "vg:celery:startup_recovery_lock" + CELERY_STARTUP_RECOVERY_LOCK_TTL_SECONDS: int = 120 + + RESOURCE_SIGN_SECRET: str = "resource-signature-secret-key-for-API-authentication" RESOURCE_SIGN_EXPIRE_SECONDS: int = 60 RESOURCE_SIGN_ARG_EXPIRE: str = "exp" @@ -134,5 +157,38 @@ class Settings(BaseSettings): HOT_OPENING_DEFAULT_VIDEO_RESOLUTION: str = "480p" HOT_OPENING_DEFAULT_TARGET_PLATFORM: str = "抖音" + # 拆镜复刻配置。 + # 原始上传视频和拆镜片段都属于 uploads 素材域;只有 generate 生成结果走 token 验签。 + SHOT_ANALYSIS_TIMEOUT_SECONDS: int = 180 + SHOT_ANALYSIS_TEMPERATURE: float = 0.1 + SHOT_ANALYSIS_MAX_TOKENS: int = 5000 + SHOT_ANALYSIS_VIDEO_FPS: float = 1.0 + SHOT_ANALYSIS_MAX_LOCAL_VIDEO_MB: int = 45 + + SHOT_SEGMENT_LOCAL_PATH: str = "./storage/uploads/shot_segments" + SHOT_SEGMENT_URL_PREFIX: str = "/uploads/shot_segments" + SHOT_SPLIT_MIN_SECONDS: float = 2 + SHOT_SPLIT_MAX_SECONDS: float = 15 + SHOT_SPLIT_END_TOLERANCE_SECONDS: float = 0.5 + SHOT_DURATION_TOLERANCE_SECONDS: float = 1.0 + SHOT_FFMPEG_TIMEOUT_SECONDS: int = 120 + SHOT_FFPROBE_TIMEOUT_SECONDS: int = 20 + FFPROBE_BIN: str = "" + + # 继续复用 gen_result_download 队列,但限制 ffmpeg 并发,避免拖慢 Chat 下载。 + SHOT_SPLIT_MAX_CONCURRENT: int = 1 + SHOT_SPLIT_MAX_RETRY_COUNT: int = 3 + SHOT_SPLIT_RETRY_BACKOFF_SECONDS: int = 30 + SHOT_SPLIT_LEASE_SECONDS: int = 10 * 60 + SHOT_SPLIT_PENDING_TIMEOUT_SECONDS: int = 5 * 60 + SHOT_SPLIT_RECOVERY_BATCH_SIZE: int = 50 + SHOT_SPLIT_LOCK_KEY_PREFIX: str = "vg:shot_replicate:split:lock" + SHOT_SPLIT_SEMAPHORE_KEY_PREFIX: str = "vg:shot_replicate:split:semaphore" + + SHOT_REPLICATE_DEFAULT_VIDEO_DURATION: int = 4 + SHOT_REPLICATE_DEFAULT_VIDEO_RATIO: str = "9:16" + SHOT_REPLICATE_DEFAULT_VIDEO_RESOLUTION: str = "480p" + SHOT_REPLICATE_DEFAULT_TARGET_PLATFORM: str = "抖音" + settings = Settings() diff --git a/video-gen-api/app/enums/__init__.py b/video-gen-api/app/enums/__init__.py index a27058d0..c26dc623 100644 --- a/video-gen-api/app/enums/__init__.py +++ b/video-gen-api/app/enums/__init__.py @@ -1,3 +1,4 @@ from app.enums.common import * from app.enums.hot_opening_replicate import * from app.enums.video_prompt_schema import * +from app.enums.shot_replicate import * diff --git a/video-gen-api/app/enums/shot_replicate.py b/video-gen-api/app/enums/shot_replicate.py new file mode 100644 index 00000000..2c48914c --- /dev/null +++ b/video-gen-api/app/enums/shot_replicate.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from enum import StrEnum + + +class ModuleCodeEnum(StrEnum): + """拆镜复刻模块编码。""" + + SHOT_REPLICATE = "shot_replicate" + + +class ShotReplicateStepCodeEnum(StrEnum): + """拆镜复刻复用通用模块步骤编码。""" + + MATERIAL_INPUT = "material_input" + IMAGE_PROMPT_OPTIMIZE = "image_prompt_optimize" + IMAGE_GENERATE = "image_generate" + VIDEO_PROMPT_OPTIMIZE = "video_prompt_optimize" + VIDEO_GENERATE = "video_generate" + + +class ShotReplicateGenerationModeEnum(StrEnum): + """复用 ChatGenerationTask 时使用的 generation_mode。""" + + SHOT_REPLICATE = "shot_replicate" + + +class ShotReplicateStepIOSchemaVersionEnum(StrEnum): + """拆镜复刻子任务 input_json/output_json 结构版本。""" + + V1 = "shot_replicate_step_io_v1" + + +class ShotTaskSetStatusEnum(StrEnum): + """拆镜总任务集状态。""" + + PENDING_ANALYSIS = "pending_analysis" + ANALYZING = "analyzing" + ANALYSIS_COMPLETED = "analysis_completed" + ANALYSIS_FAILED = "analysis_failed" + SPLITTING = "splitting" + SPLIT_COMPLETED = "split_completed" + PARTIAL_FAILED = "partial_failed" + FAILED = "failed" + DELETED = "deleted" + + +class ShotAnalysisStatusEnum(StrEnum): + """原视频/片段视频分析状态。""" + + PENDING = "pending" + PROCESSING = "processing" + COMPLETED = "completed" + FAILED = "failed" + + +class ShotSplitStatusEnum(StrEnum): + """ffmpeg 拆镜状态。""" + + NONE = "none" + PENDING = "pending" + PROCESSING = "processing" + COMPLETED = "completed" + FAILED = "failed" + RETRY_WAITING = "retry_waiting" + + +class ShotSegmentSourceModeEnum(StrEnum): + """拆镜片段来源。""" + + AI_SUGGESTION = "ai_suggestion" + CUSTOM = "custom" + + +class ShotSegmentAnalysisStatusEnum(StrEnum): + """拆镜片段分析状态。""" + + NOT_REQUIRED = "not_required" + PENDING = "pending" + PROCESSING = "processing" + COMPLETED = "completed" + FAILED = "failed" + + +class ShotSegmentReplicateStatusEnum(StrEnum): + """拆镜片段进入复刻流程后的状态。""" + + NOT_STARTED = "not_started" + PROJECT_CREATED = "project_created" + PROCESSING = "processing" + COMPLETED = "completed" + FAILED = "failed" diff --git a/video-gen-api/app/models/__init__.py b/video-gen-api/app/models/__init__.py index 82311bb8..948616db 100644 --- a/video-gen-api/app/models/__init__.py +++ b/video-gen-api/app/models/__init__.py @@ -23,6 +23,8 @@ from app.models.user_resource_month_stat import UserResourceMonthStat from app.models.user_resource_total_stat import UserResourceTotalStat from app.models.module_generation_project import ModuleGenerationProject from app.models.module_generation_step import ModuleGenerationStep +from app.models.shot_replicate_task_set import ShotReplicateTaskSet +from app.models.shot_replicate_segment import ShotReplicateSegment from app.models.user_oauth import UserOAuth from app.models.user_oauth_account import UserOAuthAccount from app.models.user_oauth_app import UserOAuthApp @@ -37,5 +39,6 @@ __all__ = [ "ChatGenerationTask", "ChatGenerationTaskEvent", "ChatProviderCallLog", "GeneratedResource", "UserResourceMonthStat", "UserResourceTotalStat", "ModuleGenerationProject", "ModuleGenerationStep", + "ShotReplicateTaskSet", "ShotReplicateSegment", "UserOAuth", "UserOAuthAccount", "UserOAuthApp", ] diff --git a/video-gen-api/app/models/base.py b/video-gen-api/app/models/base.py index cad97a0d..c4728b4f 100644 --- a/video-gen-api/app/models/base.py +++ b/video-gen-api/app/models/base.py @@ -4,6 +4,7 @@ import os from sqlalchemy import DateTime, func from sqlalchemy.ext.asyncio import AsyncAttrs, async_sessionmaker, create_async_engine from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column +from sqlalchemy.pool import NullPool from app.config import settings @@ -18,14 +19,18 @@ engine_kwargs = { "pool_pre_ping": True, } -# SQLite 本地调试时不要乱塞 pool_size/max_overflow,PostgreSQL/asyncpg 才建议配置 +# SQLite 本地调试时不要乱塞 pool_size/max_overflow,PostgreSQL/asyncpg 才建议配置。 +# 默认保持 Celery 连接池复用;只有显式开启 CELERY_DB_USE_NULLPOOL=true 时才降级 NullPool。 if _is_celery_process() and not settings.DATABASE_URL.startswith("sqlite"): - engine_kwargs.update( - pool_size=settings.CELERY_DB_POOL_SIZE, - max_overflow=settings.CELERY_DB_MAX_OVERFLOW, - pool_timeout=settings.CELERY_DB_POOL_TIMEOUT, - pool_recycle=settings.CELERY_DB_POOL_RECYCLE, - ) + if bool(getattr(settings, "CELERY_DB_USE_NULLPOOL", False)): + engine_kwargs.update(poolclass=NullPool) + else: + engine_kwargs.update( + pool_size=settings.CELERY_DB_POOL_SIZE, + max_overflow=settings.CELERY_DB_MAX_OVERFLOW, + pool_timeout=settings.CELERY_DB_POOL_TIMEOUT, + pool_recycle=settings.CELERY_DB_POOL_RECYCLE, + ) engine = create_async_engine(settings.DATABASE_URL, **engine_kwargs) @@ -63,4 +68,4 @@ async def init_database() -> None: async def close_database() -> None: - await engine.dispose() \ No newline at end of file + await engine.dispose() diff --git a/video-gen-api/app/models/shot_replicate_segment.py b/video-gen-api/app/models/shot_replicate_segment.py new file mode 100644 index 00000000..7773788d --- /dev/null +++ b/video-gen-api/app/models/shot_replicate_segment.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Any + +from sqlalchemy import DateTime, Float, ForeignKey, Index, Integer, JSON, String, Text, text +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.orm import Mapped, mapped_column + +from app.models.base import Base, SoftDeleteMixin, TimestampMixin + +_JSON_TYPE = JSON().with_variant(JSONB, "postgresql") + + +class ShotReplicateSegment(Base, TimestampMixin, SoftDeleteMixin): + """拆镜复刻片段表。""" + + __tablename__ = "shot_replicate_segments" + __table_args__ = ( + Index("idx_shot_replicate_segments_task_set", "task_set_id", "segment_index"), + Index("idx_shot_replicate_segments_user_created", "user_id", "created_at"), + Index("idx_shot_replicate_segments_source_mode", "source_mode"), + Index("idx_shot_replicate_segments_split_status", "split_status"), + Index("idx_shot_replicate_segments_analysis_status", "analysis_status"), + Index("idx_shot_replicate_segments_replicate_status", "replicate_status"), + Index("idx_shot_replicate_segments_project", "module_project_id"), + Index( + "uq_shot_replicate_segments_task_set_index_active", + "task_set_id", + "segment_index", + unique=True, + postgresql_where=text("deleted_at IS NULL"), + ), + ) + + id: Mapped[str] = mapped_column(String(32), primary_key=True) + task_set_id: Mapped[str] = mapped_column( + String(32), ForeignKey("shot_replicate_task_sets.id", ondelete="CASCADE"), index=True, nullable=False + ) + user_id: Mapped[str] = mapped_column( + String(32), ForeignKey("users.id", ondelete="CASCADE"), index=True, nullable=False + ) + + segment_index: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + source_mode: Mapped[str] = mapped_column(String(32), nullable=False, index=True) + + start_second: Mapped[float] = mapped_column(Float, nullable=False) + end_second: Mapped[float] = mapped_column(Float, nullable=False) + duration_seconds: Mapped[float] = mapped_column(Float, nullable=False) + time_node: Mapped[str] = mapped_column(String(64), nullable=False) + + split_status: Mapped[str] = mapped_column(String(32), default="pending", index=True, nullable=False) + analysis_status: Mapped[str] = mapped_column(String(32), default="pending", index=True, nullable=False) + replicate_status: Mapped[str] = mapped_column(String(32), default="not_started", index=True, nullable=False) + + segment_video_url: Mapped[str | None] = mapped_column(String(512), nullable=True) + segment_video_path: Mapped[str | None] = mapped_column(String(512), nullable=True) + + original_video_content: Mapped[str | None] = mapped_column(Text, nullable=True) + original_video_category: Mapped[str | None] = mapped_column(String(160), nullable=True) + original_video_audience: Mapped[str | None] = mapped_column(Text, nullable=True) + + segment_content: Mapped[str | None] = mapped_column(Text, nullable=True) + segment_category: Mapped[str | None] = mapped_column(String(160), nullable=True) + segment_audience: Mapped[str | None] = mapped_column(Text, nullable=True) + + analysis_json: Mapped[dict[str, Any] | list[Any] | None] = mapped_column(_JSON_TYPE, nullable=True) + ai_suggestion_json: Mapped[dict[str, Any] | list[Any] | None] = mapped_column(_JSON_TYPE, nullable=True) + module_project_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True) + + split_celery_task_id: Mapped[str | None] = mapped_column(String(160), nullable=True, index=True) + split_enqueued_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + split_started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + split_lease_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True) + split_next_retry_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True) + split_retry_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) + split_last_error: Mapped[str | None] = mapped_column(Text, nullable=True) + split_completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + + analysis_error_message: Mapped[str | None] = mapped_column(Text, nullable=True) diff --git a/video-gen-api/app/models/shot_replicate_task_set.py b/video-gen-api/app/models/shot_replicate_task_set.py new file mode 100644 index 00000000..93e1a3e9 --- /dev/null +++ b/video-gen-api/app/models/shot_replicate_task_set.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from typing import Any + +from sqlalchemy import Float, ForeignKey, Index, Integer, JSON, String, Text, text +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.orm import Mapped, mapped_column + +from app.models.base import Base, SoftDeleteMixin, TimestampMixin + +_JSON_TYPE = JSON().with_variant(JSONB, "postgresql") + + +class ShotReplicateTaskSet(Base, TimestampMixin, SoftDeleteMixin): + """拆镜复刻总任务集。""" + + __tablename__ = "shot_replicate_task_sets" + __table_args__ = ( + Index("idx_shot_replicate_task_sets_user_created", "user_id", "created_at"), + Index("idx_shot_replicate_task_sets_status", "status"), + Index("idx_shot_replicate_task_sets_analysis_status", "analysis_status"), + Index("idx_shot_replicate_task_sets_split_status", "split_status"), + Index( + "uq_shot_replicate_task_sets_user_idempotency", + "user_id", + "idempotency_key", + unique=True, + postgresql_where=text("deleted_at IS NULL AND idempotency_key IS NOT NULL"), + ), + ) + + id: Mapped[str] = mapped_column(String(32), primary_key=True) + user_id: Mapped[str] = mapped_column( + String(32), ForeignKey("users.id", ondelete="CASCADE"), index=True, nullable=False + ) + title: Mapped[str | None] = mapped_column(String(160), nullable=True) + + video_url: Mapped[str] = mapped_column(String(512), nullable=False) + video_path: Mapped[str] = mapped_column(String(512), nullable=False) + video_duration_seconds: Mapped[float] = mapped_column(Float, nullable=False) + + status: Mapped[str] = mapped_column(String(32), default="pending_analysis", index=True, nullable=False) + analysis_status: Mapped[str] = mapped_column(String(32), default="pending", index=True, nullable=False) + split_status: Mapped[str] = mapped_column(String(32), default="none", index=True, nullable=False) + + original_video_content: Mapped[str | None] = mapped_column(Text, nullable=True) + original_video_category: Mapped[str | None] = mapped_column(String(160), nullable=True) + original_video_audience: Mapped[str | None] = mapped_column(Text, nullable=True) + + ai_suggestion_json: Mapped[list[Any] | dict[str, Any] | None] = mapped_column(_JSON_TYPE, nullable=True) + analysis_raw_json: Mapped[dict[str, Any] | list[Any] | None] = mapped_column(_JSON_TYPE, nullable=True) + analysis_result_json: Mapped[dict[str, Any] | list[Any] | None] = mapped_column(_JSON_TYPE, nullable=True) + + segment_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) + completed_segment_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) + failed_segment_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) + + analysis_error_message: Mapped[str | None] = mapped_column(Text, nullable=True) + split_error_message: Mapped[str | None] = mapped_column(Text, nullable=True) + idempotency_key: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True) diff --git a/video-gen-api/app/schemas/shot_replicate.py b/video-gen-api/app/schemas/shot_replicate.py new file mode 100644 index 00000000..a018d673 --- /dev/null +++ b/video-gen-api/app/schemas/shot_replicate.py @@ -0,0 +1,734 @@ +from __future__ import annotations + +from typing import Any + +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator + +from app.schemas.common import NaiveDatetimeOptional + +SHOT_REPLICATE_PROJECT_STATUS_DESCRIPTIONS: dict[str, str] = { + "pending": "已创建但未进入流程", + "waiting_user": "等待用户手动触发下一步", + "processing": "当前有步骤处理中", + "completed": "总任务完成", + "failed": "总任务失败", + "cancelled": "总任务取消", +} + +SHOT_REPLICATE_STEP_STATUS_DESCRIPTIONS: dict[str, str] = { + "pending": "子任务待处理", + "waiting_user": "等待用户确认或触发", + "processing": "子任务处理中", + "completed": "子任务完成", + "failed": "子任务失败", + "cancelled": "子任务取消", +} + +SHOT_REPLICATE_STEP_DESCRIPTIONS: list[dict[str, Any]] = [ + {"step_index": 1, "step_code": "material_input", "name": "素材输入"}, + {"step_index": 2, "step_code": "image_prompt_optimize", "name": "图片 AI 提词"}, + {"step_index": 3, "step_code": "image_generate", "name": "图片生成"}, + {"step_index": 4, "step_code": "video_prompt_optimize", "name": "视频 AI 提词 JSON schema"}, + {"step_index": 5, "step_code": "video_generate", "name": "视频生成"}, +] + +SHOT_REPLICATE_STEP_IO_SCHEMA_VERSION = "shot_replicate_step_io_v1" + +SHOT_REPLICATE_STEP_IO_EXAMPLES: dict[str, dict[str, Any]] = { + "material_input": { + "input_json": { + "schema_version": SHOT_REPLICATE_STEP_IO_SCHEMA_VERSION, + "step_code": "material_input", + "source": {"source_step_id": None, "parent_step_id": None}, + "payload": { + "material_video_url": "https://example.com/source.mp4", + "material_image_url": "https://example.com/product.png", + "source_project_name": "参考素材项目名称", + "target_project_name": "新项目名称", + "core_content_point": "50字以内核心内容点", + }, + "context": {}, + }, + "output_json": { + "schema_version": SHOT_REPLICATE_STEP_IO_SCHEMA_VERSION, + "step_code": "material_input", + "status": "completed", + "payload": {}, + "result": {"accepted": True, "message": "素材输入已提交", "next_step_code": "image_prompt_optimize"}, + "usage": {}, + "error": {}, + }, + }, + "image_prompt_optimize": { + "input_json": { + "schema_version": SHOT_REPLICATE_STEP_IO_SCHEMA_VERSION, + "step_code": "image_prompt_optimize", + "source": {"source_step_id": "第1步素材输入ID", "parent_step_id": "第1步素材输入ID"}, + "payload": {"source_step_id": "第1步素材输入ID"}, + "context": {}, + }, + "output_json": { + "schema_version": SHOT_REPLICATE_STEP_IO_SCHEMA_VERSION, + "step_code": "image_prompt_optimize", + "status": "completed", + "payload": { + "optimized_prompt": "图片生成提示词", + "prompt": "兼容字段,同 optimized_prompt", + "original_prompt": "后端拼接的图片提词原始需求", + "references": [{"type": "video|image", "url": "...", "name": "..."}], + }, + "result": {}, + "usage": { + "input_tokens": 0, + "output_tokens": 0, + "total_tokens": 0, + "text_credits_cost": 0, + "credit_biz_key": "module_generation_step:{step_id}:attempt:1:text_prompt:charge", + }, + "error": {}, + }, + }, + "image_generate": { + "input_json": { + "schema_version": SHOT_REPLICATE_STEP_IO_SCHEMA_VERSION, + "step_code": "image_generate", + "source": {"source_step_id": "第2步图片提词ID", "parent_step_id": "第2步图片提词ID"}, + "payload": { + "engine_id": "图片引擎ID", + "params": {"image_size": "2K", "image_proportion": "1:1", "image_px": "2048x2048"}, + "prompt": "图片生成提示词", + "media_references": [{"type": "image", "url": "新产品图片", "name": "新产品图片"}], + }, + "context": {}, + }, + "output_json": { + "schema_version": SHOT_REPLICATE_STEP_IO_SCHEMA_VERSION, + "step_code": "image_generate", + "status": "completed", + "payload": {}, + "result": {"result_image_url": "/generate/images/xxx.png", "chat_task_id": "ChatGenerationTask ID"}, + "usage": {}, + "error": {}, + }, + }, + "video_prompt_optimize": { + "input_json": { + "schema_version": SHOT_REPLICATE_STEP_IO_SCHEMA_VERSION, + "step_code": "video_prompt_optimize", + "source": {"source_step_id": "第3步图片生成ID", "parent_step_id": "第3步图片生成ID"}, + "payload": { + "source_step_id": "第3步图片生成ID", + "video_config": {"engine_id": "视频引擎ID", "duration": 8, "aspect_ratio": "9:16", "resolution": "1080p"}, + "target_platform": "抖音", + }, + "context": {}, + }, + "output_json": { + "schema_version": SHOT_REPLICATE_STEP_IO_SCHEMA_VERSION, + "step_code": "video_prompt_optimize", + "status": "completed", + "payload": { + "prompt_schema": {"任务基础信息": {}, "最终提示词": {}}, + "final_prompt": "展示用最终视频提示词", + "params_used_for_prompt": {"duration": 8, "aspect_ratio": "9:16", "resolution": "1080p"}, + "target_platform": "抖音", + }, + "result": {}, + "usage": { + "input_tokens": 0, + "output_tokens": 0, + "total_tokens": 0, + "text_credits_cost": 0, + "credit_biz_key": "module_generation_step:{step_id}:attempt:1:text_prompt:charge", + }, + "error": {}, + }, + }, + "video_generate": { + "input_json": { + "schema_version": SHOT_REPLICATE_STEP_IO_SCHEMA_VERSION, + "step_code": "video_generate", + "source": {"source_step_id": "第4步视频提词ID", "parent_step_id": "第4步视频提词ID"}, + "payload": { + "engine_id": "视频引擎ID", + "params": {"duration": 8, "aspect_ratio": "9:16", "resolution": "1080p"}, + "prompt_schema": {"任务基础信息": {}, "最终提示词": {}}, + "final_prompt": "展示用最终提示词", + "media_references": [{"type": "image", "url": "第3步生成图片", "name": "新项目图片"}], + }, + "context": {}, + }, + "output_json": { + "schema_version": SHOT_REPLICATE_STEP_IO_SCHEMA_VERSION, + "step_code": "video_generate", + "status": "completed", + "payload": {}, + "result": { + "result_video_url": "/generate/videos/xxx.mp4", + "result_video_cover_url": "/generate/covers/xxx.jpg", + "chat_task_id": "ChatGenerationTask ID", + }, + "usage": {}, + "error": {}, + }, + }, +} + + +class ShotReplicateTaskCreate(BaseModel): + """创建拆镜复刻总任务项目请求体。""" + + model_config = ConfigDict( + json_schema_extra={ + "example": { + "material_video_url": "https://example.com/source.mp4", + "material_image_url": "https://example.com/product.png", + "source_project_name": "参考素材项目名称", + "target_project_name": "新项目名称", + "core_content_point": "突出产品能帮助用户认识附近新朋友", + "idempotency_key": "frontend-submit-uuid-001", + } + } + ) + + material_video_url: str = Field(..., min_length=1, description="素材视频链接,参考素材,1份。由项目已有上传接口返回,本接口不负责上传,不做后端素材校验") + material_image_url: str = Field(..., min_length=1, description="素材图片链接,新产品图片,1份。由项目已有上传接口返回,本接口不负责上传,不做后端素材校验") + source_project_name: str = Field(..., min_length=1, max_length=20, description="视频素材内容项目名称") + target_project_name: str = Field(..., min_length=1, max_length=20, description="生成项目名称") + core_content_point: str = Field(..., min_length=1, max_length=50, description="生成的项目核心内容点,最多50字") + idempotency_key: str | None = Field(None, max_length=64, description="创建总任务幂等键。只用于 module_generation_projects,不用于 ChatGenerationTask") + + @field_validator("material_video_url", "material_image_url", "source_project_name", "target_project_name", "core_content_point") + @classmethod + def _strip_required(cls, value: str) -> str: + value = str(value or "").strip() + if not value: + raise ValueError("字段不能为空") + return value + + +class ShotReplicateMaterialUpdateRequest(BaseModel): + """修改拆镜复刻第1步素材输入请求体。""" + + model_config = ConfigDict( + json_schema_extra={ + "example": { + "material_video_url": "https://example.com/new-source.mp4", + "material_image_url": "https://example.com/new-product.png", + "source_project_name": "新的参考素材项目名称", + "target_project_name": "新的生成项目名称", + "core_content_point": "新的50字以内核心内容点", + } + } + ) + + material_video_url: str | None = Field(None, min_length=1, description="素材视频链接,未传则沿用旧值") + material_image_url: str | None = Field(None, min_length=1, description="素材图片链接,未传则沿用旧值") + source_project_name: str | None = Field(None, min_length=1, max_length=20, description="视频素材内容项目名称,未传则沿用旧值") + target_project_name: str | None = Field(None, min_length=1, max_length=20, description="生成项目名称,未传则沿用旧值") + core_content_point: str | None = Field(None, min_length=1, max_length=50, description="生成项目核心内容点,最多50字,未传则沿用旧值") + + @field_validator("material_video_url", "material_image_url", "source_project_name", "target_project_name", "core_content_point", mode="before") + @classmethod + def _strip_optional(cls, value: str | None) -> str | None: + if value is None: + return None + value = str(value).strip() + if not value: + raise ValueError("字段不能为空字符串") + return value + + @model_validator(mode="after") + def _require_at_least_one(self) -> "ShotReplicateMaterialUpdateRequest": + if not any(getattr(self, field) is not None for field in ("material_video_url", "material_image_url", "source_project_name", "target_project_name", "core_content_point")): + raise ValueError("至少需要传入一个需要修改的字段") + return self + + +class ShotReplicateStepUpdate(BaseModel): + """修改拆镜复刻子任务请求体。""" + + material_video_url: str | None = Field(None, description="修改第1步素材视频链接") + material_image_url: str | None = Field(None, description="修改第1步素材图片链接") + source_project_name: str | None = Field(None, max_length=20, description="修改第1步视频素材内容项目名称") + target_project_name: str | None = Field(None, max_length=20, description="修改第1步生成项目名称") + core_content_point: str | None = Field(None, max_length=50, description="修改第1步生成项目核心内容点,最多50字") + prompt: str | None = Field(None, description="修改第2步图片提词或第4步视频最终提词") + prompt_schema: dict[str, Any] | None = Field(None, description="修改第4步视频提词 JSON schema。只对视频提词步骤有意义") + input_json: dict[str, Any] | None = Field(None, description="高级用法:合并修改当前步骤 input_json.payload") + output_json: dict[str, Any] | None = Field(None, description="高级用法:合并修改当前步骤 output_json.payload") + + +class ShotReplicateImagePromptUpdateRequest(BaseModel): + """直接修改第2步图片 AI 优化提词请求体。 + + 本接口不调用 AI、不扣积分;保存后会软删除第3、4、5步当前有效任务。 + """ + + model_config = ConfigDict( + extra="ignore", + json_schema_extra={"example": {"prompt": "用户手动修改后的图片生成提示词"}}, + ) + + prompt: str = Field(..., min_length=1, description="用户手动修改后的图片生成提示词,不能为空") + + @field_validator("prompt", mode="before") + @classmethod + def _strip_prompt(cls, value: str) -> str: + value = str(value or "").strip() + if not value: + raise ValueError("图片提示词不能为空") + return value + + +class ShotReplicateVideoPromptSchemaUpdateRequest(BaseModel): + """修改第4步视频 AI 提词 JSON schema 请求体。 + + 前端提交的 prompt_schema 只作为 patch:服务端会锁定视频时长、比例、清晰度、帧率、推荐分辨率、 + 动作/镜头/动态时间规划数组长度和时间段、输出规格限制、质量控制、合规控制、schema_version、schema_usage。 + 最终提示词允许修改,但保存前会清洗视频参数。 + """ + + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ + "example": { + "prompt_schema": { + "业务属性": {"产品名称": "脱单交友APP", "行动引导": "立即下载"}, + "最终提示词": {"主提示词": "脱单交友APP推广短视频,突出认识附近新朋友和高效匹配"}, + } + } + }, + ) + + prompt_schema: dict[str, Any] = Field(..., description="前端修改后的视频提词 JSON schema。后端只按白名单回填允许修改字段") + + @field_validator("prompt_schema") + @classmethod + def _validate_schema(cls, value: dict[str, Any]) -> dict[str, Any]: + if not isinstance(value, dict) or not value: + raise ValueError("prompt_schema 必须是非空 JSON 对象") + return value + + +class ShotReplicateGenerateImagePromptRequest(BaseModel): + """手动生成第2步图片 AI 提词请求体。当前无需请求参数。""" + + model_config = ConfigDict(extra="ignore") + + +class ShotReplicateGenerateImageRequest(BaseModel): + """根据图片提词生成新项目图片请求体。 + + ChatGenerationTask.idempotency_key 由后端自动生成,接口不再接收前端幂等键。 + """ + + model_config = ConfigDict( + extra="ignore", + json_schema_extra={"example": {"engine_id": "image_engine_xxx", "image_size": "2K", "image_proportion": "1:1", "image_px": "2048x2048"}}, + ) + + engine_id: str | None = Field(None, description="图片生成引擎ID。为空则使用当前启用且优先级最高的图片引擎") + image_size: str | None = Field(None, description="图片分辨率档位,例如 1K、2K。为空使用引擎默认值") + image_proportion: str | None = Field(None, description="图片比例,例如 1:1、16:9、9:16。为空使用默认值") + image_px: str | None = Field(None, description="图片像素尺寸,例如 2048x2048。为空时按引擎支持尺寸自动匹配") + + +class ShotReplicateGenerateVideoPromptRequest(BaseModel): + """手动生成第4步视频 AI 提词请求体。 + + 视频时长、比例、分辨率集中在本步骤确定;第5步生成视频只选择视频引擎。 + """ + + model_config = ConfigDict( + extra="ignore", + json_schema_extra={"example": {"engine_id": "video_engine_xxx", "duration": 8, "aspect_ratio": "9:16", "resolution": "1080p", "target_platform": "抖音"}}, + ) + + engine_id: str | None = Field(None, description="视频引擎ID。用于读取该引擎支持的视频时长、比例、分辨率配置;为空使用最高优先级启用引擎") + duration: int | None = Field(None, ge=1, description="希望用于视频提词规划的视频时长,单位秒。为空时优先使用 SHOT_REPLICATE_DEFAULT_VIDEO_DURATION") + aspect_ratio: str | None = Field(None, description="希望用于视频提词规划的视频比例。为空时优先使用 SHOT_REPLICATE_DEFAULT_VIDEO_RATIO") + resolution: str | None = Field(None, description="希望用于视频提词规划的视频分辨率。为空时优先使用 SHOT_REPLICATE_DEFAULT_VIDEO_RESOLUTION") + target_platform: str | None = Field(None, max_length=64, description="目标平台,例如抖音/快手/小红书。为空时使用 SHOT_REPLICATE_DEFAULT_TARGET_PLATFORM") + + +class ShotReplicateGenerateVideoRequest(BaseModel): + """根据视频提词生成最终视频请求体。 + + 只选择视频生成引擎。duration / aspect_ratio / resolution 从第4步视频提词优化结果读取。 + ChatGenerationTask.original_prompt / optimized_prompt 都写入第4步生成的 prompt_schema JSON 字符串。 + """ + + model_config = ConfigDict(extra="ignore", json_schema_extra={"example": {"engine_id": "video_engine_xxx"}}) + + engine_id: str | None = Field(None, description="视频生成引擎ID。为空优先使用第4步视频提词时选择的 engine_id,再为空使用最高优先级启用视频引擎") + + +class ShotReplicateStepOut(BaseModel): + id: str = Field(..., description="子任务ID") + project_id: str = Field(..., description="总任务项目ID,即 module_generation_projects.id") + module: str = Field(..., description="模块标识,例如 shot_replicate") + step_index: int = Field(..., description="步骤序号:1素材输入、2图片提词、3图片生成、4视频提词、5视频生成") + step_code: str = Field(..., description="步骤编码:material_input/image_prompt_optimize/image_generate/video_prompt_optimize/video_generate") + status: str = Field(..., description="步骤状态:pending/waiting_user/processing/completed/failed/cancelled") + version: int = Field(..., description="步骤版本号。重新生成或修改上游步骤后 version+1") + is_current: bool = Field(..., description="是否当前有效步骤。旧步骤会软删除且 is_current=false") + parent_step_id: str | None = Field(None, description="上一个步骤ID") + source_step_id: str | None = Field(None, description="当前步骤基于哪个上游步骤生成") + chat_task_id: str | None = Field(None, description="关联的 ChatGenerationTask ID。第3步图片生成、第5步视频生成有值") + input: dict[str, Any] | None = Field(None, description=f"步骤输入 JSON,统一 schema_version={SHOT_REPLICATE_STEP_IO_SCHEMA_VERSION}") + output: dict[str, Any] | None = Field(None, description=f"步骤输出 JSON,统一 schema_version={SHOT_REPLICATE_STEP_IO_SCHEMA_VERSION}") + error_message: str | None = Field(None, description="步骤错误信息") + created_at: NaiveDatetimeOptional = Field(None, description="创建时间") + updated_at: NaiveDatetimeOptional = Field(None, description="更新时间") + completed_at: NaiveDatetimeOptional = Field(None, description="完成时间") + + +class ShotReplicateMaterialOut(BaseModel): + material_step_id: str | None = Field(None, description="第1步素材输入子任务ID") + material_video_url: str | None = Field(None, description="素材视频链接") + material_image_url: str | None = Field(None, description="素材图片链接") + source_project_name: str | None = Field(None, description="视频素材内容项目名称") + target_project_name: str | None = Field(None, description="生成项目名称") + core_content_point: str | None = Field(None, description="生成项目核心内容点") + + +class ShotReplicateImageGenerationOut(BaseModel): + prompt_step_id: str | None = Field(None, description="第2步图片 AI 提词子任务ID") + generate_step_id: str | None = Field(None, description="第3步图片生成子任务ID") + prompt: str | None = Field(None, description="图片优化提词") + engine_id: str | None = Field(None, description="图片生成引擎ID") + engine_name: str | None = Field(None, description="图片生成引擎名称") + params: dict[str, Any] | None = Field(None, description="图片生成参数") + chat_task_id: str | None = Field(None, description="图片生成 ChatGenerationTask ID") + status: str | None = Field(None, description="图片生成状态") + result_image_url: str | None = Field(None, description="新项目图片 URL") + error_message: str | None = Field(None, description="图片生成错误信息") + + +class ShotReplicateVideoGenerationOut(BaseModel): + prompt_step_id: str | None = Field(None, description="第4步视频 AI 提词子任务ID") + generate_step_id: str | None = Field(None, description="第5步视频生成子任务ID") + prompt_schema: dict[str, Any] | None = Field(None, description="视频提词 JSON schema。第5步 ChatGenerationTask 原始提词会使用该 JSON 字符串") + final_prompt: str | None = Field(None, description="视频最终提词,仅用于前端展示") + prompt_params: dict[str, Any] | None = Field(None, description="第4步生成视频提词时使用的视频配置,例如 duration、aspect_ratio、resolution") + engine_id: str | None = Field(None, description="视频生成引擎ID") + engine_name: str | None = Field(None, description="视频生成引擎名称") + params: dict[str, Any] | None = Field(None, description="视频生成实际参数。第5步只传 engine_id,其它参数继承第4步") + chat_task_id: str | None = Field(None, description="视频生成 ChatGenerationTask ID") + status: str | None = Field(None, description="视频生成状态") + result_video_url: str | None = Field(None, description="最终视频 URL") + result_video_cover_url: str | None = Field(None, description="最终视频封面 URL") + error_message: str | None = Field(None, description="视频生成错误信息") + + +class ShotReplicateTaskDetailOut(BaseModel): + id: str = Field(..., description="总任务项目ID。这个ID就是前端项目ID") + project_id: str = Field(..., description="兼容前端命名,等同于 id") + module: str = Field(..., description="模块标识,拆镜复刻固定为 shot_replicate") + title: str | None = Field(None, description="项目标题,默认取生成项目名称") + status: str = Field(..., description="总任务状态:pending/waiting_user/processing/completed/failed/cancelled") + current_step_code: str | None = Field(None, description="当前所处步骤编码") + final_image_url: str | None = Field(None, description="最终新项目图片 URL") + final_video_url: str | None = Field(None, description="最终视频 URL") + final_video_cover_url: str | None = Field(None, description="最终视频封面 URL") + error_message: str | None = Field(None, description="总任务错误信息") + material: ShotReplicateMaterialOut = Field(default_factory=ShotReplicateMaterialOut, description="素材和项目描述信息") + image_generation: ShotReplicateImageGenerationOut = Field(default_factory=ShotReplicateImageGenerationOut, description="图片提词、图片引擎参数和图片结果") + video_generation: ShotReplicateVideoGenerationOut = Field(default_factory=ShotReplicateVideoGenerationOut, description="视频提词、视频引擎参数和视频结果") + steps: list[ShotReplicateStepOut] = Field(default_factory=list, description="当前有效子任务列表") + created_at: NaiveDatetimeOptional = Field(None, description="创建时间") + updated_at: NaiveDatetimeOptional = Field(None, description="更新时间") + completed_at: NaiveDatetimeOptional = Field(None, description="完成时间") + + +class ShotReplicateTaskListItemOut(BaseModel): + id: str = Field(..., description="总任务项目ID。这个ID就是前端项目ID") + project_id: str = Field(..., description="兼容前端命名,等同于 id") + module: str = Field(..., description="模块标识") + title: str | None = Field(None, description="项目标题") + status: str = Field(..., description="总任务状态") + current_step_code: str | None = Field(None, description="当前步骤") + target_project_name: str | None = Field(None, description="生成项目名称,来源于第1步素材输入") + final_image_url: str | None = Field(None, description="最终图片 URL") + final_video_url: str | None = Field(None, description="最终视频 URL") + error_message: str | None = Field(None, description="错误信息") + created_at: NaiveDatetimeOptional = Field(None, description="创建时间") + updated_at: NaiveDatetimeOptional = Field(None, description="更新时间") + completed_at: NaiveDatetimeOptional = Field(None, description="完成时间") + + +class ShotReplicateTaskListOut(BaseModel): + total: int = Field(..., description="总数量") + items: list[ShotReplicateTaskListItemOut] = Field(default_factory=list, description="列表数据") + + +class ShotReplicateActionOut(BaseModel): + message: str = Field(..., description="操作结果提示") + project_id: str = Field(..., description="总任务项目ID") + step_id: str | None = Field(None, description="本次创建或修改的子任务ID") + next_step_id: str | None = Field(None, description="兼容字段:当前接口不自动生成下下个任务,一般为空") + detail: ShotReplicateTaskDetailOut | None = Field(None, description="操作后的总任务详情") + + +class ShotReplicateDeleteOut(BaseModel): + message: str = Field(..., description="删除结果提示") + project_id: str = Field(..., description="被软删除的总任务项目ID") + deleted: bool = Field(..., description="是否已软删除") + + +class ShotReplicateSpecOut(BaseModel): + project_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_REPLICATE_PROJECT_STATUS_DESCRIPTIONS, description="总任务状态说明") + step_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_REPLICATE_STEP_STATUS_DESCRIPTIONS, description="子任务状态说明") + steps: list[dict[str, Any]] = Field(default_factory=lambda: SHOT_REPLICATE_STEP_DESCRIPTIONS, description="5个固定步骤说明") + step_io_schema_version: str = Field(default=SHOT_REPLICATE_STEP_IO_SCHEMA_VERSION, description="步骤 input_json/output_json 结构版本") + step_io_examples: dict[str, dict[str, Any]] = Field(default_factory=lambda: SHOT_REPLICATE_STEP_IO_EXAMPLES, description="每个步骤 input_json/output_json 示例") + + +# ======================== +# 拆镜总任务集 / 片段 API Schema +# ======================== + +SHOT_TASK_SET_STATUS_DESCRIPTIONS: dict[str, str] = { + "pending_analysis": "已创建,等待原视频分析", + "analyzing": "原视频分析中", + "analysis_completed": "原视频分析完成", + "analysis_failed": "原视频分析失败", + "splitting": "拆镜处理中", + "split_completed": "拆镜全部完成", + "partial_failed": "部分片段失败", + "failed": "总任务失败", + "deleted": "已软删", +} + +SHOT_ANALYSIS_STATUS_DESCRIPTIONS: dict[str, str] = { + "pending": "待分析", + "processing": "分析中", + "completed": "分析完成", + "failed": "分析失败", +} + +SHOT_SPLIT_STATUS_DESCRIPTIONS: dict[str, str] = { + "none": "尚未拆镜", + "pending": "待拆镜", + "processing": "拆镜中", + "completed": "拆镜完成", + "failed": "拆镜失败", + "retry_waiting": "等待恢复重试", +} + +SHOT_SEGMENT_SOURCE_MODE_DESCRIPTIONS: dict[str, str] = { + "ai_suggestion": "AI 建议拆镜", + "custom": "用户自定义拆镜", +} + +SHOT_SEGMENT_ANALYSIS_STATUS_DESCRIPTIONS: dict[str, str] = { + "not_required": "不需要单独分析,通常用于 AI 建议拆镜", + "pending": "等待片段分析", + "processing": "片段分析中", + "completed": "片段分析完成", + "failed": "片段分析失败", +} + +SHOT_SEGMENT_REPLICATE_STATUS_DESCRIPTIONS: dict[str, str] = { + "not_started": "未进入复刻流程", + "project_created": "已创建复刻项目", + "processing": "复刻流程处理中", + "completed": "复刻流程完成", + "failed": "复刻流程失败", +} + + +class ShotAISuggestionOut(BaseModel): + index: int = Field(..., description="AI 建议序号,从1开始") + start_second: float = Field(..., description="拆镜开始秒") + end_second: float = Field(..., description="拆镜结束秒") + duration_seconds: float = Field(..., description="片段时长") + time_node: str = Field(..., description="拆镜时间节点,例如 0-15秒") + content: str = Field(..., description="对应时间节点内的内容") + category: str = Field(..., description="片段分类") + audience: str = Field(..., description="片段受众人群") + + +class ShotTaskSetCreate(BaseModel): + model_config = ConfigDict( + json_schema_extra={ + "example": { + "video_url": "/uploads/2026/06/11/demo.mp4", + "video_duration_seconds": 31.42, + "title": "游戏视频拆镜", + "idempotency_key": "frontend-shot-task-001", + } + } + ) + + video_url: str = Field(..., min_length=1, description="已有上传接口返回的视频链接。必须能反解到 storage/uploads 下文件") + video_duration_seconds: float = Field(..., gt=0, description="前端获取的视频时长秒数,允许浮点;后端会用 ffprobe 校验并以后端真实时长为准") + title: str | None = Field(None, max_length=160, description="拆镜总任务标题") + idempotency_key: str | None = Field(None, max_length=64, description="创建总任务幂等键") + + @field_validator("video_url", "title", "idempotency_key", mode="before") + @classmethod + def _strip_optional_text(cls, value: str | None) -> str | None: + if value is None: + return None + value = str(value).strip() + if not value: + return None + return value + + +class ShotTaskSetListQuery(BaseModel): + status: str | None = Field(None, description="总任务状态筛选,见 ShotTaskSetStatusEnum") + analysis_status: str | None = Field(None, description="分析状态筛选,见 ShotAnalysisStatusEnum") + split_status: str | None = Field(None, description="拆镜状态筛选,见 ShotSplitStatusEnum") + keyword: str | None = Field(None, description="标题/内容关键词") + page: int = Field(1, ge=1, description="页码") + page_size: int = Field(20, ge=1, le=100, description="每页数量") + + +class ShotTaskSetOut(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + title: str | None = None + video_url: str + video_duration_seconds: float + status: str + analysis_status: str + split_status: str + original_video_content: str | None = None + original_video_category: str | None = None + original_video_audience: str | None = None + segment_count: int = 0 + completed_segment_count: int = 0 + failed_segment_count: int = 0 + analysis_error_message: str | None = None + split_error_message: str | None = None + created_at: NaiveDatetimeOptional = None + updated_at: NaiveDatetimeOptional = None + + +class ShotTaskSetListOut(BaseModel): + total: int + page: int + page_size: int + items: list[ShotTaskSetOut] + + +class ShotTaskSetDetailOut(ShotTaskSetOut): + ai_suggestions: list[ShotAISuggestionOut] = Field(default_factory=list) + analysis_result_json: dict[str, Any] | list[Any] | None = None + + +class ShotSplitByAIRequest(BaseModel): + selected_indices: list[int] | None = Field(None, description="指定 AI 建议序号。不传则全部拆") + replace_existing: bool = Field(False, description="是否软删旧 AI 建议片段后重新拆") + + +class ShotSplitCustomRequest(BaseModel): + start_second: float = Field(..., ge=0, description="自定义拆镜开始秒,允许浮点") + end_second: float = Field(..., gt=0, description="自定义拆镜结束秒,允许浮点,必须大于 start_second") + + @model_validator(mode="after") + def _check_range(self) -> "ShotSplitCustomRequest": + if self.end_second <= self.start_second: + raise ValueError("end_second 必须大于 start_second") + return self + + +class ShotSegmentOut(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + task_set_id: str + segment_index: int + segment_name: str | None = None + source_mode: str + start_second: float + end_second: float + duration_seconds: float + time_node: str + split_status: str + analysis_status: str + replicate_status: str + segment_video_url: str | None = None + original_video_content: str | None = None + original_video_category: str | None = None + original_video_audience: str | None = None + segment_content: str | None = None + segment_category: str | None = None + segment_audience: str | None = None + split_retry_count: int = 0 + split_last_error: str | None = None + analysis_error_message: str | None = None + module_project_id: str | None = None + created_at: NaiveDatetimeOptional = None + updated_at: NaiveDatetimeOptional = None + + +class ShotSegmentDetailOut(ShotSegmentOut): + analysis_json: dict[str, Any] | list[Any] | None = None + ai_suggestion_json: dict[str, Any] | list[Any] | None = None + + +class ShotSegmentListOut(BaseModel): + total: int + page: int + page_size: int + items: list[ShotSegmentOut] + + +class ShotSplitByAIOut(BaseModel): + task_set_id: str + status: str + split_status: str + created_segment_count: int + segments: list[ShotSegmentOut] + + +class ShotSplitCustomOut(BaseModel): + task_set_id: str + segment: ShotSegmentOut + + +class ShotSegmentReplicationCreateRequest(BaseModel): + model_config = ConfigDict( + json_schema_extra={ + "example": { + "target_project_name": "新产品推广视频", + "core_content_point": "突出产品附近交友和快速脱单", + "material_image_url": "/uploads/2026/06/11/product.png", + "idempotency_key": "optional-key", + } + } + ) + + target_project_name: str = Field(..., min_length=1, max_length=20, description="生成项目名称") + core_content_point: str = Field(..., min_length=1, max_length=50, description="生成项目核心内容点,最多50字") + material_image_url: str = Field(..., min_length=1, description="新产品/目标素材图片链接,来自已有上传接口") + idempotency_key: str | None = Field(None, max_length=64, description="创建 ModuleGenerationProject 幂等键") + + @field_validator("target_project_name", "core_content_point", "material_image_url", "idempotency_key", mode="before") + @classmethod + def _strip_text(cls, value: str | None) -> str | None: + if value is None: + return None + value = str(value).strip() + if not value: + return None + return value + + +class ShotReplicateSpecOut(BaseModel): + project_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_REPLICATE_PROJECT_STATUS_DESCRIPTIONS) + step_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_REPLICATE_STEP_STATUS_DESCRIPTIONS) + steps: list[dict[str, Any]] = Field(default_factory=lambda: SHOT_REPLICATE_STEP_DESCRIPTIONS) + step_io_schema_version: str = SHOT_REPLICATE_STEP_IO_SCHEMA_VERSION + step_io_examples: dict[str, dict[str, Any]] = Field(default_factory=lambda: SHOT_REPLICATE_STEP_IO_EXAMPLES) + task_set_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_TASK_SET_STATUS_DESCRIPTIONS) + analysis_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_ANALYSIS_STATUS_DESCRIPTIONS) + split_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_SPLIT_STATUS_DESCRIPTIONS) + segment_source_modes: dict[str, str] = Field(default_factory=lambda: SHOT_SEGMENT_SOURCE_MODE_DESCRIPTIONS) + segment_analysis_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_SEGMENT_ANALYSIS_STATUS_DESCRIPTIONS) + segment_replicate_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_SEGMENT_REPLICATE_STATUS_DESCRIPTIONS) diff --git a/video-gen-api/app/services/celery_download_recovery_service.py b/video-gen-api/app/services/celery_download_recovery_service.py index a74b5f28..2ee7e877 100644 --- a/video-gen-api/app/services/celery_download_recovery_service.py +++ b/video-gen-api/app/services/celery_download_recovery_service.py @@ -1,103 +1,28 @@ # app/services/celery_download_recovery_service.py from __future__ import annotations -import inspect -import json -import logging -from datetime import datetime, timezone +from datetime import datetime from typing import Any, Dict, Iterable, List, Optional, Union from app.config import settings - -try: - from redis.exceptions import RedisError -except ImportError: - RedisError = RuntimeError # type: ignore[assignment] +from app.services.redis_registry_service import ( + close_registry_redis, + datetime_to_epoch, + ensure_aware_utc, + get_registry_redis, + redis_get_due_registry_ids, + redis_get_registry_payloads, + redis_postpone_registry_item, + redis_remove_registry_item, + redis_upsert_registry_item, + utc_now, +) -logger = logging.getLogger("video_gen") - -_redis_client: Optional[Any] = None - - -def utc_now() -> datetime: - return datetime.now(timezone.utc) - - -def ensure_aware_utc(value: Optional[datetime]) -> Optional[datetime]: - if value is None: - return None - - if value.tzinfo is None: - return value.replace(tzinfo=timezone.utc) - - return value.astimezone(timezone.utc) - - -def datetime_to_epoch(value: Optional[datetime]) -> int: - checked_value = ensure_aware_utc(value) or utc_now() - return int(checked_value.timestamp()) - - -def _registry_redis_url() -> str: - return settings.CELERY_BROKER_URL or settings.REDIS_URL or "" - - -async def get_registry_redis() -> Optional[Any]: - global _redis_client - - if _redis_client is not None: - return _redis_client - - redis_url = _registry_redis_url() - if not redis_url: - return None - - try: - from redis.asyncio import Redis - except ImportError as exc: - logger.warning( - "下载容灾 Redis 注册表不可用,redis 依赖未安装。error=%s", - exc, - ) - return None - - try: - redis_client = Redis.from_url(redis_url, decode_responses=True) - await redis_client.ping() - _redis_client = redis_client - return _redis_client - except (RedisError, OSError, RuntimeError) as exc: - logger.warning( - "下载容灾 Redis 注册表不可用,降级为仅 DB 容灾。error=%s", - exc, - ) - _redis_client = None - return None - - -async def close_registry_redis() -> None: - global _redis_client - - client = _redis_client - _redis_client = None - - if client is None: - return - - try: - close_method = getattr(client, "close", None) - if close_method is None: - return - - close_result = close_method() - if inspect.isawaitable(close_result): - await close_result - except (RedisError, OSError, RuntimeError) as exc: - logger.debug( - "关闭下载容灾 Redis 注册表连接失败。error=%s", - exc, - ) +# 说明: +# - 本文件保留旧函数名,作为下载容灾兼容层。 +# - 底层 Redis Hash/ZSet 操作已迁移到 redis_registry_service.py。 +# - 下载 active key、Redis URL 选择逻辑不变,避免影响已稳定下载模块。 def build_download_active_payload( @@ -131,32 +56,12 @@ def build_download_active_payload( "attempt": int(attempt or 0), "queue": queue, "priority": priority, - "enqueue_at": ( - datetime_to_epoch(checked_enqueue_at) - if checked_enqueue_at - else None - ), - "started_at": ( - datetime_to_epoch(checked_started_at) - if checked_started_at - else None - ), + "enqueue_at": datetime_to_epoch(checked_enqueue_at) if checked_enqueue_at else None, + "started_at": datetime_to_epoch(checked_started_at) if checked_started_at else None, "updated_at": datetime_to_epoch(checked_updated_at), - "lease_until": ( - datetime_to_epoch(checked_lease_until) - if checked_lease_until - else None - ), - "next_retry_at": ( - datetime_to_epoch(checked_next_retry_at) - if checked_next_retry_at - else None - ), - "check_at": ( - datetime_to_epoch(checked_check_at) - if checked_check_at - else None - ), + "lease_until": datetime_to_epoch(checked_lease_until) if checked_lease_until else None, + "next_retry_at": datetime_to_epoch(checked_next_retry_at) if checked_next_retry_at else None, + "check_at": datetime_to_epoch(checked_check_at) if checked_check_at else None, "reason": reason, } @@ -167,56 +72,23 @@ async def upsert_download_active( payload: Dict[str, Any], check_at: Optional[Union[datetime, int, float]], ) -> None: - redis = await get_registry_redis() - if redis is None: - return - - if isinstance(check_at, datetime): - score = datetime_to_epoch(check_at) - elif check_at is None: - score = datetime_to_epoch(utc_now()) - else: - score = int(float(check_at)) - - updated_payload = dict(payload) - updated_payload["check_at"] = score - - try: - pipe: Any = redis.pipeline(transaction=True) - pipe.hset( - settings.DOWNLOAD_ACTIVE_REDIS_HASH_KEY, - record_id, - json.dumps(updated_payload, ensure_ascii=False, default=str), - ) - pipe.zadd( - settings.DOWNLOAD_ACTIVE_REDIS_ZSET_KEY, - {record_id: score}, - ) - await pipe.execute() - except (RedisError, OSError, RuntimeError, TypeError, ValueError) as exc: - logger.warning( - "写入下载容灾 Redis 注册表失败。record_id=%s, error=%s", - record_id, - exc, - ) + await redis_upsert_registry_item( + hash_key=settings.DOWNLOAD_ACTIVE_REDIS_HASH_KEY, + zset_key=settings.DOWNLOAD_ACTIVE_REDIS_ZSET_KEY, + item_id=record_id, + payload=payload, + check_at=check_at, + log_context="download_active", + ) async def remove_download_active(record_id: str) -> None: - redis = await get_registry_redis() - if redis is None: - return - - try: - pipe: Any = redis.pipeline(transaction=True) - pipe.hdel(settings.DOWNLOAD_ACTIVE_REDIS_HASH_KEY, record_id) - pipe.zrem(settings.DOWNLOAD_ACTIVE_REDIS_ZSET_KEY, record_id) - await pipe.execute() - except (RedisError, OSError, RuntimeError) as exc: - logger.warning( - "删除下载容灾 Redis 注册表失败。record_id=%s, error=%s", - record_id, - exc, - ) + await redis_remove_registry_item( + hash_key=settings.DOWNLOAD_ACTIVE_REDIS_HASH_KEY, + zset_key=settings.DOWNLOAD_ACTIVE_REDIS_ZSET_KEY, + item_id=record_id, + log_context="download_active", + ) async def get_due_download_record_ids( @@ -224,68 +96,22 @@ async def get_due_download_record_ids( limit: Optional[int] = None, now: Optional[datetime] = None, ) -> List[str]: - redis = await get_registry_redis() - if redis is None: - return [] - - batch_limit = int(limit or settings.DOWNLOAD_RECOVERY_BATCH_SIZE or 100) - score = datetime_to_epoch(now or utc_now()) - - try: - result = await redis.zrangebyscore( - settings.DOWNLOAD_ACTIVE_REDIS_ZSET_KEY, - min="-inf", - max=score, - start=0, - num=batch_limit, - ) - return [str(item) for item in result] - except (RedisError, OSError, RuntimeError, TypeError, ValueError) as exc: - logger.warning( - "扫描下载容灾 Redis ZSet 失败。error=%s", - exc, - ) - return [] + return await redis_get_due_registry_ids( + zset_key=settings.DOWNLOAD_ACTIVE_REDIS_ZSET_KEY, + limit=limit or int(settings.DOWNLOAD_RECOVERY_BATCH_SIZE or 100), + now=now, + log_context="download_active", + ) async def get_download_active_payloads( record_ids: Iterable[str], ) -> Dict[str, Dict[str, Any]]: - cleaned_record_ids = [str(item) for item in record_ids if item] - if not cleaned_record_ids: - return {} - - redis = await get_registry_redis() - if redis is None: - return {} - - try: - raw_values = await redis.hmget( - settings.DOWNLOAD_ACTIVE_REDIS_HASH_KEY, - cleaned_record_ids, - ) - except (RedisError, OSError, RuntimeError, TypeError, ValueError) as exc: - logger.warning( - "读取下载容灾 Redis Hash 失败。error=%s", - exc, - ) - return {} - - result: Dict[str, Dict[str, Any]] = {} - - for record_id, raw in zip(cleaned_record_ids, raw_values): - if not raw: - continue - - try: - value = json.loads(raw) - except (TypeError, ValueError, json.JSONDecodeError): - continue - - if isinstance(value, dict): - result[record_id] = value - - return result + return await redis_get_registry_payloads( + hash_key=settings.DOWNLOAD_ACTIVE_REDIS_HASH_KEY, + item_ids=record_ids, + log_context="download_active", + ) async def postpone_download_active_check( @@ -294,45 +120,14 @@ async def postpone_download_active_check( payload: Optional[Dict[str, Any]] = None, check_at: Optional[Union[datetime, int, float]] = None, ) -> None: - redis = await get_registry_redis() - if redis is None: - return + if check_at is None: + check_at = utc_now().timestamp() + int(settings.DOWNLOAD_TASK_QUEUE_TIMEOUT_SECONDS or 300) - if isinstance(check_at, datetime): - score = datetime_to_epoch(check_at) - elif check_at is None: - score = datetime_to_epoch(utc_now()) + int( - settings.DOWNLOAD_TASK_QUEUE_TIMEOUT_SECONDS or 300 - ) - else: - score = int(float(check_at)) - - try: - pipe: Any = redis.pipeline(transaction=True) - pipe.zadd( - settings.DOWNLOAD_ACTIVE_REDIS_ZSET_KEY, - {record_id: score}, - ) - - if payload is not None: - updated_payload = dict(payload) - updated_payload["check_at"] = score - updated_payload["updated_at"] = datetime_to_epoch(utc_now()) - - pipe.hset( - settings.DOWNLOAD_ACTIVE_REDIS_HASH_KEY, - record_id, - json.dumps( - updated_payload, - ensure_ascii=False, - default=str, - ), - ) - - await pipe.execute() - except (RedisError, OSError, RuntimeError, TypeError, ValueError) as exc: - logger.warning( - "刷新下载容灾 Redis 检查时间失败。record_id=%s, error=%s", - record_id, - exc, - ) + await redis_postpone_registry_item( + hash_key=settings.DOWNLOAD_ACTIVE_REDIS_HASH_KEY, + zset_key=settings.DOWNLOAD_ACTIVE_REDIS_ZSET_KEY, + item_id=record_id, + payload=payload, + check_at=check_at, + log_context="download_active", + ) diff --git a/video-gen-api/app/services/generation_module_hook_service.py b/video-gen-api/app/services/generation_module_hook_service.py index ebf3f03c..9a518725 100644 --- a/video-gen-api/app/services/generation_module_hook_service.py +++ b/video-gen-api/app/services/generation_module_hook_service.py @@ -23,3 +23,15 @@ async def notify_chat_generation_task_finished(db: AsyncSession, task: ChatGener await handle_chat_generation_task_completed(db, task) elif task.status == "failed": await handle_chat_generation_task_failed(db, task) + return + + if task.generation_mode == "shot_replicate": + from app.services.shot_replicate_flow_service import ( + handle_chat_generation_task_completed, + handle_chat_generation_task_failed, + ) + if task.status == "completed": + await handle_chat_generation_task_completed(db, task) + elif task.status == "failed": + await handle_chat_generation_task_failed(db, task) + return diff --git a/video-gen-api/app/services/generation_recovery_service.py b/video-gen-api/app/services/generation_recovery_service.py index 33ffd7a3..ea9ebe18 100644 --- a/video-gen-api/app/services/generation_recovery_service.py +++ b/video-gen-api/app/services/generation_recovery_service.py @@ -1,10 +1,11 @@ from __future__ import annotations +import json import logging from datetime import datetime, timedelta, timezone -from typing import Any, Dict +from typing import Any -from sqlalchemy import or_, select +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings @@ -16,13 +17,21 @@ from app.services.celery_download_recovery_service import ( postpone_download_active_check, remove_download_active, ) -from app.services.generation_log_service import log_task_event +from app.services.generation_log_service import log_provider_call, log_task_event from app.services.generation_module_hook_service import notify_chat_generation_task_finished +from app.services.generation_provider_service import poll_provider_task from app.services.generation_refund_service import mark_chat_generation_task_failed_and_refund_once +from app.services.redis_registry_service import ( + redis_get_due_registry_ids, + redis_get_registry_payloads, + redis_postpone_registry_item, + redis_remove_registry_item, +) logger = logging.getLogger("video_gen") -ALLOWED_GENERATION_MODES = {"chatapi_async", "hot_opening_replicate"} +ALLOWED_GENERATION_MODES = {"chatapi_async", "hot_opening_replicate", "shot_replicate"} +POLL_QUEUE = "gen_provider_poll" def _now() -> datetime: @@ -58,6 +67,52 @@ def _is_final_task_state(task: ChatGenerationTask) -> bool: ) +def _is_success(status: str | None) -> bool: + return str(status or "").lower() in ("succeeded", "success", "completed", "done") + + +def _is_failed(status: str | None) -> bool: + return str(status or "").lower() in ("failed", "error", "canceled", "cancelled") + + +def _engine_snapshot(task: ChatGenerationTask) -> dict[str, Any]: + try: + value = json.loads(task.engine_snapshot_json or "{}") + return value if isinstance(value, dict) else {} + except Exception: + return {} + + +def _poll_queue_timeout_at(now: datetime | None = None) -> datetime: + current_time = now or _now() + return current_time + timedelta(seconds=int(settings.POLL_TASK_QUEUE_TIMEOUT_SECONDS or 120)) + + +async def _remove_poll_active(task_id: str) -> None: + await redis_remove_registry_item( + hash_key=settings.POLL_ACTIVE_REDIS_HASH_KEY, + zset_key=settings.POLL_ACTIVE_REDIS_ZSET_KEY, + item_id=task_id, + log_context="poll_active", + ) + + +async def _postpone_poll_active( + *, + task_id: str, + payload: dict[str, Any] | None = None, + check_at: datetime | int | float | None = None, +) -> None: + await redis_postpone_registry_item( + hash_key=settings.POLL_ACTIVE_REDIS_HASH_KEY, + zset_key=settings.POLL_ACTIVE_REDIS_ZSET_KEY, + item_id=task_id, + payload=payload, + check_at=check_at or _poll_queue_timeout_at(), + log_context="poll_active", + ) + + async def recover_one_download_task( db: AsyncSession, task: ChatGenerationTask, @@ -221,7 +276,7 @@ async def recover_download_tasks_once(db: AsyncSession) -> dict[str, Any]: select(ChatGenerationTask) .where( ChatGenerationTask.deleted_at.is_(None), - ChatGenerationTask.generation_mode.in_(["chatapi_async", "hot_opening_replicate"]), + ChatGenerationTask.generation_mode.in_(["chatapi_async", "hot_opening_replicate", "shot_replicate"]), ChatGenerationTask.status == "generating", ChatGenerationTask.remote_result_url.is_not(None), ChatGenerationTask.pipeline_stage.in_( @@ -249,134 +304,363 @@ async def recover_download_tasks_once(db: AsyncSession) -> dict[str, Any]: return {"checked": len(checked_ids), "results": results} -async def recover_generation_tasks_once(db: AsyncSession) -> dict[str, Any]: - """启动时生成链路容灾扫描。 +async def _mark_timeout( + db: AsyncSession, + task: ChatGenerationTask, + *, + error_message: str = "任务超时", +) -> str: + await mark_chat_generation_task_failed_and_refund_once( + db, + task=task, + error_message=error_message, + pipeline_stage="timeout", + ) + await notify_chat_generation_task_finished(db, task) + await db.commit() + await _remove_poll_active(task.id) + await log_task_event( + task, + event_type="TASK_TIMEOUT", + to_status="failed", + to_stage="timeout", + ) + return "mark_timeout" - 只在 Celery worker 启动时跑一次,不引入 beat,不新增第四条启动命令。 - 用于把 queued/creating/waiting_remote/polling/result_ready 等中间态重新投递到现有三个队列。 + +async def _mark_failed( + db: AsyncSession, + task: ChatGenerationTask, + *, + error_message: str, + event_type: str = "POLL_FAILED", + detail: Any = None, +) -> str: + await mark_chat_generation_task_failed_and_refund_once( + db, + task=task, + error_message=error_message, + pipeline_stage="failed", + ) + await notify_chat_generation_task_finished(db, task) + await db.commit() + await _remove_poll_active(task.id) + await log_task_event(task, event_type=event_type, message=task.error_message, detail=detail) + return "mark_failed" + + +async def _try_final_poll_before_timeout(db: AsyncSession, task: ChatGenerationTask) -> str: + """超时前最后查一次供应商,避免 Celery 中断导致本地假超时。 + + 如果供应商已经成功,继续进入下载;如果仍 running 或查询失败,再按超时处理。 """ + from app.tasks.generation_download_tasks import enqueue_download_task + + if not (task.provider_task_id or task.seedance_task_id): + return await _mark_timeout(db, task) + + try: + poll_result = await poll_provider_task(db, task) + status = poll_result.get("status") + response_data = poll_result.get("response_data") + except Exception as exc: + await log_task_event( + task, + event_type="FINAL_POLL_BEFORE_TIMEOUT_ERROR", + message=str(exc), + ) + return await _mark_timeout(db, task) + + try: + provider_response = json.loads(response_data or "{}") + except Exception: + provider_response = {"raw": response_data} + + snapshot = _engine_snapshot(task) + await log_provider_call( + task, + provider=snapshot.get("provider") or "ark", + api_type=f"{task.gen_type}_final_poll_before_timeout", + model=snapshot.get("model_name"), + engine_id=task.engine_id, + status="success", + provider_task_id=task.seedance_task_id or task.provider_task_id, + response_data=provider_response, + ) + + if _is_success(status): + if task.gen_type == "image": + task.remote_result_url = poll_result.get("image_url") + task.image_tokens_used = poll_result.get("image_tokens", 0) or 0 + else: + task.remote_result_url = poll_result.get("video_url") + task.video_tokens_used = poll_result.get("video_tokens", 0) or 0 + + task.provider_response_json = response_data + if not task.remote_result_url: + return await _mark_failed( + db, + task, + error_message="供应商任务成功但未返回结果URL", + detail=poll_result, + ) + + task.pipeline_stage = "result_ready" + task.retry_count = 0 + await db.commit() + await _remove_poll_active(task.id) + await log_task_event( + task, + event_type="POLL_SUCCESS_AFTER_TIMEOUT_RECOVERY", + to_stage="result_ready", + detail=poll_result, + ) + await enqueue_download_task(db, task, recover=True, reason="final_poll_before_timeout_success") + return "recover_timeout_success_to_download" + + if _is_failed(status): + task.provider_response_json = response_data + return await _mark_failed( + db, + task, + error_message=poll_result.get("error") or f"供应商任务失败: {status}", + detail=poll_result, + ) + + await log_task_event( + task, + event_type="FINAL_POLL_BEFORE_TIMEOUT_PENDING", + message=f"status={status}", + detail=poll_result, + ) + return await _mark_timeout(db, task) + + +async def recover_one_generation_task( + db: AsyncSession, + task: ChatGenerationTask, + *, + payload: dict[str, Any] | None = None, + source: str = "startup_db", +) -> str: from app.tasks.generation_create_tasks import chatapi_create_generation_task from app.tasks.generation_download_tasks import enqueue_download_task - from app.tasks.generation_poll_tasks import poll_generation_task + from app.tasks.generation_poll_tasks import poll_generation_task, register_poll_active current_time = _now() - results: dict[str, int] = {} + redis_payload = payload or {} - query_result = await db.execute( - select(ChatGenerationTask) - .where( - ChatGenerationTask.deleted_at.is_(None), - ChatGenerationTask.generation_mode.in_(["chatapi_async", "hot_opening_replicate"]), - ChatGenerationTask.status == "generating", - ChatGenerationTask.pipeline_stage.in_( - [ - "queued", - "preparing", - "creating_provider_task", - "waiting_remote", - "polling", - "result_ready", - ] - ), - ) - .order_by(ChatGenerationTask.updated_at.asc()) - .limit(int(settings.DOWNLOAD_RECOVERY_BATCH_SIZE or 100)) - .with_for_update(skip_locked=True) - ) - tasks = query_result.scalars().all() + if not task: + return "skip_missing_task" + if task.generation_mode not in ALLOWED_GENERATION_MODES: + await _remove_poll_active(task.id) + return "clean_invalid_mode" + if _is_final_task_state(task): + await _remove_poll_active(task.id) + return "clean_final_state" + if task.status != "generating": + await _remove_poll_active(task.id) + return "clean_not_generating" - for task in tasks: - if task.deadline_at and _is_expired(task.deadline_at, current_time): - await mark_chat_generation_task_failed_and_refund_once( - db, - task=task, - error_message="任务超时", - pipeline_stage="timeout", - ) - await notify_chat_generation_task_finished(db, task) + if task.deadline_at and _is_expired(task.deadline_at, current_time): + if task.pipeline_stage in ("waiting_remote", "polling"): + return await _try_final_poll_before_timeout(db, task) + return await _mark_timeout(db, task) + + if task.pipeline_stage in ("queued", "preparing", "creating_provider_task"): + if task.provider_task_id or task.seedance_task_id: + task.pipeline_stage = "waiting_remote" await db.commit() await log_task_event( task, - event_type="TASK_TIMEOUT", - to_status="failed", - to_stage="timeout", + event_type="GENERATION_RECOVERY_ENQUEUE", + message=f"{source} 发现创建阶段已存在供应商任务ID,恢复投递轮询队列", + detail={"pipeline_stage": task.pipeline_stage, "payload": redis_payload}, ) - action = "mark_timeout" + poll_generation_task.apply_async(args=[task.id], queue=POLL_QUEUE, countdown=0) + await register_poll_active( + task, + check_at=_poll_queue_timeout_at(), + reason=f"{source}_create_stage_has_provider_id", + ) + return "recover_poll_from_create_stage" - elif task.pipeline_stage in ("queued", "preparing", "creating_provider_task"): + await log_task_event( + task, + event_type="GENERATION_RECOVERY_ENQUEUE", + message=f"{source} 发现创建阶段任务未完成,恢复投递创建队列", + detail={"pipeline_stage": task.pipeline_stage, "payload": redis_payload}, + ) + chatapi_create_generation_task.apply_async( + args=[task.id], + queue="gen_chatapi_create", + countdown=0, + ) + return "recover_create" + + if task.pipeline_stage in ("waiting_remote", "polling"): + if task.remote_result_url: + await _remove_poll_active(task.id) + await enqueue_download_task( + db, + task, + recover=True, + reason=f"{source}_waiting_remote_has_result", + ) + return "recover_waiting_has_result" + + if task.provider_task_id or task.seedance_task_id: await log_task_event( task, event_type="GENERATION_RECOVERY_ENQUEUE", - message="启动时发现创建阶段任务未完成,恢复投递创建队列", - detail={"pipeline_stage": task.pipeline_stage}, + message=f"{source} 发现远程等待/轮询阶段任务未完成,恢复投递轮询队列", + detail={"pipeline_stage": task.pipeline_stage, "payload": redis_payload}, ) - chatapi_create_generation_task.apply_async( + task.pipeline_stage = "waiting_remote" + await db.commit() + poll_generation_task.apply_async( args=[task.id], - queue="gen_chatapi_create", + queue=POLL_QUEUE, countdown=0, ) - action = "recover_create" + await register_poll_active( + task, + check_at=_poll_queue_timeout_at(), + reason=f"{source}_recover_poll", + ) + return "recover_poll" - elif task.pipeline_stage in ("waiting_remote", "polling"): - if task.remote_result_url: - await enqueue_download_task( - db, - task, - recover=True, - reason="startup_waiting_remote_has_result", - ) - action = "recover_waiting_has_result" - elif task.provider_task_id or task.seedance_task_id: - await log_task_event( - task, - event_type="GENERATION_RECOVERY_ENQUEUE", - message="启动时发现远程等待/轮询阶段任务未完成,恢复投递轮询队列", - detail={"pipeline_stage": task.pipeline_stage}, - ) - task.pipeline_stage = "waiting_remote" - await db.commit() - poll_generation_task.apply_async( - args=[task.id], - queue="gen_provider_poll", - countdown=0, - ) - action = "recover_poll" - else: - await log_task_event( - task, - event_type="GENERATION_RECOVERY_ENQUEUE", - message="启动时发现任务缺少供应商任务ID,恢复投递创建队列", - detail={"pipeline_stage": task.pipeline_stage}, - ) - task.pipeline_stage = "queued" - await db.commit() - chatapi_create_generation_task.apply_async( - args=[task.id], - queue="gen_chatapi_create", - countdown=0, - ) - action = "recover_create_missing_provider_id" + await log_task_event( + task, + event_type="GENERATION_RECOVERY_ENQUEUE", + message=f"{source} 发现任务缺少供应商任务ID,恢复投递创建队列", + detail={"pipeline_stage": task.pipeline_stage, "payload": redis_payload}, + ) + task.pipeline_stage = "queued" + await db.commit() + await _remove_poll_active(task.id) + chatapi_create_generation_task.apply_async( + args=[task.id], + queue="gen_chatapi_create", + countdown=0, + ) + return "recover_create_missing_provider_id" - elif task.pipeline_stage == "result_ready": - if task.remote_result_url: - await enqueue_download_task( - db, - task, - recover=True, - reason="startup_generation_result_ready", - ) - action = "recover_result_ready" - else: - action = "skip_result_ready_no_url" + if task.pipeline_stage == "result_ready": + await _remove_poll_active(task.id) + if task.remote_result_url: + await enqueue_download_task( + db, + task, + recover=True, + reason=f"{source}_generation_result_ready", + ) + return "recover_result_ready" + return "skip_result_ready_no_url" + + return f"skip_stage_{task.pipeline_stage}" + + +async def recover_generation_tasks_once(db: AsyncSession) -> dict[str, Any]: + """启动时生成链路容灾扫描。 + + 不新增 Celery beat,不新增 worker 命令;worker 启动时由 Redis 锁保证只投递一次。 + 恢复顺序: + 1. Redis poll active_index 到期任务; + 2. DB fallback 扫描 queued/creating/waiting_remote/polling/result_ready; + 3. 下载阶段仍由 recover_download_tasks_once 兜底。 + """ + checked_ids: set[str] = set() + results: dict[str, int] = {} + + due_poll_ids = await redis_get_due_registry_ids( + zset_key=settings.POLL_ACTIVE_REDIS_ZSET_KEY, + limit=int(settings.POLL_RECOVERY_BATCH_SIZE or settings.GENERATION_RECOVERY_BATCH_SIZE or 100), + log_context="poll_active", + ) + poll_payloads = await redis_get_registry_payloads( + hash_key=settings.POLL_ACTIVE_REDIS_HASH_KEY, + item_ids=due_poll_ids, + log_context="poll_active", + ) + + for task_id in due_poll_ids: + result = await db.execute( + select(ChatGenerationTask) + .where( + ChatGenerationTask.id == task_id, + ChatGenerationTask.deleted_at.is_(None), + ) + .with_for_update() + .limit(1) + ) + task = result.scalar_one_or_none() + if task is None: + await _remove_poll_active(task_id) + action = "clean_missing_poll_task" else: - action = f"skip_stage_{task.pipeline_stage}" - + checked_ids.add(task.id) + action = await recover_one_generation_task( + db, + task, + payload=poll_payloads.get(task_id), + source="startup_poll_redis", + ) results[action] = results.get(action, 0) + 1 + batch_size = int(settings.GENERATION_RECOVERY_BATCH_SIZE or settings.DOWNLOAD_RECOVERY_BATCH_SIZE or 100) + max_rounds = max(1, int(settings.GENERATION_RECOVERY_MAX_ROUNDS or 1)) + total_db_checked = 0 + + for _round in range(max_rounds): + query_result = await db.execute( + select(ChatGenerationTask) + .where( + ChatGenerationTask.deleted_at.is_(None), + ChatGenerationTask.generation_mode.in_(["chatapi_async", "hot_opening_replicate", "shot_replicate"]), + ChatGenerationTask.status == "generating", + ChatGenerationTask.pipeline_stage.in_( + [ + "queued", + "preparing", + "creating_provider_task", + "waiting_remote", + "polling", + "result_ready", + ] + ), + ) + .order_by(ChatGenerationTask.updated_at.asc()) + .limit(batch_size) + .with_for_update(skip_locked=True) + ) + tasks = query_result.scalars().all() + if not tasks: + break + + progressed_this_round = 0 + for task in tasks: + if task.id in checked_ids: + continue + action = await recover_one_generation_task( + db, + task, + payload=None, + source="startup_db", + ) + results[action] = results.get(action, 0) + 1 + checked_ids.add(task.id) + total_db_checked += 1 + progressed_this_round += 1 + + if len(tasks) < batch_size or progressed_this_round <= 0: + break + # 下载阶段单独跑 DB fallback。 download_result = await recover_download_tasks_once(db) return { - "checked": len(tasks), + "checked": len(checked_ids), + "db_checked": total_db_checked, "results": results, "download_recovery": download_result, } diff --git a/video-gen-api/app/services/generation_refund_service.py b/video-gen-api/app/services/generation_refund_service.py index 0b13b644..dcb0752a 100644 --- a/video-gen-api/app/services/generation_refund_service.py +++ b/video-gen-api/app/services/generation_refund_service.py @@ -180,7 +180,7 @@ async def mark_chat_generation_task_failed_and_refund_once( select(ChatGenerationTask) .where( ChatGenerationTask.id == task_id, - ChatGenerationTask.generation_mode.in_(["chatapi_async", "hot_opening_replicate"]), + ChatGenerationTask.generation_mode.in_(["chatapi_async", "hot_opening_replicate", "shot_replicate"]), ChatGenerationTask.deleted_at.is_(None), ) .with_for_update() diff --git a/video-gen-api/app/services/hot_opening_replicate_service.py b/video-gen-api/app/services/hot_opening_replicate_service.py index 040dea7b..9ece9061 100644 --- a/video-gen-api/app/services/hot_opening_replicate_service.py +++ b/video-gen-api/app/services/hot_opening_replicate_service.py @@ -44,7 +44,7 @@ from app.services.generation_billing_service import charge_module_prompt_usage from app.services.generation_refund_service import mark_chat_generation_task_failed_and_refund_once from app.services.generation_task_factory_service import create_chat_generation_task_for_module from app.services.hot_opening_video_prompt_service import build_final_video_prompt, optimize_hot_opening_video_prompt, patch_video_prompt_schema_from_client -from app.services.module_generation_log_service import log_module_event_file, log_module_prompt_event +from app.services.module_generation_log_service import log_module_error, log_module_event_file, log_module_prompt_event from app.services.llm import optimize_prompt from app.services.resource_accounting_service import soft_delete_chat_task_resources from app.services.resource_signed_url_service import build_resource_signed_url @@ -206,6 +206,28 @@ async def log_module_event( ) + +def _log_project_error( + *, + project: ModuleGenerationProject | None, + event_type: str, + message: str, + exc: BaseException | None = None, + step: ModuleGenerationStep | None = None, + detail: dict[str, Any] | None = None, +) -> None: + log_module_error( + module=(project.module if project else MODULE), + event_type=event_type, + project_id=(project.id if project else None), + step_id=(step.id if step else None), + user_id=(project.user_id if project else None), + message=message, + detail=detail, + exc=exc, + ) + + async def _get_project_for_user( db: AsyncSession, *, @@ -1039,6 +1061,17 @@ async def run_image_prompt_optimize(db: AsyncSession, *, project_id: str, step_i step.completed_at = _now() project.status = ModuleProjectStatusEnum.FAILED.value project.error_message = f"图片 AI 提词生成失败: {exc}" + log_module_prompt_event( + event_type="module_prompt_error", + project_id=project.id, + step_id=step.id, + user_id=project.user_id, + module=project.module, + prompt_type=ModulePromptTypeEnum.IMAGE_PROMPT.value, + request=locals().get("request_log", {}), + error=str(exc), + ) + _log_project_error(project=project, step=step, event_type="IMAGE_PROMPT_FAILED", message=project.error_message, exc=exc) await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.IMAGE_PROMPT_FAILED.value, message=project.error_message) return step @@ -1317,6 +1350,17 @@ async def run_video_prompt_optimize(db: AsyncSession, *, project_id: str, step_i step.completed_at = _now() project.status = ModuleProjectStatusEnum.FAILED.value project.error_message = f"视频 AI 提词生成失败: {exc}" + log_module_prompt_event( + event_type="module_prompt_error", + project_id=project.id, + step_id=step.id, + user_id=project.user_id, + module=project.module, + prompt_type=ModulePromptTypeEnum.VIDEO_PROMPT.value, + request=locals().get("request_log", {}), + error=str(exc), + ) + _log_project_error(project=project, step=step, event_type="VIDEO_PROMPT_FAILED", message=project.error_message, exc=exc) await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.VIDEO_PROMPT_FAILED.value, message=project.error_message) return step @@ -1517,6 +1561,16 @@ async def mark_hot_opening_step_dispatch_failed( step.completed_at = _now() project.status = ModuleProjectStatusEnum.FAILED.value project.error_message = error_message + log_module_error( + module=project.module, + event_type="CELERY_DISPATCH_FAILED", + project_id=project.id, + step_id=step.id, + user_id=project.user_id, + message=error_message, + detail={"reason": "celery_dispatch_failed", "chat_task_id": step.chat_task_id}, + error=error_message, + ) await log_module_event( db, project=project, diff --git a/video-gen-api/app/services/hot_opening_video_prompt_service.py b/video-gen-api/app/services/hot_opening_video_prompt_service.py index d6b2858f..218a0921 100644 --- a/video-gen-api/app/services/hot_opening_video_prompt_service.py +++ b/video-gen-api/app/services/hot_opening_video_prompt_service.py @@ -628,7 +628,7 @@ async def optimize_hot_opening_video_prompt( ) -> tuple[dict[str, Any], str, dict[str, Any]]: duration = int(video_config["duration"]) references = [ - {"type": "video", "url": material_video_url}, + {"type": "video", "url": _build_file_url_or_data_uri(material_video_url)}, {"type": "image", "url": _build_file_url_or_data_uri(generated_image_url)}, ] client_schema = build_dynamic_schema(video_config) diff --git a/video-gen-api/app/services/module_generation_log_service.py b/video-gen-api/app/services/module_generation_log_service.py index 1acb3b00..bb415a70 100644 --- a/video-gen-api/app/services/module_generation_log_service.py +++ b/video-gen-api/app/services/module_generation_log_service.py @@ -3,12 +3,14 @@ from __future__ import annotations import json import os import re +import traceback from datetime import datetime from typing import Any from app.services.log_config import LOG_DATE_FORMAT, LOG_DIR, is_enabled MAX_LOG_FIELD_LENGTH = 20000 +MAX_TRACEBACK_LENGTH = 12000 MODULE_LOG_ROOT = os.path.join(os.path.dirname(LOG_DIR), "ModuleGeneration") @@ -33,6 +35,23 @@ def _safe_dump_value(value: Any) -> Any: return value +def build_exception_detail(exc: BaseException | None, extra: dict[str, Any] | None = None) -> dict[str, Any]: + """构造统一异常日志 detail。日志方法必须吞异常,业务不能被日志影响。""" + detail: dict[str, Any] = dict(extra or {}) + if exc is not None: + tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)) + if len(tb) > MAX_TRACEBACK_LENGTH: + tb = tb[:MAX_TRACEBACK_LENGTH] + f"..." + detail.update( + { + "exception_type": type(exc).__name__, + "exception_message": str(exc), + "traceback": tb, + } + ) + return detail + + def _append_module_log(module: str, entry: dict[str, Any]) -> None: if not is_enabled(): return @@ -92,10 +111,7 @@ def log_module_prompt_event( token_usage: dict[str, Any] | None = None, error: str | None = None, ) -> None: - """记录模块 AI 提词请求/响应到 JSONL 文件。 - - 与模块事件共用同一个服务,但按 module 分目录,方便按模块排查。 - """ + """记录模块 AI 提词/分析请求和响应到 JSONL 文件。""" entry = { "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "log_type": "module_prompt", @@ -123,8 +139,15 @@ def log_module_error( message: str | None = None, detail: dict[str, Any] | None = None, error: str | None = None, + exc: BaseException | None = None, ) -> None: - """记录模块异常日志。""" + """记录模块异常日志。 + + - 兼容原有 detail/error 参数。 + - 新增 exc 后自动记录 exception_type、message、traceback。 + - 日志写入失败会被底层吞掉,不影响主流程。 + """ + merged_detail = build_exception_detail(exc, detail) entry = { "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "log_type": "module_error", @@ -134,7 +157,7 @@ def log_module_error( "step_id": step_id, "user_id": user_id, "message": message, - "detail": _safe_dump_value(detail or {}), - "error": error, + "detail": _safe_dump_value(merged_detail), + "error": error if error is not None else (str(exc) if exc is not None else None), } _append_module_log(module, entry) diff --git a/video-gen-api/app/services/redis_registry_service.py b/video-gen-api/app/services/redis_registry_service.py new file mode 100644 index 00000000..bc55102a --- /dev/null +++ b/video-gen-api/app/services/redis_registry_service.py @@ -0,0 +1,354 @@ +# app/services/redis_registry_service.py +from __future__ import annotations + +import asyncio +import inspect +import json +import logging +import os +import threading +import uuid +from datetime import datetime, timezone +from typing import Any, Dict, Iterable, List, Optional, Union + +from app.config import settings + +try: + from redis.exceptions import RedisError +except ImportError: # pragma: no cover - redis 未安装时降级 + RedisError = RuntimeError # type: ignore[assignment] + + +logger = logging.getLogger("video_gen") + +_redis_clients: Dict[tuple[int, int, int], Any] = {} + + +def utc_now() -> datetime: + return datetime.now(timezone.utc) + + +def ensure_aware_utc(value: Optional[datetime]) -> Optional[datetime]: + if value is None: + return None + if value.tzinfo is None: + return value.replace(tzinfo=timezone.utc) + return value.astimezone(timezone.utc) + + +def datetime_to_epoch(value: Optional[datetime]) -> int: + checked_value = ensure_aware_utc(value) or utc_now() + return int(checked_value.timestamp()) + + +def normalize_registry_score(value: Optional[Union[datetime, int, float]]) -> int: + if isinstance(value, datetime): + return datetime_to_epoch(value) + if value is None: + return datetime_to_epoch(utc_now()) + return int(float(value)) + + +def registry_redis_url() -> str: + """Celery 容灾注册表统一使用 Celery broker Redis。 + + 不能改成只读 settings.REDIS_URL,否则线上 CELERY_BROKER_URL 使用独立 + Redis DB 时,旧下载 active 注册表会被写到另一个库,导致恢复扫描失效。 + """ + return settings.CELERY_BROKER_URL or settings.REDIS_URL or "" + + +def _is_supported_redis_url(redis_url: str) -> bool: + if not redis_url: + return False + lowered = redis_url.lower() + return lowered.startswith(("redis://", "rediss://", "unix://")) + + +async def get_registry_redis() -> Optional[Any]: + """获取 Celery 容灾 Redis 连接。 + + 重点:redis.asyncio 的连接/连接池绑定 event loop,不能跨 loop 复用。 + Celery -P threads 或 worker_ready + task 线程混用时,如果使用单个全局 + Redis 客户端,会触发 got Future attached to a different loop。 + + 因此这里按 pid + thread_id + event_loop_id 缓存客户端,确保同一个客户端 + 只在创建它的事件循环里使用。Redis 不可用时返回 None,调用方降级为 + DB fallback,不能影响生成主链路。 + """ + redis_url = registry_redis_url() + if not _is_supported_redis_url(redis_url): + if redis_url: + logger.warning( + "Celery 容灾 Redis 注册表仅支持 redis/rediss/unix URL,当前 broker 不是 Redis,降级为 DB 容灾。url=%s", + redis_url, + ) + return None + + try: + from redis.asyncio import Redis + except ImportError as exc: + logger.warning( + "Celery 容灾 Redis 注册表不可用,redis 依赖未安装。error=%s", + exc, + ) + return None + + try: + loop = asyncio.get_running_loop() + except RuntimeError: + return None + + client_key = (os.getpid(), threading.get_ident(), id(loop)) + cached = _redis_clients.get(client_key) + if cached is not None: + return cached + + try: + redis_client = Redis.from_url(redis_url, decode_responses=True) + await redis_client.ping() + _redis_clients[client_key] = redis_client + return redis_client + except (RedisError, OSError, RuntimeError) as exc: + logger.warning( + "Celery 容灾 Redis 注册表不可用,降级为仅 DB 容灾。error=%s", + exc, + ) + _redis_clients.pop(client_key, None) + return None + + +async def close_registry_redis() -> None: + """关闭当前进程内已缓存的 Redis 注册表连接。 + + 关闭动作尽量只关闭当前 event loop 对应的客户端;如果调用方处于进程 + 退出阶段,则逐个尝试关闭,失败忽略,避免影响 worker 退出。 + """ + if not _redis_clients: + return + + try: + loop = asyncio.get_running_loop() + current_key = (os.getpid(), threading.get_ident(), id(loop)) + items = [(current_key, _redis_clients.pop(current_key, None))] + except RuntimeError: + items = list(_redis_clients.items()) + _redis_clients.clear() + + for _, client in items: + if client is None: + continue + try: + close_method = getattr(client, "close", None) or getattr(client, "aclose", None) + if close_method is None: + continue + close_result = close_method() + if inspect.isawaitable(close_result): + await close_result + except (RedisError, OSError, RuntimeError) as exc: + logger.debug("关闭 Celery 容灾 Redis 注册表连接失败。error=%s", exc) + + +async def redis_upsert_registry_item( + *, + hash_key: str, + zset_key: str, + item_id: str, + payload: Dict[str, Any], + check_at: Optional[Union[datetime, int, float]], + log_context: str = "registry", +) -> None: + redis = await get_registry_redis() + if redis is None: + return + + score = normalize_registry_score(check_at) + updated_payload = dict(payload) + updated_payload["check_at"] = score + updated_payload["updated_at"] = updated_payload.get("updated_at") or datetime_to_epoch(utc_now()) + + try: + pipe: Any = redis.pipeline(transaction=True) + pipe.hset(hash_key, item_id, json.dumps(updated_payload, ensure_ascii=False, default=str)) + pipe.zadd(zset_key, {item_id: score}) + await pipe.execute() + except (RedisError, OSError, RuntimeError, TypeError, ValueError) as exc: + logger.warning( + "写入 Redis 注册表失败。context=%s, item_id=%s, error=%s", + log_context, + item_id, + exc, + ) + + +async def redis_remove_registry_item( + *, + hash_key: str, + zset_key: str, + item_id: str, + log_context: str = "registry", +) -> None: + redis = await get_registry_redis() + if redis is None: + return + + try: + pipe: Any = redis.pipeline(transaction=True) + pipe.hdel(hash_key, item_id) + pipe.zrem(zset_key, item_id) + await pipe.execute() + except (RedisError, OSError, RuntimeError) as exc: + logger.warning( + "删除 Redis 注册表失败。context=%s, item_id=%s, error=%s", + log_context, + item_id, + exc, + ) + + +async def redis_get_due_registry_ids( + *, + zset_key: str, + limit: Optional[int] = None, + now: Optional[datetime] = None, + log_context: str = "registry", +) -> List[str]: + redis = await get_registry_redis() + if redis is None: + return [] + + batch_limit = int(limit or 100) + score = datetime_to_epoch(now or utc_now()) + + try: + result = await redis.zrangebyscore( + zset_key, + min="-inf", + max=score, + start=0, + num=batch_limit, + ) + return [str(item) for item in result] + except (RedisError, OSError, RuntimeError, TypeError, ValueError) as exc: + logger.warning("扫描 Redis ZSet 失败。context=%s, error=%s", log_context, exc) + return [] + + +async def redis_get_registry_payloads( + *, + hash_key: str, + item_ids: Iterable[str], + log_context: str = "registry", +) -> Dict[str, Dict[str, Any]]: + cleaned_item_ids = [str(item) for item in item_ids if item] + if not cleaned_item_ids: + return {} + + redis = await get_registry_redis() + if redis is None: + return {} + + try: + raw_values = await redis.hmget(hash_key, cleaned_item_ids) + except (RedisError, OSError, RuntimeError, TypeError, ValueError) as exc: + logger.warning("读取 Redis Hash 失败。context=%s, error=%s", log_context, exc) + return {} + + result: Dict[str, Dict[str, Any]] = {} + for item_id, raw in zip(cleaned_item_ids, raw_values): + if not raw: + continue + try: + value = json.loads(raw) + except (TypeError, ValueError, json.JSONDecodeError): + continue + if isinstance(value, dict): + result[item_id] = value + return result + + +async def redis_postpone_registry_item( + *, + hash_key: str, + zset_key: str, + item_id: str, + payload: Optional[Dict[str, Any]] = None, + check_at: Optional[Union[datetime, int, float]] = None, + log_context: str = "registry", +) -> None: + redis = await get_registry_redis() + if redis is None: + return + + score = normalize_registry_score(check_at) + + try: + pipe: Any = redis.pipeline(transaction=True) + pipe.zadd(zset_key, {item_id: score}) + + if payload is not None: + updated_payload = dict(payload) + updated_payload["check_at"] = score + updated_payload["updated_at"] = datetime_to_epoch(utc_now()) + pipe.hset(hash_key, item_id, json.dumps(updated_payload, ensure_ascii=False, default=str)) + + await pipe.execute() + except (RedisError, OSError, RuntimeError, TypeError, ValueError) as exc: + logger.warning( + "刷新 Redis 注册表检查时间失败。context=%s, item_id=%s, error=%s", + log_context, + item_id, + exc, + ) + + +async def redis_acquire_lock( + *, + lock_key: str, + ttl_seconds: int, + token: Optional[str] = None, + log_context: str = "lock", +) -> Optional[str]: + """尝试获取 Redis 分布式锁。 + + 返回 token 表示抢锁成功;返回 None 表示 Redis 不可用或锁已被其他 worker 持有。 + """ + redis = await get_registry_redis() + if redis is None: + return None + + lock_token = token or uuid.uuid4().hex + ttl = max(1, int(ttl_seconds or 60)) + + try: + acquired = await redis.set(lock_key, lock_token, nx=True, ex=ttl) + return lock_token if acquired else None + except (RedisError, OSError, RuntimeError, TypeError, ValueError) as exc: + logger.warning("获取 Redis 锁失败。context=%s, lock_key=%s, error=%s", log_context, lock_key, exc) + return None + + +async def redis_release_lock( + *, + lock_key: str, + token: str, + log_context: str = "lock", +) -> bool: + """只释放 token 匹配的锁,避免误删其他 worker 新抢到的锁。""" + redis = await get_registry_redis() + if redis is None: + return False + + script = """ + if redis.call('get', KEYS[1]) == ARGV[1] then + return redis.call('del', KEYS[1]) + else + return 0 + end + """ + try: + released = await redis.eval(script, 1, lock_key, token) + return bool(released) + except (RedisError, OSError, RuntimeError, TypeError, ValueError) as exc: + logger.warning("释放 Redis 锁失败。context=%s, lock_key=%s, error=%s", log_context, lock_key, exc) + return False diff --git a/video-gen-api/app/services/shot_replicate_flow_service.py b/video-gen-api/app/services/shot_replicate_flow_service.py new file mode 100644 index 00000000..22343d57 --- /dev/null +++ b/video-gen-api/app/services/shot_replicate_flow_service.py @@ -0,0 +1,1747 @@ +from __future__ import annotations + +import json +from datetime import datetime, timezone +from typing import Any + +from fastapi import HTTPException +from sqlalchemy import func, select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.config import settings +from app.enums.common import ModuleEventTypeEnum, ModuleProjectStatusEnum, ModulePromptTypeEnum, ModuleStepStatusEnum +from app.enums.shot_replicate import ShotReplicateGenerationModeEnum, ShotReplicateStepCodeEnum, ModuleCodeEnum +from app.models.chat_generation_task import ChatGenerationTask +from app.models.module_generation_project import ModuleGenerationProject +from app.models.module_generation_step import ModuleGenerationStep +from app.models.user import User +from app.schemas.shot_replicate import ( + ShotReplicateDeleteOut, + ShotReplicateGenerateImageRequest, + ShotReplicateGenerateVideoPromptRequest, + ShotReplicateGenerateVideoRequest, + ShotReplicateImageGenerationOut, + ShotReplicateImagePromptUpdateRequest, + ShotReplicateMaterialOut, + ShotReplicateMaterialUpdateRequest, + ShotReplicateStepOut, + ShotReplicateStepUpdate, + ShotReplicateTaskCreate, + ShotReplicateTaskDetailOut, + ShotReplicateTaskListItemOut, + ShotReplicateTaskListOut, + ShotReplicateVideoGenerationOut, + ShotReplicateVideoPromptSchemaUpdateRequest, +) +from app.services.generation_ai_service import ( + VIDEO_DEFAULT_DURATION, + VIDEO_DEFAULT_RATIO, + VIDEO_DEFAULT_RESOLUTION, + _get_video_engine, + _parse_list, +) +from app.services.generation_billing_service import charge_module_prompt_usage +from app.services.generation_refund_service import mark_chat_generation_task_failed_and_refund_once +from app.services.generation_task_factory_service import create_chat_generation_task_for_module +from app.services.hot_opening_video_prompt_service import ( + build_final_video_prompt, + optimize_hot_opening_video_prompt as optimize_shot_replicate_video_prompt, + patch_video_prompt_schema_from_client, +) +from app.services.module_generation_log_service import log_module_error, log_module_event_file, log_module_prompt_event +from app.services.llm import optimize_prompt +from app.services.resource_accounting_service import soft_delete_chat_task_resources +from app.services.resource_signed_url_service import build_resource_signed_url +from app.utils.id_gen import generate_id +from app.models.shot_replicate_segment import ShotReplicateSegment +from app.enums.shot_replicate import ShotSegmentReplicateStatusEnum, ShotSplitStatusEnum +from app.schemas.shot_replicate import ShotSegmentReplicationCreateRequest + +MODULE = ModuleCodeEnum.SHOT_REPLICATE.value +GENERATION_MODE = ShotReplicateGenerationModeEnum.SHOT_REPLICATE.value + +STEP_INDEX_MAP = { + ShotReplicateStepCodeEnum.MATERIAL_INPUT.value: 1, + ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value: 2, + ShotReplicateStepCodeEnum.IMAGE_GENERATE.value: 3, + ShotReplicateStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value: 4, + ShotReplicateStepCodeEnum.VIDEO_GENERATE.value: 5, +} + +STEP_IO_SCHEMA_VERSION = "shot_replicate_step_io_v1" + + +def _now() -> datetime: + return datetime.now(timezone.utc) + + +def _json(data: Any) -> str | None: + if data is None: + return None + return json.dumps(data, ensure_ascii=False, default=str) + + +def _parse_json(value: Any, fallback: Any = None) -> Any: + if value is None or value == "": + return fallback + if isinstance(value, (dict, list)): + return value + if isinstance(value, str): + try: + return json.loads(value) + except Exception: + return fallback + return fallback + + +def _step_input( + *, + step_code: str, + payload: dict[str, Any] | None = None, + source_step_id: str | None = None, + parent_step_id: str | None = None, + context: dict[str, Any] | None = None, +) -> dict[str, Any]: + return { + "schema_version": STEP_IO_SCHEMA_VERSION, + "step_code": step_code, + "source": { + "source_step_id": source_step_id, + "parent_step_id": parent_step_id, + }, + "payload": payload or {}, + "context": context or {}, + } + + +def _step_output( + *, + step_code: str, + status: str, + payload: dict[str, Any] | None = None, + result: dict[str, Any] | None = None, + usage: dict[str, Any] | None = None, + error: dict[str, Any] | None = None, +) -> dict[str, Any]: + return { + "schema_version": STEP_IO_SCHEMA_VERSION, + "step_code": step_code, + "status": status, + "payload": payload or {}, + "result": result or {}, + "usage": usage or {}, + "error": error or {}, + } + + +def _is_wrapped_step_io(value: Any) -> bool: + return isinstance(value, dict) and value.get("schema_version") == STEP_IO_SCHEMA_VERSION + + +def _step_payload(value: Any) -> dict[str, Any]: + data = _parse_json(value, {}) or {} + if _is_wrapped_step_io(data): + payload = data.get("payload") + return payload if isinstance(payload, dict) else {} + return data if isinstance(data, dict) else {} + + +def _step_result(value: Any) -> dict[str, Any]: + data = _parse_json(value, {}) or {} + if _is_wrapped_step_io(data): + result = data.get("result") + if isinstance(result, dict) and result: + return result + payload = data.get("payload") + return payload if isinstance(payload, dict) else {} + return data if isinstance(data, dict) else {} + + +def _step_usage(value: Any) -> dict[str, Any]: + data = _parse_json(value, {}) or {} + if _is_wrapped_step_io(data): + usage = data.get("usage") + return usage if isinstance(usage, dict) else {} + usage = data.get("token_usage") if isinstance(data, dict) else {} + return usage if isinstance(usage, dict) else {} + + +def _unwrap_step_output(value: Any) -> dict[str, Any]: + data = _parse_json(value, {}) or {} + if not _is_wrapped_step_io(data): + return data if isinstance(data, dict) else {} + merged: dict[str, Any] = {} + payload = data.get("payload") + result = data.get("result") + usage = data.get("usage") + if isinstance(payload, dict): + merged.update(payload) + if isinstance(result, dict): + merged.update(result) + if isinstance(usage, dict) and usage: + merged["token_usage"] = usage + return merged + + +def _merge_dict(old: dict[str, Any] | None, new: dict[str, Any] | None) -> dict[str, Any]: + merged = dict(old or {}) + for key, value in (new or {}).items(): + if value is not None: + merged[key] = value + return merged + + +async def log_module_event( + db: AsyncSession, + *, + project: ModuleGenerationProject, + event_type: str, + step: ModuleGenerationStep | None = None, + message: str | None = None, + detail: dict[str, Any] | None = None, +) -> None: + """模块事件日志只落盘,不再写 module_generation_events 表。""" + _ = db + log_module_event_file( + module=project.module, + event_type=event_type, + project_id=project.id, + step_id=step.id if step else None, + user_id=project.user_id, + message=message, + detail=detail, + ) + + + +def _log_project_error( + *, + project: ModuleGenerationProject | None, + event_type: str, + message: str, + exc: BaseException | None = None, + step: ModuleGenerationStep | None = None, + detail: dict[str, Any] | None = None, +) -> None: + log_module_error( + module=(project.module if project else MODULE), + event_type=event_type, + project_id=(project.id if project else None), + step_id=(step.id if step else None), + user_id=(project.user_id if project else None), + message=message, + detail=detail, + exc=exc, + ) + + +async def _get_project_for_user( + db: AsyncSession, + *, + project_id: str, + user: User, + for_update: bool = False, + populate_existing: bool = False, +) -> ModuleGenerationProject: + query = select(ModuleGenerationProject).where( + ModuleGenerationProject.id == project_id, + ModuleGenerationProject.module == MODULE, + ModuleGenerationProject.deleted_at.is_(None), + ) + if not user.is_admin: + query = query.where(ModuleGenerationProject.user_id == user.id) + if populate_existing: + query = query.execution_options(populate_existing=True) + if for_update: + query = query.with_for_update() + result = await db.execute(query.limit(1)) + project = result.scalar_one_or_none() + if not project: + raise HTTPException(status_code=404, detail="拆镜复刻项目不存在") + return project + + +async def _get_step_for_user( + db: AsyncSession, + *, + project_id: str, + step_id: str, + user: User, + for_update: bool = False, +) -> ModuleGenerationStep: + await _get_project_for_user(db, project_id=project_id, user=user, for_update=for_update) + query = select(ModuleGenerationStep).where( + ModuleGenerationStep.id == step_id, + ModuleGenerationStep.project_id == project_id, + ModuleGenerationStep.module == MODULE, + ModuleGenerationStep.deleted_at.is_(None), + ModuleGenerationStep.is_current == True, + ) + if not user.is_admin: + query = query.where(ModuleGenerationStep.user_id == user.id) + if for_update: + query = query.with_for_update() + result = await db.execute(query.limit(1)) + step = result.scalar_one_or_none() + if not step: + raise HTTPException(status_code=404, detail="子任务不存在") + return step + + +async def _get_current_steps(db: AsyncSession, project_id: str) -> list[ModuleGenerationStep]: + result = await db.execute( + select(ModuleGenerationStep) + .where( + ModuleGenerationStep.project_id == project_id, + ModuleGenerationStep.module == MODULE, + ModuleGenerationStep.deleted_at.is_(None), + ModuleGenerationStep.is_current == True, + ) + .order_by(ModuleGenerationStep.step_index.asc(), ModuleGenerationStep.created_at.asc()) + ) + return list(result.scalars().all()) + + +async def _get_current_step_by_code(db: AsyncSession, project_id: str, step_code: str) -> ModuleGenerationStep | None: + result = await db.execute( + select(ModuleGenerationStep) + .where( + ModuleGenerationStep.project_id == project_id, + ModuleGenerationStep.module == MODULE, + ModuleGenerationStep.step_code == step_code, + ModuleGenerationStep.is_current == True, + ModuleGenerationStep.deleted_at.is_(None), + ) + .order_by(ModuleGenerationStep.version.desc(), ModuleGenerationStep.created_at.desc()) + .limit(1) + ) + return result.scalar_one_or_none() + + +async def _next_version(db: AsyncSession, project_id: str, step_code: str) -> int: + result = await db.execute( + select(func.max(ModuleGenerationStep.version)).where( + ModuleGenerationStep.project_id == project_id, + ModuleGenerationStep.module == MODULE, + ModuleGenerationStep.step_code == step_code, + ) + ) + return int(result.scalar_one_or_none() or 0) + 1 + + +async def _create_step( + db: AsyncSession, + *, + project: ModuleGenerationProject, + step_code: str, + status: str = ModuleStepStatusEnum.PENDING.value, + parent_step_id: str | None = None, + source_step_id: str | None = None, + chat_task_id: str | None = None, + input_data: dict[str, Any] | None = None, + output_data: dict[str, Any] | None = None, +) -> ModuleGenerationStep: + version = await _next_version(db, project.id, step_code) + step = ModuleGenerationStep( + id=generate_id(), + project_id=project.id, + user_id=project.user_id, + module=project.module, + step_index=STEP_INDEX_MAP[step_code], + step_code=step_code, + status=status, + version=version, + is_current=True, + parent_step_id=parent_step_id, + source_step_id=source_step_id, + chat_task_id=chat_task_id, + input_json=_step_input( + step_code=step_code, + payload=input_data, + source_step_id=source_step_id, + parent_step_id=parent_step_id, + ) if input_data is not None else None, + output_json=_step_output( + step_code=step_code, + status=status, + result=output_data, + ) if output_data is not None else None, + started_at=_now() if status == ModuleStepStatusEnum.PROCESSING.value else None, + completed_at=_now() if status == ModuleStepStatusEnum.COMPLETED.value else None, + ) + db.add(step) + project.current_step_code = step_code + await db.flush() + await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.STEP_CREATED.value, detail={"step_code": step_code, "version": version}) + return step + + +async def _soft_delete_steps_from_index( + db: AsyncSession, + *, + project: ModuleGenerationProject, + start_index: int, + deleted_at: datetime | None = None, +) -> None: + deleted_at = deleted_at or _now() + result = await db.execute( + select(ModuleGenerationStep) + .where( + ModuleGenerationStep.project_id == project.id, + ModuleGenerationStep.module == MODULE, + ModuleGenerationStep.is_current == True, + ModuleGenerationStep.deleted_at.is_(None), + ModuleGenerationStep.step_index >= start_index, + ) + .with_for_update() + ) + steps = list(result.scalars().all()) + for step in steps: + step.is_current = False + step.deleted_at = deleted_at + if step.chat_task_id: + chat_result = await db.execute( + select(ChatGenerationTask) + .where(ChatGenerationTask.id == step.chat_task_id, ChatGenerationTask.deleted_at.is_(None)) + .with_for_update() + .limit(1) + ) + chat_task = chat_result.scalar_one_or_none() + if chat_task: + if chat_task.status == "completed": + await soft_delete_chat_task_resources(db, chat_task.id, deleted_at=deleted_at) + elif chat_task.status != "failed": + await mark_chat_generation_task_failed_and_refund_once( + db, + task=chat_task, + error_message="拆镜复刻步骤被重新生成或删除,旧生成任务已取消", + pipeline_stage="failed", + ) + chat_task.deleted_at = deleted_at + if steps: + await log_module_event( + db, + project=project, + event_type=ModuleEventTypeEnum.SOFT_DELETE_STEPS.value, + message=f"软删除第 {start_index} 步及之后的旧子任务", + detail={"step_ids": [step.id for step in steps]}, + ) + + +def _step_to_out(step: ModuleGenerationStep) -> ShotReplicateStepOut: + return ShotReplicateStepOut( + id=step.id, + project_id=step.project_id, + module=step.module, + step_index=step.step_index, + step_code=step.step_code, + status=step.status, + version=step.version, + is_current=step.is_current, + parent_step_id=step.parent_step_id, + source_step_id=step.source_step_id, + chat_task_id=step.chat_task_id, + input=_parse_json(step.input_json, {}), + output=_parse_json(step.output_json, {}), + error_message=step.error_message, + created_at=step.created_at, + updated_at=step.updated_at, + completed_at=step.completed_at, + ) + + +def _snapshot_from_chat(chat_task: ChatGenerationTask | None) -> dict[str, Any]: + if not chat_task: + return {} + return _parse_json(chat_task.engine_snapshot_json, {}) or {} + + +async def _chat_tasks_by_id(db: AsyncSession, steps: list[ModuleGenerationStep]) -> dict[str, ChatGenerationTask]: + ids = [step.chat_task_id for step in steps if step.chat_task_id] + if not ids: + return {} + result = await db.execute(select(ChatGenerationTask).where(ChatGenerationTask.id.in_(ids))) + return {task.id: task for task in result.scalars().all()} + + +async def project_to_detail_out(db: AsyncSession, project: ModuleGenerationProject) -> ShotReplicateTaskDetailOut: + steps = await _get_current_steps(db, project.id) + by_code = {step.step_code: step for step in steps} + chats = await _chat_tasks_by_id(db, steps) + + material_step = by_code.get(ShotReplicateStepCodeEnum.MATERIAL_INPUT.value) + image_prompt_step = by_code.get(ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value) + image_generate_step = by_code.get(ShotReplicateStepCodeEnum.IMAGE_GENERATE.value) + video_prompt_step = by_code.get(ShotReplicateStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value) + video_generate_step = by_code.get(ShotReplicateStepCodeEnum.VIDEO_GENERATE.value) + + material_input = _step_payload(material_step.input_json if material_step else None) + image_prompt_output = _unwrap_step_output(image_prompt_step.output_json if image_prompt_step else None) + image_generate_input = _step_payload(image_generate_step.input_json if image_generate_step else None) + image_generate_output = _unwrap_step_output(image_generate_step.output_json if image_generate_step else None) + video_prompt_input = _step_payload(video_prompt_step.input_json if video_prompt_step else None) + video_prompt_output = _unwrap_step_output(video_prompt_step.output_json if video_prompt_step else None) + video_generate_input = _step_payload(video_generate_step.input_json if video_generate_step else None) + video_generate_output = _unwrap_step_output(video_generate_step.output_json if video_generate_step else None) + + image_chat = chats.get(image_generate_step.chat_task_id) if image_generate_step and image_generate_step.chat_task_id else None + video_chat = chats.get(video_generate_step.chat_task_id) if video_generate_step and video_generate_step.chat_task_id else None + image_snapshot = _snapshot_from_chat(image_chat) + video_snapshot = _snapshot_from_chat(video_chat) + + image_url = image_generate_output.get("result_image_url") or (image_chat.image_url if image_chat else None) or project.final_image_url + video_url = video_generate_output.get("result_video_url") or (video_chat.video_url if video_chat else None) or project.final_video_url + cover_url = video_generate_output.get("result_video_cover_url") or (video_chat.video_cover_url if video_chat else None) or project.final_video_cover_url + + return ShotReplicateTaskDetailOut( + id=project.id, + project_id=project.id, + module=project.module, + title=project.title, + status=project.status, + current_step_code=project.current_step_code, + final_image_url=build_resource_signed_url(project.final_image_url) if project.final_image_url else None, + final_video_url=build_resource_signed_url(project.final_video_url) if project.final_video_url else None, + final_video_cover_url=build_resource_signed_url(project.final_video_cover_url) if project.final_video_cover_url else None, + error_message=project.error_message, + material=ShotReplicateMaterialOut( + material_step_id=material_step.id if material_step else None, + material_video_url=material_input.get("material_video_url"), + material_image_url=material_input.get("material_image_url"), + source_project_name=material_input.get("source_project_name"), + target_project_name=material_input.get("target_project_name"), + core_content_point=material_input.get("core_content_point"), + ), + image_generation=ShotReplicateImageGenerationOut( + prompt_step_id=image_prompt_step.id if image_prompt_step else None, + generate_step_id=image_generate_step.id if image_generate_step else None, + prompt=image_prompt_output.get("optimized_prompt") or image_prompt_output.get("prompt"), + engine_id=image_snapshot.get("id") or image_generate_input.get("engine_id"), + engine_name=image_snapshot.get("name") or image_generate_input.get("engine_name"), + params=image_generate_input.get("params") or image_generate_input, + chat_task_id=image_generate_step.chat_task_id if image_generate_step else None, + status=image_chat.status if image_chat else (image_generate_step.status if image_generate_step else None), + result_image_url=build_resource_signed_url(image_url) if image_url else None, + error_message=image_chat.error_message if image_chat else (image_generate_step.error_message if image_generate_step else None), + ), + video_generation=ShotReplicateVideoGenerationOut( + prompt_step_id=video_prompt_step.id if video_prompt_step else None, + generate_step_id=video_generate_step.id if video_generate_step else None, + prompt_schema=video_prompt_output.get("prompt_schema"), + final_prompt=video_prompt_output.get("final_prompt"), + prompt_params=video_prompt_output.get("params_used_for_prompt") or video_prompt_input.get("video_config"), + engine_id=video_snapshot.get("id") or video_generate_input.get("engine_id"), + engine_name=video_snapshot.get("name") or video_generate_input.get("engine_name"), + params=video_generate_input.get("params") or video_generate_input, + chat_task_id=video_generate_step.chat_task_id if video_generate_step else None, + status=video_chat.status if video_chat else (video_generate_step.status if video_generate_step else None), + result_video_url=build_resource_signed_url(video_url) if video_url else None, + result_video_cover_url=build_resource_signed_url(cover_url) if cover_url else None, + error_message=video_chat.error_message if video_chat else (video_generate_step.error_message if video_generate_step else None), + ), + steps=[_step_to_out(step) for step in steps], + created_at=project.created_at, + updated_at=project.updated_at, + completed_at=project.completed_at, + ) + + +async def create_shot_replicate_project(db: AsyncSession, current_user: User, req: ShotReplicateTaskCreate) -> ModuleGenerationProject: + if req.idempotency_key: + result = await db.execute( + select(ModuleGenerationProject) + .where( + ModuleGenerationProject.user_id == current_user.id, + ModuleGenerationProject.module == MODULE, + ModuleGenerationProject.idempotency_key == req.idempotency_key, + ModuleGenerationProject.deleted_at.is_(None), + ) + .order_by(ModuleGenerationProject.created_at.desc()) + .limit(1) + ) + existing = result.scalar_one_or_none() + if existing: + return existing + + project = ModuleGenerationProject( + id=generate_id(), + user_id=current_user.id, + module=MODULE, + title=req.target_project_name, + status=ModuleProjectStatusEnum.WAITING_USER.value, + current_step_code=ShotReplicateStepCodeEnum.MATERIAL_INPUT.value, + idempotency_key=req.idempotency_key, + ) + db.add(project) + await db.flush() + + await _create_step( + db, + project=project, + step_code=ShotReplicateStepCodeEnum.MATERIAL_INPUT.value, + status=ModuleStepStatusEnum.COMPLETED.value, + input_data={ + "material_video_url": req.material_video_url, + "material_image_url": req.material_image_url, + "source_project_name": req.source_project_name, + "target_project_name": req.target_project_name, + "core_content_point": req.core_content_point, + }, + output_data={"message": "素材输入已提交,后端不做素材文件校验。下一步请手动生成图片AI提词。"}, + ) + await log_module_event(db, project=project, event_type=ModuleEventTypeEnum.PROJECT_CREATED.value, message="创建拆镜复刻项目") + return project + + +async def list_shot_replicate_projects( + db: AsyncSession, + *, + current_user: User, + status: str | None, + page: int, + page_size: int, +) -> ShotReplicateTaskListOut: + query = select(ModuleGenerationProject).where( + ModuleGenerationProject.module == MODULE, + ModuleGenerationProject.deleted_at.is_(None), + ) + if not current_user.is_admin: + query = query.where(ModuleGenerationProject.user_id == current_user.id) + if status: + query = query.where(ModuleGenerationProject.status == status) + + total = (await db.execute(select(func.count()).select_from(query.subquery()))).scalar_one() + result = await db.execute(query.order_by(ModuleGenerationProject.created_at.desc()).offset((page - 1) * page_size).limit(page_size)) + projects = list(result.scalars().all()) + + items: list[ShotReplicateTaskListItemOut] = [] + for project in projects: + material_step = await _get_current_step_by_code(db, project.id, ShotReplicateStepCodeEnum.MATERIAL_INPUT.value) + material = _step_payload(material_step.input_json if material_step else None) + items.append( + ShotReplicateTaskListItemOut( + id=project.id, + project_id=project.id, + module=project.module, + title=project.title, + status=project.status, + current_step_code=project.current_step_code, + target_project_name=material.get("target_project_name"), + final_image_url=build_resource_signed_url(project.final_image_url) if project.final_image_url else None, + final_video_url=build_resource_signed_url(project.final_video_url) if project.final_video_url else None, + error_message=project.error_message, + created_at=project.created_at, + updated_at=project.updated_at, + completed_at=project.completed_at, + ) + ) + return ShotReplicateTaskListOut(total=total, items=items) + + +async def update_shot_replicate_step( + db: AsyncSession, + *, + current_user: User, + project_id: str, + step_id: str, + req: ShotReplicateStepUpdate, +) -> tuple[ModuleGenerationProject, ModuleGenerationStep]: + project = await _get_project_for_user(db, project_id=project_id, user=current_user, for_update=True) + step = await _get_step_for_user(db, project_id=project_id, step_id=step_id, user=current_user, for_update=True) + if step.status == ModuleStepStatusEnum.PROCESSING.value: + raise HTTPException(status_code=400, detail="当前子任务正在处理中,暂不能修改") + + input_data = _step_payload(step.input_json) + output_data = _unwrap_step_output(step.output_json) + + if step.step_code == ShotReplicateStepCodeEnum.MATERIAL_INPUT.value: + input_data = _merge_dict( + input_data, + { + "material_video_url": req.material_video_url, + "material_image_url": req.material_image_url, + "source_project_name": req.source_project_name, + "target_project_name": req.target_project_name, + "core_content_point": req.core_content_point, + }, + ) + if req.target_project_name: + project.title = req.target_project_name + elif step.step_code == ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value: + if req.prompt is not None: + output_data["optimized_prompt"] = req.prompt + output_data["prompt"] = req.prompt + elif step.step_code == ShotReplicateStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value: + if req.prompt_schema is not None: + output_data["prompt_schema"] = req.prompt_schema + if req.prompt is not None: + output_data["final_prompt"] = req.prompt + else: + if req.input_json: + input_data = _merge_dict(input_data, req.input_json) + if req.output_json: + output_data = _merge_dict(output_data, req.output_json) + + if req.input_json: + input_data = _merge_dict(input_data, req.input_json) + if req.output_json: + output_data = _merge_dict(output_data, req.output_json) + + step.input_json = _step_input(step_code=step.step_code, payload=input_data, source_step_id=step.source_step_id, parent_step_id=step.parent_step_id) + step.output_json = _step_output(step_code=step.step_code, status=ModuleStepStatusEnum.COMPLETED.value, payload=output_data) + step.status = ModuleStepStatusEnum.COMPLETED.value + step.error_message = None + step.completed_at = _now() + project.status = ModuleProjectStatusEnum.WAITING_USER.value + project.current_step_code = step.step_code + project.error_message = None + + await _soft_delete_steps_from_index(db, project=project, start_index=step.step_index + 1) + await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.STEP_UPDATED.value, message="用户修改子任务内容") + return project, step + + +async def update_shot_replicate_material_input( + db: AsyncSession, + *, + current_user: User, + project_id: str, + req: ShotReplicateMaterialUpdateRequest, +) -> tuple[str, str]: + """修改第1步素材输入。 + + 采用方案 B:软删除旧第1步及之后的当前有效步骤,然后新建第1步 version+1。 + 未传字段沿用旧第1步素材输入,避免前端只改一个字段时丢失其它素材信息。 + """ + project = await _get_project_for_user(db, project_id=project_id, user=current_user, for_update=True) + old_material_step = await _get_current_step_by_code(db, project.id, ShotReplicateStepCodeEnum.MATERIAL_INPUT.value) + old_material = _step_payload(old_material_step.input_json if old_material_step else None) + + material = { + "material_video_url": req.material_video_url if req.material_video_url is not None else old_material.get("material_video_url"), + "material_image_url": req.material_image_url if req.material_image_url is not None else old_material.get("material_image_url"), + "source_project_name": req.source_project_name if req.source_project_name is not None else old_material.get("source_project_name"), + "target_project_name": req.target_project_name if req.target_project_name is not None else old_material.get("target_project_name"), + "core_content_point": req.core_content_point if req.core_content_point is not None else old_material.get("core_content_point"), + } + + missing_fields = [key for key, value in material.items() if value is None or str(value).strip() == ""] + if missing_fields: + raise HTTPException(status_code=400, detail=f"素材输入缺少必要字段: {', '.join(missing_fields)}") + + await _soft_delete_steps_from_index(db, project=project, start_index=STEP_INDEX_MAP[ShotReplicateStepCodeEnum.MATERIAL_INPUT.value]) + + project.title = str(material["target_project_name"]) + project.status = ModuleProjectStatusEnum.WAITING_USER.value + project.current_step_code = ShotReplicateStepCodeEnum.MATERIAL_INPUT.value + project.final_image_url = None + project.final_video_url = None + project.final_video_cover_url = None + project.error_message = None + project.completed_at = None + + new_step = await _create_step( + db, + project=project, + step_code=ShotReplicateStepCodeEnum.MATERIAL_INPUT.value, + status=ModuleStepStatusEnum.COMPLETED.value, + input_data=material, + output_data={"message": "素材输入已修改,旧步骤已软删除。下一步请重新生成图片AI提词。"}, + ) + await log_module_event( + db, + project=project, + step=new_step, + event_type=ModuleEventTypeEnum.STEP_UPDATED.value, + message="用户修改素材输入并重建第1步新版本", + detail={ + "old_material_step_id": old_material_step.id if old_material_step else None, + "new_material_step_id": new_step.id, + "version": new_step.version, + }, + ) + return project.id, new_step.id + + +async def update_shot_replicate_image_prompt( + db: AsyncSession, + *, + current_user: User, + project_id: str, + step_id: str, + req: ShotReplicateImagePromptUpdateRequest, +) -> tuple[ModuleGenerationProject, ModuleGenerationStep]: + """直接修改第2步图片 AI 优化提词,不调用 AI、不扣积分。 + + 修改后软删除第3、4、5步当前有效任务,让用户从图片生成开始重新执行。 + """ + project = await _get_project_for_user(db, project_id=project_id, user=current_user, for_update=True) + step = await _get_step_for_user(db, project_id=project_id, step_id=step_id, user=current_user, for_update=True) + if step.step_code != ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value: + raise HTTPException(status_code=400, detail="只能修改第2步图片 AI 提词子任务") + if step.status != ModuleStepStatusEnum.COMPLETED.value: + raise HTTPException(status_code=400, detail="图片 AI 提词未完成,不能直接修改") + + output_data = _step_payload(step.output_json) + usage = _step_usage(step.output_json) + new_prompt = req.prompt.strip() + output_data["optimized_prompt"] = new_prompt + output_data["prompt"] = new_prompt + output_data["manual_edited"] = True + output_data["manual_edited_at"] = _now().isoformat() + + step.output_json = _step_output( + step_code=ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value, + status=ModuleStepStatusEnum.COMPLETED.value, + payload=output_data, + usage=usage, + ) + step.status = ModuleStepStatusEnum.COMPLETED.value + step.error_message = None + step.completed_at = _now() + + await _soft_delete_steps_from_index(db, project=project, start_index=STEP_INDEX_MAP[ShotReplicateStepCodeEnum.IMAGE_GENERATE.value]) + + project.status = ModuleProjectStatusEnum.WAITING_USER.value + project.current_step_code = ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value + project.final_image_url = None + project.final_video_url = None + project.final_video_cover_url = None + project.completed_at = None + project.error_message = None + + await log_module_event( + db, + project=project, + step=step, + event_type=ModuleEventTypeEnum.STEP_UPDATED.value, + message="用户直接修改图片 AI 优化提词,已软删除后续步骤", + detail={"start_deleted_step_index": STEP_INDEX_MAP[ShotReplicateStepCodeEnum.IMAGE_GENERATE.value]}, + ) + return project, step + + +async def update_shot_replicate_video_prompt_schema( + db: AsyncSession, + *, + current_user: User, + project_id: str, + step_id: str, + req: ShotReplicateVideoPromptSchemaUpdateRequest, +) -> tuple[ModuleGenerationProject, ModuleGenerationStep]: + """以前端 schema 为 patch 修改第4步视频 AI 提词,不调用 AI、不扣积分。 + + 服务端已有 schema 为基准:视频规格、数组长度、时间段、合规控制、质量控制、协议字段均锁定。 + 最终提示词允许修改,但保存前会清洗视频时长、比例、分辨率、帧率等参数。 + """ + project = await _get_project_for_user(db, project_id=project_id, user=current_user, for_update=True) + step = await _get_step_for_user(db, project_id=project_id, step_id=step_id, user=current_user, for_update=True) + if step.step_code != ShotReplicateStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value: + raise HTTPException(status_code=400, detail="只能修改第4步视频 AI 提词 JSON schema 子任务") + if step.status != ModuleStepStatusEnum.COMPLETED.value: + raise HTTPException(status_code=400, detail="视频 AI 提词未完成,不能直接修改") + + output_data = _step_payload(step.output_json) + usage = _step_usage(step.output_json) + input_data = _step_payload(step.input_json) + server_schema = output_data.get("prompt_schema") if isinstance(output_data.get("prompt_schema"), dict) else {} + video_config = output_data.get("params_used_for_prompt") or input_data.get("video_config") or {} + if not isinstance(video_config, dict) or not video_config.get("duration") or not video_config.get("aspect_ratio") or not video_config.get("resolution"): + raise HTTPException(status_code=400, detail="缺少第4步视频参数快照,不能安全修改视频 schema") + + patched_schema = patch_video_prompt_schema_from_client( + server_schema=server_schema, + client_schema=req.prompt_schema, + video_config=video_config, + ) + final_prompt = build_final_video_prompt(patched_schema) + + output_data["prompt_schema"] = patched_schema + output_data["final_prompt"] = final_prompt + output_data["params_used_for_prompt"] = video_config + output_data["manual_edited"] = True + output_data["manual_edited_at"] = _now().isoformat() + + step.output_json = _step_output( + step_code=ShotReplicateStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value, + status=ModuleStepStatusEnum.COMPLETED.value, + payload=output_data, + usage=usage, + ) + step.status = ModuleStepStatusEnum.COMPLETED.value + step.error_message = None + step.completed_at = _now() + + await _soft_delete_steps_from_index(db, project=project, start_index=STEP_INDEX_MAP[ShotReplicateStepCodeEnum.VIDEO_GENERATE.value]) + + project.status = ModuleProjectStatusEnum.WAITING_USER.value + project.current_step_code = ShotReplicateStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value + project.final_video_url = None + project.final_video_cover_url = None + project.completed_at = None + project.error_message = None + + await log_module_event( + db, + project=project, + step=step, + event_type=ModuleEventTypeEnum.STEP_UPDATED.value, + message="用户修改视频 AI 提词 schema,已软删除视频生成步骤", + detail={ + "start_deleted_step_index": STEP_INDEX_MAP[ShotReplicateStepCodeEnum.VIDEO_GENERATE.value], + "locked_fields": [ + "schema_version", + "schema_usage", + "画面属性.视频时长", + "画面属性.视频比例", + "画面属性.清晰度", + "画面属性.帧率", + "画面属性.推荐分辨率", + "动作流程[*].时间段", + "镜头流程[*].时间段", + "动态时间规划", + "输出规格限制", + "质量控制", + "合规控制", + ], + }, + ) + return project, step + + +async def submit_image_prompt_optimize( + db: AsyncSession, + *, + current_user: User, + project_id: str, + material_step_id: str | None = None, + req: Any | None = None, +) -> tuple[ModuleGenerationProject, ModuleGenerationStep]: + _ = req + project = await _get_project_for_user(db, project_id=project_id, user=current_user, for_update=True) + if material_step_id: + material_step = await _get_step_for_user(db, project_id=project_id, step_id=material_step_id, user=current_user, for_update=True) + else: + material_step = await _get_current_step_by_code(db, project_id, ShotReplicateStepCodeEnum.MATERIAL_INPUT.value) + if not material_step: + raise HTTPException(status_code=400, detail="缺少第1步素材输入子任务") + if material_step.step_code != ShotReplicateStepCodeEnum.MATERIAL_INPUT.value: + raise HTTPException(status_code=400, detail="请基于第1步素材输入子任务生成图片 AI 提词") + if material_step.status != ModuleStepStatusEnum.COMPLETED.value: + raise HTTPException(status_code=400, detail="素材输入子任务未完成,不能生成图片 AI 提词") + + await _soft_delete_steps_from_index(db, project=project, start_index=STEP_INDEX_MAP[ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value]) + step = await _create_step( + db, + project=project, + step_code=ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value, + status=ModuleStepStatusEnum.PROCESSING.value, + parent_step_id=material_step.id, + source_step_id=material_step.id, + input_data={"source_step_id": material_step.id}, + ) + project.status = ModuleProjectStatusEnum.PROCESSING.value + project.current_step_code = ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value + project.error_message = None + await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.IMAGE_PROMPT_SUBMITTED.value, message="图片 AI 提词任务已提交") + return project, step + + +async def run_image_prompt_optimize(db: AsyncSession, *, project_id: str, step_id: str | None = None) -> ModuleGenerationStep | None: + project_result = await db.execute( + select(ModuleGenerationProject) + .where(ModuleGenerationProject.id == project_id, ModuleGenerationProject.module == MODULE, ModuleGenerationProject.deleted_at.is_(None)) + .with_for_update() + .limit(1) + ) + project = project_result.scalar_one_or_none() + if not project: + return None + + material_step = await _get_current_step_by_code(db, project.id, ShotReplicateStepCodeEnum.MATERIAL_INPUT.value) + if not material_step: + project.status = ModuleProjectStatusEnum.FAILED.value + project.error_message = "缺少素材输入子任务" + return None + + if step_id: + result = await db.execute( + select(ModuleGenerationStep) + .where( + ModuleGenerationStep.id == step_id, + ModuleGenerationStep.project_id == project.id, + ModuleGenerationStep.step_code == ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value, + ModuleGenerationStep.deleted_at.is_(None), + ModuleGenerationStep.is_current == True, + ) + .with_for_update() + .limit(1) + ) + step = result.scalar_one_or_none() + else: + step = await _get_current_step_by_code(db, project.id, ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value) + if not step: + if step_id: + # 用户重复提交后,旧 Celery 消息对应的 step 可能已被软删。 + # 指定 step_id 查不到时必须静默忽略,不能重新创建步骤导致旧任务复活。 + return None + step = await _create_step( + db, + project=project, + step_code=ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value, + status=ModuleStepStatusEnum.PROCESSING.value, + parent_step_id=material_step.id, + source_step_id=material_step.id, + input_data={"source_step_id": material_step.id}, + ) + else: + step.status = ModuleStepStatusEnum.PROCESSING.value + step.started_at = _now() + step.error_message = None + + material = _step_payload(material_step.input_json) + prompt_text = ( + "请基于参考素材复刻拆镜视觉风格,用于生成新项目图片。\n" + f"视频素材内容项目名称:{material.get('source_project_name')}\n" + f"生成项目名称:{material.get('target_project_name')}\n" + f"生成项目核心内容点:{material.get('core_content_point')}\n" + "要求:参考素材视频的开头构图、主体位置、节奏和风格;结合新产品图片生成新项目推广图片;不要照抄原素材品牌、文字、水印;适合作为后续图生视频首帧。" + ) + references = [ + {"type": "video", "url": material.get("material_video_url"), "name": "参考素材视频"}, + {"type": "image", "url": material.get("material_image_url"), "name": "新产品图片"}, + ] + try: + request_log = {"original_prompt": prompt_text, "references": references, "gen_type": "image"} + log_module_prompt_event( + event_type="module_prompt_request", + project_id=project.id, + step_id=step.id, + user_id=project.user_id, + module=project.module, + prompt_type=ModulePromptTypeEnum.IMAGE_PROMPT.value, + request=request_log, + ) + optimized, token_usage = await optimize_prompt( + db, + original_prompt=prompt_text, + user_id=project.user_id, + references=references, + gen_type="image", + ) + billing = await charge_module_prompt_usage( + db, + user_id=project.user_id, + step_id=step.id, + usage=token_usage, + description="拆镜复刻-图片AI提词优化", + ) + usage = dict(token_usage or {}) + usage.update({ + "text_credits_cost": (billing.items[0].amount if billing.items else billing.total_charged), + "credit_biz_key": billing.items[0].biz_key if billing.items else None, + }) + step.status = ModuleStepStatusEnum.COMPLETED.value + step.completed_at = _now() + step.output_json = _step_output( + step_code=ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value, + status=ModuleStepStatusEnum.COMPLETED.value, + payload={ + "optimized_prompt": optimized, + "prompt": optimized, + "original_prompt": prompt_text, + "references": references, + }, + usage=usage, + ) + project.status = ModuleProjectStatusEnum.WAITING_USER.value + project.current_step_code = ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value + project.error_message = None + log_module_prompt_event( + event_type="module_prompt_response", + project_id=project.id, + step_id=step.id, + user_id=project.user_id, + module=project.module, + prompt_type=ModulePromptTypeEnum.IMAGE_PROMPT.value, + request=request_log, + response={"optimized_prompt": optimized}, + token_usage=usage, + ) + await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.IMAGE_PROMPT_SUCCESS.value, message="图片 AI 提词生成成功") + except Exception as exc: + step.status = ModuleStepStatusEnum.FAILED.value + step.error_message = str(exc) + step.completed_at = _now() + project.status = ModuleProjectStatusEnum.FAILED.value + project.error_message = f"图片 AI 提词生成失败: {exc}" + log_module_prompt_event( + event_type="module_prompt_error", + project_id=project.id, + step_id=step.id, + user_id=project.user_id, + module=project.module, + prompt_type=ModulePromptTypeEnum.IMAGE_PROMPT.value, + request=locals().get("request_log", {}), + error=str(exc), + ) + _log_project_error(project=project, step=step, event_type="IMAGE_PROMPT_FAILED", message=project.error_message, exc=exc) + await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.IMAGE_PROMPT_FAILED.value, message=project.error_message) + return step + + +async def generate_image_from_prompt( + db: AsyncSession, + *, + current_user: User, + project_id: str, + req: ShotReplicateGenerateImageRequest, + prompt_step_id: str | None = None, +) -> tuple[ModuleGenerationProject, ModuleGenerationStep, ChatGenerationTask]: + project = await _get_project_for_user(db, project_id=project_id, user=current_user, for_update=True) + if prompt_step_id: + prompt_step = await _get_step_for_user(db, project_id=project_id, step_id=prompt_step_id, user=current_user, for_update=True) + else: + prompt_step = await _get_current_step_by_code(db, project_id, ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value) + if not prompt_step: + raise HTTPException(status_code=400, detail="缺少第2步图片 AI 提词子任务") + if prompt_step.step_code != ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value: + raise HTTPException(status_code=400, detail="请基于第2步图片 AI 提词子任务生成图片") + if prompt_step.status != ModuleStepStatusEnum.COMPLETED.value: + raise HTTPException(status_code=400, detail="图片 AI 提词未完成,不能生成图片") + + await _soft_delete_steps_from_index(db, project=project, start_index=STEP_INDEX_MAP[ShotReplicateStepCodeEnum.IMAGE_GENERATE.value]) + + material_step = await _get_current_step_by_code(db, project.id, ShotReplicateStepCodeEnum.MATERIAL_INPUT.value) + material = _step_payload(material_step.input_json if material_step else None) + prompt_output = _unwrap_step_output(prompt_step.output_json) + optimized_prompt = prompt_output.get("optimized_prompt") or prompt_output.get("prompt") or "" + refs = [ + {"type": "image", "url": material.get("material_image_url"), "name": "新产品图片"}, + ] + + chat_task = await create_chat_generation_task_for_module( + db, + current_user=current_user, + generation_mode=GENERATION_MODE, + gen_type="image", + original_prompt=prompt_output.get("original_prompt") or optimized_prompt, + optimized_prompt=optimized_prompt, + engine_id=req.engine_id, + media_references=refs, + image_size=req.image_size, + image_proportion=req.image_proportion, + image_px=req.image_px, + billing_project_name=project.title or "拆镜复刻", + billing_description_prefix="拆镜复刻图片生成", + ) + step = await _create_step( + db, + project=project, + step_code=ShotReplicateStepCodeEnum.IMAGE_GENERATE.value, + status=ModuleStepStatusEnum.PROCESSING.value, + parent_step_id=prompt_step.id, + source_step_id=prompt_step.id, + chat_task_id=chat_task.id, + input_data={ + "engine_id": chat_task.engine_id, + "params": {"image_size": chat_task.image_size, "image_proportion": chat_task.image_proportion, "image_px": chat_task.image_px}, + "prompt": optimized_prompt, + "media_references": refs, + }, + ) + project.status = ModuleProjectStatusEnum.PROCESSING.value + project.current_step_code = ShotReplicateStepCodeEnum.IMAGE_GENERATE.value + project.error_message = None + await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.IMAGE_GENERATE_SUBMITTED.value, message="图片生成任务已提交", detail={"chat_task_id": chat_task.id}) + return project, step, chat_task + + +async def _resolve_video_prompt_config(db: AsyncSession, req: ShotReplicateGenerateVideoPromptRequest) -> dict[str, Any]: + engine = await _get_video_engine(db, req.engine_id) + supported_ratios = _parse_list(engine.supported_ratios, []) + supported_resolutions = _parse_list(engine.supported_resolutions, []) + supported_durations = _parse_list(engine.supported_durations, []) + + default_ratio = getattr(settings, "SHOT_REPLICATE_DEFAULT_VIDEO_RATIO", None) or VIDEO_DEFAULT_RATIO + default_resolution = getattr(settings, "SHOT_REPLICATE_DEFAULT_VIDEO_RESOLUTION", None) or VIDEO_DEFAULT_RESOLUTION + default_duration = int(getattr(settings, "SHOT_REPLICATE_DEFAULT_VIDEO_DURATION", None) or VIDEO_DEFAULT_DURATION) + + selected_ratio = req.aspect_ratio or (default_ratio if not supported_ratios or default_ratio in supported_ratios else supported_ratios[0]) + selected_resolution = req.resolution or (default_resolution if not supported_resolutions or default_resolution in supported_resolutions else supported_resolutions[0]) + selected_duration = req.duration or (default_duration if not supported_durations or default_duration in supported_durations else supported_durations[0]) + + if supported_ratios and selected_ratio not in supported_ratios: + raise HTTPException(status_code=400, detail=f"视频比例不支持: {selected_ratio}") + if supported_resolutions and selected_resolution not in supported_resolutions: + raise HTTPException(status_code=400, detail=f"视频分辨率不支持: {selected_resolution}") + if supported_durations and selected_duration not in supported_durations: + raise HTTPException(status_code=400, detail=f"视频时长不支持: {selected_duration}") + if engine.max_duration and int(selected_duration) > int(engine.max_duration): + raise HTTPException(status_code=400, detail=f"视频时长不能超过 {engine.max_duration} 秒") + + return { + "engine_id": engine.id, + "engine_name": engine.name, + "duration": int(selected_duration), + "aspect_ratio": selected_ratio, + "resolution": selected_resolution, + "supported_ratios": supported_ratios, + "supported_resolutions": supported_resolutions, + "supported_durations": supported_durations, + "max_duration": engine.max_duration, + "frame_rate": "30fps", + "reference_video_fps": max(1, int(settings.CHATAPI_VIDEO_FPS or 1)), + } + + +async def submit_video_prompt_optimize( + db: AsyncSession, + *, + current_user: User, + project_id: str, + req: ShotReplicateGenerateVideoPromptRequest, + image_step_id: str | None = None, +) -> tuple[ModuleGenerationProject, ModuleGenerationStep]: + project = await _get_project_for_user(db, project_id=project_id, user=current_user, for_update=True) + if image_step_id: + image_step = await _get_step_for_user(db, project_id=project_id, step_id=image_step_id, user=current_user, for_update=True) + else: + image_step = await _get_current_step_by_code(db, project_id, ShotReplicateStepCodeEnum.IMAGE_GENERATE.value) + if not image_step: + raise HTTPException(status_code=400, detail="缺少第3步图片生成子任务") + if image_step.step_code != ShotReplicateStepCodeEnum.IMAGE_GENERATE.value: + raise HTTPException(status_code=400, detail="请基于第3步图片生成子任务生成视频 AI 提词") + if image_step.status != ModuleStepStatusEnum.COMPLETED.value: + raise HTTPException(status_code=400, detail="图片生成子任务未完成,不能生成视频 AI 提词") + + await _soft_delete_steps_from_index(db, project=project, start_index=STEP_INDEX_MAP[ShotReplicateStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value]) + video_config = await _resolve_video_prompt_config(db, req) + step = await _create_step( + db, + project=project, + step_code=ShotReplicateStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value, + status=ModuleStepStatusEnum.PROCESSING.value, + parent_step_id=image_step.id, + source_step_id=image_step.id, + input_data={ + "source_step_id": image_step.id, + "video_config": video_config, + "target_platform": req.target_platform or getattr(settings, "SHOT_REPLICATE_DEFAULT_TARGET_PLATFORM", "抖音") or "抖音", + }, + ) + project.status = ModuleProjectStatusEnum.PROCESSING.value + project.current_step_code = ShotReplicateStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value + project.error_message = None + await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.VIDEO_PROMPT_SUBMITTED.value, message="视频 AI 提词任务已提交") + return project, step + + +async def run_video_prompt_optimize(db: AsyncSession, *, project_id: str, step_id: str | None = None) -> ModuleGenerationStep | None: + project_result = await db.execute( + select(ModuleGenerationProject) + .where(ModuleGenerationProject.id == project_id, ModuleGenerationProject.module == MODULE, ModuleGenerationProject.deleted_at.is_(None)) + .with_for_update() + .limit(1) + ) + project = project_result.scalar_one_or_none() + if not project: + return None + + material_step = await _get_current_step_by_code(db, project.id, ShotReplicateStepCodeEnum.MATERIAL_INPUT.value) + image_prompt_step = await _get_current_step_by_code(db, project.id, ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value) + image_step = await _get_current_step_by_code(db, project.id, ShotReplicateStepCodeEnum.IMAGE_GENERATE.value) + if not material_step or not image_step: + project.status = ModuleProjectStatusEnum.FAILED.value + project.error_message = "生成视频提词失败:缺少素材输入或图片生成结果" + return None + + if step_id: + result = await db.execute( + select(ModuleGenerationStep) + .where( + ModuleGenerationStep.id == step_id, + ModuleGenerationStep.project_id == project.id, + ModuleGenerationStep.step_code == ShotReplicateStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value, + ModuleGenerationStep.deleted_at.is_(None), + ModuleGenerationStep.is_current == True, + ) + .with_for_update() + .limit(1) + ) + step = result.scalar_one_or_none() + else: + step = await _get_current_step_by_code(db, project.id, ShotReplicateStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value) + if not step: + if step_id: + # 用户重复提交后,旧 Celery 消息对应的 step 可能已被软删。 + # 指定 step_id 查不到时必须静默忽略,不能把当前项目标记失败。 + return None + project.status = ModuleProjectStatusEnum.FAILED.value + project.error_message = "缺少视频 AI 提词子任务,请先手动提交视频提词生成" + return None + + step.status = ModuleStepStatusEnum.PROCESSING.value + step.started_at = _now() + step.error_message = None + + material = _step_payload(material_step.input_json) + image_output = _unwrap_step_output(image_step.output_json) + step_input = _step_payload(step.input_json) + video_config = step_input.get("video_config") or {} + target_platform = step_input.get("target_platform") or getattr(settings, "SHOT_REPLICATE_DEFAULT_TARGET_PLATFORM", "抖音") or "抖音" + generated_image_url = image_output.get("result_image_url") or project.final_image_url + if not generated_image_url: + step.status = ModuleStepStatusEnum.FAILED.value + step.error_message = "缺少新项目图片结果,不能生成视频提词" + project.status = ModuleProjectStatusEnum.FAILED.value + project.error_message = step.error_message + return step + + try: + request_log = { + "source_project_name": material.get("source_project_name") or "无", + "target_project_name": material.get("target_project_name") or "无", + "core_content_point": material.get("core_content_point") or "无", + "material_video_url": material.get("material_video_url") or "", + "generated_image_url": generated_image_url, + "video_config": video_config, + "target_platform": target_platform, + } + log_module_prompt_event( + event_type="module_prompt_request", + project_id=project.id, + step_id=step.id, + user_id=project.user_id, + module=project.module, + prompt_type=ModulePromptTypeEnum.VIDEO_PROMPT.value, + request=request_log, + ) + prompt_schema, final_prompt, token_usage = await optimize_shot_replicate_video_prompt( + db, + user_id=project.user_id, + source_project_name=request_log["source_project_name"], + target_project_name=request_log["target_project_name"], + core_content_point=request_log["core_content_point"], + material_video_url=request_log["material_video_url"], + generated_image_url=generated_image_url, + video_config=video_config, + target_platform=target_platform, + ) + billing = await charge_module_prompt_usage( + db, + user_id=project.user_id, + step_id=step.id, + usage=token_usage, + description="拆镜复刻-视频AI提词优化", + ) + usage = dict(token_usage or {}) + usage.update({ + "text_credits_cost": (billing.items[0].amount if billing.items else billing.total_charged), + "credit_biz_key": billing.items[0].biz_key if billing.items else None, + }) + step.status = ModuleStepStatusEnum.COMPLETED.value + step.completed_at = _now() + step.output_json = _step_output( + step_code=ShotReplicateStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value, + status=ModuleStepStatusEnum.COMPLETED.value, + payload={ + "prompt_schema": prompt_schema, + "final_prompt": final_prompt, + "params_used_for_prompt": video_config, + "target_platform": target_platform, + }, + usage=usage, + ) + project.status = ModuleProjectStatusEnum.WAITING_USER.value + project.current_step_code = ShotReplicateStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value + project.error_message = None + log_module_prompt_event( + event_type="module_prompt_response", + project_id=project.id, + step_id=step.id, + user_id=project.user_id, + module=project.module, + prompt_type=ModulePromptTypeEnum.VIDEO_PROMPT.value, + request=request_log, + response={"prompt_schema": prompt_schema, "final_prompt": final_prompt}, + token_usage=usage, + ) + await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.VIDEO_PROMPT_SUCCESS.value, message="视频 AI 提词生成成功") + except Exception as exc: + step.status = ModuleStepStatusEnum.FAILED.value + step.error_message = str(exc) + step.completed_at = _now() + project.status = ModuleProjectStatusEnum.FAILED.value + project.error_message = f"视频 AI 提词生成失败: {exc}" + log_module_prompt_event( + event_type="module_prompt_error", + project_id=project.id, + step_id=step.id, + user_id=project.user_id, + module=project.module, + prompt_type=ModulePromptTypeEnum.VIDEO_PROMPT.value, + request=locals().get("request_log", {}), + error=str(exc), + ) + _log_project_error(project=project, step=step, event_type="VIDEO_PROMPT_FAILED", message=project.error_message, exc=exc) + await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.VIDEO_PROMPT_FAILED.value, message=project.error_message) + return step + + +async def generate_video_from_prompt( + db: AsyncSession, + *, + current_user: User, + project_id: str, + req: ShotReplicateGenerateVideoRequest, + prompt_step_id: str | None = None, +) -> tuple[ModuleGenerationProject, ModuleGenerationStep, ChatGenerationTask]: + project = await _get_project_for_user(db, project_id=project_id, user=current_user, for_update=True) + if prompt_step_id: + prompt_step = await _get_step_for_user(db, project_id=project_id, step_id=prompt_step_id, user=current_user, for_update=True) + else: + prompt_step = await _get_current_step_by_code(db, project_id, ShotReplicateStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value) + if not prompt_step: + raise HTTPException(status_code=400, detail="缺少第4步视频 AI 提词子任务") + if prompt_step.step_code != ShotReplicateStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value: + raise HTTPException(status_code=400, detail="请基于第4步视频 AI 提词子任务生成视频") + if prompt_step.status != ModuleStepStatusEnum.COMPLETED.value: + raise HTTPException(status_code=400, detail="视频 AI 提词未完成,不能生成视频") + + await _soft_delete_steps_from_index(db, project=project, start_index=STEP_INDEX_MAP[ShotReplicateStepCodeEnum.VIDEO_GENERATE.value]) + + image_step = await _get_current_step_by_code(db, project.id, ShotReplicateStepCodeEnum.IMAGE_GENERATE.value) + image_output = _unwrap_step_output(image_step.output_json if image_step else None) + prompt_output = _unwrap_step_output(prompt_step.output_json) + final_prompt = prompt_output.get("final_prompt") or "" + prompt_schema = prompt_output.get("prompt_schema") or {} + prompt_schema_str = json.dumps(prompt_schema, ensure_ascii=False, default=str) if prompt_schema else "" + prompt_input = _step_payload(prompt_step.input_json) + prompt_params = prompt_output.get("params_used_for_prompt") or prompt_input.get("video_config") or {} + duration = int(prompt_params.get("duration") or settings.SHOT_REPLICATE_DEFAULT_VIDEO_DURATION or 4) + aspect_ratio = prompt_params.get("aspect_ratio") or settings.SHOT_REPLICATE_DEFAULT_VIDEO_RATIO or "9:16" + resolution = prompt_params.get("resolution") or settings.SHOT_REPLICATE_DEFAULT_VIDEO_RESOLUTION or "480p" + generated_image_url = image_output.get("result_image_url") or project.final_image_url + if not generated_image_url: + raise HTTPException(status_code=400, detail="缺少新项目图片结果,不能生成视频") + + refs = [ + {"type": "image", "url": _build_file_url_or_data_uri(generated_image_url), "name": "新项目图片"}, + ] + + chat_task = await create_chat_generation_task_for_module( + db, + current_user=current_user, + generation_mode=GENERATION_MODE, + gen_type="video", + original_prompt=prompt_schema_str or final_prompt, + optimized_prompt=prompt_schema_str or final_prompt, + engine_id=req.engine_id or prompt_params.get("engine_id"), + media_references=refs, + duration=duration, + aspect_ratio=aspect_ratio, + resolution=resolution, + billing_project_name=project.title or "拆镜复刻", + billing_description_prefix="拆镜复刻视频生成", + ) + step = await _create_step( + db, + project=project, + step_code=ShotReplicateStepCodeEnum.VIDEO_GENERATE.value, + status=ModuleStepStatusEnum.PROCESSING.value, + parent_step_id=prompt_step.id, + source_step_id=prompt_step.id, + chat_task_id=chat_task.id, + input_data={ + "engine_id": chat_task.engine_id, + "params": { + "duration": chat_task.duration, + "aspect_ratio": chat_task.aspect_ratio, + "resolution": chat_task.resolution, + "image_size": chat_task.image_size, + "image_proportion": chat_task.image_proportion, + "image_px": chat_task.image_px, + }, + "prompt_schema": prompt_schema, + "final_prompt": final_prompt, + "media_references": refs, + }, + ) + project.status = ModuleProjectStatusEnum.PROCESSING.value + project.current_step_code = ShotReplicateStepCodeEnum.VIDEO_GENERATE.value + project.error_message = None + await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.VIDEO_GENERATE_SUBMITTED.value, message="视频生成任务已提交", detail={"chat_task_id": chat_task.id}) + return project, step, chat_task + + +async def handle_chat_generation_task_completed(db: AsyncSession, task: ChatGenerationTask) -> None: + if not task or task.generation_mode != GENERATION_MODE: + return + result = await db.execute( + select(ModuleGenerationStep) + .where( + ModuleGenerationStep.chat_task_id == task.id, + ModuleGenerationStep.module == MODULE, + ModuleGenerationStep.is_current == True, + ModuleGenerationStep.deleted_at.is_(None), + ) + .with_for_update() + .limit(1) + ) + step = result.scalar_one_or_none() + if not step: + return + project_result = await db.execute( + select(ModuleGenerationProject) + .where(ModuleGenerationProject.id == step.project_id, ModuleGenerationProject.deleted_at.is_(None)) + .with_for_update() + .limit(1) + ) + project = project_result.scalar_one_or_none() + if not project: + return + + if step.step_code == ShotReplicateStepCodeEnum.IMAGE_GENERATE.value: + step.status = ModuleStepStatusEnum.COMPLETED.value + step.completed_at = _now() + step.output_json = _step_output( + step_code=ShotReplicateStepCodeEnum.IMAGE_GENERATE.value, + status=ModuleStepStatusEnum.COMPLETED.value, + result={"result_image_url": task.image_url, "chat_task_id": task.id}, + ) + project.final_image_url = task.image_url + project.status = ModuleProjectStatusEnum.WAITING_USER.value + project.current_step_code = ShotReplicateStepCodeEnum.IMAGE_GENERATE.value + await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.IMAGE_GENERATE_SUCCESS.value, message="图片生成完成,等待用户手动生成视频 AI 提词") + elif step.step_code == ShotReplicateStepCodeEnum.VIDEO_GENERATE.value: + step.status = ModuleStepStatusEnum.COMPLETED.value + step.completed_at = _now() + step.output_json = _step_output( + step_code=ShotReplicateStepCodeEnum.VIDEO_GENERATE.value, + status=ModuleStepStatusEnum.COMPLETED.value, + result={"result_video_url": task.video_url, "result_video_cover_url": task.video_cover_url, "chat_task_id": task.id}, + ) + project.final_video_url = task.video_url + project.final_video_cover_url = task.video_cover_url + project.status = ModuleProjectStatusEnum.COMPLETED.value + project.current_step_code = ShotReplicateStepCodeEnum.VIDEO_GENERATE.value + project.completed_at = _now() + await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.VIDEO_GENERATE_SUCCESS.value, message="视频生成完成,总任务完成") + + +async def handle_chat_generation_task_failed(db: AsyncSession, task: ChatGenerationTask) -> None: + if not task or task.generation_mode != GENERATION_MODE: + return + result = await db.execute( + select(ModuleGenerationStep) + .where(ModuleGenerationStep.chat_task_id == task.id, ModuleGenerationStep.module == MODULE, ModuleGenerationStep.is_current == True, ModuleGenerationStep.deleted_at.is_(None)) + .with_for_update() + .limit(1) + ) + step = result.scalar_one_or_none() + if not step: + return + project_result = await db.execute(select(ModuleGenerationProject).where(ModuleGenerationProject.id == step.project_id).with_for_update().limit(1)) + project = project_result.scalar_one_or_none() + if not project: + return + step.status = ModuleStepStatusEnum.FAILED.value + step.error_message = task.error_message + step.completed_at = _now() + project.status = ModuleProjectStatusEnum.FAILED.value + project.error_message = task.error_message or "生成失败" + await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.CHAT_TASK_FAILED.value, message=project.error_message, detail={"chat_task_id": task.id}) + + +async def mark_shot_replicate_step_dispatch_failed( + db: AsyncSession, + *, + current_user: User, + project_id: str, + step_id: str, + error_message: str, +) -> None: + project = await _get_project_for_user(db, project_id=project_id, user=current_user, for_update=True) + result = await db.execute( + select(ModuleGenerationStep) + .where( + ModuleGenerationStep.id == step_id, + ModuleGenerationStep.project_id == project.id, + ModuleGenerationStep.module == MODULE, + ModuleGenerationStep.deleted_at.is_(None), + ModuleGenerationStep.is_current == True, + ) + .with_for_update() + .limit(1) + ) + step = result.scalar_one_or_none() + if not step: + return + if step.chat_task_id: + await mark_chat_generation_task_failed_and_refund_once( + db, + task_id=step.chat_task_id, + error_message=error_message, + pipeline_stage="failed", + ) + step.status = ModuleStepStatusEnum.FAILED.value + step.error_message = error_message + step.completed_at = _now() + project.status = ModuleProjectStatusEnum.FAILED.value + project.error_message = error_message + log_module_error( + module=project.module, + event_type="CELERY_DISPATCH_FAILED", + project_id=project.id, + step_id=step.id, + user_id=project.user_id, + message=error_message, + detail={"reason": "celery_dispatch_failed", "chat_task_id": step.chat_task_id}, + error=error_message, + ) + await log_module_event( + db, + project=project, + step=step, + event_type=ModuleEventTypeEnum.CHAT_TASK_FAILED.value, + message=error_message, + detail={"reason": "celery_dispatch_failed"}, + ) + + +async def delete_shot_replicate_project(db: AsyncSession, *, current_user: User, project_id: str) -> ShotReplicateDeleteOut: + project = await _get_project_for_user(db, project_id=project_id, user=current_user, for_update=True) + deleted_at = _now() + project.deleted_at = deleted_at + await _soft_delete_steps_from_index(db, project=project, start_index=1, deleted_at=deleted_at) + await log_module_event(db, project=project, event_type=ModuleEventTypeEnum.PROJECT_DELETED.value, message="软删除拆镜复刻项目") + return ShotReplicateDeleteOut(message="项目已删除", project_id=project.id, deleted=True) + + +async def create_shot_replicate_project_from_segment( + db: AsyncSession, + *, + current_user: User, + segment: ShotReplicateSegment, + req: ShotSegmentReplicationCreateRequest, +) -> ModuleGenerationProject: + """从拆镜片段创建拆镜复刻项目。 + + 素材视频固定取 segment.segment_video_url,不允许前端传入或后续修改。 + """ + if segment.user_id != current_user.id and not current_user.is_admin: + raise HTTPException(status_code=404, detail="拆镜片段不存在") + if segment.split_status != ShotSplitStatusEnum.COMPLETED.value or not segment.segment_video_url: + raise HTTPException(status_code=400, detail="拆镜片段视频未完成,不能进入复刻流程") + if segment.module_project_id: + existing = await _get_project_for_user( + db, + project_id=segment.module_project_id, + user=current_user, + for_update=False, + ) + return existing + + if req.idempotency_key: + existing_result = await db.execute( + select(ModuleGenerationProject).where( + ModuleGenerationProject.user_id == current_user.id, + ModuleGenerationProject.module == MODULE, + ModuleGenerationProject.idempotency_key == req.idempotency_key, + ModuleGenerationProject.deleted_at.is_(None), + ).limit(1) + ) + existing = existing_result.scalar_one_or_none() + if existing: + segment.module_project_id = existing.id + segment.replicate_status = ShotSegmentReplicateStatusEnum.PROJECT_CREATED.value + await db.flush() + return existing + + project = ModuleGenerationProject( + id=generate_id(), + user_id=current_user.id, + module=MODULE, + title=req.target_project_name, + status=ModuleProjectStatusEnum.WAITING_USER.value, + current_step_code=ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value, + idempotency_key=req.idempotency_key, + ) + db.add(project) + await db.flush() + + payload = { + "material_video_url": segment.segment_video_url, + "material_video_locked": True, + "material_image_url": req.material_image_url, + "source_project_name": segment.segment_category or segment.original_video_category or "拆镜片段", + "target_project_name": req.target_project_name, + "core_content_point": req.core_content_point, + "source_shot_task_set_id": segment.task_set_id, + "source_segment_id": segment.id, + "source_segment_index": segment.segment_index, + "segment_time_node": segment.time_node, + } + context = { + "shot_segment": { + "id": segment.id, + "task_set_id": segment.task_set_id, + "segment_index": segment.segment_index, + "time_node": segment.time_node, + "start_second": segment.start_second, + "end_second": segment.end_second, + "source_mode": segment.source_mode, + }, + "analysis": { + "original_video_content": segment.original_video_content, + "original_video_category": segment.original_video_category, + "original_video_audience": segment.original_video_audience, + "segment_content": segment.segment_content, + "segment_category": segment.segment_category, + "segment_audience": segment.segment_audience, + }, + } + + step = await _create_step( + db, + project=project, + step_code=ShotReplicateStepCodeEnum.MATERIAL_INPUT.value, + status=ModuleStepStatusEnum.COMPLETED.value, + input_data={**payload, "source_context": context}, + output_data={ + "accepted": True, + "message": "拆镜片段素材输入已提交,素材视频已锁定", + "next_step_code": ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value, + **context["analysis"], + }, + ) + project.current_step_code = ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value + project.status = ModuleProjectStatusEnum.WAITING_USER.value + segment.module_project_id = project.id + segment.replicate_status = ShotSegmentReplicateStatusEnum.PROJECT_CREATED.value + await log_module_event( + db, + project=project, + step=step, + event_type=ModuleEventTypeEnum.PROJECT_CREATED.value, + message="从拆镜片段创建拆镜复刻项目", + detail={"segment_id": segment.id, "task_set_id": segment.task_set_id}, + ) + await db.flush() + return project + + +def _build_file_url_or_data_uri(file_url: str) -> str: + """构建模型可访问的素材 URL。 + + - /uploads 与 /uploads/shot_segments 属于上传/素材域,不走 generate token 签名。 + - /generate 下的 AI 生成资源继续走 token 签名。 + - 远程 URL / data URI 原样返回。 + """ + if file_url.startswith(("http://", "https://", "data:")): + return file_url + if file_url.startswith("/generate/"): + file_url = build_resource_signed_url(resource_url=file_url, expire_seconds=86400) + base_url = str(settings.BASE_URL or "").rstrip("/") + return f"{base_url}{file_url}" if base_url else file_url diff --git a/video-gen-api/app/services/shot_replicate_recovery_service.py b/video-gen-api/app/services/shot_replicate_recovery_service.py new file mode 100644 index 00000000..3470f89b --- /dev/null +++ b/video-gen-api/app/services/shot_replicate_recovery_service.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +from datetime import datetime, timedelta, timezone +from typing import Any + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.config import settings +from app.enums.shot_replicate import ShotSplitStatusEnum +from app.models.shot_replicate_segment import ShotReplicateSegment +from app.models.shot_replicate_task_set import ShotReplicateTaskSet +from app.services.shot_replicate_taskset_service import refresh_task_set_split_summary +from app.tasks.celery_app import celery_app + + +def _now() -> datetime: + return datetime.now(timezone.utc) + + +def _ensure_aware(value: datetime | None) -> datetime | None: + if value is None: + return None + if value.tzinfo is None: + return value.replace(tzinfo=timezone.utc) + return value.astimezone(timezone.utc) + + +def _expired(value: datetime | None, now: datetime | None = None) -> bool: + checked = _ensure_aware(value) + if checked is None: + return True + return checked <= (now or _now()) + + +def _queue_timeout(segment: ShotReplicateSegment, now: datetime | None = None) -> bool: + enqueued_at = _ensure_aware(segment.split_enqueued_at) + if enqueued_at is None: + return True + return enqueued_at + timedelta(seconds=int(settings.SHOT_SPLIT_PENDING_TIMEOUT_SECONDS or 300)) <= (now or _now()) + + +async def recover_one_split_segment(db: AsyncSession, segment: ShotReplicateSegment, *, source: str = "startup_db") -> str: + from app.tasks.shot_replicate_tasks import split_one_segment + + if not segment: + return "skip_missing_segment" + if segment.deleted_at is not None: + return "skip_deleted" + if segment.split_status == ShotSplitStatusEnum.COMPLETED.value: + return "skip_completed" + if segment.split_status == ShotSplitStatusEnum.FAILED.value: + return "skip_failed" + + current_time = _now() + should_recover = False + + if segment.split_status == ShotSplitStatusEnum.PENDING.value: + should_recover = _queue_timeout(segment, current_time) + elif segment.split_status == ShotSplitStatusEnum.PROCESSING.value: + should_recover = _expired(segment.split_lease_until, current_time) + elif segment.split_status == ShotSplitStatusEnum.RETRY_WAITING.value: + should_recover = _expired(segment.split_next_retry_at, current_time) + + if not should_recover: + return f"skip_{segment.split_status}_not_due" + + if int(segment.split_retry_count or 0) >= int(settings.SHOT_SPLIT_MAX_RETRY_COUNT or 3): + segment.split_status = ShotSplitStatusEnum.FAILED.value + segment.split_last_error = segment.split_last_error or f"{source} 恢复时超过最大重试次数" + segment.split_lease_until = None + segment.split_next_retry_at = None + await refresh_task_set_split_summary(db, segment.task_set_id) + await db.commit() + return "mark_failed_max_retry" + + segment.split_status = ShotSplitStatusEnum.PENDING.value + segment.split_enqueued_at = current_time + segment.split_lease_until = None + segment.split_next_retry_at = None + await refresh_task_set_split_summary(db, segment.task_set_id) + await db.commit() + + if celery_app: + split_one_segment.apply_async( + args=[segment.id], + queue="gen_result_download", + priority=settings.DOWNLOAD_TASK_PRIORITY_RECOVER, + countdown=0, + ) + return f"recover_{source}" + + +async def recover_shot_split_tasks_once(db: AsyncSession) -> dict[str, Any]: + """拆镜 ffmpeg 任务容灾恢复。独立扫描 shot_replicate_segments,不复用 Chat 下载 active registry。""" + batch_size = int(settings.SHOT_SPLIT_RECOVERY_BATCH_SIZE or 50) + result = await db.execute( + select(ShotReplicateSegment) + .where( + ShotReplicateSegment.deleted_at.is_(None), + ShotReplicateSegment.split_status.in_( + [ + ShotSplitStatusEnum.PENDING.value, + ShotSplitStatusEnum.PROCESSING.value, + ShotSplitStatusEnum.RETRY_WAITING.value, + ] + ), + ) + .order_by(ShotReplicateSegment.updated_at.asc()) + .limit(batch_size) + .with_for_update(skip_locked=True) + ) + segments = list(result.scalars().all()) + + checked = 0 + results: dict[str, int] = {} + touched_task_set_ids: set[str] = set() + for segment in segments: + action = await recover_one_split_segment(db, segment, source="startup_db") + checked += 1 + touched_task_set_ids.add(segment.task_set_id) + results[action] = results.get(action, 0) + 1 + + for task_set_id in touched_task_set_ids: + await refresh_task_set_split_summary(db, task_set_id) + await db.commit() + + return {"checked": checked, "results": results} diff --git a/video-gen-api/app/services/shot_replicate_taskset_service.py b/video-gen-api/app/services/shot_replicate_taskset_service.py new file mode 100644 index 00000000..2ad87bf4 --- /dev/null +++ b/video-gen-api/app/services/shot_replicate_taskset_service.py @@ -0,0 +1,529 @@ +from __future__ import annotations + +import uuid +from datetime import datetime, timezone +from typing import Any + +from fastapi import HTTPException + +from app.config import settings +from sqlalchemy import func, select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.enums.shot_replicate import ( + ModuleCodeEnum, + ShotAnalysisStatusEnum, + ShotSegmentAnalysisStatusEnum, + ShotSegmentReplicateStatusEnum, + ShotSegmentSourceModeEnum, + ShotSplitStatusEnum, + ShotTaskSetStatusEnum, +) +from app.models.shot_replicate_segment import ShotReplicateSegment +from app.models.shot_replicate_task_set import ShotReplicateTaskSet +from app.models.user import User +from app.schemas.shot_replicate import ( + ShotAISuggestionOut, + ShotSegmentDetailOut, + ShotSegmentListOut, + ShotSegmentOut, + ShotSplitByAIOut, + ShotSplitByAIRequest, + ShotSplitCustomOut, + ShotSplitCustomRequest, + ShotTaskSetCreate, + ShotTaskSetDetailOut, + ShotTaskSetListOut, + ShotTaskSetOut, +) +from app.services.module_generation_log_service import log_module_event_file +from app.services.upload_video_asset_service import ( + build_time_node, + validate_split_range, + validate_upload_video_asset, +) +from app.tasks.celery_app import celery_app +from app.utils.id_gen import generate_id + + +MODULE = ModuleCodeEnum.SHOT_REPLICATE.value + + +def _now() -> datetime: + return datetime.now(timezone.utc) + + +def _normalize_suggestions(value: Any) -> list[dict[str, Any]]: + if not isinstance(value, list): + return [] + normalized: list[dict[str, Any]] = [] + for idx, item in enumerate(value, start=1): + if not isinstance(item, dict): + continue + start = item.get("拆镜开始秒") + end = item.get("拆镜结束秒") + try: + start_f = float(start) + end_f = float(end) + except Exception: + continue + if start_f < 0 or end_f <= start_f: + continue + normalized.append( + { + "index": idx, + "start_second": start_f, + "end_second": end_f, + "duration_seconds": round(end_f - start_f, 3), + "time_node": str(item.get("拆镜时间节点") or build_time_node(start_f, end_f)), + "content": str(item.get("对应时间节点内的内容") or "无"), + "category": str(item.get("分类") or "无"), + "audience": str(item.get("受众人群") or "无"), + "raw": item, + } + ) + return normalized + + +def _task_set_to_out(task_set: ShotReplicateTaskSet) -> ShotTaskSetOut: + return ShotTaskSetOut.model_validate(task_set) + + +def _task_set_to_detail_out(task_set: ShotReplicateTaskSet) -> ShotTaskSetDetailOut: + suggestions = [ShotAISuggestionOut(**{k: v for k, v in item.items() if k != "raw"}) for item in _normalize_suggestions(task_set.ai_suggestion_json)] + base = ShotTaskSetDetailOut.model_validate(task_set) + base.ai_suggestions = suggestions + return base + + +def _segment_to_out(segment: ShotReplicateSegment) -> ShotSegmentOut: + data = ShotSegmentOut.model_validate(segment) + data.segment_name = f"片段{segment.segment_index}" + return data + + +def _segment_to_detail_out(segment: ShotReplicateSegment) -> ShotSegmentDetailOut: + data = ShotSegmentDetailOut.model_validate(segment) + data.segment_name = f"片段{segment.segment_index}" + return data + + +async def get_task_set_for_user( + db: AsyncSession, + *, + task_set_id: str, + user: User, + for_update: bool = False, +) -> ShotReplicateTaskSet: + query = select(ShotReplicateTaskSet).where( + ShotReplicateTaskSet.id == task_set_id, + ShotReplicateTaskSet.deleted_at.is_(None), + ) + if not user.is_admin: + query = query.where(ShotReplicateTaskSet.user_id == user.id) + if for_update: + query = query.with_for_update() + result = await db.execute(query.limit(1)) + task_set = result.scalar_one_or_none() + if not task_set: + raise HTTPException(status_code=404, detail="拆镜总任务集不存在") + return task_set + + +async def get_segment_for_user( + db: AsyncSession, + *, + segment_id: str, + user: User, + for_update: bool = False, +) -> ShotReplicateSegment: + query = select(ShotReplicateSegment).where( + ShotReplicateSegment.id == segment_id, + ShotReplicateSegment.deleted_at.is_(None), + ) + if not user.is_admin: + query = query.where(ShotReplicateSegment.user_id == user.id) + if for_update: + query = query.with_for_update() + result = await db.execute(query.limit(1)) + segment = result.scalar_one_or_none() + if not segment: + raise HTTPException(status_code=404, detail="拆镜片段不存在") + return segment + + +async def create_task_set(db: AsyncSession, *, current_user: User, req: ShotTaskSetCreate) -> ShotReplicateTaskSet: + if req.idempotency_key: + existing_result = await db.execute( + select(ShotReplicateTaskSet).where( + ShotReplicateTaskSet.user_id == current_user.id, + ShotReplicateTaskSet.idempotency_key == req.idempotency_key, + ShotReplicateTaskSet.deleted_at.is_(None), + ).limit(1) + ) + existing = existing_result.scalar_one_or_none() + if existing: + return existing + + asset = validate_upload_video_asset(req.video_url, req.video_duration_seconds) + task_set = ShotReplicateTaskSet( + id=generate_id(), + user_id=current_user.id, + title=req.title or "拆镜复刻任务", + video_url=asset.url, + video_path=str(asset.path), + video_duration_seconds=asset.duration_seconds, + status=ShotTaskSetStatusEnum.PENDING_ANALYSIS.value, + analysis_status=ShotAnalysisStatusEnum.PENDING.value, + split_status=ShotSplitStatusEnum.NONE.value, + segment_count=0, + completed_segment_count=0, + failed_segment_count=0, + idempotency_key=req.idempotency_key, + ) + db.add(task_set) + await db.flush() + log_module_event_file( + module=MODULE, + event_type="SHOT_TASK_SET_CREATED", + project_id=task_set.id, + user_id=task_set.user_id, + message="创建拆镜总任务集", + detail={ + "task_set_id": task_set.id, + "title": task_set.title, + "video_url": task_set.video_url, + "video_path": task_set.video_path, + "video_duration_seconds": task_set.video_duration_seconds, + "idempotency_key": task_set.idempotency_key, + }, + ) + return task_set + + +async def list_task_sets( + db: AsyncSession, + *, + current_user: User, + status: str | None = None, + analysis_status: str | None = None, + split_status: str | None = None, + keyword: str | None = None, + page: int = 1, + page_size: int = 20, +) -> ShotTaskSetListOut: + query = select(ShotReplicateTaskSet).where(ShotReplicateTaskSet.deleted_at.is_(None)) + if not current_user.is_admin: + query = query.where(ShotReplicateTaskSet.user_id == current_user.id) + if status: + query = query.where(ShotReplicateTaskSet.status == status) + if analysis_status: + query = query.where(ShotReplicateTaskSet.analysis_status == analysis_status) + if split_status: + query = query.where(ShotReplicateTaskSet.split_status == split_status) + if keyword: + like = f"%{keyword.strip()}%" + query = query.where( + (ShotReplicateTaskSet.title.ilike(like)) + | (ShotReplicateTaskSet.original_video_content.ilike(like)) + | (ShotReplicateTaskSet.original_video_category.ilike(like)) + ) + + total_result = await db.execute(select(func.count()).select_from(query.subquery())) + total = int(total_result.scalar() or 0) + rows = await db.execute( + query.order_by(ShotReplicateTaskSet.created_at.desc()) + .offset((page - 1) * page_size) + .limit(page_size) + ) + return ShotTaskSetListOut(total=total, page=page, page_size=page_size, items=[_task_set_to_out(item) for item in rows.scalars().all()]) + + +async def task_set_detail(db: AsyncSession, *, current_user: User, task_set_id: str) -> ShotTaskSetDetailOut: + task_set = await get_task_set_for_user(db, task_set_id=task_set_id, user=current_user) + return _task_set_to_detail_out(task_set) + + +async def _next_segment_index(db: AsyncSession, task_set_id: str) -> int: + result = await db.execute( + select(func.max(ShotReplicateSegment.segment_index)).where( + ShotReplicateSegment.task_set_id == task_set_id, + ShotReplicateSegment.deleted_at.is_(None), + ) + ) + return int(result.scalar() or 0) + 1 + + +async def refresh_task_set_split_summary(db: AsyncSession, task_set_id: str) -> None: + task_set_result = await db.execute(select(ShotReplicateTaskSet).where(ShotReplicateTaskSet.id == task_set_id).with_for_update().limit(1)) + task_set = task_set_result.scalar_one_or_none() + if not task_set: + return + + result = await db.execute( + select(ShotReplicateSegment).where( + ShotReplicateSegment.task_set_id == task_set_id, + ShotReplicateSegment.deleted_at.is_(None), + ) + ) + segments = list(result.scalars().all()) + total = len(segments) + completed = len([s for s in segments if s.split_status == ShotSplitStatusEnum.COMPLETED.value]) + failed = len([s for s in segments if s.split_status == ShotSplitStatusEnum.FAILED.value]) + + task_set.segment_count = total + task_set.completed_segment_count = completed + task_set.failed_segment_count = failed + + if total <= 0: + task_set.split_status = ShotSplitStatusEnum.NONE.value + if task_set.analysis_status == ShotAnalysisStatusEnum.COMPLETED.value: + task_set.status = ShotTaskSetStatusEnum.ANALYSIS_COMPLETED.value + return + + old_status = task_set.status + old_split_status = task_set.split_status + + if completed == total: + task_set.split_status = ShotSplitStatusEnum.COMPLETED.value + task_set.status = ShotTaskSetStatusEnum.SPLIT_COMPLETED.value + elif failed == total: + task_set.split_status = ShotSplitStatusEnum.FAILED.value + task_set.status = ShotTaskSetStatusEnum.FAILED.value + elif failed > 0: + task_set.split_status = ShotSplitStatusEnum.FAILED.value + task_set.status = ShotTaskSetStatusEnum.PARTIAL_FAILED.value + else: + task_set.split_status = ShotSplitStatusEnum.PROCESSING.value + task_set.status = ShotTaskSetStatusEnum.SPLITTING.value + + if old_status != task_set.status or old_split_status != task_set.split_status: + log_module_event_file( + module=MODULE, + event_type="SHOT_SPLIT_STATUS_CHANGED", + project_id=task_set.id, + user_id=task_set.user_id, + message="拆镜总任务集拆分状态变更", + detail={ + "task_set_id": task_set.id, + "from_status": old_status, + "to_status": task_set.status, + "from_split_status": old_split_status, + "to_split_status": task_set.split_status, + "segment_count": total, + "completed_segment_count": completed, + "failed_segment_count": failed, + }, + ) + + +async def create_segments_by_ai( + db: AsyncSession, + *, + current_user: User, + task_set_id: str, + req: ShotSplitByAIRequest, +) -> ShotSplitByAIOut: + task_set = await get_task_set_for_user(db, task_set_id=task_set_id, user=current_user, for_update=True) + if task_set.analysis_status != ShotAnalysisStatusEnum.COMPLETED.value: + raise HTTPException(status_code=400, detail="原视频分析未完成,不能按 AI 建议拆镜") + + suggestions = _normalize_suggestions(task_set.ai_suggestion_json) + if not suggestions: + raise HTTPException(status_code=400, detail="当前没有可用 AI 建议拆镜方案,请使用自定义拆镜") + + if req.selected_indices: + selected_set = {int(x) for x in req.selected_indices} + suggestions = [item for item in suggestions if int(item["index"]) in selected_set] + if not suggestions: + raise HTTPException(status_code=400, detail="selected_indices 没有匹配到可用 AI 建议") + + old_result = await db.execute( + select(ShotReplicateSegment).where( + ShotReplicateSegment.task_set_id == task_set.id, + ShotReplicateSegment.source_mode == ShotSegmentSourceModeEnum.AI_SUGGESTION.value, + ShotReplicateSegment.deleted_at.is_(None), + ) + ) + old_segments = list(old_result.scalars().all()) + if old_segments and not req.replace_existing: + raise HTTPException(status_code=409, detail="已存在 AI 建议拆镜片段,如需重拆请传 replace_existing=true") + if old_segments and req.replace_existing: + now = _now() + for segment in old_segments: + segment.deleted_at = now + + created: list[ShotReplicateSegment] = [] + next_index = await _next_segment_index(db, task_set.id) + for item in suggestions: + start, end, duration = validate_split_range( + start_second=item["start_second"], + end_second=item["end_second"], + video_duration_seconds=task_set.video_duration_seconds, + ) + segment = ShotReplicateSegment( + id=generate_id(), + task_set_id=task_set.id, + user_id=task_set.user_id, + segment_index=next_index, + source_mode=ShotSegmentSourceModeEnum.AI_SUGGESTION.value, + start_second=start, + end_second=end, + duration_seconds=duration, + time_node=build_time_node(start, end), + split_status=ShotSplitStatusEnum.PENDING.value, + analysis_status=ShotSegmentAnalysisStatusEnum.NOT_REQUIRED.value, + replicate_status=ShotSegmentReplicateStatusEnum.NOT_STARTED.value, + original_video_content=task_set.original_video_content, + original_video_category=task_set.original_video_category, + original_video_audience=task_set.original_video_audience, + segment_content=item.get("content"), + segment_category=item.get("category"), + segment_audience=item.get("audience"), + ai_suggestion_json=item.get("raw") or item, + split_enqueued_at=_now(), + split_celery_task_id=f"shot-split:{uuid.uuid4().hex}", + ) + db.add(segment) + created.append(segment) + next_index += 1 + + task_set.status = ShotTaskSetStatusEnum.SPLITTING.value + task_set.split_status = ShotSplitStatusEnum.PROCESSING.value + await db.flush() + await refresh_task_set_split_summary(db, task_set.id) + await db.flush() + + log_module_event_file( + module=MODULE, + event_type="SHOT_SPLIT_BY_AI_SUBMITTED", + project_id=task_set.id, + user_id=task_set.user_id, + message="按 AI 建议创建拆镜片段", + detail={ + "task_set_id": task_set.id, + "selected_indices": req.selected_indices, + "replace_existing": req.replace_existing, + "created_segment_count": len(created), + "segment_ids": [segment.id for segment in created], + }, + ) + + return ShotSplitByAIOut( + task_set_id=task_set.id, + status=task_set.status, + split_status=task_set.split_status, + created_segment_count=len(created), + segments=[_segment_to_out(segment) for segment in created], + ) + + +async def create_custom_segment( + db: AsyncSession, + *, + current_user: User, + task_set_id: str, + req: ShotSplitCustomRequest, +) -> ShotSplitCustomOut: + task_set = await get_task_set_for_user(db, task_set_id=task_set_id, user=current_user, for_update=True) + start, end, duration = validate_split_range( + start_second=req.start_second, + end_second=req.end_second, + video_duration_seconds=task_set.video_duration_seconds, + ) + next_index = await _next_segment_index(db, task_set.id) + segment = ShotReplicateSegment( + id=generate_id(), + task_set_id=task_set.id, + user_id=task_set.user_id, + segment_index=next_index, + source_mode=ShotSegmentSourceModeEnum.CUSTOM.value, + start_second=start, + end_second=end, + duration_seconds=duration, + time_node=build_time_node(start, end), + split_status=ShotSplitStatusEnum.PENDING.value, + analysis_status=ShotSegmentAnalysisStatusEnum.PENDING.value, + replicate_status=ShotSegmentReplicateStatusEnum.NOT_STARTED.value, + split_enqueued_at=_now(), + split_celery_task_id=f"shot-split:{uuid.uuid4().hex}", + ) + db.add(segment) + task_set.status = ShotTaskSetStatusEnum.SPLITTING.value + task_set.split_status = ShotSplitStatusEnum.PROCESSING.value + await db.flush() + await refresh_task_set_split_summary(db, task_set.id) + await db.flush() + log_module_event_file( + module=MODULE, + event_type="SHOT_SPLIT_CUSTOM_SUBMITTED", + project_id=task_set.id, + step_id=segment.id, + user_id=task_set.user_id, + message="按用户自定义时间创建拆镜片段", + detail={ + "task_set_id": task_set.id, + "segment_id": segment.id, + "start_second": start, + "end_second": end, + "duration_seconds": duration, + "time_node": segment.time_node, + }, + ) + return ShotSplitCustomOut(task_set_id=task_set.id, segment=_segment_to_out(segment)) + + +async def enqueue_segment_split(segment_id: str, *, countdown: int | None = None, recover: bool = False) -> None: + if not celery_app: + return + from app.tasks.shot_replicate_tasks import split_one_segment + + split_one_segment.apply_async( + args=[segment_id], + queue="gen_result_download", + countdown=countdown, + priority=settings.DOWNLOAD_TASK_PRIORITY_RECOVER if recover else settings.DOWNLOAD_TASK_PRIORITY_NORMAL, + ) + + +async def list_segments( + db: AsyncSession, + *, + current_user: User, + task_set_id: str, + source_mode: str | None = None, + split_status: str | None = None, + analysis_status: str | None = None, + replicate_status: str | None = None, + page: int = 1, + page_size: int = 20, +) -> ShotSegmentListOut: + await get_task_set_for_user(db, task_set_id=task_set_id, user=current_user) + query = select(ShotReplicateSegment).where( + ShotReplicateSegment.task_set_id == task_set_id, + ShotReplicateSegment.deleted_at.is_(None), + ) + if not current_user.is_admin: + query = query.where(ShotReplicateSegment.user_id == current_user.id) + if source_mode: + query = query.where(ShotReplicateSegment.source_mode == source_mode) + if split_status: + query = query.where(ShotReplicateSegment.split_status == split_status) + if analysis_status: + query = query.where(ShotReplicateSegment.analysis_status == analysis_status) + if replicate_status: + query = query.where(ShotReplicateSegment.replicate_status == replicate_status) + + total_result = await db.execute(select(func.count()).select_from(query.subquery())) + total = int(total_result.scalar() or 0) + rows = await db.execute( + query.order_by(ShotReplicateSegment.segment_index.asc()) + .offset((page - 1) * page_size) + .limit(page_size) + ) + return ShotSegmentListOut(total=total, page=page, page_size=page_size, items=[_segment_to_out(item) for item in rows.scalars().all()]) + + +async def segment_detail(db: AsyncSession, *, current_user: User, segment_id: str) -> ShotSegmentDetailOut: + segment = await get_segment_for_user(db, segment_id=segment_id, user=current_user) + return _segment_to_detail_out(segment) diff --git a/video-gen-api/app/services/shot_video_analysis_service.py b/video-gen-api/app/services/shot_video_analysis_service.py new file mode 100644 index 00000000..2da22640 --- /dev/null +++ b/video-gen-api/app/services/shot_video_analysis_service.py @@ -0,0 +1,521 @@ +from __future__ import annotations + +import base64 +import json +import mimetypes +import os +import re +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Literal + +import httpx +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.config import settings +from app.models.model_config import ModelConfig +from app.models.token_usage import TokenUsage +from app.services.upload_video_asset_service import resolve_upload_video_path +from app.services.resource_signed_url_service import build_resource_signed_url +from app.utils.id_gen import generate_id + +AnalysisMode = Literal["full_breakdown", "summary_only"] + + +@dataclass(slots=True) +class ShotVideoAnalysisResult: + result: dict[str, Any] + raw_response: dict[str, Any] + usage: dict[str, Any] + + +def _timeout_seconds() -> int: + return int(getattr(settings, "SHOT_ANALYSIS_TIMEOUT_SECONDS", 180) or 180) + + +def _video_fps() -> float: + return float(getattr(settings, "SHOT_ANALYSIS_VIDEO_FPS", 1.0) or 1.0) + + +def _split_min_seconds() -> float: + return float(getattr(settings, "SHOT_SPLIT_MIN_SECONDS", 1) or 1) + + +def _split_max_seconds() -> float: + return float(getattr(settings, "SHOT_SPLIT_MAX_SECONDS", 120) or 120) + + +def _resolve_local_file_path(file_url: str) -> str: + if file_url.startswith("/uploads/") or file_url.startswith("uploads/"): + return str(resolve_upload_video_path(file_url)) + return file_url + + +def build_file_url_or_data_uri(file_url: str, fallback_mime: str = "video/mp4") -> str: + if file_url.startswith(("http://", "https://", "data:")): + return file_url + file_url_sign = build_resource_signed_url(resource_url=file_url, expire_seconds=86400) + return f"{settings.BASE_URL}{file_url_sign}" + + # file_path = _resolve_local_file_path(file_url) + # path = Path(file_path) + # if not path.exists(): + # raise FileNotFoundError(f"视频文件不存在: {file_path}") + # + # max_mb = float(getattr(settings, "SHOT_ANALYSIS_MAX_LOCAL_VIDEO_MB", 45) or 45) + # size_mb = path.stat().st_size / 1024 / 1024 + # if size_mb > max_mb: + # raise ValueError(f"本地视频文件过大: {size_mb:.2f} MB,当前限制 {max_mb:g} MB") + # + # mime = mimetypes.guess_type(str(path))[0] or fallback_mime + # with open(path, "rb") as f: + # b64 = base64.b64encode(f.read()).decode("utf-8") + # return f"data:{mime};base64,{b64}" + + +def build_user_message(user_text: str, video_url: str) -> tuple[dict[str, Any], dict[str, Any]]: + real_url = build_file_url_or_data_uri(video_url) + content_parts = [ + { + "type": "video_url", + "video_url": { + "url": real_url, + "fps": _video_fps(), + }, + }, + {"type": "text", "text": user_text}, + ] + log_content_parts = [ + { + "type": "video_url", + "video_url": { + "url": video_url, + "fps": _video_fps(), + }, + }, + {"type": "text", "text": user_text}, + ] + return {"role": "user", "content": content_parts}, {"role": "user", "content": log_content_parts} + + +def build_video_analysis_system_prompt(*, mode: AnalysisMode) -> str: + if mode == "summary_only": + return """ +你是专业的短视频内容分析师、广告素材拆解师。 + +你的任务: +1. 根据用户提供的视频附件,分析这个视频片段的整体内容。 +2. 判断视频分类。 +3. 判断视频可能面向的受众人群。 +4. 必须输出严格 JSON 对象。 +5. 不输出 Markdown。 +6. 不输出解释文字。 +7. 不返回 null,未知内容填“无”。 + +顶级字段只能包含: +- 原视频内容 +- 原视频分类 +- 原视频受众人群 +- 拆镜内容剖析 + +summary_only 模式下“拆镜内容剖析”必须返回空数组。 + +安全规则: +1. 不要识别视频中人物身份。 +2. 不要猜测真实姓名、联系方式、账号身份。 +3. 如果视频是游戏录屏,只分析画面内容、玩法内容、玩家情绪表达、受众,不要编造不存在的剧情。 +4. 如果视频包含广告内容,可以分析广告品类、目标用户、转化意图,但不要编造品牌信息。 +""".strip() + + return f""" +你是专业的短视频内容分析师、广告素材拆解师、视频分镜分析师。 + +你的任务: +1. 根据用户提供的视频附件,分析原视频整体内容。 +2. 判断原视频分类。 +3. 判断原视频可能面向的受众人群。 +4. 对视频进行拆镜内容剖析。 +5. 必须输出严格 JSON 对象。 +6. 不输出 Markdown。 +7. 不输出解释文字。 +8. 不返回 null,未知内容填“无”。 + +顶级字段只能包含: +- 原视频内容 +- 原视频分类 +- 原视频受众人群 +- 拆镜内容剖析 + +拆镜内容剖析必须是数组。 + +每个拆镜片段必须包含: +- 拆镜开始秒 +- 拆镜结束秒 +- 拆镜时间节点 +- 对应时间节点内的内容 +- 分类 +- 受众人群 + +拆镜时间规则: +1. 拆镜开始秒必须是数字,例如 0、15、26。 +2. 拆镜结束秒必须是数字,例如 15、26、31。 +3. 拆镜时间节点必须由拆镜开始秒和拆镜结束秒组成,例如“0-15秒”。 +4. 禁止输出“-15秒”这种缺少开始秒的时间节点。 +5. 禁止输出“15-秒”这种缺少结束秒的时间节点。 +6. 每个拆镜片段时长不能低于 {_split_min_seconds():g} 秒。 +7. 每个拆镜片段时长不能高于 {_split_max_seconds():g} 秒。 +8. 如果某段内容不足 {_split_min_seconds():g} 秒,不要单独拆出来。 +9. 如果单个连续内容超过 {_split_max_seconds():g} 秒,需要按语义变化继续拆分。 +10. 如果没有明显镜头变化、场景变化、人物动作变化、剧情变化、字幕重点变化或语义变化,不要强行剖析。 +11. 如果无法可靠拆镜,则“拆镜内容剖析”返回空数组。 +12. 拆镜时间必须从 0 秒或视频中实际可识别的开始时间开始,不允许出现负数。 +13. 拆镜结束秒必须大于拆镜开始秒。 +14. 拆镜片段必须按时间顺序排列。 + +安全规则: +1. 不要识别视频中人物身份。 +2. 不要猜测真实姓名、联系方式、账号身份。 +3. 如果视频是游戏录屏,只分析画面内容、玩法内容、玩家情绪表达、受众,不要编造不存在的剧情。 +4. 如果视频包含广告内容,可以分析广告品类、目标用户、转化意图,但不要编造品牌信息。 +""".strip() + + +def build_video_analysis_user_text(*, mode: AnalysisMode) -> str: + if mode == "summary_only": + payload = { + "任务": "请根据上传的视频片段附件,返回这个视频片段的内容分析 JSON。", + "输出JSON格式": { + "原视频内容": "概括这个视频片段整体内容,描述主要画面、主体、场景、动作、剧情或信息点", + "原视频分类": "判断视频类型,例如:游戏视频、产品广告视频、剧情视频、口播讲解视频、教程视频、生活记录视频等", + "原视频受众人群": "判断该片段更适合的人群", + "拆镜内容剖析": [], + }, + "返回要求": ["只返回 JSON 对象", "不要返回 Markdown", "不要返回解释文字", "不要返回代码块", "不要返回 null,未知填无"], + } + return json.dumps(payload, ensure_ascii=False) + + payload = { + "任务": "请根据上传的视频附件,返回原视频内容分析和拆镜内容剖析 JSON。", + "输出JSON格式": { + "原视频内容": "概括原视频整体内容,描述主要画面、人物/主体、场景、动作、剧情或信息点", + "原视频分类": "判断视频类型,例如:游戏视频、产品广告视频、剧情视频、口播讲解视频、教程视频、生活记录视频、直播切片视频、图文快闪视频等", + "原视频受众人群": "判断该视频更适合的人群,例如:游戏玩家、年轻娱乐用户、潜在购买用户、同城社交用户等", + "拆镜内容剖析": [ + { + "拆镜开始秒": 0, + "拆镜结束秒": 15, + "拆镜时间节点": "0-15秒", + "对应时间节点内的内容": "描述这个时间片段内发生了什么", + "分类": "判断这个片段的内容分类,例如:开场吸引、冲突铺垫、玩法展示、卖点展示、情绪爆发、行动引导、结果展示等", + "受众人群": "判断这个片段主要吸引的人群", + } + ], + }, + "拆镜规则": [ + f"每个拆镜片段时长必须大于等于 {_split_min_seconds():g} 秒", + f"每个拆镜片段时长必须小于等于 {_split_max_seconds():g} 秒", + "拆镜开始秒必须是数字", + "拆镜结束秒必须是数字", + "拆镜开始秒不能是负数", + "拆镜结束秒必须大于拆镜开始秒", + "拆镜时间节点必须等于:拆镜开始秒-拆镜结束秒秒", + "禁止输出“-15秒”", + "禁止输出“15-秒”", + "如果无法判断拆镜节点,拆镜内容剖析返回空数组", + ], + "返回要求": ["只返回 JSON 对象", "不要返回 Markdown", "不要返回解释文字", "不要返回代码块", "不要返回 null,未知填无"], + } + return json.dumps(payload, ensure_ascii=False) + + +def strip_json_code_fence(text: str) -> str: + text = text.strip() + if text.startswith("```"): + text = re.sub(r"^```(?:json)?", "", text, flags=re.IGNORECASE).strip() + text = re.sub(r"```$", "", text).strip() + return text + + +def parse_model_json(content: str) -> dict[str, Any]: + cleaned = strip_json_code_fence(content) + data = json.loads(cleaned) + if not isinstance(data, dict): + raise ValueError(f"模型返回 JSON 不是对象类型: {type(data).__name__}") + return data + + +def get_message_content_or_raise(data: dict[str, Any]) -> str: + choices = data.get("choices") or [] + if not choices: + raise RuntimeError(f"模型响应没有 choices: {json.dumps(data, ensure_ascii=False)}") + choice = choices[0] + finish_reason = choice.get("finish_reason") + if finish_reason == "length": + usage = data.get("usage", {}) + raise RuntimeError(f"模型输出被长度限制截断,finish_reason={finish_reason}, usage={json.dumps(usage, ensure_ascii=False)}") + message = choice.get("message") or {} + content = message.get("content", "") + if not content: + raise RuntimeError(f"模型响应 content 为空: {json.dumps(data, ensure_ascii=False)}") + return content.strip() + + +def fill_none_with_wu(value: Any) -> Any: + if value is None: + return "无" + if isinstance(value, str): + return value if value.strip() else "无" + if isinstance(value, list): + return [fill_none_with_wu(item) for item in value] + if isinstance(value, dict): + return {k: fill_none_with_wu(v) for k, v in value.items()} + return value + + +def first_present(data: dict[str, Any], keys: list[str]) -> Any: + for key in keys: + if key in data: + return data.get(key) + return None + + +def parse_number(value: Any) -> float | None: + if isinstance(value, (int, float)): + return float(value) + text = str(value or "").strip() + if not text: + return None + m = re.search(r"-?\d+(?:\.\d+)?", text) + return float(m.group(0)) if m else None + + +def parse_time_value_to_seconds(value: str) -> float | None: + value = str(value or "").strip() + if not value: + return None + m = re.match(r"^(\d+(?:\.\d+)?)\s*(?:秒|s)?$", value, flags=re.IGNORECASE) + if m: + return float(m.group(1)) + parts = value.split(":") + if len(parts) in (2, 3) and all(re.match(r"^\d+(?:\.\d+)?$", p.strip()) for p in parts): + nums = [float(p.strip()) for p in parts] + if len(nums) == 2: + minute, second = nums + return minute * 60 + second + hour, minute, second = nums + return hour * 3600 + minute * 60 + second + return None + + +def parse_time_node_to_range(time_node: str) -> tuple[float, float] | None: + text = str(time_node or "").strip() + if not text: + return None + text = text.replace("—", "-").replace("–", "-").replace("-", "-") + text = text.replace("到", "-").replace("至", "-").replace("~", "-").replace("~", "-") + text = text.replace("第", "").replace("时间段", "").replace(":", ":") + m = re.search(r"(\d{1,2}:\d{1,2}(?::\d{1,2})?)\s*-\s*(\d{1,2}:\d{1,2}(?::\d{1,2})?)", text) + if m: + start = parse_time_value_to_seconds(m.group(1)) + end = parse_time_value_to_seconds(m.group(2)) + if start is not None and end is not None and end > start: + return start, end + m = re.search(r"(\d+(?:\.\d+)?)\s*(?:秒|s)?\s*-\s*(\d+(?:\.\d+)?)\s*(?:秒|s)?", text, flags=re.IGNORECASE) + if m: + start = float(m.group(1)) + end = float(m.group(2)) + if end > start: + return start, end + m = re.search(r"^\s*-\s*(\d+(?:\.\d+)?)\s*(?:秒|s)?\s*$", text, flags=re.IGNORECASE) + if m: + end = float(m.group(1)) + if end > 0: + return 0.0, end + return None + + +def format_second(value: float) -> int | float: + checked = float(value) + return int(checked) if checked.is_integer() else round(checked, 2) + + +def normalize_time_node_by_range(start: float, end: float) -> str: + return f"{format_second(start)}-{format_second(end)}秒" + + +def ensure_result_schema(result: dict[str, Any]) -> dict[str, Any]: + final_result = { + "原视频内容": result.get("原视频内容", "无"), + "原视频分类": result.get("原视频分类", "无"), + "原视频受众人群": result.get("原视频受众人群", "无"), + "拆镜内容剖析": result.get("拆镜内容剖析", []), + } + for key in ("原视频内容", "原视频分类", "原视频受众人群"): + if not isinstance(final_result[key], str): + final_result[key] = json.dumps(final_result[key], ensure_ascii=False) + if not isinstance(final_result["拆镜内容剖析"], list): + final_result["拆镜内容剖析"] = [] + return final_result + + +def filter_and_normalize_breakdown(result: dict[str, Any], *, mode: AnalysisMode = "full_breakdown") -> dict[str, Any]: + if mode == "summary_only": + result["拆镜内容剖析"] = [] + return result + + breakdown = result.get("拆镜内容剖析") + if not isinstance(breakdown, list): + result["拆镜内容剖析"] = [] + return result + + normalized_items: list[dict[str, Any]] = [] + for item in breakdown: + if not isinstance(item, dict): + continue + raw_start = first_present(item, ["拆镜开始秒", "开始秒", "起始秒", "开始时间", "起始时间", "start", "start_second", "start_seconds"]) + raw_end = first_present(item, ["拆镜结束秒", "结束秒", "结束时间", "end", "end_second", "end_seconds"]) + start = parse_number(raw_start) + end = parse_number(raw_end) + time_node = str(first_present(item, ["拆镜时间节点", "时间节点", "时间段", "镜头时间", "time_node", "time_range"]) or "").strip() + if start is None or end is None: + parsed_range = parse_time_node_to_range(time_node) + if parsed_range is None: + continue + start, end = parsed_range + if start is None or end is None or start < 0 or end < 0 or end <= start: + continue + duration = end - start + if duration < _split_min_seconds() or duration > _split_max_seconds(): + continue + content = first_present(item, ["对应时间节点内的内容", "内容", "画面内容", "片段内容", "镜头内容", "content"]) or "无" + category = first_present(item, ["分类", "片段分类", "内容分类", "镜头分类", "category"]) or "无" + audience = first_present(item, ["受众人群", "目标受众", "片段受众", "镜头受众", "audience"]) or "无" + normalized_items.append({ + "拆镜开始秒": format_second(start), + "拆镜结束秒": format_second(end), + "拆镜时间节点": normalize_time_node_by_range(start, end), + "对应时间节点内的内容": str(content or "无"), + "分类": str(category or "无"), + "受众人群": str(audience or "无"), + }) + normalized_items.sort(key=lambda x: float(x.get("拆镜开始秒", 0))) + result["拆镜内容剖析"] = normalized_items + return result + + +async def _select_model_config(db: AsyncSession) -> ModelConfig | None: + result = await db.execute( + select(ModelConfig) + .where(ModelConfig.is_active == True) + .order_by(ModelConfig.priority.desc()) + .limit(1) + ) + return result.scalar_one_or_none() + + +def _int_usage(value: Any) -> int: + try: + return int(value or 0) + except Exception: + return 0 + +async def analyze_video_for_shot_split( + db: AsyncSession, + video_url: str, + *, + user_id: str | None = None, + mode: AnalysisMode = "full_breakdown", +) -> ShotVideoAnalysisResult: + """调用模型完成拆镜/片段分析。 + + 模型配置统一从 model_configs 表选择当前启用且 priority 最高的配置; + 不再读取 SHOT_ANALYSIS_API_BASE / SHOT_ANALYSIS_API_KEY / SHOT_ANALYSIS_MODEL_NAME, + 也不再 fallback 到 SEEDANCE_*,避免拆镜分析走错通道。 + """ + config = await _select_model_config(db) + if not config: + raise RuntimeError("拆镜分析模型未配置:请先在 model_configs 表启用可用模型") + if not str(config.api_key or "").strip(): + raise RuntimeError(f"拆镜分析模型 API Key 为空: model_config_id={config.id}") + if not str(config.api_base or "").strip(): + raise RuntimeError(f"拆镜分析模型 API Base 为空: model_config_id={config.id}") + if not str(config.model_name or "").strip(): + raise RuntimeError(f"拆镜分析模型名称为空: model_config_id={config.id}") + + system_prompt = build_video_analysis_system_prompt(mode=mode) + user_text = build_video_analysis_user_text(mode=mode) + user_message, log_user_message = build_user_message(user_text, video_url) + + request_data: dict[str, Any] = { + "model": config.model_name, + "messages": [ + {"role": "system", "content": system_prompt}, + user_message, + ], + "max_tokens": int(getattr(settings, "SHOT_ANALYSIS_MAX_TOKENS", 5000) or getattr(config, "max_tokens", 5000) or 5000), + "temperature": float(getattr(settings, "SHOT_ANALYSIS_TEMPERATURE", 0.1) or getattr(config, "temperature", 0.1) or 0.1), + "response_format": {"type": "json_object"}, + } + log_request_data: dict[str, Any] = { + **request_data, + "messages": [ + {"role": "system", "content": system_prompt}, + log_user_message, + ], + "model_config_id": config.id, + "model_config_name": config.name, + "provider": config.provider, + "analysis_mode": mode, + } + + url = f"{str(config.api_base).rstrip('/')}/chat/completions" + async with httpx.AsyncClient(timeout=_timeout_seconds()) as client: + response = await client.post( + url, + headers={"Authorization": f"Bearer {config.api_key}", "Content-Type": "application/json"}, + json=request_data, + ) + if response.status_code >= 400: + raise RuntimeError(f"视频拆镜分析 API 请求失败: HTTP {response.status_code}: {response.text}") + + raw = response.json() + content = get_message_content_or_raise(raw) + result = parse_model_json(content) + result = fill_none_with_wu(result) + result = ensure_result_schema(result) + result = filter_and_normalize_breakdown(result, mode=mode) + + usage = raw.get("usage") or {} + token_usage = { + "input_tokens": _int_usage(usage.get("prompt_tokens") or usage.get("input_tokens")), + "output_tokens": _int_usage(usage.get("completion_tokens") or usage.get("output_tokens")), + "total_tokens": _int_usage(usage.get("total_tokens")), + "finish_reason": ((raw.get("choices") or [{}])[0] or {}).get("finish_reason"), + "model_config_id": config.id, + "model_config_name": config.name, + "model": config.model_name, + "provider": config.provider, + "video_fps": _video_fps(), + "split_min_seconds": _split_min_seconds(), + "split_max_seconds": _split_max_seconds(), + "analysis_mode": mode, + "log_request": log_request_data, + } + if not token_usage["total_tokens"]: + token_usage["total_tokens"] = token_usage["input_tokens"] + token_usage["output_tokens"] + + db.add( + TokenUsage( + id=generate_id(), + model_config_id=config.id, + user_id=user_id, + input_tokens=token_usage["input_tokens"], + output_tokens=token_usage["output_tokens"], + total_tokens=token_usage["total_tokens"], + ) + ) + await db.flush() + + return ShotVideoAnalysisResult(result=result, raw_response=raw, usage=token_usage) + diff --git a/video-gen-api/app/services/shot_video_split_service.py b/video-gen-api/app/services/shot_video_split_service.py new file mode 100644 index 00000000..bb9edb65 --- /dev/null +++ b/video-gen-api/app/services/shot_video_split_service.py @@ -0,0 +1,173 @@ +from __future__ import annotations + +import asyncio +import os +import subprocess +from dataclasses import dataclass +from pathlib import Path + +from app.config import settings +from app.services.upload_video_asset_service import ( + build_upload_url_from_path, + ensure_shot_segment_dir, + get_ffmpeg_bin, +) + + +@dataclass(slots=True) +class ShotSplitResult: + url: str + path: str + file_size_bytes: int + + +def _date_dir_from_segment_id(segment_id: str) -> str: + # 由调用方更适合按 created_at 传入;这里兜底按当前日期。 + from datetime import datetime + + return datetime.now().strftime("%Y/%m/%d") + + +def _safe_unlink(path: Path) -> None: + try: + if path.exists(): + path.unlink() + except Exception: + pass + + +def split_video_segment( + *, + source_path: str | Path, + segment_id: str, + start_second: float, + end_second: float, + date_dir: str | None = None, +) -> ShotSplitResult: + """使用 ffmpeg 拆出单个视频片段,输出到 storage/uploads/shot_segments。""" + source_path = Path(source_path) + + if not source_path.exists(): + raise RuntimeError(f"ffmpeg 拆镜失败:源视频不存在 {source_path}") + + start = max(float(start_second), 0.0) + end = max(float(end_second), 0.0) + duration = end - start + + if duration <= 0: + raise RuntimeError( + f"ffmpeg 拆镜失败:非法时间范围 start_second={start_second}, end_second={end_second}" + ) + + date_dir = date_dir or _date_dir_from_segment_id(segment_id) + output_dir = ensure_shot_segment_dir(date_dir) + output_path = output_dir / f"{segment_id}.mp4" + + # 注意: + # 不能用 xxx.mp4.part,因为 ffmpeg 会按最后一个扩展名 .part 判断输出格式,导致: + # Unable to choose an output format + # 这里改为 xxx.part.mp4,让 ffmpeg 能识别 mp4 容器。 + part_path = output_dir / f"{segment_id}.part.mp4" + + _safe_unlink(part_path) + + timeout = int(getattr(settings, "SHOT_FFMPEG_TIMEOUT_SECONDS", 120) or 120) + + cmd = [ + get_ffmpeg_bin(), + "-y", + + # 先 seek 到起始秒,再按 duration 切割,避免 -to 在不同 ffmpeg 参数位置下语义不一致。 + "-ss", + f"{start:.3f}", + "-i", + str(source_path), + "-t", + f"{duration:.3f}", + + # 只取主视频流,音频可选,避免 map 0 把字幕/数据流带进去导致 mp4 封装失败。 + "-map", + "0:v:0", + "-map", + "0:a:0?", + + # 当前是拆镜片段,重编码更稳,避免关键帧不准导致片段首尾异常。 + "-c:v", + "libx264", + "-preset", + "veryfast", + "-crf", + "23", + "-pix_fmt", + "yuv420p", + + "-c:a", + "aac", + "-b:a", + "128k", + + "-movflags", + "+faststart", + + # 即使临时文件扩展名未来被改坏,也强制指定 mp4 muxer。 + "-f", + "mp4", + + str(part_path), + ] + + try: + completed = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + timeout=timeout, + check=False, + ) + except subprocess.TimeoutExpired as exc: + _safe_unlink(part_path) + raise RuntimeError( + f"ffmpeg 拆镜超时:timeout={timeout}s, start={start:.3f}, end={end:.3f}" + ) from exc + + if completed.returncode != 0: + _safe_unlink(part_path) + raise RuntimeError( + f"ffmpeg 拆镜失败: {completed.stderr.strip() or completed.stdout.strip()}" + ) + + if not part_path.exists() or part_path.stat().st_size <= 0: + _safe_unlink(part_path) + raise RuntimeError("ffmpeg 拆镜失败:输出文件为空") + + os.replace(part_path, output_path) + + return ShotSplitResult( + url=build_upload_url_from_path(output_path), + path=str(output_path), + file_size_bytes=output_path.stat().st_size, + ) + + +async def split_video_segment_async( + *, + source_path: str | Path, + segment_id: str, + start_second: float, + end_second: float, + date_dir: str | None = None, +) -> ShotSplitResult: + """异步拆镜入口。 + + ffmpeg 本身是同步阻塞命令,不能直接在 Celery 进程内唯一 event loop 中执行。 + 这里通过 asyncio.to_thread 跑同步拆镜函数,避免阻塞 asyncpg / Redis / HTTP 等异步任务。 + """ + return await asyncio.to_thread( + split_video_segment, + source_path=source_path, + segment_id=segment_id, + start_second=start_second, + end_second=end_second, + date_dir=date_dir, + ) \ No newline at end of file diff --git a/video-gen-api/app/services/upload_video_asset_service.py b/video-gen-api/app/services/upload_video_asset_service.py new file mode 100644 index 00000000..cc18ee83 --- /dev/null +++ b/video-gen-api/app/services/upload_video_asset_service.py @@ -0,0 +1,211 @@ +from __future__ import annotations + +import json +import os +import subprocess +from dataclasses import dataclass +from pathlib import Path +from urllib.parse import urlparse + +from fastapi import HTTPException + +from app.config import settings + +VIDEO_EXTENSIONS = {".mp4", ".mov", ".m4v", ".webm", ".avi", ".mkv"} + + +@dataclass(slots=True) +class UploadVideoAsset: + url: str + path: Path + duration_seconds: float + + +def _project_root() -> Path: + return Path.cwd() + + +def _abs_path(value: str | Path) -> Path: + path = Path(value) + if not path.is_absolute(): + path = _project_root() / path + return path.resolve() + + +def upload_root() -> Path: + return _abs_path(settings.UPLOAD_LOCAL_PATH) + + +def shot_segment_root() -> Path: + return _abs_path(getattr(settings, "SHOT_SEGMENT_LOCAL_PATH", "./storage/uploads/shot_segments")) + + +def _strip_base_url(url: str) -> str: + text = str(url or "").strip() + if not text: + return text + + base_url = str(getattr(settings, "BASE_URL", "") or "").strip().rstrip("/") + if base_url and text.startswith(base_url + "/"): + return text[len(base_url):] + + parsed = urlparse(text) + if parsed.scheme in ("http", "https"): + # 只接受本系统 BASE_URL 下的上传资源;外部 URL 不允许 ffmpeg 本地切片。 + raise HTTPException(status_code=400, detail="拆镜源视频必须来自本系统上传接口,不能传外部 http/https URL") + + return text + + +def _safe_relative_from_upload_url(url: str) -> str: + value = _strip_base_url(url) + value = value.split("?", 1)[0].split("#", 1)[0] + + if value.startswith("/uploads/"): + rel = value.replace("/uploads/", "", 1) + elif value.startswith("uploads/"): + rel = value.replace("uploads/", "", 1) + else: + raise HTTPException(status_code=400, detail="拆镜源视频链接必须是 /uploads/ 下的上传资源") + + rel = rel.lstrip("/") + if not rel or ".." in Path(rel).parts: + raise HTTPException(status_code=400, detail="上传视频路径非法") + return rel + + +def resolve_upload_video_path(video_url: str) -> Path: + rel = _safe_relative_from_upload_url(video_url) + root = upload_root() + path = (root / rel).resolve() + + try: + path.relative_to(root) + except ValueError as exc: + raise HTTPException(status_code=400, detail="上传视频路径越界") from exc + + if path.suffix.lower() not in VIDEO_EXTENSIONS: + raise HTTPException(status_code=400, detail="上传资源不是支持的视频格式") + if not path.exists() or not path.is_file(): + raise HTTPException(status_code=404, detail=f"上传视频文件不存在: {video_url}") + return path + + +def build_upload_url_from_path(path: str | Path) -> str: + root = upload_root() + checked_path = _abs_path(path) + try: + rel = checked_path.relative_to(root).as_posix() + except ValueError as exc: + raise HTTPException(status_code=500, detail="生成上传资源 URL 失败:路径不在 uploads 目录下") from exc + return f"/uploads/{rel}" + + +def get_ffmpeg_bin() -> str: + return str(getattr(settings, "FFMPEG_BIN", "") or "ffmpeg") + + +def get_ffprobe_bin() -> str: + configured = str(getattr(settings, "FFPROBE_BIN", "") or "").strip() + if configured: + return configured + ffmpeg_bin = get_ffmpeg_bin() + if ffmpeg_bin.endswith("ffmpeg.exe"): + return ffmpeg_bin[:-10] + "ffprobe.exe" + if ffmpeg_bin.endswith("ffmpeg"): + return ffmpeg_bin[:-6] + "ffprobe" + return "ffprobe" + + +def probe_video_duration_seconds(video_path: str | Path) -> float: + path = _abs_path(video_path) + timeout = int(getattr(settings, "SHOT_FFPROBE_TIMEOUT_SECONDS", 20) or 20) + cmd = [ + get_ffprobe_bin(), + "-v", "error", + "-show_entries", "format=duration", + "-of", "json", + str(path), + ] + try: + completed = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + timeout=timeout, + check=False, + ) + except Exception as exc: + raise HTTPException(status_code=400, detail=f"ffprobe 获取视频时长失败: {exc}") from exc + + if completed.returncode != 0: + raise HTTPException(status_code=400, detail=f"ffprobe 获取视频时长失败: {completed.stderr.strip()}") + + try: + data = json.loads(completed.stdout or "{}") + duration = float((data.get("format") or {}).get("duration") or 0) + except Exception as exc: + raise HTTPException(status_code=400, detail="ffprobe 返回的视频时长无法解析") from exc + + if duration <= 0: + raise HTTPException(status_code=400, detail="视频时长无效") + return round(duration, 3) + + +def validate_upload_video_asset(video_url: str, frontend_duration_seconds: float | None = None) -> UploadVideoAsset: + path = resolve_upload_video_path(video_url) + real_duration = probe_video_duration_seconds(path) + + if frontend_duration_seconds is not None and frontend_duration_seconds > 0: + tolerance = float(getattr(settings, "SHOT_DURATION_TOLERANCE_SECONDS", 1.0) or 1.0) + # 超出误差时以后端 ffprobe 为准,不拒绝,避免前端浮点或浏览器 metadata 偏差导致创建失败。 + if abs(float(frontend_duration_seconds) - real_duration) <= tolerance: + real_duration = round(float(frontend_duration_seconds), 3) + + return UploadVideoAsset(url=_strip_base_url(video_url), path=path, duration_seconds=real_duration) + + +def validate_split_range(*, start_second: float, end_second: float, video_duration_seconds: float) -> tuple[float, float, float]: + start = round(float(start_second), 3) + end = round(float(end_second), 3) + + if start < 0: + raise HTTPException(status_code=400, detail="开始秒不能小于0") + if end <= start: + raise HTTPException(status_code=400, detail="结束秒必须大于开始秒") + + tolerance = float(getattr(settings, "SHOT_SPLIT_END_TOLERANCE_SECONDS", 0.5) or 0.5) + if end > float(video_duration_seconds) + tolerance: + raise HTTPException(status_code=400, detail="结束秒不能超过视频总时长") + + duration = round(end - start, 3) + min_seconds = float(getattr(settings, "SHOT_SPLIT_MIN_SECONDS", 1) or 1) + max_seconds = float(getattr(settings, "SHOT_SPLIT_MAX_SECONDS", 120) or 120) + if duration < min_seconds: + raise HTTPException(status_code=400, detail=f"拆镜片段不能低于 {min_seconds:g} 秒") + if duration > max_seconds: + raise HTTPException(status_code=400, detail=f"拆镜片段不能超过 {max_seconds:g} 秒") + return start, end, duration + + +def format_second(value: float) -> int | float: + checked = float(value) + if checked.is_integer(): + return int(checked) + return round(checked, 2) + + +def build_time_node(start_second: float, end_second: float) -> str: + return f"{format_second(start_second)}-{format_second(end_second)}秒" + + +def ensure_shot_segment_dir(date_dir: str) -> Path: + root = shot_segment_root() + output_dir = (root / date_dir).resolve() + try: + output_dir.relative_to(root) + except ValueError as exc: + raise RuntimeError("拆镜输出目录越界") from exc + os.makedirs(output_dir, exist_ok=True) + return output_dir diff --git a/video-gen-api/app/tasks/__init__.py b/video-gen-api/app/tasks/__init__.py index 1685b2f7..03e34c2c 100644 --- a/video-gen-api/app/tasks/__init__.py +++ b/video-gen-api/app/tasks/__init__.py @@ -10,7 +10,9 @@ try: generation_poll_tasks, generation_download_tasks, generation_recovery_tasks, - hot_opening_replicate_tasks + hot_opening_replicate_tasks, + shot_replicate_tasks, + shot_replicate_flow_tasks ) except Exception: pass diff --git a/video-gen-api/app/tasks/async_runner.py b/video-gen-api/app/tasks/async_runner.py index 7d6f1df8..24964dad 100644 --- a/video-gen-api/app/tasks/async_runner.py +++ b/video-gen-api/app/tasks/async_runner.py @@ -1,23 +1,35 @@ +from __future__ import annotations + import asyncio import os import threading +from concurrent.futures import Future from typing import Awaitable, TypeVar +from app.config import settings + T = TypeVar("T") _thread_local = threading.local() +_single_loop_lock = threading.RLock() +_single_loop: asyncio.AbstractEventLoop | None = None +_single_loop_thread: threading.Thread | None = None +_single_loop_pid: int | None = None +_single_loop_ready: threading.Event | None = None -def _get_or_create_loop() -> asyncio.AbstractEventLoop: - """ - 给当前进程/线程维护一个长期 event loop。 +def _runner_mode() -> str: + mode = str(getattr(settings, "CELERY_ASYNC_RUNNER_MODE", "single_loop") or "single_loop").strip().lower() + if mode not in {"single_loop", "direct"}: + return "single_loop" + return mode - Linux prefork: - 每个 Celery 子进程通常单线程跑任务,这里相当于每个子进程一个长期 loop。 - Windows -P threads: - 每个线程一个 loop,但注意 asyncpg pool 仍不适合跨线程共享; - Windows threads 模式建议继续用 NullPool 或只做本地调试。 +def _get_or_create_thread_local_loop() -> asyncio.AbstractEventLoop: + """兼容旧方案:当前线程持有一个长期 event loop。 + + 仅作为降级模式使用。长期推荐 single_loop,避免 Windows threads 下 + 多线程 event loop 复用 asyncpg / redis.asyncio 连接对象。 """ pid = os.getpid() loop = getattr(_thread_local, "loop", None) @@ -31,18 +43,104 @@ def _get_or_create_loop() -> asyncio.AbstractEventLoop: return loop +def _single_loop_worker(loop: asyncio.AbstractEventLoop, ready: threading.Event) -> None: + asyncio.set_event_loop(loop) + ready.set() + loop.run_forever() + + pending = [task for task in asyncio.all_tasks(loop) if not task.done()] + if pending: + for task in pending: + task.cancel() + loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True)) + + loop.run_until_complete(loop.shutdown_asyncgens()) + loop.close() + + +def _get_or_create_single_loop() -> asyncio.AbstractEventLoop: + """获取当前 Celery 进程内唯一 async event loop。 + + Linux prefork:每个 Celery 子进程各自一个 loop。 + Windows threads:同一 worker 进程内所有任务线程共享同一个 loop。 + """ + global _single_loop, _single_loop_thread, _single_loop_pid, _single_loop_ready + + pid = os.getpid() + with _single_loop_lock: + if ( + _single_loop is not None + and not _single_loop.is_closed() + and _single_loop_thread is not None + and _single_loop_thread.is_alive() + and _single_loop_pid == pid + ): + return _single_loop + + # fork 后 pid 变化,必须丢弃父进程状态,重新创建子进程自己的 loop。 + _single_loop = asyncio.new_event_loop() + _single_loop_pid = pid + _single_loop_ready = threading.Event() + _single_loop_thread = threading.Thread( + target=_single_loop_worker, + args=(_single_loop, _single_loop_ready), + name=f"celery-async-runner-{pid}", + daemon=True, + ) + _single_loop_thread.start() + _single_loop_ready.wait(timeout=5) + return _single_loop + + def run_async(coro: Awaitable[T]) -> T: + """Celery 同步 task 调用异步协程的统一入口。 + + 默认 single_loop 模式: + - 一个 Celery 子进程只有一个专用 event loop; + - 所有 asyncpg / redis.asyncio 操作都在这个 loop 内创建和使用; + - 避免 got Future attached to a different loop。 + + 降级 direct 模式: + - 兼容旧的线程本地 loop 方案; + - 如果使用 direct,建议同时开启 CELERY_DB_USE_NULLPOOL=true。 """ - Celery 同步 task 调用异步协程的统一入口。 - 不使用 asyncio.run(),避免每个 task 结束时关闭 event loop。 - """ - loop = _get_or_create_loop() - return loop.run_until_complete(coro) + if _runner_mode() == "direct": + loop = _get_or_create_thread_local_loop() + return loop.run_until_complete(coro) + + loop = _get_or_create_single_loop() + try: + running_loop = asyncio.get_running_loop() + except RuntimeError: + running_loop = None + + if running_loop is loop: + raise RuntimeError("run_async() 不能在 Celery async_runner 的事件循环内部被同步调用") + + future: Future[T] = asyncio.run_coroutine_threadsafe(coro, loop) + return future.result() def close_loop() -> None: + """关闭当前进程内 async runner loop。""" + global _single_loop, _single_loop_thread, _single_loop_pid, _single_loop_ready + + # 关闭 single_loop。 + with _single_loop_lock: + loop = _single_loop + thread = _single_loop_thread + if loop is not None and not loop.is_closed() and thread is not None and thread.is_alive(): + loop.call_soon_threadsafe(loop.stop) + thread.join(timeout=5) + + _single_loop = None + _single_loop_thread = None + _single_loop_pid = None + _single_loop_ready = None + + # 关闭 direct 降级模式的线程本地 loop。 loop = getattr(_thread_local, "loop", None) if loop is not None and not loop.is_closed(): loop.close() _thread_local.loop = None - _thread_local.pid = None \ No newline at end of file + _thread_local.pid = None diff --git a/video-gen-api/app/tasks/celery_app.py b/video-gen-api/app/tasks/celery_app.py index ed4b567d..2f4e0f38 100644 --- a/video-gen-api/app/tasks/celery_app.py +++ b/video-gen-api/app/tasks/celery_app.py @@ -51,6 +51,12 @@ if broker_url: "generation.download_generation_result_task": {"queue": "gen_result_download"}, "hot_opening.start_image_prompt_optimize": {"queue": "gen_chatapi_create"}, "hot_opening.start_video_prompt_optimize": {"queue": "gen_chatapi_create"}, + "shot_replicate.analyze_original_video": {"queue": "gen_chatapi_create"}, + "shot_replicate.analyze_custom_segment_video": {"queue": "gen_chatapi_create"}, + "shot_replicate.split_one_segment": {"queue": "gen_result_download"}, + "shot_replicate.start_image_prompt_optimize": {"queue": "gen_chatapi_create"}, + "shot_replicate.start_video_prompt_optimize": {"queue": "gen_chatapi_create"}, + "shot_replicate.recover_split_tasks_once": {"queue": "gen_result_download"}, "generation.recover_download_tasks_once": {"queue": "gen_result_download"}, "generation.recover_generation_tasks_once": {"queue": "gen_result_download"}, "app.tasks.cleanup.*": {"queue": "default"}, @@ -61,6 +67,18 @@ else: celery_app = None +async def _try_acquire_startup_recovery_lock() -> bool: + """任意 worker 启动时都可尝试抢恢复锁,避免依赖 hostname 命名。""" + from app.services.redis_registry_service import redis_acquire_lock + + token = await redis_acquire_lock( + lock_key=settings.CELERY_STARTUP_RECOVERY_LOCK_KEY, + ttl_seconds=int(settings.CELERY_STARTUP_RECOVERY_LOCK_TTL_SECONDS or 120), + log_context="celery_startup_recovery", + ) + return bool(token) + + @worker_ready.connect def on_worker_ready(sender=None, **kwargs): """Celery worker 启动时做一次容灾恢复。 @@ -68,13 +86,21 @@ def on_worker_ready(sender=None, **kwargs): 注意: - 不启用 Celery beat。 - 不要求新增第四条启动命令。 - - 只让 gen_result_download worker 投递恢复任务,避免三个 worker 同时重复扫描。 + - 不再依赖 worker hostname 是否包含 gen_result_download。 + - 所有 worker 都尝试抢 Redis 锁,只有抢到锁的 worker 投递恢复任务。 """ if celery_app is None: return + if not bool(getattr(settings, "CELERY_STARTUP_RECOVERY_ENABLED", True)): + logger.info("启动容灾恢复已关闭。CELERY_STARTUP_RECOVERY_ENABLED=false") + return - hostname = str(getattr(sender, "hostname", "") or "") - if "gen_result_download" not in hostname: + try: + if not run_async(_try_acquire_startup_recovery_lock()): + return + except Exception: + # Redis 不可用时不阻塞 worker 启动,避免影响稳定生成链路。 + logger.exception("启动容灾恢复锁获取失败,已跳过本次自动恢复投递") return try: @@ -82,6 +108,7 @@ def on_worker_ready(sender=None, **kwargs): recover_download_tasks_once, recover_generation_tasks_once, ) + from app.tasks.shot_replicate_tasks import recover_split_tasks_once countdown = max(0, int(settings.DOWNLOAD_RECOVERY_STARTUP_DELAY_SECONDS or 0)) @@ -95,6 +122,13 @@ def on_worker_ready(sender=None, **kwargs): queue="gen_result_download", priority=settings.DOWNLOAD_TASK_PRIORITY_RECOVER, ) + recover_split_tasks_once.apply_async( + countdown=countdown + 10, + queue="gen_result_download", + priority=settings.DOWNLOAD_TASK_PRIORITY_RECOVER, + ) + + logger.info("启动容灾恢复任务已投递。countdown=%s", countdown) except Exception: logger.exception("启动容灾恢复任务投递失败") @@ -110,14 +144,14 @@ def on_worker_process_init(**kwargs): @worker_process_shutdown.connect def on_worker_process_shutdown(**kwargs): - """子进程退出前关闭连接池和 event loop。""" + """子进程退出前关闭连接池、Redis 注册表连接和 event loop。""" try: run_async(engine.dispose()) except Exception: pass try: - from app.services.celery_download_recovery_service import close_registry_redis + from app.services.redis_registry_service import close_registry_redis run_async(close_registry_redis()) except Exception: diff --git a/video-gen-api/app/tasks/generation_create_tasks.py b/video-gen-api/app/tasks/generation_create_tasks.py index 29a5d44e..08eea6e7 100644 --- a/video-gen-api/app/tasks/generation_create_tasks.py +++ b/video-gen-api/app/tasks/generation_create_tasks.py @@ -11,9 +11,10 @@ from app.services.error_codes import extract_error_message from app.services.generation_log_service import log_task_event from app.services.generation_refund_service import mark_chat_generation_task_failed_and_refund_once from app.services.generation_provider_service import create_provider_task +from app.services.redis_registry_service import ensure_aware_utc from app.tasks.celery_app import celery_app -ALLOWED_GENERATION_MODES = {"chatapi_async", "hot_opening_replicate"} +ALLOWED_GENERATION_MODES = {"chatapi_async", "hot_opening_replicate", "shot_replicate"} def _get_first_value(obj: Any, *field_names: str) -> Optional[Any]: @@ -69,7 +70,7 @@ def _build_optimized_prompt_by_params(task: ChatGenerationTask) -> str: # 爆款开头复刻第5步的视频生成,original_prompt 已经是视频提词 JSON schema。 # 不能再追加“时长/比例/分辨率”中文参数,否则会污染 schema。 - if generation_mode == "hot_opening_replicate" and gen_type == "video": + if generation_mode in {"hot_opening_replicate", "shot_replicate"} and gen_type == "video": stripped = base_prompt.strip() if stripped.startswith("{") or stripped.startswith("["): return base_prompt @@ -129,7 +130,8 @@ async def _run(task_id: str): if task.status != "generating": return - if task.deadline_at and datetime.now(timezone.utc) > task.deadline_at: + deadline_at = ensure_aware_utc(task.deadline_at) + if deadline_at and datetime.now(timezone.utc) > deadline_at: await mark_chat_generation_task_failed_and_refund_once( db, task=task, @@ -242,7 +244,11 @@ async def _run(task_id: str): else: from app.tasks.generation_poll_tasks import poll_generation_task - poll_generation_task.delay(task.id) + poll_generation_task.apply_async( + args=[task.id], + queue="gen_provider_poll", + countdown=0, + ) except Exception as exc: try: diff --git a/video-gen-api/app/tasks/generation_download_tasks.py b/video-gen-api/app/tasks/generation_download_tasks.py index 3a27f659..df9a9c54 100644 --- a/video-gen-api/app/tasks/generation_download_tasks.py +++ b/video-gen-api/app/tasks/generation_download_tasks.py @@ -21,7 +21,7 @@ from app.services.generation_refund_service import mark_chat_generation_task_fai from app.services.resource_accounting_service import record_chat_task_generated_resource from app.tasks.celery_app import celery_app -ALLOWED_GENERATION_MODES = {"chatapi_async", "hot_opening_replicate"} +ALLOWED_GENERATION_MODES = {"chatapi_async", "hot_opening_replicate", "shot_replicate"} DOWNLOAD_QUEUE = "gen_result_download" DOWNLOAD_STAGE_QUEUED = "download_queued" diff --git a/video-gen-api/app/tasks/generation_poll_tasks.py b/video-gen-api/app/tasks/generation_poll_tasks.py index 08835b8b..ffc585f2 100644 --- a/video-gen-api/app/tasks/generation_poll_tasks.py +++ b/video-gen-api/app/tasks/generation_poll_tasks.py @@ -1,6 +1,7 @@ from app.tasks.async_runner import run_async import json -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone +from typing import Any from sqlalchemy import select @@ -11,9 +12,21 @@ from app.services.error_codes import extract_error_message from app.services.generation_log_service import log_task_event, log_provider_call from app.services.generation_refund_service import mark_chat_generation_task_failed_and_refund_once from app.services.generation_provider_service import poll_provider_task +from app.services.redis_registry_service import ( + datetime_to_epoch, + ensure_aware_utc, + redis_remove_registry_item, + redis_upsert_registry_item, + utc_now, +) from app.tasks.celery_app import celery_app -ALLOWED_GENERATION_MODES = {"chatapi_async", "hot_opening_replicate"} +ALLOWED_GENERATION_MODES = {"chatapi_async", "hot_opening_replicate", "shot_replicate"} +POLL_QUEUE = "gen_provider_poll" + + +def _now() -> datetime: + return datetime.now(timezone.utc) def _is_success(status: str) -> bool: @@ -31,6 +44,86 @@ def _engine_snapshot(task: ChatGenerationTask) -> dict: return {} +def _deadline_expired(task: ChatGenerationTask, now: datetime | None = None) -> bool: + deadline_at = ensure_aware_utc(task.deadline_at) + return bool(deadline_at and deadline_at <= (now or _now())) + + +def _poll_check_at(*, delay_seconds: int | float | None = None, now: datetime | None = None) -> datetime: + current_time = now or _now() + delay = int(delay_seconds or settings.CHATAPI_ASYNC_POLL_INTERVAL_SECONDS or 30) + grace = int(settings.POLL_TASK_QUEUE_TIMEOUT_SECONDS or 120) + return current_time + timedelta(seconds=max(1, delay) + max(0, grace)) + + +def _poll_lease_until(now: datetime | None = None) -> datetime: + current_time = now or _now() + return current_time + timedelta(seconds=int(settings.POLL_TASK_LEASE_SECONDS or 300)) + + +def _build_poll_active_payload( + task: ChatGenerationTask, + *, + stage: str, + reason: str, + next_poll_at: datetime | None = None, + check_at: datetime | None = None, +) -> dict[str, Any]: + current_time = utc_now() + checked_next_poll_at = ensure_aware_utc(next_poll_at) + checked_check_at = ensure_aware_utc(check_at) + return { + "task_id": task.id, + "provider_task_id": task.provider_task_id, + "seedance_task_id": task.seedance_task_id, + "generation_mode": task.generation_mode, + "gen_type": task.gen_type, + "stage": stage, + "queue": POLL_QUEUE, + "poll_count": int(task.poll_count or 0), + "retry_count": int(task.retry_count or 0), + "last_poll_at": datetime_to_epoch(task.last_poll_at) if task.last_poll_at else None, + "next_poll_at": datetime_to_epoch(checked_next_poll_at) if checked_next_poll_at else None, + "deadline_at": datetime_to_epoch(task.deadline_at) if task.deadline_at else None, + "check_at": datetime_to_epoch(checked_check_at) if checked_check_at else None, + "updated_at": datetime_to_epoch(current_time), + "reason": reason, + } + + +async def register_poll_active( + task: ChatGenerationTask, + *, + check_at: datetime, + reason: str, + next_poll_at: datetime | None = None, +) -> None: + payload = _build_poll_active_payload( + task, + stage=task.pipeline_stage or "", + reason=reason, + next_poll_at=next_poll_at, + check_at=check_at, + ) + await redis_upsert_registry_item( + hash_key=settings.POLL_ACTIVE_REDIS_HASH_KEY, + zset_key=settings.POLL_ACTIVE_REDIS_ZSET_KEY, + item_id=task.id, + payload=payload, + check_at=check_at, + log_context="poll_active", + ) + + +async def remove_poll_active(task_id: str) -> None: + await redis_remove_registry_item( + hash_key=settings.POLL_ACTIVE_REDIS_HASH_KEY, + zset_key=settings.POLL_ACTIVE_REDIS_ZSET_KEY, + item_id=task_id, + log_context="poll_active", + ) + + async def _notify_finished(db, task: ChatGenerationTask) -> None: from app.services.generation_module_hook_service import notify_chat_generation_task_finished @@ -55,6 +148,32 @@ async def _reload_task(db, task_id: str) -> ChatGenerationTask | None: return result.scalar_one_or_none() +async def _mark_timeout(db, task: ChatGenerationTask, *, message: str = "任务轮询超时") -> None: + await mark_chat_generation_task_failed_and_refund_once( + db, + task=task, + error_message=message, + pipeline_stage="timeout", + ) + await _notify_finished(db, task) + await db.commit() + await remove_poll_active(task.id) + await log_task_event(task, event_type="TASK_TIMEOUT", to_status="failed", to_stage="timeout") + + +async def _mark_failed(db, task: ChatGenerationTask, *, message: str, detail: Any = None) -> None: + await mark_chat_generation_task_failed_and_refund_once( + db, + task=task, + error_message=message, + pipeline_stage="failed", + ) + await _notify_finished(db, task) + await db.commit() + await remove_poll_active(task.id) + await log_task_event(task, event_type="POLL_FAILED", message=task.error_message, detail=detail) + + async def _run(task_id: str): async with async_session() as db: result = await db.execute(select(ChatGenerationTask).where( @@ -62,43 +181,37 @@ async def _run(task_id: str): ChatGenerationTask.deleted_at.is_(None), ).with_for_update().limit(1)) task = result.scalar_one_or_none() - if not task or task.generation_mode not in ALLOWED_GENERATION_MODES: + if not task: + await remove_poll_active(task_id) + return + if task.generation_mode not in ALLOWED_GENERATION_MODES: + await remove_poll_active(task.id) return # 只处理正在生成,且处于远程等待/轮询中的任务。 if task.status != "generating" or task.pipeline_stage not in ("waiting_remote", "polling"): + await remove_poll_active(task.id) return - if task.deadline_at and datetime.now(timezone.utc) > task.deadline_at: - await mark_chat_generation_task_failed_and_refund_once( - db, - task=task, - error_message="任务轮询超时", - pipeline_stage="timeout", - ) - await _notify_finished(db, task) - await db.commit() - await log_task_event(task, event_type="TASK_TIMEOUT", to_status="failed", to_stage="timeout") + if _deadline_expired(task): + await _mark_timeout(db, task, message="任务轮询超时") return if not (task.seedance_task_id or task.provider_task_id): - await mark_chat_generation_task_failed_and_refund_once( - db, - task=task, - error_message="缺少外部任务ID", - pipeline_stage="failed", - ) - await _notify_finished(db, task) - await db.commit() - await log_task_event(task, event_type="POLL_FAILED", message=task.error_message) + await _mark_failed(db, task, message="缺少外部任务ID") return - # 标记本次正在轮询。 - # 注意:pending 后会再改回 waiting_remote,避免任务长期卡在 polling。 + # 标记本次正在轮询,并登记 poll lease。 + # 如果 worker 在供应商接口调用过程中退出,启动恢复会在 lease 过期后重新投递。 task.pipeline_stage = "polling" task.poll_count = (task.poll_count or 0) + 1 - task.last_poll_at = datetime.now(timezone.utc) + task.last_poll_at = _now() await db.commit() + await register_poll_active( + task, + check_at=_poll_lease_until(task.last_poll_at), + reason="polling_lease", + ) try: poll_result = await poll_provider_task(db, task) @@ -134,20 +247,13 @@ async def _run(task_id: str): task.provider_response_json = response_data if not task.remote_result_url: - await mark_chat_generation_task_failed_and_refund_once( - db, - task=task, - error_message="供应商任务成功但未返回结果URL", - pipeline_stage="failed", - ) - await _notify_finished(db, task) - await db.commit() - await log_task_event(task, event_type="POLL_FAILED", message=task.error_message) + await _mark_failed(db, task, message="供应商任务成功但未返回结果URL", detail=poll_result) return task.pipeline_stage = "result_ready" task.retry_count = 0 await db.commit() + await remove_poll_active(task.id) await log_task_event(task, event_type="POLL_SUCCESS", to_stage="result_ready") @@ -158,34 +264,38 @@ async def _run(task_id: str): if _is_failed(status): task.provider_response_json = response_data - await mark_chat_generation_task_failed_and_refund_once( + await _mark_failed( db, - task=task, - error_message=poll_result.get("error") or f"供应商任务失败: {status}", - pipeline_stage="failed", + task, + message=poll_result.get("error") or f"供应商任务失败: {status}", + detail=poll_result, ) - await _notify_finished(db, task) - await db.commit() - await log_task_event(task, event_type="POLL_FAILED", message=task.error_message, detail=poll_result) return - # 关键修改 1: # 供应商仍在 pending / running 时,把阶段从 polling 改回 waiting_remote。 - # 这样数据库状态表示“等待下一次轮询”,不会长期停在 polling。 - # 同时可以降低重复 Celery 消息形成多条轮询链的概率。 + # 同时登记下一次 poll active,Celery countdown 丢失时可由恢复任务拉起。 task.pipeline_stage = "waiting_remote" task.retry_count = 0 await db.commit() await log_task_event(task, event_type="POLL_PENDING", message=f"status={status}") + delay_seconds = int(settings.CHATAPI_ASYNC_POLL_INTERVAL_SECONDS or 30) + next_poll_at = _now() + timedelta(seconds=max(1, delay_seconds)) + await register_poll_active( + task, + check_at=_poll_check_at(delay_seconds=delay_seconds), + next_poll_at=next_poll_at, + reason="poll_pending_next", + ) + poll_generation_task.apply_async( args=[task.id], - countdown=settings.CHATAPI_ASYNC_POLL_INTERVAL_SECONDS, + queue=POLL_QUEUE, + countdown=delay_seconds, ) except Exception as exc: - # 关键修改 2: # 异常后先 rollback,再重新查询 task,不继续使用 rollback 前的旧 ORM 对象。 try: await db.rollback() @@ -194,30 +304,33 @@ async def _run(task_id: str): task = await _reload_task(db, task_id) if not task: + await remove_poll_active(task_id) return task.retry_count = (task.retry_count or 0) + 1 if task.retry_count > settings.CHATAPI_ASYNC_MAX_RETRIES: error_message = extract_error_message(exc, "轮询") if callable(extract_error_message) else str(exc) - await mark_chat_generation_task_failed_and_refund_once( - db, - task=task, - error_message=error_message, - pipeline_stage="failed", - ) - await _notify_finished(db, task) - await db.commit() - await log_task_event(task, event_type="POLL_FAILED", message=task.error_message) + await _mark_failed(db, task, message=error_message) else: # 临时轮询异常时,不让任务停在 polling。 # 回到 waiting_remote,等待下一次重试轮询。 task.pipeline_stage = "waiting_remote" await db.commit() + delay_seconds = int(settings.CHATAPI_ASYNC_RETRY_BACKOFF_SECONDS or 30) * int(task.retry_count or 1) + next_poll_at = _now() + timedelta(seconds=max(1, delay_seconds)) + await register_poll_active( + task, + check_at=_poll_check_at(delay_seconds=delay_seconds), + next_poll_at=next_poll_at, + reason="poll_exception_retry", + ) + poll_generation_task.apply_async( args=[task.id], - countdown=settings.CHATAPI_ASYNC_RETRY_BACKOFF_SECONDS * task.retry_count, + queue=POLL_QUEUE, + countdown=delay_seconds, ) @@ -233,4 +346,4 @@ else: def apply_async(self, *args, **kwargs): raise RuntimeError("Celery is disabled") - poll_generation_task = _DisabledTask() \ No newline at end of file + poll_generation_task = _DisabledTask() diff --git a/video-gen-api/app/tasks/shot_replicate_flow_tasks.py b/video-gen-api/app/tasks/shot_replicate_flow_tasks.py new file mode 100644 index 00000000..2d574458 --- /dev/null +++ b/video-gen-api/app/tasks/shot_replicate_flow_tasks.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from app.models.base import async_session +from app.services.shot_replicate_flow_service import run_image_prompt_optimize, run_video_prompt_optimize +from app.tasks.async_runner import run_async +from app.tasks.celery_app import celery_app + + +async def _run_image_prompt(project_id: str, step_id: str | None = None): + async with async_session() as db: + await run_image_prompt_optimize(db, project_id=project_id, step_id=step_id) + await db.commit() + + +async def _run_video_prompt(project_id: str, step_id: str | None = None): + async with async_session() as db: + await run_video_prompt_optimize(db, project_id=project_id, step_id=step_id) + await db.commit() + + +if celery_app: + + @celery_app.task(name="shot_replicate.start_image_prompt_optimize", bind=True, max_retries=3, default_retry_delay=30) + def start_image_prompt_optimize(self, project_id: str, step_id: str | None = None): + return run_async(_run_image_prompt(project_id, step_id)) + + + @celery_app.task(name="shot_replicate.start_video_prompt_optimize", bind=True, max_retries=3, default_retry_delay=30) + def start_video_prompt_optimize(self, project_id: str, step_id: str | None = None): + return run_async(_run_video_prompt(project_id, step_id)) + +else: + + class _DisabledTask: + def delay(self, *args, **kwargs): + raise RuntimeError("Celery is disabled") + + def apply_async(self, *args, **kwargs): + raise RuntimeError("Celery is disabled") + + start_image_prompt_optimize = _DisabledTask() + start_video_prompt_optimize = _DisabledTask() diff --git a/video-gen-api/app/tasks/shot_replicate_tasks.py b/video-gen-api/app/tasks/shot_replicate_tasks.py new file mode 100644 index 00000000..6b98b29e --- /dev/null +++ b/video-gen-api/app/tasks/shot_replicate_tasks.py @@ -0,0 +1,505 @@ +from __future__ import annotations + +import logging +from datetime import datetime, timedelta, timezone +from typing import Any + +from sqlalchemy import select + +from app.config import settings +from app.enums.shot_replicate import ( + ModuleCodeEnum, + ShotAnalysisStatusEnum, + ShotSegmentAnalysisStatusEnum, + ShotSegmentSourceModeEnum, + ShotSplitStatusEnum, + ShotTaskSetStatusEnum, +) +from app.models.base import async_session +from app.models.shot_replicate_segment import ShotReplicateSegment +from app.models.shot_replicate_task_set import ShotReplicateTaskSet +from app.services.module_generation_log_service import log_module_error, log_module_event_file, log_module_prompt_event +from app.services.redis_registry_service import redis_acquire_lock, redis_release_lock +from app.services.shot_replicate_taskset_service import refresh_task_set_split_summary +from app.services.shot_video_analysis_service import analyze_video_for_shot_split +from app.services.shot_video_split_service import split_video_segment_async +from app.services.upload_video_asset_service import validate_split_range +from app.tasks.async_runner import run_async +from app.tasks.celery_app import celery_app + +logger = logging.getLogger("video_gen") + +MODULE = ModuleCodeEnum.SHOT_REPLICATE.value +SPLIT_QUEUE = "gen_result_download" +ANALYSIS_QUEUE = "gen_chatapi_create" + + +def _now() -> datetime: + return datetime.now(timezone.utc) + + +def _lease_until(now: datetime | None = None) -> datetime: + return (now or _now()) + timedelta(seconds=int(settings.SHOT_SPLIT_LEASE_SECONDS or 600)) + + +def _retry_at(attempt: int, now: datetime | None = None) -> datetime: + base = int(settings.SHOT_SPLIT_RETRY_BACKOFF_SECONDS or settings.DOWNLOAD_TASK_RETRY_BACKOFF_SECONDS or 30) + return (now or _now()) + timedelta(seconds=max(1, base * max(1, attempt))) + + +async def _acquire_split_semaphore(segment_id: str) -> str | None: + """简单 Redis 并发闸门:用固定槽位锁限制 ffmpeg 同时运行数量。""" + max_concurrent = max(1, int(settings.SHOT_SPLIT_MAX_CONCURRENT or 1)) + ttl = int(settings.SHOT_SPLIT_LEASE_SECONDS or 600) + for slot in range(max_concurrent): + key = f"{settings.SHOT_SPLIT_SEMAPHORE_KEY_PREFIX}:{slot}" + token = await redis_acquire_lock(lock_key=key, ttl_seconds=ttl, token=segment_id, log_context="shot_split_semaphore") + if token: + return key + return None + + +async def _release_split_semaphore(lock_key: str | None, segment_id: str) -> None: + if lock_key: + await redis_release_lock(lock_key=lock_key, token=segment_id, log_context="shot_split_semaphore") + + +async def _run_analyze_original_video(task_set_id: str) -> None: + task_set_user_id: str | None = None + video_url: str | None = None + try: + async with async_session() as db: + result = await db.execute( + select(ShotReplicateTaskSet) + .where(ShotReplicateTaskSet.id == task_set_id, ShotReplicateTaskSet.deleted_at.is_(None)) + .with_for_update() + .limit(1) + ) + task_set = result.scalar_one_or_none() + if not task_set: + return + if task_set.analysis_status == ShotAnalysisStatusEnum.COMPLETED.value: + return + task_set_user_id = task_set.user_id + video_url = task_set.video_url + task_set.status = ShotTaskSetStatusEnum.ANALYZING.value + task_set.analysis_status = ShotAnalysisStatusEnum.PROCESSING.value + task_set.analysis_error_message = None + await db.commit() + + log_module_event_file( + module=MODULE, + event_type="SHOT_ANALYSIS_STARTED", + project_id=task_set_id, + user_id=task_set_user_id, + message="原视频拆镜分析开始", + detail={"task_set_id": task_set_id, "video_url": video_url, "analysis_mode": "full_breakdown"}, + ) + + async with async_session() as db: + analyzed = await analyze_video_for_shot_split(db, video_url or "", user_id=task_set_user_id, mode="full_breakdown") + result = await db.execute( + select(ShotReplicateTaskSet) + .where(ShotReplicateTaskSet.id == task_set_id, ShotReplicateTaskSet.deleted_at.is_(None)) + .with_for_update() + .limit(1) + ) + task_set = result.scalar_one_or_none() + if not task_set: + await db.rollback() + return + result_json = analyzed.result + task_set.original_video_content = str(result_json.get("原视频内容") or "无") + task_set.original_video_category = str(result_json.get("原视频分类") or "无") + task_set.original_video_audience = str(result_json.get("原视频受众人群") or "无") + task_set.ai_suggestion_json = result_json.get("拆镜内容剖析") or [] + task_set.analysis_raw_json = analyzed.raw_response + task_set.analysis_result_json = result_json + task_set.analysis_status = ShotAnalysisStatusEnum.COMPLETED.value + task_set.status = ShotTaskSetStatusEnum.ANALYSIS_COMPLETED.value + task_set.analysis_error_message = None + await db.commit() + + log_module_prompt_event( + event_type="SHOT_ANALYSIS_SUCCESS", + project_id=task_set_id, + step_id=task_set_id, + user_id=task_set_user_id or "", + module=MODULE, + prompt_type="shot_video_analysis", + request=analyzed.usage.get("log_request") if isinstance(analyzed.usage, dict) else {}, + response=analyzed.result, + token_usage=analyzed.usage, + ) + log_module_event_file( + module=MODULE, + event_type="SHOT_ANALYSIS_SUCCESS", + project_id=task_set_id, + user_id=task_set_user_id, + message="原视频拆镜分析成功", + detail={"suggestion_count": len(analyzed.result.get("拆镜内容剖析") or []), "token_usage": analyzed.usage}, + ) + except Exception as exc: + async with async_session() as db: + result = await db.execute( + select(ShotReplicateTaskSet) + .where(ShotReplicateTaskSet.id == task_set_id, ShotReplicateTaskSet.deleted_at.is_(None)) + .with_for_update() + .limit(1) + ) + task_set = result.scalar_one_or_none() + if task_set: + task_set_user_id = task_set_user_id or task_set.user_id + task_set.status = ShotTaskSetStatusEnum.ANALYSIS_FAILED.value + task_set.analysis_status = ShotAnalysisStatusEnum.FAILED.value + task_set.analysis_error_message = str(exc) + await db.commit() + log_module_error( + module=MODULE, + event_type="SHOT_ANALYSIS_FAILED", + project_id=task_set_id, + user_id=task_set_user_id, + message="原视频拆镜分析失败", + detail={"task_set_id": task_set_id, "video_url": video_url, "analysis_mode": "full_breakdown"}, + exc=exc, + ) + + +async def _run_analyze_custom_segment_video(segment_id: str) -> None: + user_id: str | None = None + task_set_id: str | None = None + video_url: str | None = None + try: + async with async_session() as db: + result = await db.execute( + select(ShotReplicateSegment) + .where(ShotReplicateSegment.id == segment_id, ShotReplicateSegment.deleted_at.is_(None)) + .with_for_update() + .limit(1) + ) + segment = result.scalar_one_or_none() + if not segment or not segment.segment_video_url: + return + if segment.analysis_status == ShotSegmentAnalysisStatusEnum.COMPLETED.value: + return + user_id = segment.user_id + task_set_id = segment.task_set_id + video_url = segment.segment_video_url + segment.analysis_status = ShotSegmentAnalysisStatusEnum.PROCESSING.value + segment.analysis_error_message = None + await db.commit() + + log_module_event_file( + module=MODULE, + event_type="SHOT_SEGMENT_ANALYSIS_STARTED", + project_id=task_set_id, + step_id=segment_id, + user_id=user_id, + message="自定义拆镜片段分析开始", + detail={"segment_id": segment_id, "task_set_id": task_set_id, "video_url": video_url, "analysis_mode": "summary_only"}, + ) + + async with async_session() as db: + analyzed = await analyze_video_for_shot_split(db, video_url or "", user_id=user_id, mode="summary_only") + result = await db.execute( + select(ShotReplicateSegment) + .where(ShotReplicateSegment.id == segment_id, ShotReplicateSegment.deleted_at.is_(None)) + .with_for_update() + .limit(1) + ) + segment = result.scalar_one_or_none() + if not segment: + await db.rollback() + return + result_json = analyzed.result + segment.original_video_content = str(result_json.get("原视频内容") or "无") + segment.original_video_category = str(result_json.get("原视频分类") or "无") + segment.original_video_audience = str(result_json.get("原视频受众人群") or "无") + segment.segment_content = segment.original_video_content + segment.segment_category = segment.original_video_category + segment.segment_audience = segment.original_video_audience + segment.analysis_json = result_json + segment.analysis_status = ShotSegmentAnalysisStatusEnum.COMPLETED.value + segment.analysis_error_message = None + await db.commit() + + log_module_prompt_event( + event_type="SHOT_SEGMENT_ANALYSIS_SUCCESS", + project_id=task_set_id or segment_id, + step_id=segment_id, + user_id=user_id or "", + module=MODULE, + prompt_type="shot_segment_analysis", + request=analyzed.usage.get("log_request") if isinstance(analyzed.usage, dict) else {}, + response=analyzed.result, + token_usage=analyzed.usage, + ) + log_module_event_file( + module=MODULE, + event_type="SHOT_SEGMENT_ANALYSIS_SUCCESS", + project_id=task_set_id, + step_id=segment_id, + user_id=user_id, + message="自定义拆镜片段分析成功", + detail={"segment_id": segment_id, "task_set_id": task_set_id, "token_usage": analyzed.usage}, + ) + except Exception as exc: + async with async_session() as db: + result = await db.execute( + select(ShotReplicateSegment) + .where(ShotReplicateSegment.id == segment_id, ShotReplicateSegment.deleted_at.is_(None)) + .with_for_update() + .limit(1) + ) + segment = result.scalar_one_or_none() + if segment: + user_id = user_id or segment.user_id + task_set_id = task_set_id or segment.task_set_id + segment.analysis_status = ShotSegmentAnalysisStatusEnum.FAILED.value + segment.analysis_error_message = str(exc) + await db.commit() + log_module_error( + module=MODULE, + event_type="SHOT_SEGMENT_ANALYSIS_FAILED", + project_id=task_set_id, + step_id=segment_id, + user_id=user_id, + message="自定义拆镜片段分析失败", + detail={"segment_id": segment_id, "task_set_id": task_set_id, "video_url": video_url, "analysis_mode": "summary_only"}, + exc=exc, + ) + + +async def _run_split_one_segment(segment_id: str) -> None: + segment_lock_key = f"{settings.SHOT_SPLIT_LOCK_KEY_PREFIX}:{segment_id}" + segment_lock_token = await redis_acquire_lock( + lock_key=segment_lock_key, + ttl_seconds=int(settings.SHOT_SPLIT_LEASE_SECONDS or 600), + log_context="shot_split_segment_lock", + ) + if not segment_lock_token: + return + + semaphore_key: str | None = None + user_id: str | None = None + task_set_id: str | None = None + source_path: str | None = None + try: + semaphore_key = await _acquire_split_semaphore(segment_id) + if not semaphore_key: + log_module_event_file( + module=MODULE, + event_type="SHOT_SEGMENT_SPLIT_RETRY_WAITING", + step_id=segment_id, + message="拆镜 ffmpeg 并发闸门已满,稍后重试", + detail={"segment_id": segment_id, "reason": "semaphore_full"}, + ) + if celery_app: + split_one_segment.apply_async(args=[segment_id], queue=SPLIT_QUEUE, countdown=10, priority=settings.DOWNLOAD_TASK_PRIORITY_NORMAL) + return + + async with async_session() as db: + result = await db.execute( + select(ShotReplicateSegment) + .where(ShotReplicateSegment.id == segment_id, ShotReplicateSegment.deleted_at.is_(None)) + .with_for_update() + .limit(1) + ) + segment = result.scalar_one_or_none() + if not segment: + return + user_id = segment.user_id + task_set_id = segment.task_set_id + task_set_result = await db.execute( + select(ShotReplicateTaskSet) + .where(ShotReplicateTaskSet.id == segment.task_set_id, ShotReplicateTaskSet.deleted_at.is_(None)) + .with_for_update() + .limit(1) + ) + task_set = task_set_result.scalar_one_or_none() + if not task_set: + return + if segment.split_status == ShotSplitStatusEnum.COMPLETED.value and segment.segment_video_url: + return + + validate_split_range( + start_second=segment.start_second, + end_second=segment.end_second, + video_duration_seconds=task_set.video_duration_seconds, + ) + + now = _now() + segment.split_status = ShotSplitStatusEnum.PROCESSING.value + segment.split_started_at = now + segment.split_lease_until = _lease_until(now) + segment.split_retry_count = int(segment.split_retry_count or 0) + 1 + segment.split_next_retry_at = None + segment.split_last_error = None + task_set.status = ShotTaskSetStatusEnum.SPLITTING.value + task_set.split_status = ShotSplitStatusEnum.PROCESSING.value + await db.commit() + + source_path = task_set.video_path + date_dir = (segment.created_at or now).strftime("%Y/%m/%d") + start_second = segment.start_second + end_second = segment.end_second + attempt = segment.split_retry_count + + log_module_event_file( + module=MODULE, + event_type="SHOT_SEGMENT_SPLIT_STARTED", + project_id=task_set_id, + step_id=segment_id, + user_id=user_id, + message="拆镜片段 ffmpeg 切割开始", + detail={ + "segment_id": segment_id, + "task_set_id": task_set_id, + "source_path": source_path, + "start_second": start_second, + "end_second": end_second, + "attempt": attempt, + }, + ) + + split_result = await split_video_segment_async( + source_path=source_path, + segment_id=segment_id, + start_second=start_second, + end_second=end_second, + date_dir=date_dir, + ) + + async with async_session() as db: + result = await db.execute( + select(ShotReplicateSegment) + .where(ShotReplicateSegment.id == segment_id, ShotReplicateSegment.deleted_at.is_(None)) + .with_for_update() + .limit(1) + ) + segment = result.scalar_one_or_none() + if not segment: + return + segment.segment_video_url = split_result.url + segment.segment_video_path = split_result.path + segment.split_status = ShotSplitStatusEnum.COMPLETED.value + segment.split_completed_at = _now() + segment.split_lease_until = None + segment.split_next_retry_at = None + segment.split_last_error = None + await refresh_task_set_split_summary(db, segment.task_set_id) + await db.commit() + + log_module_event_file( + module=MODULE, + event_type="SHOT_SEGMENT_SPLIT_SUCCESS", + project_id=segment.task_set_id, + step_id=segment.id, + user_id=segment.user_id, + message="拆镜片段 ffmpeg 切割成功", + detail={ + "segment_id": segment.id, + "task_set_id": segment.task_set_id, + "segment_video_url": split_result.url, + "segment_video_path": split_result.path, + "source_mode": segment.source_mode, + }, + ) + + if segment.source_mode == ShotSegmentSourceModeEnum.CUSTOM.value and celery_app: + analyze_custom_segment_video.apply_async(args=[segment.id], queue=ANALYSIS_QUEUE, countdown=0) + + except Exception as exc: + next_retry_delay: int | None = None + final_failed = False + async with async_session() as db: + result = await db.execute( + select(ShotReplicateSegment) + .where(ShotReplicateSegment.id == segment_id, ShotReplicateSegment.deleted_at.is_(None)) + .with_for_update() + .limit(1) + ) + segment = result.scalar_one_or_none() + if not segment: + return + user_id = user_id or segment.user_id + task_set_id = task_set_id or segment.task_set_id + attempt = int(segment.split_retry_count or 0) + segment.split_last_error = str(exc) + segment.split_lease_until = None + if attempt >= int(settings.SHOT_SPLIT_MAX_RETRY_COUNT or 3): + segment.split_status = ShotSplitStatusEnum.FAILED.value + segment.split_next_retry_at = None + final_failed = True + else: + segment.split_status = ShotSplitStatusEnum.RETRY_WAITING.value + segment.split_next_retry_at = _retry_at(attempt) + await refresh_task_set_split_summary(db, segment.task_set_id) + await db.commit() + + if segment.split_status == ShotSplitStatusEnum.RETRY_WAITING.value and celery_app: + next_retry_delay = max(1, int(((segment.split_next_retry_at or _now()) - _now()).total_seconds())) + split_one_segment.apply_async(args=[segment_id], queue=SPLIT_QUEUE, countdown=next_retry_delay, priority=settings.DOWNLOAD_TASK_PRIORITY_RECOVER) + + log_module_error( + module=MODULE, + event_type="SHOT_SEGMENT_SPLIT_FAILED" if final_failed else "SHOT_SEGMENT_SPLIT_RETRY_WAITING", + project_id=task_set_id, + step_id=segment_id, + user_id=user_id, + message="拆镜片段 ffmpeg 切割失败" if final_failed else "拆镜片段 ffmpeg 切割失败,等待重试", + detail={ + "segment_id": segment_id, + "task_set_id": task_set_id, + "source_path": source_path, + "next_retry_delay_seconds": next_retry_delay, + "final_failed": final_failed, + }, + exc=exc, + ) + finally: + await _release_split_semaphore(semaphore_key, segment_id) + await redis_release_lock(lock_key=segment_lock_key, token=segment_lock_token, log_context="shot_split_segment_lock") + + +async def _run_recover_split_tasks_once() -> dict[str, Any]: + from app.services.shot_replicate_recovery_service import recover_shot_split_tasks_once + + async with async_session() as db: + return await recover_shot_split_tasks_once(db) + + +if celery_app: + + @celery_app.task(name="shot_replicate.analyze_original_video") + def analyze_original_video(task_set_id: str) -> None: + return run_async(_run_analyze_original_video(task_set_id)) + + + @celery_app.task(name="shot_replicate.split_one_segment", bind=True, max_retries=0) + def split_one_segment(self, segment_id: str) -> None: + return run_async(_run_split_one_segment(segment_id)) + + + @celery_app.task(name="shot_replicate.analyze_custom_segment_video") + def analyze_custom_segment_video(segment_id: str) -> None: + return run_async(_run_analyze_custom_segment_video(segment_id)) + + + @celery_app.task(name="shot_replicate.recover_split_tasks_once") + def recover_split_tasks_once() -> dict[str, Any]: + return run_async(_run_recover_split_tasks_once()) + +else: + + class _DisabledTask: + def delay(self, *args: Any, **kwargs: Any) -> None: + raise RuntimeError("Celery is disabled") + + def apply_async(self, *args: Any, **kwargs: Any) -> None: + raise RuntimeError("Celery is disabled") + + analyze_original_video = _DisabledTask() + split_one_segment = _DisabledTask() + analyze_custom_segment_video = _DisabledTask() + recover_split_tasks_once = _DisabledTask() From c7423f7790b8defe9238487134e0aafe1675edbd Mon Sep 17 00:00:00 2001 From: GinHa <15201596918@163.com> Date: Thu, 11 Jun 2026 18:02:21 +0800 Subject: [PATCH 4/7] =?UTF-8?q?=E7=89=88=E6=9C=AC=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../780226f22449_merge_changes_from_remote.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 video-gen-api/alembic/versions/780226f22449_merge_changes_from_remote.py diff --git a/video-gen-api/alembic/versions/780226f22449_merge_changes_from_remote.py b/video-gen-api/alembic/versions/780226f22449_merge_changes_from_remote.py new file mode 100644 index 00000000..eb56371b --- /dev/null +++ b/video-gen-api/alembic/versions/780226f22449_merge_changes_from_remote.py @@ -0,0 +1,25 @@ +"""merge changes from remote + +Revision ID: 780226f22449 +Revises: 8922eafcd8b0, dbb11c0b5a0a +Create Date: 2026-06-11 18:00:02.841793 +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '780226f22449' +down_revision: Union[str, None] = ('8922eafcd8b0', 'dbb11c0b5a0a') +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + pass + + +def downgrade() -> None: + pass From fe3d57465b3a115d9403b145a00b0312ca67c658 Mon Sep 17 00:00:00 2001 From: Lrd <13001933075@sina.cn> Date: Thu, 11 Jun 2026 18:07:07 +0800 Subject: [PATCH 5/7] =?UTF-8?q?=E6=B6=88=E8=80=97=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=96=87=E4=BB=B6=E5=92=8C=E8=B7=AF=E7=94=B1?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- video-gen-admin/dist/assets/index-BH90EezD.js | 329 --------------- video-gen-admin/dist/assets/index-CIsX_bex.js | 387 ++++++++++++++++++ video-gen-admin/dist/index.html | 2 +- video-gen-admin/tsconfig.tsbuildinfo | 2 +- video-gen-app/src/App.tsx | 7 +- video-gen-app/src/pages/AuthorizationPage.tsx | 233 +++-------- video-gen-app/src/pages/ConsumePage.tsx | 120 ++++++ 7 files changed, 566 insertions(+), 514 deletions(-) delete mode 100644 video-gen-admin/dist/assets/index-BH90EezD.js create mode 100644 video-gen-admin/dist/assets/index-CIsX_bex.js create mode 100644 video-gen-app/src/pages/ConsumePage.tsx diff --git a/video-gen-admin/dist/assets/index-BH90EezD.js b/video-gen-admin/dist/assets/index-BH90EezD.js deleted file mode 100644 index 99657087..00000000 --- a/video-gen-admin/dist/assets/index-BH90EezD.js +++ /dev/null @@ -1,329 +0,0 @@ -var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,a)=>(a=n==null?{}:e(i(n)),s(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var l=o((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(e.unstable_now=void 0,typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=!1,_=typeof setTimeout==`function`?setTimeout:null,v=typeof clearTimeout==`function`?clearTimeout:null,y=typeof setImmediate<`u`?setImmediate:null;function b(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function x(e){if(h=!1,b(e),!m)if(n(c)!==null)m=!0,S||(S=!0,O());else{var t=n(l);t!==null&&j(x,t.startTime-e)}}var S=!1,C=-1,w=5,T=-1;function E(){return g?!0:!(e.unstable_now()-Tt&&E());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=t);if(t=e.unstable_now(),typeof s==`function`){d.callback=s,b(t),i=!0;break b}d===n(c)&&r(c),b(t)}else r(c);d=n(c)}if(d!==null)i=!0;else{var u=n(l);u!==null&&j(x,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}i=void 0}}finally{i?O():S=!1}}}var O;if(typeof y==`function`)O=function(){y(D)};else if(typeof MessageChannel<`u`){var k=new MessageChannel,A=k.port2;k.port1.onmessage=D,O=function(){A.postMessage(null)}}else O=function(){_(D,0)};function j(t,n){C=_(function(){t(e.unstable_now())},n)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(e){e.callback=null},e.unstable_forceFrameRate=function(e){0>e||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(v(C),C=-1):h=!0,j(x,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,S||(S=!0,O()))),r},e.unstable_shouldYield=E,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),u=o(((e,t)=>{t.exports=l()})),d=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.iterator;function m(e){return typeof e!=`object`||!e?null:(e=p&&e[p]||e[`@@iterator`],typeof e==`function`?e:null)}var h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},g=Object.assign,_={};function v(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}v.prototype.isReactComponent={},v.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},v.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function y(){}y.prototype=v.prototype;function b(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}var x=b.prototype=new y;x.constructor=b,g(x,v.prototype),x.isPureReactComponent=!0;var S=Array.isArray;function C(){}var w={H:null,A:null,T:null,S:null},T=Object.prototype.hasOwnProperty;function E(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function D(e,t){return E(e.type,t,e.props)}function O(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function k(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var A=/\/+/g;function j(e,t){return typeof e==`object`&&e&&e.key!=null?k(``+e.key):t.toString(36)}function M(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(C,C):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function N(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,N(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+j(e,0):a,S(o)?(i=``,c!=null&&(i=c.replace(A,`$&/`)+`/`),N(o,r,i,``,function(e){return e})):o!=null&&(O(o)&&(o=D(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(A,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(S(e))for(var u=0;u{t.exports=d()})),p=o((e=>{var t=f();function n(e){var t=`https://react.dev/errors/`+e;if(1{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=p()})),h=o((e=>{var t=u(),n=f(),r=m();function i(e){var t=`https://react.dev/errors/`+e;if(1B||(e.current=z[B],z[B]=null,B--)}function U(e,t){B++,z[B]=e.current,e.current=t}var W=V(null),ee=V(null),te=V(null),ne=V(null);function re(e,t){switch(U(te,t),U(ee,e),U(W,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Gd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Gd(t),e=Kd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}H(W),U(W,e)}function G(){H(W),H(ee),H(te)}function ie(e){e.memoizedState!==null&&U(ne,e);var t=W.current,n=Kd(t,e.type);t!==n&&(U(ee,e),U(W,n))}function ae(e){ee.current===e&&(H(W),H(ee)),ne.current===e&&(H(ne),np._currentValue=R)}var oe,K;function se(e){if(oe===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);oe=t&&t[1]||``,K=-1)`:-1i||c[r]!==l[i]){var u=` -`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(``)&&(u=u.replace(``,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{ce=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?se(n):``}function ue(e,t){switch(e.tag){case 26:case 27:case 5:return se(e.type);case 16:return se(`Lazy`);case 13:return e.child!==t&&t!==null?se(`Suspense Fallback`):se(`Suspense`);case 19:return se(`SuspenseList`);case 0:case 15:return le(e.type,!1);case 11:return le(e.type.render,!1);case 1:return le(e.type,!0);case 31:return se(`Activity`);default:return``}}function de(e){try{var t=``,n=null;do t+=ue(e,n),n=e,e=e.return;while(e);return t}catch(e){return` -Error generating stack: `+e.message+` -`+e.stack}}var fe=Object.prototype.hasOwnProperty,pe=t.unstable_scheduleCallback,me=t.unstable_cancelCallback,he=t.unstable_shouldYield,ge=t.unstable_requestPaint,_e=t.unstable_now,ve=t.unstable_getCurrentPriorityLevel,ye=t.unstable_ImmediatePriority,be=t.unstable_UserBlockingPriority,xe=t.unstable_NormalPriority,Se=t.unstable_LowPriority,Ce=t.unstable_IdlePriority,we=t.log,Te=t.unstable_setDisableYieldValue,Ee=null,De=null;function Oe(e){if(typeof we==`function`&&Te(e),De&&typeof De.setStrictMode==`function`)try{De.setStrictMode(Ee,e)}catch{}}var ke=Math.clz32?Math.clz32:Me,Ae=Math.log,je=Math.LN2;function Me(e){return e>>>=0,e===0?32:31-(Ae(e)/je|0)|0}var Ne=256,Pe=262144,Fe=4194304;function Ie(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function Le(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=Ie(n))):i=Ie(o):i=Ie(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=Ie(n))):i=Ie(o)):i=Ie(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function Re(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function ze(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Be(){var e=Fe;return Fe<<=1,!(Fe&62914560)&&(Fe=4194304),e}function Ve(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function He(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Ue(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),tn=!1;if(en)try{var nn={};Object.defineProperty(nn,`passive`,{get:function(){tn=!0}}),window.addEventListener(`test`,nn,nn),window.removeEventListener(`test`,nn,nn)}catch{tn=!1}var rn=null,an=null,on=null;function sn(){if(on)return on;var e,t=an,n=t.length,r,i=`value`in rn?rn.value:rn.textContent,a=i.length;for(e=0;e=zn),Hn=` `,Un=!1;function Wn(e,t){switch(e){case`keyup`:return Ln.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Gn(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var Kn=!1;function qn(e,t){switch(e){case`compositionend`:return Gn(t);case`keypress`:return t.which===32?(Un=!0,Hn):null;case`textInput`:return e=t.data,e===Hn&&Un?null:e;default:return null}}function Jn(e,t){if(Kn)return e===`compositionend`||!Rn&&Wn(e,t)?(e=sn(),on=an=rn=null,Kn=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=gr(n)}}function vr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?vr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function yr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Ot(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=Ot(e.document)}return t}function br(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var xr=en&&`documentMode`in document&&11>=document.documentMode,Sr=null,Cr=null,wr=null,Tr=!1;function Er(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Tr||Sr==null||Sr!==Ot(r)||(r=Sr,`selectionStart`in r&&br(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),wr&&hr(wr,r)||(wr=r,r=kd(Cr,`onSelect`),0>=o,i-=o,_i=1<<32-ke(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),Ei&&yi(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),Ei&&yi(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return Ei&&yi(a,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),Ei&&yi(a,g),u}function b(e,r,o,c){if(typeof o==`object`&&o&&o.type===y&&o.key===null&&(o=o.props.children),typeof o==`object`&&o){switch(o.$$typeof){case _:a:{for(var l=o.key;r!==null;){if(r.key===l){if(l=o.type,l===y){if(r.tag===7){n(e,r.sibling),c=a(r,o.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===O&&ya(l)===r.type){n(e,r.sibling),c=a(r,o.props),Ea(c,o),c.return=e,e=c;break a}n(e,r);break}else t(e,r);r=r.sibling}o.type===y?(c=ii(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=ri(o.type,o.key,o.props,null,e.mode,c),Ea(c,o),c.return=e,e=c)}return s(e);case v:a:{for(l=o.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===o.containerInfo&&r.stateNode.implementation===o.implementation){n(e,r.sibling),c=a(r,o.children||[]),c.return=e,e=c;break a}else{n(e,r);break}else t(e,r);r=r.sibling}c=si(o,e.mode,c),c.return=e,e=c}return s(e);case O:return o=ya(o),b(e,r,o,c)}if(F(o))return h(e,r,o,c);if(M(o)){if(l=M(o),typeof l!=`function`)throw Error(i(150));return o=l.call(o),g(e,r,o,c)}if(typeof o.then==`function`)return b(e,r,Ta(o),c);if(o.$$typeof===C)return b(e,r,qi(e,o),c);Da(e,o)}return typeof o==`string`&&o!==``||typeof o==`number`||typeof o==`bigint`?(o=``+o,r!==null&&r.tag===6?(n(e,r.sibling),c=a(r,o),c.return=e,e=c):(n(e,r),c=ai(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{wa=0;var i=b(e,t,n,r);return Ca=null,i}catch(t){if(t===pa||t===ha)throw t;var a=$r(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var ka=Oa(!0),Aa=Oa(!1),ja=!1;function Ma(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Na(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Pa(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Fa(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,Fl&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=Xr(e),Yr(e,null,n),t}return Kr(e,r,t,n),Xr(e)}function Ia(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Ge(e,n)}}function La(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var Ra=!1;function za(){if(Ra){var e=ia;if(e!==null)throw e}}function Ba(e,t,n,r){Ra=!1;var i=e.updateQueue;ja=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,p=f!==s.lane;if(p?(Rl&f)===f:(r&f)===f){f!==0&&f===ra&&(Ra=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var m=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(m=g.payload,typeof m==`function`){d=m.call(_,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=g.payload,f=typeof m==`function`?m.call(_,d,f):m,f==null)break a;d=h({},d,f);break a;case 2:ja=!0}}f=s.callback,f!==null&&(e.flags|=64,p&&(e.flags|=8192),p=i.callbacks,p===null?i.callbacks=[f]:p.push(f))}else p={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;p=s,s=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),Kl|=o,e.lanes=o,e.memoizedState=d}}function Va(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function Ha(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=I.T,s={};I.T=s,ks(e,!1,t,n);try{var c=i(),l=I.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Os(e,t,sa(c,r),mu(e)):Os(e,t,r,mu(e))}catch(n){Os(e,t,{then:function(){},status:`rejected`,reason:n},mu())}finally{L.p=a,o!==null&&s.types!==null&&(o.types=s.types),I.T=o}}function vs(){}function ys(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=bs(e).queue;_s(e,a,t,R,n===null?vs:function(){return xs(e),n(r)})}function bs(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:R,baseState:R,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Ao,lastRenderedState:R},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Ao,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function xs(e){var t=bs(e);t.next===null&&(t=e.alternate.memoizedState),Os(e,t.next.queue,{},mu())}function Ss(){return J(np)}function Cs(){return To().memoizedState}function ws(){return To().memoizedState}function Ts(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=mu();e=Pa(n);var r=Fa(t,e,n);r!==null&&(gu(r,t,n),Ia(r,t,n)),t={cache:$i()},e.payload=t;return}t=t.return}}function Es(e,t,n){var r=mu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},As(e)?js(t,n):(n=qr(e,t,n,r),n!==null&&(gu(n,e,r),Ms(n,t,r)))}function Ds(e,t,n){Os(e,t,n,mu())}function Os(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(As(e))js(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,mr(s,o))return Kr(e,t,i,0),Il===null&&Gr(),!1}catch{}if(n=qr(e,t,i,r),n!==null)return gu(n,e,r),Ms(n,t,r),!0}return!1}function ks(e,t,n,r){if(r={lane:2,revertLane:pd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},As(e)){if(t)throw Error(i(479))}else t=qr(e,n,r,2),t!==null&&gu(t,e,2)}function As(e){var t=e.alternate;return e===io||t!==null&&t===io}function js(e,t){co=so=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Ms(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Ge(e,n)}}var Ns={readContext:J,use:Oo,useCallback:ho,useContext:ho,useEffect:ho,useImperativeHandle:ho,useLayoutEffect:ho,useInsertionEffect:ho,useMemo:ho,useReducer:ho,useRef:ho,useState:ho,useDebugValue:ho,useDeferredValue:ho,useTransition:ho,useSyncExternalStore:ho,useId:ho,useHostTransitionStatus:ho,useFormState:ho,useActionState:ho,useOptimistic:ho,useMemoCache:ho,useCacheRefresh:ho};Ns.useEffectEvent=ho;var Ps={readContext:J,use:Oo,useCallback:function(e,t){return wo().memoizedState=[e,t===void 0?null:t],e},useContext:J,useEffect:is,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),ns(4194308,4,us.bind(null,t,e),n)},useLayoutEffect:function(e,t){return ns(4194308,4,e,t)},useInsertionEffect:function(e,t){ns(4,2,e,t)},useMemo:function(e,t){var n=wo();t=t===void 0?null:t;var r=e();if(lo){Oe(!0);try{e()}finally{Oe(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=wo();if(n!==void 0){var i=n(t);if(lo){Oe(!0);try{n(t)}finally{Oe(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=Es.bind(null,io,e),[r.memoizedState,e]},useRef:function(e){var t=wo();return e={current:e},t.memoizedState=e},useState:function(e){e=Bo(e);var t=e.queue,n=Ds.bind(null,io,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:fs,useDeferredValue:function(e,t){return hs(wo(),e,t)},useTransition:function(){var e=Bo(!1);return e=_s.bind(null,io,e.queue,!0,!1),wo().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=io,a=wo();if(Ei){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),Il===null)throw Error(i(349));Rl&127||Fo(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,is(Lo.bind(null,r,o,e),[e]),r.flags|=2048,es(9,{destroy:void 0},Io.bind(null,r,o,n,t),null),n},useId:function(){var e=wo(),t=Il.identifierPrefix;if(Ei){var n=vi,r=_i;n=(r&~(1<<32-ke(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=uo++,0<\/script>`,o=o.removeChild(o.firstChild);break;case`select`:o=typeof r.is==`string`?s.createElement(`select`,{is:r.is}):s.createElement(`select`),r.multiple?o.multiple=!0:r.size&&(o.size=r.size);break;default:o=typeof r.is==`string`?s.createElement(a,{is:r.is}):s.createElement(a)}}o[Qe]=t,o[$e]=r;a:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)o.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break a;for(;s.sibling===null;){if(s.return===null||s.return===t)break a;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=o;a:switch(Rd(o,a,r),a){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Oc(t)}}return Nc(t),kc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Oc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=te.current,Ni(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=wi,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[Qe]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Fd(e.nodeValue,n)),e||Ai(t,!0)}else e=Wd(e).createTextNode(r),e[Qe]=t,t.stateNode=e}return Nc(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Ni(t),n!==null){if(e===null){if(!r)throw Error(i(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(557));e[Qe]=t}else Pi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Nc(t),e=!1}else n=Fi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(eo(t),t):(eo(t),null);if(t.flags&128)throw Error(i(558))}return Nc(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=Ni(t),r!==null&&r.dehydrated!==null){if(e===null){if(!a)throw Error(i(318));if(a=t.memoizedState,a=a===null?null:a.dehydrated,!a)throw Error(i(317));a[Qe]=t}else Pi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Nc(t),a=!1}else a=Fi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(eo(t),t):(eo(t),null)}return eo(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,a=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(a=r.alternate.memoizedState.cachePool.pool),o=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),o!==a&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),jc(t,t.updateQueue),Nc(t),null);case 4:return G(),e===null&&Td(t.stateNode.containerInfo),Nc(t),null;case 10:return Vi(t.type),Nc(t),null;case 19:if(H(to),r=t.memoizedState,r===null)return Nc(t),null;if(a=(t.flags&128)!=0,o=r.rendering,o===null)if(a)Mc(r,!1);else{if(Gl!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=no(e),o!==null){for(t.flags|=128,Mc(r,!1),e=o.updateQueue,t.updateQueue=e,jc(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)ni(n,e),n=n.sibling;return U(to,to.current&1|2),Ei&&yi(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&_e()>nu&&(t.flags|=128,a=!0,Mc(r,!1),t.lanes=4194304)}else{if(!a)if(e=no(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,jc(t,e),Mc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!Ei)return Nc(t),null}else 2*_e()-r.renderingStartTime>nu&&n!==536870912&&(t.flags|=128,a=!0,Mc(r,!1),t.lanes=4194304);r.isBackwards?(o.sibling=t.child,t.child=o):(e=r.last,e===null?t.child=o:e.sibling=o,r.last=o)}return r.tail===null?(Nc(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=_e(),e.sibling=null,n=to.current,U(to,a?n&1|2:n&1),Ei&&yi(t,r.treeForkCount),e);case 22:case 23:return eo(t),qa(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(Nc(t),t.subtreeFlags&6&&(t.flags|=8192)):Nc(t),n=t.updateQueue,n!==null&&jc(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&H(la),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Vi(Qi),Nc(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function Fc(e,t){switch(Si(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Vi(Qi),G(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return ae(t),null;case 31:if(t.memoizedState!==null){if(eo(t),t.alternate===null)throw Error(i(340));Pi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(eo(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Pi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return H(to),null;case 4:return G(),null;case 10:return Vi(t.type),null;case 22:case 23:return eo(t),qa(),e!==null&&H(la),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Vi(Qi),null;case 25:return null;default:return null}}function Ic(e,t){switch(Si(t),t.tag){case 3:Vi(Qi),G();break;case 26:case 27:case 5:ae(t);break;case 4:G();break;case 31:t.memoizedState!==null&&eo(t);break;case 13:eo(t);break;case 19:H(to);break;case 10:Vi(t.type);break;case 22:case 23:eo(t),qa(),e!==null&&H(la);break;case 24:Vi(Qi)}}function Lc(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Ku(t,t.return,e)}}function Rc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Ku(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Ku(t,t.return,e)}}function zc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{Ha(t,n)}catch(t){Ku(e,e.return,t)}}}function Bc(e,t,n){n.props=Vs(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Ku(e,t,n)}}function Vc(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Ku(e,t,n)}}function Hc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r==`function`)try{r()}catch(n){Ku(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Ku(e,t,n)}else n.current=null}function Uc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Ku(e,e.return,t)}}function Wc(e,t,n){try{var r=e.stateNode;zd(r,e.type,n,t),r[$e]=t}catch(t){Ku(e,e.return,t)}}function Gc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&tf(e.type)||e.tag===4}function Kc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Gc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&tf(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function qc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Gt));else if(r!==4&&(r===27&&tf(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(qc(e,t,n),e=e.sibling;e!==null;)qc(e,t,n),e=e.sibling}function Jc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&tf(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(Jc(e,t,n),e=e.sibling;e!==null;)Jc(e,t,n),e=e.sibling}function Yc(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Rd(t,r,n),t[Qe]=e,t[$e]=n}catch(t){Ku(e,e.return,t)}}var Xc=!1,Zc=!1,Qc=!1,$c=typeof WeakSet==`function`?WeakSet:Set,el=null;function tl(e,t){if(e=e.containerInfo,Hd=dp,e=yr(e),br(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var a=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||r!==0&&f.nodeType!==3||(l=s+r),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===r&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(Ud={focusedElem:e,selectionRange:n},dp=!1,el=t;el!==null;)if(t=el,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,el=e;else for(;el!==null;){switch(t=el,o=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Rd(o,r,n),o[Qe]=e,dt(o),r=o;break a;case`link`:var s=Gf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=_r(s,h),v=_r(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,I.T=null,n=uu,uu=null;var o=ou,s=cu;if(au=0,su=ou=null,cu=0,Fl&6)throw Error(i(331));var c=Fl;if(Fl|=4,Al(o.current),Cl(o,o.current,s,n),Fl=c,od(0,!1),De&&typeof De.onPostCommitFiberRoot==`function`)try{De.onPostCommitFiberRoot(Ee,o)}catch{}return!0}finally{L.p=a,I.T=r,Hu(e,t)}}function Gu(e,t,n){t=li(n,t),t=qs(e.stateNode,t,2),e=Fa(e,t,2),e!==null&&(He(e,2),ad(e))}function Ku(e,t,n){if(e.tag===3)Gu(e,e,n);else for(;t!==null;){if(t.tag===3){Gu(t,e,n);break}else if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(iu===null||!iu.has(r))){e=li(n,e),n=Js(2),r=Fa(t,n,2),r!==null&&(Ys(n,r,t,e),He(r,2),ad(r));break}}t=t.return}}function qu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Pl;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(Ul=!0,i.add(n),e=Ju.bind(null,e,t,n),t.then(e,e))}function Ju(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,Il===e&&(Rl&n)===n&&(Gl===4||Gl===3&&(Rl&62914560)===Rl&&300>_e()-eu?!(Fl&2)&&Cu(e,0):Jl|=n,Xl===Rl&&(Xl=0)),ad(e)}function Yu(e,t){t===0&&(t=Be()),e=Jr(e,t),e!==null&&(He(e,t),ad(e))}function Xu(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Yu(e,n)}function Zu(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}r!==null&&r.delete(t),Yu(e,n)}function Qu(e,t){return pe(e,t)}var $u=null,ed=null,td=!1,nd=!1,rd=!1,id=0;function ad(e){e!==ed&&e.next===null&&(ed===null?$u=ed=e:ed=ed.next=e),nd=!0,td||(td=!0,fd())}function od(e,t){if(!rd&&nd){rd=!0;do for(var n=!1,r=$u;r!==null;){if(!t)if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-ke(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,dd(r,a))}else a=Rl,a=Le(r,r===Il?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||Re(r,a)||(n=!0,dd(r,a));r=r.next}while(n);rd=!1}}function sd(){cd()}function cd(){nd=td=!1;var e=0;id!==0&&Yd()&&(e=id);for(var t=_e(),n=null,r=$u;r!==null;){var i=r.next,a=ld(r,t);a===0?(r.next=null,n===null?$u=i:n.next=i,i===null&&(ed=n)):(n=r,(e!==0||a&3)&&(nd=!0)),r=i}au!==0&&au!==5||od(e,!1),id!==0&&(id=0)}function ld(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&Bd(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function Tf(e,t,n){var r=wf;if(r&&typeof t==`string`&&t){var i=At(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),yf.has(i)||(yf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Rd(t,`link`,e),dt(t),r.head.appendChild(t)))}}function Ef(e){xf.D(e),Tf(`dns-prefetch`,e,null)}function Df(e,t){xf.C(e,t),Tf(`preconnect`,e,t)}function Of(e,t,n){xf.L(e,t,n);var r=wf;if(r&&e&&t){var i=`link[rel="preload"][as="`+At(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+At(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+At(n.imageSizes)+`"]`)):i+=`[href="`+At(e)+`"]`;var a=i;switch(t){case`style`:a=Pf(e);break;case`script`:a=Rf(e)}vf.has(a)||(e=h({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),vf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(Ff(a))||t===`script`&&r.querySelector(zf(a))||(t=r.createElement(`link`),Rd(t,`link`,e),dt(t),r.head.appendChild(t)))}}function kf(e,t){xf.m(e,t);var n=wf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+At(r)+`"][href="`+At(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Rf(e)}if(!vf.has(a)&&(e=h({rel:`modulepreload`,href:e},t),vf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(zf(a)))return}r=n.createElement(`link`),Rd(r,`link`,e),dt(r),n.head.appendChild(r)}}}function Af(e,t,n){xf.S(e,t,n);var r=wf;if(r&&e){var i=ut(r).hoistableStyles,a=Pf(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(Ff(a)))s.loading=5;else{e=h({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=vf.get(a))&&Hf(e,n);var c=o=r.createElement(`link`);dt(c),Rd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Vf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function jf(e,t){xf.X(e,t);var n=wf;if(n&&e){var r=ut(n).hoistableScripts,i=Rf(e),a=r.get(i);a||(a=n.querySelector(zf(i)),a||(e=h({src:e,async:!0},t),(t=vf.get(i))&&Uf(e,t),a=n.createElement(`script`),dt(a),Rd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Mf(e,t){xf.M(e,t);var n=wf;if(n&&e){var r=ut(n).hoistableScripts,i=Rf(e),a=r.get(i);a||(a=n.querySelector(zf(i)),a||(e=h({src:e,async:!0,type:`module`},t),(t=vf.get(i))&&Uf(e,t),a=n.createElement(`script`),dt(a),Rd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Nf(e,t,n,r){var a=(a=te.current)?bf(a):null;if(!a)throw Error(i(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=Pf(n.href),n=ut(a).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Pf(n.href);var o=ut(a).hoistableStyles,s=o.get(e);if(s||(a=a.ownerDocument||a,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=a.querySelector(Ff(e)))&&!o._p&&(s.instance=o,s.state.loading=5),vf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},vf.set(e,n),o||Lf(a,e,n,s.state))),t&&r===null)throw Error(i(528,``));return s}if(t&&r!==null)throw Error(i(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Rf(n),n=ut(a).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(i(444,e))}}function Pf(e){return`href="`+At(e)+`"`}function Ff(e){return`link[rel="stylesheet"][`+e+`]`}function If(e){return h({},e,{"data-precedence":e.precedence,precedence:null})}function Lf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Rd(t,`link`,n),dt(t),e.head.appendChild(t))}function Rf(e){return`[src="`+At(e)+`"]`}function zf(e){return`script[async]`+e}function Bf(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+At(n.href)+`"]`);if(r)return t.instance=r,dt(r),r;var a=h({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),dt(r),Rd(r,`style`,a),Vf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=Pf(n.href);var o=e.querySelector(Ff(a));if(o)return t.state.loading|=4,t.instance=o,dt(o),o;r=If(n),(a=vf.get(a))&&Hf(r,a),o=(e.ownerDocument||e).createElement(`link`),dt(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Rd(o,`link`,r),t.state.loading|=4,Vf(o,n.precedence,e),t.instance=o;case`script`:return o=Rf(n.src),(a=e.querySelector(zf(o)))?(t.instance=a,dt(a),a):(r=n,(a=vf.get(o))&&(r=h({},n),Uf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),dt(a),Rd(a,`link`,r),e.head.appendChild(a),t.instance=a);case`void`:return null;default:throw Error(i(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Vf(r,n.precedence,e));return t.instance}function Vf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function qf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Jf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Yf(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Pf(r.href),a=t.querySelector(Ff(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Qf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,dt(a);return}a=t.ownerDocument||t,r=If(r),(i=vf.get(i))&&Hf(r,i),a=a.createElement(`link`),dt(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Rd(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=Qf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Xf=0;function Zf(e,t){return e.stylesheets&&e.count===0&&ep(e,e.stylesheets),0Xf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Qf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)ep(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var $f=null;function ep(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,$f=new Map,t.forEach(tp,e),$f=null,Qf.call(e))}function tp(e,t){if(!(t.state.loading&4)){var n=$f.get(e);if(n)var r=n.get(null);else{n=new Map,$f.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=h()})),_=`modulepreload`,v=function(e){return`/`+e},y={},b=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}r=o(t.map(t=>{if(t=v(t,n),t in y)return;y[t]=!0;let r=t.endsWith(`.css`),i=r?`[rel="stylesheet"]`:``;if(n)for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${t}"]${i}`))return;let o=document.createElement(`link`);if(o.rel=r?`stylesheet`:_,r||(o.as=`script`),o.crossOrigin=``,o.href=t,a&&o.setAttribute(`nonce`,a),document.head.appendChild(o),r)return new Promise((e,n)=>{o.addEventListener(`load`,e),o.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},x=c(f(),1),S=`popstate`;function C(e){return typeof e==`object`&&!!e&&`pathname`in e&&`search`in e&&`hash`in e&&`state`in e&&`key`in e}function w(e={}){function t(e,t){let n=t.state?.masked,{pathname:r,search:i,hash:a}=n||e.location;return k(``,{pathname:r,search:i,hash:a},t.state&&t.state.usr||null,t.state&&t.state.key||`default`,n?{pathname:e.location.pathname,search:e.location.search,hash:e.location.hash}:void 0)}function n(e,t){return typeof t==`string`?t:A(t)}return M(t,n,null,e)}function T(e,t){if(e===!1||e==null)throw Error(t)}function E(e,t){if(!e){typeof console<`u`&&console.warn(t);try{throw Error(t)}catch{}}}function D(){return Math.random().toString(36).substring(2,10)}function O(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function k(e,t,n=null,r,i){return{pathname:typeof e==`string`?e:e.pathname,search:``,hash:``,...typeof t==`string`?j(t):t,state:n,key:t&&t.key||r||D(),mask:i}}function A({pathname:e=`/`,search:t=``,hash:n=``}){return t&&t!==`?`&&(e+=t.charAt(0)===`?`?t:`?`+t),n&&n!==`#`&&(e+=n.charAt(0)===`#`?n:`#`+n),e}function j(e){let t={};if(e){let n=e.indexOf(`#`);n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf(`?`);r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function M(e,t,n,r={}){let{window:i=document.defaultView,v5Compat:a=!1}=r,o=i.history,s=`POP`,c=null,l=u();l??(l=0,o.replaceState({...o.state,idx:l},``));function u(){return(o.state||{idx:null}).idx}function d(){s=`POP`;let e=u(),t=e==null?null:e-l;l=e,c&&c({action:s,location:h.location,delta:t})}function f(e,t){s=`PUSH`;let r=C(e)?e:k(h.location,e,t);n&&n(r,e),l=u()+1;let d=O(r,l),f=h.createHref(r.mask||r);try{o.pushState(d,``,f)}catch(e){if(e instanceof DOMException&&e.name===`DataCloneError`)throw e;i.location.assign(f)}a&&c&&c({action:s,location:h.location,delta:1})}function p(e,t){s=`REPLACE`;let r=C(e)?e:k(h.location,e,t);n&&n(r,e),l=u();let i=O(r,l),d=h.createHref(r.mask||r);o.replaceState(i,``,d),a&&c&&c({action:s,location:h.location,delta:0})}function m(e){return N(e)}let h={get action(){return s},get location(){return e(i,o)},listen(e){if(c)throw Error(`A history only accepts one active listener`);return i.addEventListener(S,d),c=e,()=>{i.removeEventListener(S,d),c=null}},createHref(e){return t(i,e)},createURL:m,encodeLocation(e){let t=m(e);return{pathname:t.pathname,search:t.search,hash:t.hash}},push:f,replace:p,go(e){return o.go(e)}};return h}function N(e,t=!1){let n=`http://localhost`;typeof window<`u`&&(n=window.location.origin===`null`?window.location.href:window.location.origin),T(n,`No window.location.(origin|href) available to create URL`);let r=typeof e==`string`?e:A(e);return r=r.replace(/ $/,`%20`),!t&&r.startsWith(`//`)&&(r=n+r),new URL(r,n)}function P(e,t,n=`/`){return F(e,t,n,!1)}function F(e,t,n,r,i){let a=se((typeof t==`string`?j(t):t).pathname||`/`,n);if(a==null)return null;let o=i??L(e),s=null,c=K(a);for(let e=0;s==null&&e{let c={relativePath:s===void 0?e.path||``:s,caseSensitive:e.caseSensitive===!0,childrenIndex:a,route:e};if(c.relativePath.startsWith(`/`)){if(!c.relativePath.startsWith(r)&&o)return;T(c.relativePath.startsWith(r),`Absolute route path "${c.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),c.relativePath=c.relativePath.slice(r.length)}let l=ge([r,c.relativePath]),u=n.concat(c);e.children&&e.children.length>0&&(T(e.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${l}".`),R(e.children,t,u,l,o)),!(e.path==null&&!e.index)&&t.push({path:l,score:re(l,e.index),routesMeta:u})};return e.forEach((e,t)=>{if(e.path===``||!e.path?.includes(`?`))a(e,t);else for(let n of z(e.path))a(e,t,!0,n)}),t}function z(e){let t=e.split(`/`);if(t.length===0)return[];let[n,...r]=t,i=n.endsWith(`?`),a=n.replace(/\?$/,``);if(r.length===0)return i?[a,``]:[a];let o=z(r.join(`/`)),s=[];return s.push(...o.map(e=>e===``?a:[a,e].join(`/`))),i&&s.push(...o),s.map(t=>e.startsWith(`/`)&&t===``?`/`:t)}function B(e){e.sort((e,t)=>e.score===t.score?G(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var V=/^:[\w-]+$/,H=3,U=2,W=1,ee=10,te=-2,ne=e=>e===`*`;function re(e,t){let n=e.split(`/`),r=n.length;return n.some(ne)&&(r+=te),t&&(r+=U),n.filter(e=>!ne(e)).reduce((e,t)=>e+(V.test(t)?H:t===``?W:ee),r)}function G(e,t){return e.length===t.length&&e.slice(0,-1).every((e,n)=>e===t[n])?e[e.length-1]-t[t.length-1]:0}function ie(e,t,n=!1){let{routesMeta:r}=e,i={},a=`/`,o=[];for(let e=0;e{if(t===`*`){let e=s[r]||``;o=a.slice(0,a.length-e.length).replace(/(.)\/+$/,`$1`)}let i=s[r];return n&&!i?e[t]=void 0:e[t]=(i||``).replace(/%2F/g,`/`),e},{}),pathname:a,pathnameBase:o,pattern:e}}function oe(e,t=!1,n=!0){E(e===`*`||!e.endsWith(`*`)||e.endsWith(`/*`),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,`/*`)}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,`/*`)}".`);let r=[],i=`^`+e.replace(/\/*\*?$/,``).replace(/^\/*/,`/`).replace(/[\\.*+^${}|()[\]]/g,`\\$&`).replace(/\/:([\w-]+)(\?)?/g,(e,t,n,i,a)=>{if(r.push({paramName:t,isOptional:n!=null}),n){let t=a.charAt(i+e.length);return t&&t!==`/`?`/([^\\/]*)`:`(?:/([^\\/]*))?`}return`/([^\\/]+)`}).replace(/\/([\w-]+)\?(\/|$)/g,`(/$1)?$2`);return e.endsWith(`*`)?(r.push({paramName:`*`}),i+=e===`*`||e===`/*`?`(.*)$`:`(?:\\/(.+)|\\/*)$`):n?i+=`\\/*$`:e!==``&&e!==`/`&&(i+=`(?:(?=\\/|$))`),[new RegExp(i,t?void 0:`i`),r]}function K(e){try{return e.split(`/`).map(e=>decodeURIComponent(e).replace(/\//g,`%2F`)).join(`/`)}catch(t){return E(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function se(e,t){if(t===`/`)return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith(`/`)?t.length-1:t.length,r=e.charAt(n);return r&&r!==`/`?null:e.slice(n)||`/`}var ce=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function le(e,t=`/`){let{pathname:n,search:r=``,hash:i=``}=typeof e==`string`?j(e):e,a;return n?(n=he(n),a=n.startsWith(`/`)?ue(n.substring(1),`/`):ue(n,t)):a=t,{pathname:a,search:ye(r),hash:be(i)}}function ue(e,t){let n=_e(t).split(`/`);return e.split(`/`).forEach(e=>{e===`..`?n.length>1&&n.pop():e!==`.`&&n.push(e)}),n.length>1?n.join(`/`):`/`}function de(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function fe(e){return e.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function pe(e){let t=fe(e);return t.map((e,n)=>n===t.length-1?e.pathname:e.pathnameBase)}function me(e,t,n,r=!1){let i;typeof e==`string`?i=j(e):(i={...e},T(!i.pathname||!i.pathname.includes(`?`),de(`?`,`pathname`,`search`,i)),T(!i.pathname||!i.pathname.includes(`#`),de(`#`,`pathname`,`hash`,i)),T(!i.search||!i.search.includes(`#`),de(`#`,`search`,`hash`,i)));let a=e===``||i.pathname===``,o=a?`/`:i.pathname,s;if(o==null)s=n;else{let e=t.length-1;if(!r&&o.startsWith(`..`)){let t=o.split(`/`);for(;t[0]===`..`;)t.shift(),--e;i.pathname=t.join(`/`)}s=e>=0?t[e]:`/`}let c=le(i,s),l=o&&o!==`/`&&o.endsWith(`/`),u=(a||o===`.`)&&n.endsWith(`/`);return!c.pathname.endsWith(`/`)&&(l||u)&&(c.pathname+=`/`),c}var he=e=>e.replace(/\/\/+/g,`/`),ge=e=>he(e.join(`/`)),_e=e=>e.replace(/\/+$/,``),ve=e=>_e(e).replace(/^\/*/,`/`),ye=e=>!e||e===`?`?``:e.startsWith(`?`)?e:`?`+e,be=e=>!e||e===`#`?``:e.startsWith(`#`)?e:`#`+e,xe=class{constructor(e,t,n,r=!1){this.status=e,this.statusText=t||``,this.internal=r,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function Se(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.internal==`boolean`&&`data`in e}function Ce(e){return ge(e.map(e=>e.route.path).filter(Boolean))||`/`}var we=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;function Te(e,t){let n=e;if(typeof n!=`string`||!ce.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,i=!1;if(we)try{let e=new URL(window.location.href),r=n.startsWith(`//`)?new URL(e.protocol+n):new URL(n),a=se(r.pathname,t);r.origin===e.origin&&a!=null?n=a+r.search+r.hash:i=!0}catch{E(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:i,to:n}}Object.getOwnPropertyNames(Object.prototype).sort().join(`\0`);var Ee=[`POST`,`PUT`,`PATCH`,`DELETE`];new Set(Ee);var De=[`GET`,...Ee];new Set(De);var Oe=x.createContext(null);Oe.displayName=`DataRouter`;var ke=x.createContext(null);ke.displayName=`DataRouterState`;var Ae=x.createContext(!1);function je(){return x.useContext(Ae)}var Me=x.createContext({isTransitioning:!1});Me.displayName=`ViewTransition`;var Ne=x.createContext(new Map);Ne.displayName=`Fetchers`;var Pe=x.createContext(null);Pe.displayName=`Await`;var Fe=x.createContext(null);Fe.displayName=`Navigation`;var Ie=x.createContext(null);Ie.displayName=`Location`;var Le=x.createContext({outlet:null,matches:[],isDataRoute:!1});Le.displayName=`Route`;var Re=x.createContext(null);Re.displayName=`RouteError`;var ze=`REACT_ROUTER_ERROR`,Be=`REDIRECT`,Ve=`ROUTE_ERROR_RESPONSE`;function He(e){if(e.startsWith(`${ze}:${Be}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`&&typeof t.location==`string`&&typeof t.reloadDocument==`boolean`&&typeof t.replace==`boolean`)return t}catch{}}function Ue(e){if(e.startsWith(`${ze}:${Ve}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`)return new xe(t.status,t.statusText,t.data)}catch{}}function We(e,{relative:t}={}){T(Ge(),`useHref() may be used only in the context of a component.`);let{basename:n,navigator:r}=x.useContext(Fe),{hash:i,pathname:a,search:o}=$e(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:ge([n,a])),r.createHref({pathname:s,search:o,hash:i})}function Ge(){return x.useContext(Ie)!=null}function Ke(){return T(Ge(),`useLocation() may be used only in the context of a component.`),x.useContext(Ie).location}var qe=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function Je(e){x.useContext(Fe).static||x.useLayoutEffect(e)}function Ye(){let{isDataRoute:e}=x.useContext(Le);return e?vt():Xe()}function Xe(){T(Ge(),`useNavigate() may be used only in the context of a component.`);let e=x.useContext(Oe),{basename:t,navigator:n}=x.useContext(Fe),{matches:r}=x.useContext(Le),{pathname:i}=Ke(),a=JSON.stringify(pe(r)),o=x.useRef(!1);return Je(()=>{o.current=!0}),x.useCallback((r,s={})=>{if(E(o.current,qe),!o.current)return;if(typeof r==`number`){n.go(r);return}let c=me(r,JSON.parse(a),i,s.relative===`path`);e==null&&t!==`/`&&(c.pathname=c.pathname===`/`?t:ge([t,c.pathname])),(s.replace?n.replace:n.push)(c,s.state,s)},[t,n,a,i,e])}var Ze=x.createContext(null);function Qe(e){let t=x.useContext(Le).outlet;return x.useMemo(()=>t&&x.createElement(Ze.Provider,{value:e},t),[t,e])}function $e(e,{relative:t}={}){let{matches:n}=x.useContext(Le),{pathname:r}=Ke(),i=JSON.stringify(pe(n));return x.useMemo(()=>me(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function et(e,t){return tt(e,t)}function tt(e,t,n){T(Ge(),`useRoutes() may be used only in the context of a component.`);let{navigator:r}=x.useContext(Fe),{matches:i}=x.useContext(Le),a=i[i.length-1],o=a?a.params:{},s=a?a.pathname:`/`,c=a?a.pathnameBase:`/`,l=a&&a.route;{let e=l&&l.path||``;bt(s,!l||e.endsWith(`*`)||e.endsWith(`*?`),`You rendered descendant (or called \`useRoutes()\`) at "${s}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. - -Please change the parent to .`)}let u=Ke(),d;if(t){let e=typeof t==`string`?j(t):t;T(c===`/`||e.pathname?.startsWith(c),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${c}" but pathname "${e.pathname}" was given in the \`location\` prop.`),d=e}else d=u;let f=d.pathname||`/`,p=f;if(c!==`/`){let e=c.replace(/^\//,``).split(`/`);p=`/`+f.replace(/^\//,``).split(`/`).slice(e.length).join(`/`)}let m=n&&n.state.matches.length?n.state.matches.map(e=>Object.assign(e,{route:n.manifest[e.route.id]||e.route})):P(e,{pathname:p});E(l||m!=null,`No routes matched location "${d.pathname}${d.search}${d.hash}" `),E(m==null||m[m.length-1].route.element!==void 0||m[m.length-1].route.Component!==void 0||m[m.length-1].route.lazy!==void 0,`Matched leaf route at location "${d.pathname}${d.search}${d.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let h=ct(m&&m.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:ge([c,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?c:ge([c,r.encodeLocation?r.encodeLocation(e.pathnameBase.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathnameBase])})),i,n);return t&&h?x.createElement(Ie.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,mask:void 0,...d},navigationType:`POP`}},h):h}function nt(){let e=_t(),t=Se(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,r=`rgba(200,200,200, 0.5)`,i={padding:`0.5rem`,backgroundColor:r},a={padding:`2px 4px`,backgroundColor:r},o=null;return console.error(`Error handled by React Router default ErrorBoundary:`,e),o=x.createElement(x.Fragment,null,x.createElement(`p`,null,`💿 Hey developer 👋`),x.createElement(`p`,null,`You can provide a way better UX than this when your app throws errors by providing your own `,x.createElement(`code`,{style:a},`ErrorBoundary`),` or`,` `,x.createElement(`code`,{style:a},`errorElement`),` prop on your route.`)),x.createElement(x.Fragment,null,x.createElement(`h2`,null,`Unexpected Application Error!`),x.createElement(`h3`,{style:{fontStyle:`italic`}},t),n?x.createElement(`pre`,{style:i},n):null,o)}var rt=x.createElement(nt,null),it=class extends x.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!==`idle`&&e.revalidation===`idle`?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error===void 0?t.error:e.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){this.props.onError?this.props.onError(e,t):console.error(`React Router caught the following error during render`,e)}render(){let e=this.state.error;if(this.context&&typeof e==`object`&&e&&`digest`in e&&typeof e.digest==`string`){let t=Ue(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:x.createElement(Le.Provider,{value:this.props.routeContext},x.createElement(Re.Provider,{value:e,children:this.props.component}));return this.context?x.createElement(ot,{error:e},t):t}};it.contextType=Ae;var at=new WeakMap;function ot({children:e,error:t}){let{basename:n}=x.useContext(Fe);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=He(t.digest);if(e){let r=at.get(t);if(r)throw r;let i=Te(e.location,n);if(we&&!at.get(t))if(i.isExternal||e.reloadDocument)window.location.href=i.absoluteURL||i.to;else{let n=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(i.to,{replace:e.replace}));throw at.set(t,n),n}return x.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${i.absoluteURL||i.to}`})}}return e}function st({routeContext:e,match:t,children:n}){let r=x.useContext(Oe);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),x.createElement(Le.Provider,{value:e},n)}function ct(e,t=[],n){let r=n?.state;if(e==null){if(!r)return null;if(r.errors)e=r.matches;else if(t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let i=e,a=r?.errors;if(a!=null){let e=i.findIndex(e=>e.route.id&&a?.[e.route.id]!==void 0);T(e>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(a).join(`,`)}`),i=i.slice(0,Math.min(i.length,e+1))}let o=!1,s=-1;if(n&&r){o=r.renderFallback;for(let e=0;e=0?i.slice(0,s+1):[i[0]];break}}}}let c=n?.onError,l=r&&c?(e,t)=>{c(e,{location:r.location,params:r.matches?.[0]?.params??{},pattern:Ce(r.matches),errorInfo:t})}:void 0;return i.reduceRight((e,n,c)=>{let u,d=!1,f=null,p=null;r&&(u=a&&n.route.id?a[n.route.id]:void 0,f=n.route.errorElement||rt,o&&(s<0&&c===0?(bt(`route-fallback`,!1,"No `HydrateFallback` element provided to render during initial hydration"),d=!0,p=null):s===c&&(d=!0,p=n.route.hydrateFallbackElement||null)));let m=t.concat(i.slice(0,c+1)),h=()=>{let t;return t=u?f:d?p:n.route.Component?x.createElement(n.route.Component,null):n.route.element?n.route.element:e,x.createElement(st,{match:n,routeContext:{outlet:e,matches:m,isDataRoute:r!=null},children:t})};return r&&(n.route.ErrorBoundary||n.route.errorElement||c===0)?x.createElement(it,{location:r.location,revalidation:r.revalidation,component:f,error:u,children:h(),routeContext:{outlet:null,matches:m,isDataRoute:!0},onError:l}):h()},null)}function lt(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function ut(e){let t=x.useContext(Oe);return T(t,lt(e)),t}function dt(e){let t=x.useContext(ke);return T(t,lt(e)),t}function ft(e){let t=x.useContext(Le);return T(t,lt(e)),t}function pt(e){let t=ft(e),n=t.matches[t.matches.length-1];return T(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function mt(){return pt(`useRouteId`)}function ht(){return dt(`useNavigation`).navigation}function gt(){let{matches:e,loaderData:t}=dt(`useMatches`);return x.useMemo(()=>e.map(e=>I(e,t)),[e,t])}function _t(){let e=x.useContext(Re),t=dt(`useRouteError`),n=pt(`useRouteError`);return e===void 0?t.errors?.[n]:e}function vt(){let{router:e}=ut(`useNavigate`),t=pt(`useNavigate`),n=x.useRef(!1);return Je(()=>{n.current=!0}),x.useCallback(async(r,i={})=>{E(n.current,qe),n.current&&(typeof r==`number`?await e.navigate(r):await e.navigate(r,{fromRouteId:t,...i}))},[e,t])}var yt={};function bt(e,t,n){!t&&!yt[e]&&(yt[e]=!0,E(!1,n))}x.memo(xt);function xt({routes:e,manifest:t,future:n,state:r,isStatic:i,onError:a}){return tt(e,void 0,{manifest:t,state:r,isStatic:i,onError:a,future:n})}function St({to:e,replace:t,state:n,relative:r}){T(Ge(),` may be used only in the context of a component.`);let{static:i}=x.useContext(Fe);E(!i,` must not be used on the initial render in a . This is a no-op, but you should modify your code so the is only ever rendered in response to some user interaction or state change.`);let{matches:a}=x.useContext(Le),{pathname:o}=Ke(),s=Ye(),c=me(e,pe(a),o,r===`path`),l=JSON.stringify(c);return x.useEffect(()=>{s(JSON.parse(l),{replace:t,state:n,relative:r})},[s,l,r,t,n]),null}function Ct(e){return Qe(e.context)}function wt(e){T(!1,`A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .`)}function Tt({basename:e=`/`,children:t=null,location:n,navigationType:r=`POP`,navigator:i,static:a=!1,useTransitions:o}){T(!Ge(),`You cannot render a inside another . You should never have more than one in your app.`);let s=e.replace(/^\/*/,`/`),c=x.useMemo(()=>({basename:s,navigator:i,static:a,useTransitions:o,future:{}}),[s,i,a,o]);typeof n==`string`&&(n=j(n));let{pathname:l=`/`,search:u=``,hash:d=``,state:f=null,key:p=`default`,mask:m}=n,h=x.useMemo(()=>{let e=se(l,s);return e==null?null:{location:{pathname:e,search:u,hash:d,state:f,key:p,mask:m},navigationType:r}},[s,l,u,d,f,p,r,m]);return E(h!=null,` is not able to match the URL "${l}${u}${d}" because it does not start with the basename, so the won't render anything.`),h==null?null:x.createElement(Fe.Provider,{value:c},x.createElement(Ie.Provider,{children:t,value:h}))}function Et({children:e,location:t}){return et(Dt(e),t)}x.Component;function Dt(e,t=[]){let n=[];return x.Children.forEach(e,(e,r)=>{if(!x.isValidElement(e))return;let i=[...t,r];if(e.type===x.Fragment){n.push.apply(n,Dt(e.props.children,i));return}T(e.type===wt,`[${typeof e.type==`string`?e.type:e.type.name}] is not a component. All component children of must be a or `),T(!e.props.index||!e.props.children,`An index route cannot have child routes.`);let a={id:e.props.id||i.join(`-`),caseSensitive:e.props.caseSensitive,element:e.props.element,Component:e.props.Component,index:e.props.index,path:e.props.path,middleware:e.props.middleware,loader:e.props.loader,action:e.props.action,hydrateFallbackElement:e.props.hydrateFallbackElement,HydrateFallback:e.props.HydrateFallback,errorElement:e.props.errorElement,ErrorBoundary:e.props.ErrorBoundary,hasErrorBoundary:e.props.hasErrorBoundary===!0||e.props.ErrorBoundary!=null||e.props.errorElement!=null,shouldRevalidate:e.props.shouldRevalidate,handle:e.props.handle,lazy:e.props.lazy};e.props.children&&(a.children=Dt(e.props.children,i)),n.push(a)}),n}var Ot=`get`,kt=`application/x-www-form-urlencoded`;function At(e){return typeof HTMLElement<`u`&&e instanceof HTMLElement}function jt(e){return At(e)&&e.tagName.toLowerCase()===`button`}function Mt(e){return At(e)&&e.tagName.toLowerCase()===`form`}function Nt(e){return At(e)&&e.tagName.toLowerCase()===`input`}function Pt(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function Ft(e,t){return e.button===0&&(!t||t===`_self`)&&!Pt(e)}var It=null;function Lt(){if(It===null)try{new FormData(document.createElement(`form`),0),It=!1}catch{It=!0}return It}var Rt=new Set([`application/x-www-form-urlencoded`,`multipart/form-data`,`text/plain`]);function zt(e){return e!=null&&!Rt.has(e)?(E(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${kt}"`),null):e}function Bt(e,t){let n,r,i,a,o;if(Mt(e)){let o=e.getAttribute(`action`);r=o?se(o,t):null,n=e.getAttribute(`method`)||Ot,i=zt(e.getAttribute(`enctype`))||kt,a=new FormData(e)}else if(jt(e)||Nt(e)&&(e.type===`submit`||e.type===`image`)){let o=e.form;if(o==null)throw Error(`Cannot submit a @@ -166,92 +156,12 @@ const AuthorizationPage: React.FC = () => { } ]; - // 获取表头数据 - const handleFetchHeader = async () => { - setHeaderLoading(true); - try { - const response = await fetchTableHeader(); - if (response.code === 200) { - // 对特定列添加render函数 - const columnsWithRender = response.data.map(col => { - if (col.dataIndex === 'index') { - return { - ...col, - render: (text: number) => {text}, - }; - } - if (col.dataIndex === 'id') { - return { - ...col, - render: (text: string) => {text}, - }; - } - if (col.dataIndex === 'type') { - return { - ...col, - render: (text: string) => { - const config = consumptionTypeConfig[text as keyof typeof consumptionTypeConfig]; - return {config?.label}; - }, - }; - } - if (col.dataIndex === 'amount') { - return { - ...col, - render: (text: number) => {text} 元, - }; - } - return col; - }); - setConsumptionColumns(columnsWithRender); - } else { - message.error(response.message); - } - } catch (error) { - message.error('获取表头失败'); - } finally { - setHeaderLoading(false); - } - }; - - // 打开消耗弹窗 - const handleOpenConsumptionModal = () => { - setShowConsumptionModal(true); - handleFetchHeader(); - }; - - // 处理点击授权按钮 - const handleAuthorize = () => { - if (selectedRowKeys.length === 0) { - message.warning('请先选择需要授权的记录'); - return; - } - setLoading(true); - // 模拟授权操作 - setTimeout(() => { - setAuthorizations(prev => - prev.map(item => - selectedRowKeys.includes(item.id) ? { ...item, status: 'active' } : item - ) - ); - setSelectedRowKeys([]); - setLoading(false); - message.success(`成功授权 ${selectedRowKeys.length} 条记录`); - }, 800); - }; - const tableData = authorizations.map((item, index) => ({ ...item, index: index + 1, key: item.id, })); - const consumptionTableData = consumptionRecords.map((item, index) => ({ - ...item, - index: index + 1, - key: item.id, - })); - return (
{/* 页面标题 */} @@ -259,7 +169,6 @@ const AuthorizationPage: React.FC = () => {

授权管理

- {/*

管理系统授权信息,查看和操作授权状态

*/}
{/* 操作栏 */} @@ -270,7 +179,6 @@ const AuthorizationPage: React.FC = () => { icon={} onClick={handleAuthorize} loading={loading} - // disabled={selectedRowKeys.length === 0} style={{ height: 40, padding: '0 24px', @@ -300,39 +208,8 @@ const AuthorizationPage: React.FC = () => { scroll={{ x: 'max-content' }} /> - - {/* 底部提示 */} - {/*
- 提示:勾选记录后点击"点击授权"按钮可批量授权 -
*/} - - {/* 消耗记录弹窗 */} - setShowConsumptionModal(false)} - footer={null} - width={800} - destroyOnClose={true} - > -
-
`共 ${total} 条记录`, - }} - rowKey="id" - bordered={false} - scroll={{ x: 'max-content' }} - /> - - ); }; -export default AuthorizationPage; \ No newline at end of file +export default AuthorizationPage; diff --git a/video-gen-app/src/pages/ConsumePage.tsx b/video-gen-app/src/pages/ConsumePage.tsx new file mode 100644 index 00000000..0ed9147f --- /dev/null +++ b/video-gen-app/src/pages/ConsumePage.tsx @@ -0,0 +1,120 @@ +import React, { useState, useEffect } from 'react'; +import { Table, Tag, Button, message } from 'antd'; +import { ArrowLeftOutlined } from '@ant-design/icons'; +import { useNavigate } from 'react-router-dom'; + +// 消耗记录数据类型 +interface ConsumptionRecord { + id: string; + authorizationId: string; + amount: number; + type: string; + description: string; + createdAt: string; +} + +// 模拟消耗记录数据 +const mockConsumptionRecords: ConsumptionRecord[] = [ + { id: 'C001', authorizationId: '1867060028363785', amount: 100, type: 'video', description: '视频生成消耗', createdAt: '2024-01-15 10:30:00' }, + { id: 'C002', authorizationId: '1867060028363785', amount: 50, type: 'audio', description: '音频转换消耗', createdAt: '2024-01-15 11:20:00' }, + { id: 'C003', authorizationId: '1867059808785418', amount: 200, type: 'video', description: '视频生成消耗', createdAt: '2024-01-14 14:45:00' }, + { id: 'C004', authorizationId: '1867060028363785', amount: 75, type: 'image', description: '图片处理消耗', createdAt: '2024-01-14 09:15:00' }, + { id: 'C005', authorizationId: '1867059757929740', amount: 150, type: 'video', description: '视频生成消耗', createdAt: '2024-01-13 16:00:00' }, +]; + +// 消耗类型配置 +const consumptionTypeConfig = { + video: { label: '视频生成', color: 'blue' }, + audio: { label: '音频转换', color: 'purple' }, + image: { label: '图片处理', color: 'green' }, +}; + +// 表头配置 +const columns = [ + { title: '序号', dataIndex: 'index', key: 'index', width: 80, fixed: 'left', render: (text: number) => {text} }, + { title: '消耗ID', dataIndex: 'id', key: 'id', ellipsis: true, render: (text: string) => {text} }, + { title: '授权ID', dataIndex: 'authorizationId', key: 'authorizationId', ellipsis: true }, + { + title: '消耗类型', + dataIndex: 'type', + key: 'type', + width: 120, + render: (text: string) => { + const config = consumptionTypeConfig[text as keyof typeof consumptionTypeConfig]; + return {config?.label}; + } + }, + { + title: '消耗金额', + dataIndex: 'amount', + key: 'amount', + width: 120, + render: (text: number) => {text} 元 + }, + { title: '消耗描述', dataIndex: 'description', key: 'description', ellipsis: true }, + { title: '消耗时间', dataIndex: 'createdAt', key: 'createdAt', width: 160, fixed: 'right' }, +]; + +const ConsumePage: React.FC = () => { + const navigate = useNavigate(); + const [consumptionRecords, setConsumptionRecords] = useState(mockConsumptionRecords); + const [loading, setLoading] = useState(false); + + useEffect(() => { + setLoading(true); + // 模拟异步获取数据 + setTimeout(() => { + setConsumptionRecords(mockConsumptionRecords); + setLoading(false); + }, 500); + }, []); + + const tableData = consumptionRecords.map((item, index) => ({ + ...item, + index: index + 1, + key: item.id, + })); + + const handleBack = () => { + navigate('/authorization'); + }; + + return ( +
+ {/* 页面标题和返回按钮 */} +
+ +

+ 消耗记录 +

+
+ + {/* 表格 */} +
+
`共 ${total} 条记录`, + }} + rowKey="id" + bordered={false} + style={{ padding: 16 }} + scroll={{ x: 'max-content' }} + /> + + + ); +}; + +export default ConsumePage; From 83e8ceee7e48c0eb54bb19dcd61ef8e16b603001 Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Thu, 11 Jun 2026 18:13:03 +0800 Subject: [PATCH 6/7] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=BE=AE=E4=BF=A1?= =?UTF-8?q?=E6=94=AF=E4=BB=98=E7=9A=84=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DEPLOYMENT.md | 10 +- video-gen-api/app/api/v1/payments.py | 98 +++++++- video-gen-api/app/services/payment.py | 333 +++++++++++++++++++++++--- video-gen-api/pyproject.toml | 1 + 4 files changed, 397 insertions(+), 45 deletions(-) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index f33f9823..05376543 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -22,8 +22,11 @@ video_item/ | Redis | >= 6 | 可选,推荐用于限流/验证码/Celery | | FFmpeg | 任意 | 可选,用于视频封面截帧 | | alipay-sdk-python | >=3.7.1160 | 可选,用于支付 | +| wechatpayv3 | >=2.0.2 | 可选,用于支付 | +| volcengine-python-sdk | >=1.1.0 | 可选,用于视频生成 | | ca-certificates | 任意 | **必须**,HTTPS 请求需要(新服务器/容器常缺) | + --- ## 二、后端部署 (video-gen-api) @@ -58,8 +61,13 @@ pip install -e ".[pg,redis,celery]" #安装阿里支付sdk pip install -e ".[pg,redis,celery,alipay]" +# 安装微信支付sdk +pip install -e ".[pg,redis,celery,alipay,wechatpayv3]" + #安装火山sdk -pip install -e ".[pg,redis,celery,alipay,volc]" +pip install -e ".[pg,redis,celery,alipay,wechatpayv3,volc]" + + ``` ### 2. 配置环境变量 diff --git a/video-gen-api/app/api/v1/payments.py b/video-gen-api/app/api/v1/payments.py index 0390ea7c..44edb008 100644 --- a/video-gen-api/app/api/v1/payments.py +++ b/video-gen-api/app/api/v1/payments.py @@ -83,13 +83,87 @@ async def recharge( @router.post("/wechat/callback") async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): - data = await request.json() - if not await verify_wechat_callback(data, db): - raise HTTPException(status_code=400, detail="签名验证失败") - order_no = data.get("out_trade_no") - if order_no: - await process_payment_success_by_order_no(db, order_no) - return {"code": "SUCCESS", "message": "OK"} + # 读取微信支付回调数据 + body_bytes = await request.body() + body_str = body_bytes.decode("utf-8") + + # 获取配置 + from app.services.payment import _get_payment_configs, _is_mock_mode, _get_wechat_client + db_configs = await _get_payment_configs(db) + + # 检查 mock 模式 + if _is_mock_mode(db_configs): + try: + import json + data = json.loads(body_str) if body_str else {} + order_no = data.get("out_trade_no") + if order_no: + await process_payment_success_by_order_no(db, order_no) + logger.info(f"Mock WeChat callback processed: order_no={order_no}") + return {"code": "SUCCESS", "message": "OK"} + except Exception as e: + logger.exception(f"Mock WeChat callback error: {e}") + return {"code": "SUCCESS", "message": "OK"} # 微信要求即使处理失败也返回成功 + + # 真实模式:使用 wechatpayv3 SDK 验证回调并解析数据 + try: + mch_id = db_configs.get("payment_wechat_mch_id", "") + private_key = db_configs.get("payment_wechat_private_key", "") + cert_serial_no = db_configs.get("payment_wechat_cert_serial_no", "") + api_v3_key = db_configs.get("payment_wechat_api_v3_key", "") + gateway = db_configs.get("payment_wechat_gateway", "") + + client = _get_wechat_client(mch_id, private_key, cert_serial_no, api_v3_key, gateway) + if not client: + logger.error("WeChat client not initialized for callback") + return {"code": "SUCCESS", "message": "OK"} + + # 从请求头获取必要信息 + headers = dict(request.headers) + timestamp = headers.get("Wechatpay-Timestamp", "") + nonce = headers.get("Wechatpay-Nonce", "") + signature = headers.get("Wechatpay-Signature", "") + serial_no = headers.get("Wechatpay-Serial", "") + + # 验证签名 + is_verified = client.verify( + timestamp=timestamp, + nonce=nonce, + body=body_str, + signature=signature, + serial_no=serial_no + ) + + if not is_verified: + logger.warning("WeChat callback signature verification failed") + raise HTTPException(status_code=400, detail="签名验证失败") + + # 解密回调数据 + decrypted_data = client.decrypt(body_str) + if not decrypted_data: + logger.error("WeChat callback decryption failed") + raise HTTPException(status_code=400, detail="数据解密失败") + + # 处理支付成功回调 + if decrypted_data.get("event_type") == "TRANSACTION.SUCCESS": + resource = decrypted_data.get("resource", {}) + order_no = resource.get("out_trade_no", "") + transaction_id = resource.get("transaction_id", "") + amount_info = resource.get("amount", {}) + total_amount = amount_info.get("total", 0) / 100 # 转换为元 + + if order_no: + await process_payment_success_by_order_no(db, order_no, transaction_id, total_amount) + logger.info( + f"WeChat callback processed: order_no={order_no}, " + f"transaction_id={transaction_id}, amount={total_amount}" + ) + + return {"code": "SUCCESS", "message": "OK"} + except Exception as e: + logger.exception(f"WeChat callback processing error: {e}") + # 微信支付要求即使处理失败也返回成功,避免重复回调 + return {"code": "SUCCESS", "message": "OK"} @router.post("/alipay/callback") @@ -183,13 +257,19 @@ async def cancel_order( if order.status != "pending": raise HTTPException(status_code=400, detail=f"订单状态为{order.status},无法取消") - # If it's an Alipay order, call close API first + # If it's an Alipay or WeChat order, call close API first + db_configs = await _get_payment_configs(db) if order.payment_method == "alipay": - db_configs = await _get_payment_configs(db) try: await _close_alipay_order(db, order, db_configs) except Exception as e: logger.exception(f"Failed to close Alipay order {order_no}: {e}") + elif order.payment_method == "wechat": + try: + from app.services.payment import _close_wechat_order + await _close_wechat_order(db, order, db_configs) + except Exception as e: + logger.exception(f"Failed to close WeChat order {order_no}: {e}") order.status = "cancelled" await db.flush() diff --git a/video-gen-api/app/services/payment.py b/video-gen-api/app/services/payment.py index 57cbca9c..5f0133f0 100644 --- a/video-gen-api/app/services/payment.py +++ b/video-gen-api/app/services/payment.py @@ -143,12 +143,17 @@ async def _check_and_expire_order(db: AsyncSession, order: PaymentOrder) -> bool f"ORDER_EXPIRED order_no={order.order_no} user={order.user_id} " f"amount={order.amount} created_at={order.created_at.isoformat()}" ) - # Also call Alipay close API if it was an Alipay order + # Also call close API if it was an Alipay or WeChat order if order.payment_method == "alipay": try: await _close_alipay_order(db, order, db_configs) except Exception as e: logger.exception(f"Failed to close Alipay order {order.order_no}: {e}") + elif order.payment_method == "wechat": + try: + await _close_wechat_order(db, order, db_configs) + except Exception as e: + logger.exception(f"Failed to close WeChat order {order.order_no}: {e}") return True return False @@ -174,12 +179,17 @@ async def expire_all_pending_orders(db: AsyncSession) -> int: logger.info( f"ORDER_EXPIRED order_no={o.order_no} user={o.user_id} amount={o.amount}" ) - # Also call Alipay close API if it was an Alipay order + # Also call close API if it was an Alipay or WeChat order if o.payment_method == "alipay": try: await _close_alipay_order(db, o, db_configs) except Exception as e: logger.exception(f"Failed to close Alipay order {o.order_no}: {e}") + elif o.payment_method == "wechat": + try: + await _close_wechat_order(db, o, db_configs) + except Exception as e: + logger.exception(f"Failed to close WeChat order {o.order_no}: {e}") if orders: await db.flush() return expired_count @@ -269,8 +279,15 @@ async def create_recharge_order( if not db_configs.get("payment_alipay_app_id") or not db_configs.get("payment_alipay_private_key"): raise ValueError("支付宝支付未完成配置,请联系管理员") elif method == "wechat": - if not db_configs.get("payment_wechat_mch_id") or not db_configs.get("payment_wechat_api_key"): - raise ValueError("微信支付未完成配置,请联系管理员") + required_configs = [ + "payment_wechat_mch_id", + "payment_wechat_private_key", + "payment_wechat_cert_serial_no", + "payment_wechat_api_v3_key" + ] + missing_configs = [c for c in required_configs if not db_configs.get(c)] + if missing_configs: + raise ValueError(f"微信支付未完成配置,缺少: {', '.join(missing_configs)},请联系管理员") total_credits = credits + bonus_credits order = PaymentOrder( @@ -307,7 +324,13 @@ async def create_recharge_order( else: # Real payment: delegate to WeChat or Alipay if method == "wechat": - _create_wechat_order(order, db_configs) + qr_code_content = _create_wechat_order(order, db_configs) + if qr_code_content: + # Attach QR code content to the order instance (transient, not persisted) + order.qr_url = qr_code_content # type: ignore[attr-defined] + else: + # Precreate failed — do not leave a pending order that can never be paid + raise ValueError("微信支付预下单失败,请检查配置或稍后重试") elif method == "alipay": qr_url = _create_alipay_order(order, db_configs) if qr_url: @@ -321,21 +344,231 @@ async def create_recharge_order( # --------------------------------------------------------------------------- -# WeChat (stub) +# WeChat Pay client (lazy singleton, recreated when config changes) # --------------------------------------------------------------------------- +_wechat_client = None +_wechat_mch_id = None -def _create_wechat_order(order: PaymentOrder, db_configs: dict[str, str]) -> None: - """Create a WeChat Pay order. Stub for real integration.""" +def _get_wechat_client( + mch_id: str, + private_key: str, + cert_serial_no: str, + api_v3_key: str, + gateway: str = "" +): + """Get or create a WeChat Pay client. Recreated if config changes.""" + global _wechat_client, _wechat_mch_id + + if _wechat_client is not None and _wechat_mch_id == mch_id: + return _wechat_client + + try: + from wechatpayv3 import WeChatPay, WeChatPayType + except ImportError: + logger.error( + "wechatpayv3 is not installed. " + "Install it with: pip install wechatpayv3" + ) + return None + + try: + # 处理私钥格式,确保是正确的 PEM 格式 + private_key_str = private_key.strip() + if not private_key_str.startswith("-----BEGIN"): + private_key_str = "-----BEGIN PRIVATE KEY-----\n" + private_key_str + "\n-----END PRIVATE KEY-----" + + # 初始化微信支付客户端 + _wechat_client = WeChatPay( + wechatpay_type=WeChatPayType.NATIVE, + mchid=mch_id, + private_key=private_key_str, + cert_serial_no=cert_serial_no, + apiv3_private_key=api_v3_key, + gateway=gateway or "https://api.mch.weixin.qq.com", + ) + _wechat_mch_id = mch_id + logger.info("WeChat Pay client initialized successfully") + return _wechat_client + except Exception as e: + logger.exception(f"Failed to initialize WeChat Pay client: {e}") + _wechat_client = None + _wechat_mch_id = None + return None + + +def _create_wechat_order(order: PaymentOrder, db_configs: dict[str, str]) -> str | None: + """Create a WeChat Pay Native order. Returns QR code content (code_url). + + Reads all WeChat config from the database (admin panel). + Returns the ``code_url`` on success, or ``None`` on failure. + """ mch_id = db_configs.get("payment_wechat_mch_id", "") - api_key = db_configs.get("payment_wechat_api_key", "") - if not mch_id or not api_key: + private_key = db_configs.get("payment_wechat_private_key", "") + cert_serial_no = db_configs.get("payment_wechat_cert_serial_no", "") + api_v3_key = db_configs.get("payment_wechat_api_v3_key", "") + gateway = db_configs.get("payment_wechat_gateway", "") + notify_url = db_configs.get("payment_wechat_notify_url", "") + + if not all([mch_id, private_key, cert_serial_no, api_v3_key]): logger.warning("WeChat payment config missing in database") - return - logger.info( - f"WeChat order created: mch_id={mch_id}, " - f"order_no={order.order_no}, amount={order.amount}" - ) + return None + + client = _get_wechat_client(mch_id, private_key, cert_serial_no, api_v3_key, gateway) + if client is None: + return None + + try: + # 调用微信支付 Native 下单接口 + result = client.pay( + description=f"充值订单 {order.order_no}", + out_trade_no=order.order_no, + amount={ + "total": int(order.amount * 100), # 微信支付以分为单位 + "currency": "CNY" + }, + notify_url=notify_url, + scene_info={ + "payer_client_ip": "127.0.0.1", + } + ) + + if result.get('code_url'): + # 注意:微信返回的 code_url 可能需要进一步处理成二维码图片地址 + logger.info(f"WeChat order created successfully: order_no={order.order_no}") + return result.get('code_url') + else: + logger.error( + f"WeChat pay failed: order_no={order.order_no}, " + f"result={result}" + ) + return None + except Exception as e: + logger.exception(f"WeChat pay exception: order_no={order.order_no}") + return None + + +async def _close_wechat_order(db: AsyncSession, order: PaymentOrder, db_configs: dict[str, str]) -> bool: + """Call WeChat Pay close API to close an unpaid order. + Returns True if the order was closed successfully. + """ + mch_id = db_configs.get("payment_wechat_mch_id", "") + private_key = db_configs.get("payment_wechat_private_key", "") + cert_serial_no = db_configs.get("payment_wechat_cert_serial_no", "") + api_v3_key = db_configs.get("payment_wechat_api_v3_key", "") + gateway = db_configs.get("payment_wechat_gateway", "") + + client = _get_wechat_client(mch_id, private_key, cert_serial_no, api_v3_key, gateway) + if client is None: + return False + + mock_mode = _is_mock_mode(db_configs) + if mock_mode: + logger.info(f"Mock mode: skipping close_wechat_order for {order.order_no}") + return True + + try: + result = client.close(out_trade_no=order.order_no) + if result: + logger.info(f"WeChat order closed: order_no={order.order_no}") + return True + else: + logger.error(f"WeChat close failed: order_no={order.order_no}") + return False + except Exception as e: + logger.exception(f"WeChat close exception: order_no={order.order_no}") + return False + + +async def _query_wechat_order(db: AsyncSession, order: PaymentOrder, db_configs: dict[str, str]) -> dict | None: + """Call WeChat Pay query API to check order status. + Returns the response data if successful, None otherwise. + """ + mch_id = db_configs.get("payment_wechat_mch_id", "") + private_key = db_configs.get("payment_wechat_private_key", "") + cert_serial_no = db_configs.get("payment_wechat_cert_serial_no", "") + api_v3_key = db_configs.get("payment_wechat_api_v3_key", "") + gateway = db_configs.get("payment_wechat_gateway", "") + + client = _get_wechat_client(mch_id, private_key, cert_serial_no, api_v3_key, gateway) + if client is None: + return None + + mock_mode = _is_mock_mode(db_configs) + if mock_mode: + logger.info(f"Mock mode: skipping query_wechat_order for {order.order_no}") + return {"trade_state": "SUCCESS"} + + try: + result = client.query(out_trade_no=order.order_no) + if result.get('trade_state'): + logger.info( + f"WeChat query succeeded: order_no={order.order_no}, " + f"trade_state={result.get('trade_state')}" + ) + return result + else: + logger.error( + f"WeChat query failed: order_no={order.order_no}, " + f"result={result}" + ) + return None + except Exception as e: + logger.exception(f"WeChat query exception: order_no={order.order_no}") + return None + + +async def _refund_wechat_order( + db: AsyncSession, + order: PaymentOrder, + refund_amount: float, + refund_reason: str, + db_configs: dict[str, str] +) -> dict: + """Call WeChat Pay refund API.""" + mch_id = db_configs.get("payment_wechat_mch_id", "") + private_key = db_configs.get("payment_wechat_private_key", "") + cert_serial_no = db_configs.get("payment_wechat_cert_serial_no", "") + api_v3_key = db_configs.get("payment_wechat_api_v3_key", "") + gateway = db_configs.get("payment_wechat_gateway", "") + + client = _get_wechat_client(mch_id, private_key, cert_serial_no, api_v3_key, gateway) + if client is None: + return {"success": False, "message": "微信支付客户端初始化失败"} + + mock_mode = _is_mock_mode(db_configs) + if mock_mode: + logger.info(f"Mock mode: skipping wechat refund for {order.order_no}") + return {"success": True} + + try: + out_refund_no = f"{order.order_no}_refund_{int(datetime.now().timestamp())}" + result = client.refund( + out_trade_no=order.order_no, + out_refund_no=out_refund_no, + amount={ + "total": int(order.amount * 100), # 订单总金额 + "refund": int(refund_amount * 100), # 退款金额 + "currency": "CNY" + }, + reason=refund_reason + ) + + if result.get('status') == 'SUCCESS': + logger.info(f"WeChat refund succeeded: order_no={order.order_no}") + return {"success": True, "refund_id": result.get('refund_id')} + else: + logger.error( + f"WeChat refund failed: order_no={order.order_no}, " + f"result={result}" + ) + return { + "success": False, + "message": f"微信退款失败: {result.get('code', '')}" + } + except Exception as e: + logger.exception(f"WeChat refund exception: order_no={order.order_no}") + return {"success": False, "message": f"微信退款异常: {str(e)}"} # --------------------------------------------------------------------------- @@ -562,7 +795,7 @@ async def _query_alipay_order(db: AsyncSession, order: PaymentOrder, db_configs: async def sync_pending_orders(db: AsyncSession) -> int: - """Check pending orders via Alipay query and update status. + """Check pending orders via Alipay/WeChat query and update status. Returns the number of orders updated. """ result = await db.execute( @@ -576,23 +809,38 @@ async def sync_pending_orders(db: AsyncSession) -> int: db_configs = await _get_payment_configs(db) for order in orders: - if order.payment_method != "alipay": - continue - try: - data = await _query_alipay_order(db, order, db_configs) - if data: - trade_status = data.get("trade_status") - if trade_status in ("TRADE_SUCCESS", "TRADE_FINISHED"): - # Order was paid but we missed the callback - trade_no = data.get("trade_no", "") - await process_payment_success_by_order_no(db, order.order_no, trade_no) - updated_count += 1 - elif trade_status in ("TRADE_CLOSED", "TRADE_CANCELLED"): - # Order was closed on Alipay side - order.status = "cancelled" - await db.flush() - updated_count += 1 + if order.payment_method == "alipay": + data = await _query_alipay_order(db, order, db_configs) + if data: + trade_status = data.get("trade_status") + if trade_status in ("TRADE_SUCCESS", "TRADE_FINISHED"): + # Order was paid but we missed the callback + trade_no = data.get("trade_no", "") + total_amount_str = data.get("total_amount", "") + total_amount = float(total_amount_str) if total_amount_str else None + await process_payment_success_by_order_no(db, order.order_no, trade_no, total_amount) + updated_count += 1 + elif trade_status in ("TRADE_CLOSED", "TRADE_CANCELLED"): + # Order was closed on Alipay side + order.status = "cancelled" + await db.flush() + updated_count += 1 + elif order.payment_method == "wechat": + data = await _query_wechat_order(db, order, db_configs) + if data: + trade_state = data.get("trade_state") + if trade_state == "SUCCESS": + # Order was paid but we missed the callback + transaction_id = data.get("transaction_id", "") + total_amount = float(data.get("amount", {}).get("total", 0)) / 100 + await process_payment_success_by_order_no(db, order.order_no, transaction_id, total_amount) + updated_count += 1 + elif trade_state in ("CLOSED", "REVOKED"): + # Order was closed on WeChat side + order.status = "cancelled" + await db.flush() + updated_count += 1 except Exception as e: logger.exception(f"Failed to sync order {order.order_no}: {e}") @@ -747,12 +995,21 @@ def _verify_alipay_sign(public_key: str, sign_content: str, sign: str, sign_type async def verify_wechat_callback(data: dict, db: AsyncSession) -> bool: - """Verify WeChat payment callback signature.""" + """Verify WeChat payment callback signature. + + Note: Since we're using wechatpayv3 SDK which handles verification internally, + in the callback handler we'll verify with the SDK. This function is kept for + interface consistency and mock mode support. + """ db_configs = await _get_payment_configs(db) mock_mode = _is_mock_mode(db_configs) if mock_mode: + logger.info("Mock mode: skipping WeChat callback verification") return True - logger.info("WeChat callback verification (real mode not implemented)") + + # 对于真实模式,我们在回调路由处理器中直接使用 SDK 验证 + # 这里我们返回 True 以保持接口一致性 + logger.info("WeChat callback verification (delegated to SDK in router)") return True @@ -881,7 +1138,7 @@ async def process_refund( if refund_amount > order.amount: return {"success": False, "message": "退款金额超过订单金额"} - # 如果是支付宝订单,调用支付宝退款API + # 根据支付方式调用相应的退款API db_configs = await _get_payment_configs(db) if order.payment_method == "alipay": refund_result = await _refund_alipay_order( @@ -889,6 +1146,12 @@ async def process_refund( ) if not refund_result.get("success"): return refund_result + elif order.payment_method == "wechat": + refund_result = await _refund_wechat_order( + db, order, refund_amount, refund_reason, db_configs + ) + if not refund_result.get("success"): + return refund_result # 扣除积分 try: diff --git a/video-gen-api/pyproject.toml b/video-gen-api/pyproject.toml index b7721cdb..68477bd0 100644 --- a/video-gen-api/pyproject.toml +++ b/video-gen-api/pyproject.toml @@ -27,6 +27,7 @@ pg = ["asyncpg>=0.30.0"] redis = ["redis>=5.2.0"] celery = ["celery>=5.4.0", "redis>=5.2.0"] alipay = ["alipay-sdk-python>=3.7.1160"] +wxpay = ["wechatpayv3>=2.0.2"] volc = ["volcengine-python-sdk>=1.1.0"] dev = [ "pytest>=8.3.0", From e96f117bbb9984698b023e0d0d60586e41f68296 Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Thu, 11 Jun 2026 18:16:22 +0800 Subject: [PATCH 7/7] =?UTF-8?q?=E5=90=8E=E5=8F=B0=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E9=A1=B5=E9=9D=A2=E5=A2=9E=E5=8A=A0=E5=AF=B9?= =?UTF-8?q?=E5=BA=94=E7=9A=84=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/AdminPaymentConfig.tsx | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/video-gen-admin/src/pages/AdminPaymentConfig.tsx b/video-gen-admin/src/pages/AdminPaymentConfig.tsx index c125f1c1..279aeb69 100644 --- a/video-gen-admin/src/pages/AdminPaymentConfig.tsx +++ b/video-gen-admin/src/pages/AdminPaymentConfig.tsx @@ -22,8 +22,10 @@ const AdminPaymentConfig: React.FC = () => { data.forEach((c: any) => { map[c.key] = c.value; }); form.setFieldsValue({ wechat_mch_id: map['payment_wechat_mch_id'] || '', - wechat_api_key: map['payment_wechat_api_key'] || '', - wechat_cert_path: map['payment_wechat_cert_path'] || '', + wechat_private_key: map['payment_wechat_private_key'] || '', + wechat_cert_serial_no: map['payment_wechat_cert_serial_no'] || '', + wechat_api_v3_key: map['payment_wechat_api_v3_key'] || '', + wechat_gateway: map['payment_wechat_gateway'] || '', wechat_notify_url: map['payment_wechat_notify_url'] || '', alipay_app_id: map['payment_alipay_app_id'] || '', alipay_private_key: map['payment_alipay_private_key'] || '', @@ -51,8 +53,10 @@ const AdminPaymentConfig: React.FC = () => { payment_mock: String(mockMode), payment_wechat_enabled: String(wechatEnabled), payment_wechat_mch_id: values.wechat_mch_id || '', - payment_wechat_api_key: values.wechat_api_key || '', - payment_wechat_cert_path: values.wechat_cert_path || '', + payment_wechat_private_key: values.wechat_private_key || '', + payment_wechat_cert_serial_no: values.wechat_cert_serial_no || '', + payment_wechat_api_v3_key: values.wechat_api_v3_key || '', + payment_wechat_gateway: values.wechat_gateway || '', payment_wechat_notify_url: values.wechat_notify_url || '', payment_alipay_enabled: String(alipayEnabled), payment_alipay_app_id: values.alipay_app_id || '', @@ -150,13 +154,19 @@ const AdminPaymentConfig: React.FC = () => { - - + + - - + + - + + + + + + +