// Package handler — 阅读(legado 书源兼容)子系统路由。 package handler import ( "io" "net/http" "os" "strconv" "strings" "github.com/gin-gonic/gin" "github.com/truewhile/MeBox/internal/middleware" "github.com/truewhile/MeBox/internal/service" "github.com/truewhile/MeBox/internal/service/reader" ) func registerReaderRoutes(authed *gin.RouterGroup, svc *service.Container) { g := authed.Group("/reader") // 书源管理 g.GET("/sources", readerListSourcesHandler(svc)) g.POST("/sources/import", readerImportSourcesHandler(svc)) g.PATCH("/sources/:id", readerUpdateSourceHandler(svc)) g.DELETE("/sources/:id", readerDeleteSourceHandler(svc)) g.POST("/sources/:id/debug", readerDebugSourceHandler(svc)) // 书源登录与源变量(登录类书源必需) g.GET("/sources/:id/login", readerSourceLoginInfoHandler(svc)) g.POST("/sources/:id/login", readerSourceLoginActionHandler(svc)) g.DELETE("/sources/:id/login", readerSourceLogoutHandler(svc)) g.PUT("/sources/:id/variable", readerSetSourceVariableHandler(svc)) g.PUT("/sources/:id/login-info", readerSetSourceLoginInfoHandler(svc)) // 书源 JS 的宿主浏览器(java.startBrowser / startBrowserAwait): // 前端轮询待办 → 在 iframe 里承载页面 → 用户点 √ 回传 DOM。 g.GET("/browser/pending", readerBrowserPendingHandler(svc)) g.POST("/browser/result", readerBrowserResultHandler(svc)) // 页面内的 fetch/XHR 经此转发(iframe 是不透明源,请求带不上书源 Cookie) g.POST("/browser/xhr", readerBrowserXHRHandler(svc)) // 搜索(多源聚合) g.POST("/search", readerSearchHandler(svc)) // 详情 / 目录 / 正文 g.GET("/book-info", readerBookInfoHandler(svc)) g.GET("/toc", readerTocHandler(svc)) g.GET("/content", readerContentHandler(svc)) // 书架 g.GET("/books", readerListBooksHandler(svc)) g.POST("/books", readerAddBookHandler(svc)) // 本地书籍(TXT / EPUB 上传导入) g.POST("/local/books", readerImportLocalBookHandler(svc)) // 服务器已有文件/目录导入:原地引用不复制,仅管理员(会读取允许根目录内的文件) g.POST("/local/books/from-path", middleware.AdminRequired(), readerImportLocalBookFromPathHandler(svc)) g.POST("/local/audiobooks", middleware.AdminRequired(), readerImportLocalAudioDirHandler(svc)) g.DELETE("/books/:id", readerRemoveBookHandler(svc)) g.PUT("/books/:id/progress", readerSaveProgressHandler(svc)) g.PUT("/books/:id/audio-config", readerSaveAudioConfigHandler(svc)) g.GET("/books/:id/chapters", readerListChaptersHandler(svc)) g.POST("/books/:id/chapters", readerReplaceChaptersHandler(svc)) g.GET("/books/:id/content", readerBookContentHandler(svc)) // 替换净化规则 g.GET("/replace-rules", readerListReplaceRulesHandler(svc)) g.POST("/replace-rules", readerCreateReplaceRuleHandler(svc)) g.PATCH("/replace-rules/:id", readerUpdateReplaceRuleHandler(svc)) g.DELETE("/replace-rules/:id", readerDeleteReplaceRuleHandler(svc)) } func readerListSourcesHandler(svc *service.Container) gin.HandlerFunc { return func(c *gin.Context) { sources, err := svc.Reader.ListSources(c.Request.Context()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"sources": sources}) } } func readerImportSourcesHandler(svc *service.Container) gin.HandlerFunc { var body struct { Text string `json:"text" binding:"required"` } return func(c *gin.Context) { if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } imported, err := svc.Reader.ImportSources(c.Request.Context(), body.Text) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"imported": imported}) } } func readerUpdateSourceHandler(svc *service.Container) gin.HandlerFunc { var body struct { Enabled *bool `json:"enabled"` } return func(c *gin.Context) { if err := c.ShouldBindJSON(&body); err != nil || body.Enabled == nil { c.JSON(http.StatusBadRequest, gin.H{"error": "enabled 字段必填"}) return } if err := svc.Reader.UpdateSourceEnabled(c.Request.Context(), c.Param("id"), *body.Enabled); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } } func readerDeleteSourceHandler(svc *service.Container) gin.HandlerFunc { return func(c *gin.Context) { if err := svc.Reader.DeleteSource(c.Request.Context(), c.Param("id")); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } } func readerDebugSourceHandler(svc *service.Container) gin.HandlerFunc { var body struct { Key string `json:"key" binding:"required"` } return func(c *gin.Context) { if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } logs, err := svc.Reader.Debug(c.Request.Context(), c.Param("id"), body.Key) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"logs": logs}) } } func readerSourceLoginInfoHandler(svc *service.Container) gin.HandlerFunc { return func(c *gin.Context) { userID := c.GetString(middleware.CtxUserID) info, err := svc.Reader.GetSourceLogin(c.Request.Context(), userID, c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, info) } } func readerSourceLoginActionHandler(svc *service.Container) gin.HandlerFunc { var body struct { // Action 为 loginUi 里按钮的 action;留空表示执行 login()(确认登录)。 Action string `json:"action"` Fields map[string]string `json:"fields"` } return func(c *gin.Context) { if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } userID := c.GetString(middleware.CtxUserID) res, err := svc.Reader.RunLoginAction(c.Request.Context(), userID, c.Param("id"), body.Action, body.Fields) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, res) } } // readerBrowserPendingHandler 前端轮询:该用户在某书源下待用户完成的页面。 // 书源的 java.startBrowserAwait 会阻塞在服务端,前端据此把页面呈现出来。 func readerBrowserPendingHandler(svc *service.Container) gin.HandlerFunc { return func(c *gin.Context) { userID := c.GetString(middleware.CtxUserID) pages := svc.Reader.PendingBrowserPages(userID, c.Query("source_id")) c.JSON(http.StatusOK, gin.H{"pages": pages}) } } // readerBrowserResultHandler 用户完成页面后回传 DOM(或取消),解除服务端阻塞。 func readerBrowserResultHandler(svc *service.Container) gin.HandlerFunc { var body struct { ID string `json:"id" binding:"required"` Body string `json:"body"` URL string `json:"url"` Cancelled bool `json:"cancelled"` } return func(c *gin.Context) { if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } userID := c.GetString(middleware.CtxUserID) if err := svc.Reader.ResolveBrowser(body.ID, userID, body.Body, body.URL, body.Cancelled); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } } // readerBrowserXHRHandler 转发承载页面内的接口请求。 // // 页面在 iframe 里是不透明源,自己的 XHR 既带不上书源 Cookie 也会被 CORS 拦, // 所以由父窗口(持 JWT)把请求转交到这里,服务端补上书源凭据再发。 func readerBrowserXHRHandler(svc *service.Container) gin.HandlerFunc { var body struct { ID string `json:"id" binding:"required"` URL string `json:"url" binding:"required"` Method string `json:"method"` Headers map[string]string `json:"headers"` Body string `json:"body"` } return func(c *gin.Context) { if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } res, err := svc.Reader.ProxyBrowserXHR( c.Request.Context(), body.ID, body.Method, body.URL, body.Headers, body.Body) if err != nil { // 交给页面自己处理失败,别把 4xx 泄成框架错误 c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, res) } } // readerBrowserPageHandler 承载待办页面本体。// // 鉴权走 HMAC 签名而非 JWT:这个地址要填进