From 05bf1b566273d5f8c71447b8a63cdb770e3d6971 Mon Sep 17 00:00:00 2001 From: Odboy Date: Mon, 1 Sep 2025 21:11:28 +0800 Subject: [PATCH] upd --- frontend/src/App.vue | 4 ++ frontend/src/router/index.js | 4 +- frontend/src/views/Settings.vue | 114 ++++++++++++++++++++++++++++++ internal/api/config_controller.go | 106 +++++++++++++++++++++++++++ internal/api/router.go | 5 ++ internal/service/scan.go | 28 ++++++-- 6 files changed, 253 insertions(+), 8 deletions(-) create mode 100644 frontend/src/views/Settings.vue create mode 100644 internal/api/config_controller.go diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 1a0e19d..b5b9715 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -24,6 +24,10 @@ 统计看板 + + + 系统配置 + diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index bdff5c0..931450b 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -3,6 +3,7 @@ import Router from 'vue-router' import Dashboard from '../views/Dashboard.vue' import MediaList from '../views/MediaList.vue' import Scan from '../views/Scan.vue' +import Settings from '../views/Settings.vue' Vue.use(Router) @@ -11,6 +12,7 @@ export default new Router({ {path: '/', redirect: '/scan'}, {path: '/dashboard', component: Dashboard}, {path: '/media', component: MediaList}, - {path: '/scan', component: Scan} + {path: '/scan', component: Scan}, + {path: '/settings', component: Settings} ] }) diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue new file mode 100644 index 0000000..a9f92b7 --- /dev/null +++ b/frontend/src/views/Settings.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/internal/api/config_controller.go b/internal/api/config_controller.go new file mode 100644 index 0000000..45ec747 --- /dev/null +++ b/internal/api/config_controller.go @@ -0,0 +1,106 @@ +// Package api 配置相关接口。 +package api + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + + "kenaito-media-picker/internal/store" +) + +// 配置键名(存储在 AppConfig 表中) +const ( + ConfigKeyNsfwType = "nsfw.type" // 检测类型:"" / "human" / "anime" + ConfigKeyNsfwThreshold = "nsfw.threshold" // 人类检测阈值 + ConfigKeyAnimeNsfwThreshold = "anime.threshold" // 二次元检测阈值 +) + +// ConfigController 配置相关接口。 +type ConfigController struct { + store *store.Store +} + +// NewConfigController 创建配置控制器。 +func NewConfigController(st *store.Store) *ConfigController { + return &ConfigController{store: st} +} + +// NsfwConfig NSFW 配置响应。 +type NsfwConfig struct { + Type string `json:"type"` // 检测类型:"" / "human" / "anime" + HumanThreshold float64 `json:"humanThreshold"` // 人类检测阈值 + AnimeThreshold float64 `json:"animeThreshold"` // 二次元检测阈值 +} + +// GetNsfwConfig 处理 GET /api/config/nsfw:获取 NSFW 配置。 +func (c *ConfigController) GetNsfwConfig(ctx *gin.Context) { + nsfwType := c.store.GetConfig(ConfigKeyNsfwType) + + humanThreshold := 0.5 + if v := c.store.GetConfig(ConfigKeyNsfwThreshold); v != "" { + if t, err := strconv.ParseFloat(v, 64); err == nil && t > 0 && t < 1 { + humanThreshold = t + } + } + + animeThreshold := 0.5 + if v := c.store.GetConfig(ConfigKeyAnimeNsfwThreshold); v != "" { + if t, err := strconv.ParseFloat(v, 64); err == nil && t > 0 && t < 1 { + animeThreshold = t + } + } + + Success(ctx, NsfwConfig{ + Type: nsfwType, + HumanThreshold: humanThreshold, + AnimeThreshold: animeThreshold, + }) +} + +// UpdateNsfwConfigReq 更新 NSFW 配置请求。 +type UpdateNsfwConfigReq struct { + Type string `json:"type"` // 检测类型:"" / "human" / "anime" + HumanThreshold float64 `json:"humanThreshold"` // 人类检测阈值 + AnimeThreshold float64 `json:"animeThreshold"` // 二次元检测阈值 +} + +// UpdateNsfwConfig 处理 PUT /api/config/nsfw:更新 NSFW 配置。 +func (c *ConfigController) UpdateNsfwConfig(ctx *gin.Context) { + var req UpdateNsfwConfigReq + if err := ctx.ShouldBindJSON(&req); err != nil { + Error(ctx, http.StatusBadRequest, "请求参数错误:"+err.Error()) + return + } + + // 校验检测类型 + if req.Type != "" && req.Type != "human" && req.Type != "anime" { + Error(ctx, http.StatusBadRequest, "检测类型必须是空、human 或 anime") + return + } + + // 校验阈值范围 + if req.HumanThreshold <= 0 || req.HumanThreshold >= 1 { + req.HumanThreshold = 0.5 + } + if req.AnimeThreshold <= 0 || req.AnimeThreshold >= 1 { + req.AnimeThreshold = 0.5 + } + + // 保存配置 + if err := c.store.SaveConfig(ConfigKeyNsfwType, req.Type); err != nil { + Error(ctx, http.StatusInternalServerError, "保存配置失败:"+err.Error()) + return + } + if err := c.store.SaveConfig(ConfigKeyNsfwThreshold, strconv.FormatFloat(req.HumanThreshold, 'f', 2, 64)); err != nil { + Error(ctx, http.StatusInternalServerError, "保存配置失败:"+err.Error()) + return + } + if err := c.store.SaveConfig(ConfigKeyAnimeNsfwThreshold, strconv.FormatFloat(req.AnimeThreshold, 'f', 2, 64)); err != nil { + Error(ctx, http.StatusInternalServerError, "保存配置失败:"+err.Error()) + return + } + + Success(ctx, nil) +} diff --git a/internal/api/router.go b/internal/api/router.go index 2d602fa..e417090 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -47,6 +47,11 @@ func NewRouter(st *store.Store) *gin.Engine { // 文件目录浏览(页面上"选择目录") fc := NewFsController() apiGroup.GET("/fs/list", fc.List) + + // 系统配置 + cc := NewConfigController(st) + apiGroup.GET("/config/nsfw", cc.GetNsfwConfig) + apiGroup.PUT("/config/nsfw", cc.UpdateNsfwConfig) } return r } diff --git a/internal/service/scan.go b/internal/service/scan.go index 36526c3..4e9136a 100644 --- a/internal/service/scan.go +++ b/internal/service/scan.go @@ -19,6 +19,7 @@ import ( "path/filepath" "regexp" "runtime/debug" + "strconv" "strings" "sync" "time" @@ -671,12 +672,20 @@ func (s *ScanService) runScan(ctx context.Context, rootDir, destDir string) erro } // ---------- 第五步(可选):色情识别 ---------- - // 用本机 python + NudeNet 起一个本地检测服务。没装 python/nudenet 时跳过, - // 不影响图片整理——检测结果只决定走普通目录还是「图片_敏感」目录。 + // 从数据库读取 NSFW 配置(支持运行时修改,立即生效) + nsfwType := s.store.GetConfig("nsfw.type") // "" / "human" / "anime" + pythonPath := config.GetAppConfig().NSFWPython + nsfwDet := (*nsfw.Detector)(nil) s.setNsfwDetectorReady(false) - if cfg := config.GetAppConfig(); cfg.NSFWEnabled { - if d, err := nsfw.Start(cfg.NSFWPython, cfg.NSFWThreshold); err != nil { + if nsfwType == "human" { + threshold := 0.5 + if v := s.store.GetConfig("nsfw.threshold"); v != "" { + if t, err := strconv.ParseFloat(v, 64); err == nil && t > 0 && t < 1 { + threshold = t + } + } + if d, err := nsfw.Start(pythonPath, threshold); err != nil { fmt.Printf("=== 色情识别未启用(图片照常处理): %v\n", err) } else { nsfwDet = d @@ -687,10 +696,15 @@ func (s *ScanService) runScan(ctx context.Context, rootDir, destDir string) erro } // ---------- 第六步(可选):动漫色情识别 ---------- - // 用本机 python + WD Tagger v3 起一个本地检测服务。没装 python/onnxruntime 时跳过。 animeNsfwDet := (*anime_nsfw.Detector)(nil) - if cfg := config.GetAppConfig(); cfg.AnimeNSFWEnabled { - if d, err := anime_nsfw.Start(cfg.NSFWPython, cfg.AnimeNSFWThreshold); err != nil { + if nsfwType == "anime" { + threshold := 0.5 + if v := s.store.GetConfig("anime.threshold"); v != "" { + if t, err := strconv.ParseFloat(v, 64); err == nil && t > 0 && t < 1 { + threshold = t + } + } + if d, err := anime_nsfw.Start(pythonPath, threshold); err != nil { fmt.Printf("=== 动漫色情识别未启用(图片照常处理): %v\n", err) } else { animeNsfwDet = d