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.
|
Copyright (c) Microsoft Corporation.
|
||||||
|
|
||||||
Permission to use, copy, modify, and/or distribute this software for any
|
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>F12</code> 打开开发者工具 → 切换到 <strong>网络 (Network)</strong> 选项卡</li>
|
||||||
<li>刷新页面,在请求列表中点击任意一个请求(如 <code>account/info</code>)</li>
|
<li>刷新页面,在请求列表中点击任意一个请求(如 <code>account/info</code>)</li>
|
||||||
<li>在右侧 <strong>请求头 (Request Headers)</strong> 中找到 <code>Cookie</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>
|
||||||
<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">
|
<link rel="stylesheet" crossorigin href="/assets/index-Ekbe64zQ.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -346,9 +346,10 @@ export async function getAllNotifierProviders(): Promise<Record<string, { name:
|
|||||||
|
|
||||||
export async function testNotifyChannel(
|
export async function testNotifyChannel(
|
||||||
channelType: string,
|
channelType: string,
|
||||||
configId?: number
|
configId?: number,
|
||||||
|
params?: Record<string, any>
|
||||||
): Promise<{ success: boolean; message: string }> {
|
): 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
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -178,7 +178,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column label="时间" width="140">
|
<el-table-column label="时间" min-width="155">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span :title="row.created_at">{{ formatTime(row.created_at) }}</span>
|
<span :title="row.created_at">{{ formatTime(row.created_at) }}</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -192,6 +192,12 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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">
|
<el-table-column label="状态" width="72" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tooltip :content="statusTip(row.status)" placement="top">
|
<el-tooltip :content="statusTip(row.status)" placement="top">
|
||||||
|
|||||||
@@ -158,21 +158,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="IP 归属地查询">
|
<el-form-item label="IP 归属地查询">
|
||||||
<div style="display: flex; gap: 8px; align-items: center; width: 100%;">
|
<div style="display:flex; flex-direction:column; gap:8px; width:100%;">
|
||||||
<el-input
|
<el-select v-model="configs.ip_geo_provider" placeholder="选择接口" style="max-width:260px">
|
||||||
v-model="configs.ip_geo_api_url"
|
<el-option label="接口盒子 (apihz.cn)" value="apihz" />
|
||||||
placeholder="https://cn.apihz.cn/api/ip/chaapi.php?id=xxx&key=***&ip={ip}&td=0"
|
</el-select>
|
||||||
style="max-width: 360px"
|
<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-button type="primary" :loading="ipGeoTesting" @click="handleTestIpGeo" size="default" style="width: 100px;">
|
<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 ? "测试中..." : "验证接口" }}
|
{{ ipGeoTesting ? "测试中..." : "验证接口" }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
<div class="form-tip">用于查询用户 IP 归属地(搜索/转存记录中显示)。留空则不做 IP 归属地查询。</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>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-divider content-position="left">Redis 缓存</el-divider>
|
<el-divider content-position="left">Redis 缓存</el-divider>
|
||||||
@@ -522,22 +519,50 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-divider content-position="left">全局事件开关</el-divider>
|
<el-divider content-position="left">全局事件开关</el-divider>
|
||||||
<div style="display:flex; flex-direction:column; gap:6px;">
|
<div style="display:grid; grid-template-columns:repeat(2,1fr); gap:10px;">
|
||||||
<div style="display:flex; align-items:center; gap:8px;">
|
<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;">
|
||||||
<el-switch v-model="globalNotifyForm.events.on_save_success" active-text="转存成功" />
|
<div style="display:flex; align-items:center; justify-content:space-between;">
|
||||||
<el-button size="small" text type="primary" @click="openTemplateEditor('on_save_success')">✏️ 编辑模板</el-button>
|
<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>
|
||||||
<div style="display:flex; align-items:center; gap:8px;">
|
<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;">
|
||||||
<el-switch v-model="globalNotifyForm.events.on_save_fail" active-text="转存失败" />
|
<div style="display:flex; align-items:center; justify-content:space-between;">
|
||||||
<el-button size="small" text type="primary" @click="openTemplateEditor('on_save_fail')">✏️ 编辑模板</el-button>
|
<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>
|
||||||
<div style="display:flex; align-items:center; gap:8px;">
|
<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;">
|
||||||
<el-switch v-model="globalNotifyForm.events.on_cookie_expire" active-text="Cookie过期" />
|
<div style="display:flex; align-items:center; justify-content:space-between;">
|
||||||
<el-button size="small" text type="primary" @click="openTemplateEditor('on_cookie_expire')">✏️ 编辑模板</el-button>
|
<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>
|
||||||
<div style="display:flex; align-items:center; gap:8px;">
|
<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;">
|
||||||
<el-switch v-model="globalNotifyForm.events.on_cleanup" active-text="清理完成" />
|
<div style="display:flex; align-items:center; justify-content:space-between;">
|
||||||
<el-button size="small" text type="primary" @click="openTemplateEditor('on_cleanup')">✏️ 编辑模板</el-button>
|
<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>
|
</div>
|
||||||
<div class="form-tip" style="margin-top:8px;">全局推送作为兜底通道。设置了推送用户的网盘配置走用户推送,未设置的走全局推送。</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" />
|
<el-switch v-model="dailyReportForm.includeUsers" active-text="用户数" :disabled="!dailyReportForm.enabled" />
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</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="上次发送">
|
<el-form-item label="上次发送">
|
||||||
<span>{{ dailyReportLastRun || '从未发送' }}</span>
|
<span>{{ dailyReportLastRun || '从未发送' }}</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -797,7 +828,6 @@ const searchAllChannels = computed({
|
|||||||
set: (val: boolean) => { configs.search_all_channels = val ? 'true' : 'false' },
|
set: (val: boolean) => { configs.search_all_channels = val ? 'true' : 'false' },
|
||||||
})
|
})
|
||||||
|
|
||||||
const autoUpdateEnabled = computed({
|
|
||||||
get: () => String(configs.auto_update_enabled) === 'true',
|
get: () => String(configs.auto_update_enabled) === 'true',
|
||||||
set: (val: boolean) => { configs.auto_update_enabled = val ? 'true' : 'false' },
|
set: (val: boolean) => { configs.auto_update_enabled = val ? 'true' : 'false' },
|
||||||
})
|
})
|
||||||
@@ -812,6 +842,7 @@ const dailyReportForm = reactive({
|
|||||||
includeSaves: true,
|
includeSaves: true,
|
||||||
includeStorage: true,
|
includeStorage: true,
|
||||||
includeUsers: true,
|
includeUsers: true,
|
||||||
|
channels: [] as string[],
|
||||||
})
|
})
|
||||||
const dailyReportPreviewing = ref(false)
|
const dailyReportPreviewing = ref(false)
|
||||||
const dailyReportSending = ref(false)
|
const dailyReportSending = ref(false)
|
||||||
@@ -1187,7 +1218,12 @@ async function testGlobalChannel(channelName: string) {
|
|||||||
if (!ch || !ch._enabled) return
|
if (!ch || !ch._enabled) return
|
||||||
ch._testing = true
|
ch._testing = true
|
||||||
try {
|
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) {
|
if (result.success) {
|
||||||
ElMessage.success(result.message)
|
ElMessage.success(result.message)
|
||||||
} else {
|
} else {
|
||||||
@@ -1407,12 +1443,12 @@ async function handleTestProxy() {
|
|||||||
async function handleTestIpGeo() {
|
async function handleTestIpGeo() {
|
||||||
ipGeoTesting.value = true
|
ipGeoTesting.value = true
|
||||||
try {
|
try {
|
||||||
const url = String(configs.ip_geo_api_url || "")
|
const apiId = String(configs.ip_geo_api_id || "")
|
||||||
if (!url) {
|
if (!apiId) {
|
||||||
ElMessage.warning("请先输入 IP 归属地查询 API 地址")
|
ElMessage.warning("请先输入 API ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const result = await testExternalService({ type: "ip_geo", url })
|
const result = await testExternalService({ type: "ip_geo", url: apiId })
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
ElMessage.success("✅ IP 归属地接口可用 — " + result.info)
|
ElMessage.success("✅ IP 归属地接口可用 — " + result.info)
|
||||||
} else {
|
} else {
|
||||||
@@ -1993,5 +2029,14 @@ async function handleRemoveLogo() {
|
|||||||
.dot-ok { background: #67c23a; }
|
.dot-ok { background: #67c23a; }
|
||||||
.dot-err { background: #f56c6c; }
|
.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>
|
</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.
|
Copyright (c) Microsoft Corporation.
|
||||||
|
|
||||||
Permission to use, copy, modify, and/or distribute this software for any
|
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>F12</code> 打开开发者工具 → 切换到 <strong>网络 (Network)</strong> 选项卡</li>
|
||||||
<li>刷新页面,在请求列表中点击任意一个请求(如 <code>account/info</code>)</li>
|
<li>刷新页面,在请求列表中点击任意一个请求(如 <code>account/info</code>)</li>
|
||||||
<li>在右侧 <strong>请求头 (Request Headers)</strong> 中找到 <code>Cookie</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>
|
||||||
<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">
|
<link rel="stylesheet" crossorigin href="/assets/index-Ekbe64zQ.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle?
|
|||||||
const db = getDb();
|
const db = getDb();
|
||||||
const ipLocation = await lookupIpLocation(ipAddress || '');
|
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 ──
|
// ── Short-term dedup: prevent duplicate saves of the same URL within 60 seconds ──
|
||||||
const DEDUP_WINDOW_SEC = 60;
|
const DEDUP_WINDOW_SEC = 60;
|
||||||
let dedupCutoff = '';
|
let dedupCutoff = '';
|
||||||
@@ -58,35 +61,52 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle?
|
|||||||
dedupCutoff = recentCutoff.cutoff;
|
dedupCutoff = recentCutoff.cutoff;
|
||||||
|
|
||||||
const recentRecord = db.prepare(
|
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 >= ?
|
WHERE source_url = ? AND created_at >= ?
|
||||||
ORDER BY created_at DESC LIMIT 1`
|
ORDER BY created_at DESC LIMIT 1`
|
||||||
).get(shareUrl, dedupCutoff) as {
|
).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;
|
error_message: string | null; folder_name: string | null; original_folder_name: string | null;
|
||||||
} | undefined;
|
} | undefined;
|
||||||
|
|
||||||
if (recentRecord) {
|
if (recentRecord) {
|
||||||
const alreadySaved = recentRecord.status === 'success' || recentRecord.status === 'reused';
|
const alreadySaved = recentRecord.status === 'success' || recentRecord.status === 'reused';
|
||||||
if (alreadySaved && recentRecord.share_url) {
|
if (alreadySaved && recentRecord.share_url) {
|
||||||
console.log(`[Share] 🛡️ Dedup: ${shareUrl} was saved ${DEDUP_WINDOW_SEC}s ago (status=${recentRecord.status}), returning existing share link`);
|
// Validate the cached link before returning — avoid returning an invalid link
|
||||||
db.prepare(
|
let dedupLinkInvalid = false;
|
||||||
`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)
|
try {
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
const { LinkValidator: DedupLinkValidator } = await import('../validation/link-validator.service');
|
||||||
).run(
|
const dedupValidator = new DedupLinkValidator();
|
||||||
cloudType, sourceTitle || null, shareUrl, cloudType,
|
const dedupValidation = await dedupValidator.validate(recentRecord.share_url, 'quark');
|
||||||
recentRecord.share_url, recentRecord.share_pwd || null,
|
if (dedupValidation.status !== 'valid') {
|
||||||
null, 0, 0, 0, 'reused', null,
|
dedupLinkInvalid = true;
|
||||||
recentRecord.folder_name || null, recentRecord.original_folder_name || null,
|
console.log(`[Share] 🛡️ Dedup link invalid (${dedupValidation.message}), falling through to normal save`);
|
||||||
ipAddress || null, ipLocation, localTimestamp(),
|
retrySave = true;
|
||||||
);
|
}
|
||||||
return {
|
} catch (err: any) {
|
||||||
success: true,
|
console.log(`[Share] 🛡️ Dedup validation error: ${err.message}, falling through`);
|
||||||
message: `🛡️ 此资源刚在 ${DEDUP_WINDOW_SEC} 秒内转存过,直接返回已有分享链接`,
|
dedupLinkInvalid = true;
|
||||||
share_url: recentRecord.share_url, shareUrl: recentRecord.share_url,
|
}
|
||||||
sharePwd: recentRecord.share_pwd || '', folderName: '',
|
if (!dedupLinkInvalid) {
|
||||||
file_count: 0, folder_count: 0, duration_ms: 0,
|
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) {
|
} catch (err: any) {
|
||||||
@@ -98,10 +118,10 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle?
|
|||||||
if (reuseEnabled !== 'false') {
|
if (reuseEnabled !== 'false') {
|
||||||
try {
|
try {
|
||||||
const existing = db.prepare(
|
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 != ''
|
WHERE source_url = ? AND status IN ('success', 'reused') AND share_url IS NOT NULL AND share_url != ''
|
||||||
ORDER BY created_at DESC LIMIT 1`
|
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) {
|
if (existing?.share_url) {
|
||||||
const { LinkValidator } = await import('../validation/link-validator.service');
|
const { LinkValidator } = await import('../validation/link-validator.service');
|
||||||
@@ -123,7 +143,7 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle?
|
|||||||
).run(
|
).run(
|
||||||
cloudType, sourceTitle || null, shareUrl, cloudType,
|
cloudType, sourceTitle || null, shareUrl, cloudType,
|
||||||
existing.share_url, existing.share_pwd || null,
|
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,
|
existing.folder_name || null, existing.original_folder_name || null,
|
||||||
ipAddress || null, ipLocation, localTimestamp(),
|
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,
|
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`);
|
console.log(`[Share] Existing share link for ${shareUrl} is invalid/expired, will re-save`);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -155,7 +176,7 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle?
|
|||||||
switch (cloudType) {
|
switch (cloudType) {
|
||||||
case 'quark': {
|
case 'quark': {
|
||||||
const driver = new QuarkDriver({ cookie: config.cookie!, nickname: config.nickname });
|
const driver = new QuarkDriver({ cookie: config.cookie!, nickname: config.nickname });
|
||||||
driverResult = await driver.saveFromShare(shareUrl, sourceTitle);
|
driverResult = await driver.saveFromShare(shareUrl, sourceTitle, retrySave);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'baidu': {
|
case 'baidu': {
|
||||||
@@ -215,12 +236,12 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle?
|
|||||||
}
|
}
|
||||||
|
|
||||||
db.prepare(
|
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)
|
`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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||||
).run(
|
).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,
|
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',
|
durationMs, driverResult.success ? 'success' : 'failed',
|
||||||
driverResult.success ? null : driverResult.message,
|
driverResult.success ? null : driverResult.message,
|
||||||
driverResult.folderName || null, driverResult.originalFolderName || null,
|
driverResult.folderName || null, driverResult.originalFolderName || null,
|
||||||
@@ -247,9 +268,9 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle?
|
|||||||
).run(config.id);
|
).run(config.id);
|
||||||
|
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`INSERT INTO save_records (source_type, source_url, target_cloud, duration_ms, status, error_message, ip_address, ip_location, created_at)
|
`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 (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||||
).run(cloudType, shareUrl, cloudType, durationMs, 'failed', errorMessage, ipAddress || null, ipLocation, localTimestamp());
|
).run(cloudType, shareUrl, cloudType, config.id, config.promotion_account || null, durationMs, 'failed', errorMessage, ipAddress || null, ipLocation, localTimestamp());
|
||||||
|
|
||||||
return { success: false, message: errorMessage };
|
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;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
verification_status?: string;
|
verification_status?: string;
|
||||||
|
promotion_account?: string;
|
||||||
|
cloud_type_uid?: string;
|
||||||
|
cookie_uid?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Cookie UID Extraction ────────────────────────────────────────
|
// ── Cookie UID Extraction ────────────────────────────────────────
|
||||||
@@ -382,6 +385,7 @@ export async function getAndValidateCredential(cloudType: string): Promise<Crede
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.cookie = cookie;
|
||||||
return {
|
return {
|
||||||
valid: true,
|
valid: true,
|
||||||
config,
|
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 toKeep = [];
|
||||||
const extensions = getSusExtensions();
|
const extensions = getSusExtensions();
|
||||||
for (const file of files) {
|
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);
|
const isSusExt = extensions.includes(ext);
|
||||||
if (containsAdKeyword(file.file_name, keywords) || isSusExt) {
|
if (containsAdKeyword(file.file_name, keywords) || isSusExt) {
|
||||||
toDelete.push(file.fid);
|
toDelete.push(file.fid);
|
||||||
|
|||||||
@@ -1,4 +1,46 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
const HOMOPHONE_MAP = {
|
||||||
|
// 网盘热门番名 — 谐音替换 (same sound, different char)
|
||||||
|
'斗': '陡', '破': '坡', '苍': '仓', '穹': '穷',
|
||||||
|
'完': '玩', '美': '每', '世': '士', '界': '介',
|
||||||
|
'凡': '烦', '人': '仁', '修': '休', '罗': '络',
|
||||||
|
'仙': '先', '逆': '腻', '遮': '折', '天': '添',
|
||||||
|
'吞': '屯', '噬': '逝', '大': '达', '主': '嘱', '宰': '崽',
|
||||||
|
'星': '惺', '辰': '晨', '变': '便', '一': '伊', '念': '捻',
|
||||||
|
'永': '泳', '恒': '横', '神': '申', '墓': '暮', '长': '尝', '生': '甥',
|
||||||
|
'剑': '箭', '来': '莱', '诡': '鬼', '秘': '蜜',
|
||||||
|
'全': '泉', '职': '值', '盘': '磐', '龙': '笼',
|
||||||
|
'雪': '血', '鹰': '莺', '莽': '蟒', '荒': '慌', '纪': '记',
|
||||||
|
'珠': '株', '王': '亡', '座': '坐', '牧': '木', '记': '计',
|
||||||
|
'沧': '舱', '元': '圆', '图': '涂', '紫': '仔', '川': '串',
|
||||||
|
'百': '白', '炼': '恋', '成': '程', '饶': '绕', '命': '冥',
|
||||||
|
// 通用谐音替换
|
||||||
|
'的': '得', '了': '啦', '是': '事', '不': '布', '我': '窝',
|
||||||
|
'你': '尼', '他': '她', '有': '友', '和': '合', '与': '予',
|
||||||
|
'上': '尚', '下': '夏', '中': '忠', '第': '弟', '集': '级',
|
||||||
|
'话': '划', '季': '际', '年': '念', '月': '阅', '日': '曰',
|
||||||
|
'新': '心', '版': '板', '高': '糕', '清': '青', '原': '源',
|
||||||
|
'小': '晓', '片': '篇', '视': '市', '频': '贫', '道': '到',
|
||||||
|
'动': '洞', '画': '话', '声': '升', '音': '因', '文': '闻',
|
||||||
|
'明': '名', '暗': '黯', '光': '广', '影': '映', '色': '瑟',
|
||||||
|
'风': '疯', '雨': '语', '花': '华', '国': '果', '家': '佳',
|
||||||
|
'战': '站', '争': '挣', '士': '仕', '兵': '宾',
|
||||||
|
'皇': '惶', '帝': '谛', '魔': '磨', '鬼': '诡', '怪': '乖',
|
||||||
|
'精': '经', '灵': '铃', '妖': '夭', '武': '舞', '侠': '狭',
|
||||||
|
'杀': '刹', '血': '雪', '刀': '叨', '枪': '呛', '炮': '泡',
|
||||||
|
'时': '石', '空': '孔', '前': '钱', '后': '厚', '东': '冬',
|
||||||
|
'南': '难', '西': '夕', '北': '备', '开': '凯', '关': '官',
|
||||||
|
'出': '初', '进': '近', '去': '趣',
|
||||||
|
'短': '短', '多': '多', '少': '少', '真': '贞', '假': '价',
|
||||||
|
'好': '郝', '坏': '怀', '对': '队', '错': '措', '以': '已',
|
||||||
|
'从': '从', '被': '被', '把': '把', '将': '将', '在': '在',
|
||||||
|
'但': '但', '就': '就', '才': '才', '也': '也', '很': '狠',
|
||||||
|
'又': '又', '再': '再', '更': '更', '最': '最', '总': '总',
|
||||||
|
'共': '共', '只': '只', '各': '各', '每': '每', '任': '任',
|
||||||
|
'所': '所', '该': '该', '本': '本',
|
||||||
|
};
|
||||||
const NOISE_CJK = '的了在是不有会可对所之也同与及但或如且乃而岂乎焉兮哉亦犹尚乃其若故盖诸焉欤' +
|
const NOISE_CJK = '的了在是不有会可对所之也同与及但或如且乃而岂乎焉兮哉亦犹尚乃其若故盖诸焉欤' +
|
||||||
'么个着过把对为从以到说时要就这那和上人家下能出得发来年心开物力些长样吧啊哦嗯嚯哇咯呗哟嘿呵哈';
|
'么个着过把对为从以到说时要就这那和上人家下能出得发来年心开物力些长样吧啊哦嗯嚯哇咯呗哟嘿呵哈';
|
||||||
// ==================== Helpers ====================
|
// ==================== Helpers ====================
|
||||||
@@ -92,7 +134,7 @@ export function magicRenameDir(dirName) {
|
|||||||
const noiseCount = Math.random() < 0.3 ? (Math.random() < 0.5 ? 1 : 2) : 0;
|
const noiseCount = Math.random() < 0.3 ? (Math.random() < 0.5 ? 1 : 2) : 0;
|
||||||
for (let n = 0; n < noiseCount; n++) {
|
for (let n = 0; n < noiseCount; n++) {
|
||||||
const pos = Math.floor(Math.random() * (baseName.length + 1));
|
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);
|
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
|
* 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 {
|
try {
|
||||||
// Parse share token from URL
|
// Parse share token from URL
|
||||||
const urlObj = new URL(shareUrl);
|
const urlObj = new URL(shareUrl);
|
||||||
@@ -72,9 +72,19 @@ export async function saveFromShare(cookie, nickname, shareUrl, sourceTitle) {
|
|||||||
const saveDirName = quark_api.dailyFolderName();
|
const saveDirName = quark_api.dailyFolderName();
|
||||||
console.log(`[Quark] saveFromShare: looking for/create dir "${saveDirName}"`);
|
console.log(`[Quark] saveFromShare: looking for/create dir "${saveDirName}"`);
|
||||||
const saveDirFid = await findOrCreateDir(cookie, saveDirName);
|
const saveDirFid = await findOrCreateDir(cookie, saveDirName);
|
||||||
const targetPdirFid = saveDirFid || '0';
|
let targetPdirFid = saveDirFid || '0';
|
||||||
|
let retrySubFolderFid = '';
|
||||||
if (saveDirFid) {
|
if (saveDirFid) {
|
||||||
console.log(`[Quark] Using save directory: ${saveDirName} (fid: ${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 {
|
else {
|
||||||
console.log(`[Quark] WARNING: failed to create/find dir "${saveDirName}", saving to root`);
|
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];
|
shareFid = savedFids[0];
|
||||||
savedFolderName = topFiles[0]?.file_name || '';
|
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
|
// Step 6: Create share link FIRST (before rename), so all files are guaranteed to be shared
|
||||||
await quark_api.humanDelay();
|
await quark_api.humanDelay();
|
||||||
let shareUrlResult = '';
|
let shareUrlResult = '';
|
||||||
@@ -239,13 +253,13 @@ export async function saveFromShare(cookie, nickname, shareUrl, sourceTitle) {
|
|||||||
/**
|
/**
|
||||||
* Create a new directory at root.
|
* Create a new directory at root.
|
||||||
*/
|
*/
|
||||||
export async function createDir(cookie, dirName) {
|
export async function createDir(cookie, dirName, parentFid = '0') {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`https://drive-pc.quark.cn/1/clouddrive/file?${quark_api.makeQuery()}`, {
|
const resp = await fetch(`https://drive-pc.quark.cn/1/clouddrive/file?${quark_api.makeQuery()}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { ...quark_api.getHeaders(cookie), 'Content-Type': 'application/json' },
|
headers: { ...quark_api.getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
pdir_fid: '0',
|
pdir_fid: parentFid,
|
||||||
file_name: dirName,
|
file_name: dirName,
|
||||||
dir: true,
|
dir: true,
|
||||||
dir_path: '',
|
dir_path: '',
|
||||||
@@ -268,9 +282,9 @@ export async function createDir(cookie, dirName) {
|
|||||||
/**
|
/**
|
||||||
* Find an existing directory by name, or create it if not found.
|
* 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 {
|
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);
|
const existing = rootFiles.find(f => f.dir && f.file_name === dirName);
|
||||||
if (existing?.fid) {
|
if (existing?.fid) {
|
||||||
console.log(`[Quark] Found existing daily folder: ${dirName} (fid: ${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) {
|
catch (err) {
|
||||||
console.log(`[Quark] findOrCreateDir list error: ${err.message}`);
|
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'}`);
|
console.log(`[Quark] createDir result for "${dirName}": ${fid || 'null'}`);
|
||||||
return fid;
|
return fid;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,8 +98,8 @@ export class QuarkDriver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Storage (Save from Share) ====================
|
// ==================== Storage (Save from Share) ====================
|
||||||
async saveFromShare(shareUrl: string, sourceTitle?: string): Promise<any> {
|
async saveFromShare(shareUrl: string, sourceTitle?: string, retrySave?: boolean): Promise<any> {
|
||||||
return saveFromShare(this.cookie, this.config.nickname || '', shareUrl, sourceTitle || '');
|
return saveFromShare(this.cookie, this.config.nickname || '', shareUrl, sourceTitle || '', retrySave);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createDir(dirName: string): Promise<any> {
|
async createDir(dirName: string): Promise<any> {
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ export async function lookupIpLocation(ip: string): Promise<string | null> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const apiUrlTemplate = getSystemConfig('ip_geo_api_url');
|
const apiId = getSystemConfig('ip_geo_api_id');
|
||||||
if (!apiUrlTemplate) return null;
|
const apiKey = getSystemConfig('ip_geo_api_key');
|
||||||
const url = apiUrlTemplate.replace('{ip}', encodeURIComponent(ip));
|
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) });
|
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
||||||
if (!res.ok) return null;
|
if (!res.ok) return null;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function getGlobalNotifyConfig(): Record<string, any> {
|
|||||||
try { return JSON.parse(raw); } catch { return {}; }
|
try { return JSON.parse(raw); } catch { return {}; }
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGlobalNotifyConfigs(): NotifyChannel[] {
|
export function getGlobalNotifyConfigs(): NotifyChannel[] {
|
||||||
const raw = getSystemConfig('global_notify_config') || '{}';
|
const raw = getSystemConfig('global_notify_config') || '{}';
|
||||||
let globalConfig: any = {};
|
let globalConfig: any = {};
|
||||||
try { globalConfig = JSON.parse(raw); } catch {}
|
try { globalConfig = JSON.parse(raw); } catch {}
|
||||||
@@ -56,7 +56,7 @@ function checkEventEnabled(eventName: string): boolean {
|
|||||||
|
|
||||||
// ======================== Core send ========================
|
// ======================== 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) {
|
for (const ch of channels) {
|
||||||
try {
|
try {
|
||||||
await notifyWith(ch.name, {
|
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 {
|
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 }> {
|
function getEventTemplates(): Record<string, { title?: string; content: string }> {
|
||||||
@@ -141,7 +141,7 @@ export function notifyConfigEvent(
|
|||||||
// Find matching push user by cloud_configs.promotion_account
|
// Find matching push user by cloud_configs.promotion_account
|
||||||
const pushUser = findPushUserForConfig(configId);
|
const pushUser = findPushUserForConfig(configId);
|
||||||
if (!pushUser) {
|
if (!pushUser) {
|
||||||
notifyEvent(eventName, title, content, level);
|
notifyEvent(eventName, title, content, level, templateVars);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ export function notifyConfigEvent(
|
|||||||
if (userChannels.length > 0) {
|
if (userChannels.length > 0) {
|
||||||
sendToChannels(userChannels, title, content, level).catch(() => {});
|
sendToChannels(userChannels, title, content, level).catch(() => {});
|
||||||
} else {
|
} 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[] = [
|
const params: NotifierParam[] = [
|
||||||
{ key: 'key', label: 'Bark Key', type: 'text', required: true, placeholder: 'xxxxxxxxxxxxxxxxx' },
|
{ 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: '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 = {
|
export const barkNotifier: Notifier = {
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
|||||||
|
|
||||||
const params: NotifierParam[] = [
|
const params: NotifierParam[] = [
|
||||||
{ key: 'webhook_url', label: 'Webhook URL', type: 'url', required: true, placeholder: 'https://oapi.dingtalk.com/robot/send?access_token=xxx' },
|
{ 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 = {
|
export const dingtalkNotifier: Notifier = {
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
|||||||
|
|
||||||
const params: NotifierParam[] = [
|
const params: NotifierParam[] = [
|
||||||
{ key: 'webhook_url', label: 'Webhook URL', type: 'url', required: true, placeholder: 'https://discord.com/api/webhooks/...' },
|
{ 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 = {
|
export const discordNotifier: Notifier = {
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
|||||||
const params: NotifierParam[] = [
|
const params: NotifierParam[] = [
|
||||||
{ key: 'server', label: '服务器地址', type: 'url', required: true, placeholder: 'https://gotify.example.com' },
|
{ 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: '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 = {
|
export const gotifyNotifier: Notifier = {
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
|||||||
|
|
||||||
const params: NotifierParam[] = [
|
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: '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 = {
|
export const larkNotifier: Notifier = {
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
|||||||
const params: NotifierParam[] = [
|
const params: NotifierParam[] = [
|
||||||
{ key: 'topic', label: 'Topic', type: 'text', required: true, placeholder: 'my-notification-topic' },
|
{ 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: '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 = {
|
export const ntfyNotifier: Notifier = {
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
|||||||
|
|
||||||
const params: NotifierParam[] = [
|
const params: NotifierParam[] = [
|
||||||
{ key: 'token', label: 'Token', type: 'password', required: true, placeholder: 'pushplus token' },
|
{ 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 = {
|
export const pushplusNotifier: Notifier = {
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
|||||||
const params: NotifierParam[] = [
|
const params: NotifierParam[] = [
|
||||||
{ key: 'key', label: 'API Key', type: 'password', required: true, placeholder: 'Qmsg API Key' },
|
{ key: 'key', label: 'API Key', type: 'password', required: true, placeholder: 'Qmsg API Key' },
|
||||||
{ key: 'qq', label: 'QQ 号', type: 'text', required: false, placeholder: '留空则推送到所有绑定QQ' },
|
{ 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 = {
|
export const qmsgNotifier: Notifier = {
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
|||||||
|
|
||||||
const params: NotifierParam[] = [
|
const params: NotifierParam[] = [
|
||||||
{ key: 'sendkey', label: 'SendKey', type: 'password', required: true, placeholder: 'Server酱 SendKey' },
|
{ 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 = {
|
export const serverchanNotifier: Notifier = {
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
|||||||
|
|
||||||
const params: NotifierParam[] = [
|
const params: NotifierParam[] = [
|
||||||
{ key: 'sendkey', label: 'SendKey', type: 'password', required: true, placeholder: 'Server酱 Turbo SendKey' },
|
{ 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 = {
|
export const serverchanturboNotifier: Notifier = {
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ const params: NotifierParam[] = [
|
|||||||
{ key: 'pass', label: '密码/授权码', type: 'password', required: true, placeholder: 'SMTP 授权码' },
|
{ key: 'pass', label: '密码/授权码', type: 'password', required: true, placeholder: 'SMTP 授权码' },
|
||||||
{ key: 'from', label: '发件人', type: 'text', required: true, placeholder: 'sender@example.com' },
|
{ key: 'from', label: '发件人', type: 'text', required: true, placeholder: 'sender@example.com' },
|
||||||
{ key: 'to', label: '收件人', type: 'text', required: true, placeholder: 'receiver@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 = {
|
export const smtpNotifier: Notifier = {
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
|||||||
const params: NotifierParam[] = [
|
const params: NotifierParam[] = [
|
||||||
{ key: 'token', label: 'Bot Token', type: 'password', required: true, placeholder: '123456:ABC-def' },
|
{ 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: '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 = {
|
export const telegramNotifier: Notifier = {
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
|||||||
|
|
||||||
const params: NotifierParam[] = [
|
const params: NotifierParam[] = [
|
||||||
{ key: 'url', label: 'Webhook URL', type: 'url', required: true, placeholder: 'https://example.com/webhook' },
|
{ 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 = {
|
export const webhookNotifier: Notifier = {
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.
|
|||||||
|
|
||||||
const params: NotifierParam[] = [
|
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: '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 = {
|
export const wechatWorkBotNotifier: Notifier = {
|
||||||
|
|||||||
@@ -315,6 +315,9 @@ function seedSystemConfigs(db: Database.Database): void {
|
|||||||
{ key: 'save_reuse_enabled', value: 'true', description: '启用分享链接复用(相同原始链接不再重复转存,直接复用之前的分享链接)' },
|
{ key: 'save_reuse_enabled', value: 'true', description: '启用分享链接复用(相同原始链接不再重复转存,直接复用之前的分享链接)' },
|
||||||
{ key: 'cleanup_last_run', value: '', description: '上次自动清理时间' },
|
{ key: 'cleanup_last_run', value: '', description: '上次自动清理时间' },
|
||||||
{ key: 'cleanup_last_stats', value: '', description: '上次清理结果统计(JSON)' },
|
{ 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(
|
const insert = db.prepare(
|
||||||
'INSERT OR IGNORE INTO system_configs (key, value, description) VALUES (?, ?, ?)'
|
'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;
|
break;
|
||||||
}
|
}
|
||||||
case 'ip_geo': {
|
case 'ip_geo': {
|
||||||
const geoUrl = url || getSystemConfig('ip_geo_api_url') || '';
|
const apiId = url || getSystemConfig('ip_geo_api_id') || '';
|
||||||
if (!geoUrl) {
|
const apiKey = getSystemConfig('ip_geo_api_key') || '';
|
||||||
res.json({ ok: false, info: '请先输入 IP 归属地查询 API 地址' });
|
if (!apiId || !apiKey) {
|
||||||
|
res.json({ ok: false, info: '请先配置 IP 归属地 API ID 和 Key' });
|
||||||
return;
|
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 response = await fetch(testUrl, { signal: AbortSignal.timeout(8000) });
|
||||||
const data: any = await response.json();
|
const data: any = await response.json();
|
||||||
const latency = Date.now() - start;
|
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 ? '连接成功' : '响应格式不符' });
|
res.json({ ok: valid, latency, info: valid ? '连接成功' : '响应格式不符' });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { getDb } from '../database/database';
|
import { getDb } from '../database/database';
|
||||||
import { getSystemConfig } from '../admin/system-config.service';
|
import { getSystemConfig } from '../admin/system-config.service';
|
||||||
import { getCloudConfigs } from '../cloud/credential.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';
|
import { CLOUD_LABELS } from '../config/cloud-constants';
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════
|
// ═══════════════════════════════════════════════
|
||||||
@@ -19,6 +19,7 @@ export interface DailyReportConfig {
|
|||||||
includeSaves: boolean;
|
includeSaves: boolean;
|
||||||
includeStorage: boolean;
|
includeStorage: boolean;
|
||||||
includeUsers: boolean;
|
includeUsers: boolean;
|
||||||
|
channels?: string[]; // 指定推送通道,空=全部全局通道
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DailyReport {
|
export interface DailyReport {
|
||||||
@@ -53,6 +54,7 @@ export function getDailyReportConfig(): DailyReportConfig {
|
|||||||
includeSaves: parsed.includeSaves !== false,
|
includeSaves: parsed.includeSaves !== false,
|
||||||
includeStorage: parsed.includeStorage !== false,
|
includeStorage: parsed.includeStorage !== false,
|
||||||
includeUsers: parsed.includeUsers !== false,
|
includeUsers: parsed.includeUsers !== false,
|
||||||
|
channels: parsed.channels || [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,7 +288,16 @@ export async function runDailyReportIfScheduled(): Promise<void> {
|
|||||||
cfg.includeUsers
|
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
|
// Record last run
|
||||||
const { getDb } = require('../database/database');
|
const { getDb } = require('../database/database');
|
||||||
@@ -332,6 +343,15 @@ export async function sendTestDailyReport(): Promise<{ success: boolean; report:
|
|||||||
cfg.includeStorage,
|
cfg.includeStorage,
|
||||||
cfg.includeUsers
|
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 };
|
return { success: true, report };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,20 +161,14 @@ export class LinkValidator {
|
|||||||
return { url, status: 'valid', cloudType, checkedAt, message: summary };
|
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. 自定义失效关键词(用户配置的"失效"信号)
|
// 3. 自定义失效关键词(用户配置的"失效"信号)
|
||||||
const invalidKeywords = loadCustomKeywords('link_invalid_keywords');
|
const invalidKeywords = loadCustomKeywords('link_invalid_keywords');
|
||||||
if (invalidKeywords.some(kw => summary.includes(kw))) {
|
if (invalidKeywords.some(kw => summary.includes(kw))) {
|
||||||
return { url, status: 'invalid', cloudType, checkedAt, message: summary };
|
return { url, status: 'invalid', cloudType, checkedAt, message: summary };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 其余全部返回 unknown
|
// 4. 其余全部返回 valid(无失效关键词命中则有效)
|
||||||
return { url, status: 'unknown', cloudType, checkedAt, message: summary || '盘搜无法确认' };
|
return { url, status: 'valid', cloudType, checkedAt, message: summary || '盘搜验证通过' };
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user