diff --git a/.gitignore b/.gitignore index 7d9e5bf9..ea6cf067 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ bak/ *.tmp .DS_Store .playwright-mcp +*.zip +*.bat # 否定规则(不忽略) # !important.log diff --git a/video-gen-admin/dist/assets/index-CxLKoDUI.js b/video-gen-admin/dist/assets/index-CxLKoDUI.js new file mode 100644 index 00000000..7b74026b --- /dev/null +++ b/video-gen-admin/dist/assets/index-CxLKoDUI.js @@ -0,0 +1,543 @@ +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)),l=(e=>typeof require<`u`?require:typeof Proxy<`u`?new Proxy(e,{get:(e,t)=>(typeof require<`u`?require:e)[t]}):e)(function(e){if(typeof require<`u`)return require.apply(this,arguments);throw Error('Calling `require` for "'+e+"\" in an environment that doesn't expose the `require` function. See https://rolldown.rs/in-depth/bundling-cjs#require-external-modules for more details.")});(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 u=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}}}})),d=o(((e,t)=>{t.exports=u()})),f=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=f()})),m=o((e=>{var t=p();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=m()})),g=o((e=>{var t=d(),n=p(),r=h();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 ie(){H(W),H(ee),H(te)}function ae(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 G(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 le(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 q(e.type,!1);case 11:return q(e.type.render,!1);case 1:return q(e.type,!0);case 31:return se(`Activity`);default:return``}}function ue(e){try{var t=``,n=null;do t+=le(e,n),n=e,e=e.return;while(e);return t}catch(e){return` +Error generating stack: `+e.message+` +`+e.stack}}var de=Object.prototype.hasOwnProperty,fe=t.unstable_scheduleCallback,pe=t.unstable_cancelCallback,me=t.unstable_shouldYield,he=t.unstable_requestPaint,ge=t.unstable_now,_e=t.unstable_getCurrentPriorityLevel,ve=t.unstable_ImmediatePriority,ye=t.unstable_UserBlockingPriority,be=t.unstable_NormalPriority,xe=t.unstable_LowPriority,Se=t.unstable_IdlePriority,Ce=t.log,we=t.unstable_setDisableYieldValue,Te=null,Ee=null;function De(e){if(typeof Ce==`function`&&we(e),Ee&&typeof Ee.setStrictMode==`function`)try{Ee.setStrictMode(Te,e)}catch{}}var Oe=Math.clz32?Math.clz32:je,ke=Math.log,Ae=Math.LN2;function je(e){return e>>>=0,e===0?32:31-(ke(e)/Ae|0)|0}var Me=256,Ne=262144,Pe=4194304;function Fe(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 Ie(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=Fe(n))):i=Fe(o):i=Fe(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=Fe(n))):i=Fe(o)):i=Fe(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 Le(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Re(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 ze(){var e=Pe;return Pe<<=1,!(Pe&62914560)&&(Pe=4194304),e}function Be(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function Ve(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function He(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),en=!1;if($t)try{var tn={};Object.defineProperty(tn,`passive`,{get:function(){en=!0}}),window.addEventListener(`test`,tn,tn),window.removeEventListener(`test`,tn,tn)}catch{en=!1}var nn=null,rn=null,an=null;function on(){if(an)return an;var e,t=rn,n=t.length,r,i=`value`in nn?nn.value:nn.textContent,a=i.length;for(e=0;e=Rn),Vn=` `,Hn=!1;function Un(e,t){switch(e){case`keyup`:return In.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Wn(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var Gn=!1;function Kn(e,t){switch(e){case`compositionend`:return Wn(t);case`keypress`:return t.which===32?(Hn=!0,Vn):null;case`textInput`:return e=t.data,e===Vn&&Hn?null:e;default:return null}}function qn(e,t){if(Gn)return e===`compositionend`||!Ln&&Un(e,t)?(e=on(),an=rn=nn=null,Gn=!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=hr(n)}}function _r(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?_r(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function vr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Dt(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=Dt(e.document)}return t}function yr(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 br=$t&&`documentMode`in document&&11>=document.documentMode,xr=null,Sr=null,Cr=null,wr=!1;function Tr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;wr||xr==null||xr!==Dt(r)||(r=xr,`selectionStart`in r&&yr(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}),Cr&&mr(Cr,r)||(Cr=r,r=kd(Sr,`onSelect`),0>=o,i-=o,gi=1<<32-Oe(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),Ti&&vi(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),Ti&&vi(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 Ti&&vi(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)}),Ti&&vi(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&&va(l)===r.type){n(e,r.sibling),c=a(r,o.props),Ta(c,o),c.return=e,e=c;break a}n(e,r);break}else t(e,r);r=r.sibling}o.type===y?(c=ri(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=ni(o.type,o.key,o.props,null,e.mode,c),Ta(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=oi(o,e.mode,c),c.return=e,e=c}return s(e);case O:return o=va(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,wa(o),c);if(o.$$typeof===C)return b(e,r,qi(e,o),c);Ea(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=ii(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{Ca=0;var i=b(e,t,n,r);return Sa=null,i}catch(t){if(t===fa||t===ma)throw t;var a=Qr(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var Oa=Da(!0),ka=Da(!1),Aa=!1;function ja(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Ma(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 Na(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Pa(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=Yr(e),Jr(e,null,n),t}return J(e,r,t,n),Yr(e)}function Fa(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,We(e,n)}}function Ia(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 La=!1;function Ra(){if(La){var e=ra;if(e!==null)throw e}}function za(e,t,n,r){La=!1;var i=e.updateQueue;Aa=!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===na&&(La=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var h=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(h=g.payload,typeof h==`function`){d=h.call(_,d,f);break a}d=h;break a;case 3:h.flags=h.flags&-65537|128;case 0:if(h=g.payload,f=typeof h==`function`?h.call(_,d,f):h,f==null)break a;d=m({},d,f);break a;case 2:Aa=!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 Ba(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function Va(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,Os(e,!1,t,n);try{var c=i(),l=I.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Ds(e,t,oa(c,r),mu(e)):Ds(e,t,r,mu(e))}catch(n){Ds(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 _s(){}function vs(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=ys(e).queue;gs(e,a,t,R,n===null?_s:function(){return bs(e),n(r)})}function ys(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:ko,lastRenderedState:R},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ko,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function bs(e){var t=ys(e);t.next===null&&(t=e.alternate.memoizedState),Ds(e,t.next.queue,{},mu())}function xs(){return Ki(np)}function Ss(){return wo().memoizedState}function Cs(){return wo().memoizedState}function ws(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=mu();e=Na(n);var r=Pa(t,e,n);r!==null&&(gu(r,t,n),Fa(r,t,n)),t={cache:Qi()},e.payload=t;return}t=t.return}}function Ts(e,t,n){var r=mu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},ks(e)?As(t,n):(n=Kr(e,t,n,r),n!==null&&(gu(n,e,r),js(n,t,r)))}function Es(e,t,n){Ds(e,t,n,mu())}function Ds(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(ks(e))As(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,pr(s,o))return J(e,t,i,0),Il===null&&Gr(),!1}catch{}if(n=Kr(e,t,i,r),n!==null)return gu(n,e,r),js(n,t,r),!0}return!1}function Os(e,t,n,r){if(r={lane:2,revertLane:pd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},ks(e)){if(t)throw Error(i(479))}else t=Kr(e,n,r,2),t!==null&&gu(t,e,2)}function ks(e){var t=e.alternate;return e===ro||t!==null&&t===ro}function As(e,t){so=oo=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function js(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,We(e,n)}}var Ms={readContext:Ki,use:Do,useCallback:mo,useContext:mo,useEffect:mo,useImperativeHandle:mo,useLayoutEffect:mo,useInsertionEffect:mo,useMemo:mo,useReducer:mo,useRef:mo,useState:mo,useDebugValue:mo,useDeferredValue:mo,useTransition:mo,useSyncExternalStore:mo,useId:mo,useHostTransitionStatus:mo,useFormState:mo,useActionState:mo,useOptimistic:mo,useMemoCache:mo,useCacheRefresh:mo};Ms.useEffectEvent=mo;var Ns={readContext:Ki,use:Do,useCallback:function(e,t){return Co().memoizedState=[e,t===void 0?null:t],e},useContext:Ki,useEffect:rs,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),ts(4194308,4,ls.bind(null,t,e),n)},useLayoutEffect:function(e,t){return ts(4194308,4,e,t)},useInsertionEffect:function(e,t){ts(4,2,e,t)},useMemo:function(e,t){var n=Co();t=t===void 0?null:t;var r=e();if(co){De(!0);try{e()}finally{De(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=Co();if(n!==void 0){var i=n(t);if(co){De(!0);try{n(t)}finally{De(!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=Ts.bind(null,ro,e),[r.memoizedState,e]},useRef:function(e){var t=Co();return e={current:e},t.memoizedState=e},useState:function(e){e=zo(e);var t=e.queue,n=Es.bind(null,ro,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:ds,useDeferredValue:function(e,t){return ms(Co(),e,t)},useTransition:function(){var e=zo(!1);return e=gs.bind(null,ro,e.queue,!0,!1),Co().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=ro,a=Co();if(Ti){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),Il===null)throw Error(i(349));Rl&127||Po(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,rs(Io.bind(null,r,o,e),[e]),r.flags|=2048,$o(9,{destroy:void 0},Fo.bind(null,r,o,n,t),null),n},useId:function(){var e=Co(),t=Il.identifierPrefix;if(Ti){var n=_i,r=gi;n=(r&~(1<<32-Oe(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=lo++,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[Ze]=t,o[Qe]=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&&Dc(t)}}return Mc(t),Oc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Dc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=te.current,Mi(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=Ci,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[Ze]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Fd(e.nodeValue,n)),e||ki(t,!0)}else e=Wd(e).createTextNode(r),e[Ze]=t,t.stateNode=e}return Mc(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Mi(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[Ze]=t}else Ni(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Mc(t),e=!1}else n=Pi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?($a(t),t):($a(t),null);if(t.flags&128)throw Error(i(558))}return Mc(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=Mi(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[Ze]=t}else Ni(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Mc(t),a=!1}else a=Pi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?($a(t),t):($a(t),null)}return $a(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),Ac(t,t.updateQueue),Mc(t),null);case 4:return ie(),e===null&&Td(t.stateNode.containerInfo),Mc(t),null;case 10:return Bi(t.type),Mc(t),null;case 19:if(H(eo),r=t.memoizedState,r===null)return Mc(t),null;if(a=(t.flags&128)!=0,o=r.rendering,o===null)if(a)jc(r,!1);else{if(Gl!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=to(e),o!==null){for(t.flags|=128,jc(r,!1),e=o.updateQueue,t.updateQueue=e,Ac(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)ti(n,e),n=n.sibling;return U(eo,eo.current&1|2),Ti&&vi(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&ge()>nu&&(t.flags|=128,a=!0,jc(r,!1),t.lanes=4194304)}else{if(!a)if(e=to(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Ac(t,e),jc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!Ti)return Mc(t),null}else 2*ge()-r.renderingStartTime>nu&&n!==536870912&&(t.flags|=128,a=!0,jc(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?(Mc(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=ge(),e.sibling=null,n=eo.current,U(eo,a?n&1|2:n&1),Ti&&vi(t,r.treeForkCount),e);case 22:case 23:return $a(t),Ka(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(Mc(t),t.subtreeFlags&6&&(t.flags|=8192)):Mc(t),n=t.updateQueue,n!==null&&Ac(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(ca),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Bi(Zi),Mc(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function Pc(e,t){switch(xi(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Bi(Zi),ie(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return G(t),null;case 31:if(t.memoizedState!==null){if($a(t),t.alternate===null)throw Error(i(340));Ni()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if($a(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Ni()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return H(eo),null;case 4:return ie(),null;case 10:return Bi(t.type),null;case 22:case 23:return $a(t),Ka(),e!==null&&H(ca),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Bi(Zi),null;case 25:return null;default:return null}}function Fc(e,t){switch(xi(t),t.tag){case 3:Bi(Zi),ie();break;case 26:case 27:case 5:G(t);break;case 4:ie();break;case 31:t.memoizedState!==null&&$a(t);break;case 13:$a(t);break;case 19:H(eo);break;case 10:Bi(t.type);break;case 22:case 23:$a(t),Ka(),e!==null&&H(ca);break;case 24:Bi(Zi)}}function Ic(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 Lc(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 Rc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{Va(t,n)}catch(t){Ku(e,e.return,t)}}}function zc(e,t,n){n.props=Bs(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Ku(e,t,n)}}function Bc(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 Vc(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 Hc(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 Uc(e,t,n){try{var r=e.stateNode;zd(r,e.type,n,t),r[Qe]=t}catch(t){Ku(e,e.return,t)}}function Wc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&tf(e.type)||e.tag===4}function Gc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Wc(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 Kc(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=Wt));else if(r!==4&&(r===27&&tf(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Kc(e,t,n),e=e.sibling;e!==null;)Kc(e,t,n),e=e.sibling}function qc(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(qc(e,t,n),e=e.sibling;e!==null;)qc(e,t,n),e=e.sibling}function Jc(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[Ze]=e,t[Qe]=n}catch(t){Ku(e,e.return,t)}}var Yc=!1,Xc=!1,Zc=!1,Qc=typeof WeakSet==`function`?WeakSet:Set,$c=null;function el(e,t){if(e=e.containerInfo,Hd=dp,e=vr(e),yr(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,$c=t;$c!==null;)if(t=$c,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,$c=e;else for(;$c!==null;){switch(t=$c,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[Ze]=e,ut(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 _=gr(s,h),v=gr(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),Sl(o,o.current,s,n),Fl=c,od(0,!1),Ee&&typeof Ee.onPostCommitFiberRoot==`function`)try{Ee.onPostCommitFiberRoot(Te,o)}catch{}return!0}finally{L.p=a,I.T=r,Hu(e,t)}}function Gu(e,t,n){t=ci(n,t),t=Ks(e.stateNode,t,2),e=Pa(e,t,2),e!==null&&(Ve(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=ci(n,e),n=qs(2),r=Pa(t,n,2),r!==null&&(Js(n,r,t,e),Ve(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>ge()-eu?!(Fl&2)&&Cu(e,0):Jl|=n,Xl===Rl&&(Xl=0)),ad(e)}function Yu(e,t){t===0&&(t=ze()),e=qr(e,t),e!==null&&(Ve(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 fe(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-Oe(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=Ie(r,r===Il?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||Le(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=ge(),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=kt(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),ut(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="`+kt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+kt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+kt(n.imageSizes)+`"]`)):i+=`[href="`+kt(e)+`"]`;var a=i;switch(t){case`style`:a=Pf(e);break;case`script`:a=Rf(e)}vf.has(a)||(e=m({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),ut(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="`+kt(r)+`"][href="`+kt(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=m({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),ut(r),n.head.appendChild(r)}}}function Af(e,t,n){xf.S(e,t,n);var r=wf;if(r&&e){var i=lt(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=m({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=vf.get(a))&&Hf(e,n);var c=o=r.createElement(`link`);ut(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=lt(n).hoistableScripts,i=Rf(e),a=r.get(i);a||(a=n.querySelector(zf(i)),a||(e=m({src:e,async:!0},t),(t=vf.get(i))&&Uf(e,t),a=n.createElement(`script`),ut(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=lt(n).hoistableScripts,i=Rf(e),a=r.get(i);a||(a=n.querySelector(zf(i)),a||(e=m({src:e,async:!0,type:`module`},t),(t=vf.get(i))&&Uf(e,t),a=n.createElement(`script`),ut(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=lt(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=lt(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=lt(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="`+kt(e)+`"`}function Ff(e){return`link[rel="stylesheet"][`+e+`]`}function If(e){return m({},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),ut(t),e.head.appendChild(t))}function Rf(e){return`[src="`+kt(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~="`+kt(n.href)+`"]`);if(r)return t.instance=r,ut(r),r;var a=m({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),ut(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,ut(o),o;r=If(n),(a=vf.get(a))&&Hf(r,a),o=(e.ownerDocument||e).createElement(`link`),ut(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,ut(a),a):(r=n,(a=vf.get(o))&&(r=m({},n),Uf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),ut(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,ut(a);return}a=t.ownerDocument||t,r=If(r),(i=vf.get(i))&&Hf(r,i),a=a.createElement(`link`),ut(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=g()}))(),v=`modulepreload`,y=function(e){return`/`+e},b={},x=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=y(t,n),t in b)return;b[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`:v,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)})},S=c(p(),1),C=`popstate`;function w(e){return typeof e==`object`&&!!e&&`pathname`in e&&`search`in e&&`hash`in e&&`state`in e&&`key`in e}function T(e={}){function t(e,t){let n=t.state?.masked,{pathname:r,search:i,hash:a}=n||e.location;return A(``,{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:j(t)}return N(t,n,null,e)}function E(e,t){if(e===!1||e==null)throw Error(t)}function D(e,t){if(!e){typeof console<`u`&&console.warn(t);try{throw Error(t)}catch{}}}function O(){return Math.random().toString(36).substring(2,10)}function k(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 A(e,t,n=null,r,i){return{pathname:typeof e==`string`?e:e.pathname,search:``,hash:``,...typeof t==`string`?M(t):t,state:n,key:t&&t.key||r||O(),mask:i}}function j({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 M(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 N(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=w(e)?e:A(h.location,e,t);n&&n(r,e),l=u()+1;let d=k(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=w(e)?e:A(h.location,e,t);n&&n(r,e),l=u();let i=k(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 P(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(C,d),c=e,()=>{i.removeEventListener(C,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 P(e,t=!1){let n=`http://localhost`;typeof window<`u`&&(n=window.location.origin===`null`?window.location.href:window.location.origin),E(n,`No window.location.(origin|href) available to create URL`);let r=typeof e==`string`?e:j(e);return r=r.replace(/ $/,`%20`),!t&&r.startsWith(`//`)&&(r=n+r),new URL(r,n)}function F(e,t,n=`/`){return I(e,t,n,!1)}function I(e,t,n,r,i){let a=ce((typeof t==`string`?M(t):t).pathname||`/`,n);if(a==null)return null;let o=i??R(e),s=null,c=se(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;E(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&&(E(e.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${l}".`),z(e.children,t,u,l,o)),!(e.path==null&&!e.index)&&t.push({path:l,score:ie(l,e.index),routesMeta:u})};return e.forEach((e,t)=>{if(e.path===``||!e.path?.includes(`?`))a(e,t);else for(let n of B(e.path))a(e,t,!0,n)}),t}function B(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=B(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 V(e){e.sort((e,t)=>e.score===t.score?ae(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var H=/^:[\w-]+$/,U=3,W=2,ee=1,te=10,ne=-2,re=e=>e===`*`;function ie(e,t){let n=e.split(`/`),r=n.length;return n.some(re)&&(r+=ne),t&&(r+=W),n.filter(e=>!re(e)).reduce((e,t)=>e+(H.test(t)?U:t===``?ee:te),r)}function ae(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 G(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 K(e,t=!1,n=!0){D(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 se(e){try{return e.split(`/`).map(e=>decodeURIComponent(e).replace(/\//g,`%2F`)).join(`/`)}catch(t){return D(!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 ce(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 q=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function le(e,t=`/`){let{pathname:n,search:r=``,hash:i=``}=typeof e==`string`?M(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=M(e):(i={...e},E(!i.pathname||!i.pathname.includes(`?`),de(`?`,`pathname`,`search`,i)),E(!i.pathname||!i.pathname.includes(`#`),de(`#`,`pathname`,`hash`,i)),E(!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`||!q.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=ce(r.pathname,t);r.origin===e.origin&&a!=null?n=a+r.search+r.hash:i=!0}catch{D(!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=S.createContext(null);Oe.displayName=`DataRouter`;var ke=S.createContext(null);ke.displayName=`DataRouterState`;var Ae=S.createContext(!1);function je(){return S.useContext(Ae)}var Me=S.createContext({isTransitioning:!1});Me.displayName=`ViewTransition`;var Ne=S.createContext(new Map);Ne.displayName=`Fetchers`;var Pe=S.createContext(null);Pe.displayName=`Await`;var Fe=S.createContext(null);Fe.displayName=`Navigation`;var Ie=S.createContext(null);Ie.displayName=`Location`;var Le=S.createContext({outlet:null,matches:[],isDataRoute:!1});Le.displayName=`Route`;var Re=S.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}={}){E(Ge(),`useHref() may be used only in the context of a component.`);let{basename:n,navigator:r}=S.useContext(Fe),{hash:i,pathname:a,search:o}=et(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:ge([n,a])),r.createHref({pathname:s,search:o,hash:i})}function Ge(){return S.useContext(Ie)!=null}function Ke(){return E(Ge(),`useLocation() may be used only in the context of a component.`),S.useContext(Ie).location}var qe=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function Je(e){S.useContext(Fe).static||S.useLayoutEffect(e)}function Ye(){let{isDataRoute:e}=S.useContext(Le);return e?yt():Xe()}function Xe(){E(Ge(),`useNavigate() may be used only in the context of a component.`);let e=S.useContext(Oe),{basename:t,navigator:n}=S.useContext(Fe),{matches:r}=S.useContext(Le),{pathname:i}=Ke(),a=JSON.stringify(pe(r)),o=S.useRef(!1);return Je(()=>{o.current=!0}),S.useCallback((r,s={})=>{if(D(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=S.createContext(null);function Qe(e){let t=S.useContext(Le).outlet;return S.useMemo(()=>t&&S.createElement(Ze.Provider,{value:e},t),[t,e])}function $e(){let{matches:e}=S.useContext(Le);return e[e.length-1]?.params??{}}function et(e,{relative:t}={}){let{matches:n}=S.useContext(Le),{pathname:r}=Ke(),i=JSON.stringify(pe(n));return S.useMemo(()=>me(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function tt(e,t){return nt(e,t)}function nt(e,t,n){E(Ge(),`useRoutes() may be used only in the context of a component.`);let{navigator:r}=S.useContext(Fe),{matches:i}=S.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||``;xt(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`?M(t):t;E(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})):F(e,{pathname:p});D(l||m!=null,`No routes matched location "${d.pathname}${d.search}${d.hash}" `),D(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=lt(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?S.createElement(Ie.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,mask:void 0,...d},navigationType:`POP`}},h):h}function rt(){let e=vt(),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=S.createElement(S.Fragment,null,S.createElement(`p`,null,`💿 Hey developer 👋`),S.createElement(`p`,null,`You can provide a way better UX than this when your app throws errors by providing your own `,S.createElement(`code`,{style:a},`ErrorBoundary`),` or`,` `,S.createElement(`code`,{style:a},`errorElement`),` prop on your route.`)),S.createElement(S.Fragment,null,S.createElement(`h2`,null,`Unexpected Application Error!`),S.createElement(`h3`,{style:{fontStyle:`italic`}},t),n?S.createElement(`pre`,{style:i},n):null,o)}var it=S.createElement(rt,null),at=class extends S.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:S.createElement(Le.Provider,{value:this.props.routeContext},S.createElement(Re.Provider,{value:e,children:this.props.component}));return this.context?S.createElement(st,{error:e},t):t}};at.contextType=Ae;var ot=new WeakMap;function st({children:e,error:t}){let{basename:n}=S.useContext(Fe);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=He(t.digest);if(e){let r=ot.get(t);if(r)throw r;let i=Te(e.location,n);if(we&&!ot.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 ot.set(t,n),n}return S.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${i.absoluteURL||i.to}`})}}return e}function ct({routeContext:e,match:t,children:n}){let r=S.useContext(Oe);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),S.createElement(Le.Provider,{value:e},n)}function lt(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);E(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||it,o&&(s<0&&c===0?(xt(`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?S.createElement(n.route.Component,null):n.route.element?n.route.element:e,S.createElement(ct,{match:n,routeContext:{outlet:e,matches:m,isDataRoute:r!=null},children:t})};return r&&(n.route.ErrorBoundary||n.route.errorElement||c===0)?S.createElement(at,{location:r.location,revalidation:r.revalidation,component:f,error:u,children:h(),routeContext:{outlet:null,matches:m,isDataRoute:!0},onError:l}):h()},null)}function ut(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function dt(e){let t=S.useContext(Oe);return E(t,ut(e)),t}function ft(e){let t=S.useContext(ke);return E(t,ut(e)),t}function pt(e){let t=S.useContext(Le);return E(t,ut(e)),t}function mt(e){let t=pt(e),n=t.matches[t.matches.length-1];return E(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function ht(){return mt(`useRouteId`)}function gt(){return ft(`useNavigation`).navigation}function _t(){let{matches:e,loaderData:t}=ft(`useMatches`);return S.useMemo(()=>e.map(e=>L(e,t)),[e,t])}function vt(){let e=S.useContext(Re),t=ft(`useRouteError`),n=mt(`useRouteError`);return e===void 0?t.errors?.[n]:e}function yt(){let{router:e}=dt(`useNavigate`),t=mt(`useNavigate`),n=S.useRef(!1);return Je(()=>{n.current=!0}),S.useCallback(async(r,i={})=>{D(n.current,qe),n.current&&(typeof r==`number`?await e.navigate(r):await e.navigate(r,{fromRouteId:t,...i}))},[e,t])}var bt={};function xt(e,t,n){!t&&!bt[e]&&(bt[e]=!0,D(!1,n))}S.memo(St);function St({routes:e,manifest:t,future:n,state:r,isStatic:i,onError:a}){return nt(e,void 0,{manifest:t,state:r,isStatic:i,onError:a,future:n})}function Ct({to:e,replace:t,state:n,relative:r}){E(Ge(),` may be used only in the context of a component.`);let{static:i}=S.useContext(Fe);D(!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}=S.useContext(Le),{pathname:o}=Ke(),s=Ye(),c=me(e,pe(a),o,r===`path`),l=JSON.stringify(c);return S.useEffect(()=>{s(JSON.parse(l),{replace:t,state:n,relative:r})},[s,l,r,t,n]),null}function wt(e){return Qe(e.context)}function Tt(e){E(!1,`A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .`)}function Et({basename:e=`/`,children:t=null,location:n,navigationType:r=`POP`,navigator:i,static:a=!1,useTransitions:o}){E(!Ge(),`You cannot render a inside another . You should never have more than one in your app.`);let s=e.replace(/^\/*/,`/`),c=S.useMemo(()=>({basename:s,navigator:i,static:a,useTransitions:o,future:{}}),[s,i,a,o]);typeof n==`string`&&(n=M(n));let{pathname:l=`/`,search:u=``,hash:d=``,state:f=null,key:p=`default`,mask:m}=n,h=S.useMemo(()=>{let e=ce(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 D(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:S.createElement(Fe.Provider,{value:c},S.createElement(Ie.Provider,{children:t,value:h}))}function Dt({children:e,location:t}){return tt(Ot(e),t)}S.Component;function Ot(e,t=[]){let n=[];return S.Children.forEach(e,(e,r)=>{if(!S.isValidElement(e))return;let i=[...t,r];if(e.type===S.Fragment){n.push.apply(n,Ot(e.props.children,i));return}E(e.type===Tt,`[${typeof e.type==`string`?e.type:e.type.name}] is not a component. All component children of must be a or `),E(!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=Ot(e.props.children,i)),n.push(a)}),n}var kt=`get`,At=`application/x-www-form-urlencoded`;function jt(e){return typeof HTMLElement<`u`&&e instanceof HTMLElement}function Mt(e){return jt(e)&&e.tagName.toLowerCase()===`button`}function Nt(e){return jt(e)&&e.tagName.toLowerCase()===`form`}function Pt(e){return jt(e)&&e.tagName.toLowerCase()===`input`}function Ft(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function It(e,t){return e.button===0&&(!t||t===`_self`)&&!Ft(e)}var Lt=null;function Rt(){if(Lt===null)try{new FormData(document.createElement(`form`),0),Lt=!1}catch{Lt=!0}return Lt}var zt=new Set([`application/x-www-form-urlencoded`,`multipart/form-data`,`text/plain`]);function Bt(e){return e!=null&&!zt.has(e)?(D(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${At}"`),null):e}function Vt(e,t){let n,r,i,a,o;if(Nt(e)){let o=e.getAttribute(`action`);r=o?ce(o,t):null,n=e.getAttribute(`method`)||kt,i=Bt(e.getAttribute(`enctype`))||At,a=new FormData(e)}else if(Mt(e)||Pt(e)&&(e.type===`submit`||e.type===`image`)){let o=e.form;if(o==null)throw Error(`Cannot submit a + + {exportProgress &&
{exportProgress}
} { loading={loading} pagination={{ current: page, - pageSize: pageSize, - total: total, - onChange: handlePageChange, + pageSize, + total, + onChange: (p, ps) => { setPage(p); setPageSize(ps); }, showSizeChanger: true, showTotal: (t) => `共 ${t} 条记录`, }} - scroll={{ x: 800 }} + scroll={{ x: 1900 }} /> diff --git a/video-gen-admin/src/pages/AdminUsers.tsx b/video-gen-admin/src/pages/AdminUsers.tsx index 2e1d06a7..6fb650c3 100644 --- a/video-gen-admin/src/pages/AdminUsers.tsx +++ b/video-gen-admin/src/pages/AdminUsers.tsx @@ -5,7 +5,7 @@ import { import { UserOutlined, WalletOutlined, SearchOutlined, StopOutlined, CheckCircleOutlined, PlusOutlined, MenuOutlined, LockOutlined, SettingOutlined, SaveOutlined, } from '@ant-design/icons'; -import { getAdminUsers, adjustCredits, toggleUserStatus, createUser, updateUserMenus, getMenuConfigs, resetUserPassword, getSystemConfigs, updateSystemConfig } from '../api'; +import { getAdminUsers, adjustCredits, toggleUserStatus, createUser, updateUserMenus, getMenuConfigs, resetUserPassword, getSystemConfigs, updateSystemConfig, updateFrontendUserKind } from '../api'; import type { AdminUser, SystemConfig } from '../types'; import { formatDate } from '../utils/formatDate'; @@ -14,6 +14,7 @@ const AdminUsers: React.FC = () => { const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); const [activeTab, setActiveTab] = useState('frontend'); + const [frontendKindFilter, setFrontendKindFilter] = useState(''); const [creditModal, setCreditModal] = useState<{ open: boolean; user: AdminUser | null }>({ open: false, user: null }); const [createModal, setCreateModal] = useState(false); const [createType, setCreateType] = useState('frontend'); @@ -36,14 +37,14 @@ const AdminUsers: React.FC = () => { const load = async () => { setLoading(true); try { - const data = await getAdminUsers(page, pageSize, search || undefined); + const data = await getAdminUsers(page, pageSize, search || undefined, activeTab, activeTab === 'frontend' ? (frontendKindFilter || undefined) : undefined); setUsers(data.items || []); setTotal(data.total || 0); } catch { /* auth error handled by client */ } setLoading(false); }; - useEffect(() => { load(); }, [page, pageSize, search]); + useEffect(() => { load(); }, [page, pageSize, search, activeTab, frontendKindFilter]); useEffect(() => { const loadCreditConfigs = async () => { @@ -116,6 +117,7 @@ const AdminUsers: React.FC = () => { phone: userType === 'frontend' ? values.phone : (values.phone || undefined), credits: values.credits || 0, user_type: userType, + frontend_user_kind: values.frontend_user_kind || 'external', }); message.success('用户创建成功'); setCreateModal(false); @@ -176,7 +178,16 @@ const AdminUsers: React.FC = () => { } catch { /* validation */ } }; - const filteredUsers = users.filter(u => u.userType === activeTab); + const handleUpdateFrontendKind = async (user: AdminUser, kind: 'internal' | 'external') => { + try { + await updateFrontendUserKind(user.id, kind); + message.success(kind === 'internal' ? '已设为前台内部用户' : '已设为前台外部用户'); + load(); + } catch (e: any) { + message.error(e?.message || '设置失败'); + } + }; + const isAdminTab = activeTab === 'admin'; const columns = [ @@ -214,6 +225,10 @@ const AdminUsers: React.FC = () => { title: '手机号', dataIndex: 'phone', width: 130, render: (v: string) => {v || '-'}, }, + ...(!isAdminTab ? [{ + title: '前台归类', dataIndex: 'frontendUserKind', width: 110, + render: (v: string) => {v === 'internal' ? '内部用户' : '外部用户'}, + }] : []), { title: '状态', dataIndex: 'isActive', width: 80, render: (v: boolean) => ( @@ -238,6 +253,12 @@ const AdminUsers: React.FC = () => { 调整积分 )} + {!isAdminTab && r.frontendUserKind !== 'internal' && ( + + )} + {!isAdminTab && r.frontendUserKind === 'internal' && ( + + )}
{ { value: 'admin', label: '后台管理员' }, ]} /> + {createType === 'frontend' && ( + + diff --git a/video-gen-admin/src/pages/Adminplatform.tsx b/video-gen-admin/src/pages/Adminplatform.tsx index 8c6cafe4..a0506a51 100644 --- a/video-gen-admin/src/pages/Adminplatform.tsx +++ b/video-gen-admin/src/pages/Adminplatform.tsx @@ -18,6 +18,26 @@ interface OpenType { updatedAt: string; } +const normalizeUploadError = (e: unknown): Error => { + if (e instanceof Error) { + return e; + } + + if (typeof e === 'string') { + return new Error(e); + } + + return new Error('上传失败'); +}; + +const assertUploadFile = (file: unknown): File => { + if (file instanceof File) { + return file; + } + + throw new Error('请选择有效图片文件'); +}; + const AdminPlatform: React.FC = () => { const [openTypes, setOpenTypes] = useState([]); const [total, setTotal] = useState(0); @@ -49,7 +69,9 @@ const AdminPlatform: React.FC = () => { } }; - useEffect(() => { load(); }, []); + useEffect(() => { + load(); + }, []); const handleCreate = async () => { try { @@ -137,53 +159,89 @@ const AdminPlatform: React.FC = () => { const columns = [ { - title: 'ID', dataIndex: 'id', width: 100, + title: 'ID', + dataIndex: 'id', + width: 100, render: (v: string) => {v}, }, { - title: '开户类型', dataIndex: 'open_type', width: 100, + title: '开户类型', + dataIndex: 'open_type', + width: 100, render: (v: number) => {v}, }, { - title: '类型名称', dataIndex: 'type_name', width: 150, + title: '类型名称', + dataIndex: 'type_name', + width: 150, render: (v: string) => {v}, }, { - title: '描述', dataIndex: 'description', width: 250, ellipsis: true, - render: (v: string) => {v}, + title: '描述', + dataIndex: 'description', + width: 250, + ellipsis: true, + render: (v: string) => ( + + {v} + + ), }, { - title: '缩略图', dataIndex: 'thumb', width: 120, - render: (v: string) => v ? thumb : '-', + title: '缩略图', + dataIndex: 'thumb', + width: 120, + render: (v: string) => ( + v ? thumb : '-' + ), }, { - title: '创建时间', dataIndex: 'createdAt', width: 160, - render: (v: string) => {formatDate(v)}, + title: '创建时间', + dataIndex: 'createdAt', + width: 160, + render: (v: string) => ( + + {formatDate(v)} + + ), }, { - title: '更新时间', dataIndex: 'updatedAt', width: 160, - render: (v: string) => {formatDate(v)}, + title: '更新时间', + dataIndex: 'updatedAt', + width: 160, + render: (v: string) => ( + + {formatDate(v)} + + ), }, { - title: '操作', width: 200, + title: '操作', + width: 200, render: (_: any, record: OpenType) => ( + > + 详情 + + > + 更新 + + > + 删除 + ), }, @@ -198,8 +256,12 @@ const AdminPlatform: React.FC = () => { 平台开户方式管理 - - + +
{ pageSize, total, showTotal: (t) => `共 ${t} 条记录`, - onChange: (p, ps) => { setPage(p); setPageSize(ps); load(p, ps); }, + onChange: (p, ps) => { + setPage(p); + setPageSize(ps); + load(p, ps); + }, }} scroll={{ x: 1000 }} /> @@ -231,7 +297,6 @@ const AdminPlatform: React.FC = () => { width={600} > -
{ name="type_name" label="类型名称" style={{ flex: 1 }} - rules={[{ required: true, message: '请输入类型名称' }, { max: 100, message: '类型名称不能超过100个字符' }]} + rules={[ + { required: true, message: '请输入类型名称' }, + { max: 100, message: '类型名称不能超过100个字符' }, + ]} >
- + { listType="picture-card" customRequest={async ({ file, onSuccess, onError }) => { try { - const res = await uploadImage(file as File); + const uploadFile = assertUploadFile(file); + const res = await uploadImage(uploadFile); console.log(res); createForm.setFieldsValue({ thumb: res.url }); - onSuccess(res); - } catch (e: any) { - onError(e); + onSuccess?.(res); + } catch (e) { + onError?.(normalizeUploadError(e)); } }} > @@ -301,7 +370,16 @@ const AdminPlatform: React.FC = () => {

开户类型: {currentOpenType.open_type}

类型名称: {currentOpenType.type_name}

描述: {currentOpenType.description}

-

缩略图: {currentOpenType.thumb ? thumb : '-'}

+

+ 缩略图:{' '} + {currentOpenType.thumb ? ( + thumb + ) : '-'} +

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

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

@@ -332,7 +410,10 @@ const AdminPlatform: React.FC = () => { @@ -349,14 +430,19 @@ const AdminPlatform: React.FC = () => { > { try { - const res = await uploadImage(file); + const uploadFile = assertUploadFile(file); + const res = await uploadImage(uploadFile); updateForm.setFieldsValue({ thumb: res.url }); - onSuccess(res); - } catch (e: any) { - onError(e); + onSuccess?.(res); + } catch (e) { + onError?.(normalizeUploadError(e)); } }} > @@ -374,4 +460,4 @@ const AdminPlatform: React.FC = () => { ); }; -export default AdminPlatform; +export default AdminPlatform; \ No newline at end of file diff --git a/video-gen-admin/src/types/index.ts b/video-gen-admin/src/types/index.ts index f548ceeb..4704d445 100644 --- a/video-gen-admin/src/types/index.ts +++ b/video-gen-admin/src/types/index.ts @@ -104,6 +104,7 @@ export interface AdminUser { isActive: boolean; isAdmin: boolean; userType: string; + frontendUserKind: 'internal' | 'external'; createdAt: string; lastLoginAt?: string; allowedMenus?: string[] | null; @@ -278,6 +279,7 @@ export interface GenerationAIEngineSnapshot { name?: string; provider?: string; modelName?: string; + model?: string; supportedModels?: string[]; defaultSize?: string; selectedSize?: string; @@ -673,3 +675,98 @@ export interface VideoPromptSchemaPreviewOut { runtimeSchema: Record; timePlan: Record[]; } + + +export interface AdminCreditRecordSummary { + totalRecharge: number; + totalConsume: number; + totalRefund: number; + transactionCount: number; + generationCount: number; + generationAttemptCount: number; + imageGenerationCount: number; + videoGenerationCount: number; + imageConsume: number; + videoConsume: number; + textConsume: number; + analysisConsume: number; + totalTokens: number; + inputTokens: number; + outputTokens: number; +} + +export interface AdminCreditRecord { + id: string; + userId: string; + username?: string; + phone?: string; + email?: string; + userType?: string; + userTypeLabel?: string; + frontendUserKind?: string; + frontendUserKindLabel?: string; + type: string; + recordType: string; + recordTypeLabel?: string; + amount: number; + balanceAfter: number; + description?: string; + relatedId?: string; + bizKey?: string; + refundForBizKey?: string; + ownerType?: string; + ownerId?: string; + ownerDeleted?: boolean; + ownerDeletedAt?: string; + attemptNo?: number; + chargeKind?: string; + chargeKindLabel?: string; + chargeAction?: string; + creditSubject?: string; + creditSubjectLabel?: string; + mediaType?: string; + mediaTypeLabel?: string; + billingScene?: string; + billingSceneLabel?: string; + sourceModule?: string; + sourceModuleLabel?: string; + sourceProjectId?: string; + sourceStepId?: string; + sourceStepCode?: string; + sourceStepCodeLabel?: string; + tokenUsageId?: string; + inputTokens: number; + outputTokens: number; + totalTokens: number; + engineType?: string; + engineId?: string; + engineName?: string; + engineProvider?: string; + engineModelName?: string; + createdAt?: string; +} + +export interface AdminCreditRecordListResponse { + items: AdminCreditRecord[]; + total: number; + summary: AdminCreditRecordSummary; +} + +export interface AdminCreditRecordQueryParams { + page?: number; + pageSize?: number; + userId?: string; + userName?: string; + userType?: string; + frontendUserKind?: string; + recordType?: string; + type?: string; + creditSubject?: string; + mediaType?: string; + chargeKind?: string; + sourceModule?: string; + sourceStepCode?: string; + billingScene?: string; + startDate?: string; + endDate?: string; +} diff --git a/video-gen-admin/src/types/xlsx-js-style.d.ts b/video-gen-admin/src/types/xlsx-js-style.d.ts new file mode 100644 index 00000000..7ccb0cac --- /dev/null +++ b/video-gen-admin/src/types/xlsx-js-style.d.ts @@ -0,0 +1,4 @@ +declare module 'xlsx-js-style' { + export const utils: any; + export function writeFile(workbook: any, filename: string, options?: any): void; +} diff --git a/video-gen-admin/src/utils/excelExport.ts b/video-gen-admin/src/utils/excelExport.ts new file mode 100644 index 00000000..14af7f9a --- /dev/null +++ b/video-gen-admin/src/utils/excelExport.ts @@ -0,0 +1,212 @@ +import * as XLSX from 'xlsx-js-style'; + +export type ExcelCellValue = string | number | boolean | Date | null | undefined; + +export interface StyledExcelColumn { + title: string; + key?: string; + width?: number; + minWidth?: number; + maxWidth?: number; + align?: 'left' | 'center' | 'right'; + numFmt?: string; + render: (row: T, rowIndex: number) => ExcelCellValue; +} + +export interface StyledExcelExportOptions { + filename: string; + sheetName: string; + title?: string; + metadataRows?: ExcelCellValue[][]; + summaryRows?: ExcelCellValue[][]; + columns: StyledExcelColumn[]; + rows: T[]; +} + +const BLACK_BORDER = { + top: { style: 'thin', color: { rgb: '000000' } }, + right: { style: 'thin', color: { rgb: '000000' } }, + bottom: { style: 'thin', color: { rgb: '000000' } }, + left: { style: 'thin', color: { rgb: '000000' } }, +}; + +const BASE_ALIGNMENT = { + vertical: 'center', + wrapText: true, +}; + +const TITLE_STYLE = { + font: { bold: true, sz: 16, color: { rgb: 'FFFFFF' } }, + fill: { fgColor: { rgb: '111827' } }, + alignment: { ...BASE_ALIGNMENT, horizontal: 'center' }, + border: BLACK_BORDER, +}; + +const SECTION_LABEL_STYLE = { + font: { bold: true, color: { rgb: '111827' } }, + fill: { fgColor: { rgb: 'F3F4F6' } }, + alignment: { ...BASE_ALIGNMENT, horizontal: 'center' }, + border: BLACK_BORDER, +}; + +const SECTION_VALUE_STYLE = { + font: { color: { rgb: '111827' } }, + fill: { fgColor: { rgb: 'FFFFFF' } }, + alignment: { ...BASE_ALIGNMENT, horizontal: 'left' }, + border: BLACK_BORDER, +}; + +const HEADER_STYLE = { + font: { bold: true, color: { rgb: 'FFFFFF' } }, + fill: { fgColor: { rgb: '374151' } }, + alignment: { ...BASE_ALIGNMENT, horizontal: 'center' }, + border: BLACK_BORDER, +}; + +const BODY_STYLE = { + font: { color: { rgb: '111827' } }, + fill: { fgColor: { rgb: 'FFFFFF' } }, + alignment: { ...BASE_ALIGNMENT, horizontal: 'left' }, + border: BLACK_BORDER, +}; + +function normalizeCellValue(value: ExcelCellValue): ExcelCellValue { + if (value === null || value === undefined) return ''; + return value; +} + +function visualLength(value: ExcelCellValue): number { + if (value === null || value === undefined) return 0; + const text = value instanceof Date ? value.toISOString() : String(value); + const lines = text.split(/\r?\n/); + return Math.max(...lines.map((line) => Array.from(line).reduce((len, ch) => len + (ch.charCodeAt(0) > 255 ? 2 : 1), 0)), 0); +} + +function clamp(value: number, min: number, max: number): number { + return Math.min(max, Math.max(min, value)); +} + +function estimateColumnWidths(rows: ExcelCellValue[][], columns: StyledExcelColumn[]): { wch: number }[] { + const totalColumns = Math.max(columns.length, ...rows.map((row) => row.length)); + return Array.from({ length: totalColumns }, (_, index) => { + const config = columns[index]; + if (config?.width) return { wch: config.width }; + + const maxLength = rows.reduce((max, row) => Math.max(max, visualLength(row[index])), visualLength(config?.title)); + const minWidth = config?.minWidth ?? 10; + const maxWidth = config?.maxWidth ?? 42; + return { wch: clamp(Math.ceil(maxLength * 1.15) + 2, minWidth, maxWidth) }; + }); +} + +function estimateRowHeight(row: ExcelCellValue[], colWidths: { wch: number }[], baseHeight = 20): number { + const maxLines = row.reduce((max, cell, index) => { + const width = Math.max(8, colWidths[index]?.wch || 12); + const text = cell === null || cell === undefined ? '' : String(cell); + const explicitLines = text.split(/\r?\n/); + const wrappedLines = explicitLines.reduce((sum, line) => sum + Math.max(1, Math.ceil(visualLength(line) / Math.max(8, width - 2))), 0); + return Math.max(max, wrappedLines); + }, 1); + + return clamp(baseHeight + (maxLines - 1) * 16, baseHeight, 120); +} + +function applyCellStyle(cell: any, style: any, numFmt?: string): void { + cell.s = { + ...style, + alignment: { ...style.alignment }, + border: BLACK_BORDER, + }; + if (numFmt) cell.z = numFmt; +} + +function isNumericCell(value: ExcelCellValue): boolean { + return typeof value === 'number' && Number.isFinite(value); +} + +export function exportStyledExcel(options: StyledExcelExportOptions): void { + const { filename, sheetName, title, metadataRows = [], summaryRows = [], columns, rows } = options; + const totalColumns = Math.max(1, columns.length); + const aoa: ExcelCellValue[][] = []; + let titleRowIndex = -1; + let headerRowIndex = -1; + + if (title) { + titleRowIndex = aoa.length; + aoa.push([title, ...Array.from({ length: totalColumns - 1 }, () => '')]); + } + + metadataRows.forEach((row) => aoa.push(row.map(normalizeCellValue))); + summaryRows.forEach((row) => aoa.push(row.map(normalizeCellValue))); + + if (metadataRows.length || summaryRows.length) { + aoa.push(Array.from({ length: totalColumns }, () => '')); + } + + headerRowIndex = aoa.length; + aoa.push(columns.map((column) => column.title)); + rows.forEach((row, rowIndex) => { + aoa.push(columns.map((column) => normalizeCellValue(column.render(row, rowIndex)))); + }); + + const worksheet = XLSX.utils.aoa_to_sheet(aoa); + const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1:A1'); + const colWidths = estimateColumnWidths(aoa, columns); + + worksheet['!cols'] = colWidths; + worksheet['!rows'] = aoa.map((row, index) => { + if (index === titleRowIndex) return { hpt: 34 }; + if (index === headerRowIndex) return { hpt: 28 }; + if (row.every((cell) => cell === '')) return { hpt: 10 }; + return { hpt: estimateRowHeight(row, colWidths) }; + }); + + if (titleRowIndex >= 0 && totalColumns > 1) { + worksheet['!merges'] = worksheet['!merges'] || []; + worksheet['!merges'].push({ + s: { r: titleRowIndex, c: 0 }, + e: { r: titleRowIndex, c: totalColumns - 1 }, + }); + } + + worksheet['!autofilter'] = { + ref: XLSX.utils.encode_range({ + s: { r: headerRowIndex, c: 0 }, + e: { r: Math.max(headerRowIndex, aoa.length - 1), c: totalColumns - 1 }, + }), + }; + + for (let rowIndex = range.s.r; rowIndex <= range.e.r; rowIndex += 1) { + for (let colIndex = range.s.c; colIndex <= range.e.c; colIndex += 1) { + const address = XLSX.utils.encode_cell({ r: rowIndex, c: colIndex }); + const cell = worksheet[address] || { t: 's', v: '' }; + worksheet[address] = cell; + + if (rowIndex === titleRowIndex) { + applyCellStyle(cell, TITLE_STYLE); + continue; + } + + if (rowIndex === headerRowIndex) { + applyCellStyle(cell, HEADER_STYLE); + continue; + } + + if (rowIndex < headerRowIndex) { + applyCellStyle(cell, colIndex === 0 ? SECTION_LABEL_STYLE : SECTION_VALUE_STYLE, isNumericCell(cell.v) ? '#,##0.00' : undefined); + continue; + } + + const column = columns[colIndex]; + const align = column?.align || (isNumericCell(cell.v) ? 'right' : 'left'); + applyCellStyle(cell, { + ...BODY_STYLE, + alignment: { ...BASE_ALIGNMENT, horizontal: align }, + }, column?.numFmt || (isNumericCell(cell.v) ? '#,##0.00' : undefined)); + } + } + + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); + XLSX.writeFile(workbook, filename); +} diff --git a/video-gen-admin/tsconfig.tsbuildinfo b/video-gen-admin/tsconfig.tsbuildinfo index e40c53cc..db3fab0b 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/admingenerationairecords.tsx","./src/pages/admingenerationrecords.tsx","./src/pages/adminhotopeningreplicationdetail.tsx","./src/pages/adminhotopeningreplications.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/adminpaymentstats.tsx","./src/pages/adminrechargepackages.tsx","./src/pages/adminreplicationprojectdetail.tsx","./src/pages/adminsettings.tsx","./src/pages/adminshotreplications.tsx","./src/pages/adminshottasksetdetail.tsx","./src/pages/adminusers.tsx","./src/pages/adminvideoengines.tsx","./src/pages/adminvideopromptschemaconfig.tsx","./src/pages/adminreplication/components/jsoncollapse.tsx","./src/pages/adminreplication/components/mediapreview.tsx","./src/pages/adminreplication/components/statustag.tsx","./src/pages/adminreplication/components/videopromptschemaviewer.tsx","./src/store/index.ts","./src/types/index.ts","./src/utils/formatdate.ts","./src/utils/videopromptschema.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/adminauthoriz.tsx","./src/pages/adminconsume.tsx","./src/pages/admincreditratios.tsx","./src/pages/admincreditrecords.tsx","./src/pages/admindashboard.tsx","./src/pages/admingenerationairecords.tsx","./src/pages/admingenerationrecords.tsx","./src/pages/adminhotopeningreplicationdetail.tsx","./src/pages/adminhotopeningreplications.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/adminpaymentstats.tsx","./src/pages/adminrechargepackages.tsx","./src/pages/adminreplicationprojectdetail.tsx","./src/pages/adminsettings.tsx","./src/pages/adminshotreplications.tsx","./src/pages/adminshottasksetdetail.tsx","./src/pages/adminusers.tsx","./src/pages/adminvideoengines.tsx","./src/pages/adminvideopromptschemaconfig.tsx","./src/pages/adminplatform.tsx","./src/pages/adminreplication/components/jsoncollapse.tsx","./src/pages/adminreplication/components/mediapreview.tsx","./src/pages/adminreplication/components/statustag.tsx","./src/pages/adminreplication/components/videopromptschemaviewer.tsx","./src/store/index.ts","./src/types/index.ts","./src/types/xlsx-js-style.d.ts","./src/utils/excelexport.ts","./src/utils/formatdate.ts","./src/utils/videopromptschema.ts"],"version":"6.0.3"} \ No newline at end of file diff --git a/video-gen-api/alembic/versions/0a8da2d3c091_add_credit_record_billing_snapshots_and_.py b/video-gen-api/alembic/versions/0a8da2d3c091_add_credit_record_billing_snapshots_and_.py new file mode 100644 index 00000000..57391279 --- /dev/null +++ b/video-gen-api/alembic/versions/0a8da2d3c091_add_credit_record_billing_snapshots_and_.py @@ -0,0 +1,935 @@ +"""add credit record billing snapshots and frontend user kind + +Revision ID: 0a8da2d3c091 +Revises: 5e2c124e5484 +Create Date: 2026-06-25 13:25:21.246025 +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "0a8da2d3c091" +down_revision: Union[str, None] = "5e2c124e5484" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # 1) 表结构变更。 + # 注意:autogenerate 生成的 upload_task.other_info 删除语句与本次需求无关,已移除。 + op.add_column("users", sa.Column("frontend_user_kind", sa.String(length=16), server_default="external", nullable=False)) + + op.add_column("credit_records", sa.Column("owner_type", sa.String(length=64), nullable=True)) + op.add_column("credit_records", sa.Column("owner_id", sa.String(length=64), nullable=True)) + op.add_column("credit_records", sa.Column("attempt_no", sa.Integer(), nullable=True)) + op.add_column("credit_records", sa.Column("charge_kind", sa.String(length=32), nullable=True)) + op.add_column("credit_records", sa.Column("charge_action", sa.String(length=16), nullable=True)) + op.add_column("credit_records", sa.Column("credit_subject", sa.String(length=32), nullable=True)) + op.add_column("credit_records", sa.Column("media_type", sa.String(length=16), nullable=True)) + op.add_column("credit_records", sa.Column("billing_scene", sa.String(length=64), nullable=True)) + op.add_column("credit_records", sa.Column("source_module", sa.String(length=64), nullable=True)) + op.add_column("credit_records", sa.Column("source_project_id", sa.String(length=64), nullable=True)) + op.add_column("credit_records", sa.Column("source_step_id", sa.String(length=64), nullable=True)) + op.add_column("credit_records", sa.Column("source_step_code", sa.String(length=64), nullable=True)) + op.add_column("credit_records", sa.Column("token_usage_id", sa.String(length=32), nullable=True)) + op.add_column("credit_records", sa.Column("input_tokens", sa.Integer(), nullable=True)) + op.add_column("credit_records", sa.Column("output_tokens", sa.Integer(), nullable=True)) + op.add_column("credit_records", sa.Column("total_tokens", sa.Integer(), nullable=True)) + op.add_column("credit_records", sa.Column("engine_type", sa.String(length=16), nullable=True)) + op.add_column("credit_records", sa.Column("engine_id", sa.String(length=32), nullable=True)) + op.add_column("credit_records", sa.Column("engine_name", sa.String(length=128), nullable=True)) + op.add_column("credit_records", sa.Column("engine_provider", sa.String(length=64), nullable=True)) + op.add_column("credit_records", sa.Column("engine_model_name", sa.String(length=128), nullable=True)) + op.add_column("credit_records", sa.Column("user_type_snapshot", sa.String(length=16), nullable=True)) + op.add_column("credit_records", sa.Column("frontend_user_kind_snapshot", sa.String(length=16), nullable=True)) + + op.add_column("module_generation_steps", sa.Column("token_usage_id", sa.String(length=32), nullable=True)) + op.add_column("module_generation_steps", sa.Column("model_config_id", sa.String(length=32), nullable=True)) + op.add_column("module_generation_steps", sa.Column("input_tokens", sa.Integer(), nullable=True)) + op.add_column("module_generation_steps", sa.Column("output_tokens", sa.Integer(), nullable=True)) + op.add_column("module_generation_steps", sa.Column("total_tokens", sa.Integer(), nullable=True)) + op.add_column("module_generation_steps", sa.Column("text_credits_cost", sa.Float(), nullable=True)) + + op.add_column("token_usage", sa.Column("owner_type", sa.String(length=64), nullable=True)) + op.add_column("token_usage", sa.Column("owner_id", sa.String(length=64), nullable=True)) + op.add_column("token_usage", sa.Column("biz_key", sa.String(length=160), nullable=True)) + op.add_column("token_usage", sa.Column("source_module", sa.String(length=64), nullable=True)) + op.add_column("token_usage", sa.Column("source_step_code", sa.String(length=64), nullable=True)) + + # 2) 历史数据补齐。 + _backfill_legacy_data() + + # 3) 索引。放在历史补齐之后,避免大批量 UPDATE 时维护过多新索引。 + op.create_index(op.f("ix_users_frontend_user_kind"), "users", ["frontend_user_kind"], unique=False) + op.create_index(op.f("ix_users_user_type"), "users", ["user_type"], unique=False) + + op.create_index(op.f("ix_credit_records_billing_scene"), "credit_records", ["billing_scene"], unique=False) + op.create_index(op.f("ix_credit_records_charge_kind"), "credit_records", ["charge_kind"], unique=False) + op.create_index(op.f("ix_credit_records_credit_subject"), "credit_records", ["credit_subject"], unique=False) + op.create_index(op.f("ix_credit_records_engine_id"), "credit_records", ["engine_id"], unique=False) + op.create_index(op.f("ix_credit_records_engine_type"), "credit_records", ["engine_type"], unique=False) + op.create_index(op.f("ix_credit_records_frontend_user_kind_snapshot"), "credit_records", ["frontend_user_kind_snapshot"], unique=False) + op.create_index(op.f("ix_credit_records_media_type"), "credit_records", ["media_type"], unique=False) + op.create_index("ix_credit_records_owner", "credit_records", ["owner_type", "owner_id"], unique=False) + op.create_index(op.f("ix_credit_records_owner_id"), "credit_records", ["owner_id"], unique=False) + op.create_index(op.f("ix_credit_records_owner_type"), "credit_records", ["owner_type"], unique=False) + op.create_index(op.f("ix_credit_records_source_module"), "credit_records", ["source_module"], unique=False) + op.create_index("ix_credit_records_source_module_scene", "credit_records", ["source_module", "billing_scene"], unique=False) + op.create_index(op.f("ix_credit_records_source_project_id"), "credit_records", ["source_project_id"], unique=False) + op.create_index(op.f("ix_credit_records_source_step_code"), "credit_records", ["source_step_code"], unique=False) + op.create_index(op.f("ix_credit_records_source_step_id"), "credit_records", ["source_step_id"], unique=False) + op.create_index("ix_credit_records_subject_media", "credit_records", ["credit_subject", "media_type"], unique=False) + op.create_index(op.f("ix_credit_records_token_usage_id"), "credit_records", ["token_usage_id"], unique=False) + op.create_index(op.f("ix_credit_records_type"), "credit_records", ["type"], unique=False) + op.create_index("ix_credit_records_user_kind_time", "credit_records", ["user_type_snapshot", "frontend_user_kind_snapshot", "created_at"], unique=False) + op.create_index(op.f("ix_credit_records_user_type_snapshot"), "credit_records", ["user_type_snapshot"], unique=False) + + op.create_index(op.f("ix_module_generation_steps_model_config_id"), "module_generation_steps", ["model_config_id"], unique=False) + op.create_index(op.f("ix_module_generation_steps_token_usage_id"), "module_generation_steps", ["token_usage_id"], unique=False) + + op.create_index("ix_token_usage_biz_key", "token_usage", ["biz_key"], unique=False) + op.create_index("ix_token_usage_owner", "token_usage", ["owner_type", "owner_id"], unique=False) + op.create_index(op.f("ix_token_usage_owner_id"), "token_usage", ["owner_id"], unique=False) + op.create_index(op.f("ix_token_usage_owner_type"), "token_usage", ["owner_type"], unique=False) + op.create_index(op.f("ix_token_usage_source_module"), "token_usage", ["source_module"], unique=False) + op.create_index(op.f("ix_token_usage_source_step_code"), "token_usage", ["source_step_code"], unique=False) + + +def _backfill_legacy_data() -> None: + """修复历史账务流水的归类、模块、步骤、token、模型和引擎快照。 + + 设计原则: + - 只补能确定的数据,不用“同用户 + 时间接近 + token 相等”这类不可靠方式强行匹配。 + - ModuleGenerationStep 包含 deleted_at 不为空的软删数据,保证历史流水仍可展示来源。 + - CreditRecord 是账务事实表,历史业务数据软删不影响流水展示。 + - CreditRecord 统一使用 engine_* 承载执行配置快照:model/image/video。 + """ + + # users.frontend_user_kind 默认外部用户。 + op.execute( + """ + UPDATE users + SET frontend_user_kind = 'external' + WHERE frontend_user_kind IS NULL OR frontend_user_kind NOT IN ('internal', 'external') + """ + ) + + # 用户类型快照:按迁移时 users 当前值兜底。 + op.execute( + """ + UPDATE credit_records cr + SET + user_type_snapshot = COALESCE(NULLIF(u.user_type, ''), CASE WHEN COALESCE(u.is_admin, false) THEN 'admin' ELSE 'frontend' END), + frontend_user_kind_snapshot = CASE + WHEN COALESCE(NULLIF(u.user_type, ''), CASE WHEN COALESCE(u.is_admin, false) THEN 'admin' ELSE 'frontend' END) = 'frontend' + THEN COALESCE(NULLIF(u.frontend_user_kind, ''), 'external') + ELSE COALESCE(NULLIF(u.frontend_user_kind, ''), 'external') + END + FROM users u + WHERE cr.user_id = u.id + """ + ) + + # 解析标准 biz_key:owner_type:owner_id:attempt:{n}:charge_kind:charge/refund。 + op.execute( + """ + UPDATE credit_records + SET + owner_type = split_part(biz_key, ':', 1), + owner_id = split_part(biz_key, ':', 2), + attempt_no = split_part(biz_key, ':', 4)::integer, + charge_kind = split_part(biz_key, ':', 5), + charge_action = split_part(biz_key, ':', 6) + WHERE biz_key IS NOT NULL + AND biz_key ~ '^[^:]+:[^:]+:attempt:[0-9]+:[^:]+:(charge|refund)$' + """ + ) + + # 少数退款记录如果自身 biz_key 不完整,用 refund_for_biz_key 兜底解析业务归属。 + op.execute( + """ + UPDATE credit_records + SET + owner_type = split_part(refund_for_biz_key, ':', 1), + owner_id = split_part(refund_for_biz_key, ':', 2), + attempt_no = split_part(refund_for_biz_key, ':', 4)::integer, + charge_kind = split_part(refund_for_biz_key, ':', 5), + charge_action = 'refund' + WHERE type = 'refund' + AND (owner_type IS NULL OR owner_type = 'unknown' OR owner_id IS NULL) + AND refund_for_biz_key IS NOT NULL + AND refund_for_biz_key ~ '^[^:]+:[^:]+:attempt:[0-9]+:[^:]+:(charge|refund)$' + """ + ) + + # 支付充值流水。 + op.execute( + """ + UPDATE credit_records cr + SET + owner_type = COALESCE(NULLIF(cr.owner_type, 'unknown'), 'payment_order'), + owner_id = COALESCE(cr.owner_id, po.id), + charge_kind = COALESCE(NULLIF(cr.charge_kind, 'unknown'), 'recharge'), + charge_action = COALESCE(cr.charge_action, 'charge'), + credit_subject = COALESCE(NULLIF(cr.credit_subject, 'unknown'), 'recharge'), + billing_scene = COALESCE(NULLIF(cr.billing_scene, 'unknown'), 'recharge'), + source_module = COALESCE(NULLIF(cr.source_module, 'unknown'), 'payment') + FROM payment_orders po + WHERE cr.related_id = po.id + AND cr.type = 'recharge' + AND (cr.description IS NULL OR cr.description NOT LIKE '管理员调整:%') + """ + ) + + # 支付退款扣回积分。历史上它通常是 type=consume,但属于支付退款,不应该混入生成消费。 + op.execute( + """ + UPDATE credit_records cr + SET + owner_type = COALESCE(NULLIF(cr.owner_type, 'unknown'), 'payment_order'), + owner_id = COALESCE(cr.owner_id, po.id), + charge_kind = COALESCE(NULLIF(cr.charge_kind, 'unknown'), 'refund'), + charge_action = COALESCE(cr.charge_action, 'refund'), + credit_subject = COALESCE(NULLIF(cr.credit_subject, 'unknown'), 'refund'), + billing_scene = COALESCE(NULLIF(cr.billing_scene, 'unknown'), 'refund'), + source_module = COALESCE(NULLIF(cr.source_module, 'unknown'), 'payment') + FROM payment_orders po + WHERE cr.related_id = po.id + AND cr.type = 'consume' + AND (cr.description ILIKE '%退款%' OR po.status = 'refunded') + """ + ) + + # 管理员手动调整,不混入普通充值/消费。 + op.execute( + """ + UPDATE credit_records cr + SET + owner_type = COALESCE(NULLIF(cr.owner_type, 'unknown'), 'admin_adjust'), + owner_id = COALESCE(cr.owner_id, cr.related_id), + charge_kind = 'admin_adjust', + charge_action = CASE WHEN cr.type = 'refund' THEN 'refund' ELSE 'charge' END, + credit_subject = 'admin_adjust', + billing_scene = 'admin_adjust', + source_module = 'admin' + WHERE cr.description LIKE '管理员调整:%' + """ + ) + + # GenerationRecord 旧版提示词优化裸扣费:generation.py /optimize 历史上未传 related_id / biz_key / record_meta。 + # 只做严格唯一匹配:同用户、同项目名称描述、同扣费金额,并且 credit_record 与 generation_record 均唯一时才回填。 + op.execute( + """ + WITH candidates AS ( + SELECT + cr.id AS credit_record_id, + gr.id AS generation_record_id, + gr.text_tokens_used, + gr.text_credits_cost + FROM credit_records cr + JOIN generation_records gr + ON gr.user_id = cr.user_id + JOIN projects p + ON p.id = gr.project_id + WHERE cr.type = 'consume' + AND cr.biz_key IS NULL + AND cr.refund_for_biz_key IS NULL + AND cr.related_id IS NULL + AND (cr.owner_type IS NULL OR cr.owner_type = 'unknown') + AND (cr.source_module IS NULL OR cr.source_module = 'unknown') + AND (cr.billing_scene IS NULL OR cr.billing_scene = 'unknown') + AND COALESCE(gr.text_credits_cost, 0) > 0 + AND COALESCE(gr.text_tokens_used, 0) > 0 + AND cr.description = '提示词优化 - ' || p.name + AND ABS(ABS(cr.amount)::numeric - ROUND(COALESCE(gr.text_credits_cost, 0)::numeric, 2)) < 0.01 + ), + unique_credit AS ( + SELECT credit_record_id + FROM candidates + GROUP BY credit_record_id + HAVING COUNT(*) = 1 + ), + unique_generation AS ( + SELECT generation_record_id + FROM candidates + GROUP BY generation_record_id + HAVING COUNT(*) = 1 + ), + matched AS ( + SELECT c.* + FROM candidates c + JOIN unique_credit uc ON uc.credit_record_id = c.credit_record_id + JOIN unique_generation ug ON ug.generation_record_id = c.generation_record_id + ) + UPDATE credit_records cr + SET + related_id = COALESCE(cr.related_id, matched.generation_record_id), + owner_type = 'generation_record', + owner_id = matched.generation_record_id, + attempt_no = COALESCE(cr.attempt_no, 1), + charge_kind = 'text_prompt', + charge_action = 'charge', + credit_subject = 'text', + billing_scene = 'generation_record_text_prompt_optimize', + source_module = 'generation_record', + total_tokens = COALESCE(cr.total_tokens, NULLIF(matched.text_tokens_used, 0)), + engine_type = COALESCE(NULLIF(cr.engine_type, 'unknown'), 'model') + FROM matched + WHERE cr.id = matched.credit_record_id + """ + ) + + # GenerationRecord:项目记录提词/文件解析/图片理解/图片生成/视频生成。 + op.execute( + """ + UPDATE credit_records cr + SET + owner_type = COALESCE(NULLIF(cr.owner_type, 'unknown'), 'generation_record'), + owner_id = COALESCE(cr.owner_id, gr.id), + related_id = COALESCE(cr.related_id, gr.id), + source_module = COALESCE(NULLIF(cr.source_module, 'unknown'), 'generation_record'), + media_type = CASE + WHEN COALESCE(NULLIF(cr.charge_kind, 'unknown'), '') = 'media' AND lower(COALESCE(gr.gen_type, '')) IN ('image', 'video') THEN lower(gr.gen_type) + ELSE cr.media_type + END, + total_tokens = CASE + WHEN COALESCE(NULLIF(cr.charge_kind, 'unknown'), '') = 'text_prompt' THEN COALESCE(cr.total_tokens, NULLIF(gr.text_tokens_used, 0)) + WHEN COALESCE(NULLIF(cr.charge_kind, 'unknown'), '') = 'media' AND lower(COALESCE(gr.gen_type, '')) = 'image' THEN COALESCE(cr.total_tokens, NULLIF(gr.image_tokens_used, 0)) + WHEN COALESCE(NULLIF(cr.charge_kind, 'unknown'), '') = 'media' AND lower(COALESCE(gr.gen_type, '')) = 'video' THEN COALESCE(cr.total_tokens, NULLIF(gr.video_tokens_used, 0)) + ELSE cr.total_tokens + END, + credit_subject = CASE + WHEN COALESCE(NULLIF(cr.charge_kind, 'unknown'), '') = 'media' THEN 'media' + WHEN COALESCE(NULLIF(cr.charge_kind, 'unknown'), '') IN ('text_prompt', 'file_parse', 'vision_input') THEN 'text' + ELSE cr.credit_subject + END, + billing_scene = CASE + WHEN COALESCE(NULLIF(cr.charge_kind, 'unknown'), '') = 'text_prompt' THEN 'generation_record_text_prompt_optimize' + WHEN COALESCE(NULLIF(cr.charge_kind, 'unknown'), '') = 'file_parse' THEN 'generation_record_file_parse' + WHEN COALESCE(NULLIF(cr.charge_kind, 'unknown'), '') = 'vision_input' THEN 'generation_record_vision_input' + WHEN COALESCE(NULLIF(cr.charge_kind, 'unknown'), '') = 'media' AND lower(COALESCE(gr.gen_type, '')) = 'image' THEN 'generation_record_image_generate' + WHEN COALESCE(NULLIF(cr.charge_kind, 'unknown'), '') = 'media' AND lower(COALESCE(gr.gen_type, '')) = 'video' THEN 'generation_record_video_generate' + ELSE cr.billing_scene + END, + engine_type = CASE + WHEN COALESCE(NULLIF(cr.charge_kind, 'unknown'), '') IN ('text_prompt', 'file_parse', 'vision_input') THEN COALESCE(NULLIF(cr.engine_type, 'unknown'), 'model') + ELSE cr.engine_type + END + FROM generation_records gr + WHERE (cr.owner_type = 'generation_record' AND cr.owner_id = gr.id) + OR ((cr.owner_type IS NULL OR cr.owner_type = 'unknown') AND cr.related_id = gr.id) + """ + ) + + # ModuleGenerationStep 提词优化:从 output_json.usage.credit_biz_key 精准匹配 CreditRecord.biz_key。 + # 包含软删步骤,不加 deleted_at 过滤。 + op.execute( + """ + WITH step_usage AS ( + SELECT + s.id AS step_id, + s.project_id, + s.module, + s.step_code, + (s.output_json::jsonb -> 'usage') AS usage, + COALESCE( + NULLIF(s.output_json::jsonb -> 'usage' ->> 'token_usage_id', ''), + NULLIF(s.output_json::jsonb -> 'usage' ->> 'tokenUsageId', ''), + NULLIF(s.output_json::jsonb ->> 'token_usage_id', ''), + NULLIF(s.output_json::jsonb ->> 'tokenUsageId', '') + ) AS token_usage_id, + COALESCE( + NULLIF(s.output_json::jsonb -> 'usage' ->> 'model_config_id', ''), + NULLIF(s.output_json::jsonb -> 'usage' ->> 'modelConfigId', ''), + NULLIF(s.output_json::jsonb -> 'usage' -> 'model_config' ->> 'id', ''), + NULLIF(s.output_json::jsonb -> 'usage' -> 'modelConfig' ->> 'id', ''), + NULLIF(s.output_json::jsonb -> 'usage' -> 'model_config_snapshot' ->> 'id', ''), + NULLIF(s.output_json::jsonb ->> 'model_config_id', ''), + NULLIF(s.output_json::jsonb ->> 'modelConfigId', ''), + NULLIF(s.output_json::jsonb -> 'model_config' ->> 'id', ''), + NULLIF(s.output_json::jsonb -> 'modelConfig' ->> 'id', ''), + NULLIF(s.output_json::jsonb -> 'model_config_snapshot' ->> 'id', '') + ) AS model_config_id + FROM module_generation_steps s + WHERE s.output_json IS NOT NULL + AND jsonb_typeof(s.output_json::jsonb) = 'object' + AND jsonb_typeof(s.output_json::jsonb -> 'usage') = 'object' + ) + UPDATE module_generation_steps s + SET + token_usage_id = COALESCE(s.token_usage_id, step_usage.token_usage_id), + model_config_id = COALESCE(s.model_config_id, step_usage.model_config_id), + input_tokens = COALESCE( + s.input_tokens, + CASE WHEN COALESCE(step_usage.usage ->> 'input_tokens', '') ~ '^[0-9]+$' THEN (step_usage.usage ->> 'input_tokens')::integer ELSE NULL END + ), + output_tokens = COALESCE( + s.output_tokens, + CASE WHEN COALESCE(step_usage.usage ->> 'output_tokens', '') ~ '^[0-9]+$' THEN (step_usage.usage ->> 'output_tokens')::integer ELSE NULL END + ), + total_tokens = COALESCE( + s.total_tokens, + CASE WHEN COALESCE(step_usage.usage ->> 'total_tokens', '') ~ '^[0-9]+$' THEN (step_usage.usage ->> 'total_tokens')::integer ELSE NULL END, + CASE + WHEN COALESCE(step_usage.usage ->> 'input_tokens', '') ~ '^[0-9]+$' + AND COALESCE(step_usage.usage ->> 'output_tokens', '') ~ '^[0-9]+$' + THEN (step_usage.usage ->> 'input_tokens')::integer + (step_usage.usage ->> 'output_tokens')::integer + ELSE NULL + END + ), + text_credits_cost = COALESCE( + s.text_credits_cost, + CASE WHEN COALESCE(step_usage.usage ->> 'text_credits_cost', '') ~ '^[0-9]+(\\.[0-9]+)?$' THEN (step_usage.usage ->> 'text_credits_cost')::double precision ELSE NULL END + ) + FROM step_usage + WHERE s.id = step_usage.step_id + """ + ) + + op.execute( + """ + WITH step_usage AS ( + SELECT + s.id AS step_id, + s.project_id, + s.module, + s.step_code, + (s.output_json::jsonb -> 'usage') AS usage, + COALESCE( + NULLIF(s.token_usage_id, ''), + NULLIF(s.output_json::jsonb -> 'usage' ->> 'token_usage_id', ''), + NULLIF(s.output_json::jsonb -> 'usage' ->> 'tokenUsageId', ''), + NULLIF(s.output_json::jsonb ->> 'token_usage_id', ''), + NULLIF(s.output_json::jsonb ->> 'tokenUsageId', '') + ) AS step_token_usage_id, + COALESCE( + NULLIF(s.model_config_id, ''), + NULLIF(s.output_json::jsonb -> 'usage' ->> 'model_config_id', ''), + NULLIF(s.output_json::jsonb -> 'usage' ->> 'modelConfigId', ''), + NULLIF(s.output_json::jsonb -> 'usage' -> 'model_config' ->> 'id', ''), + NULLIF(s.output_json::jsonb -> 'usage' -> 'modelConfig' ->> 'id', ''), + NULLIF(s.output_json::jsonb -> 'usage' -> 'model_config_snapshot' ->> 'id', ''), + NULLIF(s.output_json::jsonb ->> 'model_config_id', ''), + NULLIF(s.output_json::jsonb ->> 'modelConfigId', ''), + NULLIF(s.output_json::jsonb -> 'model_config' ->> 'id', ''), + NULLIF(s.output_json::jsonb -> 'modelConfig' ->> 'id', ''), + NULLIF(s.output_json::jsonb -> 'model_config_snapshot' ->> 'id', '') + ) AS step_model_config_id, + COALESCE( + NULLIF(s.output_json::jsonb -> 'usage' ->> 'model_config_name', ''), + NULLIF(s.output_json::jsonb -> 'usage' ->> 'modelConfigName', ''), + NULLIF(s.output_json::jsonb -> 'usage' -> 'model_config' ->> 'name', ''), + NULLIF(s.output_json::jsonb -> 'usage' -> 'modelConfig' ->> 'name', ''), + NULLIF(s.output_json::jsonb -> 'usage' -> 'model_config_snapshot' ->> 'name', ''), + NULLIF(s.output_json::jsonb ->> 'model_config_name', ''), + NULLIF(s.output_json::jsonb ->> 'modelConfigName', ''), + NULLIF(s.output_json::jsonb -> 'model_config' ->> 'name', ''), + NULLIF(s.output_json::jsonb -> 'modelConfig' ->> 'name', ''), + NULLIF(s.output_json::jsonb -> 'model_config_snapshot' ->> 'name', '') + ) AS step_model_config_name, + COALESCE( + NULLIF(s.output_json::jsonb -> 'usage' ->> 'model_provider', ''), + NULLIF(s.output_json::jsonb -> 'usage' ->> 'modelProvider', ''), + NULLIF(s.output_json::jsonb -> 'usage' ->> 'provider', ''), + NULLIF(s.output_json::jsonb -> 'usage' -> 'model_config' ->> 'provider', ''), + NULLIF(s.output_json::jsonb -> 'usage' -> 'modelConfig' ->> 'provider', ''), + NULLIF(s.output_json::jsonb -> 'usage' -> 'model_config_snapshot' ->> 'provider', ''), + NULLIF(s.output_json::jsonb ->> 'model_provider', ''), + NULLIF(s.output_json::jsonb ->> 'modelProvider', ''), + NULLIF(s.output_json::jsonb ->> 'provider', ''), + NULLIF(s.output_json::jsonb -> 'model_config' ->> 'provider', ''), + NULLIF(s.output_json::jsonb -> 'modelConfig' ->> 'provider', ''), + NULLIF(s.output_json::jsonb -> 'model_config_snapshot' ->> 'provider', '') + ) AS step_model_provider, + COALESCE( + NULLIF(s.output_json::jsonb -> 'usage' ->> 'model_name', ''), + NULLIF(s.output_json::jsonb -> 'usage' ->> 'modelName', ''), + NULLIF(s.output_json::jsonb -> 'usage' -> 'model_config' ->> 'model_name', ''), + NULLIF(s.output_json::jsonb -> 'usage' -> 'modelConfig' ->> 'model_name', ''), + NULLIF(s.output_json::jsonb -> 'usage' -> 'model_config_snapshot' ->> 'model_name', ''), + NULLIF(s.output_json::jsonb -> 'usage' -> 'model_config' ->> 'modelName', ''), + NULLIF(s.output_json::jsonb -> 'usage' -> 'modelConfig' ->> 'modelName', ''), + NULLIF(s.output_json::jsonb -> 'usage' -> 'model_config_snapshot' ->> 'modelName', ''), + NULLIF(s.output_json::jsonb ->> 'model_name', ''), + NULLIF(s.output_json::jsonb ->> 'modelName', ''), + NULLIF(s.output_json::jsonb -> 'model_config' ->> 'model_name', ''), + NULLIF(s.output_json::jsonb -> 'modelConfig' ->> 'model_name', ''), + NULLIF(s.output_json::jsonb -> 'model_config_snapshot' ->> 'model_name', ''), + NULLIF(s.output_json::jsonb -> 'model_config' ->> 'modelName', ''), + NULLIF(s.output_json::jsonb -> 'modelConfig' ->> 'modelName', ''), + NULLIF(s.output_json::jsonb -> 'model_config_snapshot' ->> 'modelName', '') + ) AS step_model_name + FROM module_generation_steps s + WHERE s.output_json IS NOT NULL + AND jsonb_typeof(s.output_json::jsonb) = 'object' + AND jsonb_typeof(s.output_json::jsonb -> 'usage') = 'object' + ) + UPDATE credit_records cr + SET + owner_type = COALESCE(NULLIF(cr.owner_type, 'unknown'), 'module_generation_step'), + owner_id = COALESCE(cr.owner_id, step_usage.step_id), + charge_kind = COALESCE(NULLIF(cr.charge_kind, 'unknown'), 'text_prompt'), + charge_action = COALESCE(cr.charge_action, 'charge'), + credit_subject = COALESCE(NULLIF(cr.credit_subject, 'unknown'), 'text'), + billing_scene = COALESCE( + NULLIF(cr.billing_scene, 'unknown'), + CASE + WHEN step_usage.module = 'hot_opening_replicate' AND step_usage.step_code = 'image_prompt_optimize' THEN 'hot_opening_image_prompt_optimize' + WHEN step_usage.module = 'hot_opening_replicate' AND step_usage.step_code = 'video_prompt_optimize' THEN 'hot_opening_video_prompt_optimize' + WHEN step_usage.module = 'shot_replicate' AND step_usage.step_code = 'image_prompt_optimize' THEN 'shot_image_prompt_optimize' + WHEN step_usage.module = 'shot_replicate' AND step_usage.step_code = 'video_prompt_optimize' THEN 'shot_video_prompt_optimize' + ELSE 'unknown' + END + ), + source_module = COALESCE(NULLIF(cr.source_module, 'unknown'), step_usage.module), + source_project_id = COALESCE(cr.source_project_id, step_usage.project_id), + source_step_id = COALESCE(cr.source_step_id, step_usage.step_id), + source_step_code = COALESCE(cr.source_step_code, step_usage.step_code), + token_usage_id = COALESCE(cr.token_usage_id, step_usage.step_token_usage_id), + input_tokens = COALESCE( + cr.input_tokens, + CASE WHEN COALESCE(step_usage.usage ->> 'input_tokens', '') ~ '^[0-9]+$' THEN (step_usage.usage ->> 'input_tokens')::integer ELSE NULL END + ), + output_tokens = COALESCE( + cr.output_tokens, + CASE WHEN COALESCE(step_usage.usage ->> 'output_tokens', '') ~ '^[0-9]+$' THEN (step_usage.usage ->> 'output_tokens')::integer ELSE NULL END + ), + total_tokens = COALESCE( + cr.total_tokens, + CASE WHEN COALESCE(step_usage.usage ->> 'total_tokens', '') ~ '^[0-9]+$' THEN (step_usage.usage ->> 'total_tokens')::integer ELSE NULL END, + CASE + WHEN COALESCE(step_usage.usage ->> 'input_tokens', '') ~ '^[0-9]+$' + AND COALESCE(step_usage.usage ->> 'output_tokens', '') ~ '^[0-9]+$' + THEN (step_usage.usage ->> 'input_tokens')::integer + (step_usage.usage ->> 'output_tokens')::integer + ELSE NULL + END + ), + engine_type = COALESCE(NULLIF(cr.engine_type, 'unknown'), 'model'), + engine_id = COALESCE(cr.engine_id, step_usage.step_model_config_id), + engine_name = COALESCE(cr.engine_name, step_usage.step_model_config_name), + engine_provider = COALESCE(cr.engine_provider, step_usage.step_model_provider), + engine_model_name = COALESCE(cr.engine_model_name, step_usage.step_model_name) + FROM step_usage + WHERE cr.biz_key = step_usage.usage ->> 'credit_biz_key' + """ + ) + + # TokenUsage 如果 usage 中已经记录 token_usage_id,则可以安全补 owner;否则不强行按时间/token 猜。 + op.execute( + """ + WITH step_usage AS ( + SELECT + s.id AS step_id, + s.module, + s.step_code, + (s.output_json::jsonb -> 'usage') AS usage + FROM module_generation_steps s + WHERE s.output_json IS NOT NULL + AND jsonb_typeof(s.output_json::jsonb) = 'object' + AND jsonb_typeof(s.output_json::jsonb -> 'usage') = 'object' + AND NULLIF(s.output_json::jsonb -> 'usage' ->> 'token_usage_id', '') IS NOT NULL + ) + UPDATE token_usage tu + SET + owner_type = COALESCE(NULLIF(tu.owner_type, 'unknown'), 'module_generation_step'), + owner_id = COALESCE(tu.owner_id, step_usage.step_id), + biz_key = COALESCE(tu.biz_key, NULLIF(step_usage.usage ->> 'credit_biz_key', '')), + source_module = COALESCE(NULLIF(tu.source_module, 'unknown'), step_usage.module), + source_step_code = COALESCE(tu.source_step_code, step_usage.step_code) + FROM step_usage + WHERE tu.id = step_usage.usage ->> 'token_usage_id' + """ + ) + + # 拆镜复刻原视频整体分析流水:归属到任务集。 + op.execute( + """ + UPDATE credit_records cr + SET + owner_type = COALESCE(NULLIF(cr.owner_type, 'unknown'), 'shot_replicate_task_set'), + owner_id = COALESCE(cr.owner_id, ts.id), + attempt_no = COALESCE(cr.attempt_no, 1), + charge_kind = COALESCE(NULLIF(cr.charge_kind, 'unknown'), 'video_analysis'), + charge_action = COALESCE(cr.charge_action, CASE WHEN cr.type = 'refund' THEN 'refund' ELSE 'charge' END), + credit_subject = COALESCE(NULLIF(cr.credit_subject, 'unknown'), 'analysis'), + media_type = COALESCE(cr.media_type, 'video'), + billing_scene = COALESCE(NULLIF(cr.billing_scene, 'unknown'), 'shot_original_video_analysis'), + source_module = COALESCE(NULLIF(cr.source_module, 'unknown'), 'shot_replicate'), + source_project_id = COALESCE(cr.source_project_id, ts.id), + source_step_code = COALESCE(cr.source_step_code, 'video_analysis'), + engine_type = COALESCE(NULLIF(cr.engine_type, 'unknown'), 'model') + FROM shot_replicate_task_sets ts + WHERE ( + (cr.owner_type = 'shot_replicate_task_set' AND cr.owner_id = ts.id) + OR ((cr.owner_type IS NULL OR cr.owner_type = 'unknown') AND cr.related_id = ts.id) + ) + AND cr.type IN ('consume', 'refund') + AND ( + cr.charge_kind = 'video_analysis' + OR cr.billing_scene IN ('shot_video_analysis', 'shot_original_video_analysis') + OR cr.description ILIKE '%视频分析%' + OR cr.description ILIKE '%原视频分析%' + ) + """ + ) + + # 拆镜复刻手动片段分析流水:归属到片段。 + op.execute( + """ + UPDATE credit_records cr + SET + owner_type = COALESCE(NULLIF(cr.owner_type, 'unknown'), 'shot_replicate_segment'), + owner_id = COALESCE(cr.owner_id, sg.id), + attempt_no = COALESCE(cr.attempt_no, 1), + charge_kind = COALESCE(NULLIF(cr.charge_kind, 'unknown'), 'video_analysis'), + charge_action = COALESCE(cr.charge_action, CASE WHEN cr.type = 'refund' THEN 'refund' ELSE 'charge' END), + credit_subject = COALESCE(NULLIF(cr.credit_subject, 'unknown'), 'analysis'), + media_type = COALESCE(cr.media_type, 'video'), + billing_scene = COALESCE(NULLIF(cr.billing_scene, 'unknown'), 'shot_segment_video_analysis'), + source_module = COALESCE(NULLIF(cr.source_module, 'unknown'), 'shot_replicate'), + source_project_id = COALESCE(cr.source_project_id, sg.task_set_id), + source_step_id = COALESCE(cr.source_step_id, sg.id), + source_step_code = COALESCE(cr.source_step_code, 'video_analysis'), + engine_type = COALESCE(NULLIF(cr.engine_type, 'unknown'), 'model') + FROM shot_replicate_segments sg + WHERE ( + (cr.owner_type = 'shot_replicate_segment' AND cr.owner_id = sg.id) + OR ((cr.owner_type IS NULL OR cr.owner_type = 'unknown') AND cr.related_id = sg.id) + ) + AND cr.type IN ('consume', 'refund') + AND ( + cr.charge_kind = 'video_analysis' + OR cr.billing_scene IN ('shot_video_analysis', 'shot_segment_video_analysis') + OR cr.description ILIKE '%片段分析%' + OR cr.description ILIKE '%手动%分析%' + ) + """ + ) + + # Module media generation:ChatGenerationTask 被模块步骤引用时,优先归到具体模块/步骤,而不是 AI 创作。 + op.execute( + """ + WITH step_task AS ( + SELECT DISTINCT ON (s.chat_task_id) + s.chat_task_id, + s.id AS step_id, + s.project_id, + s.module, + s.step_code + FROM module_generation_steps s + WHERE s.chat_task_id IS NOT NULL + ORDER BY s.chat_task_id, s.is_current DESC, s.updated_at DESC + ) + UPDATE credit_records cr + SET + source_module = COALESCE(NULLIF(cr.source_module, 'unknown'), step_task.module), + source_project_id = COALESCE(cr.source_project_id, step_task.project_id), + source_step_id = COALESCE(cr.source_step_id, step_task.step_id), + source_step_code = COALESCE(cr.source_step_code, step_task.step_code), + billing_scene = COALESCE( + NULLIF(cr.billing_scene, 'unknown'), + CASE + WHEN step_task.module = 'hot_opening_replicate' AND step_task.step_code = 'image_generate' THEN 'hot_opening_image_generate' + WHEN step_task.module = 'hot_opening_replicate' AND step_task.step_code = 'video_generate' THEN 'hot_opening_video_generate' + WHEN step_task.module = 'shot_replicate' AND step_task.step_code = 'image_generate' THEN 'shot_image_generate' + WHEN step_task.module = 'shot_replicate' AND step_task.step_code = 'video_generate' THEN 'shot_video_generate' + ELSE cr.billing_scene + END + ) + FROM step_task + WHERE cr.owner_type = 'chat_generation_task' + AND cr.owner_id = step_task.chat_task_id + """ + ) + + # ChatGenerationTask:AI 创作图片/视频生成,以及媒体 token/引擎快照。 + op.execute( + """ + UPDATE credit_records cr + SET + owner_type = COALESCE(NULLIF(cr.owner_type, 'unknown'), 'chat_generation_task'), + owner_id = COALESCE(cr.owner_id, t.id), + source_module = COALESCE(NULLIF(cr.source_module, 'unknown'), 'ai_creation'), + credit_subject = CASE WHEN COALESCE(NULLIF(cr.charge_kind, 'unknown'), '') = 'media' THEN 'media' ELSE cr.credit_subject END, + media_type = CASE + WHEN COALESCE(NULLIF(cr.charge_kind, 'unknown'), '') = 'media' AND lower(COALESCE(t.gen_type, '')) IN ('image', 'video') THEN lower(t.gen_type) + ELSE cr.media_type + END, + billing_scene = COALESCE( + NULLIF(cr.billing_scene, 'unknown'), + CASE + WHEN COALESCE(NULLIF(cr.charge_kind, 'unknown'), '') = 'media' AND lower(COALESCE(t.gen_type, '')) = 'image' THEN 'ai_creation_image_generate' + WHEN COALESCE(NULLIF(cr.charge_kind, 'unknown'), '') = 'media' AND lower(COALESCE(t.gen_type, '')) = 'video' THEN 'ai_creation_video_generate' + ELSE cr.billing_scene + END + ), + engine_type = CASE + WHEN COALESCE(NULLIF(cr.charge_kind, 'unknown'), '') = 'media' AND lower(COALESCE(t.gen_type, '')) IN ('image', 'video') THEN lower(t.gen_type) + ELSE cr.engine_type + END, + engine_id = COALESCE(cr.engine_id, t.engine_id), + total_tokens = CASE + WHEN COALESCE(NULLIF(cr.charge_kind, 'unknown'), '') = 'media' AND lower(COALESCE(t.gen_type, '')) = 'image' THEN COALESCE(cr.total_tokens, NULLIF(t.image_tokens_used, 0)) + WHEN COALESCE(NULLIF(cr.charge_kind, 'unknown'), '') = 'media' AND lower(COALESCE(t.gen_type, '')) = 'video' THEN COALESCE(cr.total_tokens, NULLIF(t.video_tokens_used, 0)) + ELSE cr.total_tokens + END + FROM chat_generation_tasks t + WHERE (cr.owner_type = 'chat_generation_task' AND cr.owner_id = t.id) + OR ((cr.owner_type IS NULL OR cr.owner_type = 'unknown') AND cr.related_id = t.id) + """ + ) + + # 从 TokenUsage 补 CreditRecord 的 token 和 ModelConfig 执行快照,前提是已经能安全匹配 token_usage_id。 + op.execute( + """ + UPDATE credit_records cr + SET + input_tokens = COALESCE(cr.input_tokens, NULLIF(tu.input_tokens, 0)), + output_tokens = COALESCE(cr.output_tokens, NULLIF(tu.output_tokens, 0)), + total_tokens = COALESCE(cr.total_tokens, NULLIF(tu.total_tokens, 0)), + engine_type = CASE + WHEN COALESCE(NULLIF(cr.charge_kind, 'unknown'), '') IN ('text_prompt', 'file_parse', 'vision_input', 'video_analysis') + OR COALESCE(NULLIF(cr.credit_subject, 'unknown'), '') IN ('text', 'analysis') + THEN COALESCE(NULLIF(cr.engine_type, 'unknown'), 'model') + ELSE cr.engine_type + END, + engine_id = CASE + WHEN COALESCE(NULLIF(cr.charge_kind, 'unknown'), '') IN ('text_prompt', 'file_parse', 'vision_input', 'video_analysis') + OR COALESCE(NULLIF(cr.credit_subject, 'unknown'), '') IN ('text', 'analysis') + THEN COALESCE(cr.engine_id, tu.model_config_id) + ELSE cr.engine_id + END + FROM token_usage tu + WHERE cr.token_usage_id = tu.id + """ + ) + + # ModelConfig 快照统一冷备到 engine_*,engine_type=model。 + op.execute( + """ + UPDATE credit_records cr + SET + engine_type = COALESCE(NULLIF(cr.engine_type, 'unknown'), 'model'), + engine_name = COALESCE(cr.engine_name, mc.name), + engine_provider = COALESCE(cr.engine_provider, mc.provider), + engine_model_name = COALESCE(cr.engine_model_name, mc.model_name) + FROM model_configs mc + WHERE cr.engine_id = mc.id + AND ( + cr.engine_type = 'model' + OR cr.charge_kind IN ('text_prompt', 'file_parse', 'vision_input', 'video_analysis') + OR cr.credit_subject IN ('text', 'analysis') + ) + """ + ) + + # 图片引擎快照。 + op.execute( + """ + UPDATE credit_records cr + SET + engine_name = COALESCE(cr.engine_name, ie.name), + engine_provider = COALESCE(cr.engine_provider, ie.provider), + engine_model_name = COALESCE(cr.engine_model_name, ie.model_name), + engine_type = COALESCE(NULLIF(cr.engine_type, 'unknown'), 'image') + FROM image_engines ie + WHERE cr.engine_id = ie.id + AND (cr.engine_type = 'image' OR cr.media_type = 'image' OR cr.engine_type IS NULL OR cr.engine_type = 'unknown') + """ + ) + + # 视频引擎快照。 + op.execute( + """ + UPDATE credit_records cr + SET + engine_name = COALESCE(cr.engine_name, ve.name), + engine_provider = COALESCE(cr.engine_provider, ve.provider), + engine_model_name = COALESCE(cr.engine_model_name, ve.model_name), + engine_type = COALESCE(NULLIF(cr.engine_type, 'unknown'), 'video') + FROM video_engines ve + WHERE cr.engine_id = ve.id + AND (cr.engine_type = 'video' OR cr.media_type = 'video' OR cr.engine_type IS NULL OR cr.engine_type = 'unknown') + """ + ) + + # 退款流水复制原消费流水快照,但 billing_scene 固定为 refund,方便筛选回退流水。 + op.execute( + """ + UPDATE credit_records r + SET + owner_type = COALESCE(NULLIF(r.owner_type, 'unknown'), c.owner_type), + owner_id = COALESCE(r.owner_id, c.owner_id), + attempt_no = COALESCE(r.attempt_no, c.attempt_no), + charge_kind = COALESCE(NULLIF(r.charge_kind, 'unknown'), c.charge_kind), + charge_action = 'refund', + credit_subject = COALESCE(NULLIF(r.credit_subject, 'unknown'), c.credit_subject, 'refund'), + media_type = COALESCE(r.media_type, c.media_type), + billing_scene = 'refund', + source_module = COALESCE(NULLIF(r.source_module, 'unknown'), c.source_module), + source_project_id = COALESCE(r.source_project_id, c.source_project_id), + source_step_id = COALESCE(r.source_step_id, c.source_step_id), + source_step_code = COALESCE(r.source_step_code, c.source_step_code), + token_usage_id = COALESCE(r.token_usage_id, c.token_usage_id), + input_tokens = COALESCE(r.input_tokens, c.input_tokens), + output_tokens = COALESCE(r.output_tokens, c.output_tokens), + total_tokens = COALESCE(r.total_tokens, c.total_tokens), + engine_type = COALESCE(NULLIF(r.engine_type, 'unknown'), c.engine_type), + engine_id = COALESCE(r.engine_id, c.engine_id), + engine_name = COALESCE(r.engine_name, c.engine_name), + engine_provider = COALESCE(r.engine_provider, c.engine_provider), + engine_model_name = COALESCE(r.engine_model_name, c.engine_model_name), + user_type_snapshot = COALESCE(r.user_type_snapshot, c.user_type_snapshot), + frontend_user_kind_snapshot = COALESCE(r.frontend_user_kind_snapshot, c.frontend_user_kind_snapshot) + FROM credit_records c + WHERE r.type = 'refund' + AND r.refund_for_biz_key = c.biz_key + """ + ) + + # 基础分类兜底。 + op.execute( + """ + UPDATE credit_records + SET + charge_action = COALESCE(charge_action, CASE WHEN type = 'refund' THEN 'refund' ELSE 'charge' END), + charge_kind = COALESCE( + NULLIF(charge_kind, 'unknown'), + CASE + WHEN type = 'recharge' THEN 'recharge' + WHEN type = 'refund' THEN 'refund' + ELSE 'unknown' + END + ), + credit_subject = COALESCE( + NULLIF(credit_subject, 'unknown'), + CASE + WHEN type = 'recharge' THEN 'recharge' + WHEN type = 'refund' THEN 'refund' + WHEN charge_kind = 'media' THEN 'media' + WHEN charge_kind IN ('text_prompt', 'file_parse', 'vision_input') THEN 'text' + WHEN charge_kind = 'module_create' THEN 'module' + WHEN charge_kind = 'video_analysis' THEN 'analysis' + WHEN charge_kind = 'video_split' THEN 'split' + WHEN charge_kind = 'admin_adjust' THEN 'admin_adjust' + ELSE 'unknown' + END + ), + source_module = COALESCE(NULLIF(source_module, 'unknown'), CASE WHEN type = 'recharge' THEN 'payment' ELSE 'unknown' END), + billing_scene = COALESCE( + NULLIF(billing_scene, 'unknown'), + CASE + WHEN type = 'recharge' THEN 'recharge' + WHEN type = 'refund' THEN 'refund' + ELSE 'unknown' + END + ), + owner_type = COALESCE(NULLIF(owner_type, 'unknown'), 'unknown'), + engine_type = CASE + WHEN charge_kind IN ('text_prompt', 'file_parse', 'vision_input', 'video_analysis') + OR credit_subject IN ('text', 'analysis') + THEN COALESCE(NULLIF(engine_type, 'unknown'), 'model') + ELSE engine_type + END + WHERE charge_kind IS NULL + OR charge_kind = 'unknown' + OR charge_action IS NULL + OR credit_subject IS NULL + OR credit_subject = 'unknown' + OR source_module IS NULL + OR source_module = 'unknown' + OR billing_scene IS NULL + OR billing_scene = 'unknown' + OR owner_type IS NULL + OR owner_type = 'unknown' + """ + ) + + # 用户快照兜底,避免后台筛选 NULL。 + op.execute( + """ + UPDATE credit_records + SET + user_type_snapshot = COALESCE(user_type_snapshot, 'frontend'), + frontend_user_kind_snapshot = COALESCE(frontend_user_kind_snapshot, 'external') + WHERE user_type_snapshot IS NULL OR frontend_user_kind_snapshot IS NULL + """ + ) + + +def downgrade() -> None: + op.drop_index(op.f("ix_users_user_type"), table_name="users") + op.drop_index(op.f("ix_users_frontend_user_kind"), table_name="users") + + op.drop_index(op.f("ix_token_usage_source_step_code"), table_name="token_usage") + op.drop_index(op.f("ix_token_usage_source_module"), table_name="token_usage") + op.drop_index(op.f("ix_token_usage_owner_type"), table_name="token_usage") + op.drop_index(op.f("ix_token_usage_owner_id"), table_name="token_usage") + op.drop_index("ix_token_usage_owner", table_name="token_usage") + op.drop_index("ix_token_usage_biz_key", table_name="token_usage") + + op.drop_index(op.f("ix_module_generation_steps_token_usage_id"), table_name="module_generation_steps") + op.drop_index(op.f("ix_module_generation_steps_model_config_id"), table_name="module_generation_steps") + + op.drop_index(op.f("ix_credit_records_user_type_snapshot"), table_name="credit_records") + op.drop_index("ix_credit_records_user_kind_time", table_name="credit_records") + op.drop_index(op.f("ix_credit_records_type"), table_name="credit_records") + op.drop_index(op.f("ix_credit_records_token_usage_id"), table_name="credit_records") + op.drop_index("ix_credit_records_subject_media", table_name="credit_records") + op.drop_index(op.f("ix_credit_records_source_step_id"), table_name="credit_records") + op.drop_index(op.f("ix_credit_records_source_step_code"), table_name="credit_records") + op.drop_index(op.f("ix_credit_records_source_project_id"), table_name="credit_records") + op.drop_index("ix_credit_records_source_module_scene", table_name="credit_records") + op.drop_index(op.f("ix_credit_records_source_module"), table_name="credit_records") + op.drop_index(op.f("ix_credit_records_owner_type"), table_name="credit_records") + op.drop_index(op.f("ix_credit_records_owner_id"), table_name="credit_records") + op.drop_index("ix_credit_records_owner", table_name="credit_records") + op.drop_index(op.f("ix_credit_records_media_type"), table_name="credit_records") + op.drop_index(op.f("ix_credit_records_frontend_user_kind_snapshot"), table_name="credit_records") + op.drop_index(op.f("ix_credit_records_engine_type"), table_name="credit_records") + op.drop_index(op.f("ix_credit_records_engine_id"), table_name="credit_records") + op.drop_index(op.f("ix_credit_records_credit_subject"), table_name="credit_records") + op.drop_index(op.f("ix_credit_records_charge_kind"), table_name="credit_records") + op.drop_index(op.f("ix_credit_records_billing_scene"), table_name="credit_records") + + op.drop_column("token_usage", "source_step_code") + op.drop_column("token_usage", "source_module") + op.drop_column("token_usage", "biz_key") + op.drop_column("token_usage", "owner_id") + op.drop_column("token_usage", "owner_type") + + op.drop_column("module_generation_steps", "text_credits_cost") + op.drop_column("module_generation_steps", "total_tokens") + op.drop_column("module_generation_steps", "output_tokens") + op.drop_column("module_generation_steps", "input_tokens") + op.drop_column("module_generation_steps", "model_config_id") + op.drop_column("module_generation_steps", "token_usage_id") + + op.drop_column("credit_records", "frontend_user_kind_snapshot") + op.drop_column("credit_records", "user_type_snapshot") + op.drop_column("credit_records", "engine_model_name") + op.drop_column("credit_records", "engine_provider") + op.drop_column("credit_records", "engine_name") + op.drop_column("credit_records", "engine_id") + op.drop_column("credit_records", "engine_type") + op.drop_column("credit_records", "total_tokens") + op.drop_column("credit_records", "output_tokens") + op.drop_column("credit_records", "input_tokens") + op.drop_column("credit_records", "token_usage_id") + op.drop_column("credit_records", "source_step_code") + op.drop_column("credit_records", "source_step_id") + op.drop_column("credit_records", "source_project_id") + op.drop_column("credit_records", "source_module") + op.drop_column("credit_records", "billing_scene") + op.drop_column("credit_records", "media_type") + op.drop_column("credit_records", "credit_subject") + op.drop_column("credit_records", "charge_action") + op.drop_column("credit_records", "charge_kind") + op.drop_column("credit_records", "attempt_no") + op.drop_column("credit_records", "owner_id") + op.drop_column("credit_records", "owner_type") + + op.drop_column("users", "frontend_user_kind") diff --git a/video-gen-api/alembic/versions/c72a6f69e641_merge_changes_from_remote_add_credit_.py b/video-gen-api/alembic/versions/c72a6f69e641_merge_changes_from_remote_add_credit_.py new file mode 100644 index 00000000..a56353bb --- /dev/null +++ b/video-gen-api/alembic/versions/c72a6f69e641_merge_changes_from_remote_add_credit_.py @@ -0,0 +1,25 @@ +"""merge changes from remote add credit record and upload_task add other_info + +Revision ID: c72a6f69e641 +Revises: 0a8da2d3c091, a65cf38abbb8 +Create Date: 2026-06-25 17:02:01.095460 +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'c72a6f69e641' +down_revision: Union[str, None] = ('0a8da2d3c091', 'a65cf38abbb8') +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + pass + + +def downgrade() -> None: + pass diff --git a/video-gen-api/app/api/v1/admin.py b/video-gen-api/app/api/v1/admin.py index 8226626f..3d49b55a 100644 --- a/video-gen-api/app/api/v1/admin.py +++ b/video-gen-api/app/api/v1/admin.py @@ -22,6 +22,7 @@ from app.models.video_engine import VideoEngine from app.models.image_engine import ImageEngine from app.models.credit_ratio import CreditRatio from app.models.operation_log import OperationLog +from app.enums.user import FrontendUserKind, UserType from app.schemas.admin import ( CreditAdjustRequest, ModelConfigCreate, @@ -33,6 +34,7 @@ from app.schemas.admin import ( CreateUserRequest, UpdateMenusRequest, ResetPasswordRequest, + UpdateFrontendUserKindRequest, OperationLogOut, ) from app.schemas.industry import IndustryConfigCreate, IndustryConfigOut @@ -40,6 +42,8 @@ from app.schemas.video_engine import VideoEngineCreate, VideoEngineOut from app.schemas.image_engine import ImageEngineCreate, ImageEngineOut from app.schemas.credit_ratio import CreditRatioCreate, CreditRatioOut from app.services.credits import add_credits, deduct_credits +from app.services.credit_record_meta_service import build_admin_adjust_meta +from app.services.admin_credit_record_service import list_admin_credit_records from app.services.notification import create_notification from app.services.auth import hash_password, verify_password from app.services.operation_log import log_operation @@ -82,15 +86,24 @@ async def list_users( page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=1000), search: str = Query(""), + user_type: str | None = Query(None, pattern="^(frontend|admin)$"), + frontend_user_kind: str | None = Query(None, pattern="^(internal|external)$"), admin: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), ): query = select(User).order_by(User.created_at.desc()) count_query = select(func.count(User.id)) if search: - search_filter = (User.username.ilike(f"%{search}%")) | (User.email.ilike(f"%{search}%")) + like = f"%{search}%" + search_filter = (User.username.ilike(like)) | (User.email.ilike(like)) | (User.phone.ilike(like)) query = query.where(search_filter) count_query = count_query.where(search_filter) + if user_type: + query = query.where(User.user_type == user_type) + count_query = count_query.where(User.user_type == user_type) + if frontend_user_kind: + query = query.where(User.user_type == UserType.FRONTEND.value, User.frontend_user_kind == frontend_user_kind) + count_query = count_query.where(User.user_type == UserType.FRONTEND.value, User.frontend_user_kind == frontend_user_kind) total = (await db.execute(count_query)).scalar() or 0 result = await db.execute(query.offset((page - 1) * page_size).limit(page_size)) items = result.scalars().all() @@ -132,6 +145,7 @@ async def create_user( credits=req.credits, is_admin=(req.user_type == "admin"), user_type=req.user_type, + frontend_user_kind=req.frontend_user_kind if req.user_type == "frontend" else FrontendUserKind.EXTERNAL.value, allowed_menus=req.allowed_menus, ) user.credits = round(user.credits, 2) @@ -180,9 +194,9 @@ async def adjust_credits( db: AsyncSession = Depends(get_db), ): if req.amount > 0: - await add_credits(db, user_id, req.amount, f"管理员调整: {req.description}") + await add_credits(db, user_id, req.amount, f"管理员调整: {req.description}", record_meta=build_admin_adjust_meta()) else: - await deduct_credits(db, user_id, abs(req.amount), f"管理员调整: {req.description}") + await deduct_credits(db, user_id, abs(req.amount), f"管理员调整: {req.description}", record_meta=build_admin_adjust_meta()) await create_notification( db, user_id, "积分变动通知", f"您的积分已{'增加' if req.amount > 0 else '扣除'}{abs(req.amount)}积分。原因:{req.description}", @@ -208,6 +222,25 @@ async def update_user_status( return {"message": "ok"} +@router.put("/users/{user_id}/frontend-kind", response_model=AdminUserOut) +async def update_user_frontend_kind( + user_id: str, + req: UpdateFrontendUserKindRequest, + admin: User = Depends(get_admin_user), + db: AsyncSession = Depends(get_db), +): + result = await db.execute(select(User).where(User.id == user_id).limit(1)) + user = result.scalar_one_or_none() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + if user.user_type != UserType.FRONTEND.value: + raise HTTPException(status_code=400, detail="仅前台用户支持设置内部/外部归类") + user.frontend_user_kind = req.frontend_user_kind or FrontendUserKind.EXTERNAL.value + await db.flush() + await log_operation(db, admin.id, admin.username, f"设置前台用户归类为 {user.frontend_user_kind}", "PUT", f"/admin/users/{user_id}/frontend-kind") + return user + + @router.put("/users/{user_id}/reset-password") async def reset_user_password( user_id: str, @@ -248,66 +281,43 @@ async def admin_change_password( @router.get("/credit-records") async def list_credit_records( page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=500), + page_size: int = Query(20, ge=1, le=1000), user_id: str | None = Query(None), user_name: str | None = Query(None), + user_type: str | None = Query(None), + frontend_user_kind: str | None = Query(None), + record_type: str | None = Query(None), type: str | None = Query(None), + credit_subject: str | None = Query(None), + media_type: str | None = Query(None), + charge_kind: str | None = Query(None), + source_module: str | None = Query(None), + source_step_code: str | None = Query(None), + billing_scene: str | None = Query(None), start_date: str = Query(None), end_date: str = Query(None), admin: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), ): - """List all credit transaction records with filters.""" - query = select(CreditRecord, User.username).join( - User, CreditRecord.user_id == User.id, isouter=True - ).order_by(CreditRecord.created_at.desc(), CreditRecord.id.desc()) - - count_query = select(func.count(CreditRecord.id)) - - if user_id: - query = query.where(CreditRecord.user_id == user_id) - count_query = count_query.where(CreditRecord.user_id == user_id) - if user_name: - query = query.where(User.username.like(f'%{user_name}%')) - count_query = count_query.join(User, CreditRecord.user_id == User.id).where(User.username.like(f'%{user_name}%')) - if type: - query = query.where(CreditRecord.type == type) - count_query = count_query.where(CreditRecord.type == type) - - try: - if start_date: - date_start = datetime.strptime(start_date, "%Y-%m-%d") - query = query.where(CreditRecord.created_at >= date_start) - count_query = count_query.where(CreditRecord.created_at >= date_start) - if end_date: - date_end = datetime.strptime(end_date, "%Y-%m-%d") - date_end = date_end.replace(hour=23, minute=59, second=59, microsecond=999999) - query = query.where(CreditRecord.created_at <= date_end) - count_query = count_query.where(CreditRecord.created_at <= date_end) - except: - pass - - total = (await db.execute(count_query)).scalar() or 0 - result = await db.execute(query.offset((page - 1) * page_size).limit(page_size)) - rows = result.all() - - return { - "total": total, - "items": [ - { - "id": record.id, - "user_id": record.user_id, - "username": username, - "type": record.type, - "amount": round(record.amount, 2), - "balance_after": round(record.balance_after, 2), - "description": record.description, - "related_id": record.related_id, - "created_at": _iso(record.created_at), - } - for record, username in rows - ], - } + """List all credit transaction records with filters and full summary.""" + return await list_admin_credit_records( + db, + page=page, + page_size=page_size, + user_id=user_id, + user_name=user_name, + user_type=user_type, + frontend_user_kind=frontend_user_kind, + record_type=record_type or type, + credit_subject=credit_subject, + media_type=media_type, + charge_kind=charge_kind, + source_module=source_module, + source_step_code=source_step_code, + billing_scene=billing_scene, + start_date=start_date, + end_date=end_date, + ) # ── Notification Admin ─────────────────────────────────── diff --git a/video-gen-api/app/api/v1/generation.py b/video-gen-api/app/api/v1/generation.py index 8b6b4dd0..a4c40c9f 100644 --- a/video-gen-api/app/api/v1/generation.py +++ b/video-gen-api/app/api/v1/generation.py @@ -35,12 +35,15 @@ from app.services.resource_accounting_service import ( ) from app.services.resource_signed_url_service import build_resource_signed_url from app.services.generation_billing_service import ( + CHARGE_TEXT_PROMPT, OWNER_GENERATION_RECORD, + build_credit_biz_key, charge_generation_media_by_params, charge_generation_media_for_record, get_next_credit_attempt_no, ) from app.services.generation_refund_service import mark_generation_record_failed_and_refund_once +from app.services.credit_record_meta_service import build_generation_record_prompt_meta from app.services.video_cover_service import async_create_video_cover_for_local_video from app.utils.id_gen import generate_id from app.utils.exceptions import InsufficientCreditsError, RecordNotFoundError, InvalidStatusError @@ -304,9 +307,27 @@ async def optimize( failed_record_id = record.id failed_user_id = current_user.id try: + prompt_attempt_no = 1 + prompt_biz_key = build_credit_biz_key( + owner_type=OWNER_GENERATION_RECORD, + owner_id=record.id, + attempt_no=prompt_attempt_no, + charge_kind=CHARGE_TEXT_PROMPT, + action="charge", + ) + prompt_meta = await build_generation_record_prompt_meta( + db, + record_id=record.id, + attempt_no=prompt_attempt_no, + charge_kind=CHARGE_TEXT_PROMPT, + usage=token_usage, + ) await deduct_credits( db, current_user.id, text_credits, f"提示词优化 - {project.name}", + related_id=record.id, + biz_key=prompt_biz_key, + record_meta=prompt_meta, ) except InsufficientCreditsError as e: # /optimize 阶段只处理提示词优化扣费。 diff --git a/video-gen-api/app/enums/__init__.py b/video-gen-api/app/enums/__init__.py index c26dc623..a116920b 100644 --- a/video-gen-api/app/enums/__init__.py +++ b/video-gen-api/app/enums/__init__.py @@ -1,4 +1,8 @@ from app.enums.common import * from app.enums.hot_opening_replicate import * -from app.enums.video_prompt_schema import * +from app.enums.module_generation_flow import * from app.enums.shot_replicate import * +from app.enums.video_prompt_schema import * +from app.enums.user import * +from app.enums.credit_record import * +from app.enums.token_usage import * diff --git a/video-gen-api/app/enums/credit_record.py b/video-gen-api/app/enums/credit_record.py new file mode 100644 index 00000000..1a6c0e58 --- /dev/null +++ b/video-gen-api/app/enums/credit_record.py @@ -0,0 +1,193 @@ +from enum import Enum + + +class CreditRecordType(str, Enum): + RECHARGE = "recharge" + CONSUME = "consume" + REFUND = "refund" + + +class CreditRecordOwnerType(str, Enum): + GENERATION_RECORD = "generation_record" + CHAT_GENERATION_TASK = "chat_generation_task" + MODULE_GENERATION_PROJECT = "module_generation_project" + MODULE_GENERATION_STEP = "module_generation_step" + SHOT_REPLICATE_TASK_SET = "shot_replicate_task_set" + SHOT_REPLICATE_SEGMENT = "shot_replicate_segment" + PAYMENT_ORDER = "payment_order" + ADMIN_ADJUST = "admin_adjust" + UNKNOWN = "unknown" + + +class CreditRecordChargeKind(str, Enum): + MEDIA = "media" + TEXT_PROMPT = "text_prompt" + FILE_PARSE = "file_parse" + VISION_INPUT = "vision_input" + MODULE_CREATE = "module_create" + VIDEO_ANALYSIS = "video_analysis" + VIDEO_SPLIT = "video_split" + RECHARGE = "recharge" + REFUND = "refund" + ADMIN_ADJUST = "admin_adjust" + UNKNOWN = "unknown" + + +class CreditRecordSubject(str, Enum): + MEDIA = "media" + TEXT = "text" + MODULE = "module" + ANALYSIS = "analysis" + SPLIT = "split" + RECHARGE = "recharge" + REFUND = "refund" + ADMIN_ADJUST = "admin_adjust" + UNKNOWN = "unknown" + + +class CreditRecordMediaType(str, Enum): + IMAGE = "image" + VIDEO = "video" + + +class CreditRecordAction(str, Enum): + CHARGE = "charge" + REFUND = "refund" + + +class CreditRecordSourceModule(str, Enum): + AI_CREATION = "ai_creation" + GENERATION_RECORD = "generation_record" + HOT_OPENING_REPLICATE = "hot_opening_replicate" + SHOT_REPLICATE = "shot_replicate" + PAYMENT = "payment" + ADMIN = "admin" + UNKNOWN = "unknown" + + +class CreditRecordSourceStepCode(str, Enum): + MATERIAL_INPUT = "material_input" + IMAGE_PROMPT_OPTIMIZE = "image_prompt_optimize" + IMAGE_GENERATE = "image_generate" + VIDEO_PROMPT_OPTIMIZE = "video_prompt_optimize" + VIDEO_GENERATE = "video_generate" + VIDEO_ANALYSIS = "video_analysis" + UNKNOWN = "unknown" + + +class CreditRecordBillingScene(str, Enum): + AI_CREATION_IMAGE_GENERATE = "ai_creation_image_generate" + AI_CREATION_VIDEO_GENERATE = "ai_creation_video_generate" + + GENERATION_RECORD_TEXT_PROMPT_OPTIMIZE = "generation_record_text_prompt_optimize" + GENERATION_RECORD_IMAGE_GENERATE = "generation_record_image_generate" + GENERATION_RECORD_VIDEO_GENERATE = "generation_record_video_generate" + GENERATION_RECORD_FILE_PARSE = "generation_record_file_parse" + GENERATION_RECORD_VISION_INPUT = "generation_record_vision_input" + + HOT_OPENING_PROJECT_CREATE = "hot_opening_project_create" + HOT_OPENING_IMAGE_PROMPT_OPTIMIZE = "hot_opening_image_prompt_optimize" + HOT_OPENING_IMAGE_GENERATE = "hot_opening_image_generate" + HOT_OPENING_VIDEO_PROMPT_OPTIMIZE = "hot_opening_video_prompt_optimize" + HOT_OPENING_VIDEO_GENERATE = "hot_opening_video_generate" + + SHOT_VIDEO_ANALYSIS = "shot_video_analysis" + SHOT_ORIGINAL_VIDEO_ANALYSIS = "shot_original_video_analysis" + SHOT_SEGMENT_VIDEO_ANALYSIS = "shot_segment_video_analysis" + SHOT_VIDEO_SPLIT = "shot_video_split" + SHOT_SEGMENT_REPLICATE_CREATE = "shot_segment_replicate_create" + SHOT_IMAGE_PROMPT_OPTIMIZE = "shot_image_prompt_optimize" + SHOT_IMAGE_GENERATE = "shot_image_generate" + SHOT_VIDEO_PROMPT_OPTIMIZE = "shot_video_prompt_optimize" + SHOT_VIDEO_GENERATE = "shot_video_generate" + + RECHARGE = "recharge" + ADMIN_ADJUST = "admin_adjust" + REFUND = "refund" + UNKNOWN = "unknown" + + +CREDIT_RECORD_TYPE_LABELS = { + CreditRecordType.RECHARGE.value: "充值", + CreditRecordType.CONSUME.value: "消费", + CreditRecordType.REFUND.value: "回退", +} + +CREDIT_RECORD_SUBJECT_LABELS = { + CreditRecordSubject.MEDIA.value: "图片/视频生成积分", + CreditRecordSubject.TEXT.value: "提词优化积分", + CreditRecordSubject.MODULE.value: "模块功能积分", + CreditRecordSubject.ANALYSIS.value: "分析积分", + CreditRecordSubject.SPLIT.value: "切片积分", + CreditRecordSubject.RECHARGE.value: "充值积分", + CreditRecordSubject.REFUND.value: "回退积分", + CreditRecordSubject.ADMIN_ADJUST.value: "管理员调整", + CreditRecordSubject.UNKNOWN.value: "历史未知", +} + +CREDIT_RECORD_CHARGE_KIND_LABELS = { + CreditRecordChargeKind.MEDIA.value: "媒体生成", + CreditRecordChargeKind.TEXT_PROMPT.value: "提词优化", + CreditRecordChargeKind.FILE_PARSE.value: "文件解析", + CreditRecordChargeKind.VISION_INPUT.value: "图片理解", + CreditRecordChargeKind.MODULE_CREATE.value: "创建模块项目", + CreditRecordChargeKind.VIDEO_ANALYSIS.value: "视频分析", + CreditRecordChargeKind.VIDEO_SPLIT.value: "视频切片", + CreditRecordChargeKind.RECHARGE.value: "充值", + CreditRecordChargeKind.REFUND.value: "回退", + CreditRecordChargeKind.ADMIN_ADJUST.value: "管理员调整", + CreditRecordChargeKind.UNKNOWN.value: "历史未知", +} + +CREDIT_RECORD_MEDIA_TYPE_LABELS = { + CreditRecordMediaType.IMAGE.value: "图片", + CreditRecordMediaType.VIDEO.value: "视频", +} + +CREDIT_RECORD_SOURCE_MODULE_LABELS = { + CreditRecordSourceModule.AI_CREATION.value: "AI创作", + CreditRecordSourceModule.GENERATION_RECORD.value: "项目记录", + CreditRecordSourceModule.HOT_OPENING_REPLICATE.value: "爆款开头复刻", + CreditRecordSourceModule.SHOT_REPLICATE.value: "拆镜复刻", + CreditRecordSourceModule.PAYMENT.value: "支付充值", + CreditRecordSourceModule.ADMIN.value: "后台管理", + CreditRecordSourceModule.UNKNOWN.value: "历史未知", +} + +CREDIT_RECORD_SOURCE_STEP_CODE_LABELS = { + CreditRecordSourceStepCode.MATERIAL_INPUT.value: "素材输入", + CreditRecordSourceStepCode.IMAGE_PROMPT_OPTIMIZE.value: "图片提词优化", + CreditRecordSourceStepCode.IMAGE_GENERATE.value: "图片生成", + CreditRecordSourceStepCode.VIDEO_PROMPT_OPTIMIZE.value: "视频提词优化", + CreditRecordSourceStepCode.VIDEO_GENERATE.value: "视频生成", + CreditRecordSourceStepCode.VIDEO_ANALYSIS.value: "视频分析", + CreditRecordSourceStepCode.UNKNOWN.value: "历史未知", +} + +CREDIT_RECORD_BILLING_SCENE_LABELS = { + CreditRecordBillingScene.AI_CREATION_IMAGE_GENERATE.value: "AI创作图片生成", + CreditRecordBillingScene.AI_CREATION_VIDEO_GENERATE.value: "AI创作视频生成", + CreditRecordBillingScene.GENERATION_RECORD_TEXT_PROMPT_OPTIMIZE.value: "项目记录提词优化", + CreditRecordBillingScene.GENERATION_RECORD_IMAGE_GENERATE.value: "项目记录图片生成", + CreditRecordBillingScene.GENERATION_RECORD_VIDEO_GENERATE.value: "项目记录视频生成", + CreditRecordBillingScene.GENERATION_RECORD_FILE_PARSE.value: "项目记录文件解析", + CreditRecordBillingScene.GENERATION_RECORD_VISION_INPUT.value: "项目记录图片理解", + CreditRecordBillingScene.HOT_OPENING_PROJECT_CREATE.value: "爆款开头复刻创建项目", + CreditRecordBillingScene.HOT_OPENING_IMAGE_PROMPT_OPTIMIZE.value: "爆款开头复刻图片提词优化", + CreditRecordBillingScene.HOT_OPENING_IMAGE_GENERATE.value: "爆款开头复刻图片生成", + CreditRecordBillingScene.HOT_OPENING_VIDEO_PROMPT_OPTIMIZE.value: "爆款开头复刻视频提词优化", + CreditRecordBillingScene.HOT_OPENING_VIDEO_GENERATE.value: "爆款开头复刻视频生成", + CreditRecordBillingScene.SHOT_VIDEO_ANALYSIS.value: "拆镜视频分析", + CreditRecordBillingScene.SHOT_ORIGINAL_VIDEO_ANALYSIS.value: "拆镜复刻原视频分析", + CreditRecordBillingScene.SHOT_SEGMENT_VIDEO_ANALYSIS.value: "拆镜复刻片段视频分析", + CreditRecordBillingScene.SHOT_VIDEO_SPLIT.value: "拆镜切片", + CreditRecordBillingScene.SHOT_SEGMENT_REPLICATE_CREATE.value: "从切片创建复刻项目", + CreditRecordBillingScene.SHOT_IMAGE_PROMPT_OPTIMIZE.value: "拆镜复刻图片提词优化", + CreditRecordBillingScene.SHOT_IMAGE_GENERATE.value: "拆镜复刻图片生成", + CreditRecordBillingScene.SHOT_VIDEO_PROMPT_OPTIMIZE.value: "拆镜复刻视频提词优化", + CreditRecordBillingScene.SHOT_VIDEO_GENERATE.value: "拆镜复刻视频生成", + CreditRecordBillingScene.RECHARGE.value: "充值", + CreditRecordBillingScene.ADMIN_ADJUST.value: "管理员调整", + CreditRecordBillingScene.REFUND.value: "回退", + CreditRecordBillingScene.UNKNOWN.value: "历史未知", +} diff --git a/video-gen-api/app/enums/token_usage.py b/video-gen-api/app/enums/token_usage.py new file mode 100644 index 00000000..9161ff26 --- /dev/null +++ b/video-gen-api/app/enums/token_usage.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class TokenUsageOwnerType(str, Enum): + GENERATION_RECORD = "generation_record" + CHAT_GENERATION_TASK = "chat_generation_task" + MODULE_GENERATION_STEP = "module_generation_step" + SHOT_REPLICATE_TASK_SET = "shot_replicate_task_set" + SHOT_REPLICATE_SEGMENT = "shot_replicate_segment" + UNKNOWN = "unknown" diff --git a/video-gen-api/app/enums/user.py b/video-gen-api/app/enums/user.py new file mode 100644 index 00000000..5ae95afc --- /dev/null +++ b/video-gen-api/app/enums/user.py @@ -0,0 +1,22 @@ +from enum import Enum + + +class UserType(str, Enum): + ADMIN = "admin" + FRONTEND = "frontend" + + +class FrontendUserKind(str, Enum): + INTERNAL = "internal" + EXTERNAL = "external" + + +USER_TYPE_LABELS = { + UserType.ADMIN.value: "后台用户", + UserType.FRONTEND.value: "前台用户", +} + +FRONTEND_USER_KIND_LABELS = { + FrontendUserKind.INTERNAL.value: "前台内部用户", + FrontendUserKind.EXTERNAL.value: "前台外部用户", +} diff --git a/video-gen-api/app/models/credit_record.py b/video-gen-api/app/models/credit_record.py index 908e3c84..d01d2e14 100644 --- a/video-gen-api/app/models/credit_record.py +++ b/video-gen-api/app/models/credit_record.py @@ -1,4 +1,4 @@ -from sqlalchemy import Float, ForeignKey, Index, String +from sqlalchemy import Float, ForeignKey, Index, Integer, String from sqlalchemy.orm import Mapped, mapped_column from app.models.base import Base, TimestampMixin @@ -12,13 +12,17 @@ class CreditRecord(Base, TimestampMixin): Index("uq_credit_records_user_biz_key", "user_id", "biz_key", unique=True), Index("ix_credit_records_user_refund_for_biz_key", "user_id", "refund_for_biz_key"), Index("ix_credit_records_related_type", "related_id", "type"), + Index("ix_credit_records_owner", "owner_type", "owner_id"), + Index("ix_credit_records_subject_media", "credit_subject", "media_type"), + Index("ix_credit_records_source_module_scene", "source_module", "billing_scene"), + Index("ix_credit_records_user_kind_time", "user_type_snapshot", "frontend_user_kind_snapshot", "created_at"), ) 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 ) - type: Mapped[str] = mapped_column(String(16)) + type: Mapped[str] = mapped_column(String(16), index=True) amount: Mapped[float] = mapped_column(Float) balance_after: Mapped[float] = mapped_column(Float) description: Mapped[str] = mapped_column(String(256)) @@ -31,3 +35,34 @@ class CreditRecord(Base, TimestampMixin): # 如果当前流水是退款,记录它退的是哪一次扣费。 # 例如:generation_record:{record_id}:attempt:1:media:charge refund_for_biz_key: Mapped[str | None] = mapped_column(String(160), nullable=True, index=True) + + # 账务快照字段:保证业务步骤/资源软删后,流水仍可独立展示。 + owner_type: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True) + owner_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True) + attempt_no: Mapped[int | None] = mapped_column(Integer, nullable=True) + + charge_kind: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True) + charge_action: Mapped[str | None] = mapped_column(String(16), nullable=True) + credit_subject: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True) + media_type: Mapped[str | None] = mapped_column(String(16), nullable=True, index=True) + billing_scene: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True) + + source_module: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True) + source_project_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True) + source_step_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True) + source_step_code: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True) + + token_usage_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True) + input_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True) + output_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True) + total_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True) + + + engine_type: Mapped[str | None] = mapped_column(String(16), nullable=True) + engine_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True) + engine_name: Mapped[str | None] = mapped_column(String(128), nullable=True) + engine_provider: Mapped[str | None] = mapped_column(String(64), nullable=True) + engine_model_name: Mapped[str | None] = mapped_column(String(128), nullable=True) + + user_type_snapshot: Mapped[str | None] = mapped_column(String(16), nullable=True, index=True) + frontend_user_kind_snapshot: Mapped[str | None] = mapped_column(String(16), nullable=True, index=True) diff --git a/video-gen-api/app/models/module_generation_step.py b/video-gen-api/app/models/module_generation_step.py index 81156be1..1d378050 100644 --- a/video-gen-api/app/models/module_generation_step.py +++ b/video-gen-api/app/models/module_generation_step.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime from typing import Any -from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, JSON, String, Text +from sqlalchemy import Boolean, DateTime, Float, ForeignKey, Index, Integer, JSON, String, Text from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column @@ -72,3 +72,11 @@ class ModuleGenerationStep(Base, TimestampMixin, SoftDeleteMixin): error_message: Mapped[str | None] = mapped_column(Text, nullable=True) started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + + # 提词优化成本快照,避免后台/详情页反复解析 output_json.usage。 + token_usage_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True) + model_config_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True) + input_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True) + output_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True) + total_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True) + text_credits_cost: Mapped[float | None] = mapped_column(Float, nullable=True) diff --git a/video-gen-api/app/models/token_usage.py b/video-gen-api/app/models/token_usage.py index c08fd0d8..de403537 100644 --- a/video-gen-api/app/models/token_usage.py +++ b/video-gen-api/app/models/token_usage.py @@ -1,6 +1,4 @@ -from datetime import datetime - -from sqlalchemy import DateTime, ForeignKey, Integer, String +from sqlalchemy import ForeignKey, Index, Integer, String from sqlalchemy.orm import Mapped, mapped_column from app.models.base import Base, TimestampMixin @@ -8,6 +6,10 @@ from app.models.base import Base, TimestampMixin class TokenUsage(Base, TimestampMixin): __tablename__ = "token_usage" + __table_args__ = ( + Index("ix_token_usage_owner", "owner_type", "owner_id"), + Index("ix_token_usage_biz_key", "biz_key"), + ) id: Mapped[str] = mapped_column(String(32), primary_key=True) model_config_id: Mapped[str | None] = mapped_column( @@ -19,3 +21,9 @@ class TokenUsage(Base, TimestampMixin): input_tokens: Mapped[int] = mapped_column(Integer, default=0) output_tokens: Mapped[int] = mapped_column(Integer, default=0) total_tokens: Mapped[int] = mapped_column(Integer, default=0) + + owner_type: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True) + owner_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True) + biz_key: Mapped[str | None] = mapped_column(String(160), nullable=True, index=True) + source_module: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True) + source_step_code: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True) diff --git a/video-gen-api/app/models/user.py b/video-gen-api/app/models/user.py index 573d6d87..ae276098 100644 --- a/video-gen-api/app/models/user.py +++ b/video-gen-api/app/models/user.py @@ -1,8 +1,9 @@ from datetime import datetime -from sqlalchemy import Boolean, DateTime, Float, Integer, String, JSON +from sqlalchemy import Boolean, DateTime, Float, String, JSON from sqlalchemy.orm import Mapped, mapped_column +from app.enums.user import FrontendUserKind from app.models.base import Base, TimestampMixin @@ -19,7 +20,15 @@ class User(Base, TimestampMixin): credits: Mapped[float] = mapped_column(Float, default=0.0) is_active: Mapped[bool] = mapped_column(Boolean, default=True) is_admin: Mapped[bool] = mapped_column(Boolean, default=False) - user_type: Mapped[str] = mapped_column(String(16), default="frontend") + user_type: Mapped[str] = mapped_column(String(16), default="frontend", index=True) + # 仅前台用户有业务意义;默认外部用户。取消内部标记时也设置回 external。 + frontend_user_kind: Mapped[str] = mapped_column( + String(16), + default=FrontendUserKind.EXTERNAL.value, + server_default=FrontendUserKind.EXTERNAL.value, + index=True, + nullable=False, + ) last_login_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) diff --git a/video-gen-api/app/schemas/admin.py b/video-gen-api/app/schemas/admin.py index f454475d..8a6abfc6 100644 --- a/video-gen-api/app/schemas/admin.py +++ b/video-gen-api/app/schemas/admin.py @@ -50,6 +50,7 @@ class AdminUserOut(BaseModel): is_active: bool is_admin: bool user_type: str = "frontend" + frontend_user_kind: str = "external" created_at: NaiveDatetime last_login_at: NaiveDatetimeOptional = None allowed_menus: list | None = None @@ -64,9 +65,14 @@ class CreateUserRequest(BaseModel): phone: str | None = None credits: float = 0.0 user_type: str = Field(default="frontend", pattern="^(frontend|admin)$") + frontend_user_kind: str = Field(default="external", pattern="^(internal|external)$") allowed_menus: list | None = None +class UpdateFrontendUserKindRequest(BaseModel): + frontend_user_kind: str = Field(..., pattern="^(internal|external)$") + + class UpdateMenusRequest(BaseModel): allowed_menus: list | None = None @@ -104,3 +110,78 @@ class AdminStatsOut(BaseModel): last_period_records: int = 0 last_period_revenue: float = 0.0 last_period_credits_consumed: float = 0.0 + + +class AdminCreditRecordSummaryOut(BaseModel): + total_recharge: float = 0.0 + total_consume: float = 0.0 + total_refund: float = 0.0 + transaction_count: int = 0 + generation_count: int = 0 + generation_attempt_count: int = 0 + image_generation_count: int = 0 + video_generation_count: int = 0 + image_consume: float = 0.0 + video_consume: float = 0.0 + text_consume: float = 0.0 + analysis_consume: float = 0.0 + total_tokens: int = 0 + input_tokens: int = 0 + output_tokens: int = 0 + + +class AdminCreditRecordOut(BaseModel): + id: str + user_id: str + username: str | None = None + phone: str | None = None + email: str | None = None + user_type: str | None = None + user_type_label: str | None = None + frontend_user_kind: str | None = None + frontend_user_kind_label: str | None = None + type: str + record_type: str + record_type_label: str | None = None + amount: float + balance_after: float + description: str | None = None + related_id: str | None = None + biz_key: str | None = None + refund_for_biz_key: str | None = None + owner_type: str | None = None + owner_id: str | None = None + owner_deleted: bool = False + owner_deleted_at: str | None = None + attempt_no: int | None = None + charge_kind: str | None = None + charge_kind_label: str | None = None + charge_action: str | None = None + credit_subject: str | None = None + credit_subject_label: str | None = None + media_type: str | None = None + media_type_label: str | None = None + billing_scene: str | None = None + billing_scene_label: str | None = None + source_module: str | None = None + source_module_label: str | None = None + source_project_id: str | None = None + source_step_id: str | None = None + source_step_code: str | None = None + source_step_code_label: str | None = None + token_usage_id: str | None = None + input_tokens: int = 0 + output_tokens: int = 0 + total_tokens: int = 0 + engine_type: str | None = None + engine_id: str | None = None + engine_name: str | None = None + engine_provider: str | None = None + engine_model_name: str | None = None + created_at: str | None = None + + +class AdminCreditRecordListOut(BaseModel): + items: list[AdminCreditRecordOut] + total: int + summary: AdminCreditRecordSummaryOut diff --git a/video-gen-api/app/services/admin_credit_record_service.py b/video-gen-api/app/services/admin_credit_record_service.py new file mode 100644 index 00000000..690d1303 --- /dev/null +++ b/video-gen-api/app/services/admin_credit_record_service.py @@ -0,0 +1,299 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Any + +from sqlalchemy import and_, case, distinct, func, or_, select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.enums.credit_record import ( + CREDIT_RECORD_BILLING_SCENE_LABELS, + CREDIT_RECORD_CHARGE_KIND_LABELS, + CREDIT_RECORD_MEDIA_TYPE_LABELS, + CREDIT_RECORD_SOURCE_MODULE_LABELS, + CREDIT_RECORD_SOURCE_STEP_CODE_LABELS, + CREDIT_RECORD_SUBJECT_LABELS, + CREDIT_RECORD_TYPE_LABELS, + CreditRecordSubject, +) +from app.enums.user import FRONTEND_USER_KIND_LABELS, USER_TYPE_LABELS, UserType +from app.models.chat_generation_task import ChatGenerationTask +from app.models.credit_record import CreditRecord +from app.models.generation_record import GenerationRecord +from app.models.module_generation_project import ModuleGenerationProject +from app.models.module_generation_step import ModuleGenerationStep +from app.models.shot_replicate_segment import ShotReplicateSegment +from app.models.shot_replicate_task_set import ShotReplicateTaskSet +from app.models.user import User + + +def _iso(dt: Any) -> str | None: + if dt is None: + return None + try: + return dt.isoformat() + except Exception: + return str(dt) + + +def _round2(value: Any) -> float: + try: + return round(float(value or 0), 2) + except Exception: + return 0.0 + + +def _as_date_start(value: str | None) -> datetime | None: + if not value: + return None + try: + return datetime.strptime(value, "%Y-%m-%d") + except Exception: + return None + + +def _as_date_end(value: str | None) -> datetime | None: + if not value: + return None + try: + return datetime.strptime(value, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=999999) + except Exception: + return None + + +def _label(mapping: dict[str, str], value: str | None) -> str: + if not value: + return "-" + return mapping.get(value, value) + + +def _build_filters( + *, + user_id: str | None = None, + user_name: str | None = None, + user_type: str | None = None, + frontend_user_kind: str | None = None, + record_type: str | None = None, + credit_subject: str | None = None, + media_type: str | None = None, + charge_kind: str | None = None, + source_module: str | None = None, + source_step_code: str | None = None, + billing_scene: str | None = None, + start_date: str | None = None, + end_date: str | None = None, +) -> list[Any]: + filters: list[Any] = [] + if user_id: + filters.append(CreditRecord.user_id == user_id) + if user_name: + like = f"%{user_name}%" + filters.append(or_(User.username.ilike(like), User.phone.ilike(like), User.email.ilike(like))) + if user_type: + filters.append(CreditRecord.user_type_snapshot == user_type) + if frontend_user_kind: + filters.append(CreditRecord.frontend_user_kind_snapshot == frontend_user_kind) + filters.append(CreditRecord.user_type_snapshot == UserType.FRONTEND.value) + if record_type: + filters.append(CreditRecord.type == record_type) + if credit_subject: + filters.append(CreditRecord.credit_subject == credit_subject) + if media_type: + filters.append(CreditRecord.media_type == media_type) + if charge_kind: + filters.append(CreditRecord.charge_kind == charge_kind) + if source_module: + filters.append(CreditRecord.source_module == source_module) + if source_step_code: + filters.append(CreditRecord.source_step_code == source_step_code) + if billing_scene: + filters.append(CreditRecord.billing_scene == billing_scene) + start = _as_date_start(start_date) + end = _as_date_end(end_date) + if start: + filters.append(CreditRecord.created_at >= start) + if end: + filters.append(CreditRecord.created_at <= end) + return filters + + +async def _load_deleted_map(db: AsyncSession, records: list[CreditRecord]) -> dict[tuple[str, str], tuple[bool, str | None]]: + grouped: dict[str, set[str]] = {} + for record in records: + if record.owner_type and record.owner_id: + grouped.setdefault(record.owner_type, set()).add(record.owner_id) + + model_map: dict[str, Any] = { + "chat_generation_task": ChatGenerationTask, + "generation_record": GenerationRecord, + "module_generation_project": ModuleGenerationProject, + "module_generation_step": ModuleGenerationStep, + "shot_replicate_task_set": ShotReplicateTaskSet, + "shot_replicate_segment": ShotReplicateSegment, + } + deleted_map: dict[tuple[str, str], tuple[bool, str | None]] = {} + for owner_type, ids in grouped.items(): + model = model_map.get(owner_type) + if not model or not ids: + continue + result = await db.execute(select(model.id, model.deleted_at).where(model.id.in_(ids))) + found = {row[0]: row[1] for row in result.all()} + for owner_id in ids: + deleted_at = found.get(owner_id) + deleted_map[(owner_type, owner_id)] = (bool(deleted_at), _iso(deleted_at)) if owner_id in found else (False, None) + return deleted_map + + +def _record_to_item(record: CreditRecord, user: User | None, deleted_map: dict[tuple[str, str], tuple[bool, str | None]]) -> dict[str, Any]: + owner_deleted = False + owner_deleted_at = None + if record.owner_type and record.owner_id: + owner_deleted, owner_deleted_at = deleted_map.get((record.owner_type, record.owner_id), (False, None)) + + user_type = record.user_type_snapshot or (user.user_type if user else None) + frontend_kind = record.frontend_user_kind_snapshot or (getattr(user, "frontend_user_kind", None) if user else None) + return { + "id": record.id, + "user_id": record.user_id, + "username": user.username if user else None, + "phone": user.phone if user else None, + "email": user.email if user else None, + "user_type": user_type, + "user_type_label": _label(USER_TYPE_LABELS, user_type), + "frontend_user_kind": frontend_kind, + "frontend_user_kind_label": _label(FRONTEND_USER_KIND_LABELS, frontend_kind), + "type": record.type, + "record_type": record.type, + "record_type_label": _label(CREDIT_RECORD_TYPE_LABELS, record.type), + "amount": _round2(record.amount), + "balance_after": _round2(record.balance_after), + "description": record.description, + "related_id": record.related_id, + "biz_key": record.biz_key, + "refund_for_biz_key": record.refund_for_biz_key, + "owner_type": record.owner_type, + "owner_id": record.owner_id, + "owner_deleted": owner_deleted, + "owner_deleted_at": owner_deleted_at, + "attempt_no": record.attempt_no, + "charge_kind": record.charge_kind, + "charge_kind_label": _label(CREDIT_RECORD_CHARGE_KIND_LABELS, record.charge_kind), + "charge_action": record.charge_action, + "credit_subject": record.credit_subject, + "credit_subject_label": _label(CREDIT_RECORD_SUBJECT_LABELS, record.credit_subject), + "media_type": record.media_type, + "media_type_label": _label(CREDIT_RECORD_MEDIA_TYPE_LABELS, record.media_type), + "billing_scene": record.billing_scene, + "billing_scene_label": _label(CREDIT_RECORD_BILLING_SCENE_LABELS, record.billing_scene), + "source_module": record.source_module, + "source_module_label": _label(CREDIT_RECORD_SOURCE_MODULE_LABELS, record.source_module), + "source_project_id": record.source_project_id, + "source_step_id": record.source_step_id, + "source_step_code": record.source_step_code, + "source_step_code_label": _label(CREDIT_RECORD_SOURCE_STEP_CODE_LABELS, record.source_step_code), + "token_usage_id": record.token_usage_id, + "input_tokens": record.input_tokens or 0, + "output_tokens": record.output_tokens or 0, + "total_tokens": record.total_tokens or 0, + "engine_type": record.engine_type, + "engine_id": record.engine_id, + "engine_name": record.engine_name, + "engine_provider": record.engine_provider, + "engine_model_name": record.engine_model_name, + "created_at": _iso(record.created_at), + } + + +async def list_admin_credit_records( + db: AsyncSession, + *, + page: int, + page_size: int, + user_id: str | None = None, + user_name: str | None = None, + user_type: str | None = None, + frontend_user_kind: str | None = None, + record_type: str | None = None, + credit_subject: str | None = None, + media_type: str | None = None, + charge_kind: str | None = None, + source_module: str | None = None, + source_step_code: str | None = None, + billing_scene: str | None = None, + start_date: str | None = None, + end_date: str | None = None, +) -> dict[str, Any]: + page = max(int(page or 1), 1) + page_size = min(max(int(page_size or 20), 1), 1000) + filters = _build_filters( + user_id=user_id, + user_name=user_name, + user_type=user_type, + frontend_user_kind=frontend_user_kind, + record_type=record_type, + credit_subject=credit_subject, + media_type=media_type, + charge_kind=charge_kind, + source_module=source_module, + source_step_code=source_step_code, + billing_scene=billing_scene, + start_date=start_date, + end_date=end_date, + ) + where_clause = and_(*filters) if filters else None + + base_query = select(CreditRecord, User).join(User, CreditRecord.user_id == User.id, isouter=True) + count_query = select(func.count(CreditRecord.id)).select_from(CreditRecord).join(User, CreditRecord.user_id == User.id, isouter=True) + if where_clause is not None: + base_query = base_query.where(where_clause) + count_query = count_query.where(where_clause) + total = (await db.execute(count_query)).scalar() or 0 + + result = await db.execute( + base_query.order_by(CreditRecord.created_at.desc(), CreditRecord.id.desc()) + .offset((page - 1) * page_size) + .limit(page_size) + ) + rows = result.all() + records = [row[0] for row in rows] + deleted_map = await _load_deleted_map(db, records) + items = [_record_to_item(record, user, deleted_map) for record, user in rows] + + summary_query = select( + func.coalesce(func.sum(case((CreditRecord.type == "recharge", CreditRecord.amount), else_=0)), 0), + func.coalesce(func.sum(case((CreditRecord.type == "consume", func.abs(CreditRecord.amount)), else_=0)), 0), + func.coalesce(func.sum(case((CreditRecord.type == "refund", CreditRecord.amount), else_=0)), 0), + func.count(CreditRecord.id), + func.count(distinct(case((and_(CreditRecord.credit_subject == CreditRecordSubject.MEDIA.value, CreditRecord.type == "consume"), func.concat(CreditRecord.owner_type, ":", CreditRecord.owner_id)), else_=None))), + func.count(case((and_(CreditRecord.credit_subject == CreditRecordSubject.MEDIA.value, CreditRecord.type == "consume"), 1), else_=None)), + func.count(distinct(case((and_(CreditRecord.credit_subject == CreditRecordSubject.MEDIA.value, CreditRecord.media_type == "image", CreditRecord.type == "consume"), func.concat(CreditRecord.owner_type, ":", CreditRecord.owner_id)), else_=None))), + func.count(distinct(case((and_(CreditRecord.credit_subject == CreditRecordSubject.MEDIA.value, CreditRecord.media_type == "video", CreditRecord.type == "consume"), func.concat(CreditRecord.owner_type, ":", CreditRecord.owner_id)), else_=None))), + func.coalesce(func.sum(case((and_(CreditRecord.credit_subject == CreditRecordSubject.MEDIA.value, CreditRecord.media_type == "image", CreditRecord.type == "consume"), func.abs(CreditRecord.amount)), else_=0)), 0), + func.coalesce(func.sum(case((and_(CreditRecord.credit_subject == CreditRecordSubject.MEDIA.value, CreditRecord.media_type == "video", CreditRecord.type == "consume"), func.abs(CreditRecord.amount)), else_=0)), 0), + func.coalesce(func.sum(case((and_(CreditRecord.credit_subject == CreditRecordSubject.TEXT.value, CreditRecord.type == "consume"), func.abs(CreditRecord.amount)), else_=0)), 0), + func.coalesce(func.sum(case((and_(CreditRecord.credit_subject == CreditRecordSubject.ANALYSIS.value, CreditRecord.type == "consume"), func.abs(CreditRecord.amount)), else_=0)), 0), + func.coalesce(func.sum(CreditRecord.total_tokens), 0), + func.coalesce(func.sum(CreditRecord.input_tokens), 0), + func.coalesce(func.sum(CreditRecord.output_tokens), 0), + ).select_from(CreditRecord).join(User, CreditRecord.user_id == User.id, isouter=True) + if where_clause is not None: + summary_query = summary_query.where(where_clause) + s = (await db.execute(summary_query)).one() + summary = { + "total_recharge": _round2(s[0]), + "total_consume": _round2(s[1]), + "total_refund": _round2(s[2]), + "transaction_count": int(s[3] or 0), + "generation_count": int(s[4] or 0), + "generation_attempt_count": int(s[5] or 0), + "image_generation_count": int(s[6] or 0), + "video_generation_count": int(s[7] or 0), + "image_consume": _round2(s[8]), + "video_consume": _round2(s[9]), + "text_consume": _round2(s[10]), + "analysis_consume": _round2(s[11]), + "total_tokens": int(s[12] or 0), + "input_tokens": int(s[13] or 0), + "output_tokens": int(s[14] or 0), + } + return {"items": items, "total": total, "summary": summary} diff --git a/video-gen-api/app/services/credit_record_meta_service.py b/video-gen-api/app/services/credit_record_meta_service.py new file mode 100644 index 00000000..14339537 --- /dev/null +++ b/video-gen-api/app/services/credit_record_meta_service.py @@ -0,0 +1,360 @@ +from __future__ import annotations + +from dataclasses import asdict, dataclass +from typing import Any, Mapping + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.enums.credit_record import ( + CreditRecordAction, + CreditRecordBillingScene, + CreditRecordChargeKind, + CreditRecordOwnerType, + CreditRecordSourceModule, + CreditRecordSourceStepCode, + CreditRecordSubject, +) +from app.enums.user import FrontendUserKind +from app.models.image_engine import ImageEngine +from app.models.model_config import ModelConfig +from app.models.module_generation_step import ModuleGenerationStep +from app.models.user import User +from app.models.video_engine import VideoEngine + + +@dataclass(slots=True) +class CreditRecordMeta: + owner_type: str | None = None + owner_id: str | None = None + attempt_no: int | None = None + + charge_kind: str | None = None + charge_action: str | None = None + credit_subject: str | None = None + media_type: str | None = None + billing_scene: str | None = None + + source_module: str | None = None + source_project_id: str | None = None + source_step_id: str | None = None + source_step_code: str | None = None + + token_usage_id: str | None = None + input_tokens: int | None = None + output_tokens: int | None = None + total_tokens: int | None = None + + # 统一执行配置冷备字段: + # - engine_type=model 时,engine_* 来源于 model_configs + # - engine_type=image 时,engine_* 来源于 image_engines + # - engine_type=video 时,engine_* 来源于 video_engines + engine_type: str | None = None + engine_id: str | None = None + engine_name: str | None = None + engine_provider: str | None = None + engine_model_name: str | None = None + + user_type_snapshot: str | None = None + frontend_user_kind_snapshot: str | None = None + + def to_record_kwargs(self) -> dict[str, Any]: + return {k: v for k, v in asdict(self).items() if v is not None} + + +def _safe_int(value: Any, default: int = 0) -> int: + try: + if value is None or value == "": + return default + return int(value) + except Exception: + return default + + +def _normalize_frontend_kind(value: str | None) -> str: + return value or FrontendUserKind.EXTERNAL.value + + +async def with_user_snapshot(db: AsyncSession, meta: CreditRecordMeta, user_id: str) -> CreditRecordMeta: + result = await db.execute(select(User).where(User.id == user_id).limit(1)) + user = result.scalar_one_or_none() + if user: + meta.user_type_snapshot = user.user_type + meta.frontend_user_kind_snapshot = _normalize_frontend_kind(getattr(user, "frontend_user_kind", None)) + return meta + + +async def get_model_snapshot(db: AsyncSession, model_config_id: str | None) -> dict[str, str | None]: + """把 ModelConfig 冷备为统一 engine_* 快照。""" + if not model_config_id: + return {"engine_type": "model"} + result = await db.execute(select(ModelConfig).where(ModelConfig.id == model_config_id).limit(1)) + config = result.scalar_one_or_none() + if not config: + return {"engine_type": "model", "engine_id": model_config_id} + return { + "engine_type": "model", + "engine_id": config.id, + "engine_name": config.name, + "engine_provider": config.provider, + "engine_model_name": config.model_name, + } + + +def _model_snapshot_from_usage(usage: Mapping[str, Any]) -> dict[str, str | None]: + model_config_id = usage.get("model_config_id") + return { + "engine_type": "model", + "engine_id": model_config_id, + "engine_name": usage.get("model_config_name") or usage.get("engine_name"), + "engine_provider": usage.get("model_provider") or usage.get("provider") or usage.get("engine_provider"), + "engine_model_name": usage.get("model_name") or usage.get("model") or usage.get("engine_model_name"), + } + + +async def _apply_model_snapshot(db: AsyncSession, meta: CreditRecordMeta, usage: Mapping[str, Any]) -> CreditRecordMeta: + for key, value in _model_snapshot_from_usage(usage).items(): + setattr(meta, key, value) + if meta.engine_id and not meta.engine_model_name: + for key, value in (await get_model_snapshot(db, meta.engine_id)).items(): + setattr(meta, key, value) + return meta + + +async def get_engine_snapshot(db: AsyncSession, *, gen_type: str, engine_id: str | None) -> dict[str, str | None]: + if not engine_id: + return {"engine_type": (gen_type or None)} + gen_type = (gen_type or "").lower().strip() + if gen_type == "image": + result = await db.execute(select(ImageEngine).where(ImageEngine.id == engine_id).limit(1)) + else: + result = await db.execute(select(VideoEngine).where(VideoEngine.id == engine_id).limit(1)) + engine = result.scalar_one_or_none() + if not engine: + return {"engine_type": gen_type or None, "engine_id": engine_id} + return { + "engine_type": gen_type or None, + "engine_id": engine.id, + "engine_name": getattr(engine, "name", None), + "engine_provider": getattr(engine, "provider", None), + "engine_model_name": getattr(engine, "model_name", None), + } + + +def infer_module_billing_scene(*, source_module: str | None, source_step_code: str | None, media_type: str | None = None) -> str | None: + if source_module == CreditRecordSourceModule.HOT_OPENING_REPLICATE.value: + mapping = { + "image_prompt_optimize": CreditRecordBillingScene.HOT_OPENING_IMAGE_PROMPT_OPTIMIZE.value, + "image_generate": CreditRecordBillingScene.HOT_OPENING_IMAGE_GENERATE.value, + "video_prompt_optimize": CreditRecordBillingScene.HOT_OPENING_VIDEO_PROMPT_OPTIMIZE.value, + "video_generate": CreditRecordBillingScene.HOT_OPENING_VIDEO_GENERATE.value, + } + return mapping.get(source_step_code) + if source_module == CreditRecordSourceModule.SHOT_REPLICATE.value: + mapping = { + "image_prompt_optimize": CreditRecordBillingScene.SHOT_IMAGE_PROMPT_OPTIMIZE.value, + "image_generate": CreditRecordBillingScene.SHOT_IMAGE_GENERATE.value, + "video_prompt_optimize": CreditRecordBillingScene.SHOT_VIDEO_PROMPT_OPTIMIZE.value, + "video_generate": CreditRecordBillingScene.SHOT_VIDEO_GENERATE.value, + CreditRecordSourceStepCode.VIDEO_ANALYSIS.value: CreditRecordBillingScene.SHOT_VIDEO_ANALYSIS.value, + } + return mapping.get(source_step_code) + if source_module == CreditRecordSourceModule.AI_CREATION.value: + return CreditRecordBillingScene.AI_CREATION_IMAGE_GENERATE.value if media_type == "image" else CreditRecordBillingScene.AI_CREATION_VIDEO_GENERATE.value + if source_module == CreditRecordSourceModule.GENERATION_RECORD.value: + return CreditRecordBillingScene.GENERATION_RECORD_IMAGE_GENERATE.value if media_type == "image" else CreditRecordBillingScene.GENERATION_RECORD_VIDEO_GENERATE.value + return None + + +def build_admin_adjust_meta(*, action: str = CreditRecordAction.CHARGE.value) -> CreditRecordMeta: + return CreditRecordMeta( + owner_type=CreditRecordOwnerType.ADMIN_ADJUST.value, + charge_kind=CreditRecordChargeKind.ADMIN_ADJUST.value, + charge_action=action, + credit_subject=CreditRecordSubject.ADMIN_ADJUST.value, + source_module=CreditRecordSourceModule.ADMIN.value, + billing_scene=CreditRecordBillingScene.ADMIN_ADJUST.value, + ) + + +def build_recharge_meta(*, owner_id: str | None = None) -> CreditRecordMeta: + return CreditRecordMeta( + owner_type=CreditRecordOwnerType.PAYMENT_ORDER.value if owner_id else None, + owner_id=owner_id, + charge_kind=CreditRecordChargeKind.RECHARGE.value, + charge_action=CreditRecordAction.CHARGE.value, + credit_subject=CreditRecordSubject.RECHARGE.value, + source_module=CreditRecordSourceModule.PAYMENT.value, + billing_scene=CreditRecordBillingScene.RECHARGE.value, + ) + + +def build_payment_refund_meta(*, owner_id: str | None = None) -> CreditRecordMeta: + return CreditRecordMeta( + owner_type=CreditRecordOwnerType.PAYMENT_ORDER.value if owner_id else None, + owner_id=owner_id, + charge_kind=CreditRecordChargeKind.REFUND.value, + charge_action=CreditRecordAction.REFUND.value, + credit_subject=CreditRecordSubject.REFUND.value, + source_module=CreditRecordSourceModule.PAYMENT.value, + billing_scene=CreditRecordBillingScene.REFUND.value, + ) + + +async def build_generation_media_meta( + db: AsyncSession, + *, + owner_type: str, + owner_id: str, + attempt_no: int, + gen_type: str, + engine_id: str | None, + source_module: str | None = None, + source_project_id: str | None = None, + source_step_id: str | None = None, + source_step_code: str | None = None, + billing_scene: str | None = None, +) -> CreditRecordMeta: + media_type = (gen_type or "").lower().strip() or None + if source_module is None: + source_module = CreditRecordSourceModule.GENERATION_RECORD.value if owner_type == CreditRecordOwnerType.GENERATION_RECORD.value else CreditRecordSourceModule.AI_CREATION.value + if billing_scene is None: + billing_scene = infer_module_billing_scene(source_module=source_module, source_step_code=source_step_code, media_type=media_type) + meta = CreditRecordMeta( + owner_type=owner_type, + owner_id=owner_id, + attempt_no=attempt_no, + charge_kind=CreditRecordChargeKind.MEDIA.value, + charge_action=CreditRecordAction.CHARGE.value, + credit_subject=CreditRecordSubject.MEDIA.value, + media_type=media_type, + billing_scene=billing_scene, + source_module=source_module, + source_project_id=source_project_id, + source_step_id=source_step_id, + source_step_code=source_step_code, + ) + for key, value in (await get_engine_snapshot(db, gen_type=media_type or "", engine_id=engine_id)).items(): + setattr(meta, key, value) + return meta + + +async def build_generation_record_prompt_meta( + db: AsyncSession, + *, + record_id: str, + attempt_no: int, + charge_kind: str, + usage: Mapping[str, Any], +) -> CreditRecordMeta: + scene_map = { + CreditRecordChargeKind.TEXT_PROMPT.value: CreditRecordBillingScene.GENERATION_RECORD_TEXT_PROMPT_OPTIMIZE.value, + CreditRecordChargeKind.FILE_PARSE.value: CreditRecordBillingScene.GENERATION_RECORD_FILE_PARSE.value, + CreditRecordChargeKind.VISION_INPUT.value: CreditRecordBillingScene.GENERATION_RECORD_VISION_INPUT.value, + } + meta = CreditRecordMeta( + owner_type=CreditRecordOwnerType.GENERATION_RECORD.value, + owner_id=record_id, + attempt_no=attempt_no, + charge_kind=charge_kind, + charge_action=CreditRecordAction.CHARGE.value, + credit_subject=CreditRecordSubject.TEXT.value, + billing_scene=scene_map.get(charge_kind), + source_module=CreditRecordSourceModule.GENERATION_RECORD.value, + token_usage_id=usage.get("token_usage_id"), + input_tokens=_safe_int(usage.get("input_tokens")), + output_tokens=_safe_int(usage.get("output_tokens")), + total_tokens=_safe_int(usage.get("total_tokens"), _safe_int(usage.get("input_tokens")) + _safe_int(usage.get("output_tokens"))), + ) + return await _apply_model_snapshot(db, meta, usage) + + +async def build_module_step_prompt_meta( + db: AsyncSession, + *, + step_id: str, + attempt_no: int, + usage: Mapping[str, Any], +) -> CreditRecordMeta: + result = await db.execute(select(ModuleGenerationStep).where(ModuleGenerationStep.id == step_id).limit(1)) + step = result.scalar_one_or_none() + source_module = getattr(step, "module", None) if step else None + source_step_code = getattr(step, "step_code", None) if step else None + meta = CreditRecordMeta( + owner_type=CreditRecordOwnerType.MODULE_GENERATION_STEP.value, + owner_id=step_id, + attempt_no=attempt_no, + charge_kind=CreditRecordChargeKind.TEXT_PROMPT.value, + charge_action=CreditRecordAction.CHARGE.value, + credit_subject=CreditRecordSubject.TEXT.value, + billing_scene=infer_module_billing_scene(source_module=source_module, source_step_code=source_step_code), + source_module=source_module, + source_project_id=getattr(step, "project_id", None) if step else None, + source_step_id=step_id, + source_step_code=source_step_code, + token_usage_id=usage.get("token_usage_id"), + input_tokens=_safe_int(usage.get("input_tokens")), + output_tokens=_safe_int(usage.get("output_tokens")), + total_tokens=_safe_int(usage.get("total_tokens"), _safe_int(usage.get("input_tokens")) + _safe_int(usage.get("output_tokens"))), + ) + return await _apply_model_snapshot(db, meta, usage) + + +async def build_shot_video_analysis_meta( + db: AsyncSession, + *, + owner_type: str, + owner_id: str, + attempt_no: int, + usage: Mapping[str, Any], + billing_scene: str, + source_project_id: str | None = None, + source_step_id: str | None = None, +) -> CreditRecordMeta: + meta = CreditRecordMeta( + owner_type=owner_type, + owner_id=owner_id, + attempt_no=attempt_no, + charge_kind=CreditRecordChargeKind.VIDEO_ANALYSIS.value, + charge_action=CreditRecordAction.CHARGE.value, + credit_subject=CreditRecordSubject.ANALYSIS.value, + media_type="video", + billing_scene=billing_scene, + source_module=CreditRecordSourceModule.SHOT_REPLICATE.value, + source_project_id=source_project_id, + source_step_id=source_step_id, + source_step_code=CreditRecordSourceStepCode.VIDEO_ANALYSIS.value, + token_usage_id=usage.get("token_usage_id"), + input_tokens=_safe_int(usage.get("input_tokens")), + output_tokens=_safe_int(usage.get("output_tokens")), + total_tokens=_safe_int(usage.get("total_tokens"), _safe_int(usage.get("input_tokens")) + _safe_int(usage.get("output_tokens"))), + ) + return await _apply_model_snapshot(db, meta, usage) + + +def build_refund_meta_from_charge(charge: Any, *, attempt_no: int | None = None) -> CreditRecordMeta: + return CreditRecordMeta( + owner_type=getattr(charge, "owner_type", None), + owner_id=getattr(charge, "owner_id", None), + attempt_no=attempt_no or getattr(charge, "attempt_no", None), + charge_kind=getattr(charge, "charge_kind", None), + charge_action=CreditRecordAction.REFUND.value, + credit_subject=getattr(charge, "credit_subject", None) or CreditRecordSubject.REFUND.value, + media_type=getattr(charge, "media_type", None), + billing_scene=CreditRecordBillingScene.REFUND.value, + source_module=getattr(charge, "source_module", None), + source_project_id=getattr(charge, "source_project_id", None), + source_step_id=getattr(charge, "source_step_id", None), + source_step_code=getattr(charge, "source_step_code", None), + token_usage_id=getattr(charge, "token_usage_id", None), + input_tokens=getattr(charge, "input_tokens", None), + output_tokens=getattr(charge, "output_tokens", None), + total_tokens=getattr(charge, "total_tokens", None), + engine_type=getattr(charge, "engine_type", None), + engine_id=getattr(charge, "engine_id", None), + engine_name=getattr(charge, "engine_name", None), + engine_provider=getattr(charge, "engine_provider", None), + engine_model_name=getattr(charge, "engine_model_name", None), + user_type_snapshot=getattr(charge, "user_type_snapshot", None), + frontend_user_kind_snapshot=getattr(charge, "frontend_user_kind_snapshot", None), + ) diff --git a/video-gen-api/app/services/credits.py b/video-gen-api/app/services/credits.py index fc3d045f..d00797bd 100644 --- a/video-gen-api/app/services/credits.py +++ b/video-gen-api/app/services/credits.py @@ -11,6 +11,7 @@ from app.models.image_engine import ImageEngine from app.models.credit_ratio import CreditRatio from app.utils.id_gen import generate_id from app.utils.exceptions import InsufficientCreditsError +from app.services.credit_record_meta_service import CreditRecordMeta, with_user_snapshot async def calc_text_credits(db: AsyncSession, input_tokens: int, output_tokens: int) -> float: @@ -167,6 +168,7 @@ async def deduct_credits( *, biz_key: str | None = None, refund_for_biz_key: str | None = None, + record_meta: CreditRecordMeta | dict | None = None, ) -> User: """扣减用户积分,并写入消费流水。 @@ -196,6 +198,13 @@ async def deduct_credits( raise InsufficientCreditsError() user.credits = round(float(user.credits or 0) - amount, 2) + meta_kwargs = {} + if record_meta: + if isinstance(record_meta, CreditRecordMeta): + record_meta = await with_user_snapshot(db, record_meta, user_id) + meta_kwargs = record_meta.to_record_kwargs() + elif isinstance(record_meta, dict): + meta_kwargs = {k: v for k, v in record_meta.items() if v is not None} record = CreditRecord( id=generate_id(), user_id=user_id, @@ -206,6 +215,7 @@ async def deduct_credits( related_id=related_id, biz_key=biz_key, refund_for_biz_key=refund_for_biz_key, + **meta_kwargs, ) db.add(record) await db.flush() @@ -222,6 +232,7 @@ async def add_credits( record_type: str = "recharge", biz_key: str | None = None, refund_for_biz_key: str | None = None, + record_meta: CreditRecordMeta | dict | None = None, ) -> User: """增加用户积分,并写入流水。 @@ -243,6 +254,13 @@ async def add_credits( return user user.credits = round(float(user.credits or 0) + amount, 2) + meta_kwargs = {} + if record_meta: + if isinstance(record_meta, CreditRecordMeta): + record_meta = await with_user_snapshot(db, record_meta, user_id) + meta_kwargs = record_meta.to_record_kwargs() + elif isinstance(record_meta, dict): + meta_kwargs = {k: v for k, v in record_meta.items() if v is not None} record = CreditRecord( id=generate_id(), user_id=user_id, @@ -253,6 +271,7 @@ async def add_credits( related_id=related_id, biz_key=biz_key, refund_for_biz_key=refund_for_biz_key, + **meta_kwargs, ) db.add(record) await db.flush() @@ -268,6 +287,7 @@ async def refund_credits( *, biz_key: str | None = None, refund_for_biz_key: str | None = None, + record_meta: CreditRecordMeta | dict | None = None, ) -> User: """生成失败积分回退。""" return await add_credits( @@ -279,6 +299,7 @@ async def refund_credits( record_type="refund", biz_key=biz_key, refund_for_biz_key=refund_for_biz_key, + record_meta=record_meta, ) diff --git a/video-gen-api/app/services/generation_billing_service.py b/video-gen-api/app/services/generation_billing_service.py index 51604d16..2a6a6417 100644 --- a/video-gen-api/app/services/generation_billing_service.py +++ b/video-gen-api/app/services/generation_billing_service.py @@ -4,23 +4,36 @@ import re from dataclasses import asdict, dataclass from typing import Any, Mapping -from sqlalchemy import func, select +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession +from app.enums.credit_record import CreditRecordBillingScene, CreditRecordChargeKind, CreditRecordOwnerType, CreditRecordSourceModule from app.models.credit_record import CreditRecord from app.models.generation_record import GenerationRecord +from app.models.module_generation_step import ModuleGenerationStep +from app.models.token_usage import TokenUsage from app.models.system_config import SystemConfig +from app.services.credit_record_meta_service import ( + CreditRecordMeta, + build_generation_media_meta, + build_generation_record_prompt_meta, + build_module_step_prompt_meta, + build_shot_video_analysis_meta, +) from app.services.credits import calc_image_credits, calc_text_credits, calc_video_credits, deduct_credits -CHARGE_TEXT_PROMPT = "text_prompt" -CHARGE_FILE_PARSE = "file_parse" -CHARGE_VISION_INPUT = "vision_input" -CHARGE_MEDIA = "media" +CHARGE_TEXT_PROMPT = CreditRecordChargeKind.TEXT_PROMPT.value +CHARGE_FILE_PARSE = CreditRecordChargeKind.FILE_PARSE.value +CHARGE_VISION_INPUT = CreditRecordChargeKind.VISION_INPUT.value +CHARGE_MEDIA = CreditRecordChargeKind.MEDIA.value +CHARGE_VIDEO_ANALYSIS = CreditRecordChargeKind.VIDEO_ANALYSIS.value -OWNER_GENERATION_RECORD = "generation_record" -OWNER_CHAT_GENERATION_TASK = "chat_generation_task" -OWNER_MODULE_GENERATION_STEP = "module_generation_step" +OWNER_GENERATION_RECORD = CreditRecordOwnerType.GENERATION_RECORD.value +OWNER_CHAT_GENERATION_TASK = CreditRecordOwnerType.CHAT_GENERATION_TASK.value +OWNER_MODULE_GENERATION_STEP = CreditRecordOwnerType.MODULE_GENERATION_STEP.value +OWNER_SHOT_REPLICATE_TASK_SET = CreditRecordOwnerType.SHOT_REPLICATE_TASK_SET.value +OWNER_SHOT_REPLICATE_SEGMENT = CreditRecordOwnerType.SHOT_REPLICATE_SEGMENT.value _BIZ_KEY_PATTERN = re.compile( r"^(?P[^:]+):(?P[^:]+):attempt:(?P\d+):(?P[^:]+):(?Pcharge|refund)$" @@ -169,10 +182,12 @@ async def deduct_credits_locked_once( charge_key: str, biz_key: str | None = None, attempt_no: int | None = None, + record_meta: CreditRecordMeta | dict | None = None, ) -> BillingItem: """按 biz_key 做幂等扣费。 charge_key 只保留为业务分类;正式幂等以 biz_key 为准。 + record_meta 负责把业务归属、模块、步骤、token、模型快照写入 CreditRecord。 """ amount = _round2(amount) if amount <= 0: @@ -197,6 +212,7 @@ async def deduct_credits_locked_once( description=description, related_id=related_id, biz_key=biz_key, + record_meta=record_meta, ) return BillingItem(charge_key=charge_key, amount=amount, charged=True, biz_key=biz_key, attempt_no=attempt_no) @@ -208,7 +224,7 @@ async def charge_chatapi_prompt_usage( usage: Mapping[str, Any], project_name: str | None = None, ) -> BillingSummary: - """提示词整理扣费仍按记录维度一次性幂等,不参与生成失败媒体退款。""" + """项目记录提示词整理扣费;不参与生成失败媒体退款。""" project_name = project_name or "AI生成任务" items: list[BillingItem] = [] @@ -219,12 +235,19 @@ async def charge_chatapi_prompt_usage( input_tokens = _safe_int(usage.get("input_tokens")) output_tokens = _safe_int(usage.get("output_tokens")) text_credits = await calc_text_credits(db, input_tokens, output_tokens) + text_meta = await build_generation_record_prompt_meta( + db, + record_id=record.id, + attempt_no=attempt_no, + charge_kind=CHARGE_TEXT_PROMPT, + usage=usage, + ) items.append( await deduct_credits_locked_once( db, user_id=record.user_id, amount=text_credits, - description=f"ChatAPI提示词整理", + description=f"提示词优化 - {project_name}", related_id=record.id, charge_key=CHARGE_TEXT_PROMPT, biz_key=build_credit_biz_key( @@ -235,17 +258,25 @@ async def charge_chatapi_prompt_usage( action="charge", ), attempt_no=attempt_no, + record_meta=text_meta, ) ) file_tokens = usage.get("file_parse_tokens") or usage.get("file_tokens") or usage.get("document_tokens") or 0 file_parse_credits = await _calc_optional_token_credits(db, _safe_int(file_tokens), "file_parse_credits_per_1000_tokens") + file_meta = await build_generation_record_prompt_meta( + db, + record_id=record.id, + attempt_no=attempt_no, + charge_kind=CHARGE_FILE_PARSE, + usage={**dict(usage), "total_tokens": _safe_int(file_tokens), "input_tokens": _safe_int(file_tokens), "output_tokens": 0}, + ) items.append( await deduct_credits_locked_once( db, user_id=record.user_id, amount=file_parse_credits, - description=f"文件解析Token", + description="文件解析Token", related_id=record.id, charge_key=CHARGE_FILE_PARSE, biz_key=build_credit_biz_key( @@ -256,17 +287,25 @@ async def charge_chatapi_prompt_usage( action="charge", ), attempt_no=attempt_no, + record_meta=file_meta, ) ) vision_tokens = usage.get("vision_input_tokens") or usage.get("image_input_tokens") or usage.get("image_tokens") or 0 vision_input_credits = await _calc_optional_token_credits(db, _safe_int(vision_tokens), "vision_input_credits_per_1000_tokens") + vision_meta = await build_generation_record_prompt_meta( + db, + record_id=record.id, + attempt_no=attempt_no, + charge_kind=CHARGE_VISION_INPUT, + usage={**dict(usage), "total_tokens": _safe_int(vision_tokens), "input_tokens": _safe_int(vision_tokens), "output_tokens": 0}, + ) items.append( await deduct_credits_locked_once( db, user_id=record.user_id, amount=vision_input_credits, - description=f"图片理解Token", + description="图片理解Token", related_id=record.id, charge_key=CHARGE_VISION_INPUT, biz_key=build_credit_biz_key( @@ -277,6 +316,7 @@ async def charge_chatapi_prompt_usage( action="charge", ), attempt_no=attempt_no, + record_meta=vision_meta, ) ) @@ -297,7 +337,7 @@ async def charge_module_prompt_usage( description: str, attempt_no: int = 1, ) -> BillingSummary: - """爆款开头复刻模块图片/视频 AI 提词扣文本积分。 + """爆款开头复刻/拆镜复刻模块图片/视频 AI 提词扣文本积分。 文本提词属于已经发生的 LLM 消费: - 调用成功后按 input_tokens + output_tokens 扣费。 @@ -314,6 +354,7 @@ async def charge_module_prompt_usage( charge_kind=CHARGE_TEXT_PROMPT, action="charge", ) + record_meta = await build_module_step_prompt_meta(db, step_id=step_id, attempt_no=attempt_no, usage=usage) item = await deduct_credits_locked_once( db, user_id=user_id, @@ -323,10 +364,92 @@ async def charge_module_prompt_usage( charge_key=CHARGE_TEXT_PROMPT, biz_key=biz_key, attempt_no=attempt_no, + record_meta=record_meta, ) + + result = await db.execute(select(ModuleGenerationStep).where(ModuleGenerationStep.id == step_id).limit(1)) + step = result.scalar_one_or_none() + if step: + step.token_usage_id = record_meta.token_usage_id + step.model_config_id = usage.get("model_config_id") + step.input_tokens = record_meta.input_tokens + step.output_tokens = record_meta.output_tokens + step.total_tokens = record_meta.total_tokens + step.text_credits_cost = text_credits + return BillingSummary(record_id=step_id, user_id=user_id, items=[item]) +async def charge_shot_video_analysis_usage( + db: AsyncSession, + *, + user_id: str, + owner_type: str, + owner_id: str, + usage: Mapping[str, Any], + description: str, + billing_scene: str, + source_project_id: str | None = None, + source_step_id: str | None = None, + attempt_no: int | None = None, +) -> BillingSummary: + """拆镜复刻视频分析扣分析积分。 + + 视频分析属于“文字提示词 + 视频素材”的模型调用类消费, + 按 input_tokens + output_tokens 参考文本积分规则计费, + 但账务归类为 analysis/video_analysis,避免混入提词优化统计。 + """ + attempt_no = attempt_no or await get_next_credit_attempt_no( + db, + owner_type=owner_type, + owner_id=owner_id, + charge_kind=CHARGE_VIDEO_ANALYSIS, + ) + input_tokens = _safe_int(usage.get("input_tokens")) + output_tokens = _safe_int(usage.get("output_tokens")) + amount = await calc_text_credits(db, input_tokens, output_tokens) + biz_key = build_credit_biz_key( + owner_type=owner_type, + owner_id=owner_id, + attempt_no=attempt_no, + charge_kind=CHARGE_VIDEO_ANALYSIS, + action="charge", + ) + record_meta = await build_shot_video_analysis_meta( + db, + owner_type=owner_type, + owner_id=owner_id, + attempt_no=attempt_no, + usage=usage, + billing_scene=billing_scene, + source_project_id=source_project_id, + source_step_id=source_step_id, + ) + item = await deduct_credits_locked_once( + db, + user_id=user_id, + amount=amount, + description=description, + related_id=owner_id, + charge_key=CHARGE_VIDEO_ANALYSIS, + biz_key=biz_key, + attempt_no=attempt_no, + record_meta=record_meta, + ) + + if record_meta.token_usage_id: + result = await db.execute(select(TokenUsage).where(TokenUsage.id == record_meta.token_usage_id).limit(1)) + token_usage = result.scalar_one_or_none() + if token_usage: + token_usage.owner_type = token_usage.owner_type or owner_type + token_usage.owner_id = token_usage.owner_id or owner_id + token_usage.biz_key = token_usage.biz_key or biz_key + token_usage.source_module = token_usage.source_module or CreditRecordSourceModule.SHOT_REPLICATE.value + token_usage.source_step_code = token_usage.source_step_code or "video_analysis" + + return BillingSummary(record_id=owner_id, user_id=user_id, items=[item]) + + async def charge_generation_media_by_params( db: AsyncSession, *, @@ -341,6 +464,11 @@ async def charge_generation_media_by_params( description_prefix: str = "AI创作-", owner_type: str = OWNER_CHAT_GENERATION_TASK, attempt_no: int | None = None, + source_module: str | None = None, + source_project_id: str | None = None, + source_step_id: str | None = None, + source_step_code: str | None = None, + billing_scene: str | None = None, ) -> BillingSummary: """图片/视频媒体生成扣费。 @@ -363,6 +491,19 @@ async def charge_generation_media_by_params( action="charge", ) items: list[BillingItem] = [] + record_meta = await build_generation_media_meta( + db, + owner_type=owner_type, + owner_id=record_id, + attempt_no=attempt_no, + gen_type=gen_type, + engine_id=engine_id, + source_module=source_module, + source_project_id=source_project_id, + source_step_id=source_step_id, + source_step_code=source_step_code, + billing_scene=billing_scene, + ) if gen_type == "image": size = image_size or "2K" @@ -377,6 +518,7 @@ async def charge_generation_media_by_params( charge_key=CHARGE_MEDIA, biz_key=biz_key, attempt_no=attempt_no, + record_meta=record_meta, ) ) elif gen_type == "video": @@ -391,6 +533,7 @@ async def charge_generation_media_by_params( charge_key=CHARGE_MEDIA, biz_key=biz_key, attempt_no=attempt_no, + record_meta=record_meta, ) ) else: @@ -419,4 +562,5 @@ async def charge_generation_media_for_record( description_prefix=description_prefix, owner_type=OWNER_GENERATION_RECORD, attempt_no=attempt_no, + source_module=CreditRecordSourceModule.GENERATION_RECORD.value, ) diff --git a/video-gen-api/app/services/generation_prompt_service.py b/video-gen-api/app/services/generation_prompt_service.py index 25c7408f..f7719919 100644 --- a/video-gen-api/app/services/generation_prompt_service.py +++ b/video-gen-api/app/services/generation_prompt_service.py @@ -152,10 +152,13 @@ async def build_prompt_with_chatapi(db: AsyncSession, record: ChatGenerationTask if not content: raise RuntimeError("ChatAPI未返回有效prompt") + token_usage_id = generate_id() db.add(TokenUsage( - id=generate_id(), + id=token_usage_id, model_config_id=config.id, user_id=record.user_id, + owner_type="generation_record", + owner_id=record.id, input_tokens=input_tokens, output_tokens=output_tokens, total_tokens=total_tokens, @@ -178,6 +181,11 @@ async def build_prompt_with_chatapi(db: AsyncSession, record: ChatGenerationTask total_tokens=total_tokens, ) return content, { + "token_usage_id": token_usage_id, + "model_config_id": config.id, + "model_config_name": config.name, + "model_provider": config.provider, + "model_name": config.model_name, "input_tokens": input_tokens, "output_tokens": output_tokens, "total_tokens": total_tokens, diff --git a/video-gen-api/app/services/generation_refund_service.py b/video-gen-api/app/services/generation_refund_service.py index dcb0752a..7c937776 100644 --- a/video-gen-api/app/services/generation_refund_service.py +++ b/video-gen-api/app/services/generation_refund_service.py @@ -10,6 +10,7 @@ from app.models.chat_generation_task import ChatGenerationTask from app.models.credit_record import CreditRecord from app.models.generation_record import GenerationRecord from app.services.credits import refund_credits +from app.services.credit_record_meta_service import build_refund_meta_from_charge from app.services.generation_billing_service import ( CHARGE_MEDIA, OWNER_CHAT_GENERATION_TASK, @@ -113,6 +114,7 @@ async def refund_unrefunded_media_charges( related_id=owner_id, biz_key=refund_biz_key, refund_for_biz_key=charge.biz_key, + record_meta=build_refund_meta_from_charge(charge, attempt_no=attempt_no), ) total_refunded = round(total_refunded + amount, 2) return total_refunded diff --git a/video-gen-api/app/services/generation_task_factory_service.py b/video-gen-api/app/services/generation_task_factory_service.py index 21381157..6e4bbfab 100644 --- a/video-gen-api/app/services/generation_task_factory_service.py +++ b/video-gen-api/app/services/generation_task_factory_service.py @@ -63,6 +63,11 @@ async def create_chat_generation_task_for_module( resolution: str | None = None, billing_project_name: str = "模块生成任务", billing_description_prefix: str = "模块生成-", + billing_source_module: str | None = None, + billing_source_project_id: str | None = None, + billing_source_step_id: str | None = None, + billing_source_step_code: str | None = None, + billing_scene: str | None = None, ) -> ChatGenerationTask: """创建可复用的 ChatGenerationTask 子任务。 @@ -106,6 +111,11 @@ async def create_chat_generation_task_for_module( description_prefix=billing_description_prefix, owner_type=OWNER_CHAT_GENERATION_TASK, attempt_no=1, + source_module=billing_source_module, + source_project_id=billing_source_project_id, + source_step_id=billing_source_step_id, + source_step_code=billing_source_step_code, + billing_scene=billing_scene, ) snapshot = _build_image_snapshot(engine, size, proportion, px) task = ChatGenerationTask( @@ -155,6 +165,11 @@ async def create_chat_generation_task_for_module( description_prefix=billing_description_prefix, owner_type=OWNER_CHAT_GENERATION_TASK, attempt_no=1, + source_module=billing_source_module, + source_project_id=billing_source_project_id, + source_step_id=billing_source_step_id, + source_step_code=billing_source_step_code, + billing_scene=billing_scene, ) snapshot = _build_video_snapshot(engine, ratio, selected_resolution, selected_duration) task = ChatGenerationTask( 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 70bd5ddd..2a76bb99 100644 --- a/video-gen-api/app/services/hot_opening_replicate_service.py +++ b/video-gen-api/app/services/hot_opening_replicate_service.py @@ -951,6 +951,24 @@ async def generate_image_from_prompt( {"type": "image", "url": material.get("material_image_url"), "name": "新产品图片"}, ] + step = await _create_step( + db, + project=project, + step_code=HotOpeningStepCodeEnum.IMAGE_GENERATE.value, + status=ModuleStepStatusEnum.PROCESSING.value, + parent_step_id=prompt_step.id, + source_step_id=prompt_step.id, + input_data={ + "engine_id": req.engine_id, + "params": { + "image_size": req.image_size, + "image_proportion": req.image_proportion, + "image_px": req.image_px, + }, + "prompt": optimized_prompt, + "media_references": refs, + }, + ) chat_task = await create_chat_generation_task_for_module( db, current_user=current_user, @@ -965,21 +983,30 @@ async def generate_image_from_prompt( image_px=req.image_px, billing_project_name=project.title or "爆款开头复刻", billing_description_prefix="爆款开头复刻图片生成", + billing_source_module=project.module, + billing_source_project_id=project.id, + billing_source_step_id=step.id, + billing_source_step_code=HotOpeningStepCodeEnum.IMAGE_GENERATE.value, ) - step = await _create_step( - db, - project=project, - step_code=HotOpeningStepCodeEnum.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, - }, + step.chat_task_id = chat_task.id + _force_set_json( + step, + "input_json", + _step_input( + step_code=HotOpeningStepCodeEnum.IMAGE_GENERATE.value, + source_step_id=prompt_step.id, + parent_step_id=prompt_step.id, + payload={ + "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 = HotOpeningStepCodeEnum.IMAGE_GENERATE.value @@ -1261,6 +1288,25 @@ async def generate_video_from_prompt( {"type": "image", "url": _build_file_url_or_data_uri(generated_image_url), "name": "新项目图片"}, ] + step = await _create_step( + db, + project=project, + step_code=HotOpeningStepCodeEnum.VIDEO_GENERATE.value, + status=ModuleStepStatusEnum.PROCESSING.value, + parent_step_id=prompt_step.id, + source_step_id=prompt_step.id, + input_data={ + "engine_id": req.engine_id or prompt_params.get("engine_id"), + "params": { + "duration": duration, + "aspect_ratio": aspect_ratio, + "resolution": resolution, + }, + "prompt_schema": prompt_schema, + "final_prompt": final_prompt, + "media_references": refs, + }, + ) chat_task = await create_chat_generation_task_for_module( db, current_user=current_user, @@ -1275,29 +1321,34 @@ async def generate_video_from_prompt( resolution=resolution, billing_project_name=project.title or "爆款开头复刻", billing_description_prefix="爆款开头复刻视频生成", + billing_source_module=project.module, + billing_source_project_id=project.id, + billing_source_step_id=step.id, + billing_source_step_code=HotOpeningStepCodeEnum.VIDEO_GENERATE.value, ) - step = await _create_step( - db, - project=project, - step_code=HotOpeningStepCodeEnum.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, + step.chat_task_id = chat_task.id + _force_set_json( + step, + "input_json", + _step_input( + step_code=HotOpeningStepCodeEnum.VIDEO_GENERATE.value, + source_step_id=prompt_step.id, + parent_step_id=prompt_step.id, + payload={ + "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, }, - "prompt_schema": prompt_schema, - "final_prompt": final_prompt, - "media_references": refs, - }, + ), ) project.status = ModuleProjectStatusEnum.PROCESSING.value project.current_step_code = HotOpeningStepCodeEnum.VIDEO_GENERATE.value 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 a6077153..1a384a41 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 @@ -1488,9 +1488,10 @@ async def optimize_hot_opening_video_prompt( "total_tokens": int(usage.get("total_tokens") or 0), "log_user_message": log_user_message, } + token_usage_id = generate_id() db.add( TokenUsage( - id=generate_id(), + id=token_usage_id, model_config_id=config.id, user_id=user_id, input_tokens=token_usage["input_tokens"], @@ -1499,6 +1500,13 @@ async def optimize_hot_opening_video_prompt( ) ) await db.flush() + token_usage.update({ + "token_usage_id": token_usage_id, + "model_config_id": config.id, + "model_config_name": config.name, + "model_provider": config.provider, + "model_name": config.model_name, + }) result = parse_model_json(content) result = normalize_video_prompt_schema_from_ai(result, video_config, schema_config_snapshot) diff --git a/video-gen-api/app/services/llm.py b/video-gen-api/app/services/llm.py index fa70a529..8f1447bb 100644 --- a/video-gen-api/app/services/llm.py +++ b/video-gen-api/app/services/llm.py @@ -370,9 +370,11 @@ async def _call_openai_compatible( output_tokens = usage.get("completion_tokens", 0) total_tokens = usage.get("total_tokens", input_tokens + output_tokens) + token_usage_id = None if db is not None: + token_usage_id = generate_id() record = TokenUsage( - id=generate_id(), + id=token_usage_id, model_config_id=config.id, user_id=user_id, input_tokens=input_tokens, @@ -384,6 +386,11 @@ async def _call_openai_compatible( content = data["choices"][0]["message"]["content"].strip() token_usage = { + "token_usage_id": token_usage_id, + "model_config_id": config.id, + "model_config_name": config.name, + "model_provider": config.provider, + "model_name": config.model_name, "input_tokens": input_tokens, "output_tokens": output_tokens, "total_tokens": total_tokens, diff --git a/video-gen-api/app/services/payment.py b/video-gen-api/app/services/payment.py index b6c61ab3..bc269881 100644 --- a/video-gen-api/app/services/payment.py +++ b/video-gen-api/app/services/payment.py @@ -17,9 +17,45 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.models.payment_order import PaymentOrder from app.models.system_config import SystemConfig +from app.enums.credit_record import ( + CreditRecordAction, + CreditRecordBillingScene, + CreditRecordChargeKind, + CreditRecordOwnerType, + CreditRecordSourceModule, + CreditRecordSubject, +) +from app.services.credit_record_meta_service import CreditRecordMeta, build_recharge_meta from app.services.credits import add_credits, deduct_credits from app.utils.id_gen import generate_id, generate_order_no + + +def _payment_biz_key(order: PaymentOrder, *, charge_kind: str, action: str) -> str: + return ( + f"{CreditRecordOwnerType.PAYMENT_ORDER.value}:{order.id}:" + f"attempt:1:{charge_kind}:{action}" + ) + + +def _payment_recharge_meta(order: PaymentOrder) -> CreditRecordMeta: + meta = build_recharge_meta(owner_id=order.id) + meta.attempt_no = 1 + return meta + + +def _payment_refund_meta(order: PaymentOrder) -> CreditRecordMeta: + return CreditRecordMeta( + owner_type=CreditRecordOwnerType.PAYMENT_ORDER.value, + owner_id=order.id, + attempt_no=1, + charge_kind=CreditRecordChargeKind.REFUND.value, + charge_action=CreditRecordAction.REFUND.value, + credit_subject=CreditRecordSubject.REFUND.value, + billing_scene=CreditRecordBillingScene.REFUND.value, + source_module=CreditRecordSourceModule.PAYMENT.value, + ) + # --------------------------------------------------------------------------- # Payment logger → log/payment/YYYY-MM-DD.log (one file per day, no cleanup) # --------------------------------------------------------------------------- @@ -321,6 +357,12 @@ async def create_recharge_order( total_credits, desc, related_id=order.id, + biz_key=_payment_biz_key( + order, + charge_kind=CreditRecordChargeKind.RECHARGE.value, + action=CreditRecordAction.CHARGE.value, + ), + record_meta=_payment_recharge_meta(order), ) await db.flush() else: @@ -1094,6 +1136,12 @@ async def process_payment_success(db: AsyncSession, order_id: str): order.credits, f"充值成功({order.credits}积分)", related_id=order.id, + biz_key=_payment_biz_key( + order, + charge_kind=CreditRecordChargeKind.RECHARGE.value, + action=CreditRecordAction.CHARGE.value, + ), + record_meta=_payment_recharge_meta(order), ) await db.commit() @@ -1152,6 +1200,12 @@ async def process_payment_success_by_order_no( order.credits, f"充值成功({order.credits}积分), 订单号: {order_no}, 金额: {order.amount}", related_id=order.id, + biz_key=_payment_biz_key( + order, + charge_kind=CreditRecordChargeKind.RECHARGE.value, + action=CreditRecordAction.CHARGE.value, + ), + record_meta=_payment_recharge_meta(order), ) await db.commit() logger.info( @@ -1220,6 +1274,17 @@ async def process_refund( order.credits, refund_reason, related_id=order.id, + biz_key=_payment_biz_key( + order, + charge_kind=CreditRecordChargeKind.REFUND.value, + action=CreditRecordAction.REFUND.value, + ), + refund_for_biz_key=_payment_biz_key( + order, + charge_kind=CreditRecordChargeKind.RECHARGE.value, + action=CreditRecordAction.CHARGE.value, + ), + record_meta=_payment_refund_meta(order), ) except Exception as e: logger.exception(f"Failed to deduct credits for refund: {e}") diff --git a/video-gen-api/app/services/shot_replicate_flow_service.py b/video-gen-api/app/services/shot_replicate_flow_service.py index f0a2350a..3e0d0d68 100644 --- a/video-gen-api/app/services/shot_replicate_flow_service.py +++ b/video-gen-api/app/services/shot_replicate_flow_service.py @@ -903,6 +903,24 @@ async def generate_image_from_prompt( {"type": "image", "url": material.get("material_image_url"), "name": "新产品图片"}, ] + 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, + input_data={ + "engine_id": req.engine_id, + "params": { + "image_size": req.image_size, + "image_proportion": req.image_proportion, + "image_px": req.image_px, + }, + "prompt": optimized_prompt, + "media_references": refs, + }, + ) chat_task = await create_chat_generation_task_for_module( db, current_user=current_user, @@ -917,21 +935,30 @@ async def generate_image_from_prompt( image_px=req.image_px, billing_project_name=project.title or "拆镜复刻", billing_description_prefix="拆镜复刻图片生成", + billing_source_module=project.module, + billing_source_project_id=project.id, + billing_source_step_id=step.id, + billing_source_step_code=ShotReplicateStepCodeEnum.IMAGE_GENERATE.value, ) - 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, - }, + step.chat_task_id = chat_task.id + _force_set_json( + step, + "input_json", + _step_input( + step_code=ShotReplicateStepCodeEnum.IMAGE_GENERATE.value, + source_step_id=prompt_step.id, + parent_step_id=prompt_step.id, + payload={ + "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 @@ -1223,6 +1250,25 @@ async def generate_video_from_prompt( {"type": "image", "url": _build_file_url_or_data_uri(generated_image_url), "name": "新项目图片"}, ] + 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, + input_data={ + "engine_id": req.engine_id or prompt_params.get("engine_id"), + "params": { + "duration": duration, + "aspect_ratio": aspect_ratio, + "resolution": resolution, + }, + "prompt_schema": prompt_schema, + "final_prompt": final_prompt, + "media_references": refs, + }, + ) chat_task = await create_chat_generation_task_for_module( db, current_user=current_user, @@ -1237,29 +1283,34 @@ async def generate_video_from_prompt( resolution=resolution, billing_project_name=project.title or "拆镜复刻", billing_description_prefix="拆镜复刻视频生成", + billing_source_module=project.module, + billing_source_project_id=project.id, + billing_source_step_id=step.id, + billing_source_step_code=ShotReplicateStepCodeEnum.VIDEO_GENERATE.value, ) - 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, + step.chat_task_id = chat_task.id + _force_set_json( + step, + "input_json", + _step_input( + step_code=ShotReplicateStepCodeEnum.VIDEO_GENERATE.value, + source_step_id=prompt_step.id, + parent_step_id=prompt_step.id, + payload={ + "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, }, - "prompt_schema": prompt_schema, - "final_prompt": final_prompt, - "media_references": refs, - }, + ), ) project.status = ModuleProjectStatusEnum.PROCESSING.value project.current_step_code = ShotReplicateStepCodeEnum.VIDEO_GENERATE.value diff --git a/video-gen-api/app/services/shot_video_analysis_service.py b/video-gen-api/app/services/shot_video_analysis_service.py index 2da22640..643a5c2e 100644 --- a/video-gen-api/app/services/shot_video_analysis_service.py +++ b/video-gen-api/app/services/shot_video_analysis_service.py @@ -505,9 +505,10 @@ async def analyze_video_for_shot_split( if not token_usage["total_tokens"]: token_usage["total_tokens"] = token_usage["input_tokens"] + token_usage["output_tokens"] + token_usage_id = generate_id() db.add( TokenUsage( - id=generate_id(), + id=token_usage_id, model_config_id=config.id, user_id=user_id, input_tokens=token_usage["input_tokens"], @@ -516,6 +517,13 @@ async def analyze_video_for_shot_split( ) ) await db.flush() + token_usage.update({ + "token_usage_id": token_usage_id, + "model_config_id": config.id, + "model_config_name": config.name, + "model_provider": config.provider, + "model_name": config.model_name, + }) return ShotVideoAnalysisResult(result=result, raw_response=raw, usage=token_usage) diff --git a/video-gen-api/app/tasks/shot_replicate_tasks.py b/video-gen-api/app/tasks/shot_replicate_tasks.py index 2bf43753..8c5a5c73 100644 --- a/video-gen-api/app/tasks/shot_replicate_tasks.py +++ b/video-gen-api/app/tasks/shot_replicate_tasks.py @@ -7,6 +7,7 @@ from typing import Any from sqlalchemy import select from app.config import settings +from app.enums.credit_record import CreditRecordBillingScene, CreditRecordOwnerType from app.enums.shot_replicate import ( ModuleCodeEnum, ShotAnalysisStatusEnum, @@ -36,6 +37,7 @@ from app.services.module_async_recovery_service import ( ) 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.generation_billing_service import charge_shot_video_analysis_usage 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 @@ -141,6 +143,16 @@ async def _run_analyze_original_video(task_set_id: str) -> None: task_set.analysis_status = ShotAnalysisStatusEnum.COMPLETED.value task_set.status = ShotTaskSetStatusEnum.ANALYSIS_COMPLETED.value task_set.analysis_error_message = None + await charge_shot_video_analysis_usage( + db, + user_id=task_set.user_id, + owner_type=CreditRecordOwnerType.SHOT_REPLICATE_TASK_SET.value, + owner_id=task_set.id, + usage=analyzed.usage, + description="拆镜复刻-原视频分析", + billing_scene=CreditRecordBillingScene.SHOT_ORIGINAL_VIDEO_ANALYSIS.value, + source_project_id=task_set.id, + ) await db.commit() await cleanup_active_if_terminal(db, object_type=OBJECT_SHOT_TASK_SET_ANALYSIS, object_id=task_set_id) @@ -260,6 +272,17 @@ async def _run_analyze_custom_segment_video(segment_id: str) -> None: segment.analysis_json = result_json segment.analysis_status = ShotSegmentAnalysisStatusEnum.COMPLETED.value segment.analysis_error_message = None + await charge_shot_video_analysis_usage( + db, + user_id=segment.user_id, + owner_type=CreditRecordOwnerType.SHOT_REPLICATE_SEGMENT.value, + owner_id=segment.id, + usage=analyzed.usage, + description="拆镜复刻-片段视频分析", + billing_scene=CreditRecordBillingScene.SHOT_SEGMENT_VIDEO_ANALYSIS.value, + source_project_id=segment.task_set_id, + source_step_id=segment.id, + ) await db.commit() await cleanup_active_if_terminal(db, object_type=OBJECT_SHOT_SEGMENT_ANALYSIS, object_id=segment_id) diff --git a/video-gen-app/dist/assets/index-CeDXrP7R.js b/video-gen-app/dist/assets/index-CeDXrP7R.js new file mode 100644 index 00000000..a7540881 --- /dev/null +++ b/video-gen-app/dist/assets/index-CeDXrP7R.js @@ -0,0 +1,442 @@ +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)),l=(e=>typeof require<`u`?require:typeof Proxy<`u`?new Proxy(e,{get:(e,t)=>(typeof require<`u`?require:e)[t]}):e)(function(e){if(typeof require<`u`)return require.apply(this,arguments);throw Error('Calling `require` for "'+e+"\" in an environment that doesn't expose the `require` function. See https://rolldown.rs/in-depth/bundling-cjs#require-external-modules for more details.")});(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 u=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}}}})),d=o(((e,t)=>{t.exports=u()})),f=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=f()})),m=o((e=>{var t=p();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=m()})),g=o((e=>{var t=d(),n=p(),r=h();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),G=V(null);function ne(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)?Ud(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Ud(t),e=Wd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}H(W),U(W,e)}function K(){H(W),H(ee),H(te)}function q(e){e.memoizedState!==null&&U(G,e);var t=W.current,n=Wd(t,e.type);t!==n&&(U(ee,e),U(W,n))}function J(e){ee.current===e&&(H(W),H(ee)),G.current===e&&(H(G),ep._currentValue=R)}var re,Y;function ie(e){if(re===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);re=t&&t[1]||``,Y=-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{ae=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?ie(n):``}function se(e,t){switch(e.tag){case 26:case 27:case 5:return ie(e.type);case 16:return ie(`Lazy`);case 13:return e.child!==t&&t!==null?ie(`Suspense Fallback`):ie(`Suspense`);case 19:return ie(`SuspenseList`);case 0:case 15:return oe(e.type,!1);case 11:return oe(e.type.render,!1);case 1:return oe(e.type,!0);case 31:return ie(`Activity`);default:return``}}function ce(e){try{var t=``,n=null;do t+=se(e,n),n=e,e=e.return;while(e);return t}catch(e){return` +Error generating stack: `+e.message+` +`+e.stack}}var le=Object.prototype.hasOwnProperty,ue=t.unstable_scheduleCallback,de=t.unstable_cancelCallback,fe=t.unstable_shouldYield,pe=t.unstable_requestPaint,me=t.unstable_now,he=t.unstable_getCurrentPriorityLevel,ge=t.unstable_ImmediatePriority,_e=t.unstable_UserBlockingPriority,ve=t.unstable_NormalPriority,ye=t.unstable_LowPriority,be=t.unstable_IdlePriority,xe=t.log,Se=t.unstable_setDisableYieldValue,Ce=null,we=null;function Te(e){if(typeof xe==`function`&&Se(e),we&&typeof we.setStrictMode==`function`)try{we.setStrictMode(Ce,e)}catch{}}var Ee=Math.clz32?Math.clz32:ke,De=Math.log,Oe=Math.LN2;function ke(e){return e>>>=0,e===0?32:31-(De(e)/Oe|0)|0}var Ae=256,je=262144,Me=4194304;function Ne(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 Pe(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=Ne(n))):i=Ne(o):i=Ne(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=Ne(n))):i=Ne(o)):i=Ne(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 Fe(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Ie(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 Le(){var e=Me;return Me<<=1,!(Me&62914560)&&(Me=4194304),e}function Re(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function ze(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Be(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),Qt=!1;if(Zt)try{var $t={};Object.defineProperty($t,`passive`,{get:function(){Qt=!0}}),window.addEventListener(`test`,$t,$t),window.removeEventListener(`test`,$t,$t)}catch{Qt=!1}var en=null,tn=null,nn=null;function rn(){if(nn)return nn;var e,t=tn,n=t.length,r,i=`value`in en?en.value:en.textContent,a=i.length;for(e=0;e=In),zn=` `,Bn=!1;function Vn(e,t){switch(e){case`keyup`:return Pn.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Hn(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var Un=!1;function Wn(e,t){switch(e){case`compositionend`:return Hn(t);case`keypress`:return t.which===32?(Bn=!0,zn):null;case`textInput`:return e=t.data,e===zn&&Bn?null:e;default:return null}}function Gn(e,t){if(Un)return e===`compositionend`||!Fn&&Vn(e,t)?(e=rn(),nn=tn=en=null,Un=!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=pr(n)}}function hr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?hr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function gr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Tt(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=Tt(e.document)}return t}function _r(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 vr=Zt&&`documentMode`in document&&11>=document.documentMode,yr=null,br=null,xr=null,Sr=!1;function Cr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Sr||yr==null||yr!==Tt(r)||(r=yr,`selectionStart`in r&&_r(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}),xr&&fr(xr,r)||(xr=r,r=Dd(br,`onSelect`),0>=o,i-=o,hi=1<<32-Ee(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),wi&&_i(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),wi&&_i(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 wi&&_i(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)}),wi&&_i(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&&_a(l)===r.type){n(e,r.sibling),c=a(r,o.props),wa(c,o),c.return=e,e=c;break a}n(e,r);break}else t(e,r);r=r.sibling}o.type===y?(c=ni(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=ti(o.type,o.key,o.props,null,e.mode,c),wa(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=ai(o,e.mode,c),c.return=e,e=c}return s(e);case O:return o=_a(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,Ca(o),c);if(o.$$typeof===C)return b(e,r,Ki(e,o),c);Ta(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=ri(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{Sa=0;var i=b(e,t,n,r);return xa=null,i}catch(t){if(t===da||t===pa)throw t;var a=Zr(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var Da=Ea(!0),Oa=Ea(!1),ka=!1;function Aa(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function ja(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 Ma(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Na(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,Nl&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=Jr(e),qr(e,null,n),t}return Wr(e,r,t,n),Jr(e)}function Pa(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,He(e,n)}}function Fa(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 Ia=!1;function La(){if(Ia){var e=ra;if(e!==null)throw e}}function Ra(e,t,n,r){Ia=!1;var i=e.updateQueue;ka=!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?(Il&f)===f:(r&f)===f){f!==0&&f===na&&(Ia=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var h=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(h=g.payload,typeof h==`function`){d=h.call(_,d,f);break a}d=h;break a;case 3:h.flags=h.flags&-65537|128;case 0:if(h=g.payload,f=typeof h==`function`?h.call(_,d,f):h,f==null)break a;d=m({},d,f);break a;case 2:ka=!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),Wl|=o,e.lanes=o,e.memoizedState=d}}function za(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function Z(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,Es(e,!1,t,n);try{var c=i(),l=I.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Ts(e,t,aa(c,r),fu(e)):Ts(e,t,r,fu(e))}catch(n){Ts(e,t,{then:function(){},status:`rejected`,reason:n},fu())}finally{L.p=a,o!==null&&s.types!==null&&(o.types=s.types),I.T=o}}function hs(){}function gs(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=_s(e).queue;ms(e,a,t,R,n===null?hs:function(){return vs(e),n(r)})}function _s(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:Do,lastRenderedState:R},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Do,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function vs(e){var t=_s(e);t.next===null&&(t=e.alternate.memoizedState),Ts(e,t.next.queue,{},fu())}function ys(){return Gi(ep)}function bs(){return So().memoizedState}function xs(){return So().memoizedState}function Ss(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=fu();e=Ma(n);var r=Na(t,e,n);r!==null&&(mu(r,t,n),Pa(r,t,n)),t={cache:Qi()},e.payload=t;return}t=t.return}}function Cs(e,t,n){var r=fu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Ds(e)?Os(t,n):(n=Gr(e,t,n,r),n!==null&&(mu(n,e,r),ks(n,t,r)))}function ws(e,t,n){Ts(e,t,n,fu())}function Ts(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Ds(e))Os(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,dr(s,o))return Wr(e,t,i,0),Pl===null&&Ur(),!1}catch{}if(n=Gr(e,t,i,r),n!==null)return mu(n,e,r),ks(n,t,r),!0}return!1}function Es(e,t,n,r){if(r={lane:2,revertLane:dd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Ds(e)){if(t)throw Error(i(479))}else t=Gr(e,n,r,2),t!==null&&mu(t,e,2)}function Ds(e){var t=e.alternate;return e===to||t!==null&&t===to}function Os(e,t){ao=io=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function ks(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,He(e,n)}}var As={readContext:Gi,use:To,useCallback:fo,useContext:fo,useEffect:fo,useImperativeHandle:fo,useLayoutEffect:fo,useInsertionEffect:fo,useMemo:fo,useReducer:fo,useRef:fo,useState:fo,useDebugValue:fo,useDeferredValue:fo,useTransition:fo,useSyncExternalStore:fo,useId:fo,useHostTransitionStatus:fo,useFormState:fo,useActionState:fo,useOptimistic:fo,useMemoCache:fo,useCacheRefresh:fo};As.useEffectEvent=fo;var js={readContext:Gi,use:To,useCallback:function(e,t){return xo().memoizedState=[e,t===void 0?null:t],e},useContext:Gi,useEffect:ts,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),$o(4194308,4,ss.bind(null,t,e),n)},useLayoutEffect:function(e,t){return $o(4194308,4,e,t)},useInsertionEffect:function(e,t){$o(4,2,e,t)},useMemo:function(e,t){var n=xo();t=t===void 0?null:t;var r=e();if(oo){Te(!0);try{e()}finally{Te(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=xo();if(n!==void 0){var i=n(t);if(oo){Te(!0);try{n(t)}finally{Te(!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=Cs.bind(null,to,e),[r.memoizedState,e]},useRef:function(e){var t=xo();return e={current:e},t.memoizedState=e},useState:function(e){e=Lo(e);var t=e.queue,n=ws.bind(null,to,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:ls,useDeferredValue:function(e,t){return fs(xo(),e,t)},useTransition:function(){var e=Lo(!1);return e=ms.bind(null,to,e.queue,!0,!1),xo().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=to,a=xo();if(wi){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),Pl===null)throw Error(i(349));Il&127||Mo(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,ts(Po.bind(null,r,o,e),[e]),r.flags|=2048,Zo(9,{destroy:void 0},No.bind(null,r,o,n,t),null),n},useId:function(){var e=xo(),t=Pl.identifierPrefix;if(wi){var n=gi,r=hi;n=(r&~(1<<32-Ee(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=so++,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[Ye]=t,o[Xe]=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(Id(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&&Tc(t)}}return Ac(t),Ec(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Tc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=te.current,ji(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=Si,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[Ye]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Nd(e.nodeValue,n)),e||Oi(t,!0)}else e=Hd(e).createTextNode(r),e[Ye]=t,t.stateNode=e}return Ac(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=ji(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[Ye]=t}else Mi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Ac(t),e=!1}else n=Ni(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(Za(t),t):(Za(t),null);if(t.flags&128)throw Error(i(558))}return Ac(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=ji(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[Ye]=t}else Mi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Ac(t),a=!1}else a=Ni(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(Za(t),t):(Za(t),null)}return Za(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),Oc(t,t.updateQueue),Ac(t),null);case 4:return K(),e===null&&Cd(t.stateNode.containerInfo),Ac(t),null;case 10:return zi(t.type),Ac(t),null;case 19:if(H(Qa),r=t.memoizedState,r===null)return Ac(t),null;if(a=(t.flags&128)!=0,o=r.rendering,o===null)if(a)kc(r,!1);else{if(Ul!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=$a(e),o!==null){for(t.flags|=128,kc(r,!1),e=o.updateQueue,t.updateQueue=e,Oc(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)ei(n,e),n=n.sibling;return U(Qa,Qa.current&1|2),wi&&_i(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&me()>eu&&(t.flags|=128,a=!0,kc(r,!1),t.lanes=4194304)}else{if(!a)if(e=$a(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Oc(t,e),kc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!wi)return Ac(t),null}else 2*me()-r.renderingStartTime>eu&&n!==536870912&&(t.flags|=128,a=!0,kc(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?(Ac(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=me(),e.sibling=null,n=Qa.current,U(Qa,a?n&1|2:n&1),wi&&_i(t,r.treeForkCount),e);case 22:case 23:return Za(t),Wa(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(Ac(t),t.subtreeFlags&6&&(t.flags|=8192)):Ac(t),n=t.updateQueue,n!==null&&Oc(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(sa),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),zi(Zi),Ac(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function Mc(e,t){switch(bi(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return zi(Zi),K(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return J(t),null;case 31:if(t.memoizedState!==null){if(Za(t),t.alternate===null)throw Error(i(340));Mi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(Za(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Mi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return H(Qa),null;case 4:return K(),null;case 10:return zi(t.type),null;case 22:case 23:return Za(t),Wa(),e!==null&&H(sa),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return zi(Zi),null;case 25:return null;default:return null}}function Nc(e,t){switch(bi(t),t.tag){case 3:zi(Zi),K();break;case 26:case 27:case 5:J(t);break;case 4:K();break;case 31:t.memoizedState!==null&&Za(t);break;case 13:Za(t);break;case 19:H(Qa);break;case 10:zi(t.type);break;case 22:case 23:Za(t),Wa(),e!==null&&H(sa);break;case 24:zi(Zi)}}function Pc(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){Wu(t,t.return,e)}}function Fc(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){Wu(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Wu(t,t.return,e)}}function Ic(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{Z(t,n)}catch(t){Wu(e,e.return,t)}}}function Lc(e,t,n){n.props=Rs(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Wu(e,t,n)}}function Rc(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){Wu(e,t,n)}}function zc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r==`function`)try{r()}catch(n){Wu(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){Wu(e,t,n)}else n.current=null}function Bc(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){Wu(e,e.return,t)}}function Vc(e,t,n){try{var r=e.stateNode;Ld(r,e.type,n,t),r[Xe]=t}catch(t){Wu(e,e.return,t)}}function Hc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&$d(e.type)||e.tag===4}function Uc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Hc(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&&$d(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 Wc(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=Ht));else if(r!==4&&(r===27&&$d(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Wc(e,t,n),e=e.sibling;e!==null;)Wc(e,t,n),e=e.sibling}function Gc(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&&$d(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(Gc(e,t,n),e=e.sibling;e!==null;)Gc(e,t,n),e=e.sibling}function Kc(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Id(t,r,n),t[Ye]=e,t[Xe]=n}catch(t){Wu(e,e.return,t)}}var qc=!1,Jc=!1,Yc=!1,Xc=typeof WeakSet==`function`?WeakSet:Set,Zc=null;function Qc(e,t){if(e=e.containerInfo,Bd=lp,e=gr(e),_r(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(Vd={focusedElem:e,selectionRange:n},lp=!1,Zc=t;Zc!==null;)if(t=Zc,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,Zc=e;else for(;Zc!==null;){switch(t=Zc,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`))),Id(o,r,n),o[Ye]=e,ct(o),r=o;break a;case`link`:var s=Uf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=mr(s,h),v=mr(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=cu,cu=null;var o=iu,s=ou;if(ru=0,au=iu=null,ou=0,Nl&6)throw Error(i(331));var c=Nl;if(Nl|=4,Ol(o.current),bl(o,o.current,s,n),Nl=c,id(0,!1),we&&typeof we.onPostCommitFiberRoot==`function`)try{we.onPostCommitFiberRoot(Ce,o)}catch{}return!0}finally{L.p=a,I.T=r,Bu(e,t)}}function Uu(e,t,n){t=si(n,t),t=Ws(e.stateNode,t,2),e=Na(e,t,2),e!==null&&(ze(e,2),rd(e))}function Wu(e,t,n){if(e.tag===3)Uu(e,e,n);else for(;t!==null;){if(t.tag===3){Uu(t,e,n);break}else if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(nu===null||!nu.has(r))){e=si(n,e),n=Gs(2),r=Na(t,n,2),r!==null&&(Ks(n,r,t,e),ze(r,2),rd(r));break}}t=t.return}}function Gu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Ml;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)||(Vl=!0,i.add(n),e=Ku.bind(null,e,t,n),t.then(e,e))}function Ku(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,Pl===e&&(Il&n)===n&&(Ul===4||Ul===3&&(Il&62914560)===Il&&300>me()-Ql?!(Nl&2)&&xu(e,0):Kl|=n,Jl===Il&&(Jl=0)),rd(e)}function qu(e,t){t===0&&(t=Le()),e=Kr(e,t),e!==null&&(ze(e,t),rd(e))}function Ju(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),qu(e,n)}function Yu(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),qu(e,n)}function Xu(e,t){return ue(e,t)}var Zu=null,Qu=null,$u=!1,ed=!1,td=!1,nd=0;function rd(e){e!==Qu&&e.next===null&&(Qu===null?Zu=Qu=e:Qu=Qu.next=e),ed=!0,$u||($u=!0,ud())}function id(e,t){if(!td&&ed){td=!0;do for(var n=!1,r=Zu;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-Ee(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,ld(r,a))}else a=Il,a=Pe(r,r===Pl?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||Fe(r,a)||(n=!0,ld(r,a));r=r.next}while(n);td=!1}}function ad(){od()}function od(){ed=$u=!1;var e=0;nd!==0&&qd()&&(e=nd);for(var t=me(),n=null,r=Zu;r!==null;){var i=r.next,a=sd(r,t);a===0?(r.next=null,n===null?Zu=i:n.next=i,i===null&&(Qu=n)):(n=r,(e!==0||a&3)&&(ed=!0)),r=i}ru!==0&&ru!==5||id(e,!1),nd!==0&&(nd=0)}function sd(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&&Rd(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function Cf(e,t,n){var r=Sf;if(r&&typeof t==`string`&&t){var i=Dt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),_f.has(i)||(_f.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Id(t,`link`,e),ct(t),r.head.appendChild(t)))}}function wf(e){yf.D(e),Cf(`dns-prefetch`,e,null)}function Tf(e,t){yf.C(e,t),Cf(`preconnect`,e,t)}function Ef(e,t,n){yf.L(e,t,n);var r=Sf;if(r&&e&&t){var i=`link[rel="preload"][as="`+Dt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Dt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Dt(n.imageSizes)+`"]`)):i+=`[href="`+Dt(e)+`"]`;var a=i;switch(t){case`style`:a=Mf(e);break;case`script`:a=If(e)}gf.has(a)||(e=m({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),gf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(Nf(a))||t===`script`&&r.querySelector(Lf(a))||(t=r.createElement(`link`),Id(t,`link`,e),ct(t),r.head.appendChild(t)))}}function Df(e,t){yf.m(e,t);var n=Sf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Dt(r)+`"][href="`+Dt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=If(e)}if(!gf.has(a)&&(e=m({rel:`modulepreload`,href:e},t),gf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Lf(a)))return}r=n.createElement(`link`),Id(r,`link`,e),ct(r),n.head.appendChild(r)}}}function Of(e,t,n){yf.S(e,t,n);var r=Sf;if(r&&e){var i=st(r).hoistableStyles,a=Mf(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(Nf(a)))s.loading=5;else{e=m({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=gf.get(a))&&Bf(e,n);var c=o=r.createElement(`link`);ct(c),Id(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,zf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function kf(e,t){yf.X(e,t);var n=Sf;if(n&&e){var r=st(n).hoistableScripts,i=If(e),a=r.get(i);a||(a=n.querySelector(Lf(i)),a||(e=m({src:e,async:!0},t),(t=gf.get(i))&&Vf(e,t),a=n.createElement(`script`),ct(a),Id(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Af(e,t){yf.M(e,t);var n=Sf;if(n&&e){var r=st(n).hoistableScripts,i=If(e),a=r.get(i);a||(a=n.querySelector(Lf(i)),a||(e=m({src:e,async:!0,type:`module`},t),(t=gf.get(i))&&Vf(e,t),a=n.createElement(`script`),ct(a),Id(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function jf(e,t,n,r){var a=(a=te.current)?vf(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=Mf(n.href),n=st(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=Mf(n.href);var o=st(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(Nf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),gf.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},gf.set(e,n),o||Ff(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=If(n),n=st(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 Mf(e){return`href="`+Dt(e)+`"`}function Nf(e){return`link[rel="stylesheet"][`+e+`]`}function Pf(e){return m({},e,{"data-precedence":e.precedence,precedence:null})}function Ff(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}),Id(t,`link`,n),ct(t),e.head.appendChild(t))}function If(e){return`[src="`+Dt(e)+`"]`}function Lf(e){return`script[async]`+e}function Rf(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Dt(n.href)+`"]`);if(r)return t.instance=r,ct(r),r;var a=m({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),ct(r),Id(r,`style`,a),zf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=Mf(n.href);var o=e.querySelector(Nf(a));if(o)return t.state.loading|=4,t.instance=o,ct(o),o;r=Pf(n),(a=gf.get(a))&&Bf(r,a),o=(e.ownerDocument||e).createElement(`link`),ct(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Id(o,`link`,r),t.state.loading|=4,zf(o,n.precedence,e),t.instance=o;case`script`:return o=If(n.src),(a=e.querySelector(Lf(o)))?(t.instance=a,ct(a),a):(r=n,(a=gf.get(o))&&(r=m({},n),Vf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),ct(a),Id(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,zf(r,n.precedence,e));return t.instance}function zf(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 Gf(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 Kf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function qf(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=Mf(r.href),a=t.querySelector(Nf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Xf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,ct(a);return}a=t.ownerDocument||t,r=Pf(r),(i=gf.get(i))&&Bf(r,i),a=a.createElement(`link`),ct(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Id(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=Xf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Jf=0;function Yf(e,t){return e.stylesheets&&e.count===0&&Qf(e,e.stylesheets),0Jf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Xf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Qf(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Zf=null;function Qf(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Zf=new Map,t.forEach($f,e),Zf=null,Xf.call(e))}function $f(e,t){if(!(t.state.loading&4)){var n=Zf.get(e);if(n)var r=n.get(null);else{n=new Map,Zf.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=g()}))(),v=`modulepreload`,y=function(e){return`/`+e},b={},x=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=y(t,n),t in b)return;b[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`:v,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)})},S=c(p(),1),C=`popstate`;function w(e){return typeof e==`object`&&!!e&&`pathname`in e&&`search`in e&&`hash`in e&&`state`in e&&`key`in e}function T(e={}){function t(e,t){let n=t.state?.masked,{pathname:r,search:i,hash:a}=n||e.location;return A(``,{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:j(t)}return N(t,n,null,e)}function E(e,t){if(e===!1||e==null)throw Error(t)}function D(e,t){if(!e){typeof console<`u`&&console.warn(t);try{throw Error(t)}catch{}}}function O(){return Math.random().toString(36).substring(2,10)}function k(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 A(e,t,n=null,r,i){return{pathname:typeof e==`string`?e:e.pathname,search:``,hash:``,...typeof t==`string`?M(t):t,state:n,key:t&&t.key||r||O(),mask:i}}function j({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 M(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 N(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=w(e)?e:A(h.location,e,t);n&&n(r,e),l=u()+1;let d=k(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=w(e)?e:A(h.location,e,t);n&&n(r,e),l=u();let i=k(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 P(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(C,d),c=e,()=>{i.removeEventListener(C,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 P(e,t=!1){let n=`http://localhost`;typeof window<`u`&&(n=window.location.origin===`null`?window.location.href:window.location.origin),E(n,`No window.location.(origin|href) available to create URL`);let r=typeof e==`string`?e:j(e);return r=r.replace(/ $/,`%20`),!t&&r.startsWith(`//`)&&(r=n+r),new URL(r,n)}function F(e,t,n=`/`){return I(e,t,n,!1)}function I(e,t,n,r,i){let a=ae((typeof t==`string`?M(t):t).pathname||`/`,n);if(a==null)return null;let o=i??R(e),s=null,c=ie(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;E(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=me([r,c.relativePath]),u=n.concat(c);e.children&&e.children.length>0&&(E(e.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${l}".`),z(e.children,t,u,l,o)),!(e.path==null&&!e.index)&&t.push({path:l,score:K(l,e.index),routesMeta:u})};return e.forEach((e,t)=>{if(e.path===``||!e.path?.includes(`?`))a(e,t);else for(let n of B(e.path))a(e,t,!0,n)}),t}function B(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=B(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 V(e){e.sort((e,t)=>e.score===t.score?q(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var H=/^:[\w-]+$/,U=3,W=2,ee=1,te=10,G=-2,ne=e=>e===`*`;function K(e,t){let n=e.split(`/`),r=n.length;return n.some(ne)&&(r+=G),t&&(r+=W),n.filter(e=>!ne(e)).reduce((e,t)=>e+(H.test(t)?U:t===``?ee:te),r)}function q(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 J(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 Y(e,t=!1,n=!0){D(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 ie(e){try{return e.split(`/`).map(e=>decodeURIComponent(e).replace(/\//g,`%2F`)).join(`/`)}catch(t){return D(!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 ae(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 oe=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function se(e,t=`/`){let{pathname:n,search:r=``,hash:i=``}=typeof e==`string`?M(e):e,a;return n?(n=pe(n),a=n.startsWith(`/`)?ce(n.substring(1),`/`):ce(n,t)):a=t,{pathname:a,search:_e(r),hash:ve(i)}}function ce(e,t){let n=he(t).split(`/`);return e.split(`/`).forEach(e=>{e===`..`?n.length>1&&n.pop():e!==`.`&&n.push(e)}),n.length>1?n.join(`/`):`/`}function le(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 ue(e){return e.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function de(e){let t=ue(e);return t.map((e,n)=>n===t.length-1?e.pathname:e.pathnameBase)}function fe(e,t,n,r=!1){let i;typeof e==`string`?i=M(e):(i={...e},E(!i.pathname||!i.pathname.includes(`?`),le(`?`,`pathname`,`search`,i)),E(!i.pathname||!i.pathname.includes(`#`),le(`#`,`pathname`,`hash`,i)),E(!i.search||!i.search.includes(`#`),le(`#`,`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=se(i,s),l=o&&o!==`/`&&o.endsWith(`/`),u=(a||o===`.`)&&n.endsWith(`/`);return!c.pathname.endsWith(`/`)&&(l||u)&&(c.pathname+=`/`),c}var pe=e=>e.replace(/\/\/+/g,`/`),me=e=>pe(e.join(`/`)),he=e=>e.replace(/\/+$/,``),ge=e=>he(e).replace(/^\/*/,`/`),_e=e=>!e||e===`?`?``:e.startsWith(`?`)?e:`?`+e,ve=e=>!e||e===`#`?``:e.startsWith(`#`)?e:`#`+e,ye=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 be(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.internal==`boolean`&&`data`in e}function xe(e){return me(e.map(e=>e.route.path).filter(Boolean))||`/`}var Se=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;function Ce(e,t){let n=e;if(typeof n!=`string`||!oe.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,i=!1;if(Se)try{let e=new URL(window.location.href),r=n.startsWith(`//`)?new URL(e.protocol+n):new URL(n),a=ae(r.pathname,t);r.origin===e.origin&&a!=null?n=a+r.search+r.hash:i=!0}catch{D(!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 we=[`POST`,`PUT`,`PATCH`,`DELETE`];new Set(we);var Te=[`GET`,...we];new Set(Te);var Ee=S.createContext(null);Ee.displayName=`DataRouter`;var De=S.createContext(null);De.displayName=`DataRouterState`;var Oe=S.createContext(!1);function ke(){return S.useContext(Oe)}var Ae=S.createContext({isTransitioning:!1});Ae.displayName=`ViewTransition`;var je=S.createContext(new Map);je.displayName=`Fetchers`;var Me=S.createContext(null);Me.displayName=`Await`;var Ne=S.createContext(null);Ne.displayName=`Navigation`;var Pe=S.createContext(null);Pe.displayName=`Location`;var Fe=S.createContext({outlet:null,matches:[],isDataRoute:!1});Fe.displayName=`Route`;var Ie=S.createContext(null);Ie.displayName=`RouteError`;var Le=`REACT_ROUTER_ERROR`,Re=`REDIRECT`,ze=`ROUTE_ERROR_RESPONSE`;function Be(e){if(e.startsWith(`${Le}:${Re}:{`))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 Ve(e){if(e.startsWith(`${Le}:${ze}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`)return new ye(t.status,t.statusText,t.data)}catch{}}function He(e,{relative:t}={}){E(Ue(),`useHref() may be used only in the context of a component.`);let{basename:n,navigator:r}=S.useContext(Ne),{hash:i,pathname:a,search:o}=Qe(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:me([n,a])),r.createHref({pathname:s,search:o,hash:i})}function Ue(){return S.useContext(Pe)!=null}function We(){return E(Ue(),`useLocation() may be used only in the context of a component.`),S.useContext(Pe).location}var Ge=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function Ke(e){S.useContext(Ne).static||S.useLayoutEffect(e)}function qe(){let{isDataRoute:e}=S.useContext(Fe);return e?_t():Je()}function Je(){E(Ue(),`useNavigate() may be used only in the context of a component.`);let e=S.useContext(Ee),{basename:t,navigator:n}=S.useContext(Ne),{matches:r}=S.useContext(Fe),{pathname:i}=We(),a=JSON.stringify(de(r)),o=S.useRef(!1);return Ke(()=>{o.current=!0}),S.useCallback((r,s={})=>{if(D(o.current,Ge),!o.current)return;if(typeof r==`number`){n.go(r);return}let c=fe(r,JSON.parse(a),i,s.relative===`path`);e==null&&t!==`/`&&(c.pathname=c.pathname===`/`?t:me([t,c.pathname])),(s.replace?n.replace:n.push)(c,s.state,s)},[t,n,a,i,e])}var Ye=S.createContext(null);function Xe(e){let t=S.useContext(Fe).outlet;return S.useMemo(()=>t&&S.createElement(Ye.Provider,{value:e},t),[t,e])}function Ze(){let{matches:e}=S.useContext(Fe);return e[e.length-1]?.params??{}}function Qe(e,{relative:t}={}){let{matches:n}=S.useContext(Fe),{pathname:r}=We(),i=JSON.stringify(de(n));return S.useMemo(()=>fe(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function $e(e,t){return et(e,t)}function et(e,t,n){E(Ue(),`useRoutes() may be used only in the context of a component.`);let{navigator:r}=S.useContext(Ne),{matches:i}=S.useContext(Fe),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||``;yt(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=We(),d;if(t){let e=typeof t==`string`?M(t):t;E(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})):F(e,{pathname:p});D(l||m!=null,`No routes matched location "${d.pathname}${d.search}${d.hash}" `),D(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=st(m&&m.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:me([c,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?c:me([c,r.encodeLocation?r.encodeLocation(e.pathnameBase.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathnameBase])})),i,n);return t&&h?S.createElement(Pe.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,mask:void 0,...d},navigationType:`POP`}},h):h}function tt(){let e=gt(),t=be(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=S.createElement(S.Fragment,null,S.createElement(`p`,null,`💿 Hey developer 👋`),S.createElement(`p`,null,`You can provide a way better UX than this when your app throws errors by providing your own `,S.createElement(`code`,{style:a},`ErrorBoundary`),` or`,` `,S.createElement(`code`,{style:a},`errorElement`),` prop on your route.`)),S.createElement(S.Fragment,null,S.createElement(`h2`,null,`Unexpected Application Error!`),S.createElement(`h3`,{style:{fontStyle:`italic`}},t),n?S.createElement(`pre`,{style:i},n):null,o)}var nt=S.createElement(tt,null),rt=class extends S.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=Ve(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:S.createElement(Fe.Provider,{value:this.props.routeContext},S.createElement(Ie.Provider,{value:e,children:this.props.component}));return this.context?S.createElement(at,{error:e},t):t}};rt.contextType=Oe;var it=new WeakMap;function at({children:e,error:t}){let{basename:n}=S.useContext(Ne);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=Be(t.digest);if(e){let r=it.get(t);if(r)throw r;let i=Ce(e.location,n);if(Se&&!it.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 it.set(t,n),n}return S.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${i.absoluteURL||i.to}`})}}return e}function ot({routeContext:e,match:t,children:n}){let r=S.useContext(Ee);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),S.createElement(Fe.Provider,{value:e},n)}function st(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);E(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:xe(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||nt,o&&(s<0&&c===0?(yt(`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?S.createElement(n.route.Component,null):n.route.element?n.route.element:e,S.createElement(ot,{match:n,routeContext:{outlet:e,matches:m,isDataRoute:r!=null},children:t})};return r&&(n.route.ErrorBoundary||n.route.errorElement||c===0)?S.createElement(rt,{location:r.location,revalidation:r.revalidation,component:f,error:u,children:h(),routeContext:{outlet:null,matches:m,isDataRoute:!0},onError:l}):h()},null)}function ct(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function lt(e){let t=S.useContext(Ee);return E(t,ct(e)),t}function ut(e){let t=S.useContext(De);return E(t,ct(e)),t}function dt(e){let t=S.useContext(Fe);return E(t,ct(e)),t}function ft(e){let t=dt(e),n=t.matches[t.matches.length-1];return E(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function pt(){return ft(`useRouteId`)}function mt(){return ut(`useNavigation`).navigation}function ht(){let{matches:e,loaderData:t}=ut(`useMatches`);return S.useMemo(()=>e.map(e=>L(e,t)),[e,t])}function gt(){let e=S.useContext(Ie),t=ut(`useRouteError`),n=ft(`useRouteError`);return e===void 0?t.errors?.[n]:e}function _t(){let{router:e}=lt(`useNavigate`),t=ft(`useNavigate`),n=S.useRef(!1);return Ke(()=>{n.current=!0}),S.useCallback(async(r,i={})=>{D(n.current,Ge),n.current&&(typeof r==`number`?await e.navigate(r):await e.navigate(r,{fromRouteId:t,...i}))},[e,t])}var vt={};function yt(e,t,n){!t&&!vt[e]&&(vt[e]=!0,D(!1,n))}S.memo(bt);function bt({routes:e,manifest:t,future:n,state:r,isStatic:i,onError:a}){return et(e,void 0,{manifest:t,state:r,isStatic:i,onError:a,future:n})}function xt({to:e,replace:t,state:n,relative:r}){E(Ue(),` may be used only in the context of a component.`);let{static:i}=S.useContext(Ne);D(!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}=S.useContext(Fe),{pathname:o}=We(),s=qe(),c=fe(e,de(a),o,r===`path`),l=JSON.stringify(c);return S.useEffect(()=>{s(JSON.parse(l),{replace:t,state:n,relative:r})},[s,l,r,t,n]),null}function St(e){return Xe(e.context)}function Ct(e){E(!1,`A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .`)}function wt({basename:e=`/`,children:t=null,location:n,navigationType:r=`POP`,navigator:i,static:a=!1,useTransitions:o}){E(!Ue(),`You cannot render a inside another . You should never have more than one in your app.`);let s=e.replace(/^\/*/,`/`),c=S.useMemo(()=>({basename:s,navigator:i,static:a,useTransitions:o,future:{}}),[s,i,a,o]);typeof n==`string`&&(n=M(n));let{pathname:l=`/`,search:u=``,hash:d=``,state:f=null,key:p=`default`,mask:m}=n,h=S.useMemo(()=>{let e=ae(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 D(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:S.createElement(Ne.Provider,{value:c},S.createElement(Pe.Provider,{children:t,value:h}))}function Tt({children:e,location:t}){return $e(Et(e),t)}S.Component;function Et(e,t=[]){let n=[];return S.Children.forEach(e,(e,r)=>{if(!S.isValidElement(e))return;let i=[...t,r];if(e.type===S.Fragment){n.push.apply(n,Et(e.props.children,i));return}E(e.type===Ct,`[${typeof e.type==`string`?e.type:e.type.name}] is not a component. All component children of must be a or `),E(!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=Et(e.props.children,i)),n.push(a)}),n}var Dt=`get`,Ot=`application/x-www-form-urlencoded`;function kt(e){return typeof HTMLElement<`u`&&e instanceof HTMLElement}function At(e){return kt(e)&&e.tagName.toLowerCase()===`button`}function jt(e){return kt(e)&&e.tagName.toLowerCase()===`form`}function Mt(e){return kt(e)&&e.tagName.toLowerCase()===`input`}function Nt(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function Pt(e,t){return e.button===0&&(!t||t===`_self`)&&!Nt(e)}function Ft(e=``){return new URLSearchParams(typeof e==`string`||Array.isArray(e)||e instanceof URLSearchParams?e:Object.keys(e).reduce((t,n)=>{let r=e[n];return t.concat(Array.isArray(r)?r.map(e=>[n,e]):[[n,r]])},[]))}function It(e,t){let n=Ft(e);return t&&t.forEach((e,r)=>{n.has(r)||t.getAll(r).forEach(e=>{n.append(r,e)})}),n}var Lt=null;function Rt(){if(Lt===null)try{new FormData(document.createElement(`form`),0),Lt=!1}catch{Lt=!0}return Lt}var zt=new Set([`application/x-www-form-urlencoded`,`multipart/form-data`,`text/plain`]);function Bt(e){return e!=null&&!zt.has(e)?(D(!1,`"${e}" is not a valid \`encType\` for \`\`/\`\` and will default to "${Ot}"`),null):e}function Vt(e,t){let n,r,i,a,o;if(jt(e)){let o=e.getAttribute(`action`);r=o?ae(o,t):null,n=e.getAttribute(`method`)||Dt,i=Bt(e.getAttribute(`enctype`))||Ot,a=new FormData(e)}else if(At(e)||Mt(e)&&(e.type===`submit`||e.type===`image`)){let o=e.form;if(o==null)throw Error(`Cannot submit a