refactor: 删除版本更新功能 + 报表页自动刷新

删除版本与更新功能:
- routes/admin_api/update.py: 删除整个文件
- routes/admin_api/__init__.py: 移除 update 模块注册
- admin-frontend/src/pages/SystemPage.vue: 移除版本更新UI区块
- admin-frontend/src/api/update.js: 删除整个文件
- 删除 static/admin/assets/update-*.js

报表页自动刷新:
- admin-frontend/src/pages/ReportPage.vue: 添加 setInterval 每1秒刷新
- 在 onMounted 启动定时器,onUnmounted 清除
- 覆盖统计数据、运行中任务、系统信息等所有动态数据

🤖 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 12:41:26 +08:00
parent 89f3fd9759
commit 3d9dba272e
31 changed files with 84 additions and 546 deletions

View File

@@ -1,26 +0,0 @@
import { api } from './client'
export async function fetchUpdateStatus() {
const { data } = await api.get('/update/status')
return data
}
export async function fetchUpdateResult() {
const { data } = await api.get('/update/result')
return data
}
export async function fetchUpdateLog(params = {}) {
const { data } = await api.get('/update/log', { params })
return data
}
export async function requestUpdateCheck() {
const { data } = await api.post('/update/check', {})
return data
}
export async function requestUpdateRun(payload = {}) {
const { data } = await api.post('/update/run', payload)
return data
}

View File

