feat(report): add 24h slow-sql dashboard and metrics api

This commit is contained in:
2026-02-07 14:07:07 +08:00
parent 52dd7ac9e5
commit 6a9858cdec
29 changed files with 427 additions and 57 deletions

View File

@@ -27,6 +27,10 @@ DB_LOCK_RETRY_COUNT=3
DB_LOCK_RETRY_BASE_MS=50 DB_LOCK_RETRY_BASE_MS=50
DB_SLOW_QUERY_MS=120 DB_SLOW_QUERY_MS=120
DB_SLOW_QUERY_SQL_MAX_LEN=240 DB_SLOW_QUERY_SQL_MAX_LEN=240
DB_SLOW_SQL_WINDOW_SECONDS=86400
DB_SLOW_SQL_TOP_LIMIT=12
DB_SLOW_SQL_RECENT_LIMIT=50
DB_SLOW_SQL_MAX_EVENTS=20000
DB_PRAGMA_OPTIMIZE_INTERVAL_SECONDS=21600 DB_PRAGMA_OPTIMIZE_INTERVAL_SECONDS=21600
DB_ANALYZE_INTERVAL_SECONDS=86400 DB_ANALYZE_INTERVAL_SECONDS=86400
DB_WAL_CHECKPOINT_INTERVAL_SECONDS=43200 DB_WAL_CHECKPOINT_INTERVAL_SECONDS=43200

View File

