前言

基于 Web 的小说转换工具,支持将 txt 文件转换为 epub、mobi、azw3 等电子书格式

其实这个项目是今年八月做的,一直没有分享。

之前在找txt电子书转换EPUB工具的时候,很多都是有广告的,在GitHub上找到一个go开发的命令行工具:kaf-cli ,挺好用的,不过每次都要输入命令有点麻烦,所以我就用go-gin做成了网站,方便时候。

发在了读书群里,也有很多人在使用。

不过最近这个网站已经让我的服务器不堪重负,只能被迫暂停了。

本文简单分享一下这个网站,作为2025年的收尾吧~

这个项目还催生了 Alpine.js 实现国际化的功能。


本项目是从 ystyle/kaf-cli fork 而来,在保留原有命令行功能的基础上,新增了 Web 可视化界面,提供更便捷的使用体验。

项目地址: https://github.com/Deali-Axy/ebook-generator

主要特性

🌐 Web 功能

  • 📁 文件上传:支持 txt 文件上传,最大 50MB
  • 🔄 格式转换:支持转换为 epub、mobi、azw3 格式
  • 📊 实时进度:通过 SSE 实时查看转换进度
  • 📥 文件下载:转换完成后可下载电子书文件
  • 🗑️ 自动清理:支持手动清理临时文件
  • 📖 API 文档:集成 Swagger UI,方便调试
  • 🎨 可视化界面:简洁易用的 HTML 界面

📚 转换功能

  • 自动识别书名和章节
  • 自动识别字符编码(解决中文乱码)
  • 自定义章节标题识别规则
  • 自定义卷的标题识别规则
  • 自动给章节正文生成加粗居中的标题
  • 段落自动识别和缩进
  • 支持生成 Orly 风格的书籍封面
  • 知轩藏书格式文件名自动提取书名和作者
  • 超快速转换(epub 格式生成 300 章/s 以上速度)

截图

主页面

转换进度和下载

web代码

简单贴一些代码吧,其实我还做了登录注册功能的,不过有bug,前端就没搭配加上去了~

// main 启动Web服务
func main() {
	// 设置Gin模式
	if os.Getenv("GIN_MODE") == "" {
		gin.SetMode(gin.DebugMode)
	}

	// 初始化服务管理器
	serviceManager, err := initServiceManager()
	if err != nil {
		log.Fatal("初始化服务管理器失败:", err)
	}

	// 启动所有服务
	if err := serviceManager.Start(); err != nil {
		log.Fatal("启动服务失败:", err)
	}

	// 设置优雅关闭
	defer func() {
		if err := serviceManager.Stop(); err != nil {
			log.Printf("停止服务时出错: %v", err)
		}
	}()

	// 初始化Web服务相关组件
	initWebServices(serviceManager)

	// 创建Gin引擎
	r := gin.Default()

	// 初始化SEO服务
	seoService := initSEOService()

	// 添加中间件
	r.Use(middleware.CORS())
	r.Use(middleware.Logger())
	r.Use(middleware.Recovery())
	r.Use(middleware.AddSecurityHeaders())
	r.Use(middleware.AddCacheHeaders())
	r.Use(middleware.SEOMiddleware(seoService))

	// 设置文件上传大小限制 (50MB)
	r.MaxMultipartMemory = 50 << 20

	// API路由组
	api := r.Group("/api")
	{
		// 基础转换功能
		api.POST("/upload", handlers.UploadFile)
		api.POST("/convert", handlers.ConvertBook)
		api.GET("/status/:taskId", handlers.GetTaskStatus)
		api.GET("/download/:fileId", handlers.DownloadFile)
		api.DELETE("/cleanup/:taskId", handlers.CleanupTask)
		api.GET("/events/:taskId", handlers.GetTaskEvents)

		// 用户认证相关路由
		auth := api.Group("/auth")
		{
			auth.POST("/register", handlers.Register)
			auth.POST("/login", handlers.Login)
			auth.GET("/profile", handlers.AuthMiddleware(), handlers.GetProfile)
			auth.PUT("/profile", handlers.AuthMiddleware(), handlers.UpdateProfile)
			auth.POST("/logout", handlers.AuthMiddleware(), handlers.Logout)
			auth.POST("/refresh", handlers.AuthMiddleware(), handlers.RefreshToken)
			auth.PUT("/password", handlers.AuthMiddleware(), handlers.ChangePassword)
		}

		// 转换历史相关路由(需要认证)
		history := api.Group("/history")
		history.Use(handlers.AuthMiddleware())
		{
			history.GET("", handlers.GetHistories)
			history.GET("/stats", handlers.GetHistoryStats)
			history.DELETE("/:id", handlers.DeleteHistory)
		}

		// 转换预设相关路由(需要认证)
		presets := api.Group("/presets")
		presets.Use(handlers.AuthMiddleware())
		{
			presets.POST("", handlers.CreatePreset)
			presets.GET("", handlers.GetPresets)
			presets.GET("/:id", handlers.GetPreset)
			presets.PUT("/:id", handlers.UpdatePreset)
			presets.DELETE("/:id", handlers.DeletePreset)
		}

		// 批量转换相关路由(需要认证)
		batch := api.Group("/batch")
		batch.Use(handlers.AuthMiddleware())
		{
			batch.POST("/convert", handlers.BatchConvert)
		}
	}

	// 集成Swagger文档
	r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

	// 静态文件服务
	r.Static("/static", "./web/static")
	r.StaticFile("/", "./web/static/index.html")
	r.StaticFile("/demo", "./web/static/index.html")

	// SEO相关文件 - 动态生成
	r.GET("/sitemap.xml", handlers.GenerateSitemap)
	r.GET("/robots.txt", handlers.GenerateRobotsTxt)

	// SEO状态监控
	seo := r.Group("/seo")
	{
		seo.GET("/status", handlers.GetSEOStatus)
	}

	// 健康检查接口
	r.GET("/health", func(c *gin.Context) {
		c.JSON(200, gin.H{"status": "ok"})
	})

	// 启动服务
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	log.Printf("服务启动在端口: %s", port)
	log.Printf("Swagger文档地址: http://localhost:%s/swagger/index.html", port)

	if err := r.Run(":" + port); err != nil {
		log.Fatal("启动服务失败:", err)
	}
}

小结

就这样吧,时间不早了,祝大家新年快乐吧~