14 Commits

Author SHA1 Message Date
94d8fa455d v0.3.51: Fix broken seed, retry save with random suffix folder for new share link 2026-05-18 01:38:10 +08:00
9f959ca87b v0.3.50: Retry save creates new folder with random suffix to produce genuinely different share link 2026-05-18 01:24:37 +08:00
7f4ab50557 v0.3.49: Dedup validation — validate cached link before returning to avoid showing invalid links 2026-05-18 01:06:28 +08:00
e4e3884ffc fix: 转存记录时间列155px+推广账号列140px,避免截断 2026-05-17 23:19:52 +08:00
a609379d20 v0.3.41 2026-05-17 23:12:50 +08:00
d78412646e v0.3.41: save_records新增推广账号/promotion_account字段 2026-05-17 23:12:46 +08:00
7e22c879b9 v0.3.40: 6组累积修复 — cookie解密/rename模块/通知模板/IP归属地 2026-05-17 23:04:00 +08:00
879d5bea95 refactor: IP归属地改为apihz.cn id+key配置,后端测试修复验证条件 2026-05-17 23:02:16 +08:00
8333d203db fix: applyTemplate用??替代||避免空字符串被回退为占位符 2026-05-17 22:46:00 +08:00
a51ffb4de3 fix: notifyEvent二次应用模板冲掉已替换变量 → 传递templateVars 2026-05-17 22:43:32 +08:00
1080c530a7 fix: 恢复丢失的 HOMOPHONE_MAP 同音字映射表 (从 v0.2.4) 2026-05-17 22:22:15 +08:00
b22cddc7f7 fix: quark-rename.js 缺少 crypto import 2026-05-17 22:14:48 +08:00
98b779a622 fix: getAndValidateCredential 返回加密cookie导致夸克API 401 2026-05-17 22:11:05 +08:00
cf2796666d v0.3.34: 每日汇报增加推送通道选择UI 2026-05-17 21:52:37 +08:00
71 changed files with 728 additions and 596 deletions

View File

@@ -1 +1 @@
0.3.31 0.3.51

View File

@@ -1 +1 @@
0.3.31 0.3.51

View File

@@ -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

View File

@@ -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};

View File

@@ -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};

View File

@@ -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};

View File

@@ -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

View File

@@ -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}

View 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

File diff suppressed because one or more lines are too long

View File

@@ -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>

View File

@@ -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
} }

View File

@@ -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">

View File

@@ -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 ID10014356" 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>

View File

@@ -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

View File

@@ -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};

View File

@@ -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};

View File

@@ -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};

View File

@@ -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

View File

@@ -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}

View 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

File diff suppressed because one or more lines are too long

View File

@@ -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>

View File

@@ -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 };
} }

View 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);
}
}
}

View File

@@ -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,

View File

@@ -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}`,
};
}
}

View File

@@ -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);

View File

@@ -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);
} }
} }

View File

@@ -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);
@@ -69,8 +69,10 @@ export async function saveFromShare(cookie, nickname, shareUrl, sourceTitle) {
const fidTokens = topFiles.map(f => f.share_fid_token); const fidTokens = topFiles.map(f => f.share_fid_token);
// 按日期创建/查找文件夹,每天的转存存入当天文件夹 // 按日期创建/查找文件夹,每天的转存存入当天文件夹
await quark_api.humanDelay(); await quark_api.humanDelay();
const saveDirName = quark_api.dailyFolderName(); const saveDirName = retrySave
console.log(`[Quark] saveFromShare: looking for/create dir "${saveDirName}"`); ? quark_api.dailyFolderName() + '_' + Math.random().toString(36).slice(2, 6)
: quark_api.dailyFolderName();
console.log(`[Quark] saveFromShare: looking for/create dir "${saveDirName}"${retrySave ? ' (retry)' : ''}`);
const saveDirFid = await findOrCreateDir(cookie, saveDirName); const saveDirFid = await findOrCreateDir(cookie, saveDirName);
const targetPdirFid = saveDirFid || '0'; const targetPdirFid = saveDirFid || '0';
if (saveDirFid) { if (saveDirFid) {

View File

@@ -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> {

View File

@@ -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;

View File

@@ -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);
} }
} }

View File

@@ -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 = {

View File

@@ -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 = {

View File

@@ -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 = {

View File

@@ -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 = {

View File

@@ -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 = {

View File

@@ -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 = {

View File

@@ -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 = {

View File

@@ -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 = {

View File

@@ -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 = {

View File

@@ -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 = {

View File

@@ -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 = {

View File

@@ -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 = {

View File

@@ -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 = {

View File

@@ -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 = {

View File

@@ -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 (?, ?, ?)'

View File

@@ -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;
} }

View File

@@ -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 };
} }

View File

@@ -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;
} }