Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 38097da994 | |||
| 94d8fa455d | |||
| 9f959ca87b | |||
| 7f4ab50557 | |||
| e4e3884ffc | |||
| a609379d20 | |||
| d78412646e | |||
| 7e22c879b9 | |||
| 879d5bea95 | |||
| 8333d203db | |||
| a51ffb4de3 | |||
| 1080c530a7 | |||
| b22cddc7f7 | |||
| 98b779a622 | |||
| cf2796666d |
@@ -1 +1 @@
|
||||
0.3.31
|
||||
0.3.52
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import{x as qu,m as $_,h as Ce,B as Hg,d as q_,o as K_,a as Tt,c as Nt,K as ml,L as _l,b as Q,F as Vr,r as Gr,f as $t,w as qt,e as Oe,v as Ka,j as Hr,i as Q_,t as wt,n as Nc,y as Ei,l as zc,p as J_,E as j_,u as t1}from"./index-DT1mRj5t.js";import{a as e1,h as r1,c as i1,i as n1,j as a1,t as o1,_ as s1}from"./_plugin-vue_export-helper-1Z-znrfZ.js";import l1 from"./CloudConfig-CgJ44DuS.js";import u1 from"./SystemConfig-BeJ_jebO.js";import f1 from"./SaveRecords-CZh2kRyW.js";import"./index-Bn7NwETH.js";import"./CloudBadge-BEGriYUm.js";/*! *****************************************************************************
|
||||
import{x as qu,m as $_,h as Ce,B as Hg,d as q_,o as K_,a as Tt,c as Nt,K as ml,L as _l,b as Q,F as Vr,r as Gr,f as $t,w as qt,e as Oe,v as Ka,j as Hr,i as Q_,t as wt,n as Nc,y as Ei,l as zc,p as J_,E as j_,u as t1}from"./index-CK-9TfWb.js";import{a as e1,h as r1,c as i1,i as n1,j as a1,t as o1,_ as s1}from"./_plugin-vue_export-helper-D4DENoBS.js";import l1 from"./CloudConfig-vPJUzF1U.js";import u1 from"./SystemConfig-tnevz2yA.js";import f1 from"./SaveRecords-BcXS42JB.js";import"./index-Bn7NwETH.js";import"./CloudBadge-OCEQ2GP-.js";/*! *****************************************************************************
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
@@ -1 +1 @@
|
||||
import{d as B,o as N,a as V,c as I,b as n,t as c,f as e,w as t,h as g,v as y,j as u,k as r,C as M,l,D as T,G as j,H as q,I as z,J as A,u as D,z as H}from"./index-DT1mRj5t.js";import{a as L,_ as R}from"./_plugin-vue_export-helper-1Z-znrfZ.js";const E={class:"admin-layout"},G={class:"admin-sidebar"},J={class:"sidebar-brand"},W={class:"sidebar-brand-text"},F={class:"sidebar-version"},K={class:"admin-content"},O={class:"content-header"},P={class:"content-breadcrumb"},Q={class:"breadcrumb-current"},U={class:"content-actions"},X={class:"content-body"},Y=B({__name:"AdminLayout",setup(Z){const d=D(),f=H(),m=g(""),_=g(""),b={dashboard:"仪表盘","cloud-configs-toggle":"网盘设置及授权","cloud-configs-cleanup":"存储清理","sys-site":"网站设置","sys-services":"外部服务 & 缓存","sys-strategy":"性能配置","sys-password":"修改管理员密码","sys-notify":"消息推送","sys-daily-report":"每日汇报","save-records":"转存日志"},p=y(()=>{const o=f.name;return o==="admin-cloud-configs"?"cloud-configs-toggle":o==="admin-cleanup"?"cloud-configs-cleanup":o==="admin-system"?f.query.section||"sys-site":o==="admin-save-records"?"save-records":"dashboard"}),x=y(()=>b[p.value]||"仪表盘");function w(o){o==="dashboard"?d.push("/admin/dashboard"):o==="cloud-configs-toggle"?d.push("/admin/cloud-configs"):o==="cloud-configs-cleanup"?d.push("/admin/cleanup"):o.startsWith("sys-")?d.push({path:"/admin/system",query:{section:o}}):o==="save-records"?d.push("/admin/save-records"):o==="logout"&&(localStorage.removeItem("admin_token"),d.push("/admin/login"))}function h(){d.push("/")}return N(async()=>{try{const o=await L();m.value=o.site_name||""}catch{}try{const s=await(await fetch("/health")).json();_.value=s.version}catch{}}),(o,s)=>{const i=u("el-icon"),a=u("el-menu-item"),v=u("el-sub-menu"),C=u("el-menu"),k=u("el-button"),S=u("router-view");return V(),I("div",E,[n("aside",G,[n("div",J,[s[1]||(s[1]=n("div",{class:"sidebar-logo"},"☁️",-1)),n("div",W,[n("h2",null,c(m.value||"CloudSearch"),1),s[0]||(s[0]=n("p",null,"管理控制台",-1))])]),e(C,{"default-active":p.value,class:"sidebar-menu",onSelect:w},{default:t(()=>[e(a,{index:"dashboard"},{default:t(()=>[e(i,null,{default:t(()=>[e(r(M))]),_:1}),s[2]||(s[2]=n("span",null,"仪表盘",-1))]),_:1}),e(v,{index:"cloud-configs"},{title:t(()=>[e(i,null,{default:t(()=>[e(r(T))]),_:1}),s[3]||(s[3]=n("span",null,"网盘管理",-1))]),default:t(()=>[e(a,{index:"cloud-configs-toggle"},{default:t(()=>[...s[4]||(s[4]=[l("📋 设置及授权",-1)])]),_:1}),e(a,{index:"cloud-configs-cleanup"},{default:t(()=>[...s[5]||(s[5]=[l("🧹 存储清理",-1)])]),_:1})]),_:1}),e(v,{index:"system"},{title:t(()=>[e(i,null,{default:t(()=>[e(r(j))]),_:1}),s[6]||(s[6]=n("span",null,"系统设置",-1))]),default:t(()=>[e(a,{index:"sys-site"},{default:t(()=>[...s[7]||(s[7]=[l("🌐 网站设置",-1)])]),_:1}),e(a,{index:"sys-services"},{default:t(()=>[...s[8]||(s[8]=[l("🔗 外部服务 & 缓存",-1)])]),_:1}),e(a,{index:"sys-strategy"},{default:t(()=>[...s[9]||(s[9]=[l("⚡ 性能配置",-1)])]),_:1}),e(a,{index:"sys-password"},{default:t(()=>[...s[10]||(s[10]=[l("🔑 修改密码",-1)])]),_:1}),e(a,{index:"sys-notify"},{default:t(()=>[...s[11]||(s[11]=[l("📬 消息推送",-1)])]),_:1}),e(a,{index:"sys-daily-report"},{default:t(()=>[...s[12]||(s[12]=[l("📊 每日汇报",-1)])]),_:1})]),_:1}),e(a,{index:"save-records"},{default:t(()=>[e(i,null,{default:t(()=>[e(r(q))]),_:1}),s[13]||(s[13]=n("span",null,"转存日志",-1))]),_:1}),s[15]||(s[15]=n("div",{class:"sidebar-spacer"},null,-1)),n("div",F,"v"+c(_.value),1),e(a,{index:"logout"},{default:t(()=>[e(i,null,{default:t(()=>[e(r(z))]),_:1}),s[14]||(s[14]=n("span",null,"退出登录",-1))]),_:1})]),_:1},8,["default-active"])]),n("div",K,[n("header",O,[n("div",P,[n("span",Q,c(x.value),1)]),n("div",U,[e(k,{text:"",size:"small",onClick:h},{default:t(()=>[e(i,null,{default:t(()=>[e(r(A))]),_:1}),s[16]||(s[16]=l(" 返回前台 ",-1))]),_:1})])]),n("main",X,[e(S)])])])}}}),es=R(Y,[["__scopeId","data-v-647abf08"]]);export{es as default};
|
||||
import{d as B,o as N,a as V,c as I,b as n,t as c,f as e,w as t,h as g,v as y,j as u,k as r,C as M,l,D as T,G as j,H as q,I as z,J as A,u as D,z as H}from"./index-CK-9TfWb.js";import{a as L,_ as R}from"./_plugin-vue_export-helper-D4DENoBS.js";const E={class:"admin-layout"},G={class:"admin-sidebar"},J={class:"sidebar-brand"},W={class:"sidebar-brand-text"},F={class:"sidebar-version"},K={class:"admin-content"},O={class:"content-header"},P={class:"content-breadcrumb"},Q={class:"breadcrumb-current"},U={class:"content-actions"},X={class:"content-body"},Y=B({__name:"AdminLayout",setup(Z){const d=D(),f=H(),m=g(""),_=g(""),b={dashboard:"仪表盘","cloud-configs-toggle":"网盘设置及授权","cloud-configs-cleanup":"存储清理","sys-site":"网站设置","sys-services":"外部服务 & 缓存","sys-strategy":"性能配置","sys-password":"修改管理员密码","sys-notify":"消息推送","sys-daily-report":"每日汇报","save-records":"转存日志"},p=y(()=>{const o=f.name;return o==="admin-cloud-configs"?"cloud-configs-toggle":o==="admin-cleanup"?"cloud-configs-cleanup":o==="admin-system"?f.query.section||"sys-site":o==="admin-save-records"?"save-records":"dashboard"}),x=y(()=>b[p.value]||"仪表盘");function w(o){o==="dashboard"?d.push("/admin/dashboard"):o==="cloud-configs-toggle"?d.push("/admin/cloud-configs"):o==="cloud-configs-cleanup"?d.push("/admin/cleanup"):o.startsWith("sys-")?d.push({path:"/admin/system",query:{section:o}}):o==="save-records"?d.push("/admin/save-records"):o==="logout"&&(localStorage.removeItem("admin_token"),d.push("/admin/login"))}function h(){d.push("/")}return N(async()=>{try{const o=await L();m.value=o.site_name||""}catch{}try{const s=await(await fetch("/health")).json();_.value=s.version}catch{}}),(o,s)=>{const i=u("el-icon"),a=u("el-menu-item"),v=u("el-sub-menu"),C=u("el-menu"),k=u("el-button"),S=u("router-view");return V(),I("div",E,[n("aside",G,[n("div",J,[s[1]||(s[1]=n("div",{class:"sidebar-logo"},"☁️",-1)),n("div",W,[n("h2",null,c(m.value||"CloudSearch"),1),s[0]||(s[0]=n("p",null,"管理控制台",-1))])]),e(C,{"default-active":p.value,class:"sidebar-menu",onSelect:w},{default:t(()=>[e(a,{index:"dashboard"},{default:t(()=>[e(i,null,{default:t(()=>[e(r(M))]),_:1}),s[2]||(s[2]=n("span",null,"仪表盘",-1))]),_:1}),e(v,{index:"cloud-configs"},{title:t(()=>[e(i,null,{default:t(()=>[e(r(T))]),_:1}),s[3]||(s[3]=n("span",null,"网盘管理",-1))]),default:t(()=>[e(a,{index:"cloud-configs-toggle"},{default:t(()=>[...s[4]||(s[4]=[l("📋 设置及授权",-1)])]),_:1}),e(a,{index:"cloud-configs-cleanup"},{default:t(()=>[...s[5]||(s[5]=[l("🧹 存储清理",-1)])]),_:1})]),_:1}),e(v,{index:"system"},{title:t(()=>[e(i,null,{default:t(()=>[e(r(j))]),_:1}),s[6]||(s[6]=n("span",null,"系统设置",-1))]),default:t(()=>[e(a,{index:"sys-site"},{default:t(()=>[...s[7]||(s[7]=[l("🌐 网站设置",-1)])]),_:1}),e(a,{index:"sys-services"},{default:t(()=>[...s[8]||(s[8]=[l("🔗 外部服务 & 缓存",-1)])]),_:1}),e(a,{index:"sys-strategy"},{default:t(()=>[...s[9]||(s[9]=[l("⚡ 性能配置",-1)])]),_:1}),e(a,{index:"sys-password"},{default:t(()=>[...s[10]||(s[10]=[l("🔑 修改密码",-1)])]),_:1}),e(a,{index:"sys-notify"},{default:t(()=>[...s[11]||(s[11]=[l("📬 消息推送",-1)])]),_:1}),e(a,{index:"sys-daily-report"},{default:t(()=>[...s[12]||(s[12]=[l("📊 每日汇报",-1)])]),_:1})]),_:1}),e(a,{index:"save-records"},{default:t(()=>[e(i,null,{default:t(()=>[e(r(q))]),_:1}),s[13]||(s[13]=n("span",null,"转存日志",-1))]),_:1}),s[15]||(s[15]=n("div",{class:"sidebar-spacer"},null,-1)),n("div",F,"v"+c(_.value),1),e(a,{index:"logout"},{default:t(()=>[e(i,null,{default:t(()=>[e(r(z))]),_:1}),s[14]||(s[14]=n("span",null,"退出登录",-1))]),_:1})]),_:1},8,["default-active"])]),n("div",K,[n("header",O,[n("div",P,[n("span",Q,c(x.value),1)]),n("div",U,[e(k,{text:"",size:"small",onClick:h},{default:t(()=>[e(i,null,{default:t(()=>[e(r(A))]),_:1}),s[16]||(s[16]=l(" 返回前台 ",-1))]),_:1})])]),n("main",X,[e(S)])])])}}}),es=R(Y,[["__scopeId","data-v-647abf08"]]);export{es as default};
|
||||
@@ -1 +1 @@
|
||||
import{d as k,o as C,a as w,c as y,b as a,t as m,f as t,w as i,g as x,e as L,h as d,j as p,l as N,i as S,E as B}from"./index-DT1mRj5t.js";import{a as E,d as M,_ as U}from"./_plugin-vue_export-helper-1Z-znrfZ.js";const j={class:"admin-login-page"},q={class:"login-card"},A={class:"login-brand"},I={class:"login-title"},K={key:0,class:"error-msg"},R={class:"login-footer"},z=k({__name:"AdminLogin",setup(D){const f=d(),u=d(!1),c=d(""),g=d(""),v=d("");E().then(l=>{l.site_name&&(g.value=l.site_name)}).catch(()=>{});const s=S({username:"",password:""}),b={username:[{required:!0,message:"请输入用户名",trigger:"blur"}],password:[{required:!0,message:"请输入密码",trigger:"blur"}]};async function h(){var e,r,n;if(await((e=f.value)==null?void 0:e.validate().catch(()=>!1))){u.value=!0,c.value="";try{const o=await M(s.username,s.password);localStorage.setItem("admin_token",o.token),B.success("登录成功"),window.location.href="/admin"}catch(o){c.value=((n=(r=o==null?void 0:o.response)==null?void 0:r.data)==null?void 0:n.message)||(o==null?void 0:o.message)||"登录失败"}finally{u.value=!1}}}return C(async()=>{try{const e=await(await fetch("/health")).json();v.value=e.version||""}catch{}}),(l,e)=>{const r=p("el-input"),n=p("el-form-item"),o=p("el-button"),V=p("el-form");return w(),y("div",j,[e[4]||(e[4]=a("div",{class:"login-bg-pattern"},null,-1)),a("div",q,[a("div",A,[e[2]||(e[2]=a("div",{class:"login-logo"},"☁️",-1)),a("h1",I,m(g.value||"CloudSearch"),1),e[3]||(e[3]=a("p",{class:"login-subtitle"},"管理后台",-1))]),t(V,{ref_key:"formRef",ref:f,model:s,rules:b,"label-width":"0",size:"large",onKeyup:x(h,["enter"])},{default:i(()=>[t(n,{prop:"username"},{default:i(()=>[t(r,{modelValue:s.username,"onUpdate:modelValue":e[0]||(e[0]=_=>s.username=_),placeholder:"用户名","prefix-icon":"User"},null,8,["modelValue"])]),_:1}),t(n,{prop:"password"},{default:i(()=>[t(r,{modelValue:s.password,"onUpdate:modelValue":e[1]||(e[1]=_=>s.password=_),type:"password",placeholder:"密码","prefix-icon":"Lock","show-password":""},null,8,["modelValue"])]),_:1}),t(n,null,{default:i(()=>[t(o,{type:"primary",loading:u.value,class:"login-btn",onClick:h},{default:i(()=>[N(m(u.value?"登录中...":"登 录"),1)]),_:1},8,["loading"])]),_:1})]),_:1},8,["model"]),c.value?(w(),y("p",K,m(c.value),1)):L("",!0),a("p",R,"CloudSearch v"+m(v.value),1)])])}}}),G=U(z,[["__scopeId","data-v-bd0b6672"]]);export{G as default};
|
||||
import{d as k,o as C,a as w,c as y,b as a,t as m,f as t,w as i,g as x,e as L,h as d,j as p,l as N,i as S,E as B}from"./index-CK-9TfWb.js";import{a as E,d as M,_ as U}from"./_plugin-vue_export-helper-D4DENoBS.js";const j={class:"admin-login-page"},q={class:"login-card"},A={class:"login-brand"},I={class:"login-title"},K={key:0,class:"error-msg"},R={class:"login-footer"},z=k({__name:"AdminLogin",setup(D){const f=d(),u=d(!1),c=d(""),g=d(""),v=d("");E().then(l=>{l.site_name&&(g.value=l.site_name)}).catch(()=>{});const s=S({username:"",password:""}),b={username:[{required:!0,message:"请输入用户名",trigger:"blur"}],password:[{required:!0,message:"请输入密码",trigger:"blur"}]};async function h(){var e,r,n;if(await((e=f.value)==null?void 0:e.validate().catch(()=>!1))){u.value=!0,c.value="";try{const o=await M(s.username,s.password);localStorage.setItem("admin_token",o.token),B.success("登录成功"),window.location.href="/admin"}catch(o){c.value=((n=(r=o==null?void 0:o.response)==null?void 0:r.data)==null?void 0:n.message)||(o==null?void 0:o.message)||"登录失败"}finally{u.value=!1}}}return C(async()=>{try{const e=await(await fetch("/health")).json();v.value=e.version||""}catch{}}),(l,e)=>{const r=p("el-input"),n=p("el-form-item"),o=p("el-button"),V=p("el-form");return w(),y("div",j,[e[4]||(e[4]=a("div",{class:"login-bg-pattern"},null,-1)),a("div",q,[a("div",A,[e[2]||(e[2]=a("div",{class:"login-logo"},"☁️",-1)),a("h1",I,m(g.value||"CloudSearch"),1),e[3]||(e[3]=a("p",{class:"login-subtitle"},"管理后台",-1))]),t(V,{ref_key:"formRef",ref:f,model:s,rules:b,"label-width":"0",size:"large",onKeyup:x(h,["enter"])},{default:i(()=>[t(n,{prop:"username"},{default:i(()=>[t(r,{modelValue:s.username,"onUpdate:modelValue":e[0]||(e[0]=_=>s.username=_),placeholder:"用户名","prefix-icon":"User"},null,8,["modelValue"])]),_:1}),t(n,{prop:"password"},{default:i(()=>[t(r,{modelValue:s.password,"onUpdate:modelValue":e[1]||(e[1]=_=>s.password=_),type:"password",placeholder:"密码","prefix-icon":"Lock","show-password":""},null,8,["modelValue"])]),_:1}),t(n,null,{default:i(()=>[t(o,{type:"primary",loading:u.value,class:"login-btn",onClick:h},{default:i(()=>[N(m(u.value?"登录中...":"登 录"),1)]),_:1},8,["loading"])]),_:1})]),_:1},8,["model"]),c.value?(w(),y("p",K,m(c.value),1)):L("",!0),a("p",R,"CloudSearch v"+m(v.value),1)])])}}}),G=U(z,[["__scopeId","data-v-bd0b6672"]]);export{G as default};
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{C as s,b as a,a as n}from"./index-Bn7NwETH.js";import{d as l,a as t,c,p as d,k as o,e as r,l as u,t as m}from"./index-DT1mRj5t.js";import{_}from"./_plugin-vue_export-helper-1Z-znrfZ.js";const p=["src"],i=l({__name:"CloudBadge",props:{cloud_type:{},showIcon:{type:Boolean}},setup(e){return(C,y)=>(t(),c("span",{class:"cloud-badge",style:d({background:o(s)[e.cloud_type]})},[e.showIcon&&o(a)[e.cloud_type]?(t(),c("img",{key:0,src:o(a)[e.cloud_type],class:"badge-icon"},null,8,p)):r("",!0),u(" "+m(o(n)[e.cloud_type]),1)],4))}}),L=_(i,[["__scopeId","data-v-9106805f"]]);export{L as C};
|
||||
import{C as s,b as a,a as n}from"./index-Bn7NwETH.js";import{d as l,a as t,c,p as d,k as o,e as r,l as u,t as m}from"./index-CK-9TfWb.js";import{_}from"./_plugin-vue_export-helper-D4DENoBS.js";const p=["src"],i=l({__name:"CloudBadge",props:{cloud_type:{},showIcon:{type:Boolean}},setup(e){return(C,y)=>(t(),c("span",{class:"cloud-badge",style:d({background:o(s)[e.cloud_type]})},[e.showIcon&&o(a)[e.cloud_type]?(t(),c("img",{key:0,src:o(a)[e.cloud_type],class:"badge-icon"},null,8,p)):r("",!0),u(" "+m(o(n)[e.cloud_type]),1)],4))}}),L=_(i,[["__scopeId","data-v-9106805f"]]);export{L as C};
|
||||
@@ -1,4 +1,4 @@
|
||||
import{d as ke,o as L,m as ve,E as _,a as c,c as k,f as n,w as a,b as r,h as C,j as p,i as be,F as R,r as K,t as v,y as g,l as d,e as A,k as Ce,M as he,p as xe,n as H,K as Be,L as Te,v as h}from"./index-DT1mRj5t.js";import{a as x}from"./index-Bn7NwETH.js";import{c as we,k as Fe,h as Ne,l as G,t as Ve,u as P,m as ze,n as Se,o as $e,_ as Ue}from"./_plugin-vue_export-helper-1Z-znrfZ.js";import{C as De}from"./CloudBadge-BEGriYUm.js";const Ie={class:"cloud-config"},Me={class:"cloud-toggle-grid"},Oe=["src"],qe={class:"cloud-label"},Ee={class:"toolbar"},Le={key:0,class:"nickname-text"},Re={key:0,class:"promotion-text"},Ke={key:0,class:"uid-cell"},Ae={key:0,class:"verifying"},He={key:0,class:"storage-cell"},Ge={class:"storage-bar-wrap"},Pe={class:"storage-text"},je={class:"storage-used"},Je={class:"storage-total"},Qe={class:"storage-free"},We={key:0,class:"save-count"},Xe={style:{"line-height":"1.6"}},Ye={class:"cookie-tips-header"},Ze={class:"cookie-tips-title"},et=["innerHTML"],tt=ke({__name:"CloudConfig",setup(ot){const z=C([]),D=C(),F=C([]),B=C(!1),T=C(!1),b=C(null),l=be({cloud_type:"",nickname:"",promotion_account:"",is_transfer_enabled:!1,cookie:"",_verifying:!1,_storageUsed:"",_storageTotal:""}),j=h(()=>({cloud_type:[{required:!0,message:"请选择网盘类型",trigger:"change"}],nickname:[{required:!1,message:"请填写昵称(区分多个同类型网盘)",trigger:"blur"}],promotion_account:[{required:!0,message:"请填写推广平台及账号",trigger:"blur"}]})),J=h(()=>Object.entries(x)),Q=h(()=>{if(!l.cloud_type)return"请先选择网盘类型";const t=l.cloud_type;return t==="quark"||t==="baidu"?`请输入 ${x[t]||t} 的完整 Cookie`:b.value?"留空则保持原有":"输入完整 Cookie"}),W=h(()=>x[l.cloud_type]||l.cloud_type||""),X=h(()=>{const t=l.cloud_type;return t?{quark:`<li>在电脑上打开 <a href="https://pan.quark.cn" target="_blank">pan.quark.cn</a> 并登录你的夸克账号</li>
|
||||
import{d as ke,o as L,m as ve,E as _,a as c,c as k,f as n,w as a,b as r,h as C,j as p,i as be,F as R,r as K,t as v,y as g,l as d,e as A,k as Ce,M as he,p as xe,n as H,K as Be,L as Te,v as h}from"./index-CK-9TfWb.js";import{a as x}from"./index-Bn7NwETH.js";import{c as we,k as Fe,h as Ne,l as G,t as Ve,u as P,m as ze,n as Se,o as $e,_ as Ue}from"./_plugin-vue_export-helper-D4DENoBS.js";import{C as De}from"./CloudBadge-OCEQ2GP-.js";const Ie={class:"cloud-config"},Me={class:"cloud-toggle-grid"},Oe=["src"],qe={class:"cloud-label"},Ee={class:"toolbar"},Le={key:0,class:"nickname-text"},Re={key:0,class:"promotion-text"},Ke={key:0,class:"uid-cell"},Ae={key:0,class:"verifying"},He={key:0,class:"storage-cell"},Ge={class:"storage-bar-wrap"},Pe={class:"storage-text"},je={class:"storage-used"},Je={class:"storage-total"},Qe={class:"storage-free"},We={key:0,class:"save-count"},Xe={style:{"line-height":"1.6"}},Ye={class:"cookie-tips-header"},Ze={class:"cookie-tips-title"},et=["innerHTML"],tt=ke({__name:"CloudConfig",setup(ot){const z=C([]),D=C(),F=C([]),B=C(!1),T=C(!1),b=C(null),l=be({cloud_type:"",nickname:"",promotion_account:"",is_transfer_enabled:!1,cookie:"",_verifying:!1,_storageUsed:"",_storageTotal:""}),j=h(()=>({cloud_type:[{required:!0,message:"请选择网盘类型",trigger:"change"}],nickname:[{required:!1,message:"请填写昵称(区分多个同类型网盘)",trigger:"blur"}],promotion_account:[{required:!0,message:"请填写推广平台及账号",trigger:"blur"}]})),J=h(()=>Object.entries(x)),Q=h(()=>{if(!l.cloud_type)return"请先选择网盘类型";const t=l.cloud_type;return t==="quark"||t==="baidu"?`请输入 ${x[t]||t} 的完整 Cookie`:b.value?"留空则保持原有":"输入完整 Cookie"}),W=h(()=>x[l.cloud_type]||l.cloud_type||""),X=h(()=>{const t=l.cloud_type;return t?{quark:`<li>在电脑上打开 <a href="https://pan.quark.cn" target="_blank">pan.quark.cn</a> 并登录你的夸克账号</li>
|
||||
<li>按 <code>F12</code> 打开开发者工具 → 切换到 <strong>网络 (Network)</strong> 选项卡</li>
|
||||
<li>刷新页面,在请求列表中点击任意一个请求(如 <code>account/info</code>)</li>
|
||||
<li>在右侧 <strong>请求头 (Request Headers)</strong> 中找到 <code>Cookie</code> 字段</li>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.el-card[data-v-1e3ebf03]{margin-bottom:20px}.el-card[data-v-1e3ebf03] .el-card__header{font-weight:600;font-size:15px}[data-v-1e3ebf03] .el-divider__text.is-left{left:0;padding-left:0}.form-tip[data-v-1e3ebf03]{font-size:12px;color:#909399;margin-top:4px}.fallback-upload-wrap[data-v-1e3ebf03]{display:flex;flex-direction:column;gap:12px}.fallback-upload-row[data-v-1e3ebf03]{display:flex;align-items:center;flex-wrap:wrap}.fallback-preview[data-v-1e3ebf03]{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.fallback-preview img[data-v-1e3ebf03]{max-width:100%;height:auto;max-height:120px;border-radius:8px;border:1px solid var(--border-color);background:#f0f0f0;object-fit:contain}.strategy-grid[data-v-1e3ebf03]{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px 16px}.grid-cell[data-v-1e3ebf03]{display:flex;flex-direction:column;gap:4px}.strategy-section[data-v-1e3ebf03]{padding:0 4px}.field-block[data-v-1e3ebf03]{margin:12px 0}.field-label-row[data-v-1e3ebf03]{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.field-label[data-v-1e3ebf03]{font-size:14px;font-weight:500;color:#303133;white-space:nowrap}.field-desc[data-v-1e3ebf03]{font-size:12px;color:#909399;margin:3px 0 0;line-height:1.5}.keyword-input-row[data-v-1e3ebf03]{display:flex;gap:8px;flex:1;min-width:200px}.tag-list[data-v-1e3ebf03]{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.tag-empty[data-v-1e3ebf03]{font-size:13px;color:#c0c4cc;margin-top:8px}.filter-rule-help[data-v-1e3ebf03]{margin-top:8px;padding:10px 12px;background:#f8f9fa;border-radius:8px;border:1px solid #e8e8e8}.filter-rule-help .help-title[data-v-1e3ebf03]{font-weight:600;font-size:13px;margin:8px 0 4px;color:#333}.filter-rule-help .help-title[data-v-1e3ebf03]:first-child{margin-top:0}.filter-rule-help .help-row[data-v-1e3ebf03]{font-size:12px;color:#555;margin:3px 0;line-height:1.6}.filter-rule-help .help-row code[data-v-1e3ebf03]{background:#eef1f5;padding:1px 5px;border-radius:3px;font-size:11px;font-family:monospace}.filter-rules-help[data-v-1e3ebf03]{margin-top:8px;padding:12px;background:#f8f9fa;border-radius:8px;border:1px solid #e8e8e8}.help-title[data-v-1e3ebf03]{font-weight:600;font-size:13px;margin:10px 0 6px;color:#333}.help-title[data-v-1e3ebf03]:first-child{margin-top:0}.help-row[data-v-1e3ebf03]{font-size:12px;color:#555;margin:3px 0;line-height:1.6}.help-row code[data-v-1e3ebf03]{background:#eef1f5;padding:1px 5px;border-radius:3px;font-size:11px;font-family:monospace}.help-sample[data-v-1e3ebf03]{background:#1e1e1e;color:#d4d4d4;padding:10px 14px;border-radius:6px;font-size:12px;line-height:1.6;overflow-x:auto;white-space:pre;margin:6px 0 0;font-family:monospace}.help-preview-row[data-v-1e3ebf03]{font-size:13px;margin:4px 0;display:flex;align-items:center;gap:6px}.help-preview-label[data-v-1e3ebf03]{color:#888;min-width:70px;font-size:12px}.help-preview-original[data-v-1e3ebf03]{color:#e74c3c}.help-preview-filtered[data-v-1e3ebf03]{color:#27ae60;font-weight:500}.filter-input-row[data-v-1e3ebf03]{display:flex;gap:8px;width:100%;margin-bottom:8px}.filter-tag-list[data-v-1e3ebf03]{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}.filter-empty[data-v-1e3ebf03]{font-size:13px;color:#c0c4cc;padding:8px 0;margin-bottom:8px}.db-status-grid[data-v-1e3ebf03]{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px;margin-top:8px}.db-stat-item[data-v-1e3ebf03]{background:#f8f9fa;border-radius:10px;padding:16px 12px;text-align:center;border:1px solid #eee;transition:transform .15s,box-shadow .15s}.db-stat-item[data-v-1e3ebf03]:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0000000f}.db-stat-value[data-v-1e3ebf03]{white-space:nowrap;font-size:24px;font-weight:700;color:#303133;margin-bottom:4px}.db-stat-value.text-success[data-v-1e3ebf03]{color:#67c23a}.db-stat-value.text-warning[data-v-1e3ebf03]{color:#e6a23c}.db-stat-label[data-v-1e3ebf03]{font-size:12px;color:#909399}@media (max-width: 900px){.strategy-grid[data-v-1e3ebf03]{grid-template-columns:1fr 1fr}}@media (max-width: 600px){.strategy-grid[data-v-1e3ebf03]{grid-template-columns:1fr}}.pansou-status-grid[data-v-1e3ebf03]{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px;margin-top:8px}.status-dot[data-v-1e3ebf03]{width:8px;height:8px;border-radius:50%;display:inline-block}.dot-ok[data-v-1e3ebf03]{background:#67c23a}.dot-err[data-v-1e3ebf03]{background:#f56c6c}
|
||||
@@ -0,0 +1 @@
|
||||
.el-card[data-v-21262a13]{margin-bottom:20px}.el-card[data-v-21262a13] .el-card__header{font-weight:600;font-size:15px}[data-v-21262a13] .el-divider__text.is-left{left:0;padding-left:0}.form-tip[data-v-21262a13]{font-size:12px;color:#909399;margin-top:4px}.fallback-upload-wrap[data-v-21262a13]{display:flex;flex-direction:column;gap:12px}.fallback-upload-row[data-v-21262a13]{display:flex;align-items:center;flex-wrap:wrap}.fallback-preview[data-v-21262a13]{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.fallback-preview img[data-v-21262a13]{max-width:100%;height:auto;max-height:120px;border-radius:8px;border:1px solid var(--border-color);background:#f0f0f0;object-fit:contain}.strategy-grid[data-v-21262a13]{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px 16px}.grid-cell[data-v-21262a13]{display:flex;flex-direction:column;gap:4px}.strategy-section[data-v-21262a13]{padding:0 4px}.field-block[data-v-21262a13]{margin:12px 0}.field-label-row[data-v-21262a13]{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.field-label[data-v-21262a13]{font-size:14px;font-weight:500;color:#303133;white-space:nowrap}.field-desc[data-v-21262a13]{font-size:12px;color:#909399;margin:3px 0 0;line-height:1.5}.keyword-input-row[data-v-21262a13]{display:flex;gap:8px;flex:1;min-width:200px}.tag-list[data-v-21262a13]{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.tag-empty[data-v-21262a13]{font-size:13px;color:#c0c4cc;margin-top:8px}.filter-rule-help[data-v-21262a13]{margin-top:8px;padding:10px 12px;background:#f8f9fa;border-radius:8px;border:1px solid #e8e8e8}.filter-rule-help .help-title[data-v-21262a13]{font-weight:600;font-size:13px;margin:8px 0 4px;color:#333}.filter-rule-help .help-title[data-v-21262a13]:first-child{margin-top:0}.filter-rule-help .help-row[data-v-21262a13]{font-size:12px;color:#555;margin:3px 0;line-height:1.6}.filter-rule-help .help-row code[data-v-21262a13]{background:#eef1f5;padding:1px 5px;border-radius:3px;font-size:11px;font-family:monospace}.filter-rules-help[data-v-21262a13]{margin-top:8px;padding:12px;background:#f8f9fa;border-radius:8px;border:1px solid #e8e8e8}.help-title[data-v-21262a13]{font-weight:600;font-size:13px;margin:10px 0 6px;color:#333}.help-title[data-v-21262a13]:first-child{margin-top:0}.help-row[data-v-21262a13]{font-size:12px;color:#555;margin:3px 0;line-height:1.6}.help-row code[data-v-21262a13]{background:#eef1f5;padding:1px 5px;border-radius:3px;font-size:11px;font-family:monospace}.help-sample[data-v-21262a13]{background:#1e1e1e;color:#d4d4d4;padding:10px 14px;border-radius:6px;font-size:12px;line-height:1.6;overflow-x:auto;white-space:pre;margin:6px 0 0;font-family:monospace}.help-preview-row[data-v-21262a13]{font-size:13px;margin:4px 0;display:flex;align-items:center;gap:6px}.help-preview-label[data-v-21262a13]{color:#888;min-width:70px;font-size:12px}.help-preview-original[data-v-21262a13]{color:#e74c3c}.help-preview-filtered[data-v-21262a13]{color:#27ae60;font-weight:500}.filter-input-row[data-v-21262a13]{display:flex;gap:8px;width:100%;margin-bottom:8px}.filter-tag-list[data-v-21262a13]{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}.filter-empty[data-v-21262a13]{font-size:13px;color:#c0c4cc;padding:8px 0;margin-bottom:8px}.db-status-grid[data-v-21262a13]{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px;margin-top:8px}.db-stat-item[data-v-21262a13]{background:#f8f9fa;border-radius:10px;padding:16px 12px;text-align:center;border:1px solid #eee;transition:transform .15s,box-shadow .15s}.db-stat-item[data-v-21262a13]:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0000000f}.db-stat-value[data-v-21262a13]{white-space:nowrap;font-size:24px;font-weight:700;color:#303133;margin-bottom:4px}.db-stat-value.text-success[data-v-21262a13]{color:#67c23a}.db-stat-value.text-warning[data-v-21262a13]{color:#e6a23c}.db-stat-label[data-v-21262a13]{font-size:12px;color:#909399}@media (max-width: 900px){.strategy-grid[data-v-21262a13]{grid-template-columns:1fr 1fr}}@media (max-width: 600px){.strategy-grid[data-v-21262a13]{grid-template-columns:1fr}}.pansou-status-grid[data-v-21262a13]{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px;margin-top:8px}.status-dot[data-v-21262a13]{width:8px;height:8px;border-radius:50%;display:inline-block}.dot-ok[data-v-21262a13]{background:#67c23a}.dot-err[data-v-21262a13]{background:#f56c6c}.event-card.active[data-v-21262a13]{border-color:var(--el-color-primary)!important;background:var(--el-color-primary-light-9)}.event-card[data-v-21262a13]{cursor:default}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -21,7 +21,7 @@
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-DT1mRj5t.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CK-9TfWb.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Ekbe64zQ.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -346,9 +346,10 @@ export async function getAllNotifierProviders(): Promise<Record<string, { name:
|
||||
|
||||
export async function testNotifyChannel(
|
||||
channelType: string,
|
||||
configId?: number
|
||||
configId?: number,
|
||||
params?: Record<string, any>
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
const { data } = await api.post('/admin/notify/test', { channelType, configId })
|
||||
const { data } = await api.post('/admin/notify/test', { channelType, configId, params })
|
||||
return data
|
||||
}
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="时间" width="140">
|
||||
<el-table-column label="时间" min-width="155">
|
||||
<template #default="{ row }">
|
||||
<span :title="row.created_at">{{ formatTime(row.created_at) }}</span>
|
||||
</template>
|
||||
@@ -192,6 +192,12 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="推广账号" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.promotion_account || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="状态" width="72" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip :content="statusTip(row.status)" placement="top">
|
||||
|
||||
@@ -158,21 +158,18 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="IP 归属地查询">
|
||||
<div style="display: flex; gap: 8px; align-items: center; width: 100%;">
|
||||
<el-input
|
||||
v-model="configs.ip_geo_api_url"
|
||||
placeholder="https://cn.apihz.cn/api/ip/chaapi.php?id=xxx&key=***&ip={ip}&td=0"
|
||||
style="max-width: 360px"
|
||||
/>
|
||||
<el-button type="primary" :loading="ipGeoTesting" @click="handleTestIpGeo" size="default" style="width: 100px;">
|
||||
<div style="display:flex; flex-direction:column; gap:8px; width:100%;">
|
||||
<el-select v-model="configs.ip_geo_provider" placeholder="选择接口" style="max-width:260px">
|
||||
<el-option label="接口盒子 (apihz.cn)" value="apihz" />
|
||||
</el-select>
|
||||
<template v-if="configs.ip_geo_provider === 'apihz'">
|
||||
<el-input v-model="configs.ip_geo_api_id" placeholder="API ID(如:10014356)" style="max-width:360px" />
|
||||
<el-input v-model="configs.ip_geo_api_key" placeholder="API Key" type="password" show-password style="max-width:360px" />
|
||||
</template>
|
||||
<el-button type="primary" :loading="ipGeoTesting" @click="handleTestIpGeo" size="small" style="width:100px">
|
||||
{{ ipGeoTesting ? "测试中..." : "验证接口" }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="form-tip" style="margin-top: 4px;">
|
||||
IP 归属地查询 API 地址,<code>{ip}</code> 会被替换为实际 IP。
|
||||
</div>
|
||||
<div style="color: var(--el-color-warning); font-size: 13px; margin-top: 2px; width: 100%;">
|
||||
⚠ 当前仅支持 接口盒子(apihz.cn) 格式
|
||||
<div class="form-tip">用于查询用户 IP 归属地(搜索/转存记录中显示)。留空则不做 IP 归属地查询。</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-divider content-position="left">Redis 缓存</el-divider>
|
||||
@@ -522,22 +519,50 @@
|
||||
</div>
|
||||
</div>
|
||||
<el-divider content-position="left">全局事件开关</el-divider>
|
||||
<div style="display:flex; flex-direction:column; gap:6px;">
|
||||
<div style="display:flex; align-items:center; gap:8px;">
|
||||
<el-switch v-model="globalNotifyForm.events.on_save_success" active-text="转存成功" />
|
||||
<el-button size="small" text type="primary" @click="openTemplateEditor('on_save_success')">✏️ 编辑模板</el-button>
|
||||
<div style="display:grid; grid-template-columns:repeat(2,1fr); gap:10px;">
|
||||
<div class="event-card" :class="{ active: globalNotifyForm.events.on_save_success }" style="padding:10px 14px; border-radius:8px; border:1px solid var(--el-border-color-light); transition:all .2s;">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between;">
|
||||
<span style="display:flex; align-items:center; gap:6px; font-size:14px; font-weight:500;">
|
||||
<span>✅</span> 转存成功
|
||||
</span>
|
||||
<el-switch v-model="globalNotifyForm.events.on_save_success" size="small" />
|
||||
</div>
|
||||
<div style="margin-top:4px;">
|
||||
<el-button size="small" text type="primary" @click="openTemplateEditor('on_save_success')">✏️ 编辑模板</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; align-items:center; gap:8px;">
|
||||
<el-switch v-model="globalNotifyForm.events.on_save_fail" active-text="转存失败" />
|
||||
<el-button size="small" text type="primary" @click="openTemplateEditor('on_save_fail')">✏️ 编辑模板</el-button>
|
||||
<div class="event-card" :class="{ active: globalNotifyForm.events.on_save_fail }" style="padding:10px 14px; border-radius:8px; border:1px solid var(--el-border-color-light); transition:all .2s;">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between;">
|
||||
<span style="display:flex; align-items:center; gap:6px; font-size:14px; font-weight:500;">
|
||||
<span>⚠️</span> 转存失败
|
||||
</span>
|
||||
<el-switch v-model="globalNotifyForm.events.on_save_fail" size="small" />
|
||||
</div>
|
||||
<div style="margin-top:4px;">
|
||||
<el-button size="small" text type="primary" @click="openTemplateEditor('on_save_fail')">✏️ 编辑模板</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; align-items:center; gap:8px;">
|
||||
<el-switch v-model="globalNotifyForm.events.on_cookie_expire" active-text="Cookie过期" />
|
||||
<el-button size="small" text type="primary" @click="openTemplateEditor('on_cookie_expire')">✏️ 编辑模板</el-button>
|
||||
<div class="event-card" :class="{ active: globalNotifyForm.events.on_cookie_expire }" style="padding:10px 14px; border-radius:8px; border:1px solid var(--el-border-color-light); transition:all .2s;">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between;">
|
||||
<span style="display:flex; align-items:center; gap:6px; font-size:14px; font-weight:500;">
|
||||
<span>🍪</span> Cookie过期
|
||||
</span>
|
||||
<el-switch v-model="globalNotifyForm.events.on_cookie_expire" size="small" />
|
||||
</div>
|
||||
<div style="margin-top:4px;">
|
||||
<el-button size="small" text type="primary" @click="openTemplateEditor('on_cookie_expire')">✏️ 编辑模板</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; align-items:center; gap:8px;">
|
||||
<el-switch v-model="globalNotifyForm.events.on_cleanup" active-text="清理完成" />
|
||||
<el-button size="small" text type="primary" @click="openTemplateEditor('on_cleanup')">✏️ 编辑模板</el-button>
|
||||
<div class="event-card" :class="{ active: globalNotifyForm.events.on_cleanup }" style="padding:10px 14px; border-radius:8px; border:1px solid var(--el-border-color-light); transition:all .2s;">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between;">
|
||||
<span style="display:flex; align-items:center; gap:6px; font-size:14px; font-weight:500;">
|
||||
<span>🧹</span> 清理完成
|
||||
</span>
|
||||
<el-switch v-model="globalNotifyForm.events.on_cleanup" size="small" />
|
||||
</div>
|
||||
<div style="margin-top:4px;">
|
||||
<el-button size="small" text type="primary" @click="openTemplateEditor('on_cleanup')">✏️ 编辑模板</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-tip" style="margin-top:8px;">全局推送作为兜底通道。设置了推送用户的网盘配置走用户推送,未设置的走全局推送。</div>
|
||||
@@ -683,6 +708,12 @@
|
||||
<el-switch v-model="dailyReportForm.includeUsers" active-text="用户数" :disabled="!dailyReportForm.enabled" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="推送通道">
|
||||
<el-select v-model="dailyReportForm.channels" multiple placeholder="留空=全部全局通道" :disabled="!dailyReportForm.enabled" style="width:100%;max-width:480px">
|
||||
<el-option v-for="(np, nk) in notifyProviders" :key="nk" :label="np.label" :value="nk" />
|
||||
</el-select>
|
||||
<div class="form-tip">留空则发送至全部已启用的全局通道</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="上次发送">
|
||||
<span>{{ dailyReportLastRun || '从未发送' }}</span>
|
||||
</el-form-item>
|
||||
@@ -797,7 +828,6 @@ const searchAllChannels = computed({
|
||||
set: (val: boolean) => { configs.search_all_channels = val ? 'true' : 'false' },
|
||||
})
|
||||
|
||||
const autoUpdateEnabled = computed({
|
||||
get: () => String(configs.auto_update_enabled) === 'true',
|
||||
set: (val: boolean) => { configs.auto_update_enabled = val ? 'true' : 'false' },
|
||||
})
|
||||
@@ -812,6 +842,7 @@ const dailyReportForm = reactive({
|
||||
includeSaves: true,
|
||||
includeStorage: true,
|
||||
includeUsers: true,
|
||||
channels: [] as string[],
|
||||
})
|
||||
const dailyReportPreviewing = ref(false)
|
||||
const dailyReportSending = ref(false)
|
||||
@@ -1187,7 +1218,12 @@ async function testGlobalChannel(channelName: string) {
|
||||
if (!ch || !ch._enabled) return
|
||||
ch._testing = true
|
||||
try {
|
||||
const result = await testNotifyChannel(channelName)
|
||||
// 过滤掉前端标记字段,只传实际参数
|
||||
const params: Record<string, any> = {}
|
||||
for (const [k, v] of Object.entries(ch)) {
|
||||
if (!k.startsWith('_')) params[k] = v
|
||||
}
|
||||
const result = await testNotifyChannel(channelName, undefined, params)
|
||||
if (result.success) {
|
||||
ElMessage.success(result.message)
|
||||
} else {
|
||||
@@ -1407,12 +1443,12 @@ async function handleTestProxy() {
|
||||
async function handleTestIpGeo() {
|
||||
ipGeoTesting.value = true
|
||||
try {
|
||||
const url = String(configs.ip_geo_api_url || "")
|
||||
if (!url) {
|
||||
ElMessage.warning("请先输入 IP 归属地查询 API 地址")
|
||||
const apiId = String(configs.ip_geo_api_id || "")
|
||||
if (!apiId) {
|
||||
ElMessage.warning("请先输入 API ID")
|
||||
return
|
||||
}
|
||||
const result = await testExternalService({ type: "ip_geo", url })
|
||||
const result = await testExternalService({ type: "ip_geo", url: apiId })
|
||||
if (result.ok) {
|
||||
ElMessage.success("✅ IP 归属地接口可用 — " + result.info)
|
||||
} else {
|
||||
@@ -1993,5 +2029,14 @@ async function handleRemoveLogo() {
|
||||
.dot-ok { background: #67c23a; }
|
||||
.dot-err { background: #f56c6c; }
|
||||
|
||||
/* 事件开关卡片高亮 */
|
||||
.event-card.active {
|
||||
border-color: var(--el-color-primary) !important;
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
.event-card {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import{x as qu,m as $_,h as Ce,B as Hg,d as q_,o as K_,a as Tt,c as Nt,K as ml,L as _l,b as Q,F as Vr,r as Gr,f as $t,w as qt,e as Oe,v as Ka,j as Hr,i as Q_,t as wt,n as Nc,y as Ei,l as zc,p as J_,E as j_,u as t1}from"./index-DT1mRj5t.js";import{a as e1,h as r1,c as i1,i as n1,j as a1,t as o1,_ as s1}from"./_plugin-vue_export-helper-1Z-znrfZ.js";import l1 from"./CloudConfig-CgJ44DuS.js";import u1 from"./SystemConfig-BeJ_jebO.js";import f1 from"./SaveRecords-CZh2kRyW.js";import"./index-Bn7NwETH.js";import"./CloudBadge-BEGriYUm.js";/*! *****************************************************************************
|
||||
import{x as qu,m as $_,h as Ce,B as Hg,d as q_,o as K_,a as Tt,c as Nt,K as ml,L as _l,b as Q,F as Vr,r as Gr,f as $t,w as qt,e as Oe,v as Ka,j as Hr,i as Q_,t as wt,n as Nc,y as Ei,l as zc,p as J_,E as j_,u as t1}from"./index-CK-9TfWb.js";import{a as e1,h as r1,c as i1,i as n1,j as a1,t as o1,_ as s1}from"./_plugin-vue_export-helper-D4DENoBS.js";import l1 from"./CloudConfig-vPJUzF1U.js";import u1 from"./SystemConfig-tnevz2yA.js";import f1 from"./SaveRecords-BcXS42JB.js";import"./index-Bn7NwETH.js";import"./CloudBadge-OCEQ2GP-.js";/*! *****************************************************************************
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
@@ -1 +1 @@
|
||||
import{d as B,o as N,a as V,c as I,b as n,t as c,f as e,w as t,h as g,v as y,j as u,k as r,C as M,l,D as T,G as j,H as q,I as z,J as A,u as D,z as H}from"./index-DT1mRj5t.js";import{a as L,_ as R}from"./_plugin-vue_export-helper-1Z-znrfZ.js";const E={class:"admin-layout"},G={class:"admin-sidebar"},J={class:"sidebar-brand"},W={class:"sidebar-brand-text"},F={class:"sidebar-version"},K={class:"admin-content"},O={class:"content-header"},P={class:"content-breadcrumb"},Q={class:"breadcrumb-current"},U={class:"content-actions"},X={class:"content-body"},Y=B({__name:"AdminLayout",setup(Z){const d=D(),f=H(),m=g(""),_=g(""),b={dashboard:"仪表盘","cloud-configs-toggle":"网盘设置及授权","cloud-configs-cleanup":"存储清理","sys-site":"网站设置","sys-services":"外部服务 & 缓存","sys-strategy":"性能配置","sys-password":"修改管理员密码","sys-notify":"消息推送","sys-daily-report":"每日汇报","save-records":"转存日志"},p=y(()=>{const o=f.name;return o==="admin-cloud-configs"?"cloud-configs-toggle":o==="admin-cleanup"?"cloud-configs-cleanup":o==="admin-system"?f.query.section||"sys-site":o==="admin-save-records"?"save-records":"dashboard"}),x=y(()=>b[p.value]||"仪表盘");function w(o){o==="dashboard"?d.push("/admin/dashboard"):o==="cloud-configs-toggle"?d.push("/admin/cloud-configs"):o==="cloud-configs-cleanup"?d.push("/admin/cleanup"):o.startsWith("sys-")?d.push({path:"/admin/system",query:{section:o}}):o==="save-records"?d.push("/admin/save-records"):o==="logout"&&(localStorage.removeItem("admin_token"),d.push("/admin/login"))}function h(){d.push("/")}return N(async()=>{try{const o=await L();m.value=o.site_name||""}catch{}try{const s=await(await fetch("/health")).json();_.value=s.version}catch{}}),(o,s)=>{const i=u("el-icon"),a=u("el-menu-item"),v=u("el-sub-menu"),C=u("el-menu"),k=u("el-button"),S=u("router-view");return V(),I("div",E,[n("aside",G,[n("div",J,[s[1]||(s[1]=n("div",{class:"sidebar-logo"},"☁️",-1)),n("div",W,[n("h2",null,c(m.value||"CloudSearch"),1),s[0]||(s[0]=n("p",null,"管理控制台",-1))])]),e(C,{"default-active":p.value,class:"sidebar-menu",onSelect:w},{default:t(()=>[e(a,{index:"dashboard"},{default:t(()=>[e(i,null,{default:t(()=>[e(r(M))]),_:1}),s[2]||(s[2]=n("span",null,"仪表盘",-1))]),_:1}),e(v,{index:"cloud-configs"},{title:t(()=>[e(i,null,{default:t(()=>[e(r(T))]),_:1}),s[3]||(s[3]=n("span",null,"网盘管理",-1))]),default:t(()=>[e(a,{index:"cloud-configs-toggle"},{default:t(()=>[...s[4]||(s[4]=[l("📋 设置及授权",-1)])]),_:1}),e(a,{index:"cloud-configs-cleanup"},{default:t(()=>[...s[5]||(s[5]=[l("🧹 存储清理",-1)])]),_:1})]),_:1}),e(v,{index:"system"},{title:t(()=>[e(i,null,{default:t(()=>[e(r(j))]),_:1}),s[6]||(s[6]=n("span",null,"系统设置",-1))]),default:t(()=>[e(a,{index:"sys-site"},{default:t(()=>[...s[7]||(s[7]=[l("🌐 网站设置",-1)])]),_:1}),e(a,{index:"sys-services"},{default:t(()=>[...s[8]||(s[8]=[l("🔗 外部服务 & 缓存",-1)])]),_:1}),e(a,{index:"sys-strategy"},{default:t(()=>[...s[9]||(s[9]=[l("⚡ 性能配置",-1)])]),_:1}),e(a,{index:"sys-password"},{default:t(()=>[...s[10]||(s[10]=[l("🔑 修改密码",-1)])]),_:1}),e(a,{index:"sys-notify"},{default:t(()=>[...s[11]||(s[11]=[l("📬 消息推送",-1)])]),_:1}),e(a,{index:"sys-daily-report"},{default:t(()=>[...s[12]||(s[12]=[l("📊 每日汇报",-1)])]),_:1})]),_:1}),e(a,{index:"save-records"},{default:t(()=>[e(i,null,{default:t(()=>[e(r(q))]),_:1}),s[13]||(s[13]=n("span",null,"转存日志",-1))]),_:1}),s[15]||(s[15]=n("div",{class:"sidebar-spacer"},null,-1)),n("div",F,"v"+c(_.value),1),e(a,{index:"logout"},{default:t(()=>[e(i,null,{default:t(()=>[e(r(z))]),_:1}),s[14]||(s[14]=n("span",null,"退出登录",-1))]),_:1})]),_:1},8,["default-active"])]),n("div",K,[n("header",O,[n("div",P,[n("span",Q,c(x.value),1)]),n("div",U,[e(k,{text:"",size:"small",onClick:h},{default:t(()=>[e(i,null,{default:t(()=>[e(r(A))]),_:1}),s[16]||(s[16]=l(" 返回前台 ",-1))]),_:1})])]),n("main",X,[e(S)])])])}}}),es=R(Y,[["__scopeId","data-v-647abf08"]]);export{es as default};
|
||||
import{d as B,o as N,a as V,c as I,b as n,t as c,f as e,w as t,h as g,v as y,j as u,k as r,C as M,l,D as T,G as j,H as q,I as z,J as A,u as D,z as H}from"./index-CK-9TfWb.js";import{a as L,_ as R}from"./_plugin-vue_export-helper-D4DENoBS.js";const E={class:"admin-layout"},G={class:"admin-sidebar"},J={class:"sidebar-brand"},W={class:"sidebar-brand-text"},F={class:"sidebar-version"},K={class:"admin-content"},O={class:"content-header"},P={class:"content-breadcrumb"},Q={class:"breadcrumb-current"},U={class:"content-actions"},X={class:"content-body"},Y=B({__name:"AdminLayout",setup(Z){const d=D(),f=H(),m=g(""),_=g(""),b={dashboard:"仪表盘","cloud-configs-toggle":"网盘设置及授权","cloud-configs-cleanup":"存储清理","sys-site":"网站设置","sys-services":"外部服务 & 缓存","sys-strategy":"性能配置","sys-password":"修改管理员密码","sys-notify":"消息推送","sys-daily-report":"每日汇报","save-records":"转存日志"},p=y(()=>{const o=f.name;return o==="admin-cloud-configs"?"cloud-configs-toggle":o==="admin-cleanup"?"cloud-configs-cleanup":o==="admin-system"?f.query.section||"sys-site":o==="admin-save-records"?"save-records":"dashboard"}),x=y(()=>b[p.value]||"仪表盘");function w(o){o==="dashboard"?d.push("/admin/dashboard"):o==="cloud-configs-toggle"?d.push("/admin/cloud-configs"):o==="cloud-configs-cleanup"?d.push("/admin/cleanup"):o.startsWith("sys-")?d.push({path:"/admin/system",query:{section:o}}):o==="save-records"?d.push("/admin/save-records"):o==="logout"&&(localStorage.removeItem("admin_token"),d.push("/admin/login"))}function h(){d.push("/")}return N(async()=>{try{const o=await L();m.value=o.site_name||""}catch{}try{const s=await(await fetch("/health")).json();_.value=s.version}catch{}}),(o,s)=>{const i=u("el-icon"),a=u("el-menu-item"),v=u("el-sub-menu"),C=u("el-menu"),k=u("el-button"),S=u("router-view");return V(),I("div",E,[n("aside",G,[n("div",J,[s[1]||(s[1]=n("div",{class:"sidebar-logo"},"☁️",-1)),n("div",W,[n("h2",null,c(m.value||"CloudSearch"),1),s[0]||(s[0]=n("p",null,"管理控制台",-1))])]),e(C,{"default-active":p.value,class:"sidebar-menu",onSelect:w},{default:t(()=>[e(a,{index:"dashboard"},{default:t(()=>[e(i,null,{default:t(()=>[e(r(M))]),_:1}),s[2]||(s[2]=n("span",null,"仪表盘",-1))]),_:1}),e(v,{index:"cloud-configs"},{title:t(()=>[e(i,null,{default:t(()=>[e(r(T))]),_:1}),s[3]||(s[3]=n("span",null,"网盘管理",-1))]),default:t(()=>[e(a,{index:"cloud-configs-toggle"},{default:t(()=>[...s[4]||(s[4]=[l("📋 设置及授权",-1)])]),_:1}),e(a,{index:"cloud-configs-cleanup"},{default:t(()=>[...s[5]||(s[5]=[l("🧹 存储清理",-1)])]),_:1})]),_:1}),e(v,{index:"system"},{title:t(()=>[e(i,null,{default:t(()=>[e(r(j))]),_:1}),s[6]||(s[6]=n("span",null,"系统设置",-1))]),default:t(()=>[e(a,{index:"sys-site"},{default:t(()=>[...s[7]||(s[7]=[l("🌐 网站设置",-1)])]),_:1}),e(a,{index:"sys-services"},{default:t(()=>[...s[8]||(s[8]=[l("🔗 外部服务 & 缓存",-1)])]),_:1}),e(a,{index:"sys-strategy"},{default:t(()=>[...s[9]||(s[9]=[l("⚡ 性能配置",-1)])]),_:1}),e(a,{index:"sys-password"},{default:t(()=>[...s[10]||(s[10]=[l("🔑 修改密码",-1)])]),_:1}),e(a,{index:"sys-notify"},{default:t(()=>[...s[11]||(s[11]=[l("📬 消息推送",-1)])]),_:1}),e(a,{index:"sys-daily-report"},{default:t(()=>[...s[12]||(s[12]=[l("📊 每日汇报",-1)])]),_:1})]),_:1}),e(a,{index:"save-records"},{default:t(()=>[e(i,null,{default:t(()=>[e(r(q))]),_:1}),s[13]||(s[13]=n("span",null,"转存日志",-1))]),_:1}),s[15]||(s[15]=n("div",{class:"sidebar-spacer"},null,-1)),n("div",F,"v"+c(_.value),1),e(a,{index:"logout"},{default:t(()=>[e(i,null,{default:t(()=>[e(r(z))]),_:1}),s[14]||(s[14]=n("span",null,"退出登录",-1))]),_:1})]),_:1},8,["default-active"])]),n("div",K,[n("header",O,[n("div",P,[n("span",Q,c(x.value),1)]),n("div",U,[e(k,{text:"",size:"small",onClick:h},{default:t(()=>[e(i,null,{default:t(()=>[e(r(A))]),_:1}),s[16]||(s[16]=l(" 返回前台 ",-1))]),_:1})])]),n("main",X,[e(S)])])])}}}),es=R(Y,[["__scopeId","data-v-647abf08"]]);export{es as default};
|
||||
@@ -1 +1 @@
|
||||
import{d as k,o as C,a as w,c as y,b as a,t as m,f as t,w as i,g as x,e as L,h as d,j as p,l as N,i as S,E as B}from"./index-DT1mRj5t.js";import{a as E,d as M,_ as U}from"./_plugin-vue_export-helper-1Z-znrfZ.js";const j={class:"admin-login-page"},q={class:"login-card"},A={class:"login-brand"},I={class:"login-title"},K={key:0,class:"error-msg"},R={class:"login-footer"},z=k({__name:"AdminLogin",setup(D){const f=d(),u=d(!1),c=d(""),g=d(""),v=d("");E().then(l=>{l.site_name&&(g.value=l.site_name)}).catch(()=>{});const s=S({username:"",password:""}),b={username:[{required:!0,message:"请输入用户名",trigger:"blur"}],password:[{required:!0,message:"请输入密码",trigger:"blur"}]};async function h(){var e,r,n;if(await((e=f.value)==null?void 0:e.validate().catch(()=>!1))){u.value=!0,c.value="";try{const o=await M(s.username,s.password);localStorage.setItem("admin_token",o.token),B.success("登录成功"),window.location.href="/admin"}catch(o){c.value=((n=(r=o==null?void 0:o.response)==null?void 0:r.data)==null?void 0:n.message)||(o==null?void 0:o.message)||"登录失败"}finally{u.value=!1}}}return C(async()=>{try{const e=await(await fetch("/health")).json();v.value=e.version||""}catch{}}),(l,e)=>{const r=p("el-input"),n=p("el-form-item"),o=p("el-button"),V=p("el-form");return w(),y("div",j,[e[4]||(e[4]=a("div",{class:"login-bg-pattern"},null,-1)),a("div",q,[a("div",A,[e[2]||(e[2]=a("div",{class:"login-logo"},"☁️",-1)),a("h1",I,m(g.value||"CloudSearch"),1),e[3]||(e[3]=a("p",{class:"login-subtitle"},"管理后台",-1))]),t(V,{ref_key:"formRef",ref:f,model:s,rules:b,"label-width":"0",size:"large",onKeyup:x(h,["enter"])},{default:i(()=>[t(n,{prop:"username"},{default:i(()=>[t(r,{modelValue:s.username,"onUpdate:modelValue":e[0]||(e[0]=_=>s.username=_),placeholder:"用户名","prefix-icon":"User"},null,8,["modelValue"])]),_:1}),t(n,{prop:"password"},{default:i(()=>[t(r,{modelValue:s.password,"onUpdate:modelValue":e[1]||(e[1]=_=>s.password=_),type:"password",placeholder:"密码","prefix-icon":"Lock","show-password":""},null,8,["modelValue"])]),_:1}),t(n,null,{default:i(()=>[t(o,{type:"primary",loading:u.value,class:"login-btn",onClick:h},{default:i(()=>[N(m(u.value?"登录中...":"登 录"),1)]),_:1},8,["loading"])]),_:1})]),_:1},8,["model"]),c.value?(w(),y("p",K,m(c.value),1)):L("",!0),a("p",R,"CloudSearch v"+m(v.value),1)])])}}}),G=U(z,[["__scopeId","data-v-bd0b6672"]]);export{G as default};
|
||||
import{d as k,o as C,a as w,c as y,b as a,t as m,f as t,w as i,g as x,e as L,h as d,j as p,l as N,i as S,E as B}from"./index-CK-9TfWb.js";import{a as E,d as M,_ as U}from"./_plugin-vue_export-helper-D4DENoBS.js";const j={class:"admin-login-page"},q={class:"login-card"},A={class:"login-brand"},I={class:"login-title"},K={key:0,class:"error-msg"},R={class:"login-footer"},z=k({__name:"AdminLogin",setup(D){const f=d(),u=d(!1),c=d(""),g=d(""),v=d("");E().then(l=>{l.site_name&&(g.value=l.site_name)}).catch(()=>{});const s=S({username:"",password:""}),b={username:[{required:!0,message:"请输入用户名",trigger:"blur"}],password:[{required:!0,message:"请输入密码",trigger:"blur"}]};async function h(){var e,r,n;if(await((e=f.value)==null?void 0:e.validate().catch(()=>!1))){u.value=!0,c.value="";try{const o=await M(s.username,s.password);localStorage.setItem("admin_token",o.token),B.success("登录成功"),window.location.href="/admin"}catch(o){c.value=((n=(r=o==null?void 0:o.response)==null?void 0:r.data)==null?void 0:n.message)||(o==null?void 0:o.message)||"登录失败"}finally{u.value=!1}}}return C(async()=>{try{const e=await(await fetch("/health")).json();v.value=e.version||""}catch{}}),(l,e)=>{const r=p("el-input"),n=p("el-form-item"),o=p("el-button"),V=p("el-form");return w(),y("div",j,[e[4]||(e[4]=a("div",{class:"login-bg-pattern"},null,-1)),a("div",q,[a("div",A,[e[2]||(e[2]=a("div",{class:"login-logo"},"☁️",-1)),a("h1",I,m(g.value||"CloudSearch"),1),e[3]||(e[3]=a("p",{class:"login-subtitle"},"管理后台",-1))]),t(V,{ref_key:"formRef",ref:f,model:s,rules:b,"label-width":"0",size:"large",onKeyup:x(h,["enter"])},{default:i(()=>[t(n,{prop:"username"},{default:i(()=>[t(r,{modelValue:s.username,"onUpdate:modelValue":e[0]||(e[0]=_=>s.username=_),placeholder:"用户名","prefix-icon":"User"},null,8,["modelValue"])]),_:1}),t(n,{prop:"password"},{default:i(()=>[t(r,{modelValue:s.password,"onUpdate:modelValue":e[1]||(e[1]=_=>s.password=_),type:"password",placeholder:"密码","prefix-icon":"Lock","show-password":""},null,8,["modelValue"])]),_:1}),t(n,null,{default:i(()=>[t(o,{type:"primary",loading:u.value,class:"login-btn",onClick:h},{default:i(()=>[N(m(u.value?"登录中...":"登 录"),1)]),_:1},8,["loading"])]),_:1})]),_:1},8,["model"]),c.value?(w(),y("p",K,m(c.value),1)):L("",!0),a("p",R,"CloudSearch v"+m(v.value),1)])])}}}),G=U(z,[["__scopeId","data-v-bd0b6672"]]);export{G as default};
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{C as s,b as a,a as n}from"./index-Bn7NwETH.js";import{d as l,a as t,c,p as d,k as o,e as r,l as u,t as m}from"./index-DT1mRj5t.js";import{_}from"./_plugin-vue_export-helper-1Z-znrfZ.js";const p=["src"],i=l({__name:"CloudBadge",props:{cloud_type:{},showIcon:{type:Boolean}},setup(e){return(C,y)=>(t(),c("span",{class:"cloud-badge",style:d({background:o(s)[e.cloud_type]})},[e.showIcon&&o(a)[e.cloud_type]?(t(),c("img",{key:0,src:o(a)[e.cloud_type],class:"badge-icon"},null,8,p)):r("",!0),u(" "+m(o(n)[e.cloud_type]),1)],4))}}),L=_(i,[["__scopeId","data-v-9106805f"]]);export{L as C};
|
||||
import{C as s,b as a,a as n}from"./index-Bn7NwETH.js";import{d as l,a as t,c,p as d,k as o,e as r,l as u,t as m}from"./index-CK-9TfWb.js";import{_}from"./_plugin-vue_export-helper-D4DENoBS.js";const p=["src"],i=l({__name:"CloudBadge",props:{cloud_type:{},showIcon:{type:Boolean}},setup(e){return(C,y)=>(t(),c("span",{class:"cloud-badge",style:d({background:o(s)[e.cloud_type]})},[e.showIcon&&o(a)[e.cloud_type]?(t(),c("img",{key:0,src:o(a)[e.cloud_type],class:"badge-icon"},null,8,p)):r("",!0),u(" "+m(o(n)[e.cloud_type]),1)],4))}}),L=_(i,[["__scopeId","data-v-9106805f"]]);export{L as C};
|
||||
@@ -1,4 +1,4 @@
|
||||
import{d as ke,o as L,m as ve,E as _,a as c,c as k,f as n,w as a,b as r,h as C,j as p,i as be,F as R,r as K,t as v,y as g,l as d,e as A,k as Ce,M as he,p as xe,n as H,K as Be,L as Te,v as h}from"./index-DT1mRj5t.js";import{a as x}from"./index-Bn7NwETH.js";import{c as we,k as Fe,h as Ne,l as G,t as Ve,u as P,m as ze,n as Se,o as $e,_ as Ue}from"./_plugin-vue_export-helper-1Z-znrfZ.js";import{C as De}from"./CloudBadge-BEGriYUm.js";const Ie={class:"cloud-config"},Me={class:"cloud-toggle-grid"},Oe=["src"],qe={class:"cloud-label"},Ee={class:"toolbar"},Le={key:0,class:"nickname-text"},Re={key:0,class:"promotion-text"},Ke={key:0,class:"uid-cell"},Ae={key:0,class:"verifying"},He={key:0,class:"storage-cell"},Ge={class:"storage-bar-wrap"},Pe={class:"storage-text"},je={class:"storage-used"},Je={class:"storage-total"},Qe={class:"storage-free"},We={key:0,class:"save-count"},Xe={style:{"line-height":"1.6"}},Ye={class:"cookie-tips-header"},Ze={class:"cookie-tips-title"},et=["innerHTML"],tt=ke({__name:"CloudConfig",setup(ot){const z=C([]),D=C(),F=C([]),B=C(!1),T=C(!1),b=C(null),l=be({cloud_type:"",nickname:"",promotion_account:"",is_transfer_enabled:!1,cookie:"",_verifying:!1,_storageUsed:"",_storageTotal:""}),j=h(()=>({cloud_type:[{required:!0,message:"请选择网盘类型",trigger:"change"}],nickname:[{required:!1,message:"请填写昵称(区分多个同类型网盘)",trigger:"blur"}],promotion_account:[{required:!0,message:"请填写推广平台及账号",trigger:"blur"}]})),J=h(()=>Object.entries(x)),Q=h(()=>{if(!l.cloud_type)return"请先选择网盘类型";const t=l.cloud_type;return t==="quark"||t==="baidu"?`请输入 ${x[t]||t} 的完整 Cookie`:b.value?"留空则保持原有":"输入完整 Cookie"}),W=h(()=>x[l.cloud_type]||l.cloud_type||""),X=h(()=>{const t=l.cloud_type;return t?{quark:`<li>在电脑上打开 <a href="https://pan.quark.cn" target="_blank">pan.quark.cn</a> 并登录你的夸克账号</li>
|
||||
import{d as ke,o as L,m as ve,E as _,a as c,c as k,f as n,w as a,b as r,h as C,j as p,i as be,F as R,r as K,t as v,y as g,l as d,e as A,k as Ce,M as he,p as xe,n as H,K as Be,L as Te,v as h}from"./index-CK-9TfWb.js";import{a as x}from"./index-Bn7NwETH.js";import{c as we,k as Fe,h as Ne,l as G,t as Ve,u as P,m as ze,n as Se,o as $e,_ as Ue}from"./_plugin-vue_export-helper-D4DENoBS.js";import{C as De}from"./CloudBadge-OCEQ2GP-.js";const Ie={class:"cloud-config"},Me={class:"cloud-toggle-grid"},Oe=["src"],qe={class:"cloud-label"},Ee={class:"toolbar"},Le={key:0,class:"nickname-text"},Re={key:0,class:"promotion-text"},Ke={key:0,class:"uid-cell"},Ae={key:0,class:"verifying"},He={key:0,class:"storage-cell"},Ge={class:"storage-bar-wrap"},Pe={class:"storage-text"},je={class:"storage-used"},Je={class:"storage-total"},Qe={class:"storage-free"},We={key:0,class:"save-count"},Xe={style:{"line-height":"1.6"}},Ye={class:"cookie-tips-header"},Ze={class:"cookie-tips-title"},et=["innerHTML"],tt=ke({__name:"CloudConfig",setup(ot){const z=C([]),D=C(),F=C([]),B=C(!1),T=C(!1),b=C(null),l=be({cloud_type:"",nickname:"",promotion_account:"",is_transfer_enabled:!1,cookie:"",_verifying:!1,_storageUsed:"",_storageTotal:""}),j=h(()=>({cloud_type:[{required:!0,message:"请选择网盘类型",trigger:"change"}],nickname:[{required:!1,message:"请填写昵称(区分多个同类型网盘)",trigger:"blur"}],promotion_account:[{required:!0,message:"请填写推广平台及账号",trigger:"blur"}]})),J=h(()=>Object.entries(x)),Q=h(()=>{if(!l.cloud_type)return"请先选择网盘类型";const t=l.cloud_type;return t==="quark"||t==="baidu"?`请输入 ${x[t]||t} 的完整 Cookie`:b.value?"留空则保持原有":"输入完整 Cookie"}),W=h(()=>x[l.cloud_type]||l.cloud_type||""),X=h(()=>{const t=l.cloud_type;return t?{quark:`<li>在电脑上打开 <a href="https://pan.quark.cn" target="_blank">pan.quark.cn</a> 并登录你的夸克账号</li>
|
||||
<li>按 <code>F12</code> 打开开发者工具 → 切换到 <strong>网络 (Network)</strong> 选项卡</li>
|
||||
<li>刷新页面,在请求列表中点击任意一个请求(如 <code>account/info</code>)</li>
|
||||
<li>在右侧 <strong>请求头 (Request Headers)</strong> 中找到 <code>Cookie</code> 字段</li>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
source_clean/frontend/assets/SaveRecords-BBwQkCBh.css
Normal file
1
source_clean/frontend/assets/SaveRecords-BBwQkCBh.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.el-card[data-v-1e3ebf03]{margin-bottom:20px}.el-card[data-v-1e3ebf03] .el-card__header{font-weight:600;font-size:15px}[data-v-1e3ebf03] .el-divider__text.is-left{left:0;padding-left:0}.form-tip[data-v-1e3ebf03]{font-size:12px;color:#909399;margin-top:4px}.fallback-upload-wrap[data-v-1e3ebf03]{display:flex;flex-direction:column;gap:12px}.fallback-upload-row[data-v-1e3ebf03]{display:flex;align-items:center;flex-wrap:wrap}.fallback-preview[data-v-1e3ebf03]{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.fallback-preview img[data-v-1e3ebf03]{max-width:100%;height:auto;max-height:120px;border-radius:8px;border:1px solid var(--border-color);background:#f0f0f0;object-fit:contain}.strategy-grid[data-v-1e3ebf03]{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px 16px}.grid-cell[data-v-1e3ebf03]{display:flex;flex-direction:column;gap:4px}.strategy-section[data-v-1e3ebf03]{padding:0 4px}.field-block[data-v-1e3ebf03]{margin:12px 0}.field-label-row[data-v-1e3ebf03]{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.field-label[data-v-1e3ebf03]{font-size:14px;font-weight:500;color:#303133;white-space:nowrap}.field-desc[data-v-1e3ebf03]{font-size:12px;color:#909399;margin:3px 0 0;line-height:1.5}.keyword-input-row[data-v-1e3ebf03]{display:flex;gap:8px;flex:1;min-width:200px}.tag-list[data-v-1e3ebf03]{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.tag-empty[data-v-1e3ebf03]{font-size:13px;color:#c0c4cc;margin-top:8px}.filter-rule-help[data-v-1e3ebf03]{margin-top:8px;padding:10px 12px;background:#f8f9fa;border-radius:8px;border:1px solid #e8e8e8}.filter-rule-help .help-title[data-v-1e3ebf03]{font-weight:600;font-size:13px;margin:8px 0 4px;color:#333}.filter-rule-help .help-title[data-v-1e3ebf03]:first-child{margin-top:0}.filter-rule-help .help-row[data-v-1e3ebf03]{font-size:12px;color:#555;margin:3px 0;line-height:1.6}.filter-rule-help .help-row code[data-v-1e3ebf03]{background:#eef1f5;padding:1px 5px;border-radius:3px;font-size:11px;font-family:monospace}.filter-rules-help[data-v-1e3ebf03]{margin-top:8px;padding:12px;background:#f8f9fa;border-radius:8px;border:1px solid #e8e8e8}.help-title[data-v-1e3ebf03]{font-weight:600;font-size:13px;margin:10px 0 6px;color:#333}.help-title[data-v-1e3ebf03]:first-child{margin-top:0}.help-row[data-v-1e3ebf03]{font-size:12px;color:#555;margin:3px 0;line-height:1.6}.help-row code[data-v-1e3ebf03]{background:#eef1f5;padding:1px 5px;border-radius:3px;font-size:11px;font-family:monospace}.help-sample[data-v-1e3ebf03]{background:#1e1e1e;color:#d4d4d4;padding:10px 14px;border-radius:6px;font-size:12px;line-height:1.6;overflow-x:auto;white-space:pre;margin:6px 0 0;font-family:monospace}.help-preview-row[data-v-1e3ebf03]{font-size:13px;margin:4px 0;display:flex;align-items:center;gap:6px}.help-preview-label[data-v-1e3ebf03]{color:#888;min-width:70px;font-size:12px}.help-preview-original[data-v-1e3ebf03]{color:#e74c3c}.help-preview-filtered[data-v-1e3ebf03]{color:#27ae60;font-weight:500}.filter-input-row[data-v-1e3ebf03]{display:flex;gap:8px;width:100%;margin-bottom:8px}.filter-tag-list[data-v-1e3ebf03]{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}.filter-empty[data-v-1e3ebf03]{font-size:13px;color:#c0c4cc;padding:8px 0;margin-bottom:8px}.db-status-grid[data-v-1e3ebf03]{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px;margin-top:8px}.db-stat-item[data-v-1e3ebf03]{background:#f8f9fa;border-radius:10px;padding:16px 12px;text-align:center;border:1px solid #eee;transition:transform .15s,box-shadow .15s}.db-stat-item[data-v-1e3ebf03]:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0000000f}.db-stat-value[data-v-1e3ebf03]{white-space:nowrap;font-size:24px;font-weight:700;color:#303133;margin-bottom:4px}.db-stat-value.text-success[data-v-1e3ebf03]{color:#67c23a}.db-stat-value.text-warning[data-v-1e3ebf03]{color:#e6a23c}.db-stat-label[data-v-1e3ebf03]{font-size:12px;color:#909399}@media (max-width: 900px){.strategy-grid[data-v-1e3ebf03]{grid-template-columns:1fr 1fr}}@media (max-width: 600px){.strategy-grid[data-v-1e3ebf03]{grid-template-columns:1fr}}.pansou-status-grid[data-v-1e3ebf03]{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px;margin-top:8px}.status-dot[data-v-1e3ebf03]{width:8px;height:8px;border-radius:50%;display:inline-block}.dot-ok[data-v-1e3ebf03]{background:#67c23a}.dot-err[data-v-1e3ebf03]{background:#f56c6c}
|
||||
1
source_clean/frontend/assets/SystemConfig-B_wrXPYD.css
Normal file
1
source_clean/frontend/assets/SystemConfig-B_wrXPYD.css
Normal file
@@ -0,0 +1 @@
|
||||
.el-card[data-v-21262a13]{margin-bottom:20px}.el-card[data-v-21262a13] .el-card__header{font-weight:600;font-size:15px}[data-v-21262a13] .el-divider__text.is-left{left:0;padding-left:0}.form-tip[data-v-21262a13]{font-size:12px;color:#909399;margin-top:4px}.fallback-upload-wrap[data-v-21262a13]{display:flex;flex-direction:column;gap:12px}.fallback-upload-row[data-v-21262a13]{display:flex;align-items:center;flex-wrap:wrap}.fallback-preview[data-v-21262a13]{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.fallback-preview img[data-v-21262a13]{max-width:100%;height:auto;max-height:120px;border-radius:8px;border:1px solid var(--border-color);background:#f0f0f0;object-fit:contain}.strategy-grid[data-v-21262a13]{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px 16px}.grid-cell[data-v-21262a13]{display:flex;flex-direction:column;gap:4px}.strategy-section[data-v-21262a13]{padding:0 4px}.field-block[data-v-21262a13]{margin:12px 0}.field-label-row[data-v-21262a13]{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.field-label[data-v-21262a13]{font-size:14px;font-weight:500;color:#303133;white-space:nowrap}.field-desc[data-v-21262a13]{font-size:12px;color:#909399;margin:3px 0 0;line-height:1.5}.keyword-input-row[data-v-21262a13]{display:flex;gap:8px;flex:1;min-width:200px}.tag-list[data-v-21262a13]{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.tag-empty[data-v-21262a13]{font-size:13px;color:#c0c4cc;margin-top:8px}.filter-rule-help[data-v-21262a13]{margin-top:8px;padding:10px 12px;background:#f8f9fa;border-radius:8px;border:1px solid #e8e8e8}.filter-rule-help .help-title[data-v-21262a13]{font-weight:600;font-size:13px;margin:8px 0 4px;color:#333}.filter-rule-help .help-title[data-v-21262a13]:first-child{margin-top:0}.filter-rule-help .help-row[data-v-21262a13]{font-size:12px;color:#555;margin:3px 0;line-height:1.6}.filter-rule-help .help-row code[data-v-21262a13]{background:#eef1f5;padding:1px 5px;border-radius:3px;font-size:11px;font-family:monospace}.filter-rules-help[data-v-21262a13]{margin-top:8px;padding:12px;background:#f8f9fa;border-radius:8px;border:1px solid #e8e8e8}.help-title[data-v-21262a13]{font-weight:600;font-size:13px;margin:10px 0 6px;color:#333}.help-title[data-v-21262a13]:first-child{margin-top:0}.help-row[data-v-21262a13]{font-size:12px;color:#555;margin:3px 0;line-height:1.6}.help-row code[data-v-21262a13]{background:#eef1f5;padding:1px 5px;border-radius:3px;font-size:11px;font-family:monospace}.help-sample[data-v-21262a13]{background:#1e1e1e;color:#d4d4d4;padding:10px 14px;border-radius:6px;font-size:12px;line-height:1.6;overflow-x:auto;white-space:pre;margin:6px 0 0;font-family:monospace}.help-preview-row[data-v-21262a13]{font-size:13px;margin:4px 0;display:flex;align-items:center;gap:6px}.help-preview-label[data-v-21262a13]{color:#888;min-width:70px;font-size:12px}.help-preview-original[data-v-21262a13]{color:#e74c3c}.help-preview-filtered[data-v-21262a13]{color:#27ae60;font-weight:500}.filter-input-row[data-v-21262a13]{display:flex;gap:8px;width:100%;margin-bottom:8px}.filter-tag-list[data-v-21262a13]{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}.filter-empty[data-v-21262a13]{font-size:13px;color:#c0c4cc;padding:8px 0;margin-bottom:8px}.db-status-grid[data-v-21262a13]{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px;margin-top:8px}.db-stat-item[data-v-21262a13]{background:#f8f9fa;border-radius:10px;padding:16px 12px;text-align:center;border:1px solid #eee;transition:transform .15s,box-shadow .15s}.db-stat-item[data-v-21262a13]:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0000000f}.db-stat-value[data-v-21262a13]{white-space:nowrap;font-size:24px;font-weight:700;color:#303133;margin-bottom:4px}.db-stat-value.text-success[data-v-21262a13]{color:#67c23a}.db-stat-value.text-warning[data-v-21262a13]{color:#e6a23c}.db-stat-label[data-v-21262a13]{font-size:12px;color:#909399}@media (max-width: 900px){.strategy-grid[data-v-21262a13]{grid-template-columns:1fr 1fr}}@media (max-width: 600px){.strategy-grid[data-v-21262a13]{grid-template-columns:1fr}}.pansou-status-grid[data-v-21262a13]{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px;margin-top:8px}.status-dot[data-v-21262a13]{width:8px;height:8px;border-radius:50%;display:inline-block}.dot-ok[data-v-21262a13]{background:#67c23a}.dot-err[data-v-21262a13]{background:#f56c6c}.event-card.active[data-v-21262a13]{border-color:var(--el-color-primary)!important;background:var(--el-color-primary-light-9)}.event-card[data-v-21262a13]{cursor:default}
|
||||
File diff suppressed because one or more lines are too long
27
source_clean/frontend/assets/SystemConfig-tnevz2yA.js
Normal file
27
source_clean/frontend/assets/SystemConfig-tnevz2yA.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -21,7 +21,7 @@
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-DT1mRj5t.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CK-9TfWb.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Ekbe64zQ.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -48,6 +48,9 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle?
|
||||
const db = getDb();
|
||||
const ipLocation = await lookupIpLocation(ipAddress || '');
|
||||
|
||||
// ── Track if this is a re-save after link was found invalid
|
||||
let retrySave = false;
|
||||
|
||||
// ── Short-term dedup: prevent duplicate saves of the same URL within 60 seconds ──
|
||||
const DEDUP_WINDOW_SEC = 60;
|
||||
let dedupCutoff = '';
|
||||
@@ -58,35 +61,52 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle?
|
||||
dedupCutoff = recentCutoff.cutoff;
|
||||
|
||||
const recentRecord = db.prepare(
|
||||
`SELECT share_url, share_pwd, status, error_message, folder_name, original_folder_name FROM save_records
|
||||
`SELECT share_url, share_pwd, status, file_size, error_message, folder_name, original_folder_name FROM save_records
|
||||
WHERE source_url = ? AND created_at >= ?
|
||||
ORDER BY created_at DESC LIMIT 1`
|
||||
).get(shareUrl, dedupCutoff) as {
|
||||
share_url: string | null; share_pwd: string | null; status: string;
|
||||
share_url: string | null; share_pwd: string | null; status: string; file_size: string | null;
|
||||
error_message: string | null; folder_name: string | null; original_folder_name: string | null;
|
||||
} | undefined;
|
||||
|
||||
if (recentRecord) {
|
||||
const alreadySaved = recentRecord.status === 'success' || recentRecord.status === 'reused';
|
||||
if (alreadySaved && recentRecord.share_url) {
|
||||
console.log(`[Share] 🛡️ Dedup: ${shareUrl} was saved ${DEDUP_WINDOW_SEC}s ago (status=${recentRecord.status}), returning existing share link`);
|
||||
db.prepare(
|
||||
`INSERT INTO save_records (source_type, source_title, source_url, target_cloud, share_url, share_pwd, file_size, file_count, folder_count, duration_ms, status, error_message, folder_name, original_folder_name, ip_address, ip_location, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run(
|
||||
cloudType, sourceTitle || null, shareUrl, cloudType,
|
||||
recentRecord.share_url, recentRecord.share_pwd || null,
|
||||
null, 0, 0, 0, 'reused', null,
|
||||
recentRecord.folder_name || null, recentRecord.original_folder_name || null,
|
||||
ipAddress || null, ipLocation, localTimestamp(),
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
message: `🛡️ 此资源刚在 ${DEDUP_WINDOW_SEC} 秒内转存过,直接返回已有分享链接`,
|
||||
share_url: recentRecord.share_url, shareUrl: recentRecord.share_url,
|
||||
sharePwd: recentRecord.share_pwd || '', folderName: '',
|
||||
file_count: 0, folder_count: 0, duration_ms: 0,
|
||||
};
|
||||
// Validate the cached link before returning — avoid returning an invalid link
|
||||
let dedupLinkInvalid = false;
|
||||
try {
|
||||
const { LinkValidator: DedupLinkValidator } = await import('../validation/link-validator.service');
|
||||
const dedupValidator = new DedupLinkValidator();
|
||||
const dedupValidation = await dedupValidator.validate(recentRecord.share_url, 'quark');
|
||||
if (dedupValidation.status !== 'valid') {
|
||||
dedupLinkInvalid = true;
|
||||
console.log(`[Share] 🛡️ Dedup link invalid (${dedupValidation.message}), falling through to normal save`);
|
||||
retrySave = true;
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.log(`[Share] 🛡️ Dedup validation error: ${err.message}, falling through`);
|
||||
dedupLinkInvalid = true;
|
||||
}
|
||||
if (!dedupLinkInvalid) {
|
||||
console.log(`[Share] 🛡️ Dedup: ${shareUrl} was saved ${DEDUP_WINDOW_SEC}s ago (status=${recentRecord.status}), returning existing share link`);
|
||||
db.prepare(
|
||||
`INSERT INTO save_records (source_type, source_title, source_url, target_cloud, share_url, share_pwd, file_size, file_count, folder_count, duration_ms, status, error_message, folder_name, original_folder_name, ip_address, ip_location, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run(
|
||||
cloudType, sourceTitle || null, shareUrl, cloudType,
|
||||
recentRecord.share_url, recentRecord.share_pwd || null,
|
||||
recentRecord.file_size || null, 0, 0, 0, 'reused', null,
|
||||
recentRecord.folder_name || null, recentRecord.original_folder_name || null,
|
||||
ipAddress || null, ipLocation, localTimestamp(),
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
message: `🛡️ 此资源刚在 ${DEDUP_WINDOW_SEC} 秒内转存过,直接返回已有分享链接`,
|
||||
share_url: recentRecord.share_url, shareUrl: recentRecord.share_url,
|
||||
sharePwd: recentRecord.share_pwd || '', folderName: '',
|
||||
file_count: 0, folder_count: 0, duration_ms: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
@@ -98,10 +118,10 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle?
|
||||
if (reuseEnabled !== 'false') {
|
||||
try {
|
||||
const existing = db.prepare(
|
||||
`SELECT share_url, share_pwd, folder_name, original_folder_name FROM save_records
|
||||
`SELECT share_url, share_pwd, file_size, folder_name, original_folder_name FROM save_records
|
||||
WHERE source_url = ? AND status IN ('success', 'reused') AND share_url IS NOT NULL AND share_url != ''
|
||||
ORDER BY created_at DESC LIMIT 1`
|
||||
).get(shareUrl) as { share_url: string; share_pwd: string; folder_name: string | null; original_folder_name: string | null } | undefined;
|
||||
).get(shareUrl) as { share_url: string; share_pwd: string; file_size: string | null; folder_name: string | null; original_folder_name: string | null } | undefined;
|
||||
|
||||
if (existing?.share_url) {
|
||||
const { LinkValidator } = await import('../validation/link-validator.service');
|
||||
@@ -123,7 +143,7 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle?
|
||||
).run(
|
||||
cloudType, sourceTitle || null, shareUrl, cloudType,
|
||||
existing.share_url, existing.share_pwd || null,
|
||||
null, 0, 0, 0, reuseStatus, null,
|
||||
existing.file_size || null, 0, 0, 0, reuseStatus, null,
|
||||
existing.folder_name || null, existing.original_folder_name || null,
|
||||
ipAddress || null, ipLocation, localTimestamp(),
|
||||
);
|
||||
@@ -134,6 +154,7 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle?
|
||||
file_count: 0, folder_count: 0, duration_ms: 0,
|
||||
};
|
||||
}
|
||||
retrySave = true;
|
||||
console.log(`[Share] Existing share link for ${shareUrl} is invalid/expired, will re-save`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
@@ -155,7 +176,7 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle?
|
||||
switch (cloudType) {
|
||||
case 'quark': {
|
||||
const driver = new QuarkDriver({ cookie: config.cookie!, nickname: config.nickname });
|
||||
driverResult = await driver.saveFromShare(shareUrl, sourceTitle);
|
||||
driverResult = await driver.saveFromShare(shareUrl, sourceTitle, retrySave);
|
||||
break;
|
||||
}
|
||||
case 'baidu': {
|
||||
@@ -215,12 +236,12 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle?
|
||||
}
|
||||
|
||||
db.prepare(
|
||||
`INSERT INTO save_records (source_type, source_title, source_url, target_cloud, share_url, share_pwd, file_size, file_count, folder_count, duration_ms, status, error_message, folder_name, original_folder_name, ip_address, ip_location, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
`INSERT INTO save_records (source_type, source_title, source_url, target_cloud, config_id, promotion_account, share_url, share_pwd, file_size, file_count, folder_count, duration_ms, status, error_message, folder_name, original_folder_name, ip_address, ip_location, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run(
|
||||
cloudType, sourceTitle || driverResult.folderName || null, shareUrl, cloudType,
|
||||
cloudType, sourceTitle || driverResult.folderName || null, shareUrl, cloudType, config.id, config.promotion_account || null,
|
||||
driverResult.shareUrl || null, driverResult.sharePwd || null,
|
||||
null, driverResult.fileCount || 0, driverResult.folderCount || 0,
|
||||
(driverResult as any).fileSize || null, driverResult.fileCount || 0, driverResult.folderCount || 0,
|
||||
durationMs, driverResult.success ? 'success' : 'failed',
|
||||
driverResult.success ? null : driverResult.message,
|
||||
driverResult.folderName || null, driverResult.originalFolderName || null,
|
||||
@@ -247,9 +268,9 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle?
|
||||
).run(config.id);
|
||||
|
||||
db.prepare(
|
||||
`INSERT INTO save_records (source_type, source_url, target_cloud, duration_ms, status, error_message, ip_address, ip_location, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run(cloudType, shareUrl, cloudType, durationMs, 'failed', errorMessage, ipAddress || null, ipLocation, localTimestamp());
|
||||
`INSERT INTO save_records (source_type, source_url, target_cloud, config_id, promotion_account, duration_ms, status, error_message, ip_address, ip_location, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run(cloudType, shareUrl, cloudType, config.id, config.promotion_account || null, durationMs, 'failed', errorMessage, ipAddress || null, ipLocation, localTimestamp());
|
||||
|
||||
return { success: false, message: errorMessage };
|
||||
}
|
||||
|
||||
389
source_clean/src/cloud/cloud.service.ts.bak3
Normal file
389
source_clean/src/cloud/cloud.service.ts.bak3
Normal file
@@ -0,0 +1,389 @@
|
||||
import { getDb } from '../database/database';
|
||||
import { localTimestamp, formatLocalDateTime } from '../utils/time';
|
||||
import { getSystemConfig } from '../admin/system-config.service';
|
||||
import { QuarkDriver } from './drivers/quark.driver';
|
||||
import { BaiduDriver } from './drivers/baidu.driver';
|
||||
import { CloudConfig, getAndValidateCredential, getActiveCloudConfigs } from './credential.service';
|
||||
import { lookupIpLocation } from './ip-lookup';
|
||||
import { notifyConfigEvent } from './notification.service';
|
||||
|
||||
/** In-flight save dedup: prevents concurrent saves of the same URL (race condition fix) */
|
||||
const inFlightSaves = new Map<string, Promise<SaveResult>>();
|
||||
|
||||
export interface SaveResult {
|
||||
success: boolean;
|
||||
shareUrl?: string;
|
||||
share_url?: string;
|
||||
sharePwd?: string;
|
||||
folderName?: string;
|
||||
message: string;
|
||||
file_count?: number;
|
||||
folder_count?: number;
|
||||
duration_ms?: number;
|
||||
}
|
||||
|
||||
export interface SaveRecord {
|
||||
id: number;
|
||||
source_type: string;
|
||||
source_title: string | null;
|
||||
source_url: string;
|
||||
target_cloud: string;
|
||||
share_url: string | null;
|
||||
share_pwd: string | null;
|
||||
file_size: string | null;
|
||||
file_count: number;
|
||||
folder_count: number;
|
||||
duration_ms: number;
|
||||
status: string;
|
||||
error_message: string | null;
|
||||
folder_name: string | null;
|
||||
original_folder_name: string | null;
|
||||
ip_address: string | null;
|
||||
ip_location: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/** Core save logic extracted so inFlight dedup can wrap it */
|
||||
async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle?: string, ipAddress?: string): Promise<SaveResult> {
|
||||
const db = getDb();
|
||||
const ipLocation = await lookupIpLocation(ipAddress || '');
|
||||
|
||||
// ── Short-term dedup: prevent duplicate saves of the same URL within 60 seconds ──
|
||||
const DEDUP_WINDOW_SEC = 60;
|
||||
let dedupCutoff = '';
|
||||
try {
|
||||
const recentCutoff = db.prepare(
|
||||
`SELECT datetime('now','localtime', '-${DEDUP_WINDOW_SEC} seconds') as cutoff`
|
||||
).get() as { cutoff: string };
|
||||
dedupCutoff = recentCutoff.cutoff;
|
||||
|
||||
const recentRecord = db.prepare(
|
||||
`SELECT share_url, share_pwd, status, file_size, error_message, folder_name, original_folder_name FROM save_records
|
||||
WHERE source_url = ? AND created_at >= ?
|
||||
ORDER BY created_at DESC LIMIT 1`
|
||||
).get(shareUrl, dedupCutoff) as {
|
||||
share_url: string | null; share_pwd: string | null; status: string; file_size: string | null;
|
||||
error_message: string | null; folder_name: string | null; original_folder_name: string | null;
|
||||
} | undefined;
|
||||
|
||||
if (recentRecord) {
|
||||
const alreadySaved = recentRecord.status === 'success' || recentRecord.status === 'reused';
|
||||
if (alreadySaved && recentRecord.share_url) {
|
||||
console.log(`[Share] 🛡️ Dedup: ${shareUrl} was saved ${DEDUP_WINDOW_SEC}s ago (status=${recentRecord.status}), returning existing share link`);
|
||||
db.prepare(
|
||||
`INSERT INTO save_records (source_type, source_title, source_url, target_cloud, share_url, share_pwd, file_size, file_count, folder_count, duration_ms, status, error_message, folder_name, original_folder_name, ip_address, ip_location, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run(
|
||||
cloudType, sourceTitle || null, shareUrl, cloudType,
|
||||
recentRecord.share_url, recentRecord.share_pwd || null,
|
||||
recentRecord.file_size || null, 0, 0, 0, 'reused', null,
|
||||
recentRecord.folder_name || null, recentRecord.original_folder_name || null,
|
||||
ipAddress || null, ipLocation, localTimestamp(),
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
message: `🛡️ 此资源刚在 ${DEDUP_WINDOW_SEC} 秒内转存过,直接返回已有分享链接`,
|
||||
share_url: recentRecord.share_url, shareUrl: recentRecord.share_url,
|
||||
sharePwd: recentRecord.share_pwd || '', folderName: '',
|
||||
file_count: 0, folder_count: 0, duration_ms: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.log(`[Share] Dedup check failed: ${err.message}, proceeding with normal save`);
|
||||
}
|
||||
|
||||
// ── Share link reuse: if same source URL was already saved successfully, validate and reuse ──
|
||||
const reuseEnabled = getSystemConfig('save_reuse_enabled');
|
||||
if (reuseEnabled !== 'false') {
|
||||
try {
|
||||
const existing = db.prepare(
|
||||
`SELECT share_url, share_pwd, file_size, folder_name, original_folder_name FROM save_records
|
||||
WHERE source_url = ? AND status IN ('success', 'reused') AND share_url IS NOT NULL AND share_url != ''
|
||||
ORDER BY created_at DESC LIMIT 1`
|
||||
).get(shareUrl) as { share_url: string; share_pwd: string; file_size: string | null; folder_name: string | null; original_folder_name: string | null } | undefined;
|
||||
|
||||
if (existing?.share_url) {
|
||||
const { LinkValidator } = await import('../validation/link-validator.service');
|
||||
const validator = new LinkValidator();
|
||||
const validation = await validator.validate(existing.share_url, 'quark');
|
||||
if (validation.status === 'valid') {
|
||||
const isFirstReuse = dedupCutoff ? !db.prepare(
|
||||
`SELECT 1 FROM save_records WHERE source_url = ? AND created_at >= ? AND status = 'reused' LIMIT 1`
|
||||
).get(shareUrl, dedupCutoff) : true;
|
||||
const reuseStatus = isFirstReuse ? 'success' : 'reused';
|
||||
const reuseMsg = isFirstReuse
|
||||
? `♻️ 检测到此资源之前已转存过,直接复用已存在的分享链接`
|
||||
: `♻️ 短时间内重复请求,复用已有分享链接`;
|
||||
|
||||
console.log(`[Share] ♻️ Reusing existing share link for ${shareUrl}: ${existing.share_url} (firstReuse=${isFirstReuse})`);
|
||||
db.prepare(
|
||||
`INSERT INTO save_records (source_type, source_title, source_url, target_cloud, share_url, share_pwd, file_size, file_count, folder_count, duration_ms, status, error_message, folder_name, original_folder_name, ip_address, ip_location, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run(
|
||||
cloudType, sourceTitle || null, shareUrl, cloudType,
|
||||
existing.share_url, existing.share_pwd || null,
|
||||
existing.file_size || null, 0, 0, 0, reuseStatus, null,
|
||||
existing.folder_name || null, existing.original_folder_name || null,
|
||||
ipAddress || null, ipLocation, localTimestamp(),
|
||||
);
|
||||
return {
|
||||
success: true, message: reuseMsg,
|
||||
share_url: existing.share_url, shareUrl: existing.share_url,
|
||||
sharePwd: existing.share_pwd || '', folderName: '',
|
||||
file_count: 0, folder_count: 0, duration_ms: 0,
|
||||
};
|
||||
}
|
||||
console.log(`[Share] Existing share link for ${shareUrl} is invalid/expired, will re-save`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.log(`[Share] Link reuse check failed: ${err.message}, proceeding with normal save`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Unified credential validation ──
|
||||
const credential = await getAndValidateCredential(cloudType);
|
||||
if (!credential.valid || !credential.config) {
|
||||
return { success: false, message: credential.message };
|
||||
}
|
||||
const config = credential.config;
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
let driverResult: { success: boolean; message: string; shareUrl?: string; sharePwd?: string; folderName?: string; fileCount?: number; folderCount?: number; originalFolderName?: string };
|
||||
|
||||
switch (cloudType) {
|
||||
case 'quark': {
|
||||
const driver = new QuarkDriver({ cookie: config.cookie!, nickname: config.nickname });
|
||||
driverResult = await driver.saveFromShare(shareUrl, sourceTitle);
|
||||
break;
|
||||
}
|
||||
case 'baidu': {
|
||||
const driver = new BaiduDriver({ cookie: config.cookie!, nickname: config.nickname });
|
||||
driverResult = await driver.saveFromShare(shareUrl, sourceTitle);
|
||||
break;
|
||||
}
|
||||
case 'aliyun':
|
||||
return { success: false, message: '阿里云盘保存功能暂未实现' };
|
||||
default:
|
||||
return { success: false, message: `暂不支持 ${cloudType} 的保存功能` };
|
||||
}
|
||||
|
||||
const durationMs = Date.now() - startTime;
|
||||
|
||||
if (driverResult.success) {
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET last_used_at = datetime('now','localtime'), total_saves = total_saves + 1, consecutive_failures = 0 WHERE id = ?`
|
||||
).run(config.id);
|
||||
const nickname = config.nickname || cloudType;
|
||||
notifyConfigEvent(config.id, 'save_success', `✅ 转存成功`, `**${cloudType}** · ${nickname}
|
||||
文件: ${driverResult.folderName || sourceTitle || shareUrl}
|
||||
耗时: ${((Date.now() - startTime) / 1000).toFixed(1)}s`, 'info', {
|
||||
file_name: driverResult.folderName || sourceTitle || shareUrl || '',
|
||||
file_size: '',
|
||||
cloud_type: cloudType,
|
||||
nickname: nickname || '',
|
||||
duration: ((Date.now() - startTime) / 1000).toFixed(1),
|
||||
share_url: shareUrl,
|
||||
});
|
||||
} else if ((driverResult as any).cookieExpired) {
|
||||
// Cookie expired — don't count as failure, user needs to re-login
|
||||
notifyConfigEvent(config.id, 'cookie_expire', `⚠️ Cookie过期`, `**${cloudType}** · ${config.nickname || '未知'}
|
||||
链接: ${shareUrl}
|
||||
请重新登录`, 'error', {
|
||||
cloud_type: cloudType,
|
||||
nickname: config.nickname || '',
|
||||
share_url: shareUrl,
|
||||
});
|
||||
} else {
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET consecutive_failures = consecutive_failures + 1 WHERE id = ?`
|
||||
).run(config.id);
|
||||
const failCount = (db.prepare(`SELECT consecutive_failures FROM cloud_configs WHERE id = ?`).get(config.id) as any)?.consecutive_failures || 0;
|
||||
if (failCount >= 3) {
|
||||
notifyConfigEvent(config.id, 'save_fail', `❌ 转存连续失败 ${failCount} 次`, `**${cloudType}** · ${config.nickname || '未知'}
|
||||
链接: ${shareUrl}
|
||||
错误: ${driverResult.message}`, 'warn', {
|
||||
file_name: sourceTitle || shareUrl || '',
|
||||
fail_count: String(failCount),
|
||||
cloud_type: cloudType,
|
||||
nickname: config.nickname || '',
|
||||
error: driverResult.message || '',
|
||||
share_url: shareUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
db.prepare(
|
||||
`INSERT INTO save_records (source_type, source_title, source_url, target_cloud, config_id, promotion_account, share_url, share_pwd, file_size, file_count, folder_count, duration_ms, status, error_message, folder_name, original_folder_name, ip_address, ip_location, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run(
|
||||
cloudType, sourceTitle || driverResult.folderName || null, shareUrl, cloudType, config.id, config.promotion_account || null,
|
||||
driverResult.shareUrl || null, driverResult.sharePwd || null,
|
||||
(driverResult as any).fileSize || null, driverResult.fileCount || 0, driverResult.folderCount || 0,
|
||||
durationMs, driverResult.success ? 'success' : 'failed',
|
||||
driverResult.success ? null : driverResult.message,
|
||||
driverResult.folderName || null, driverResult.originalFolderName || null,
|
||||
ipAddress || null, ipLocation, localTimestamp(),
|
||||
);
|
||||
|
||||
return {
|
||||
success: driverResult.success,
|
||||
message: driverResult.message,
|
||||
share_url: driverResult.shareUrl || '',
|
||||
shareUrl: driverResult.shareUrl,
|
||||
sharePwd: (driverResult as any).sharePwd || '',
|
||||
folderName: driverResult.folderName || '',
|
||||
file_count: driverResult.fileCount || 0,
|
||||
folder_count: driverResult.folderCount || 0,
|
||||
duration_ms: durationMs,
|
||||
};
|
||||
} catch (err: any) {
|
||||
const durationMs = Date.now() - startTime;
|
||||
const errorMessage = err.message || 'Failed to save to cloud';
|
||||
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET consecutive_failures = consecutive_failures + 1 WHERE id = ?`
|
||||
).run(config.id);
|
||||
|
||||
db.prepare(
|
||||
`INSERT INTO save_records (source_type, source_url, target_cloud, config_id, promotion_account, duration_ms, status, error_message, ip_address, ip_location, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run(cloudType, shareUrl, cloudType, config.id, config.promotion_account || null, durationMs, 'failed', errorMessage, ipAddress || null, ipLocation, localTimestamp());
|
||||
|
||||
return { success: false, message: errorMessage };
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveFromShare(shareUrl: string, cloudType: string, sourceTitle?: string, ipAddress?: string): Promise<SaveResult> {
|
||||
const key = `${cloudType}:${shareUrl}`;
|
||||
|
||||
const inflight = inFlightSaves.get(key);
|
||||
if (inflight) {
|
||||
console.log(`[Share] ⏳ In-flight: ${shareUrl} — another save is already running, awaiting result`);
|
||||
return inflight;
|
||||
}
|
||||
|
||||
const promise = doSaveFromShare(shareUrl, cloudType, sourceTitle, ipAddress);
|
||||
inFlightSaves.set(key, promise);
|
||||
try {
|
||||
return await promise;
|
||||
} finally {
|
||||
inFlightSaves.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Save Records ──────────────────────────────────────────────────
|
||||
|
||||
export function getSaveRecords(page: number = 1, pageSize: number = 20, startDate?: string, endDate?: string, status?: string, sourceType?: string, keyword?: string): { total: number; records: SaveRecord[]; summary?: { total: number; success: number; failed: number; reused: number } } {
|
||||
const db = getDb();
|
||||
const offset = (page - 1) * pageSize;
|
||||
const conditions: string[] = [];
|
||||
const params: any[] = [];
|
||||
const summaryConditions: string[] = [];
|
||||
const summaryParams: any[] = [];
|
||||
if (startDate) {
|
||||
conditions.push('created_at >= ?'); params.push(startDate);
|
||||
summaryConditions.push('created_at >= ?'); summaryParams.push(startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
conditions.push('created_at < ?'); params.push(endDate);
|
||||
summaryConditions.push('created_at < ?'); summaryParams.push(endDate);
|
||||
}
|
||||
if (status) { conditions.push('status = ?'); params.push(status); }
|
||||
if (sourceType) {
|
||||
conditions.push('source_type = ?'); params.push(sourceType);
|
||||
summaryConditions.push('source_type = ?'); summaryParams.push(sourceType);
|
||||
}
|
||||
if (keyword) { conditions.push('source_title LIKE ?'); params.push(`%${keyword}%`); }
|
||||
const where = conditions.length > 0 ? 'WHERE ' + conditions.join(' AND ') : '';
|
||||
const total = (db.prepare(`SELECT COUNT(*) as count FROM save_records ${where}`).get(...params) as any).count;
|
||||
const records = db.prepare(
|
||||
`SELECT * FROM save_records ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`
|
||||
).all(...params, pageSize, offset) as SaveRecord[];
|
||||
|
||||
const summaryWhere = summaryConditions.length > 0 ? 'WHERE ' + summaryConditions.join(' AND ') : '';
|
||||
const summaryRows = db.prepare(
|
||||
`SELECT status, COUNT(*) as cnt FROM save_records ${summaryWhere} GROUP BY status`
|
||||
).all(...summaryParams) as { status: string; cnt: number }[];
|
||||
let sumTotal = 0, sumSuccess = 0, sumFailed = 0, sumReused = 0;
|
||||
for (const r of summaryRows) {
|
||||
sumTotal += r.cnt;
|
||||
if (r.status === 'success') sumSuccess = r.cnt;
|
||||
else if (r.status === 'failed') sumFailed = r.cnt;
|
||||
else if (r.status === 'reused') sumReused = r.cnt;
|
||||
}
|
||||
const summary = { total: sumTotal, success: sumSuccess, failed: sumFailed, reused: sumReused };
|
||||
|
||||
return { total, records, summary };
|
||||
}
|
||||
|
||||
export function cleanupOldSaveRecords(): void {
|
||||
const db = getDb();
|
||||
const cutoff = formatLocalDateTime(new Date(Date.now() - 60 * 24 * 60 * 60 * 1000));
|
||||
const deleted = db.prepare('DELETE FROM save_records WHERE created_at < ?').run(cutoff);
|
||||
console.log(`[Cleanup] Deleted ${deleted.changes} save records older than 60 days (before ${cutoff})`);
|
||||
}
|
||||
|
||||
// ── Storage Refresh ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Refresh storage info for all active cloud configs that have a getStorageInfo method.
|
||||
* Supports quark and baidu drivers.
|
||||
*/
|
||||
export async function refreshAllStorageInfo(): Promise<void> {
|
||||
const configs = getActiveCloudConfigs().filter(c => c.cookie);
|
||||
if (configs.length === 0) return;
|
||||
|
||||
// Driver mapping: cloud_type → { module, class }
|
||||
const DRIVER_REGISTRY: Record<string, { module: string; cls: string }> = {
|
||||
quark: { module: './drivers/quark.driver', cls: 'QuarkDriver' },
|
||||
baidu: { module: './drivers/baidu.driver', cls: 'BaiduDriver' },
|
||||
};
|
||||
|
||||
for (const cfg of configs) {
|
||||
const entry = DRIVER_REGISTRY[cfg.cloud_type];
|
||||
if (!entry) continue; // no getStorageInfo support for this cloud type
|
||||
|
||||
try {
|
||||
const mod = require(entry.module);
|
||||
const Driver = mod[entry.cls];
|
||||
if (!Driver) continue;
|
||||
|
||||
const driver = new Driver({ cookie: cfg.cookie, nickname: cfg.nickname });
|
||||
|
||||
// Try getStorageInfo (now uses fast /member API for accurate data)
|
||||
let storage: any;
|
||||
try {
|
||||
storage = await driver.getStorageInfo();
|
||||
} catch {
|
||||
if (typeof driver.getStorageInfoQuick === 'function') {
|
||||
storage = await driver.getStorageInfoQuick();
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!storage) continue;
|
||||
|
||||
// Get formatted strings — some drivers return {used, total, usedBytes, totalBytes}
|
||||
const used = storage.used || '计算中...';
|
||||
const total = storage.total || '-';
|
||||
|
||||
// Only update if we got meaningful data
|
||||
const hasRealData =
|
||||
(storage.totalBytes > 0 || storage.usedBytes > 0) || // quark returns these
|
||||
(used !== '-' && used !== '0 B' && used !== '计算中...'); // baidu check
|
||||
|
||||
if (hasRealData) {
|
||||
const db = getDb();
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET storage_used = ?, storage_total = ? WHERE id = ?`
|
||||
).run(used, total, cfg.id);
|
||||
console.log(`[Storage] Refreshed ${cfg.cloud_type}#${cfg.id}: ${used} / ${total}`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(`[Storage] Failed to refresh ${cfg.cloud_type}#${cfg.id}:`, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,9 @@ export interface CloudConfig {
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
verification_status?: string;
|
||||
promotion_account?: string;
|
||||
cloud_type_uid?: string;
|
||||
cookie_uid?: string;
|
||||
}
|
||||
|
||||
// ── Cookie UID Extraction ────────────────────────────────────────
|
||||
@@ -382,6 +385,7 @@ export async function getAndValidateCredential(cloudType: string): Promise<Crede
|
||||
};
|
||||
}
|
||||
|
||||
config.cookie = cookie;
|
||||
return {
|
||||
valid: true,
|
||||
config,
|
||||
|
||||
@@ -1,383 +0,0 @@
|
||||
import { getDb } from '../database/database';
|
||||
import { encrypt, decrypt, isEncrypted } from '../utils/crypto';
|
||||
import { localTimestamp, formatLocalDate, formatLocalDateTime } from '../utils/time';
|
||||
|
||||
export interface CloudConfig {
|
||||
id: number;
|
||||
cloud_type: string;
|
||||
cookie?: string;
|
||||
nickname?: string;
|
||||
is_active: number;
|
||||
storage_used?: string;
|
||||
storage_total?: string;
|
||||
checkin_status: string; // 'none'|'success'|'failed'|'pending'|'skipped'
|
||||
last_checkin_at?: string;
|
||||
checkin_message?: string;
|
||||
consecutive_failures: number;
|
||||
last_used_at?: string;
|
||||
total_saves: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
verification_status?: string;
|
||||
}
|
||||
|
||||
// ── Cookie UID Extraction ────────────────────────────────────────
|
||||
|
||||
function extractCookieUid(cookie: string): string {
|
||||
|
||||
function decryptCookie(encrypted: string): string {
|
||||
if (!encrypted) return '';
|
||||
if (!isEncrypted(encrypted)) return encrypted;
|
||||
return decrypt(encrypted);
|
||||
}
|
||||
if (!cookie) return '';
|
||||
let m = cookie.match(/__uid=([a-zA-Z0-9+/=_-]+)/);
|
||||
if (m) return m[1];
|
||||
m = cookie.match(/b-user-id=([a-zA-Z0-9-]+)/);
|
||||
if (m) return m[1];
|
||||
return '';
|
||||
}
|
||||
|
||||
// ── Config CRUD ──────────────────────────────────────────────────
|
||||
|
||||
export function getCloudConfigs(): CloudConfig[] {
|
||||
const db = getDb();
|
||||
return db.prepare(
|
||||
`SELECT id, cloud_type, cookie, nickname, is_active, storage_used, storage_total,
|
||||
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||||
last_used_at, total_saves, created_at, updated_at, verification_status
|
||||
FROM cloud_configs ORDER BY id ASC`
|
||||
).all() as CloudConfig[];
|
||||
}
|
||||
|
||||
export function getAvailableClouds(): CloudConfig[] {
|
||||
const db = getDb();
|
||||
return db.prepare(
|
||||
`SELECT id, cloud_type, nickname, is_active, storage_used, storage_total,
|
||||
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||||
last_used_at, total_saves, created_at, updated_at
|
||||
FROM cloud_configs WHERE is_active = 1 ORDER BY id ASC`
|
||||
).all() as CloudConfig[];
|
||||
}
|
||||
|
||||
/** Returns the first active config matching the given cloud type. */
|
||||
export function getCloudConfigByType(cloudType: string): CloudConfig | undefined {
|
||||
const db = getDb();
|
||||
const cfg = db.prepare(
|
||||
`SELECT id, cloud_type, cookie, nickname, is_active, storage_used, storage_total,
|
||||
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||||
last_used_at, total_saves, created_at, updated_at, verification_status
|
||||
FROM cloud_configs WHERE cloud_type = ? AND is_active = 1
|
||||
ORDER BY id ASC LIMIT 1`
|
||||
).get(cloudType) as CloudConfig | undefined;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
export function getCloudConfigById(id: number): CloudConfig | undefined {
|
||||
const db = getDb();
|
||||
const cfg = db.prepare(
|
||||
`SELECT id, cloud_type, cookie, nickname, is_active, storage_used, storage_total,
|
||||
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||||
last_used_at, total_saves, created_at, updated_at, verification_status
|
||||
FROM cloud_configs WHERE id = ?`
|
||||
).get(id) as CloudConfig | undefined;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
/** Returns all active cloud configs (used by save flow for cloud type switching). */
|
||||
export function getActiveCloudConfigs(): CloudConfig[] {
|
||||
const db = getDb();
|
||||
return db.prepare(
|
||||
`SELECT id, cloud_type, cookie, nickname, is_active, storage_used, storage_total,
|
||||
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||||
last_used_at, total_saves, created_at, updated_at
|
||||
FROM cloud_configs WHERE is_active = 1
|
||||
ORDER BY cloud_type ASC, id ASC`
|
||||
).all() as CloudConfig[];
|
||||
}
|
||||
|
||||
export function saveCloudConfig(data: {
|
||||
id?: number;
|
||||
cloud_type: string;
|
||||
cookie?: string;
|
||||
nickname?: string;
|
||||
cookie_uid?: string;
|
||||
promotion_account?: string;
|
||||
is_active?: number;
|
||||
storage_used?: string;
|
||||
storage_total?: string;
|
||||
}): CloudConfig {
|
||||
const db = getDb();
|
||||
|
||||
const cookieUidForUpdate = data.cookie ? extractCookieUid(data.cookie) : null;
|
||||
const encryptedCookie = data.cookie ? encrypt(data.cookie) : null;
|
||||
|
||||
if (data.id) {
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET
|
||||
cloud_type = COALESCE(?, cloud_type),
|
||||
cookie = COALESCE(?, cookie),
|
||||
nickname = COALESCE(?, nickname),
|
||||
cookie_uid = COALESCE(?, cookie_uid),
|
||||
promotion_account = COALESCE(?, promotion_account),
|
||||
is_active = COALESCE(?, is_active),
|
||||
storage_used = COALESCE(?, storage_used),
|
||||
storage_total = COALESCE(?, storage_total),
|
||||
consecutive_failures = 0,
|
||||
updated_at = ?
|
||||
WHERE id = ?`
|
||||
).run(data.cloud_type, encryptedCookie, data.nickname || null, cookieUidForUpdate || null, data.promotion_account || null, data.is_active ?? 1, data.storage_used || null, data.storage_total || null, localTimestamp(), data.id);
|
||||
} else {
|
||||
const existing = db.prepare(
|
||||
'SELECT id, nickname FROM cloud_configs WHERE cloud_type = ? AND is_active = 1 LIMIT 1'
|
||||
).get(data.cloud_type) as any;
|
||||
if (existing) {
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET
|
||||
cookie = COALESCE(?, cookie),
|
||||
nickname = COALESCE(?, nickname),
|
||||
cookie_uid = COALESCE(?, cookie_uid),
|
||||
promotion_account = COALESCE(?, promotion_account),
|
||||
is_active = COALESCE(?, is_active),
|
||||
storage_used = COALESCE(?, storage_used),
|
||||
storage_total = COALESCE(?, storage_total),
|
||||
consecutive_failures = 0,
|
||||
updated_at = ?
|
||||
WHERE id = ?`
|
||||
).run(encryptedCookie, data.nickname || null, cookieUidForUpdate || null, data.promotion_account || null, data.is_active ?? 1, data.storage_used || null, data.storage_total || null, localTimestamp(), existing.id);
|
||||
} else {
|
||||
db.prepare(
|
||||
'INSERT INTO cloud_configs (cloud_type, cookie, nickname, cookie_uid, promotion_account, is_active, storage_used, storage_total, consecutive_failures) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)'
|
||||
).run(data.cloud_type, encryptedCookie, data.nickname || null, cookieUidForUpdate || null, data.promotion_account || null, data.is_active ?? 1, data.storage_used || null, data.storage_total || null);
|
||||
}
|
||||
}
|
||||
|
||||
const savedId = data.id || (db.prepare('SELECT last_insert_rowid() as id').get() as any).id;
|
||||
return db.prepare(
|
||||
`SELECT id, cloud_type, cookie, nickname, is_active, storage_used, storage_total,
|
||||
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||||
last_used_at, total_saves, created_at, updated_at
|
||||
FROM cloud_configs WHERE id = ?`
|
||||
).get(savedId) as CloudConfig;
|
||||
}
|
||||
|
||||
export function deleteCloudConfig(id: number): boolean {
|
||||
const db = getDb();
|
||||
const result = db.prepare('DELETE FROM cloud_configs WHERE id = ?').run(id);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
// ── Cookie Validation ────────────────────────────────────────────
|
||||
|
||||
async function fetchQuarkNickname(cookie: string): Promise<string | null> {
|
||||
const MAX_RETRIES = 2;
|
||||
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
const response = await fetch('https://pan.quark.cn/account/info', {
|
||||
headers: {
|
||||
'Cookie': cookie,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'Referer': 'https://pan.quark.cn/',
|
||||
},
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
const data = await response.json() as any;
|
||||
if (data?.data?.nickname) return data.data.nickname;
|
||||
} catch {
|
||||
if (attempt < MAX_RETRIES) {
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function testCloudConnection(id: number): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
nickname?: string;
|
||||
storage_used?: string;
|
||||
storage_total?: string;
|
||||
}> {
|
||||
const config = getCloudConfigById(id);
|
||||
if (!config) {
|
||||
return { success: false, message: 'Cloud config not found' };
|
||||
}
|
||||
|
||||
if (!config.cookie) {
|
||||
return { success: false, message: 'Cookie not configured' };
|
||||
}
|
||||
|
||||
try {
|
||||
let valid = false;
|
||||
let nickname = '';
|
||||
let storageUsed = config.storage_used || '';
|
||||
let storageTotal = config.storage_total || '';
|
||||
|
||||
if (config.cloud_type === 'baidu') {
|
||||
const { BaiduDriver } = require('./drivers/baidu.driver');
|
||||
const driver = new BaiduDriver({ cookie: config.cookie, nickname: config.nickname });
|
||||
valid = await driver.validate();
|
||||
if (valid) {
|
||||
const info = await driver.getUserInfo();
|
||||
if (info) {
|
||||
nickname = config.nickname || info.nickname || '百度网盘';
|
||||
const fmt = (b: number) => b >= 1024**3 ? (b/1024**3).toFixed(2)+' GB' : (b/1024**2).toFixed(2)+' MB';
|
||||
storageUsed = fmt(info.usedBytes);
|
||||
storageTotal = fmt(info.totalBytes);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const { QuarkDriver } = require('./drivers/quark.driver');
|
||||
const driver = new QuarkDriver({ cookie: config.cookie, nickname: config.nickname });
|
||||
valid = await driver.validate();
|
||||
if (valid) {
|
||||
nickname = config.nickname || (await fetchQuarkNickname(config.cookie)) || '夸克网盘';
|
||||
const storage = await driver.getStorageInfoQuick();
|
||||
storageTotal = (storage.total !== '-' && storage.total !== '0 B') ? storage.total : (config.storage_total || '');
|
||||
}
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
if (!valid) {
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET verification_status = 'invalid', updated_at = ? WHERE id = ?`
|
||||
).run(localTimestamp(), id);
|
||||
return { success: false, message: '连接失败:Cookie 无效或已过期,或网络暂时异常' };
|
||||
}
|
||||
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET nickname = ?, storage_total = ?, storage_used = ?, is_active = 1, verification_status = 'valid', updated_at = ? WHERE id = ?`
|
||||
).run(nickname, storageTotal, storageUsed, localTimestamp(), id);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '连接成功',
|
||||
nickname,
|
||||
storage_used: storageUsed,
|
||||
storage_total: storageTotal,
|
||||
};
|
||||
} catch (err: any) {
|
||||
try {
|
||||
const db = getDb();
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET verification_status = 'invalid', updated_at = ? WHERE id = ?`
|
||||
).run(localTimestamp(), id);
|
||||
} catch {}
|
||||
return { success: false, message: `连接失败:${err.message || '未知错误'}` };
|
||||
}
|
||||
}
|
||||
|
||||
export async function testCloudConnectionWithCookie(cloudType: string, cookie: string): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
nickname?: string;
|
||||
storage_used?: string;
|
||||
storage_total?: string;
|
||||
}> {
|
||||
try {
|
||||
const { QuarkDriver } = require('./drivers/quark.driver');
|
||||
const driver = new QuarkDriver({ cookie, nickname: '' });
|
||||
const valid = await driver.validate();
|
||||
if (!valid) {
|
||||
return { success: false, message: '连接失败:Cookie 无效或已过期' };
|
||||
}
|
||||
const nickname = (await fetchQuarkNickname(cookie)) || cloudType;
|
||||
const storage = await driver.getStorageInfo();
|
||||
return {
|
||||
success: true,
|
||||
message: '连接成功',
|
||||
nickname,
|
||||
storage_used: storage.used,
|
||||
storage_total: storage.total,
|
||||
};
|
||||
} catch (err: any) {
|
||||
return { success: false, message: `连接失败:${err.message || '未知错误'}` };
|
||||
}
|
||||
}
|
||||
|
||||
// ── Unified Credential Validation ─────────────────────────────────
|
||||
|
||||
export interface CredentialValidationResult {
|
||||
valid: boolean;
|
||||
config?: CloudConfig;
|
||||
errorCode?: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get and validate a credential for the given cloud type.
|
||||
*
|
||||
* This is the unified entry point for all save/transfer operations.
|
||||
* It handles:
|
||||
* 1. Finding an active config with < 5 consecutive failures (round-robin)
|
||||
* 2. Validating cookie freshness via driver.validate()
|
||||
* 3. Returning structured result with error codes
|
||||
*
|
||||
* Reference: search-ucmao get_and_validate_credential() pattern.
|
||||
*/
|
||||
export async function getAndValidateCredential(cloudType: string): Promise<CredentialValidationResult> {
|
||||
const db = getDb();
|
||||
|
||||
const config = db.prepare(
|
||||
`SELECT * FROM cloud_configs
|
||||
WHERE cloud_type = ? AND is_active = 1
|
||||
AND consecutive_failures < 5
|
||||
ORDER BY last_used_at ASC NULLS FIRST
|
||||
LIMIT 1`
|
||||
).get(cloudType) as CloudConfig | undefined;
|
||||
|
||||
if (!config) {
|
||||
return {
|
||||
valid: false,
|
||||
errorCode: 'NO_AVAILABLE_DRIVE',
|
||||
message: `Cloud type "${cloudType}" is not configured or no available drives`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!config.cookie) {
|
||||
return {
|
||||
valid: false,
|
||||
errorCode: 'COOKIE_MISSING',
|
||||
message: `Cookie not configured for ${cloudType} drive #${config.id}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
let cookieValid = false;
|
||||
if (cloudType === 'baidu') {
|
||||
const { BaiduDriver } = require('./drivers/baidu.driver');
|
||||
const driver = new BaiduDriver({ cookie: config.cookie, nickname: config.nickname });
|
||||
cookieValid = await driver.validate();
|
||||
} else {
|
||||
const { QuarkDriver } = require('./drivers/quark.driver');
|
||||
const driver = new QuarkDriver({ cookie: config.cookie, nickname: config.nickname });
|
||||
cookieValid = await driver.validate();
|
||||
}
|
||||
|
||||
if (!cookieValid) {
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET verification_status = 'invalid', updated_at = ? WHERE id = ?`
|
||||
).run(localTimestamp(), config.id);
|
||||
return {
|
||||
valid: false,
|
||||
errorCode: 'COOKIE_EXPIRED',
|
||||
message: `Cookie expired or invalid for ${cloudType} drive #${config.id}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
config,
|
||||
message: 'ok',
|
||||
};
|
||||
} catch (err: any) {
|
||||
return {
|
||||
valid: false,
|
||||
errorCode: 'VALIDATION_ERROR',
|
||||
message: `Credential validation failed: ${err.message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ export async function deleteAdFiles(cookie, dirFid, keywords) {
|
||||
const toKeep = [];
|
||||
const extensions = getSusExtensions();
|
||||
for (const file of files) {
|
||||
const ext = file.file.split(".").pop()?.toLowerCase() || "";
|
||||
const ext = file.file_name.split(".").pop()?.toLowerCase() || "";
|
||||
const isSusExt = extensions.includes(ext);
|
||||
if (containsAdKeyword(file.file_name, keywords) || isSusExt) {
|
||||
toDelete.push(file.fid);
|
||||
|
||||
@@ -1,4 +1,46 @@
|
||||
// @ts-nocheck
|
||||
import crypto from "crypto";
|
||||
|
||||
const HOMOPHONE_MAP = {
|
||||
// 网盘热门番名 — 谐音替换 (same sound, different char)
|
||||
'斗': '陡', '破': '坡', '苍': '仓', '穹': '穷',
|
||||
'完': '玩', '美': '每', '世': '士', '界': '介',
|
||||
'凡': '烦', '人': '仁', '修': '休', '罗': '络',
|
||||
'仙': '先', '逆': '腻', '遮': '折', '天': '添',
|
||||
'吞': '屯', '噬': '逝', '大': '达', '主': '嘱', '宰': '崽',
|
||||
'星': '惺', '辰': '晨', '变': '便', '一': '伊', '念': '捻',
|
||||
'永': '泳', '恒': '横', '神': '申', '墓': '暮', '长': '尝', '生': '甥',
|
||||
'剑': '箭', '来': '莱', '诡': '鬼', '秘': '蜜',
|
||||
'全': '泉', '职': '值', '盘': '磐', '龙': '笼',
|
||||
'雪': '血', '鹰': '莺', '莽': '蟒', '荒': '慌', '纪': '记',
|
||||
'珠': '株', '王': '亡', '座': '坐', '牧': '木', '记': '计',
|
||||
'沧': '舱', '元': '圆', '图': '涂', '紫': '仔', '川': '串',
|
||||
'百': '白', '炼': '恋', '成': '程', '饶': '绕', '命': '冥',
|
||||
// 通用谐音替换
|
||||
'的': '得', '了': '啦', '是': '事', '不': '布', '我': '窝',
|
||||
'你': '尼', '他': '她', '有': '友', '和': '合', '与': '予',
|
||||
'上': '尚', '下': '夏', '中': '忠', '第': '弟', '集': '级',
|
||||
'话': '划', '季': '际', '年': '念', '月': '阅', '日': '曰',
|
||||
'新': '心', '版': '板', '高': '糕', '清': '青', '原': '源',
|
||||
'小': '晓', '片': '篇', '视': '市', '频': '贫', '道': '到',
|
||||
'动': '洞', '画': '话', '声': '升', '音': '因', '文': '闻',
|
||||
'明': '名', '暗': '黯', '光': '广', '影': '映', '色': '瑟',
|
||||
'风': '疯', '雨': '语', '花': '华', '国': '果', '家': '佳',
|
||||
'战': '站', '争': '挣', '士': '仕', '兵': '宾',
|
||||
'皇': '惶', '帝': '谛', '魔': '磨', '鬼': '诡', '怪': '乖',
|
||||
'精': '经', '灵': '铃', '妖': '夭', '武': '舞', '侠': '狭',
|
||||
'杀': '刹', '血': '雪', '刀': '叨', '枪': '呛', '炮': '泡',
|
||||
'时': '石', '空': '孔', '前': '钱', '后': '厚', '东': '冬',
|
||||
'南': '难', '西': '夕', '北': '备', '开': '凯', '关': '官',
|
||||
'出': '初', '进': '近', '去': '趣',
|
||||
'短': '短', '多': '多', '少': '少', '真': '贞', '假': '价',
|
||||
'好': '郝', '坏': '怀', '对': '队', '错': '措', '以': '已',
|
||||
'从': '从', '被': '被', '把': '把', '将': '将', '在': '在',
|
||||
'但': '但', '就': '就', '才': '才', '也': '也', '很': '狠',
|
||||
'又': '又', '再': '再', '更': '更', '最': '最', '总': '总',
|
||||
'共': '共', '只': '只', '各': '各', '每': '每', '任': '任',
|
||||
'所': '所', '该': '该', '本': '本',
|
||||
};
|
||||
const NOISE_CJK = '的了在是不有会可对所之也同与及但或如且乃而岂乎焉兮哉亦犹尚乃其若故盖诸焉欤' +
|
||||
'么个着过把对为从以到说时要就这那和上人家下能出得发来年心开物力些长样吧啊哦嗯嚯哇咯呗哟嘿呵哈';
|
||||
// ==================== Helpers ====================
|
||||
@@ -92,7 +134,7 @@ export function magicRenameDir(dirName) {
|
||||
const noiseCount = Math.random() < 0.3 ? (Math.random() < 0.5 ? 1 : 2) : 0;
|
||||
for (let n = 0; n < noiseCount; n++) {
|
||||
const pos = Math.floor(Math.random() * (baseName.length + 1));
|
||||
const ink = NOISE_CJK[Math.floor(Math.random() * NOISE.length)];
|
||||
const ink = NOISE_CJK[Math.floor(Math.random() * NOISE_CJK.length)];
|
||||
baseName = baseName.slice(0, pos) + ink + baseName.slice(pos);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
||||
*
|
||||
* Flow: token → detail → save → wait_task → rename → share
|
||||
*/
|
||||
export async function saveFromShare(cookie, nickname, shareUrl, sourceTitle) {
|
||||
export async function saveFromShare(cookie, nickname, shareUrl, sourceTitle, retrySave = false) {
|
||||
try {
|
||||
// Parse share token from URL
|
||||
const urlObj = new URL(shareUrl);
|
||||
@@ -72,9 +72,19 @@ export async function saveFromShare(cookie, nickname, shareUrl, sourceTitle) {
|
||||
const saveDirName = quark_api.dailyFolderName();
|
||||
console.log(`[Quark] saveFromShare: looking for/create dir "${saveDirName}"`);
|
||||
const saveDirFid = await findOrCreateDir(cookie, saveDirName);
|
||||
const targetPdirFid = saveDirFid || '0';
|
||||
let targetPdirFid = saveDirFid || '0';
|
||||
let retrySubFolderFid = '';
|
||||
if (saveDirFid) {
|
||||
console.log(`[Quark] Using save directory: ${saveDirName} (fid: ${saveDirFid})`);
|
||||
if (retrySave) {
|
||||
const subName = 'retry_' + Math.random().toString(36).slice(2, 6);
|
||||
console.log(`[Quark] Retry: creating subfolder "${subName}" inside "${saveDirName}"`);
|
||||
retrySubFolderFid = await findOrCreateDir(cookie, subName, saveDirFid);
|
||||
if (retrySubFolderFid) {
|
||||
targetPdirFid = retrySubFolderFid;
|
||||
console.log(`[Quark] Retry: saving to subfolder ${subName} (fid: ${retrySubFolderFid})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log(`[Quark] WARNING: failed to create/find dir "${saveDirName}", saving to root`);
|
||||
@@ -107,6 +117,10 @@ export async function saveFromShare(cookie, nickname, shareUrl, sourceTitle) {
|
||||
shareFid = savedFids[0];
|
||||
savedFolderName = topFiles[0]?.file_name || '';
|
||||
}
|
||||
if (retrySave && retrySubFolderFid) {
|
||||
shareFid = retrySubFolderFid;
|
||||
console.log(`[Quark] Retry: sharing subfolder (fid: ${retrySubFolderFid}) instead of saved content`);
|
||||
}
|
||||
// Step 6: Create share link FIRST (before rename), so all files are guaranteed to be shared
|
||||
await quark_api.humanDelay();
|
||||
let shareUrlResult = '';
|
||||
@@ -239,13 +253,13 @@ export async function saveFromShare(cookie, nickname, shareUrl, sourceTitle) {
|
||||
/**
|
||||
* Create a new directory at root.
|
||||
*/
|
||||
export async function createDir(cookie, dirName) {
|
||||
export async function createDir(cookie, dirName, parentFid = '0') {
|
||||
try {
|
||||
const resp = await fetch(`https://drive-pc.quark.cn/1/clouddrive/file?${quark_api.makeQuery()}`, {
|
||||
method: 'POST',
|
||||
headers: { ...quark_api.getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
pdir_fid: '0',
|
||||
pdir_fid: parentFid,
|
||||
file_name: dirName,
|
||||
dir: true,
|
||||
dir_path: '',
|
||||
@@ -268,9 +282,9 @@ export async function createDir(cookie, dirName) {
|
||||
/**
|
||||
* Find an existing directory by name, or create it if not found.
|
||||
*/
|
||||
export async function findOrCreateDir(cookie, dirName) {
|
||||
export async function findOrCreateDir(cookie, dirName, parentFid = '0') {
|
||||
try {
|
||||
const rootFiles = await quark_api.listDirAllPages(cookie, '0');
|
||||
const rootFiles = await quark_api.listDirAllPages(cookie, parentFid);
|
||||
const existing = rootFiles.find(f => f.dir && f.file_name === dirName);
|
||||
if (existing?.fid) {
|
||||
console.log(`[Quark] Found existing daily folder: ${dirName} (fid: ${existing.fid})`);
|
||||
@@ -281,7 +295,7 @@ export async function findOrCreateDir(cookie, dirName) {
|
||||
catch (err) {
|
||||
console.log(`[Quark] findOrCreateDir list error: ${err.message}`);
|
||||
}
|
||||
const fid = await createDir(cookie, dirName);
|
||||
const fid = await createDir(cookie, dirName, parentFid);
|
||||
console.log(`[Quark] createDir result for "${dirName}": ${fid || 'null'}`);
|
||||
return fid;
|
||||
}
|
||||
|
||||
@@ -98,8 +98,8 @@ export class QuarkDriver {
|
||||
}
|
||||
|
||||
// ==================== Storage (Save from Share) ====================
|
||||
async saveFromShare(shareUrl: string, sourceTitle?: string): Promise<any> {
|
||||
return saveFromShare(this.cookie, this.config.nickname || '', shareUrl, sourceTitle || '');
|
||||
async saveFromShare(shareUrl: string, sourceTitle?: string, retrySave?: boolean): Promise<any> {
|
||||
return saveFromShare(this.cookie, this.config.nickname || '', shareUrl, sourceTitle || '', retrySave);
|
||||
}
|
||||
|
||||
async createDir(dirName: string): Promise<any> {
|
||||
|
||||
@@ -10,9 +10,10 @@ export async function lookupIpLocation(ip: string): Promise<string | null> {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const apiUrlTemplate = getSystemConfig('ip_geo_api_url');
|
||||
if (!apiUrlTemplate) return null;
|
||||
const url = apiUrlTemplate.replace('{ip}', encodeURIComponent(ip));
|
||||
const apiId = getSystemConfig('ip_geo_api_id');
|
||||
const apiKey = getSystemConfig('ip_geo_api_key');
|
||||
if (!apiId || !apiKey) return null;
|
||||
const url = `https://cn.apihz.cn/api/ip/chaapi.php?id=${encodeURIComponent(apiId)}&key=${encodeURIComponent(apiKey)}&ip=${encodeURIComponent(ip)}&td=0`;
|
||||
|
||||
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
||||
if (!res.ok) return null;
|
||||
|
||||
@@ -24,7 +24,7 @@ export function getGlobalNotifyConfig(): Record<string, any> {
|
||||
try { return JSON.parse(raw); } catch { return {}; }
|
||||
}
|
||||
|
||||
function getGlobalNotifyConfigs(): NotifyChannel[] {
|
||||
export function getGlobalNotifyConfigs(): NotifyChannel[] {
|
||||
const raw = getSystemConfig('global_notify_config') || '{}';
|
||||
let globalConfig: any = {};
|
||||
try { globalConfig = JSON.parse(raw); } catch {}
|
||||
@@ -56,7 +56,7 @@ function checkEventEnabled(eventName: string): boolean {
|
||||
|
||||
// ======================== Core send ========================
|
||||
|
||||
async function sendToChannels(channels: NotifyChannel[], title: string, content: string, level: NotifyLevel): Promise<void> {
|
||||
export async function sendToChannels(channels: NotifyChannel[], title: string, content: string, level: NotifyLevel): Promise<void> {
|
||||
for (const ch of channels) {
|
||||
try {
|
||||
await notifyWith(ch.name, {
|
||||
@@ -92,7 +92,7 @@ export function notifyInfo(title: string, detail: string): void {
|
||||
}
|
||||
|
||||
function applyTemplate(template: string, vars: Record<string, string>): string {
|
||||
return template.replace(/\{([^}]+)\}/g, (_, key) => vars[key] || '{' + key + '}');
|
||||
return template.replace(/\{([^}]+)\}/g, (_, key) => vars[key] ?? '{' + key + '}');
|
||||
}
|
||||
|
||||
function getEventTemplates(): Record<string, { title?: string; content: string }> {
|
||||
@@ -141,7 +141,7 @@ export function notifyConfigEvent(
|
||||
// Find matching push user by cloud_configs.promotion_account
|
||||
const pushUser = findPushUserForConfig(configId);
|
||||
if (!pushUser) {
|
||||
notifyEvent(eventName, title, content, level);
|
||||
notifyEvent(eventName, title, content, level, templateVars);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ export function notifyConfigEvent(
|
||||
if (userChannels.length > 0) {
|
||||
sendToChannels(userChannels, title, content, level).catch(() => {});
|
||||
} else {
|
||||
notifyEvent(eventName, title, content, level);
|
||||
notifyEvent(eventName, title, content, level, templateVars);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
||||
const params: NotifierParam[] = [
|
||||
{ key: 'key', label: 'Bark Key', type: 'text', required: true, placeholder: 'xxxxxxxxxxxxxxxxx' },
|
||||
{ key: 'server', label: '服务器', type: 'url', default: 'https://api.day.app', required: false, placeholder: 'https://api.day.app' },
|
||||
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch 通知', required: false },
|
||||
{ key: 'content', label: '内容', type: 'text', required: true }
|
||||
|
||||
];
|
||||
|
||||
export const barkNotifier: Notifier = {
|
||||
|
||||
@@ -2,8 +2,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
||||
|
||||
const params: NotifierParam[] = [
|
||||
{ key: 'webhook_url', label: 'Webhook URL', type: 'url', required: true, placeholder: 'https://oapi.dingtalk.com/robot/send?access_token=xxx' },
|
||||
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch', required: false },
|
||||
{ key: 'content', label: '内容', type: 'text', required: true }
|
||||
|
||||
];
|
||||
|
||||
export const dingtalkNotifier: Notifier = {
|
||||
|
||||
@@ -2,8 +2,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
||||
|
||||
const params: NotifierParam[] = [
|
||||
{ key: 'webhook_url', label: 'Webhook URL', type: 'url', required: true, placeholder: 'https://discord.com/api/webhooks/...' },
|
||||
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch', required: false },
|
||||
{ key: 'content', label: '内容', type: 'text', required: true }
|
||||
|
||||
];
|
||||
|
||||
export const discordNotifier: Notifier = {
|
||||
|
||||
@@ -3,8 +3,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
||||
const params: NotifierParam[] = [
|
||||
{ key: 'server', label: '服务器地址', type: 'url', required: true, placeholder: 'https://gotify.example.com' },
|
||||
{ key: 'token', label: 'App Token', type: 'password', required: true, placeholder: 'Gotify App Token' },
|
||||
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch', required: false },
|
||||
{ key: 'content', label: '内容', type: 'text', required: true }
|
||||
|
||||
];
|
||||
|
||||
export const gotifyNotifier: Notifier = {
|
||||
|
||||
@@ -2,8 +2,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
||||
|
||||
const params: NotifierParam[] = [
|
||||
{ key: 'webhook_url', label: 'Webhook URL', type: 'url', required: true, placeholder: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxx' },
|
||||
{ key: 'title', label: '\u6807\u9898', type: 'text', default: 'CloudSearch', required: false },
|
||||
{ key: 'content', label: '\u5185\u5bb9', type: 'text', required: true }
|
||||
|
||||
];
|
||||
|
||||
export const larkNotifier: Notifier = {
|
||||
|
||||
@@ -3,8 +3,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
||||
const params: NotifierParam[] = [
|
||||
{ key: 'topic', label: 'Topic', type: 'text', required: true, placeholder: 'my-notification-topic' },
|
||||
{ key: 'server', label: '服务器', type: 'url', default: 'https://ntfy.sh', required: false, placeholder: 'https://ntfy.sh' },
|
||||
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch', required: false },
|
||||
{ key: 'content', label: '内容', type: 'text', required: true }
|
||||
|
||||
];
|
||||
|
||||
export const ntfyNotifier: Notifier = {
|
||||
|
||||
@@ -2,8 +2,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
||||
|
||||
const params: NotifierParam[] = [
|
||||
{ key: 'token', label: 'Token', type: 'password', required: true, placeholder: 'pushplus token' },
|
||||
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch', required: false },
|
||||
{ key: 'content', label: '内容', type: 'text', required: true }
|
||||
|
||||
];
|
||||
|
||||
export const pushplusNotifier: Notifier = {
|
||||
|
||||
@@ -3,8 +3,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
||||
const params: NotifierParam[] = [
|
||||
{ key: 'key', label: 'API Key', type: 'password', required: true, placeholder: 'Qmsg API Key' },
|
||||
{ key: 'qq', label: 'QQ 号', type: 'text', required: false, placeholder: '留空则推送到所有绑定QQ' },
|
||||
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch', required: false },
|
||||
{ key: 'content', label: '内容', type: 'text', required: true }
|
||||
|
||||
];
|
||||
|
||||
export const qmsgNotifier: Notifier = {
|
||||
|
||||
@@ -2,8 +2,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
||||
|
||||
const params: NotifierParam[] = [
|
||||
{ key: 'sendkey', label: 'SendKey', type: 'password', required: true, placeholder: 'Server酱 SendKey' },
|
||||
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch', required: false },
|
||||
{ key: 'content', label: '内容', type: 'text', required: true }
|
||||
|
||||
];
|
||||
|
||||
export const serverchanNotifier: Notifier = {
|
||||
|
||||
@@ -2,8 +2,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
||||
|
||||
const params: NotifierParam[] = [
|
||||
{ key: 'sendkey', label: 'SendKey', type: 'password', required: true, placeholder: 'Server酱 Turbo SendKey' },
|
||||
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch', required: false },
|
||||
{ key: 'content', label: '内容', type: 'text', required: true }
|
||||
|
||||
];
|
||||
|
||||
export const serverchanturboNotifier: Notifier = {
|
||||
|
||||
@@ -8,8 +8,7 @@ const params: NotifierParam[] = [
|
||||
{ key: 'pass', label: '密码/授权码', type: 'password', required: true, placeholder: 'SMTP 授权码' },
|
||||
{ key: 'from', label: '发件人', type: 'text', required: true, placeholder: 'sender@example.com' },
|
||||
{ key: 'to', label: '收件人', type: 'text', required: true, placeholder: 'receiver@example.com' },
|
||||
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch', required: false },
|
||||
{ key: 'content', label: '内容', type: 'text', required: true }
|
||||
|
||||
];
|
||||
|
||||
export const smtpNotifier: Notifier = {
|
||||
|
||||
@@ -3,8 +3,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
||||
const params: NotifierParam[] = [
|
||||
{ key: 'token', label: 'Bot Token', type: 'password', required: true, placeholder: '123456:ABC-def' },
|
||||
{ key: 'chat_id', label: 'Chat ID', type: 'text', required: true, placeholder: '@频道 或 -1001234567890' },
|
||||
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch', required: false },
|
||||
{ key: 'content', label: '内容', type: 'text', required: true }
|
||||
|
||||
];
|
||||
|
||||
export const telegramNotifier: Notifier = {
|
||||
|
||||
@@ -2,8 +2,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
||||
|
||||
const params: NotifierParam[] = [
|
||||
{ key: 'url', label: 'Webhook URL', type: 'url', required: true, placeholder: 'https://example.com/webhook' },
|
||||
{ key: 'title', label: '\u6807\u9898', type: 'text', default: 'CloudSearch', required: false },
|
||||
{ key: 'content', label: '\u5185\u5bb9', type: 'text', required: true }
|
||||
|
||||
];
|
||||
|
||||
export const webhookNotifier: Notifier = {
|
||||
|
||||
@@ -2,8 +2,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
||||
|
||||
const params: NotifierParam[] = [
|
||||
{ key: 'webhook_url', label: 'Webhook URL', type: 'url', required: true, placeholder: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx' },
|
||||
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch', required: false },
|
||||
{ key: 'content', label: '内容', type: 'text', required: true }
|
||||
|
||||
];
|
||||
|
||||
export const wechatWorkBotNotifier: Notifier = {
|
||||
|
||||
@@ -315,6 +315,9 @@ function seedSystemConfigs(db: Database.Database): void {
|
||||
{ key: 'save_reuse_enabled', value: 'true', description: '启用分享链接复用(相同原始链接不再重复转存,直接复用之前的分享链接)' },
|
||||
{ key: 'cleanup_last_run', value: '', description: '上次自动清理时间' },
|
||||
{ key: 'cleanup_last_stats', value: '', description: '上次清理结果统计(JSON)' },
|
||||
{ key: 'search_all_channels', value: 'false', description: '使用所有频道参与搜索(包含未启用频道)' },
|
||||
{ key: 'ip_geo_provider', value: 'apihz', description: 'IP 归属地查询接口提供商' },
|
||||
{ key: 'auto_update_enabled', value: 'false', description: '自动更新镜像(预留,暂未实现)' },
|
||||
];
|
||||
const insert = db.prepare(
|
||||
'INSERT OR IGNORE INTO system_configs (key, value, description) VALUES (?, ?, ?)'
|
||||
|
||||
@@ -535,16 +535,17 @@ router.post('/admin/test-external-service', async (req: Request, res: Response)
|
||||
break;
|
||||
}
|
||||
case 'ip_geo': {
|
||||
const geoUrl = url || getSystemConfig('ip_geo_api_url') || '';
|
||||
if (!geoUrl) {
|
||||
res.json({ ok: false, info: '请先输入 IP 归属地查询 API 地址' });
|
||||
const apiId = url || getSystemConfig('ip_geo_api_id') || '';
|
||||
const apiKey = getSystemConfig('ip_geo_api_key') || '';
|
||||
if (!apiId || !apiKey) {
|
||||
res.json({ ok: false, info: '请先配置 IP 归属地 API ID 和 Key' });
|
||||
return;
|
||||
}
|
||||
const testUrl = geoUrl.replace('{ip}', '8.8.8.8');
|
||||
const testUrl = `https://cn.apihz.cn/api/ip/chaapi.php?id=${encodeURIComponent(apiId)}&key=${encodeURIComponent(apiKey)}&ip=8.8.8.8&td=0`;
|
||||
const response = await fetch(testUrl, { signal: AbortSignal.timeout(8000) });
|
||||
const data: any = await response.json();
|
||||
const latency = Date.now() - start;
|
||||
const valid = !!(data?.country || data?.region || data?.city || data?.countryCode);
|
||||
const valid = data?.code === 200;
|
||||
res.json({ ok: valid, latency, info: valid ? '连接成功' : '响应格式不符' });
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { getDb } from '../database/database';
|
||||
import { getSystemConfig } from '../admin/system-config.service';
|
||||
import { getCloudConfigs } from '../cloud/credential.service';
|
||||
import { notify } from '../cloud/notification.service';
|
||||
import { notify, getGlobalNotifyConfigs, sendToChannels } from '../cloud/notification.service';
|
||||
import { CLOUD_LABELS } from '../config/cloud-constants';
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
@@ -19,6 +19,7 @@ export interface DailyReportConfig {
|
||||
includeSaves: boolean;
|
||||
includeStorage: boolean;
|
||||
includeUsers: boolean;
|
||||
channels?: string[]; // 指定推送通道,空=全部全局通道
|
||||
}
|
||||
|
||||
export interface DailyReport {
|
||||
@@ -53,6 +54,7 @@ export function getDailyReportConfig(): DailyReportConfig {
|
||||
includeSaves: parsed.includeSaves !== false,
|
||||
includeStorage: parsed.includeStorage !== false,
|
||||
includeUsers: parsed.includeUsers !== false,
|
||||
channels: parsed.channels || [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -286,7 +288,16 @@ export async function runDailyReportIfScheduled(): Promise<void> {
|
||||
cfg.includeUsers
|
||||
);
|
||||
|
||||
notify('CloudSearch 每日汇报', content, 'info');
|
||||
// 通道过滤:如果配置了 channels 则只发指定通道
|
||||
if (cfg.channels && cfg.channels.length > 0) {
|
||||
const allChannels = getGlobalNotifyConfigs();
|
||||
const filtered = allChannels.filter(c => cfg.channels!.includes(c.name));
|
||||
if (filtered.length > 0) {
|
||||
sendToChannels(filtered, 'CloudSearch 每日汇报', content, 'info').catch(() => {});
|
||||
}
|
||||
} else {
|
||||
notify('CloudSearch 每日汇报', content, 'info');
|
||||
}
|
||||
|
||||
// Record last run
|
||||
const { getDb } = require('../database/database');
|
||||
@@ -332,6 +343,15 @@ export async function sendTestDailyReport(): Promise<{ success: boolean; report:
|
||||
cfg.includeStorage,
|
||||
cfg.includeUsers
|
||||
);
|
||||
notify('CloudSearch 每日汇报 [测试]', content, 'info');
|
||||
const tcfg = getDailyReportConfig();
|
||||
if (tcfg.channels && tcfg.channels.length > 0) {
|
||||
const allChannels = getGlobalNotifyConfigs();
|
||||
const filtered = allChannels.filter(c => tcfg.channels!.includes(c.name));
|
||||
if (filtered.length > 0) {
|
||||
sendToChannels(filtered, 'CloudSearch 每日汇报 [测试]', content, 'info').catch(() => {});
|
||||
}
|
||||
} else {
|
||||
notify('CloudSearch 每日汇报 [测试]', content, 'info');
|
||||
}
|
||||
return { success: true, report };
|
||||
}
|
||||
|
||||
@@ -161,20 +161,14 @@ export class LinkValidator {
|
||||
return { url, status: 'valid', cloudType, checkedAt, message: summary };
|
||||
}
|
||||
|
||||
// 2. 自定义确认关键词(用户配置的"有效"信号)
|
||||
const validKeywords = loadCustomKeywords('link_valid_keywords');
|
||||
if (validKeywords.some(kw => summary.includes(kw))) {
|
||||
return { url, status: 'valid', cloudType, checkedAt, message: summary };
|
||||
}
|
||||
|
||||
// 3. 自定义失效关键词(用户配置的"失效"信号)
|
||||
const invalidKeywords = loadCustomKeywords('link_invalid_keywords');
|
||||
if (invalidKeywords.some(kw => summary.includes(kw))) {
|
||||
return { url, status: 'invalid', cloudType, checkedAt, message: summary };
|
||||
}
|
||||
|
||||
// 4. 其余全部返回 unknown
|
||||
return { url, status: 'unknown', cloudType, checkedAt, message: summary || '盘搜无法确认' };
|
||||
// 4. 其余全部返回 valid(无失效关键词命中则有效)
|
||||
return { url, status: 'valid', cloudType, checkedAt, message: summary || '盘搜验证通过' };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user