更新系统页面和更新功能

- 更新 admin-frontend 系统页面和更新 API
- 更新 routes 和 services 中的更新逻辑
- 重新构建前端静态资源
This commit is contained in:
2025-12-15 15:58:12 +08:00
parent de6d269fb4
commit a8b9f225bd
22 changed files with 165 additions and 62 deletions

View File

@@ -20,8 +20,7 @@ export async function requestUpdateCheck() {
return data
}
export async function requestUpdateRun() {
const { data } = await api.post('/update/run', {})
export async function requestUpdateRun(payload = {}) {
const { data } = await api.post('/update/run', payload)
return data
}

View File

@@ -37,6 +37,7 @@ const updateStatusError = ref('')
const updateResult = ref(null)
const updateLog = ref('')
const updateLogTruncated = ref(false)
const updateBuildNoCache = ref(false)
let updatePollTimer = null
const weekdaysOptions = [
@@ -316,10 +317,11 @@ async function onCheckUpdate() {
async function onRunUpdate() {
const status = updateStatus.value
const remote = status?.remote_commit ? shortCommit(status.remote_commit) : '-'
const buildFlags = updateBuildNoCache.value ? '\n\n构建选项: 强制重建(--no-cache' : ''
try {
await ElMessageBox.confirm(
`确定开始“一键更新”吗?\n\n目标版本: ${remote}\n\n更新将会重建并重启服务页面可能短暂不可用系统会先备份数据库。`,
`确定开始“一键更新”吗?\n\n目标版本: ${remote}${buildFlags}\n\n更新将会重建并重启服务页面可能短暂不可用系统会先备份数据库。`,
'一键更新确认',
{ confirmButtonText: '开始更新', cancelButtonText: '取消', type: 'warning' },
)
@@ -329,7 +331,7 @@ async function onRunUpdate() {
updateActionLoading.value = true
try {
const res = await requestUpdateRun()
const res = await requestUpdateRun({ build_no_cache: updateBuildNoCache.value ? 1 : 0 })
ElMessage.success(res?.message || '已提交更新请求')
startUpdatePolling()
setTimeout(() => loadUpdateInfo(), 800)
@@ -498,6 +500,11 @@ onBeforeUnmount(stopUpdatePolling)
</el-descriptions-item>
</el-descriptions>
<div class="row-actions" style="align-items: center">
<el-checkbox v-model="updateBuildNoCache">强制重建--no-cache</el-checkbox>
<div class="help" style="margin-top: 0">依赖变更或构建异常时建议开启更新会更慢</div>
</div>
<div class="row-actions">
<el-button @click="loadUpdateInfo" :disabled="updateActionLoading">刷新更新信息</el-button>
<el-button @click="onCheckUpdate" :loading="updateActionLoading">检查更新</el-button>

View File

@@ -43,6 +43,28 @@ def _has_pending_request() -> bool:
return False
def _parse_bool_field(data: dict, key: str) -> bool | None:
if not isinstance(data, dict) or key not in data:
return None
value = data.get(key)
if isinstance(value, bool):
return value
if isinstance(value, int):
if value in (0, 1):
return bool(value)
raise ValueError(f"{key} 必须是 0/1 或 true/false")
if isinstance(value, str):
text = value.strip().lower()
if text in ("1", "true", "yes", "y", "on"):
return True
if text in ("0", "false", "no", "n", "off", ""):
return False
raise ValueError(f"{key} 必须是 0/1 或 true/false")
if value is None:
return None
raise ValueError(f"{key} 必须是 0/1 或 true/false")
@admin_api_bp.route("/update/status", methods=["GET"])
@admin_required
def get_update_status_api():
@@ -127,6 +149,17 @@ def request_update_run_api():
if _has_pending_request():
return jsonify({"error": "已有更新请求正在处理中,请稍后再试"}), 409
data = request.json or {}
try:
build_no_cache = _parse_bool_field(data, "build_no_cache")
if build_no_cache is None:
build_no_cache = _parse_bool_field(data, "no_cache")
build_pull = _parse_bool_field(data, "build_pull")
if build_pull is None:
build_pull = _parse_bool_field(data, "pull")
except ValueError as e:
return jsonify({"error": str(e)}), 400
job_id = _make_job_id(prefix="upd")
payload = {
"job_id": job_id,
@@ -134,6 +167,8 @@ def request_update_run_api():
"requested_at": get_beijing_now().strftime("%Y-%m-%d %H:%M:%S"),
"requested_by": session.get("admin_username") or "",
"requested_ip": _request_ip(),
"build_no_cache": bool(build_no_cache) if build_no_cache is not None else False,
"build_pull": bool(build_pull) if build_pull is not None else False,
}
write_json_atomic(get_update_request_path(), payload)
return jsonify(
@@ -143,4 +178,3 @@ def request_update_run_api():
"message": "已提交更新请求服务将重启页面可能短暂不可用请等待1-2分钟后刷新",
}
), 200

View File

@@ -79,6 +79,7 @@ class TaskScheduler:
self._pending = [] # heap: (priority, submitted_at, seq, task)
self._pending_by_account = {} # {account_id: task}
self._seq = 0
self._known_account_ids = set()
self._running_global = 0
self._running_by_user = {} # {user_id: running_count}
@@ -143,6 +144,18 @@ class TaskScheduler:
except Exception:
pass
# 最后兜底:清理本调度器提交过的 active_task避免测试/重启时被“任务已在运行中”误拦截
try:
with self._cond:
known_ids = set(self._known_account_ids) | set(self._pending_by_account.keys())
self._pending.clear()
self._pending_by_account.clear()
self._cond.notify_all()
for account_id in known_ids:
safe_remove_task(account_id)
except Exception:
pass
def update_limits(self, max_global: int = None, max_per_user: int = None, max_queue_size: int = None):
"""动态更新并发/队列上限(不影响已在运行的任务)"""
with self._cond:
@@ -215,6 +228,7 @@ class TaskScheduler:
done_callback=done_callback,
)
self._pending_by_account[account_id] = task
self._known_account_ids.add(account_id)
priority = 0 if is_vip else 1
heapq.heappush(self._pending, (priority, task.submitted_at, task.seq, task))
self._cond.notify_all()

View File

@@ -3,22 +3,22 @@
"file": "assets/datetime-ZCuLLiQt.js",
"name": "datetime"
},
"_tasks-CFlwbjTx.js": {
"file": "assets/tasks-CFlwbjTx.js",
"_tasks-C0sohTWS.js": {
"file": "assets/tasks-C0sohTWS.js",
"name": "tasks",
"imports": [
"index.html"
]
},
"_users-B1Ww8zl1.js": {
"file": "assets/users-B1Ww8zl1.js",
"_users-a9w9WKCH.js": {
"file": "assets/users-a9w9WKCH.js",
"name": "users",
"imports": [
"index.html"
]
},
"index.html": {
"file": "assets/index-CKlvOJnw.js",
"file": "assets/index-BkLe6kG5.js",
"name": "index",
"src": "index.html",
"isEntry": true,
@@ -38,7 +38,7 @@
]
},
"src/pages/AnnouncementsPage.vue": {
"file": "assets/AnnouncementsPage-DptOTv9v.js",
"file": "assets/AnnouncementsPage-B6R49DrY.js",
"name": "AnnouncementsPage",
"src": "src/pages/AnnouncementsPage.vue",
"isDynamicEntry": true,
@@ -50,7 +50,7 @@
]
},
"src/pages/EmailPage.vue": {
"file": "assets/EmailPage-A0u1uqDL.js",
"file": "assets/EmailPage-CSqw8er9.js",
"name": "EmailPage",
"src": "src/pages/EmailPage.vue",
"isDynamicEntry": true,
@@ -62,7 +62,7 @@
]
},
"src/pages/FeedbacksPage.vue": {
"file": "assets/FeedbacksPage-2XhnmXAD.js",
"file": "assets/FeedbacksPage-B9rEDaI0.js",
"name": "FeedbacksPage",
"src": "src/pages/FeedbacksPage.vue",
"isDynamicEntry": true,
@@ -74,13 +74,13 @@
]
},
"src/pages/LogsPage.vue": {
"file": "assets/LogsPage-C6eCwbXQ.js",
"file": "assets/LogsPage-eWUoUxcN.js",
"name": "LogsPage",
"src": "src/pages/LogsPage.vue",
"isDynamicEntry": true,
"imports": [
"_users-B1Ww8zl1.js",
"_tasks-CFlwbjTx.js",
"_users-a9w9WKCH.js",
"_tasks-C0sohTWS.js",
"index.html"
],
"css": [
@@ -88,12 +88,12 @@
]
},
"src/pages/PendingPage.vue": {
"file": "assets/PendingPage-CsinHQq-.js",
"file": "assets/PendingPage-BVSAb_Yr.js",
"name": "PendingPage",
"src": "src/pages/PendingPage.vue",
"isDynamicEntry": true,
"imports": [
"_users-B1Ww8zl1.js",
"_users-a9w9WKCH.js",
"index.html",
"_datetime-ZCuLLiQt.js"
],
@@ -102,7 +102,7 @@
]
},
"src/pages/SettingsPage.vue": {
"file": "assets/SettingsPage-D9ploTWf.js",
"file": "assets/SettingsPage-CVlYx8ta.js",
"name": "SettingsPage",
"src": "src/pages/SettingsPage.vue",
"isDynamicEntry": true,
@@ -114,12 +114,12 @@
]
},
"src/pages/StatsPage.vue": {
"file": "assets/StatsPage-CxBcYwKE.js",
"file": "assets/StatsPage-BRQO0uQE.js",
"name": "StatsPage",
"src": "src/pages/StatsPage.vue",
"isDynamicEntry": true,
"imports": [
"_tasks-CFlwbjTx.js",
"_tasks-C0sohTWS.js",
"index.html"
],
"css": [
@@ -127,7 +127,7 @@
]
},
"src/pages/SystemPage.vue": {
"file": "assets/SystemPage-DMNoY1AU.js",
"file": "assets/SystemPage-StAFDOuj.js",
"name": "SystemPage",
"src": "src/pages/SystemPage.vue",
"isDynamicEntry": true,
@@ -135,16 +135,16 @@
"index.html"
],
"css": [
"assets/SystemPage-B0TEzUGV.css"
"assets/SystemPage-CdLP1b5Y.css"
]
},
"src/pages/UsersPage.vue": {
"file": "assets/UsersPage-BzRNJLnT.js",
"file": "assets/UsersPage-DkSQZ5YZ.js",
"name": "UsersPage",
"src": "src/pages/UsersPage.vue",
"isDynamicEntry": true,
"imports": [
"_users-B1Ww8zl1.js",
"_users-a9w9WKCH.js",
"_datetime-ZCuLLiQt.js",
"index.html"
],

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 +1 @@
import{f as E,a as I,r as A}from"./users-B1Ww8zl1.js";import{_ as M,r as p,o as q,c as W,a as i,b as t,w as a,d,i as T,f as F,e as G,g as f,h as r,j as $,k as x,l as H,t as k,E as m,m as g,n as J,p as K}from"./index-CKlvOJnw.js";import{p as L}from"./datetime-ZCuLLiQt.js";const O={class:"page-stack"},Q={class:"app-page-title"},X={class:"table-wrap"},Y={class:"user-cell"},Z={class:"table-wrap"},ee={__name:"PendingPage",setup(te){const B=T("refreshStats",null),_=T("refreshNavBadges",null),v=p([]),c=p([]),w=p(!1),y=p(!1);function D(s){const e=s?.vip_expire_time;if(!e)return!1;if(String(e).startsWith("2099-12-31"))return!0;const o=L(e);return o?o.getTime()>Date.now():!1}async function j(){w.value=!0;try{v.value=await E()}catch{v.value=[]}finally{w.value=!1}}async function h(){y.value=!0;try{c.value=await F()}catch{c.value=[]}finally{y.value=!1}}async function u(){await Promise.all([j(),h()]),await _?.({pendingResets:c.value.length})}async function N(s){try{await m.confirm(`确定通过用户「${s.username}」的注册申请吗?`,"审核通过",{confirmButtonText:"通过",cancelButtonText:"取消",type:"success"})}catch{return}try{await I(s.id),g.success("用户审核通过"),await u(),await B?.()}catch{}}async function U(s){try{await m.confirm(`确定拒绝用户「${s.username}」的注册申请吗?`,"拒绝申请",{confirmButtonText:"拒绝",cancelButtonText:"取消",type:"warning"})}catch{return}try{await A(s.id),g.success("已拒绝用户"),await u(),await B?.()}catch{}}async function V(s){try{await m.confirm(`确定批准「${s.username}」的密码重置申请吗?`,"批准重置",{confirmButtonText:"批准",cancelButtonText:"取消",type:"success"})}catch{return}try{const e=await J(s.id);g.success(e?.message||"密码重置申请已批准"),await h(),await _?.({pendingResets:c.value.length})}catch{}}async function z(s){try{await m.confirm(`确定拒绝「${s.username}」的密码重置申请吗?`,"拒绝重置",{confirmButtonText:"拒绝",cancelButtonText:"取消",type:"warning"})}catch{return}try{const e=await K(s.id);g.success(e?.message||"密码重置申请已拒绝"),await h(),await _?.({pendingResets:c.value.length})}catch{}}return q(u),(s,e)=>{const o=d("el-button"),l=d("el-table-column"),S=d("el-tag"),R=d("el-table"),C=d("el-card"),P=G("loading");return f(),W("div",O,[i("div",Q,[e[1]||(e[1]=i("h2",null,"待审核",-1)),i("div",null,[t(o,{onClick:u},{default:a(()=>[...e[0]||(e[0]=[r("刷新",-1)])]),_:1})])]),t(C,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:a(()=>[e[5]||(e[5]=i("h3",{class:"section-title"},"用户注册审核",-1)),i("div",X,[$((f(),x(R,{data:v.value,style:{width:"100%"}},{default:a(()=>[t(l,{prop:"id",label:"ID",width:"80"}),t(l,{label:"用户名","min-width":"200"},{default:a(({row:n})=>[i("div",Y,[i("strong",null,k(n.username),1),D(n)?(f(),x(S,{key:0,type:"warning",effect:"light",size:"small"},{default:a(()=>[...e[2]||(e[2]=[r("VIP",-1)])]),_:1})):H("",!0)])]),_:1}),t(l,{prop:"email",label:"邮箱","min-width":"220"},{default:a(({row:n})=>[r(k(n.email||"-"),1)]),_:1}),t(l,{prop:"created_at",label:"注册时间","min-width":"180"}),t(l,{label:"操作",width:"180",fixed:"right"},{default:a(({row:n})=>[t(o,{type:"success",size:"small",onClick:b=>N(n)},{default:a(()=>[...e[3]||(e[3]=[r("通过",-1)])]),_:1},8,["onClick"]),t(o,{type:"danger",size:"small",onClick:b=>U(n)},{default:a(()=>[...e[4]||(e[4]=[r("拒绝",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[P,w.value]])])]),_:1}),t(C,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:a(()=>[e[8]||(e[8]=i("h3",{class:"section-title"},"密码重置审核",-1)),i("div",Z,[$((f(),x(R,{data:c.value,style:{width:"100%"}},{default:a(()=>[t(l,{prop:"id",label:"申请ID",width:"90"}),t(l,{prop:"username",label:"用户名","min-width":"200"}),t(l,{prop:"email",label:"邮箱","min-width":"220"},{default:a(({row:n})=>[r(k(n.email||"-"),1)]),_:1}),t(l,{prop:"created_at",label:"申请时间","min-width":"180"}),t(l,{label:"操作",width:"180",fixed:"right"},{default:a(({row:n})=>[t(o,{type:"success",size:"small",onClick:b=>V(n)},{default:a(()=>[...e[6]||(e[6]=[r("批准",-1)])]),_:1},8,["onClick"]),t(o,{type:"danger",size:"small",onClick:b=>z(n)},{default:a(()=>[...e[7]||(e[7]=[r("拒绝",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[P,y.value]])])]),_:1})])}}},le=M(ee,[["__scopeId","data-v-f2aa6820"]]);export{le as default};
import{f as E,a as I,r as A}from"./users-a9w9WKCH.js";import{_ as M,r as p,o as q,c as W,a as i,b as t,w as a,d,i as T,f as F,e as G,g as f,h as r,j as $,k as x,l as H,t as k,E as m,m as g,n as J,p as K}from"./index-BkLe6kG5.js";import{p as L}from"./datetime-ZCuLLiQt.js";const O={class:"page-stack"},Q={class:"app-page-title"},X={class:"table-wrap"},Y={class:"user-cell"},Z={class:"table-wrap"},ee={__name:"PendingPage",setup(te){const B=T("refreshStats",null),_=T("refreshNavBadges",null),v=p([]),c=p([]),w=p(!1),y=p(!1);function D(s){const e=s?.vip_expire_time;if(!e)return!1;if(String(e).startsWith("2099-12-31"))return!0;const o=L(e);return o?o.getTime()>Date.now():!1}async function j(){w.value=!0;try{v.value=await E()}catch{v.value=[]}finally{w.value=!1}}async function h(){y.value=!0;try{c.value=await F()}catch{c.value=[]}finally{y.value=!1}}async function u(){await Promise.all([j(),h()]),await _?.({pendingResets:c.value.length})}async function N(s){try{await m.confirm(`确定通过用户「${s.username}」的注册申请吗?`,"审核通过",{confirmButtonText:"通过",cancelButtonText:"取消",type:"success"})}catch{return}try{await I(s.id),g.success("用户审核通过"),await u(),await B?.()}catch{}}async function U(s){try{await m.confirm(`确定拒绝用户「${s.username}」的注册申请吗?`,"拒绝申请",{confirmButtonText:"拒绝",cancelButtonText:"取消",type:"warning"})}catch{return}try{await A(s.id),g.success("已拒绝用户"),await u(),await B?.()}catch{}}async function V(s){try{await m.confirm(`确定批准「${s.username}」的密码重置申请吗?`,"批准重置",{confirmButtonText:"批准",cancelButtonText:"取消",type:"success"})}catch{return}try{const e=await J(s.id);g.success(e?.message||"密码重置申请已批准"),await h(),await _?.({pendingResets:c.value.length})}catch{}}async function z(s){try{await m.confirm(`确定拒绝「${s.username}」的密码重置申请吗?`,"拒绝重置",{confirmButtonText:"拒绝",cancelButtonText:"取消",type:"warning"})}catch{return}try{const e=await K(s.id);g.success(e?.message||"密码重置申请已拒绝"),await h(),await _?.({pendingResets:c.value.length})}catch{}}return q(u),(s,e)=>{const o=d("el-button"),l=d("el-table-column"),S=d("el-tag"),R=d("el-table"),C=d("el-card"),P=G("loading");return f(),W("div",O,[i("div",Q,[e[1]||(e[1]=i("h2",null,"待审核",-1)),i("div",null,[t(o,{onClick:u},{default:a(()=>[...e[0]||(e[0]=[r("刷新",-1)])]),_:1})])]),t(C,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:a(()=>[e[5]||(e[5]=i("h3",{class:"section-title"},"用户注册审核",-1)),i("div",X,[$((f(),x(R,{data:v.value,style:{width:"100%"}},{default:a(()=>[t(l,{prop:"id",label:"ID",width:"80"}),t(l,{label:"用户名","min-width":"200"},{default:a(({row:n})=>[i("div",Y,[i("strong",null,k(n.username),1),D(n)?(f(),x(S,{key:0,type:"warning",effect:"light",size:"small"},{default:a(()=>[...e[2]||(e[2]=[r("VIP",-1)])]),_:1})):H("",!0)])]),_:1}),t(l,{prop:"email",label:"邮箱","min-width":"220"},{default:a(({row:n})=>[r(k(n.email||"-"),1)]),_:1}),t(l,{prop:"created_at",label:"注册时间","min-width":"180"}),t(l,{label:"操作",width:"180",fixed:"right"},{default:a(({row:n})=>[t(o,{type:"success",size:"small",onClick:b=>N(n)},{default:a(()=>[...e[3]||(e[3]=[r("通过",-1)])]),_:1},8,["onClick"]),t(o,{type:"danger",size:"small",onClick:b=>U(n)},{default:a(()=>[...e[4]||(e[4]=[r("拒绝",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[P,w.value]])])]),_:1}),t(C,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:a(()=>[e[8]||(e[8]=i("h3",{class:"section-title"},"密码重置审核",-1)),i("div",Z,[$((f(),x(R,{data:c.value,style:{width:"100%"}},{default:a(()=>[t(l,{prop:"id",label:"申请ID",width:"90"}),t(l,{prop:"username",label:"用户名","min-width":"200"}),t(l,{prop:"email",label:"邮箱","min-width":"220"},{default:a(({row:n})=>[r(k(n.email||"-"),1)]),_:1}),t(l,{prop:"created_at",label:"申请时间","min-width":"180"}),t(l,{label:"操作",width:"180",fixed:"right"},{default:a(({row:n})=>[t(o,{type:"success",size:"small",onClick:b=>V(n)},{default:a(()=>[...e[6]||(e[6]=[r("批准",-1)])]),_:1},8,["onClick"]),t(o,{type:"danger",size:"small",onClick:b=>z(n)},{default:a(()=>[...e[7]||(e[7]=[r("拒绝",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[P,y.value]])])]),_:1})])}}},le=M(ee,[["__scopeId","data-v-f2aa6820"]]);export{le as default};

View File

@@ -1 +1 @@
import{B as m,_ as T,r as p,c as h,a as r,b as a,w as s,d as u,g as k,h as b,m as d,E as x}from"./index-CKlvOJnw.js";async function C(o){const{data:t}=await m.put("/admin/username",{new_username:o});return t}async function E(o){const{data:t}=await m.put("/admin/password",{new_password:o});return t}async function P(){const{data:o}=await m.post("/logout");return o}const U={class:"page-stack"},N={__name:"SettingsPage",setup(o){const t=p(""),i=p(""),n=p(!1);async function f(){try{await P()}catch{}finally{window.location.href="/yuyx"}}async function B(){const l=t.value.trim();if(!l){d.error("请输入新用户名");return}try{await x.confirm(`确定将管理员用户名修改为「${l}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await C(l),d.success("用户名修改成功,请重新登录"),t.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function V(){const l=i.value;if(!l){d.error("请输入新密码");return}if(l.length<6){d.error("密码至少6个字符");return}try{await x.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await E(l),d.success("密码修改成功,请重新登录"),i.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",U,[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:B},{default:s(()=>[...e[2]||(e[2]=[b("保存用户名",-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:i.value,"onUpdate:modelValue":e[1]||(e[1]=c=>i.value=c),type:"password","show-password":"",placeholder:"输入新密码",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:V},{default:s(()=>[...e[4]||(e[4]=[b("保存密码",-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};
import{B as m,_ as T,r as p,c as h,a as r,b as a,w as s,d as u,g as k,h as b,m as d,E as x}from"./index-BkLe6kG5.js";async function C(o){const{data:t}=await m.put("/admin/username",{new_username:o});return t}async function E(o){const{data:t}=await m.put("/admin/password",{new_password:o});return t}async function P(){const{data:o}=await m.post("/logout");return o}const U={class:"page-stack"},N={__name:"SettingsPage",setup(o){const t=p(""),i=p(""),n=p(!1);async function f(){try{await P()}catch{}finally{window.location.href="/yuyx"}}async function B(){const l=t.value.trim();if(!l){d.error("请输入新用户名");return}try{await x.confirm(`确定将管理员用户名修改为「${l}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await C(l),d.success("用户名修改成功,请重新登录"),t.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function V(){const l=i.value;if(!l){d.error("请输入新密码");return}if(l.length<6){d.error("密码至少6个字符");return}try{await x.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await E(l),d.success("密码修改成功,请重新登录"),i.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",U,[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:B},{default:s(()=>[...e[2]||(e[2]=[b("保存用户名",-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:i.value,"onUpdate:modelValue":e[1]||(e[1]=c=>i.value=c),type:"password","show-password":"",placeholder:"输入新密码",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:V},{default:s(()=>[...e[4]||(e[4]=[b("保存密码",-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};

File diff suppressed because one or more lines are too long

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{B as a}from"./index-CKlvOJnw.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{e as a,o as b,r as c,i as d,f as e,c as f};
import{B as a}from"./index-BkLe6kG5.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{e as a,o as b,r as c,i as d,f as e,c as f};

View File

@@ -1 +1 @@
import{B as a}from"./index-CKlvOJnw.js";async function r(){const{data:s}=await a.get("/users");return s}async function c(){const{data:s}=await a.get("/users/pending");return s}async function o(s){const{data:t}=await a.post(`/users/${s}/approve`);return t}async function i(s){const{data:t}=await a.post(`/users/${s}/reject`);return t}async function u(s){const{data:t}=await a.delete(`/users/${s}`);return t}async function d(s,t){const{data:e}=await a.post(`/users/${s}/vip`,{days:t});return e}async function p(s){const{data:t}=await a.delete(`/users/${s}/vip`);return t}async function f(s,t){const{data:e}=await a.post(`/users/${s}/reset_password`,{new_password:t});return e}export{o as a,r as b,p as c,f as d,u as e,c as f,i as r,d as s};
import{B as a}from"./index-BkLe6kG5.js";async function r(){const{data:s}=await a.get("/users");return s}async function c(){const{data:s}=await a.get("/users/pending");return s}async function o(s){const{data:t}=await a.post(`/users/${s}/approve`);return t}async function i(s){const{data:t}=await a.post(`/users/${s}/reject`);return t}async function u(s){const{data:t}=await a.delete(`/users/${s}`);return t}async function d(s,t){const{data:e}=await a.post(`/users/${s}/vip`,{days:t});return e}async function p(s){const{data:t}=await a.delete(`/users/${s}/vip`);return t}async function f(s,t){const{data:e}=await a.post(`/users/${s}/reset_password`,{new_password:t});return e}export{o as a,r as b,p as c,f as d,u as e,c as f,i as r,d 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-CKlvOJnw.js"></script>
<script type="module" crossorigin src="./assets/index-BkLe6kG5.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-BIDpnzAs.css">
</head>
<body>

View File

@@ -66,6 +66,15 @@ def sanitize_job_id(value: object) -> str:
return text
def _as_bool(value: object) -> bool:
if isinstance(value, bool):
return value
if isinstance(value, int):
return value != 0
text = str(value or "").strip().lower()
return text in ("1", "true", "yes", "y", "on")
def _run(cmd: list[str], *, cwd: Path, log_fp, env: Optional[dict] = None, check: bool = True) -> subprocess.CompletedProcess:
log_fp.write(f"[{ts_str()}] $ {' '.join(cmd)}\n")
log_fp.flush()
@@ -305,7 +314,16 @@ def consume_request(paths: Paths) -> Tuple[dict, Optional[str]]:
return data, None
def handle_update_job(*, paths: Paths, branch: str, health_url: str, job_id: str, requested_by: str) -> None:
def handle_update_job(
*,
paths: Paths,
branch: str,
health_url: str,
job_id: str,
requested_by: str,
build_no_cache: bool = False,
build_pull: bool = False,
) -> None:
ensure_dirs(paths)
log_path = paths.jobs_dir / f"{job_id}.log"
with open(log_path, "a", encoding="utf-8") as log_fp:
@@ -323,6 +341,8 @@ def handle_update_job(*, paths: Paths, branch: str, health_url: str, job_id: str
"duration_seconds": None,
"requested_by": requested_by,
"branch": branch,
"build_no_cache": bool(build_no_cache),
"build_pull": bool(build_pull),
"from_commit": None,
"to_commit": None,
"backup_db": None,
@@ -360,7 +380,30 @@ def handle_update_job(*, paths: Paths, branch: str, health_url: str, job_id: str
result["stage"] = "docker_build"
result["message"] = "构建容器镜像"
write_result(paths, result)
_run([*compose, "build"], cwd=paths.repo_dir, log_fp=log_fp)
build_no_cache = bool(result.get("build_no_cache") is True)
build_pull = bool(result.get("build_pull") is True)
build_cmd = [*compose, "build"]
if build_pull:
build_cmd.append("--pull")
if build_no_cache:
build_cmd.append("--no-cache")
try:
_run(build_cmd, cwd=paths.repo_dir, log_fp=log_fp)
except subprocess.CalledProcessError as e:
if (not build_no_cache) and (e.returncode != 0):
log_fp.write(f"[{ts_str()}] build failed, retry with --no-cache\n")
log_fp.flush()
build_no_cache = True
result["build_no_cache"] = True
write_result(paths, result)
retry_cmd = [*compose, "build"]
if build_pull:
retry_cmd.append("--pull")
retry_cmd.append("--no-cache")
_run(retry_cmd, cwd=paths.repo_dir, log_fp=log_fp)
else:
raise
result["stage"] = "docker_up"
result["message"] = "重建并启动服务"
@@ -501,12 +544,16 @@ def main(argv: list[str]) -> int:
elif action == "check":
handle_check_job(paths=paths, branch=branch, job_id=job_id, requested_by=requested_by)
else:
build_no_cache = _as_bool(req.get("build_no_cache") or req.get("no_cache") or False)
build_pull = _as_bool(req.get("build_pull") or req.get("pull") or False)
handle_update_job(
paths=paths,
branch=branch,
health_url=health_url,
job_id=job_id,
requested_by=requested_by,
build_no_cache=build_no_cache,
build_pull=build_pull,
)
last_check_ts = time.time()