feat: 风险分定时衰减 + 密码提示修复 + 浏览器池API + next回跳

1. 风险分衰减定时任务:
   - services/scheduler.py: 每天 CST 04:00 自动执行 decay_scores()
   - 支持 RISK_SCORE_DECAY_TIME_CST 环境变量覆盖

2. 密码长度提示统一为8位:
   - app-frontend/src/pages/RegisterPage.vue
   - app-frontend/src/layouts/AppLayout.vue
   - admin-frontend/src/pages/SettingsPage.vue
   - templates/register.html

3. 浏览器池统计API:
   - GET /yuyx/api/browser_pool/stats
   - 返回 worker 状态、队列等待数等信息
   - browser_pool_worker.py: 增强 get_stats() 方法

4. 登录后支持 next 参数回跳:
   - app-frontend/src/pages/LoginPage.vue: 检查 ?next= 参数
   - 仅允许站内路径(防止开放重定向)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-27 18:28:21 +08:00
parent 3d9dba272e
commit 1b20478a08
49 changed files with 305 additions and 160 deletions

View File

@@ -1,34 +1,34 @@
{
"_email-D-nWLD-A.js": {
"file": "assets/email-D-nWLD-A.js",
"_email-DSz2K4-y.js": {
"file": "assets/email-DSz2K4-y.js",
"name": "email",
"imports": [
"index.html"
]
},
"_system-CJ2QU_TO.js": {
"file": "assets/system-CJ2QU_TO.js",
"_system-C_4M4EtK.js": {
"file": "assets/system-C_4M4EtK.js",
"name": "system",
"imports": [
"index.html"
]
},
"_tasks-CEM7_KIT.js": {
"file": "assets/tasks-CEM7_KIT.js",
"_tasks-yIlAy2Ne.js": {
"file": "assets/tasks-yIlAy2Ne.js",
"name": "tasks",
"imports": [
"index.html"
]
},
"_users-CA0gIT8G.js": {
"file": "assets/users-CA0gIT8G.js",
"_users-BNPg4OEj.js": {
"file": "assets/users-BNPg4OEj.js",
"name": "users",
"imports": [
"index.html"
]
},
"index.html": {
"file": "assets/index-akVRSJTL.js",
"file": "assets/index-Dx-1XhY8.js",
"name": "index",
"src": "index.html",
"isEntry": true,
@@ -48,7 +48,7 @@
]
},
"src/pages/AnnouncementsPage.vue": {
"file": "assets/AnnouncementsPage-7ij3KbUN.js",
"file": "assets/AnnouncementsPage-DP-v4_4f.js",
"name": "AnnouncementsPage",
"src": "src/pages/AnnouncementsPage.vue",
"isDynamicEntry": true,
@@ -60,12 +60,12 @@
]
},
"src/pages/EmailPage.vue": {
"file": "assets/EmailPage-CuPwCZn-.js",
"file": "assets/EmailPage-4etTfx9H.js",
"name": "EmailPage",
"src": "src/pages/EmailPage.vue",
"isDynamicEntry": true,
"imports": [
"_email-D-nWLD-A.js",
"_email-DSz2K4-y.js",
"index.html"
],
"css": [
@@ -73,7 +73,7 @@
]
},
"src/pages/FeedbacksPage.vue": {
"file": "assets/FeedbacksPage-CSDNvoUn.js",
"file": "assets/FeedbacksPage-BlN6FFbD.js",
"name": "FeedbacksPage",
"src": "src/pages/FeedbacksPage.vue",
"isDynamicEntry": true,
@@ -85,13 +85,13 @@
]
},
"src/pages/LogsPage.vue": {
"file": "assets/LogsPage-DDxhRTa7.js",
"file": "assets/LogsPage-adLViVmd.js",
"name": "LogsPage",
"src": "src/pages/LogsPage.vue",
"isDynamicEntry": true,
"imports": [
"_users-CA0gIT8G.js",
"_tasks-CEM7_KIT.js",
"_users-BNPg4OEj.js",
"_tasks-yIlAy2Ne.js",
"index.html"
],
"css": [
@@ -99,22 +99,22 @@
]
},
"src/pages/ReportPage.vue": {
"file": "assets/ReportPage-CoI2Nht-.js",
"file": "assets/ReportPage-DxDL6AXa.js",
"name": "ReportPage",
"src": "src/pages/ReportPage.vue",
"isDynamicEntry": true,
"imports": [
"index.html",
"_email-D-nWLD-A.js",
"_tasks-CEM7_KIT.js",
"_system-CJ2QU_TO.js"
"_email-DSz2K4-y.js",
"_tasks-yIlAy2Ne.js",
"_system-C_4M4EtK.js"
],
"css": [
"assets/ReportPage-CW7RwLmI.css"
]
},
"src/pages/SecurityPage.vue": {
"file": "assets/SecurityPage-CQQKpFcS.js",
"file": "assets/SecurityPage-BkxWxQhW.js",
"name": "SecurityPage",
"src": "src/pages/SecurityPage.vue",
"isDynamicEntry": true,
@@ -126,7 +126,7 @@
]
},
"src/pages/SettingsPage.vue": {
"file": "assets/SettingsPage-BpSZamEk.js",
"file": "assets/SettingsPage-D_cVneyv.js",
"name": "SettingsPage",
"src": "src/pages/SettingsPage.vue",
"isDynamicEntry": true,
@@ -134,16 +134,16 @@
"index.html"
],
"css": [
"assets/SettingsPage-DGdwb4W2.css"
"assets/SettingsPage-DKTq8S2K.css"
]
},
"src/pages/SystemPage.vue": {
"file": "assets/SystemPage-DUY6QC8Y.js",
"file": "assets/SystemPage-Cph4odbt.js",
"name": "SystemPage",
"src": "src/pages/SystemPage.vue",
"isDynamicEntry": true,
"imports": [
"_system-CJ2QU_TO.js",
"_system-C_4M4EtK.js",
"index.html"
],
"css": [
@@ -151,12 +151,12 @@
]
},
"src/pages/UsersPage.vue": {
"file": "assets/UsersPage-hj_Nb-9c.js",
"file": "assets/UsersPage-19tzoQBx.js",
"name": "UsersPage",
"src": "src/pages/UsersPage.vue",
"isDynamicEntry": true,
"imports": [
"_users-CA0gIT8G.js",
"_users-BNPg4OEj.js",
"index.html"
],
"css": [

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 @@
import{P as m,_ as T,r as p,e as u,f as h,g as k,h as r,j as a,w as s,p as x,K as i,J as b}from"./index-akVRSJTL.js";async function P(o){const{data:t}=await m.put("/admin/username",{new_username:o});return t}async function C(o){const{data:t}=await m.put("/admin/password",{new_password:o});return t}async function U(){const{data:o}=await m.post("/logout");return o}const E={class:"page-stack"},N={__name:"SettingsPage",setup(o){const t=p(""),d=p(""),n=p(!1);async function f(){try{await U()}catch{}finally{window.location.href="/yuyx"}}async function V(){const l=t.value.trim();if(!l){i.error("请输入新用户名");return}try{await b.confirm(`确定将管理员用户名修改为「${l}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await P(l),i.success("用户名修改成功,请重新登录"),t.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function B(){const l=d.value;if(!l){i.error("请输入新密码");return}if(l.length<6){i.error("密码至少6个字符");return}try{await b.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await C(l),i.success("密码修改成功,请重新登录"),d.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}return(l,e)=>{const w=u("el-input"),v=u("el-form-item"),y=u("el-form"),_=u("el-button"),g=u("el-card");return k(),h("div",E,[e[7]||(e[7]=r("div",{class:"app-page-title"},[r("h2",null,"设置"),r("span",{class:"app-muted"},"管理员账号设置")],-1)),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[3]||(e[3]=r("h3",{class:"section-title"},"修改管理员用户名",-1)),a(y,{"label-width":"120px"},{default:s(()=>[a(v,{label:"新用户名"},{default:s(()=>[a(w,{modelValue:t.value,"onUpdate:modelValue":e[0]||(e[0]=c=>t.value=c),placeholder:"输入新用户名",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:V},{default:s(()=>[...e[2]||(e[2]=[x("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[5]||(e[5]=r("h3",{class:"section-title"},"修改管理员密码",-1)),a(y,{"label-width":"120px"},{default:s(()=>[a(v,{label:"新密码"},{default:s(()=>[a(w,{modelValue:d.value,"onUpdate:modelValue":e[1]||(e[1]=c=>d.value=c),type:"password","show-password":"",placeholder:"输入新密码",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:B},{default:s(()=>[...e[4]||(e[4]=[x("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码至少8位且包含字母与数字。",-1))]),_:1})])}}},M=T(N,[["__scopeId","data-v-2f4b840f"]]);export{M as default};

View File

@@ -1 +0,0 @@
.page-stack[data-v-2f4b840f]{display:flex;flex-direction:column;gap:12px}.card[data-v-2f4b840f]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-title[data-v-2f4b840f]{margin:0 0 12px;font-size:14px;font-weight:800}.help[data-v-2f4b840f]{margin-top:10px;font-size:12px;color:var(--app-muted)}

View File

@@ -0,0 +1 @@
.page-stack[data-v-12a26d11]{display:flex;flex-direction:column;gap:12px}.card[data-v-12a26d11]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-title[data-v-12a26d11]{margin:0 0 12px;font-size:14px;font-weight:800}.help[data-v-12a26d11]{margin-top:10px;font-size:12px;color:var(--app-muted)}

View File

@@ -0,0 +1 @@
import{P as m,_ as B,r as p,e as u,f as T,g as P,h as r,j as a,w as l,p as x,K as i,J as b}from"./index-Dx-1XhY8.js";async function C(o){const{data:s}=await m.put("/admin/username",{new_username:o});return s}async function S(o){const{data:s}=await m.put("/admin/password",{new_password:o});return s}async function U(){const{data:o}=await m.post("/logout");return o}const A={class:"page-stack"},E={__name:"SettingsPage",setup(o){const s=p(""),d=p(""),n=p(!1);function k(t){const e=String(t||"");return e.length<8?{ok:!1,message:"密码长度至少8位"}:e.length>128?{ok:!1,message:"密码长度不能超过128个字符"}:!/[a-zA-Z]/.test(e)||!/\d/.test(e)?{ok:!1,message:"密码必须包含字母和数字"}:{ok:!0,message:""}}async function f(){try{await U()}catch{}finally{window.location.href="/yuyx"}}async function V(){const t=s.value.trim();if(!t){i.error("请输入新用户名");return}try{await b.confirm(`确定将管理员用户名修改为「${t}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await C(t),i.success("用户名修改成功,请重新登录"),s.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function h(){const t=d.value;if(!t){i.error("请输入新密码");return}const e=k(t);if(!e.ok){i.error(e.message);return}try{await b.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await S(t),i.success("密码修改成功,请重新登录"),d.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}return(t,e)=>{const g=u("el-input"),w=u("el-form-item"),v=u("el-form"),y=u("el-button"),_=u("el-card");return P(),T("div",A,[e[7]||(e[7]=r("div",{class:"app-page-title"},[r("h2",null,"设置"),r("span",{class:"app-muted"},"管理员账号设置")],-1)),a(_,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:l(()=>[e[3]||(e[3]=r("h3",{class:"section-title"},"修改管理员用户名",-1)),a(v,{"label-width":"120px"},{default:l(()=>[a(w,{label:"新用户名"},{default:l(()=>[a(g,{modelValue:s.value,"onUpdate:modelValue":e[0]||(e[0]=c=>s.value=c),placeholder:"输入新用户名",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(y,{type:"primary",loading:n.value,onClick:V},{default:l(()=>[...e[2]||(e[2]=[x("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),a(_,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:l(()=>[e[5]||(e[5]=r("h3",{class:"section-title"},"修改管理员密码",-1)),a(v,{"label-width":"120px"},{default:l(()=>[a(w,{label:"新密码"},{default:l(()=>[a(g,{modelValue:d.value,"onUpdate:modelValue":e[1]||(e[1]=c=>d.value=c),type:"password","show-password":"",placeholder:"输入新密码",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(y,{type:"primary",loading:n.value,onClick:h},{default:l(()=>[...e[4]||(e[4]=[x("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码至少8位且包含字母与数字。",-1))]),_:1})])}}},M=B(E,[["__scopeId","data-v-12a26d11"]]);export{M as default};

View File

@@ -1,4 +1,4 @@
import{f as ae,u as S,e as te}from"./system-CJ2QU_TO.js";import{P as D,_ as oe,r as s,c as ne,o as ue,e as d,H as se,I as re,g as V,f as F,h as n,j as l,w as t,p as c,A as E,m as $,F as de,q as ie,n as me,J as T,K as p}from"./index-akVRSJTL.js";async function pe(){const{data:f}=await D.get("/proxy/config");return f}async function ce(f){const{data:v}=await D.post("/proxy/config",f);return v}async function ve(f){const{data:v}=await D.post("/proxy/test",f);return v}const ye={class:"page-stack"},_e={class:"app-page-title"},fe={class:"row-actions"},xe={class:"row-actions"},be={__name:"SystemPage",setup(f){const v=s(!1),g=s(2),w=s(1),k=s(3),i=s(!1),P=s("02:00"),x=s("应读"),y=s(["1","2","3","4","5","6","7"]),b=s(!1),_=s(""),C=s(3),B=s(!1),N=s(10),I=s(7),j=[{label:"周一",value:"1"},{label:"周二",value:"2"},{label:"周三",value:"3"},{label:"周四",value:"4"},{label:"周五",value:"5"},{label:"周六",value:"6"},{label:"周日",value:"7"}],L={1:"周一",2:"周二",3:"周三",4:"周四",5:"周五",6:"周六",7:"周日"},O=ne(()=>(y.value||[]).map(a=>L[Number(a)]||a).join("、"));function W(a){return String(a)==="注册前未读"?"注册前未读":"应读"}async function H(){v.value=!0;try{const[a,e]=await Promise.all([ae(),pe()]);g.value=a.max_concurrent_global??2,w.value=a.max_concurrent_per_account??1,k.value=a.max_screenshot_concurrent??3,i.value=(a.schedule_enabled??0)===1,P.value=a.schedule_time||"02:00",x.value=W(a.schedule_browse_type);const u=String(a.schedule_weekdays||"1,2,3,4,5,6,7").split(",").map(m=>m.trim()).filter(Boolean);y.value=u.length?u:["1","2","3","4","5","6","7"],B.value=(a.auto_approve_enabled??0)===1,N.value=a.auto_approve_hourly_limit??10,I.value=a.auto_approve_vip_days??7,b.value=(e.proxy_enabled??0)===1,_.value=e.proxy_api_url||"",C.value=e.proxy_expire_minutes??3}catch{}finally{v.value=!1}}async function q(){const a={max_concurrent_global:Number(g.value),max_concurrent_per_account:Number(w.value),max_screenshot_concurrent:Number(k.value)};try{await T.confirm(`确定更新并发配置吗?
import{f as ae,u as S,e as te}from"./system-C_4M4EtK.js";import{P as D,_ as oe,r as s,c as ne,o as ue,e as d,H as se,I as re,g as V,f as F,h as n,j as l,w as t,p as c,A as E,m as $,F as de,q as ie,n as me,J as T,K as p}from"./index-Dx-1XhY8.js";async function pe(){const{data:f}=await D.get("/proxy/config");return f}async function ce(f){const{data:v}=await D.post("/proxy/config",f);return v}async function ve(f){const{data:v}=await D.post("/proxy/test",f);return v}const ye={class:"page-stack"},_e={class:"app-page-title"},fe={class:"row-actions"},xe={class:"row-actions"},be={__name:"SystemPage",setup(f){const v=s(!1),g=s(2),w=s(1),k=s(3),i=s(!1),P=s("02:00"),x=s("应读"),y=s(["1","2","3","4","5","6","7"]),b=s(!1),_=s(""),C=s(3),B=s(!1),N=s(10),I=s(7),j=[{label:"周一",value:"1"},{label:"周二",value:"2"},{label:"周三",value:"3"},{label:"周四",value:"4"},{label:"周五",value:"5"},{label:"周六",value:"6"},{label:"周日",value:"7"}],L={1:"周一",2:"周二",3:"周三",4:"周四",5:"周五",6:"周六",7:"周日"},O=ne(()=>(y.value||[]).map(a=>L[Number(a)]||a).join("、"));function W(a){return String(a)==="注册前未读"?"注册前未读":"应读"}async function H(){v.value=!0;try{const[a,e]=await Promise.all([ae(),pe()]);g.value=a.max_concurrent_global??2,w.value=a.max_concurrent_per_account??1,k.value=a.max_screenshot_concurrent??3,i.value=(a.schedule_enabled??0)===1,P.value=a.schedule_time||"02:00",x.value=W(a.schedule_browse_type);const u=String(a.schedule_weekdays||"1,2,3,4,5,6,7").split(",").map(m=>m.trim()).filter(Boolean);y.value=u.length?u:["1","2","3","4","5","6","7"],B.value=(a.auto_approve_enabled??0)===1,N.value=a.auto_approve_hourly_limit??10,I.value=a.auto_approve_vip_days??7,b.value=(e.proxy_enabled??0)===1,_.value=e.proxy_api_url||"",C.value=e.proxy_expire_minutes??3}catch{}finally{v.value=!1}}async function q(){const a={max_concurrent_global:Number(g.value),max_concurrent_per_account:Number(w.value),max_screenshot_concurrent:Number(k.value)};try{await T.confirm(`确定更新并发配置吗?
全局并发数: ${a.max_concurrent_global}
单账号并发数: ${a.max_concurrent_per_account}

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{P as n}from"./index-akVRSJTL.js";async function i(){const{data:a}=await n.get("/email/settings");return a}async function e(a){const{data:t}=await n.post("/email/settings",a);return t}async function c(){const{data:a}=await n.get("/email/stats");return a}async function o(a){const{data:t}=await n.get("/email/logs",{params:a});return t}async function l(a){const{data:t}=await n.post("/email/logs/cleanup",{days:a});return t}export{o as a,i as b,l as c,c as f,e as u};
import{P as n}from"./index-Dx-1XhY8.js";async function i(){const{data:a}=await n.get("/email/settings");return a}async function e(a){const{data:t}=await n.post("/email/settings",a);return t}async function c(){const{data:a}=await n.get("/email/stats");return a}async function o(a){const{data:t}=await n.get("/email/logs",{params:a});return t}async function l(a){const{data:t}=await n.post("/email/logs/cleanup",{days:a});return t}export{o as a,i as b,l as c,c as f,e as u};

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{P as e}from"./index-akVRSJTL.js";async function s(){const{data:t}=await e.get("/system/config");return t}async function c(t){const{data:a}=await e.post("/system/config",t);return a}async function o(){const{data:t}=await e.post("/schedule/execute",{});return t}export{o as e,s as f,c as u};
import{P as e}from"./index-Dx-1XhY8.js";async function s(){const{data:t}=await e.get("/system/config");return t}async function c(t){const{data:a}=await e.post("/system/config",t);return a}async function o(){const{data:t}=await e.post("/schedule/execute",{});return t}export{o as e,s as f,c as u};

View File

@@ -1 +1 @@
import{P as a}from"./index-akVRSJTL.js";async function c(){const{data:t}=await a.get("/server/info");return t}async function e(){const{data:t}=await a.get("/docker_stats");return t}async function o(){const{data:t}=await a.get("/task/stats");return t}async function r(){const{data:t}=await a.get("/task/running");return t}async function i(t){const{data:s}=await a.get("/task/logs",{params:t});return s}async function f(t){const{data:s}=await a.post("/task/logs/clear",{days:t});return s}export{r as a,c as b,e as c,i as d,f as e,o as f};
import{P as a}from"./index-Dx-1XhY8.js";async function c(){const{data:t}=await a.get("/server/info");return t}async function e(){const{data:t}=await a.get("/docker_stats");return t}async function o(){const{data:t}=await a.get("/task/stats");return t}async function r(){const{data:t}=await a.get("/task/running");return t}async function i(t){const{data:s}=await a.get("/task/logs",{params:t});return s}async function f(t){const{data:s}=await a.post("/task/logs/clear",{days:t});return s}export{r as a,c as b,e as c,i as d,f as e,o as f};

View File

@@ -1 +1 @@
import{P as t}from"./index-akVRSJTL.js";async function n(){const{data:s}=await t.get("/users");return s}async function o(s){const{data:a}=await t.post(`/users/${s}/approve`);return a}async function c(s){const{data:a}=await t.post(`/users/${s}/reject`);return a}async function i(s){const{data:a}=await t.delete(`/users/${s}`);return a}async function u(s,a){const{data:e}=await t.post(`/users/${s}/vip`,{days:a});return e}async function p(s){const{data:a}=await t.delete(`/users/${s}/vip`);return a}async function d(s,a){const{data:e}=await t.post(`/users/${s}/reset_password`,{new_password:a});return e}export{o as a,p as b,d as c,i as d,n as f,c as r,u as s};
import{P as t}from"./index-Dx-1XhY8.js";async function n(){const{data:s}=await t.get("/users");return s}async function o(s){const{data:a}=await t.post(`/users/${s}/approve`);return a}async function c(s){const{data:a}=await t.post(`/users/${s}/reject`);return a}async function i(s){const{data:a}=await t.delete(`/users/${s}`);return a}async function u(s,a){const{data:e}=await t.post(`/users/${s}/vip`,{days:a});return e}async function p(s){const{data:a}=await t.delete(`/users/${s}/vip`);return a}async function d(s,a){const{data:e}=await t.post(`/users/${s}/reset_password`,{new_password:a});return e}export{o as a,p as b,d as c,i as d,n as f,c as r,u as s};

View File

@@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>后台管理 - 知识管理平台</title>
<script type="module" crossorigin src="./assets/index-akVRSJTL.js"></script>
<script type="module" crossorigin src="./assets/index-Dx-1XhY8.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-_5Ec1Hmd.css">
</head>
<body>