@@ -1,5 +1,5 @@
<script setup> <script setup>
import { computed, inject, onMounted, ref } from 'vue' import { computed, inject, onMounted, onUnmounted, ref } from 'vue'
import { import {
Calendar, Calendar,
ChatLineSquare, ChatLineSquare,
@@ -19,7 +19,6 @@ import { fetchFeedbackStats } from '../api/feedbacks'
import { fetchEmailStats } from '../api/email' import { fetchEmailStats } from '../api/email'
import { fetchDockerStats, fetchRunningTasks, fetchServerInfo, fetchTaskStats } from '../api/tasks' import { fetchDockerStats, fetchRunningTasks, fetchServerInfo, fetchTaskStats } from '../api/tasks'
import { fetchSystemConfig } from '../api/system' import { fetchSystemConfig } from '../api/system'
import { fetchUpdateResult, fetchUpdateStatus } from '../api/update'
const refreshStats = inject('refreshStats', null) const refreshStats = inject('refreshStats', null)
const adminStats = inject('adminStats', null) const adminStats = inject('adminStats', null)
@@ -34,9 +33,6 @@ const feedbackStats = ref(null)
const serverInfo = ref(null) const serverInfo = ref(null)
const dockerStats = ref(null) const dockerStats = ref(null)
const systemConfig = ref(null) const systemConfig = ref(null)
const updateStatus = ref(null)
const updateStatusError = ref('')
const updateResult = ref(null)
const queueTab = ref('running') const queueTab = ref('running')
function recordUpdatedAt() { function recordUpdatedAt() {
@@ -63,12 +59,6 @@ function parsePercent(value) {
return n return n
} }
function shortCommit(value) {
const text = String(value ?? '').trim()
if (!text) return '-'
return text.length > 12 ? `${text.slice(0, 12)}` : text
}
function sourceLabel(source) { function sourceLabel(source) {
const raw = String(source ?? '').trim() const raw = String(source ?? '').trim()
if (!raw) return '手动' if (!raw) return '手动'
@@ -155,9 +145,6 @@ const runningCountsLabel = computed(() => {
return `运行中 ${runningCount} / 排队 ${queuingCount} / 并发上限 ${maxGlobal || maxConcurrentGlobal.value || '-'}` return `运行中 ${runningCount} / 排队 ${queuingCount} / 并发上限 ${maxGlobal || maxConcurrentGlobal.value || '-'}`
}) })
const updateAvailable = computed(() => Boolean(updateStatus.value?.update_available))
const updateRunning = computed(() => updateResult.value?.status === 'running')
async function refreshAll() { async function refreshAll() {
if (loading.value) return if (loading.value) return
loading.value = true loading.value = true
@@ -170,8 +157,6 @@ async function refreshAll() {
serverResult, serverResult,
dockerResult, dockerResult,
configResult, configResult,
updateStatusResult,
updateResultResult,
] = await Promise.allSettled([ ] = await Promise.allSettled([
fetchTaskStats(), fetchTaskStats(),
fetchRunningTasks(), fetchRunningTasks(),
@@ -180,8 +165,6 @@ async function refreshAll() {
fetchServerInfo(), fetchServerInfo(),
fetchDockerStats(), fetchDockerStats(),
fetchSystemConfig(), fetchSystemConfig(),
fetchUpdateStatus(),
fetchUpdateResult(),
]) ])
taskStats.value = taskResult.status === 'fulfilled' ? taskResult.value : null taskStats.value = taskResult.status === 'fulfilled' ? taskResult.value : null
@@ -192,22 +175,6 @@ async function refreshAll() {
dockerStats.value = dockerResult.status === 'fulfilled' ? dockerResult.value : null dockerStats.value = dockerResult.status === 'fulfilled' ? dockerResult.value : null
systemConfig.value = configResult.status === 'fulfilled' ? configResult.value : null systemConfig.value = configResult.status === 'fulfilled' ? configResult.value : null
if (updateStatusResult.status === 'fulfilled') {
const res = updateStatusResult.value
if (res?.ok) {
updateStatus.value = res.data || null
updateStatusError.value = ''
} else {
updateStatus.value = null
updateStatusError.value = res?.error || '未发现更新状态Update-Agent 可能未运行)'
}
} else {
updateStatus.value = null
updateStatusError.value = ''
}
updateResult.value = updateResultResult.status === 'fulfilled' && updateResultResult.value?.ok ? updateResultResult.value.data : null
await refreshStats?.() await refreshStats?.()
recordUpdatedAt() recordUpdatedAt()
} finally { } finally {
@@ -215,7 +182,19 @@ async function refreshAll() {
} }
} }
onMounted(refreshAll) let refreshTimer = null
onMounted(() => {
refreshAll()
refreshTimer = setInterval(refreshAll, 1000)
})
onUnmounted(() => {
if (refreshTimer) {
clearInterval(refreshTimer)
refreshTimer = null
}
})
</script> </script>
<template> <template>
@@ -225,10 +204,6 @@ onMounted(refreshAll)
<div class="hero-title"> <div class="hero-title">
<div class="hero-title-row"> <div class="hero-title-row">
<h2>报表中心</h2> <h2>报表中心</h2>
<el-tag v-if="updateStatusError" type="info" effect="dark">更新状态未知</el-tag>
<el-tag v-else-if="updateAvailable" type="warning" effect="dark">新版本可更新</el-tag>
<el-tag v-else type="success" effect="dark">已是最新</el-tag>
<el-tag v-if="updateRunning" type="warning" effect="plain">更新中</el-tag>
</div> </div>
<div class="hero-meta app-muted"> <div class="hero-meta app-muted">
<span v-if="lastUpdatedAt">更新时间{{ lastUpdatedAt }}</span> <span v-if="lastUpdatedAt">更新时间{{ lastUpdatedAt }}</span>
@@ -584,21 +559,12 @@ onMounted(refreshAll)
<el-icon><Tools /></el-icon> <el-icon><Tools /></el-icon>
</div> </div>
<div class="head-text"> <div class="head-text">
<div class="panel-title">配置与更新</div> <div class="panel-title">配置概览</div>
<div class="panel-sub app-muted">定时/代理/并发与版本</div> <div class="panel-sub app-muted">定时 / 代理 / 并发</div>
</div> </div>
</div> </div>
<el-tag v-if="updateAvailable" effect="dark" type="warning">可更新</el-tag>
</div> </div>
<el-alert
v-if="updateStatusError"
type="info"
:closable="false"
:title="updateStatusError"
style="margin-bottom: 12px"
/>
<div class="config-grid"> <div class="config-grid">
<div class="config-item"> <div class="config-item">
<div class="config-k app-muted">定时任务</div> <div class="config-k app-muted">定时任务</div>
@@ -631,18 +597,6 @@ onMounted(refreshAll)
</div> </div>
</div> </div>
</div> </div>
<div class="divider"></div>
<div class="sub-title">版本信息</div>
<el-descriptions border :column="1" size="small">
<el-descriptions-item label="本地版本(commit)">{{ shortCommit(updateStatus?.local_commit) }}</el-descriptions-item>
<el-descriptions-item label="远端版本(commit)">{{ shortCommit(updateStatus?.remote_commit) }}</el-descriptions-item>
<el-descriptions-item label="最近检查时间">{{ updateStatus?.checked_at || '-' }}</el-descriptions-item>
<el-descriptions-item v-if="updateResult?.job_id" label="最近更新">
<span>job {{ updateResult.job_id }} / {{ updateResult?.status || '-' }}</span>
</el-descriptions-item>
</el-descriptions>
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>

View File

@@ -1,10 +1,9 @@
<script setup> <script setup>
import { computed, onBeforeUnmount, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { fetchSystemConfig, updateSystemConfig, executeScheduleNow } from '../api/system' import { fetchSystemConfig, updateSystemConfig, executeScheduleNow } from '../api/system'
import { fetchProxyConfig, testProxy, updateProxyConfig } from '../api/proxy' import { fetchProxyConfig, testProxy, updateProxyConfig } from '../api/proxy'
import { fetchUpdateLog, fetchUpdateResult, fetchUpdateStatus, requestUpdateCheck, requestUpdateRun } from '../api/update'
const loading = ref(false) const loading = ref(false)
@@ -29,17 +28,6 @@ const autoApproveEnabled = ref(false)
const autoApproveHourlyLimit = ref(10) const autoApproveHourlyLimit = ref(10)
const autoApproveVipDays = ref(7) const autoApproveVipDays = ref(7)
// 自动更新
const updateLoading = ref(false)
const updateActionLoading = ref(false)
const updateStatus = ref(null)
const updateStatusError = ref('')
const updateResult = ref(null)
const updateLog = ref('')
const updateLogTruncated = ref(false)
const updateBuildNoCache = ref(false)
let updatePollTimer = null
const weekdaysOptions = [ const weekdaysOptions = [
{ label: '周一', value: '1' }, { label: '周一', value: '1' },
{ label: '周二', value: '2' }, { label: '周二', value: '2' },
@@ -71,59 +59,6 @@ function normalizeBrowseType(value) {
return '应读' return '应读'
} }
function shortCommit(value) {
const text = String(value || '').trim()
if (!text) return '-'
return text.length > 12 ? `${text.slice(0, 12)}` : text
}
async function loadUpdateInfo({ withLog = true } = {}) {
updateLoading.value = true
updateStatusError.value = ''
try {
const [statusRes, resultRes] = await Promise.all([fetchUpdateStatus(), fetchUpdateResult()])
if (statusRes?.ok) {
updateStatus.value = statusRes.data || null
} else {
updateStatus.value = null
updateStatusError.value = statusRes?.error || '未发现更新状态Update-Agent 可能未运行)'
}
updateResult.value = resultRes?.ok ? resultRes.data : null
const jobId = updateResult.value?.job_id
if (withLog && jobId) {
const logRes = await fetchUpdateLog({ job_id: jobId, max_bytes: 200000 })
updateLog.value = logRes?.log || ''
updateLogTruncated.value = !!logRes?.truncated
} else {
updateLog.value = ''
updateLogTruncated.value = false
}
} catch {
// handled by interceptor
} finally {
updateLoading.value = false
}
}
function startUpdatePolling() {
if (updatePollTimer) return
updatePollTimer = setInterval(async () => {
if (updateResult.value?.status === 'running') {
await loadUpdateInfo()
}
}, 5000)
}
function stopUpdatePolling() {
if (updatePollTimer) {
clearInterval(updatePollTimer)
updatePollTimer = null
}
}
async function loadAll() { async function loadAll() {
loading.value = true loading.value = true
try { try {
@@ -150,9 +85,6 @@ async function loadAll() {
proxyEnabled.value = (proxy.proxy_enabled ?? 0) === 1 proxyEnabled.value = (proxy.proxy_enabled ?? 0) === 1
proxyApiUrl.value = proxy.proxy_api_url || '' proxyApiUrl.value = proxy.proxy_api_url || ''
proxyExpireMinutes.value = proxy.proxy_expire_minutes ?? 3 proxyExpireMinutes.value = proxy.proxy_expire_minutes ?? 3
await loadUpdateInfo({ withLog: false })
startUpdatePolling()
} catch { } catch {
// handled by interceptor // handled by interceptor
} finally { } finally {
@@ -301,49 +233,7 @@ async function saveAutoApprove() {
} }
} }
async function onCheckUpdate() {
updateActionLoading.value = true
try {
const res = await requestUpdateCheck()
ElMessage.success(res?.success ? '已触发检查更新' : '已提交检查请求')
setTimeout(() => loadUpdateInfo({ withLog: false }), 800)
} catch {
// handled by interceptor
} finally {
updateActionLoading.value = false
}
}
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}${buildFlags}\n\n更新将会重建并重启服务页面可能短暂不可用系统会先备份数据库。`,
'一键更新确认',
{ confirmButtonText: '开始更新', cancelButtonText: '取消', type: 'warning' },
)
} catch {
return
}
updateActionLoading.value = true
try {
const res = await requestUpdateRun({ build_no_cache: updateBuildNoCache.value ? 1 : 0 })
ElMessage.success(res?.message || '已提交更新请求')
startUpdatePolling()
setTimeout(() => loadUpdateInfo(), 800)
} catch {
// handled by interceptor
} finally {
updateActionLoading.value = false
}
}
onMounted(loadAll) onMounted(loadAll)
onBeforeUnmount(stopUpdatePolling)
</script> </script>
<template> <template>
@@ -457,89 +347,6 @@ onBeforeUnmount(stopUpdatePolling)
<el-button type="primary" @click="saveAutoApprove">保存注册设置</el-button> <el-button type="primary" @click="saveAutoApprove">保存注册设置</el-button>
</el-card> </el-card>
<el-card shadow="never" :body-style="{ padding: '16px' }" class="card" v-loading="updateLoading">
<h3 class="section-title">版本与更新</h3>
<el-alert
v-if="updateStatus?.update_available"
type="warning"
:closable="false"
title="检测到新版本:可以在此页面点击“一键更新”升级并自动重启服务。"
style="margin-bottom: 10px"
/>
<el-alert
v-if="updateStatusError"
type="info"
:closable="false"
:title="updateStatusError"
style="margin-bottom: 10px"
/>
<el-descriptions border :column="1" size="small" style="margin-bottom: 10px">
<el-descriptions-item label="本地版本(commit)">
{{ shortCommit(updateStatus?.local_commit) }}
</el-descriptions-item>
<el-descriptions-item label="远端版本(commit)">
{{ shortCommit(updateStatus?.remote_commit) }}
</el-descriptions-item>
<el-descriptions-item label="是否有更新">
<el-tag v-if="updateStatus?.update_available" type="danger"></el-tag>
<el-tag v-else type="success"></el-tag>
</el-descriptions-item>
<el-descriptions-item label="工作区修改">
<el-tag v-if="updateStatus?.dirty" type="warning">有未提交修改</el-tag>
<el-tag v-else type="info">干净</el-tag>
</el-descriptions-item>
<el-descriptions-item label="最近检查时间">
{{ updateStatus?.checked_at || '-' }}
</el-descriptions-item>
<el-descriptions-item v-if="updateStatus?.error" label="检查错误">
{{ updateStatus?.error }}
</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>
<el-button type="danger" @click="onRunUpdate" :loading="updateActionLoading" :disabled="!updateStatus?.update_available">
一键更新
</el-button>
</div>
<el-divider content-position="left">最近一次更新结果</el-divider>
<el-descriptions v-if="updateResult" border :column="1" size="small" style="margin-bottom: 10px">
<el-descriptions-item label="job_id">{{ updateResult.job_id }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag v-if="updateResult.status === 'running'" type="warning">运行中</el-tag>
<el-tag v-else-if="updateResult.status === 'success'" type="success">成功</el-tag>
<el-tag v-else type="danger">失败</el-tag>
</el-descriptions-item>
<el-descriptions-item label="阶段">{{ updateResult.stage || '-' }}</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ updateResult.started_at || '-' }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ updateResult.finished_at || '-' }}</el-descriptions-item>
<el-descriptions-item label="耗时(秒)">{{ updateResult.duration_seconds ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="更新前(commit)">{{ shortCommit(updateResult.from_commit) }}</el-descriptions-item>
<el-descriptions-item label="更新后(commit)">{{ shortCommit(updateResult.to_commit) }}</el-descriptions-item>
<el-descriptions-item label="健康检查">
<span v-if="updateResult.health_ok === true">通过{{ updateResult.health_message }}</span>
<span v-else-if="updateResult.health_ok === false">失败{{ updateResult.health_message }}</span>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item v-if="updateResult.error" label="错误">{{ updateResult.error }}</el-descriptions-item>
</el-descriptions>
<div v-else class="help">暂无更新记录</div>
<el-divider content-position="left">更新日志</el-divider>
<div class="help" v-if="updateLogTruncated">日志过长仅展示末尾内容</div>
<el-input v-model="updateLog" type="textarea" :rows="10" readonly placeholder="暂无日志" />
</el-card>
</div> </div>
</template> </template>

View File

@@ -8,7 +8,6 @@ admin_api_bp = Blueprint("admin_api", __name__, url_prefix="/yuyx/api")
# Import side effects: register routes on blueprint # Import side effects: register routes on blueprint
from routes.admin_api import core as _core # noqa: F401 from routes.admin_api import core as _core # noqa: F401
from routes.admin_api import update as _update # noqa: F401
# Export security blueprint for app registration # Export security blueprint for app registration
from routes.admin_api.security import security_bp # noqa: F401 from routes.admin_api.security import security_bp # noqa: F401

View File

@@ -1,190 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
import os
import time
import uuid
from flask import jsonify, request, session
from routes.admin_api import admin_api_bp
from routes.decorators import admin_required
from services.time_utils import get_beijing_now
from services.update_files import (
ensure_update_dirs,
get_update_job_log_path,
get_update_request_path,
get_update_result_path,
get_update_status_path,
load_json_file,
sanitize_job_id,
tail_text_file,
write_json_atomic,
)
def _request_ip() -> str:
try:
return request.headers.get("X-Forwarded-For", "").split(",")[0].strip() or request.remote_addr or ""
except Exception:
return ""
def _make_job_id(prefix: str = "upd") -> str:
now_str = get_beijing_now().strftime("%Y%m%d_%H%M%S")
rand = uuid.uuid4().hex[:8]
return f"{prefix}_{now_str}_{rand}"
def _has_pending_request() -> bool:
try:
return os.path.exists(get_update_request_path())
except Exception:
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")
def _admin_reauth_required() -> bool:
try:
return time.time() > float(session.get("admin_reauth_until", 0) or 0)
except Exception:
return True
@admin_api_bp.route("/update/status", methods=["GET"])
@admin_required
def get_update_status_api():
"""读取宿主机 Update-Agent 写入的 update/status.json。"""
ensure_update_dirs()
status_path = get_update_status_path()
data, err = load_json_file(status_path)
if err:
return jsonify({"ok": False, "error": f"读取 status 失败: {err}", "data": data}), 200
if not data:
return jsonify({"ok": False, "error": "未发现更新状态Update-Agent 可能未运行)"}), 200
data.setdefault("update_available", False)
return jsonify({"ok": True, "data": data}), 200
@admin_api_bp.route("/update/result", methods=["GET"])
@admin_required
def get_update_result_api():
"""读取 update/result.json最近一次更新执行结果"""
ensure_update_dirs()
result_path = get_update_result_path()
data, err = load_json_file(result_path)
if err:
return jsonify({"ok": False, "error": f"读取 result 失败: {err}", "data": data}), 200
if not data:
return jsonify({"ok": True, "data": None}), 200
return jsonify({"ok": True, "data": data}), 200
@admin_api_bp.route("/update/log", methods=["GET"])
@admin_required
def get_update_log_api():
"""读取 update/jobs/<job_id>.log 的末尾内容(用于后台展示进度)。"""
ensure_update_dirs()
job_id = sanitize_job_id(request.args.get("job_id"))
if not job_id:
# 若未指定,则尝试用 result.json 的 job_id
result_data, _ = load_json_file(get_update_result_path())
job_id = sanitize_job_id(result_data.get("job_id") if isinstance(result_data, dict) else None)
if not job_id:
return jsonify({"ok": True, "job_id": None, "log": "", "truncated": False}), 200
max_bytes = request.args.get("max_bytes", "200000")
try:
max_bytes_i = int(max_bytes)
except Exception:
max_bytes_i = 200_000
max_bytes_i = max(10_000, min(2_000_000, max_bytes_i))
log_path = get_update_job_log_path(job_id)
text, truncated = tail_text_file(log_path, max_bytes=max_bytes_i)
return jsonify({"ok": True, "job_id": job_id, "log": text, "truncated": truncated}), 200
@admin_api_bp.route("/update/check", methods=["POST"])
@admin_required
def request_update_check_api():
"""请求宿主机 Update-Agent 立刻执行一次检查更新。"""
ensure_update_dirs()
if _has_pending_request():
return jsonify({"error": "已有更新请求正在处理中,请稍后再试"}), 409
job_id = _make_job_id(prefix="chk")
payload = {
"job_id": job_id,
"action": "check",
"requested_at": get_beijing_now().strftime("%Y-%m-%d %H:%M:%S"),
"requested_by": session.get("admin_username") or "",
"requested_ip": _request_ip(),
}
write_json_atomic(get_update_request_path(), payload)
return jsonify({"success": True, "job_id": job_id}), 200
@admin_api_bp.route("/update/run", methods=["POST"])
@admin_required
def request_update_run_api():
"""请求宿主机 Update-Agent 执行一键更新并重启服务。"""
ensure_update_dirs()
if _admin_reauth_required():
return jsonify({"error": "需要二次确认", "code": "reauth_required"}), 401
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,
"action": "update",
"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(
{
"success": True,
"job_id": job_id,
"message": "已提交更新请求服务将重启页面可能短暂不可用请等待1-2分钟后刷新",
}
), 200

View File

@@ -1,34 +1,34 @@
{ {
"_email-BghJNgj1.js": { "_email-D-nWLD-A.js": {
"file": "assets/email-BghJNgj1.js", "file": "assets/email-D-nWLD-A.js",
"name": "email", "name": "email",
"imports": [ "imports": [
"index.html" "index.html"
] ]
}, },
"_tasks-Cx_Yf55V.js": { "_system-CJ2QU_TO.js": {
"file": "assets/tasks-Cx_Yf55V.js", "file": "assets/system-CJ2QU_TO.js",
"name": "system",
"imports": [
"index.html"
]
},
"_tasks-CEM7_KIT.js": {
"file": "assets/tasks-CEM7_KIT.js",
"name": "tasks", "name": "tasks",
"imports": [ "imports": [
"index.html" "index.html"
] ]
}, },
"_update-D34iQbO6.js": { "_users-CA0gIT8G.js": {
"file": "assets/update-D34iQbO6.js", "file": "assets/users-CA0gIT8G.js",
"name": "update",
"imports": [
"index.html"
]
},
"_users-DCcrmSwH.js": {
"file": "assets/users-DCcrmSwH.js",
"name": "users", "name": "users",
"imports": [ "imports": [
"index.html" "index.html"
] ]
}, },
"index.html": { "index.html": {
"file": "assets/index-C9w-iZIr.js", "file": "assets/index-akVRSJTL.js",
"name": "index", "name": "index",
"src": "index.html", "src": "index.html",
"isEntry": true, "isEntry": true,
@@ -48,7 +48,7 @@
] ]
}, },
"src/pages/AnnouncementsPage.vue": { "src/pages/AnnouncementsPage.vue": {
"file": "assets/AnnouncementsPage-DEX_yASt.js", "file": "assets/AnnouncementsPage-7ij3KbUN.js",
"name": "AnnouncementsPage", "name": "AnnouncementsPage",
"src": "src/pages/AnnouncementsPage.vue", "src": "src/pages/AnnouncementsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -60,12 +60,12 @@
] ]
}, },
"src/pages/EmailPage.vue": { "src/pages/EmailPage.vue": {
"file": "assets/EmailPage-Cev_X_Ce.js", "file": "assets/EmailPage-CuPwCZn-.js",
"name": "EmailPage", "name": "EmailPage",
"src": "src/pages/EmailPage.vue", "src": "src/pages/EmailPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_email-BghJNgj1.js", "_email-D-nWLD-A.js",
"index.html" "index.html"
], ],
"css": [ "css": [
@@ -73,7 +73,7 @@
] ]
}, },
"src/pages/FeedbacksPage.vue": { "src/pages/FeedbacksPage.vue": {
"file": "assets/FeedbacksPage-BKxylUkG.js", "file": "assets/FeedbacksPage-CSDNvoUn.js",
"name": "FeedbacksPage", "name": "FeedbacksPage",
"src": "src/pages/FeedbacksPage.vue", "src": "src/pages/FeedbacksPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -85,13 +85,13 @@
] ]
}, },
"src/pages/LogsPage.vue": { "src/pages/LogsPage.vue": {
"file": "assets/LogsPage-CemQ-Y_T.js", "file": "assets/LogsPage-DDxhRTa7.js",
"name": "LogsPage", "name": "LogsPage",
"src": "src/pages/LogsPage.vue", "src": "src/pages/LogsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_users-DCcrmSwH.js", "_users-CA0gIT8G.js",
"_tasks-Cx_Yf55V.js", "_tasks-CEM7_KIT.js",
"index.html" "index.html"
], ],
"css": [ "css": [
@@ -99,22 +99,22 @@
] ]
}, },
"src/pages/ReportPage.vue": { "src/pages/ReportPage.vue": {
"file": "assets/ReportPage-D6vDD1zK.js", "file": "assets/ReportPage-CoI2Nht-.js",
"name": "ReportPage", "name": "ReportPage",
"src": "src/pages/ReportPage.vue", "src": "src/pages/ReportPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"index.html", "index.html",
"_email-BghJNgj1.js", "_email-D-nWLD-A.js",
"_tasks-Cx_Yf55V.js", "_tasks-CEM7_KIT.js",
"_update-D34iQbO6.js" "_system-CJ2QU_TO.js"
], ],
"css": [ "css": [
"assets/ReportPage-CSbGJlZV.css" "assets/ReportPage-CW7RwLmI.css"
] ]
}, },
"src/pages/SecurityPage.vue": { "src/pages/SecurityPage.vue": {
"file": "assets/SecurityPage-DGvsGoGa.js", "file": "assets/SecurityPage-CQQKpFcS.js",
"name": "SecurityPage", "name": "SecurityPage",
"src": "src/pages/SecurityPage.vue", "src": "src/pages/SecurityPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -126,7 +126,7 @@
] ]
}, },
"src/pages/SettingsPage.vue": { "src/pages/SettingsPage.vue": {
"file": "assets/SettingsPage-Bw1ItHlK.js", "file": "assets/SettingsPage-BpSZamEk.js",
"name": "SettingsPage", "name": "SettingsPage",
"src": "src/pages/SettingsPage.vue", "src": "src/pages/SettingsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -138,25 +138,25 @@
] ]
}, },
"src/pages/SystemPage.vue": { "src/pages/SystemPage.vue": {
"file": "assets/SystemPage-RgAQwtHu.js", "file": "assets/SystemPage-DUY6QC8Y.js",
"name": "SystemPage", "name": "SystemPage",
"src": "src/pages/SystemPage.vue", "src": "src/pages/SystemPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_update-D34iQbO6.js", "_system-CJ2QU_TO.js",
"index.html" "index.html"
], ],
"css": [ "css": [
"assets/SystemPage-BjTkcmTG.css" "assets/SystemPage-DhVR0HeO.css"
] ]
}, },
"src/pages/UsersPage.vue": { "src/pages/UsersPage.vue": {
"file": "assets/UsersPage-CFbr6Y3k.js", "file": "assets/UsersPage-hj_Nb-9c.js",
"name": "UsersPage", "name": "UsersPage",
"src": "src/pages/UsersPage.vue", "src": "src/pages/UsersPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_users-DCcrmSwH.js", "_users-CA0gIT8G.js",
"index.html" "index.html"
], ],
"css": [ "css": [

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{O as m,_ as T,r as p,d as u,e as h,f as k,g as r,h as a,w as s,n as x,J as d,I as b}from"./index-C9w-iZIr.js";async function C(o){const{data:t}=await m.put("/admin/username",{new_username:o});return t}async function P(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(""),i=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){d.error("请输入新用户名");return}try{await b.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 B(){const l=i.value;if(!l){d.error("请输入新密码");return}if(l.length<6){d.error("密码至少6个字符");return}try{await b.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await P(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",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: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:B},{default:s(()=>[...e[4]||(e[4]=[x("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码至少8位且包含字母与数字。",-1))]),_:1})])}}},I=T(N,[["__scopeId","data-v-2f4b840f"]]);export{I as default}; 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-d88590f1]{display:flex;flex-direction:column;gap:12px}.card[data-v-d88590f1]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-title[data-v-d88590f1]{margin:0 0 12px;font-size:14px;font-weight:800}.help[data-v-d88590f1]{margin-top:6px;font-size:12px;color:var(--app-muted)}.row-actions[data-v-d88590f1]{display:flex;flex-wrap:wrap;gap:10px}

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -1 +1 @@
import{O as n}from"./index-C9w-iZIr.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-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};

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -1 +1 @@
import{O as a}from"./index-C9w-iZIr.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-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};

View File

@@ -1 +0,0 @@
import{O as a}from"./index-C9w-iZIr.js";async function s(){const{data:t}=await a.get("/system/config");return t}async function c(t){const{data:e}=await a.post("/system/config",t);return e}async function u(){const{data:t}=await a.post("/schedule/execute",{});return t}async function o(){const{data:t}=await a.get("/update/status");return t}async function r(){const{data:t}=await a.get("/update/result");return t}async function d(t={}){const{data:e}=await a.get("/update/log",{params:t});return e}async function i(){const{data:t}=await a.post("/update/check",{});return t}async function f(t={}){const{data:e}=await a.post("/update/run",t);return e}export{o as a,r as b,d as c,f as d,u as e,s as f,i as r,c as u};

View File

@@ -1 +1 @@
import{O as t}from"./index-C9w-iZIr.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-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};

View File

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