@@ -15,6 +15,11 @@ export async function fetchRequestMetrics() {
return data return data
} }
export async function fetchSlowSqlMetrics() {
const { data } = await api.get('/slow_sql_metrics')
return data
}
export async function fetchTaskStats() { export async function fetchTaskStats() {
const { data } = await api.get('/task/stats') const { data } = await api.get('/task/stats')
return data return data

View File

@@ -17,7 +17,7 @@ import {
import { fetchFeedbackStats } from '../api/feedbacks' import { fetchFeedbackStats } from '../api/feedbacks'
import { fetchEmailStats } from '../api/email' import { fetchEmailStats } from '../api/email'
import { fetchDockerStats, fetchRequestMetrics, fetchRunningTasks, fetchServerInfo, fetchTaskStats } from '../api/tasks' import { fetchDockerStats, fetchRequestMetrics, fetchRunningTasks, fetchServerInfo, fetchSlowSqlMetrics, fetchTaskStats } from '../api/tasks'
import { fetchBrowserPoolStats } from '../api/browser_pool' import { fetchBrowserPoolStats } from '../api/browser_pool'
import { fetchSystemConfig } from '../api/system' import { fetchSystemConfig } from '../api/system'
import MetricGrid from '../components/MetricGrid.vue' import MetricGrid from '../components/MetricGrid.vue'
@@ -38,7 +38,9 @@ const dockerStats = ref(null)
const browserPoolStats = ref(null) const browserPoolStats = ref(null)
const systemConfig = ref(null) const systemConfig = ref(null)
const requestMetrics = ref(null) const requestMetrics = ref(null)
const slowSqlMetrics = ref(null)
const requestDetailsOpen = ref(false) const requestDetailsOpen = ref(false)
const slowSqlDetailsOpen = ref(false)
const queueTab = ref('running') const queueTab = ref('running')
function recordUpdatedAt() { function recordUpdatedAt() {
@@ -350,6 +352,66 @@ const requestModuleDesc = computed(() => {
return `均值 ${avgMs} · 峰值 ${maxMs} · 慢阈 ${slowThreshold} · 最近 ${lastAt}` return `均值 ${avgMs} · 峰值 ${maxMs} · 慢阈 ${slowThreshold} · 最近 ${lastAt}`
}) })
const slowSqlTopRows = computed(() => {
const rows = Array.isArray(slowSqlMetrics.value?.top_sql) ? slowSqlMetrics.value.top_sql : []
return rows.slice(0, 3)
})
const slowSqlWindowHours = computed(() => {
const seconds = normalizeCount(slowSqlMetrics.value?.window_seconds)
if (seconds <= 0) return 24
return Math.max(1, Math.round(seconds / 3600))
})
const slowSqlModuleItems = computed(() => {
const items = [
{ label: `慢SQL(${slowSqlWindowHours.value}h)`, value: normalizeCount(slowSqlMetrics.value?.total_slow_queries) },
{ label: '去重SQL', value: normalizeCount(slowSqlMetrics.value?.unique_sql) },
{ label: '平均耗时', value: formatMs(slowSqlMetrics.value?.avg_duration_ms) },
{ label: '峰值耗时', value: formatMs(slowSqlMetrics.value?.max_duration_ms) },
]
slowSqlTopRows.value.forEach((row, index) => {
items.push({
label: `慢SQL${index + 1}`,
value: `${formatMs(row?.max_ms)} · ${String(row?.sql || '-')}`,
})
})
return items
})
const slowSqlModuleDesc = computed(() => {
const threshold = formatMs(slowSqlMetrics.value?.slow_threshold_ms)
const lastAt = formatUnixTime(slowSqlMetrics.value?.last_slow_ts)
return `窗口 ${slowSqlWindowHours.value}h · 慢阈 ${threshold} · 最近 ${lastAt}`
})
const slowSqlDetailTopRows = computed(() => {
const rows = Array.isArray(slowSqlMetrics.value?.top_sql) ? slowSqlMetrics.value.top_sql : []
return rows.map((row, index) => ({
rank: index + 1,
sql: String(row?.sql || '-'),
count: normalizeCount(row?.count),
avg_ms: formatMs(row?.avg_ms),
max_ms: formatMs(row?.max_ms),
last_seen: formatUnixDateTime(row?.last_ts),
sample_params: String(row?.sample_params || '-'),
}))
})
const slowSqlRecentRows = computed(() => {
const rows = Array.isArray(slowSqlMetrics.value?.recent_slow_sql) ? slowSqlMetrics.value.recent_slow_sql : []
return [...rows]
.sort((a, b) => Number(b?.time || 0) - Number(a?.time || 0))
.map((row) => ({
time_text: formatUnixDateTime(row?.time),
sql: String(row?.sql || '-'),
duration_ms: formatMs(row?.duration_ms),
params: String(row?.params || '-'),
}))
})
const requestDetailTopPaths = computed(() => { const requestDetailTopPaths = computed(() => {
const rows = Array.isArray(requestMetrics.value?.top_paths) ? requestMetrics.value.top_paths : [] const rows = Array.isArray(requestMetrics.value?.top_paths) ? requestMetrics.value.top_paths : []
return rows.map((row, index) => ({ return rows.map((row, index) => ({
@@ -387,6 +449,10 @@ function openRequestDetails() {
requestDetailsOpen.value = true requestDetailsOpen.value = true
} }
function openSlowSqlDetails() {
slowSqlDetailsOpen.value = true
}
const configModuleItems = computed(() => [ const configModuleItems = computed(() => [
{ label: '定时任务', value: scheduleEnabled.value ? '启用' : '关闭' }, { label: '定时任务', value: scheduleEnabled.value ? '启用' : '关闭' },
{ label: '执行时间', value: scheduleTime.value || '-' }, { label: '执行时间', value: scheduleTime.value || '-' },
@@ -448,6 +514,13 @@ const mobileModules = computed(() => [
tone: 'purple', tone: 'purple',
items: requestModuleItems.value, items: requestModuleItems.value,
}, },
{
key: 'slow_sql',
title: '慢SQL监控',
desc: slowSqlModuleDesc.value,
tone: 'red',
items: slowSqlModuleItems.value,
},
{ {
key: 'worker', key: 'worker',
title: '截图线程池', title: '截图线程池',
@@ -481,6 +554,7 @@ async function refreshAll(options = {}) {
dockerResult, dockerResult,
browserPoolResult, browserPoolResult,
requestMetricsResult, requestMetricsResult,
slowSqlResult,
configResult, configResult,
] = await Promise.allSettled([ ] = await Promise.allSettled([
fetchTaskStats(), fetchTaskStats(),
@@ -491,6 +565,7 @@ async function refreshAll(options = {}) {
fetchDockerStats(), fetchDockerStats(),
fetchBrowserPoolStats(), fetchBrowserPoolStats(),
fetchRequestMetrics(), fetchRequestMetrics(),
fetchSlowSqlMetrics(),
fetchSystemConfig(), fetchSystemConfig(),
]) ])
@@ -502,6 +577,7 @@ async function refreshAll(options = {}) {
if (dockerResult.status === 'fulfilled') dockerStats.value = dockerResult.value if (dockerResult.status === 'fulfilled') dockerStats.value = dockerResult.value
if (browserPoolResult.status === 'fulfilled') browserPoolStats.value = browserPoolResult.value if (browserPoolResult.status === 'fulfilled') browserPoolStats.value = browserPoolResult.value
if (requestMetricsResult.status === 'fulfilled') requestMetrics.value = requestMetricsResult.value if (requestMetricsResult.status === 'fulfilled') requestMetrics.value = requestMetricsResult.value
if (slowSqlResult.status === 'fulfilled') slowSqlMetrics.value = slowSqlResult.value
if (configResult.status === 'fulfilled') systemConfig.value = configResult.value if (configResult.status === 'fulfilled') systemConfig.value = configResult.value
await refreshStats?.() await refreshStats?.()
@@ -599,8 +675,15 @@ onUnmounted(() => {
<div class="mobile-metric-value">{{ item.value }}</div> <div class="mobile-metric-value">{{ item.value }}</div>
</div> </div>
</div> </div>
<div v-if="module.key === 'request'" class="module-extra-actions"> <div v-if="module.key === 'request' || module.key === 'slow_sql'" class="module-extra-actions">
<el-button size="small" type="primary" plain @click="openRequestDetails">查看慢接口详情</el-button> <el-button
size="small"
type="primary"
plain
@click="module.key === 'request' ? openRequestDetails() : openSlowSqlDetails()"
>
{{ module.key === 'request' ? '查看慢接口详情' : '查看慢SQL详情' }}
</el-button>
</div> </div>
</el-card> </el-card>
</section> </section>
@@ -644,6 +727,43 @@ onUnmounted(() => {
</div> </div>
</div> </div>
</el-dialog> </el-dialog>
<el-dialog v-model="slowSqlDetailsOpen" title="慢SQL详情近24小时" width="min(1080px, 96vw)">
<div class="request-dialog-summary app-muted">
<span>慢SQL总数{{ normalizeCount(slowSqlMetrics?.total_slow_queries) }}</span>
<span>去重SQL{{ normalizeCount(slowSqlMetrics?.unique_sql) }}</span>
<span>平均耗时{{ formatMs(slowSqlMetrics?.avg_duration_ms) }}</span>
<span>峰值耗时{{ formatMs(slowSqlMetrics?.max_duration_ms) }}</span>
<span>慢阈值{{ formatMs(slowSqlMetrics?.slow_threshold_ms) }}</span>
</div>
<div class="request-dialog-block">
<div class="request-dialog-title">TOP 慢SQL按出现次数</div>
<div class="table-wrap">
<el-table :data="slowSqlDetailTopRows" size="small" max-height="320">
<el-table-column prop="rank" label="#" width="60" />
<el-table-column prop="sql" label="SQL" min-width="400" show-overflow-tooltip />
<el-table-column prop="count" label="次数" width="90" />
<el-table-column prop="avg_ms" label="平均耗时" width="120" />
<el-table-column prop="max_ms" label="峰值耗时" width="120" />
<el-table-column prop="last_seen" label="最近出现" width="180" />
<el-table-column prop="sample_params" label="参数样本" min-width="140" show-overflow-tooltip />
</el-table>
</div>
</div>
<div class="request-dialog-block">
<div class="request-dialog-title">最近慢SQL</div>
<div class="table-wrap">
<el-table :data="slowSqlRecentRows" size="small" max-height="320">
<el-table-column prop="time_text" label="时间" width="180" />
<el-table-column prop="sql" label="SQL" min-width="420" show-overflow-tooltip />
<el-table-column prop="duration_ms" label="耗时" width="110" />
<el-table-column prop="params" label="参数" min-width="130" show-overflow-tooltip />
</el-table>
</div>
</div>
</el-dialog>
</div> </div>
</template> </template>

View File

@@ -135,6 +135,10 @@ class Config:
DB_LOCK_RETRY_BASE_MS = int(os.environ.get("DB_LOCK_RETRY_BASE_MS", "50")) DB_LOCK_RETRY_BASE_MS = int(os.environ.get("DB_LOCK_RETRY_BASE_MS", "50"))
DB_SLOW_QUERY_MS = int(os.environ.get("DB_SLOW_QUERY_MS", "120")) DB_SLOW_QUERY_MS = int(os.environ.get("DB_SLOW_QUERY_MS", "120"))
DB_SLOW_QUERY_SQL_MAX_LEN = int(os.environ.get("DB_SLOW_QUERY_SQL_MAX_LEN", "240")) DB_SLOW_QUERY_SQL_MAX_LEN = int(os.environ.get("DB_SLOW_QUERY_SQL_MAX_LEN", "240"))
DB_SLOW_SQL_WINDOW_SECONDS = int(os.environ.get("DB_SLOW_SQL_WINDOW_SECONDS", "86400"))
DB_SLOW_SQL_TOP_LIMIT = int(os.environ.get("DB_SLOW_SQL_TOP_LIMIT", "12"))
DB_SLOW_SQL_RECENT_LIMIT = int(os.environ.get("DB_SLOW_SQL_RECENT_LIMIT", "50"))
DB_SLOW_SQL_MAX_EVENTS = int(os.environ.get("DB_SLOW_SQL_MAX_EVENTS", "20000"))
DB_PRAGMA_OPTIMIZE_INTERVAL_SECONDS = int(os.environ.get("DB_PRAGMA_OPTIMIZE_INTERVAL_SECONDS", "21600")) DB_PRAGMA_OPTIMIZE_INTERVAL_SECONDS = int(os.environ.get("DB_PRAGMA_OPTIMIZE_INTERVAL_SECONDS", "21600"))
DB_ANALYZE_INTERVAL_SECONDS = int(os.environ.get("DB_ANALYZE_INTERVAL_SECONDS", "86400")) DB_ANALYZE_INTERVAL_SECONDS = int(os.environ.get("DB_ANALYZE_INTERVAL_SECONDS", "86400"))
DB_WAL_CHECKPOINT_INTERVAL_SECONDS = int(os.environ.get("DB_WAL_CHECKPOINT_INTERVAL_SECONDS", "43200")) DB_WAL_CHECKPOINT_INTERVAL_SECONDS = int(os.environ.get("DB_WAL_CHECKPOINT_INTERVAL_SECONDS", "43200"))
@@ -280,6 +284,14 @@ class Config:
errors.append("DB_SLOW_QUERY_MS不能为负数") errors.append("DB_SLOW_QUERY_MS不能为负数")
if cls.DB_SLOW_QUERY_SQL_MAX_LEN < 80: if cls.DB_SLOW_QUERY_SQL_MAX_LEN < 80:
errors.append("DB_SLOW_QUERY_SQL_MAX_LEN建议至少80") errors.append("DB_SLOW_QUERY_SQL_MAX_LEN建议至少80")
if cls.DB_SLOW_SQL_WINDOW_SECONDS < 600:
errors.append("DB_SLOW_SQL_WINDOW_SECONDS建议至少600")
if cls.DB_SLOW_SQL_TOP_LIMIT < 5:
errors.append("DB_SLOW_SQL_TOP_LIMIT建议至少5")
if cls.DB_SLOW_SQL_RECENT_LIMIT < 10:
errors.append("DB_SLOW_SQL_RECENT_LIMIT建议至少10")
if cls.DB_SLOW_SQL_MAX_EVENTS < cls.DB_SLOW_SQL_RECENT_LIMIT:
errors.append("DB_SLOW_SQL_MAX_EVENTS必须不小于DB_SLOW_SQL_RECENT_LIMIT")
# 验证日志配置 # 验证日志配置
if cls.LOG_LEVEL not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]: if cls.LOG_LEVEL not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:

View File

@@ -302,9 +302,17 @@ class PooledConnection:
return return
if elapsed_ms < DB_SLOW_QUERY_MS: if elapsed_ms < DB_SLOW_QUERY_MS:
return return
logger.warning(
f"[慢SQL] {elapsed_ms:.1f}ms sql=\"{_compact_sql(sql)}\" params={_describe_params(parameters)}" params_info = _describe_params(parameters)
)
try:
from services.slow_sql_metrics import record_slow_sql
record_slow_sql(sql=sql, duration_ms=elapsed_ms, params_info=params_info)
except Exception:
pass
logger.warning(f"[慢SQL] {elapsed_ms:.1f}ms sql=\"{_compact_sql(sql)}\" params={params_info}")
def cursor(self): def cursor(self):
"""获取游标""" """获取游标"""

View File

@@ -33,6 +33,12 @@ services:
- DB_FILE=data/app_data.db - DB_FILE=data/app_data.db
- DB_POOL_SIZE=5 - DB_POOL_SIZE=5
- SYSTEM_CONFIG_CACHE_TTL_SECONDS=30 - SYSTEM_CONFIG_CACHE_TTL_SECONDS=30
- DB_SLOW_QUERY_MS=120
- DB_SLOW_SQL_WINDOW_SECONDS=86400
- DB_SLOW_SQL_TOP_LIMIT=12
- DB_SLOW_SQL_RECENT_LIMIT=50
- DB_SLOW_SQL_MAX_EVENTS=20000
- ADMIN_SLOW_SQL_METRICS_CACHE_TTL_SECONDS=3
# 并发控制配置 # 并发控制配置
- MAX_CONCURRENT_GLOBAL=2 - MAX_CONCURRENT_GLOBAL=2
- MAX_CONCURRENT_PER_ACCOUNT=1 - MAX_CONCURRENT_PER_ACCOUNT=1

View File

@@ -14,6 +14,7 @@ from flask import jsonify, session
from routes.admin_api import admin_api_bp from routes.admin_api import admin_api_bp
from routes.decorators import admin_required from routes.decorators import admin_required
from services.request_metrics import get_request_metrics_snapshot from services.request_metrics import get_request_metrics_snapshot
from services.slow_sql_metrics import get_slow_sql_metrics_snapshot
from services.time_utils import BEIJING_TZ, get_beijing_now from services.time_utils import BEIJING_TZ, get_beijing_now
logger = get_logger("app") logger = get_logger("app")
@@ -30,6 +31,10 @@ _REQUEST_METRICS_CACHE_TTL = max(1.0, float(os.environ.get("ADMIN_REQUEST_METRIC
_request_metrics_cache: dict[str, object] = {"expires_at_monotonic": 0.0, "data": None} _request_metrics_cache: dict[str, object] = {"expires_at_monotonic": 0.0, "data": None}
_request_metrics_cache_lock = threading.Lock() _request_metrics_cache_lock = threading.Lock()
_SLOW_SQL_METRICS_CACHE_TTL = max(1.0, float(os.environ.get("ADMIN_SLOW_SQL_METRICS_CACHE_TTL_SECONDS", "3")))
_slow_sql_metrics_cache: dict[str, object] = {"expires_at_monotonic": 0.0, "data": None}
_slow_sql_metrics_cache_lock = threading.Lock()
def _get_system_stats_cached() -> dict: def _get_system_stats_cached() -> dict:
now = time.monotonic() now = time.monotonic()
@@ -65,6 +70,23 @@ def _get_request_metrics_cached() -> dict:
return dict(fresh_data) return dict(fresh_data)
def _get_slow_sql_metrics_cached() -> dict:
now = time.monotonic()
with _slow_sql_metrics_cache_lock:
expires_at = float(_slow_sql_metrics_cache.get("expires_at_monotonic") or 0.0)
cached_data = _slow_sql_metrics_cache.get("data")
if isinstance(cached_data, dict) and now < expires_at:
return dict(cached_data)
fresh_data = get_slow_sql_metrics_snapshot() or {}
with _slow_sql_metrics_cache_lock:
_slow_sql_metrics_cache["data"] = dict(fresh_data)
_slow_sql_metrics_cache["expires_at_monotonic"] = now + _SLOW_SQL_METRICS_CACHE_TTL
return dict(fresh_data)
@admin_api_bp.route("/stats", methods=["GET"]) @admin_api_bp.route("/stats", methods=["GET"])
@admin_required @admin_required
def get_system_stats(): def get_system_stats():
@@ -86,6 +108,18 @@ def get_request_metrics():
return jsonify({"error": "获取请求级监控指标失败"}), 500 return jsonify({"error": "获取请求级监控指标失败"}), 500
@admin_api_bp.route("/slow_sql_metrics", methods=["GET"])
@admin_required
def get_slow_sql_metrics():
"""获取慢 SQL 监控指标"""
try:
metrics = _get_slow_sql_metrics_cached()
return jsonify(metrics)
except Exception as e:
logger.exception(f"获取慢 SQL 监控指标失败: {e}")
return jsonify({"error": "获取慢 SQL 监控指标失败"}), 500
@admin_api_bp.route("/browser_pool/stats", methods=["GET"]) @admin_api_bp.route("/browser_pool/stats", methods=["GET"])
@admin_required @admin_required
def get_browser_pool_stats(): def get_browser_pool_stats():

View File

@@ -0,0 +1,181 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
慢 SQL 指标(轻量内存版)
- 记录超过阈值的 SQL 执行样本
- 维护近窗口期默认24小时聚合统计
- 输出 TOP SQL 与最近慢 SQL 列表
"""
from __future__ import annotations
import os
import threading
import time
from collections import deque
_SLOW_SQL_THRESHOLD_MS = max(0.0, float(os.environ.get("DB_SLOW_QUERY_MS", "120") or 120))
_WINDOW_SECONDS = max(600, int(os.environ.get("DB_SLOW_SQL_WINDOW_SECONDS", "86400") or 86400))
_TOP_LIMIT = max(5, int(os.environ.get("DB_SLOW_SQL_TOP_LIMIT", "12") or 12))
_RECENT_LIMIT = max(10, int(os.environ.get("DB_SLOW_SQL_RECENT_LIMIT", "50") or 50))
_MAX_EVENTS = max(_RECENT_LIMIT, int(os.environ.get("DB_SLOW_SQL_MAX_EVENTS", "20000") or 20000))
_SQL_MAX_LEN = max(80, int(os.environ.get("DB_SLOW_QUERY_SQL_MAX_LEN", "240") or 240))
_lock = threading.Lock()
_state = {
"start_ts": time.time(),
"last_slow_ts": 0.0,
"events": deque(),
"recent": deque(maxlen=_RECENT_LIMIT),
}
def _compact_text(value: str, max_len: int) -> str:
text = " ".join(str(value or "").split())
if len(text) <= max_len:
return text
return f"{text[: max_len - 3]}..."
def _compact_sql(sql: str) -> str:
return _compact_text(str(sql or ""), _SQL_MAX_LEN)
def _compact_params(params_info: str) -> str:
return _compact_text(str(params_info or "none"), 64)
def _prune_events_locked(now_ts: float) -> None:
cutoff_ts = now_ts - float(_WINDOW_SECONDS)
events = _state["events"]
while events and float(events[0].get("time", 0.0) or 0.0) < cutoff_ts:
events.popleft()
overflow = len(events) - int(_MAX_EVENTS)
while overflow > 0 and events:
events.popleft()
overflow -= 1
def record_slow_sql(*, sql: str, duration_ms: float, params_info: str = "none") -> None:
duration = max(0.0, float(duration_ms or 0.0))
now = time.time()
sql_text = _compact_sql(sql)
params_text = _compact_params(params_info)
event = {
"time": now,
"sql": sql_text,
"duration_ms": round(duration, 2),
"params": params_text,
}
with _lock:
_prune_events_locked(now)
_state["events"].append(event)
_state["recent"].append(event)
_state["last_slow_ts"] = now
def get_slow_sql_metrics_snapshot() -> dict:
now = time.time()
with _lock:
_prune_events_locked(now)
events = list(_state["events"])
recent_rows = list(_state["recent"])
last_slow_ts = float(_state.get("last_slow_ts") or 0.0)
grouped: dict[str, dict] = {}
total_duration_ms = 0.0
max_duration_ms = 0.0
for item in events:
sql_text = str(item.get("sql") or "-")
duration = float(item.get("duration_ms") or 0.0)
ts = float(item.get("time") or 0.0)
params_text = str(item.get("params") or "none")
total_duration_ms += duration
if duration > max_duration_ms:
max_duration_ms = duration
bucket = grouped.get(sql_text)
if bucket is None:
bucket = {
"sql": sql_text,
"count": 0,
"total_ms": 0.0,
"max_ms": 0.0,
"last_ts": 0.0,
"params": params_text,
}
grouped[sql_text] = bucket
bucket["count"] = int(bucket["count"] or 0) + 1
bucket["total_ms"] = float(bucket["total_ms"] or 0.0) + duration
if duration > float(bucket["max_ms"] or 0.0):
bucket["max_ms"] = duration
bucket["params"] = params_text
if ts >= float(bucket["last_ts"] or 0.0):
bucket["last_ts"] = ts
top_sql_rows = sorted(
grouped.values(),
key=lambda row: (
int(row.get("count", 0) or 0),
float(row.get("max_ms", 0.0) or 0.0),
float(row.get("total_ms", 0.0) or 0.0),
),
reverse=True,
)[:_TOP_LIMIT]
top_sql = []
for idx, row in enumerate(top_sql_rows, start=1):
count = int(row.get("count", 0) or 0)
total_ms = float(row.get("total_ms", 0.0) or 0.0)
avg_ms = (total_ms / count) if count > 0 else 0.0
top_sql.append(
{
"rank": idx,
"sql": row.get("sql") or "-",
"count": count,
"avg_ms": round(avg_ms, 2),
"max_ms": round(float(row.get("max_ms", 0.0) or 0.0), 2),
"last_ts": int(float(row.get("last_ts", 0.0) or 0.0)),
"sample_params": row.get("params") or "none",
}
)
cutoff_ts = now - float(_WINDOW_SECONDS)
recent = [
{
"time": int(float(item.get("time") or 0.0)),
"sql": str(item.get("sql") or "-"),
"duration_ms": round(float(item.get("duration_ms") or 0.0), 2),
"params": str(item.get("params") or "none"),
}
for item in recent_rows
if float(item.get("time") or 0.0) >= cutoff_ts
]
total_events = len(events)
avg_duration_ms = round((total_duration_ms / total_events), 2) if total_events > 0 else 0.0
return {
"since_ts": int(float(events[0].get("time") or 0.0)) if events else 0,
"window_seconds": _WINDOW_SECONDS,
"top_limit": _TOP_LIMIT,
"recent_limit": _RECENT_LIMIT,
"slow_threshold_ms": _SLOW_SQL_THRESHOLD_MS,
"total_slow_queries": total_events,
"unique_sql": len(grouped),
"avg_duration_ms": avg_duration_ms,
"max_duration_ms": round(max_duration_ms, 2),
"last_slow_ts": int(last_slow_ts) if last_slow_ts > 0 else 0,
"top_sql": top_sql,
"recent_slow_sql": recent,
}

View File

@@ -1,6 +1,6 @@
{ {
"_MetricGrid-Db4tVG3V.js": { "_MetricGrid-Cm-6uQPC.js": {
"file": "assets/MetricGrid-Db4tVG3V.js", "file": "assets/MetricGrid-Cm-6uQPC.js",
"name": "MetricGrid", "name": "MetricGrid",
"imports": [ "imports": [
"index.html", "index.html",
@@ -14,29 +14,29 @@
"file": "assets/MetricGrid-yP_dkP6X.css", "file": "assets/MetricGrid-yP_dkP6X.css",
"src": "_MetricGrid-yP_dkP6X.css" "src": "_MetricGrid-yP_dkP6X.css"
}, },
"_email-DUrghb1a.js": { "_email-UCdCFkiw.js": {
"file": "assets/email-DUrghb1a.js", "file": "assets/email-UCdCFkiw.js",
"name": "email", "name": "email",
"imports": [ "imports": [
"index.html" "index.html"
] ]
}, },
"_system-CKJgzNxM.js": { "_system-BqUnjCUC.js": {
"file": "assets/system-CKJgzNxM.js", "file": "assets/system-BqUnjCUC.js",
"name": "system", "name": "system",
"imports": [ "imports": [
"index.html" "index.html"
] ]
}, },
"_tasks-h6MdOVMy.js": { "_tasks-DW-oE78v.js": {
"file": "assets/tasks-h6MdOVMy.js", "file": "assets/tasks-DW-oE78v.js",
"name": "tasks", "name": "tasks",
"imports": [ "imports": [
"index.html" "index.html"
] ]
}, },
"_users-C4yl3Txu.js": { "_users-LqVgYOIa.js": {
"file": "assets/users-C4yl3Txu.js", "file": "assets/users-LqVgYOIa.js",
"name": "users", "name": "users",
"imports": [ "imports": [
"index.html" "index.html"
@@ -54,7 +54,7 @@
"src": "_vendor-C68yOrAy.css" "src": "_vendor-C68yOrAy.css"
}, },
"index.html": { "index.html": {
"file": "assets/index-UGawl5hI.js", "file": "assets/index-C51y9xCM.js",
"name": "index", "name": "index",
"src": "index.html", "src": "index.html",
"isEntry": true, "isEntry": true,
@@ -77,7 +77,7 @@
] ]
}, },
"src/pages/AnnouncementsPage.vue": { "src/pages/AnnouncementsPage.vue": {
"file": "assets/AnnouncementsPage-C6-W6_wH.js", "file": "assets/AnnouncementsPage-D4vGLsnP.js",
"name": "AnnouncementsPage", "name": "AnnouncementsPage",
"src": "src/pages/AnnouncementsPage.vue", "src": "src/pages/AnnouncementsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -90,14 +90,14 @@
] ]
}, },
"src/pages/EmailPage.vue": { "src/pages/EmailPage.vue": {
"file": "assets/EmailPage-CewNq_Xr.js", "file": "assets/EmailPage-BgnCtp6d.js",
"name": "EmailPage", "name": "EmailPage",
"src": "src/pages/EmailPage.vue", "src": "src/pages/EmailPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_email-DUrghb1a.js", "_email-UCdCFkiw.js",
"index.html", "index.html",
"_MetricGrid-Db4tVG3V.js", "_MetricGrid-Cm-6uQPC.js",
"_vendor-BczUEOE_.js" "_vendor-BczUEOE_.js"
], ],
"css": [ "css": [
@@ -105,13 +105,13 @@
] ]
}, },
"src/pages/FeedbacksPage.vue": { "src/pages/FeedbacksPage.vue": {
"file": "assets/FeedbacksPage-ZRHvop0J.js", "file": "assets/FeedbacksPage-B-5AB1AS.js",
"name": "FeedbacksPage", "name": "FeedbacksPage",
"src": "src/pages/FeedbacksPage.vue", "src": "src/pages/FeedbacksPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"index.html", "index.html",
"_MetricGrid-Db4tVG3V.js", "_MetricGrid-Cm-6uQPC.js",
"_vendor-BczUEOE_.js" "_vendor-BczUEOE_.js"
], ],
"css": [ "css": [
@@ -119,13 +119,13 @@
] ]
}, },
"src/pages/LogsPage.vue": { "src/pages/LogsPage.vue": {
"file": "assets/LogsPage-_MZy14g6.js", "file": "assets/LogsPage-DF5QqYRJ.js",
"name": "LogsPage", "name": "LogsPage",
"src": "src/pages/LogsPage.vue", "src": "src/pages/LogsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_users-C4yl3Txu.js", "_users-LqVgYOIa.js",
"_tasks-h6MdOVMy.js", "_tasks-DW-oE78v.js",
"index.html", "index.html",
"_vendor-BczUEOE_.js" "_vendor-BczUEOE_.js"
], ],
@@ -134,30 +134,30 @@
] ]
}, },
"src/pages/ReportPage.vue": { "src/pages/ReportPage.vue": {
"file": "assets/ReportPage-CBBSz-4x.js", "file": "assets/ReportPage-CJumwOUP.js",
"name": "ReportPage", "name": "ReportPage",
"src": "src/pages/ReportPage.vue", "src": "src/pages/ReportPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_vendor-BczUEOE_.js", "_vendor-BczUEOE_.js",
"index.html", "index.html",
"_email-DUrghb1a.js", "_email-UCdCFkiw.js",
"_tasks-h6MdOVMy.js", "_tasks-DW-oE78v.js",
"_system-CKJgzNxM.js", "_system-BqUnjCUC.js",
"_MetricGrid-Db4tVG3V.js" "_MetricGrid-Cm-6uQPC.js"
], ],
"css": [ "css": [
"assets/ReportPage-DlF2eaTa.css" "assets/ReportPage-Csv-Q6wC.css"
] ]
}, },
"src/pages/SecurityPage.vue": { "src/pages/SecurityPage.vue": {
"file": "assets/SecurityPage-BYXzNSAi.js", "file": "assets/SecurityPage-rmlzXSVY.js",
"name": "SecurityPage", "name": "SecurityPage",
"src": "src/pages/SecurityPage.vue", "src": "src/pages/SecurityPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"index.html", "index.html",
"_MetricGrid-Db4tVG3V.js", "_MetricGrid-Cm-6uQPC.js",
"_vendor-BczUEOE_.js" "_vendor-BczUEOE_.js"
], ],
"css": [ "css": [
@@ -165,7 +165,7 @@
] ]
}, },
"src/pages/SettingsPage.vue": { "src/pages/SettingsPage.vue": {
"file": "assets/SettingsPage-DQr6tcde.js", "file": "assets/SettingsPage-Czik5vp6.js",
"name": "SettingsPage", "name": "SettingsPage",
"src": "src/pages/SettingsPage.vue", "src": "src/pages/SettingsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -178,12 +178,12 @@
] ]
}, },
"src/pages/SystemPage.vue": { "src/pages/SystemPage.vue": {
"file": "assets/SystemPage-DSDI53G_.js", "file": "assets/SystemPage-CYpwmM8C.js",
"name": "SystemPage", "name": "SystemPage",
"src": "src/pages/SystemPage.vue", "src": "src/pages/SystemPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_system-CKJgzNxM.js", "_system-BqUnjCUC.js",
"index.html", "index.html",
"_vendor-BczUEOE_.js" "_vendor-BczUEOE_.js"
], ],
@@ -192,12 +192,12 @@
] ]
}, },
"src/pages/UsersPage.vue": { "src/pages/UsersPage.vue": {
"file": "assets/UsersPage-cY1psjBR.js", "file": "assets/UsersPage-hjzizufJ.js",
"name": "UsersPage", "name": "UsersPage",
"src": "src/pages/UsersPage.vue", "src": "src/pages/UsersPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_users-C4yl3Txu.js", "_users-LqVgYOIa.js",
"index.html", "index.html",
"_vendor-BczUEOE_.js" "_vendor-BczUEOE_.js"
], ],

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{_}from"./index-UGawl5hI.js";import{r as n,l as s,o as t,F as r,m as u,V as p,k as o,i as l,j as y,w as h,c as i,A as k,B as c,C as v,a0 as f}from"./vendor-BczUEOE_.js";const b={class:"metric-top"},x={key:0,class:"metric-icon"},B={class:"metric-label"},C={class:"metric-value"},g={key:0,class:"metric-hint app-muted"},N={__name:"MetricGrid",props:{items:{type:Array,default:()=>[]},loading:{type:Boolean,default:!1},minWidth:{type:Number,default:180}},setup(a){return(V,w)=>{const d=n("el-icon"),m=n("el-skeleton");return t(),s("div",{class:"metric-grid",style:f({"--metric-min":`${a.minWidth}px`})},[(t(!0),s(r,null,u(a.items,e=>(t(),s("div",{key:e?.key||e?.label,class:p(["metric-card",`metric-tone--${e?.tone||"blue"}`])},[o("div",b,[e?.icon?(t(),s("div",x,[y(d,null,{default:h(()=>[(t(),i(k(e.icon)))]),_:2},1024)])):l("",!0),o("div",B,c(e?.label||"-"),1)]),o("div",C,[a.loading?(t(),i(m,{key:0,rows:1,animated:""})):(t(),s(r,{key:1},[v(c(e?.value??0),1)],64))]),e?.hint||e?.sub?(t(),s("div",g,c(e?.hint||e?.sub),1)):l("",!0)],2))),128))],4)}}},A=_(N,[["__scopeId","data-v-00e217d4"]]);export{A as M}; import{_}from"./index-C51y9xCM.js";import{r as n,l as s,o as t,F as r,m as u,V as p,k as o,i as l,j as y,w as h,c as i,A as k,B as c,C as v,a0 as f}from"./vendor-BczUEOE_.js";const b={class:"metric-top"},x={key:0,class:"metric-icon"},B={class:"metric-label"},C={class:"metric-value"},g={key:0,class:"metric-hint app-muted"},N={__name:"MetricGrid",props:{items:{type:Array,default:()=>[]},loading:{type:Boolean,default:!1},minWidth:{type:Number,default:180}},setup(a){return(V,w)=>{const d=n("el-icon"),m=n("el-skeleton");return t(),s("div",{class:"metric-grid",style:f({"--metric-min":`${a.minWidth}px`})},[(t(!0),s(r,null,u(a.items,e=>(t(),s("div",{key:e?.key||e?.label,class:p(["metric-card",`metric-tone--${e?.tone||"blue"}`])},[o("div",b,[e?.icon?(t(),s("div",x,[y(d,null,{default:h(()=>[(t(),i(k(e.icon)))]),_:2},1024)])):l("",!0),o("div",B,c(e?.label||"-"),1)]),o("div",C,[a.loading?(t(),i(m,{key:0,rows:1,animated:""})):(t(),s(r,{key:1},[v(c(e?.value??0),1)],64))]),e?.hint||e?.sub?(t(),s("div",g,c(e?.hint||e?.sub),1)):l("",!0)],2))),128))],4)}}},A=_(N,[["__scopeId","data-v-00e217d4"]]);export{A as M};

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{a as m,_ as h}from"./index-UGawl5hI.js";import{e as p,r as u,l as T,o as C,k as r,j as a,w as o,C as x,b as i,E as b}from"./vendor-BczUEOE_.js";async function P(l){const{data:s}=await m.put("/admin/username",{new_username:l});return s}async function E(l){const{data:s}=await m.put("/admin/password",{new_password:l});return s}async function S(){const{data:l}=await m.post("/logout");return l}const U={class:"page-stack"},A={__name:"SettingsPage",setup(l){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 S()}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 P(t),i.success("用户名修改成功,请重新登录"),s.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function B(){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 E(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 C(),T("div",U,[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:o(()=>[e[3]||(e[3]=r("h3",{class:"section-title"},"修改管理员用户名",-1)),a(v,{"label-width":"120px"},{default:o(()=>[a(w,{label:"新用户名"},{default:o(()=>[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:o(()=>[...e[2]||(e[2]=[x("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),a(_,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:o(()=>[e[5]||(e[5]=r("h3",{class:"section-title"},"修改管理员密码",-1)),a(v,{"label-width":"120px"},{default:o(()=>[a(w,{label:"新密码"},{default:o(()=>[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:B},{default:o(()=>[...e[4]||(e[4]=[x("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码至少8位且包含字母与数字。",-1))]),_:1})])}}},j=h(A,[["__scopeId","data-v-83d3840a"]]);export{j as default}; import{a as m,_ as h}from"./index-C51y9xCM.js";import{e as p,r as u,l as T,o as C,k as r,j as a,w as o,C as x,b as i,E as b}from"./vendor-BczUEOE_.js";async function P(l){const{data:s}=await m.put("/admin/username",{new_username:l});return s}async function E(l){const{data:s}=await m.put("/admin/password",{new_password:l});return s}async function S(){const{data:l}=await m.post("/logout");return l}const U={class:"page-stack"},A={__name:"SettingsPage",setup(l){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 S()}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 P(t),i.success("用户名修改成功,请重新登录"),s.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function B(){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 E(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 C(),T("div",U,[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:o(()=>[e[3]||(e[3]=r("h3",{class:"section-title"},"修改管理员用户名",-1)),a(v,{"label-width":"120px"},{default:o(()=>[a(w,{label:"新用户名"},{default:o(()=>[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:o(()=>[...e[2]||(e[2]=[x("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),a(_,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:o(()=>[e[5]||(e[5]=r("h3",{class:"section-title"},"修改管理员密码",-1)),a(v,{"label-width":"120px"},{default:o(()=>[a(w,{label:"新密码"},{default:o(()=>[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:B},{default:o(()=>[...e[4]||(e[4]=[x("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码至少8位且包含字母与数字。",-1))]),_:1})])}}},j=h(A,[["__scopeId","data-v-83d3840a"]]);export{j as default};

View File

@@ -1,4 +1,4 @@
import{f as ge,u as Y}from"./system-CKJgzNxM.js";import{a as P,_ as Ve}from"./index-UGawl5hI.js";import{e as n,f as ke,a1 as xe,h as be,g as we,r as v,W as Ue,X as Ce,o as g,l as V,k as s,j as l,w as t,C as k,i as Z,B as ee,E as ne,b as c}from"./vendor-BczUEOE_.js";async function le(r={}){const{data:m}=await P.get("/kdocs/status",{params:r});return m}async function Pe(r={}){const m={force:!0,...r},{data:x}=await P.post("/kdocs/qr",m);return x}async function Ie(){const{data:r}=await P.post("/kdocs/clear-login",{});return r}async function Ae(){const{data:r}=await P.get("/proxy/config");return r}async function Ne(r){const{data:m}=await P.post("/proxy/config",r);return m}async function Se(r){const{data:m}=await P.post("/proxy/test",r);return m}const De={class:"page-stack"},Ke={class:"config-grid"},Ee={class:"row-actions"},Be={class:"row-actions"},Te={class:"row-actions"},Le={class:"section-head"},he={class:"status-inline app-muted"},Qe={key:0},$e={key:1},qe={key:2},Me={class:"kdocs-inline"},Re={class:"kdocs-range"},Fe={class:"row-actions"},He={key:0,class:"help"},ze={key:1,class:"help"},Oe={class:"kdocs-qr"},je=["src"],Ge={__name:"SystemPage",setup(r){const m=n(!1),x=n(2),A=n(1),N=n(3),I=n(!1),f=n(""),S=n(3),D=n(!1),K=n(10),E=n(7),B=n(!1),T=n(""),L=n(""),h=n(""),Q=n(0),$=n("A"),q=n("D"),M=n(0),R=n(0),F=n(!1),H=n(""),p=n({}),b=n(!1),w=n(""),ae=n(!1),z=n(!1),U=n(!1),C=n(!1),O=n("");let j=null;const oe=ke(()=>z.value||U.value||C.value);function d(a){if(!a){O.value="";return}const e=new Date().toLocaleTimeString("zh-CN",{hour12:!1});O.value=`${a} (${e})`}async function ue(){m.value=!0;try{const[a,e,i]=await Promise.all([ge(),Ae(),le().catch(()=>({}))]);x.value=a.max_concurrent_global??2,A.value=a.max_concurrent_per_account??1,N.value=a.max_screenshot_concurrent??3,D.value=(a.auto_approve_enabled??0)===1,K.value=a.auto_approve_hourly_limit??10,E.value=a.auto_approve_vip_days??7,I.value=(e.proxy_enabled??0)===1,f.value=e.proxy_api_url||"",S.value=e.proxy_expire_minutes??3,B.value=(a.kdocs_enabled??0)===1,T.value=a.kdocs_doc_url||"",L.value=a.kdocs_default_unit||"",h.value=a.kdocs_sheet_name||"",Q.value=a.kdocs_sheet_index??0,$.value=(a.kdocs_unit_column||"A").toUpperCase(),q.value=(a.kdocs_image_column||"D").toUpperCase(),M.value=a.kdocs_row_start??0,R.value=a.kdocs_row_end??0,F.value=(a.kdocs_admin_notify_enabled??0)===1,H.value=a.kdocs_admin_notify_email||"",p.value=i||{}}catch{}finally{m.value=!1}}async function de(){const a={max_concurrent_global:Number(x.value),max_concurrent_per_account:Number(A.value),max_screenshot_concurrent:Number(N.value)};try{await ne.confirm(`确定更新并发配置吗? import{f as ge,u as Y}from"./system-BqUnjCUC.js";import{a as P,_ as Ve}from"./index-C51y9xCM.js";import{e as n,f as ke,a1 as xe,h as be,g as we,r as v,W as Ue,X as Ce,o as g,l as V,k as s,j as l,w as t,C as k,i as Z,B as ee,E as ne,b as c}from"./vendor-BczUEOE_.js";async function le(r={}){const{data:m}=await P.get("/kdocs/status",{params:r});return m}async function Pe(r={}){const m={force:!0,...r},{data:x}=await P.post("/kdocs/qr",m);return x}async function Ie(){const{data:r}=await P.post("/kdocs/clear-login",{});return r}async function Ae(){const{data:r}=await P.get("/proxy/config");return r}async function Ne(r){const{data:m}=await P.post("/proxy/config",r);return m}async function Se(r){const{data:m}=await P.post("/proxy/test",r);return m}const De={class:"page-stack"},Ke={class:"config-grid"},Ee={class:"row-actions"},Be={class:"row-actions"},Te={class:"row-actions"},Le={class:"section-head"},he={class:"status-inline app-muted"},Qe={key:0},$e={key:1},qe={key:2},Me={class:"kdocs-inline"},Re={class:"kdocs-range"},Fe={class:"row-actions"},He={key:0,class:"help"},ze={key:1,class:"help"},Oe={class:"kdocs-qr"},je=["src"],Ge={__name:"SystemPage",setup(r){const m=n(!1),x=n(2),A=n(1),N=n(3),I=n(!1),f=n(""),S=n(3),D=n(!1),K=n(10),E=n(7),B=n(!1),T=n(""),L=n(""),h=n(""),Q=n(0),$=n("A"),q=n("D"),M=n(0),R=n(0),F=n(!1),H=n(""),p=n({}),b=n(!1),w=n(""),ae=n(!1),z=n(!1),U=n(!1),C=n(!1),O=n("");let j=null;const oe=ke(()=>z.value||U.value||C.value);function d(a){if(!a){O.value="";return}const e=new Date().toLocaleTimeString("zh-CN",{hour12:!1});O.value=`${a} (${e})`}async function ue(){m.value=!0;try{const[a,e,i]=await Promise.all([ge(),Ae(),le().catch(()=>({}))]);x.value=a.max_concurrent_global??2,A.value=a.max_concurrent_per_account??1,N.value=a.max_screenshot_concurrent??3,D.value=(a.auto_approve_enabled??0)===1,K.value=a.auto_approve_hourly_limit??10,E.value=a.auto_approve_vip_days??7,I.value=(e.proxy_enabled??0)===1,f.value=e.proxy_api_url||"",S.value=e.proxy_expire_minutes??3,B.value=(a.kdocs_enabled??0)===1,T.value=a.kdocs_doc_url||"",L.value=a.kdocs_default_unit||"",h.value=a.kdocs_sheet_name||"",Q.value=a.kdocs_sheet_index??0,$.value=(a.kdocs_unit_column||"A").toUpperCase(),q.value=(a.kdocs_image_column||"D").toUpperCase(),M.value=a.kdocs_row_start??0,R.value=a.kdocs_row_end??0,F.value=(a.kdocs_admin_notify_enabled??0)===1,H.value=a.kdocs_admin_notify_email||"",p.value=i||{}}catch{}finally{m.value=!1}}async function de(){const a={max_concurrent_global:Number(x.value),max_concurrent_per_account:Number(A.value),max_screenshot_concurrent:Number(N.value)};try{await ne.confirm(`确定更新并发配置吗?
全局并发数: ${a.max_concurrent_global} 全局并发数: ${a.max_concurrent_global}
单账号并发数: ${a.max_concurrent_per_account} 单账号并发数: ${a.max_concurrent_per_account}

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{a as n}from"./index-UGawl5hI.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{a as n}from"./index-C51y9xCM.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{a}from"./index-UGawl5hI.js";async function o(){const{data:t}=await a.get("/system/config");return t}async function e(t){const{data:n}=await a.post("/system/config",t);return n}export{o as f,e as u}; import{a}from"./index-C51y9xCM.js";async function o(){const{data:t}=await a.get("/system/config");return t}async function e(t){const{data:n}=await a.post("/system/config",t);return n}export{o as f,e as u};

View File

@@ -0,0 +1 @@
import{a}from"./index-C51y9xCM.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 r(){const{data:t}=await a.get("/request_metrics");return t}async function o(){const{data:t}=await a.get("/slow_sql_metrics");return t}async function i(){const{data:t}=await a.get("/task/stats");return t}async function u(){const{data:t}=await a.get("/task/running");return t}async function f(t){const{data:s}=await a.get("/task/logs",{params:t});return s}async function g(t){const{data:s}=await a.post("/task/logs/clear",{days:t});return s}export{u as a,c as b,e as c,r as d,o as e,i as f,f as g,g as h};

View File

@@ -1 +0,0 @@
import{a}from"./index-UGawl5hI.js";async function e(){const{data:t}=await a.get("/server/info");return t}async function c(){const{data:t}=await a.get("/docker_stats");return t}async function r(){const{data:t}=await a.get("/request_metrics");return t}async function o(){const{data:t}=await a.get("/task/stats");return t}async function i(){const{data:t}=await a.get("/task/running");return t}async function u(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{i as a,e as b,c,r as d,u as e,o as f,f as g};

View File

@@ -1 +1 @@
import{a as t}from"./index-UGawl5hI.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{a as t}from"./index-C51y9xCM.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-UGawl5hI.js"></script> <script type="module" crossorigin src="./assets/index-C51y9xCM.js"></script>
<link rel="modulepreload" crossorigin href="./assets/vendor-BczUEOE_.js"> <link rel="modulepreload" crossorigin href="./assets/vendor-BczUEOE_.js">
<link rel="stylesheet" crossorigin href="./assets/vendor-C68yOrAy.css"> <link rel="stylesheet" crossorigin href="./assets/vendor-C68yOrAy.css">
<link rel="stylesheet" crossorigin href="./assets/index-a3a11Ghn.css"> <link rel="stylesheet" crossorigin href="./assets/index-a3a11Ghn.css">