├── .gitignore ├── CHANGELOG.md ├── CLA.md ├── LICENSE ├── Logger.go ├── README.md ├── README.zh-CN.md ├── componentBaiLian.go ├── componentGitee.go ├── componentLKE.go ├── componentMCP.go ├── componentOpenAI.go ├── componentOpenAI_test.go ├── const.go ├── funcMap.go ├── funcmap_test.go ├── go.mod ├── go.sum ├── main.go ├── minragEntity.go ├── minragdatadir ├── dict.zip ├── extensions │ ├── libsimple.dll │ ├── libsimple.dylib │ ├── libsimple.so │ ├── libsimple.so-aarch64 │ ├── vec0.dll │ ├── vec0.dylib │ └── vec0.so ├── install_config.json ├── libgcc_s_seh-1.dll ├── locales │ ├── en-US.json │ └── zh-CN.json ├── minrag.sql ├── public │ ├── demo.png │ ├── doc │ │ ├── image │ │ │ ├── 000.png │ │ │ ├── 001.png │ │ │ ├── 002.png │ │ │ ├── 003.png │ │ │ ├── 004.png │ │ │ ├── 005.png │ │ │ ├── 006.png │ │ │ ├── 007.png │ │ │ ├── 008.png │ │ │ ├── 009.png │ │ │ ├── 010.png │ │ │ ├── 011.png │ │ │ ├── 012.png │ │ │ ├── 013.png │ │ │ ├── 014.png │ │ │ ├── 015.png │ │ │ ├── 016.png │ │ │ ├── 017.png │ │ │ ├── 018.png │ │ │ ├── 019.png │ │ │ ├── 020.png │ │ │ ├── 021.png │ │ │ ├── 022.png │ │ │ ├── 023.png │ │ │ └── 024.png │ │ └── index.md │ ├── favicon.png │ ├── gongan.png │ ├── index.png │ ├── logo.png │ ├── minrag-logo-1080.png │ ├── minrag-logo.png │ ├── minrag-logo.svg │ ├── minrag-文字-logo.svg │ └── mqtt3.1.1.html ├── template │ ├── admin │ │ ├── agent │ │ │ ├── list.html │ │ │ ├── save.html │ │ │ └── update.html │ │ ├── bodyend.html │ │ ├── bodystart.html │ │ ├── component │ │ │ ├── list.html │ │ │ ├── save.html │ │ │ └── update.html │ │ ├── config │ │ │ └── update.html │ │ ├── css │ │ │ └── tree.css │ │ ├── document │ │ │ ├── list.html │ │ │ └── update.html │ │ ├── error.html │ │ ├── header.html │ │ ├── index.html │ │ ├── install.html │ │ ├── js │ │ │ ├── cherry-markdown │ │ │ │ ├── cherry-markdown.min.css │ │ │ │ ├── cherry-markdown.min.js │ │ │ │ └── fonts │ │ │ │ │ ├── ch-icon.eot │ │ │ │ │ ├── ch-icon.svg │ │ │ │ │ ├── ch-icon.ttf │ │ │ │ │ ├── ch-icon.woff │ │ │ │ │ └── ch-icon.woff2 │ │ │ ├── layui │ │ │ │ ├── css │ │ │ │ │ ├── layui.css │ │ │ │ │ └── layui.css.map │ │ │ │ ├── font │ │ │ │ │ ├── iconfont.eot │ │ │ │ │ ├── iconfont.svg │ │ │ │ │ ├── iconfont.ttf │ │ │ │ │ ├── iconfont.woff │ │ │ │ │ └── iconfont.woff2 │ │ │ │ ├── layui.js │ │ │ │ ├── layui.js.map │ │ │ │ ├── test.html │ │ │ │ ├── 免责声明.html │ │ │ │ └── 官方文档.html │ │ │ └── sha3.min.js │ │ ├── knowledgeBase │ │ │ ├── save.html │ │ │ └── update.html │ │ ├── login.html │ │ ├── site │ │ │ └── update.html │ │ ├── themeTemplate │ │ │ └── list.html │ │ └── user │ │ │ └── update.html │ └── theme │ │ └── default │ │ ├── agent.html │ │ ├── css │ │ └── agent.css │ │ ├── error.html │ │ ├── index.html │ │ └── js │ │ ├── jquery-3.7.1.min.js │ │ └── marked.min.js └── upload │ └── upload.txt ├── routeAdmin.go ├── routeWeb.go ├── serviceAgent.go ├── serviceConfig.go ├── serviceDocument.go ├── serviceKnowledgeBase.go ├── serviceUser.go ├── utilCaptcha.go ├── utilFunctionCalling.go ├── utilGenStaticFile.go ├── utilHertz.go ├── utilHttp.go ├── utilJWT.go ├── utilLocale.go ├── utilSQLite.go ├── utilSQLite_test.go ├── utilZIP.go └── utilzip_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # idea ignore 2 | .idea/ 3 | *.ipr 4 | *.iml 5 | *.iws 6 | upload/ 7 | install_config.json 8 | search-data.json 9 | install.html 10 | install.html.bak 11 | statichtml 12 | testTable/ 13 | dict/ 14 | natsdata/ 15 | *.db 16 | *.db.fail 17 | .vscode/ 18 | .VSCodeCounter/ 19 | 20 | *.swp 21 | 22 | # temp ignore 23 | *.hertz.gz 24 | *.gz 25 | *.log 26 | *.cache 27 | *.diff 28 | *.exe 29 | *.exe~ 30 | *.patch 31 | *.tmp 32 | *debug.test 33 | debug.test 34 | __debug_bin 35 | 36 | # system ignore 37 | .DS_Store 38 | Thumbs.db 39 | 40 | # project 41 | *.cert 42 | *.key 43 | .test 44 | iprepo.txt 45 | 46 | 47 | _output -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | v0.1.0 2 | - 完善注释,文档 3 | 4 | v0.0.9 5 | - 升级FTS5分词组件 6 | - 增加WebScraper组件,实现网络爬虫 7 | - 增加HtmlCleaner组件,清理html标签 8 | - 完善注释,文档 9 | 10 | v0.0.8 11 | - 修复日志记录的bug,只记录文本内容 12 | - OpenAIChatMemory默认上下文长度为3 13 | - 完善注释,文档 14 | 15 | v0.0.7 16 | - 目录需要是755权限,才能正常读取,上传的文件默认是644 17 | - 完善注释,文档 18 | 19 | v0.0.6 20 | - 全平台兼容DeepSeek R1思维链输出方式 21 | - 完善注释,文档 22 | 23 | v0.0.5 24 | - 增加TikaConverter组件,支持tika文档解析 25 | - 增加文档说明 26 | - 修复删除按钮功能 27 | - 完善注释,文档 28 | 29 | v0.0.4 30 | - 支持DeepSeek R1思维链 31 | - 优化聊天界面 32 | - 项目Logo 33 | - 完善注释,文档 34 | 35 | v0.0.3 36 | - 依赖Go 1.24 37 | - 支持字节火山引擎 38 | - 支持阿里云百炼平台,新增BaiLianDocumentChunkReranker组件 39 | - 完善注释,文档 40 | 41 | v0.0.2 42 | - 增强windows系统的兼容性 43 | - 组件默认初始化DefaultHeaders 44 | - 支持百度千帆平台和腾讯云LKE知识引擎,新增LKEDocumentEmbedder,LKETextEmbedder,LKEDocumentChunkReranker和GiteeDocumentChunkReranker组件 45 | - 完善注释,文档 46 | 47 | v0.0.1 48 | - 实现14个核心组件 49 | - 支持function calling 50 | - 支持完整的Pipeline功能 51 | - 基于gpress代码初始化版本 52 | - 完善注释文档 -------------------------------------------------------------------------------- /CLA.md: -------------------------------------------------------------------------------- 1 | 2 | **minRAG贡献者许可协议** 3 | 4 | 感谢您对minRAG社区(“社区”或者“我们”)拥有及/或管理的开源项目(“项目”)的关注,请完整、仔细、充分阅读本《贡献者许可协议》(“协议”)后,在对协议项下条款均无异议的前提下,签署本协议。 5 | 6 | **提交贡献即认为签署了本协议,若对本协议有异议,请勿提交贡献。** 7 | 8 | 为确保社区及项目合法合规,同时也避免侵犯任何人士的合法权益,我们希望确保: 9 | (1)您对您所提交的贡献具有完整充分的合法权利; 10 | (2)您的提交行为合法且不侵权。 11 | 12 | 您在提交贡献后,仍然对贡献享有应有的合法权利。您不会因为提交贡献而承担任何未约定的义务。 13 | 14 | **一、定义** 15 | 为本协议之目的,除非上下文另有说明,本协议中用语分别具有本条所指含义: 16 | 1.1 “中国”指中华人民共和国。 17 | 1.2 “法律”包括但不限于任何适用的法典、法律、法规、规章、司法解释及规范性文件,包括但不限于《中华人民共和国著作权法》(“著作权法”)、《中华人民共和国计算机软件保护条例》、《中华人民共和国专利法》(“专利法”)。如无特别说明或者其他约定,本协议项下争议优先适用中国法律。 18 | 1.3 “社区”或“我们”指minRAG社区。 19 | 1.4 “项目”指社区拥有及/或管理的项目,包括但不限于软件、软件所包含的程序、代码、文字、图片、图形、文档或者其他信息。于本协议签署之日,项目托管于Gitee [https://gitee.com/minrag](https://gitee.com/minrag),托管地址今后可能随实际情况发生变化,具体访问地址以社区实时公布为准。 20 | 1.5 “贡献者”指签署本协议并向社区提交贡献的任何自然人、法人、其他组织以及法律规定的其他主体。 21 | 1.6 “贡献”指由贡献者根据本协议向社区提交的任何程序、代码、文字、图片、图形、文档或者其他作品。 22 | 1.7 “提交”指将“贡献”通过电子邮件或者其他形式发送给“项目”或者“社区”,其他形式包括但不限于讨论、在“项目”相关的电子邮件列表上的交流、在“项目”相关的源代码修订控制、问题跟踪等系统中的操作,但不包括以书面方式明确标记为“非贡献”的交流。 23 | 1.8 “著作权”指《著作权法》规定的各项权利;在任何不适用中国法律的争议中,应指包括《保护文学艺术作品伯尔尼公约》、《世界知识产权组织版权公约》、《与贸易有关的知识产权协定》等国际公约及争议准据法项下规定在内的全部可适用权利。 24 | 1.9 “专利权”指《专利法》规定的各项权利;在任何不适用中国法律的争议中,应指包括《保护工业产权巴黎公约》、《与贸易有关的知识产权协定》等国际公约及争议准据法项下规定在内的全部可适用权利。 25 | 1.10 “合法权利”包括但不限于著作权、版权、专利权、商标权、隐私权以及法律规定的其他权利。 26 | 1.11 “作品”指《著作权法》规定的在文学、艺术和科学领域内具有独创性并能以一定形式表现的智力成果以及《专利法》规定的产品、方法或者其改进所提出的新的技术方案、对产品的形状、构造或者其结合所提出的适于实用的新的技术方案、对产品的整体或者局部的形状、图案或者其结合以及色彩与形状、图案的结合所作出的富有美感并适于工业应用的新设计。在任何不适用中国法律的争议中,应指与著作权及专利权相对应的全部可适用标的。 27 | 1.12 “原创作品”指贡献者作为作者创作、开发的作品或者发明创造。 28 | 1.13 “职务作品”指贡献者作为作者创作、开发但依法属于《著作权法》规定的职务作品的作品或者《专利法》规定的职务发明创造。 29 | 1.14 “合作作品”指贡献者作为作者之一创作、开发但依法属于《著作权法》规定的合作作品的作品或者《专利法》规定的合作完成的发明创造。 30 | 1.15 “委托作品”指贡献者作为作者创作、开发但依法属于《著作权法》规定的委托作品的作品或者《专利法》规定的委托完成的发明创造。 31 | 1.16 “非原创作品”指贡献者以外的人创作、开发的作品。为免歧义,贡献者作为作者之一参与的合作作品属于原创作品,但作为贡献提交时,贡献者应取得合作作者的合法授权。 32 | 33 | **二、著作权许可授权** 34 | 2.1 贡献一经提交,即视为贡献者免费且不可撤销地授予社区、项目及其全部或者部分的接收者永久、非排他、全球性的著作权许可,修改、复制、发行、传播、改编、注释、整理、汇编、开发、展示、分发、再许可或以其他合法方式使用贡献及使用贡献产生的成果。 35 | 36 | **三、专利权许可授权** 37 | 3.1 贡献一经提交,即视为贡献者免费且不可撤销地授予社区、项目及其全部或者部分的接收者永久、非排他、全球性的专利权许可,制造、委托制造、销售、许诺销售、进口或以其他合法方式使用贡献及使用贡献产生的成果。 38 | 39 | **四、稳定运行承诺** 40 | 4.1 社区承诺绝不会主动关闭项目,具体指社区绝不会主动对外关闭项目的访问权限或者清空项目所包含的全部信息。 41 | 42 | **五、原创及不侵权承诺** 43 | 5.1 除严格按第5.4条要求明确标记为非原创的情形之外,贡献者承诺所提交的贡献完全为原创作品,不侵害他人的合法权利,且无以下任何一种情况: 44 |   5.1.1 属于职务作品,单位未签署本协议及/或未同意贡献者提交; 45 |   5.1.2 属于合作作品,合作作者未签署本协议及/或未同意贡献者提交; 46 |   5.1.3 属于委托作品或者著作权、专利权已经对外转让,著作权、专利权的实际持有人未签署本协议及/或未同意贡献者提交。 47 | 5.2 如存在第5.1条任何一种情形的,贡献者应先取得相应授权后再进行提交: 48 |   5.2.1 如有第5.1.1条的情形,贡献者应事先取得单位的授权; 49 |   5.2.2 如有第5.1.2条的情形,贡献者应事先取得合作作者的授权; 50 |   5.2.3 如有第5.1条的情形,贡献者应事先取得著作权、专利权实际持有人的授权。 51 | 5.3 在遵守本第五条约定的基础上,单位作为贡献者签署本协议的,可以授权指定人士以单位名义、单位员工名义或者个人名义提交贡献。 52 | 5.4 如贡献者提交的贡献为非原创作品,则贡献者应当明确标记出非原创部分,并完整列出原创者及/或著作权、专利权持有者的名称。 53 | 5.5 除其他条款约定外,如贡献者提交的贡献在使用时可能涉及任何第三方权利,则贡献者还应当在提交时予以说明,并披露该等限制的完整详细信息。 54 | 5.6 请留意:社区希望保护贡献者的合法权益,免受任何人士的侵犯;社区也倡导尊重原创、保护知识产权,不允许侵犯他人的合法权益。如贡献者提交贡献时,冒名、盗用或者擅自提交他人享有著作权或其他合法权利的作品或者以其他方式侵犯他人合法权益,导致社区承担责任和损失的,社区保留追究责任的权利。 55 | 56 | **六、贡献者权利保护** 57 | 6.1 贡献者签署本协议不应视为放弃原创者身份或署名权。本协议不影响贡献者将其贡献合法用于其他任何目的的权利,社区或者任何其他人士亦不应凭借本协议要求贡献者放弃其他任何合法权利。 58 | 6.2 贡献者签署本协议不应视为承诺对其提交的贡献承担后续支持、维保、适用性、品质保证等义务。社区或者任何其他人士亦不应凭借本协议要求贡献者承担任何未约定的义务。 -------------------------------------------------------------------------------- /Logger.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 minRAG Authors. 2 | // 3 | // This file is part of minRAG. 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package main 19 | 20 | import ( 21 | "context" 22 | 23 | "github.com/cloudwego/hertz/pkg/common/hlog" 24 | ) 25 | 26 | func init() { 27 | // 设置默认的日志显示信息,显示文件和行号 28 | // Set the default log display information, display file and line number. 29 | // log.SetFlags(log.Llongfile | log.LstdFlags) 30 | // 设置日志级别 31 | hlog.SetLevel(hlog.LevelError) 32 | } 33 | 34 | // LogCallDepth 记录日志调用层级,用于定位到业务层代码 35 | // Log Call Depth Record the log call level, used to locate the business layer code 36 | var LogCallDepth = 4 37 | 38 | // FuncLogError 记录error日志 39 | // FuncLogError Record error log 40 | var FuncLogError func(ctx context.Context, err error) = defaultLogError 41 | 42 | // FuncLogPanic 记录panic日志,默认使用"defaultLogPanic"实现 43 | // FuncLogPanic Record panic log, using "defaultLogPanic" by default 44 | var FuncLogPanic func(ctx context.Context, err error) = defaultLogPanic 45 | 46 | func defaultLogError(ctx context.Context, err error) { 47 | //log.Output(LogCallDepth, fmt.Sprintln(err)) 48 | hlog.Error(err) 49 | } 50 | 51 | func defaultLogPanic(ctx context.Context, err error) { 52 | defaultLogError(ctx, err) 53 | } 54 | -------------------------------------------------------------------------------- /componentBaiLian.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 minRAG Authors. 2 | // 3 | // This file is part of minRAG. 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | // 百度千帆平台,主要适配Reranker模型 19 | package main 20 | 21 | import ( 22 | "context" 23 | "encoding/json" 24 | "errors" 25 | "net/http" 26 | "strings" 27 | "time" 28 | ) 29 | 30 | // https://help.aliyun.com/zh/model-studio/developer-reference/text-rerank-api 31 | // BaiLianDocumentChunkReranker 阿里百炼的重排序模型 32 | type BaiLianDocumentChunkReranker struct { 33 | APIKey string `json:"api_key,omitempty"` 34 | Model string `json:"model,omitempty"` 35 | BaseURL string `json:"base_url,omitempty"` 36 | DefaultHeaders map[string]string `json:"defaultHeaders,omitempty"` 37 | Timeout int `json:"timeout,omitempty"` 38 | // Query 需要查询的关键字 39 | Query string `json:"query,omitempty"` 40 | // TopN 检索多少条 41 | TopN int `json:"top_n,omitempty"` 42 | // Score ranker的score匹配分数 43 | Score float32 `json:"score,omitempty"` 44 | client *http.Client `json:"-"` 45 | } 46 | 47 | func (component *BaiLianDocumentChunkReranker) Initialization(ctx context.Context, input map[string]interface{}) error { 48 | if component.Timeout == 0 { 49 | component.Timeout = 180 50 | } 51 | 52 | component.client = &http.Client{ 53 | Timeout: time.Second * time.Duration(component.Timeout), 54 | } 55 | 56 | if component.APIKey == "" { 57 | component.APIKey = config.AIAPIkey 58 | } 59 | if component.BaseURL == "" { 60 | if config.AIBaseURL == "" { 61 | return nil 62 | } 63 | index := strings.Index(config.AIBaseURL, "/v1") 64 | if index <= 0 { 65 | return nil 66 | } 67 | component.BaseURL = config.AIBaseURL[:index] + "/services/rerank/text-rerank/text-rerank" 68 | } 69 | if component.DefaultHeaders == nil { 70 | component.DefaultHeaders = make(map[string]string, 0) 71 | } 72 | return nil 73 | } 74 | func (component *BaiLianDocumentChunkReranker) Run(ctx context.Context, input map[string]interface{}) error { 75 | topN := 0 76 | var score float32 = 0.0 77 | dcs, has := input["documentChunks"] 78 | if !has || dcs == nil { 79 | err := errors.New(funcT("input['documentChunks'] cannot be empty")) 80 | input[errorKey] = err 81 | return err 82 | } 83 | queryObj, has := input["query"] 84 | if !has { 85 | return errors.New(funcT("input['query'] cannot be empty")) 86 | } 87 | query := queryObj.(string) 88 | if query == "" { 89 | return errors.New(funcT("input['query'] cannot be empty")) 90 | } 91 | 92 | tId, has := input["topN"] 93 | if has { 94 | topN = tId.(int) 95 | } 96 | if topN == 0 { 97 | topN = component.TopN 98 | } 99 | if topN == 0 { 100 | topN = 5 101 | } 102 | disId, has := input["score"] 103 | if has { 104 | score = disId.(float32) 105 | } 106 | if score <= 0 { 107 | score = component.Score 108 | } 109 | 110 | documentChunks := dcs.([]DocumentChunk) 111 | if topN > len(documentChunks) { 112 | topN = len(documentChunks) 113 | } 114 | if len(documentChunks) < 1 { //没有文档,不需要重排 115 | return nil 116 | } 117 | documents := make([]string, 0) 118 | for i := 0; i < len(documentChunks); i++ { 119 | documents = append(documents, documentChunks[i].Markdown) 120 | } 121 | 122 | bodyMap := map[string]interface{}{ 123 | "model": component.Model, 124 | "input": map[string]interface{}{ 125 | "query": query, 126 | "documents": documents, 127 | }, 128 | "parameters": map[string]interface{}{ 129 | "top_n": topN, 130 | "return_documents": true, 131 | }, 132 | } 133 | 134 | rsStringByte, err := httpPostJsonBody(component.client, component.APIKey, component.BaseURL, component.DefaultHeaders, bodyMap) 135 | if err != nil { 136 | input[errorKey] = err 137 | return err 138 | } 139 | 140 | rs := struct { 141 | Output struct { 142 | Results []struct { 143 | Document struct { 144 | Text string `json:"text,omitempty"` 145 | } `json:"document,omitempty"` 146 | RelevanceScore float32 `json:"relevance_score,omitempty"` 147 | } `json:"results,omitempty"` 148 | } `json:"output,omitempty"` 149 | }{} 150 | 151 | err = json.Unmarshal(rsStringByte, &rs) 152 | if err != nil { 153 | input[errorKey] = err 154 | return err 155 | } 156 | rerankerDCS := make([]DocumentChunk, 0) 157 | for i := 0; i < len(rs.Output.Results); i++ { 158 | markdown := rs.Output.Results[i].Document.Text 159 | for j := 0; j < len(documentChunks); j++ { 160 | dc := documentChunks[j] 161 | if markdown == dc.Markdown { //相等 162 | dc.Score = rs.Output.Results[i].RelevanceScore 163 | rerankerDCS = append(rerankerDCS, dc) 164 | break 165 | } 166 | } 167 | } 168 | rerankerDCS = sortDocumentChunksScore(rerankerDCS, topN, score) 169 | input["documentChunks"] = rerankerDCS 170 | return nil 171 | } 172 | -------------------------------------------------------------------------------- /componentGitee.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 minRAG Authors. 2 | // 3 | // This file is part of minRAG. 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | // 百度千帆平台,主要适配Reranker模型 19 | package main 20 | 21 | import ( 22 | "context" 23 | "encoding/json" 24 | "errors" 25 | "net/http" 26 | "strings" 27 | "time" 28 | ) 29 | 30 | // GiteeDocumentChunkReranker 百度千帆对DocumentChunks进行重新排序 31 | type GiteeDocumentChunkReranker struct { 32 | APIKey string `json:"api_key,omitempty"` 33 | Model string `json:"model,omitempty"` 34 | BaseURL string `json:"base_url,omitempty"` 35 | DefaultHeaders map[string]string `json:"defaultHeaders,omitempty"` 36 | Timeout int `json:"timeout,omitempty"` 37 | // Query 需要查询的关键字 38 | Query string `json:"query,omitempty"` 39 | // TopN 检索多少条 40 | TopN int `json:"top_n,omitempty"` 41 | // Score ranker的score匹配分数 42 | Score float32 `json:"score,omitempty"` 43 | client *http.Client `json:"-"` 44 | } 45 | 46 | func (component *GiteeDocumentChunkReranker) Initialization(ctx context.Context, input map[string]interface{}) error { 47 | if component.Timeout == 0 { 48 | component.Timeout = 180 49 | } 50 | 51 | component.client = &http.Client{ 52 | Timeout: time.Second * time.Duration(component.Timeout), 53 | } 54 | 55 | if component.APIKey == "" { 56 | component.APIKey = config.AIAPIkey 57 | } 58 | if component.BaseURL == "" { 59 | if config.AIBaseURL == "" { 60 | return nil 61 | } 62 | index := strings.Index(config.AIBaseURL, "/v1") 63 | if index <= 0 { 64 | return nil 65 | } 66 | component.BaseURL = config.AIBaseURL[:index] + "/api/serverless/bge-reranker-v2-m3/rerank" 67 | } 68 | if component.DefaultHeaders == nil { 69 | component.DefaultHeaders = make(map[string]string, 0) 70 | } 71 | return nil 72 | } 73 | func (component *GiteeDocumentChunkReranker) Run(ctx context.Context, input map[string]interface{}) error { 74 | topN := 0 75 | var score float32 = 0.0 76 | dcs, has := input["documentChunks"] 77 | if !has || dcs == nil { 78 | err := errors.New(funcT("input['documentChunks'] cannot be empty")) 79 | input[errorKey] = err 80 | return err 81 | } 82 | queryObj, has := input["query"] 83 | if !has { 84 | return errors.New(funcT("input['query'] cannot be empty")) 85 | } 86 | query := queryObj.(string) 87 | if query == "" { 88 | return errors.New(funcT("input['query'] cannot be empty")) 89 | } 90 | 91 | tId, has := input["topN"] 92 | if has { 93 | topN = tId.(int) 94 | } 95 | if topN == 0 { 96 | topN = component.TopN 97 | } 98 | if topN == 0 { 99 | topN = 5 100 | } 101 | disId, has := input["score"] 102 | if has { 103 | score = disId.(float32) 104 | } 105 | if score <= 0 { 106 | score = component.Score 107 | } 108 | 109 | documentChunks := dcs.([]DocumentChunk) 110 | if topN > len(documentChunks) { 111 | topN = len(documentChunks) 112 | } 113 | if len(documentChunks) < 1 { //没有文档,不需要重排 114 | return nil 115 | } 116 | documents := make([]string, 0) 117 | for i := 0; i < len(documentChunks); i++ { 118 | documents = append(documents, documentChunks[i].Markdown) 119 | } 120 | 121 | bodyMap := map[string]interface{}{ 122 | "model": component.Model, 123 | "query": query, 124 | "top_n": topN, 125 | "documents": documents, 126 | } 127 | 128 | rsStringByte, err := httpPostJsonBody(component.client, component.APIKey, component.BaseURL, component.DefaultHeaders, bodyMap) 129 | if err != nil { 130 | input[errorKey] = err 131 | return err 132 | } 133 | 134 | rs := struct { 135 | Results []struct { 136 | Document struct { 137 | Text string `json:"text,omitempty"` 138 | } `json:"document,omitempty"` 139 | RelevanceScore float32 `json:"relevance_score,omitempty"` 140 | } `json:"results,omitempty"` 141 | }{} 142 | 143 | err = json.Unmarshal(rsStringByte, &rs) 144 | if err != nil { 145 | input[errorKey] = err 146 | return err 147 | } 148 | rerankerDCS := make([]DocumentChunk, 0) 149 | for i := 0; i < len(rs.Results); i++ { 150 | markdown := rs.Results[i].Document.Text 151 | for j := 0; j < len(documentChunks); j++ { 152 | dc := documentChunks[j] 153 | if markdown == dc.Markdown { //相等 154 | dc.Score = rs.Results[i].RelevanceScore 155 | rerankerDCS = append(rerankerDCS, dc) 156 | break 157 | } 158 | } 159 | } 160 | rerankerDCS = sortDocumentChunksScore(rerankerDCS, topN, score) 161 | input["documentChunks"] = rerankerDCS 162 | return nil 163 | } 164 | -------------------------------------------------------------------------------- /componentMCP.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 minRAG Authors. 2 | // 3 | // This file is part of minRAG. 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | // MCP组件库 19 | package main 20 | 21 | import ( 22 | "bufio" 23 | "bytes" 24 | "context" 25 | "encoding/json" 26 | "fmt" 27 | "io" 28 | "net/http" 29 | "strings" 30 | "time" 31 | ) 32 | 33 | // ToolDefinition 表示 MCP Server 提供的工具定义 34 | type ToolDefinition struct { 35 | Name string `json:"name"` 36 | Description string `json:"description"` 37 | Parameters interface{} `json:"parameters"` // 参数模式 (JSON Schema) 38 | } 39 | 40 | // MCPClient 实现 MCP Streamable HTTP 协议客户端 41 | type MCPClient struct { 42 | BaseURL string 43 | HTTPClient *http.Client 44 | SessionID string // 会话状态保持 45 | Tools []ToolDefinition // 缓存的工具列表 46 | } 47 | 48 | func NewMCPClient(baseURL string) *MCPClient { 49 | return &MCPClient{ 50 | BaseURL: strings.TrimSuffix(baseURL, "/"), 51 | HTTPClient: &http.Client{ 52 | Transport: &http.Transport{ 53 | DisableCompression: true, 54 | }, 55 | }, 56 | } 57 | } 58 | 59 | // DiscoverTools 发现 MCP Server 提供的工具列表和参数 60 | func (c *MCPClient) DiscoverTools(ctx context.Context) error { 61 | // 调用标准的 list_tools 接口获取工具信息 62 | respData, err := c.callRPC(ctx, "GET", "list_tools", nil) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | // 解析工具定义 68 | var tools []ToolDefinition 69 | if err := json.Unmarshal(respData, &tools); err != nil { 70 | return fmt.Errorf("解析工具列表失败: %w", err) 71 | } 72 | 73 | c.Tools = tools 74 | return nil 75 | } 76 | 77 | // GetTools 返回缓存的工具列表 78 | func (c *MCPClient) GetTools() []ToolDefinition { 79 | return c.Tools 80 | } 81 | 82 | // Call 发送请求并处理响应 83 | func (c *MCPClient) Call( 84 | ctx context.Context, 85 | method, toolName string, 86 | params interface{}, 87 | callback func(data []byte) error, 88 | ) error { 89 | // 1. 构建 MCP 协议请求体 90 | reqBody := map[string]interface{}{ 91 | "jsonrpc": "2.0", 92 | "method": toolName, 93 | "params": params, 94 | "id": time.Now().UnixNano(), 95 | } 96 | 97 | return c.callRPCWithCallback(ctx, method, toolName, reqBody, callback) 98 | } 99 | 100 | // callRPC 调用 RPC 方法并返回完整响应 101 | func (c *MCPClient) callRPC( 102 | ctx context.Context, 103 | method, procedure string, 104 | params interface{}, 105 | ) ([]byte, error) { 106 | var respData []byte 107 | err := c.callRPCWithCallback(ctx, method, procedure, params, func(data []byte) error { 108 | respData = append(respData, data...) 109 | return nil 110 | }) 111 | return respData, err 112 | } 113 | 114 | // callRPCWithCallback 核心 RPC 调用方法 115 | func (c *MCPClient) callRPCWithCallback( 116 | ctx context.Context, 117 | method, procedure string, 118 | params interface{}, 119 | callback func(data []byte) error, 120 | ) error { 121 | jsonBody, _ := json.Marshal(params) 122 | 123 | // 2. 创建 HTTP 请求 124 | req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+"/mcp", bytes.NewReader(jsonBody)) 125 | if err != nil { 126 | return err 127 | } 128 | 129 | // 3. 设置 MCP 协议头 130 | req.Header.Set("Content-Type", "application/json") 131 | req.Header.Set("Accept", "application/json, text/event-stream") 132 | if c.SessionID != "" { 133 | req.Header.Set("Mcp-Session-Id", c.SessionID) 134 | } 135 | 136 | // 4. 发送请求 137 | resp, err := c.HTTPClient.Do(req) 138 | if err != nil { 139 | return err 140 | } 141 | defer resp.Body.Close() 142 | 143 | // 5. 保存会话ID 144 | if sessionID := resp.Header.Get("Mcp-Session-Id"); sessionID != "" { 145 | c.SessionID = sessionID 146 | } 147 | 148 | // 6. 根据内容类型选择处理模式 149 | contentType := resp.Header.Get("Content-Type") 150 | switch { 151 | case strings.Contains(contentType, "text/event-stream"): 152 | return c.handleSSEStream(resp.Body, callback) 153 | case strings.Contains(resp.Header.Get("Transfer-Encoding"), "chunked"): 154 | return c.handleChunkedStream(resp.Body, callback) 155 | default: 156 | return c.handleHTTP(resp, callback) 157 | } 158 | } 159 | 160 | // handleSSEStream 处理Server-Sent Events流 161 | func (c *MCPClient) handleSSEStream(body io.ReadCloser, callback func([]byte) error) error { 162 | defer body.Close() 163 | reader := bufio.NewReader(body) 164 | 165 | var eventData bytes.Buffer 166 | for { 167 | line, err := reader.ReadBytes('\n') 168 | if err != nil { 169 | if err == io.EOF { 170 | break 171 | } 172 | return err 173 | } 174 | 175 | line = bytes.TrimSpace(line) 176 | 177 | if len(line) == 0 { 178 | if eventData.Len() > 0 { 179 | if err := callback(eventData.Bytes()); err != nil { 180 | return err 181 | } 182 | eventData.Reset() 183 | } 184 | continue 185 | } 186 | 187 | switch { 188 | case bytes.HasPrefix(line, []byte("data:")): 189 | data := bytes.TrimSpace(line[5:]) 190 | if eventData.Len() > 0 { 191 | eventData.WriteByte('\n') 192 | } 193 | eventData.Write(data) 194 | } 195 | } 196 | 197 | if eventData.Len() > 0 { 198 | return callback(eventData.Bytes()) 199 | } 200 | 201 | return nil 202 | } 203 | 204 | // handleChunkedStream 处理分块传输编码 205 | func (c *MCPClient) handleChunkedStream(body io.ReadCloser, callback func([]byte) error) error { 206 | defer body.Close() 207 | reader := bufio.NewReader(body) 208 | 209 | for { 210 | chunkSizeLine, err := reader.ReadString('\n') 211 | if err != nil { 212 | if err == io.EOF { 213 | break 214 | } 215 | return err 216 | } 217 | 218 | chunkSizeLine = strings.TrimSpace(chunkSizeLine) 219 | if chunkSizeLine == "0" { 220 | break 221 | } 222 | 223 | var chunkSize int 224 | _, err = fmt.Sscanf(chunkSizeLine, "%x", &chunkSize) 225 | if err != nil { 226 | return err 227 | } 228 | 229 | data := make([]byte, chunkSize) 230 | if _, err := io.ReadFull(reader, data); err != nil { 231 | return err 232 | } 233 | 234 | if err := callback(data); err != nil { 235 | return err 236 | } 237 | 238 | if _, err := reader.Discard(2); err != nil { 239 | return err 240 | } 241 | } 242 | return nil 243 | } 244 | 245 | // handleHTTP 处理标准 HTTP 响应 246 | func (c *MCPClient) handleHTTP(resp *http.Response, callback func([]byte) error) error { 247 | defer resp.Body.Close() 248 | data, err := io.ReadAll(resp.Body) 249 | if err != nil { 250 | return err 251 | } 252 | return callback(data) 253 | } 254 | -------------------------------------------------------------------------------- /componentOpenAI_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestHtmlCleaner(t *testing.T) { 10 | mk := ` 11 |

这是一个示例文本。

链接 12 | ` 13 | hc := HtmlCleaner{} 14 | input := make(map[string]interface{}, 0) 15 | document := &Document{Markdown: mk} 16 | input["document"] = document 17 | ctx := context.Background() 18 | hc.Initialization(ctx, input) 19 | err := hc.Run(ctx, input) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | fmt.Println(document.Markdown) 24 | } 25 | 26 | func TestWebScraper(t *testing.T) { 27 | ws := WebScraper{QuerySelector: []string{"#s-top-left"}} 28 | ws.Depth = 2 29 | input := make(map[string]interface{}, 0) 30 | document := &Document{Markdown: ""} 31 | input["document"] = document 32 | ws.WebURL = "https://www.baidu.com" 33 | ctx := context.Background() 34 | ws.Initialization(ctx, input) 35 | herf, err := ws.FetchPage(ctx, document, input) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | fmt.Println(document) 40 | fmt.Println("-------------------") 41 | fmt.Println(herf) 42 | 43 | } 44 | -------------------------------------------------------------------------------- /const.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 minRAG Authors. 2 | // 3 | // This file is part of minRAG. 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package main 19 | 20 | const ( 21 | // 默认名称 22 | appName = "minrag" 23 | 24 | // 基本目录 25 | datadir = "minragdatadir/" 26 | // 数据目录,如果不存在认为是第一次安装启动,会创建默认的数据 27 | sqliteDBfile = datadir + "minrag.db" 28 | 29 | // config 配置的表名称 30 | tableConfigName = "config" 31 | 32 | // user 用户的表名称 33 | tableUserName = "user" 34 | // site 站点信息 35 | tableSiteName = "site" 36 | 37 | // 知识库 knowledgeBase 38 | tableKnowledgeBaseName = "knowledgeBase" 39 | 40 | // 文档 41 | tableDocumentName = "document" 42 | 43 | // 文档分块 44 | tableDocumentChunkName = "document_chunk" 45 | 46 | // 向量化的数据表 47 | tableVecDocumentChunkName = "vec_document_chunk" 48 | 49 | // 组件表 50 | tableComponentName = "component" 51 | 52 | // 智能体 53 | tableAgentName = "agent" 54 | 55 | // 聊天室 56 | tableChatRoomName = "chat_room" 57 | 58 | // 消息日志 59 | tableMessageLogName = "message_log" 60 | 61 | //---------------------------// 62 | 63 | // 模板的路径 64 | templateDir = datadir + "template/" 65 | 66 | // 主题的路径 67 | themeDir = templateDir + "theme/" 68 | 69 | // 静态化文件目录,网站生成的静态html 70 | staticHtmlDir = datadir + "statichtml/" 71 | 72 | tokenUserId = "userId" 73 | 74 | letters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 75 | 76 | userTypeKey = "userType" //0访客,1管理员 77 | 78 | defaultPageSize = 10 79 | 80 | // 静态文件压缩后缀,兼容Nginx gzip_static 81 | compressedFileSuffix = ".gz" 82 | 83 | //版本号 84 | version = "v0.1.0" 85 | ) 86 | -------------------------------------------------------------------------------- /funcmap_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 minRAG Authors. 2 | // 3 | // This file is part of minRAG. 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package main 19 | 20 | import ( 21 | "reflect" 22 | "testing" 23 | ) 24 | 25 | func TestDataSliceKnowledgeBase2Tree(t *testing.T) { 26 | type args struct { 27 | categories []*KnowledgeBase 28 | } 29 | tests := []struct { 30 | name string 31 | args args 32 | want []*KnowledgeBase 33 | }{ 34 | { 35 | name: "无节点", 36 | args: args{ 37 | categories: nil, 38 | }, 39 | want: []*KnowledgeBase{}, 40 | }, 41 | { 42 | name: "两级节点", 43 | args: args{ 44 | categories: []*KnowledgeBase{ 45 | {Id: "1", Name: "KnowledgeBase 1", Pid: ""}, 46 | {Id: "2", Name: "KnowledgeBase 2", Pid: "1"}, 47 | {Id: "3", Name: "KnowledgeBase 3", Pid: ""}, 48 | {Id: "4", Name: "KnowledgeBase 4", Pid: "3"}, 49 | }, 50 | }, 51 | want: []*KnowledgeBase{ 52 | { 53 | Id: "1", 54 | Name: "KnowledgeBase 1", 55 | Leaf: []*KnowledgeBase{{Id: "2", Name: "KnowledgeBase 2", Pid: "1"}}, 56 | }, 57 | { 58 | Id: "3", 59 | Name: "KnowledgeBase 3", 60 | Leaf: []*KnowledgeBase{{Id: "4", Name: "KnowledgeBase 4", Pid: "3"}}, 61 | }, 62 | }, 63 | }, 64 | { 65 | name: "多级节点", 66 | args: args{ 67 | categories: []*KnowledgeBase{ 68 | {Id: "1", Name: "KnowledgeBase 1", Pid: ""}, 69 | {Id: "2", Name: "KnowledgeBase 2", Pid: "1"}, 70 | {Id: "3", Name: "KnowledgeBase 3", Pid: "1"}, 71 | {Id: "4", Name: "KnowledgeBase 4", Pid: "2"}, 72 | {Id: "5", Name: "KnowledgeBase 5", Pid: "2"}, 73 | {Id: "6", Name: "KnowledgeBase 6", Pid: "3"}, 74 | }, 75 | }, 76 | want: []*KnowledgeBase{ 77 | { 78 | Id: "1", 79 | Name: "KnowledgeBase 1", 80 | Leaf: []*KnowledgeBase{ 81 | { 82 | Id: "2", 83 | Pid: "1", 84 | Name: "KnowledgeBase 2", 85 | Leaf: []*KnowledgeBase{ 86 | { 87 | Id: "4", 88 | Pid: "2", 89 | Name: "KnowledgeBase 4", 90 | }, 91 | { 92 | Id: "5", 93 | Pid: "2", 94 | Name: "KnowledgeBase 5", 95 | }, 96 | }, 97 | }, 98 | { 99 | Id: "3", 100 | Pid: "1", 101 | Name: "KnowledgeBase 3", 102 | Leaf: []*KnowledgeBase{ 103 | { 104 | Id: "6", 105 | Pid: "3", 106 | Name: "KnowledgeBase 6", 107 | }, 108 | }, 109 | }, 110 | }, 111 | }, 112 | }, 113 | }, 114 | { 115 | name: "多颗树", 116 | args: args{ 117 | categories: []*KnowledgeBase{ 118 | {Id: "1", Name: "KnowledgeBase 1", Pid: ""}, 119 | {Id: "2", Name: "KnowledgeBase 2", Pid: "1"}, 120 | {Id: "3", Name: "KnowledgeBase 3", Pid: ""}, 121 | {Id: "4", Name: "KnowledgeBase 4", Pid: "3"}, 122 | {Id: "5", Name: "KnowledgeBase 5", Pid: ""}, 123 | {Id: "6", Name: "KnowledgeBase 6", Pid: "5"}, 124 | }, 125 | }, 126 | want: []*KnowledgeBase{ 127 | { 128 | Id: "1", 129 | Name: "KnowledgeBase 1", 130 | Leaf: []*KnowledgeBase{ 131 | { 132 | Id: "2", 133 | Pid: "1", 134 | Name: "KnowledgeBase 2", 135 | }, 136 | }, 137 | }, 138 | { 139 | Id: "3", 140 | Name: "KnowledgeBase 3", 141 | Leaf: []*KnowledgeBase{ 142 | { 143 | Id: "4", 144 | Pid: "3", 145 | Name: "KnowledgeBase 4", 146 | }, 147 | }, 148 | }, 149 | { 150 | Id: "5", 151 | Name: "KnowledgeBase 5", 152 | Leaf: []*KnowledgeBase{ 153 | { 154 | Id: "6", 155 | Pid: "5", 156 | Name: "KnowledgeBase 6", 157 | }, 158 | }, 159 | }, 160 | }, 161 | }, 162 | } 163 | for _, tt := range tests { 164 | t.Run(tt.name, func(t *testing.T) { 165 | if got := sliceKnowledgeBase2Tree(tt.args.categories); !reflect.DeepEqual(got, tt.want) { 166 | t.Errorf("sliceKnowledgeBase2Tree() = %v, want %v", got, tt.want) 167 | } 168 | }) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module gitee.com/minrag/minrag 2 | 3 | go 1.24 4 | 5 | require ( 6 | gitee.com/chunanyong/zorm v1.7.8 7 | github.com/chromedp/chromedp v0.13.6 8 | github.com/cloudwego/hertz v0.10.0 9 | github.com/mattn/go-sqlite3 v1.14.28 10 | github.com/mojocn/base64Captcha v1.3.8 11 | golang.org/x/crypto v0.38.0 12 | golang.org/x/net v0.40.0 13 | ) 14 | 15 | require ( 16 | github.com/bytedance/gopkg v0.1.1 // indirect 17 | github.com/bytedance/sonic v1.13.2 // indirect 18 | github.com/bytedance/sonic/loader v0.2.4 // indirect 19 | github.com/chromedp/cdproto v0.0.0-20250403032234-65de8f5d025b // indirect 20 | github.com/chromedp/sysutil v1.1.0 // indirect 21 | github.com/cloudwego/base64x v0.1.5 // indirect 22 | github.com/cloudwego/gopkg v0.1.4 // indirect 23 | github.com/cloudwego/netpoll v0.7.0 // indirect 24 | github.com/fsnotify/fsnotify v1.5.4 // indirect 25 | github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 // indirect 26 | github.com/gobwas/httphead v0.1.0 // indirect 27 | github.com/gobwas/pool v0.2.1 // indirect 28 | github.com/gobwas/ws v1.4.0 // indirect 29 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect 30 | github.com/golang/protobuf v1.5.0 // indirect 31 | github.com/klauspost/cpuid/v2 v2.0.9 // indirect 32 | github.com/nyaruka/phonenumbers v1.0.55 // indirect 33 | github.com/tidwall/gjson v1.14.4 // indirect 34 | github.com/tidwall/match v1.1.1 // indirect 35 | github.com/tidwall/pretty v1.2.0 // indirect 36 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 37 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect 38 | golang.org/x/image v0.23.0 // indirect 39 | golang.org/x/sys v0.33.0 // indirect 40 | google.golang.org/protobuf v1.27.1 // indirect 41 | ) 42 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 minRAG Authors. 2 | // 3 | // This file is part of minRAG. 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package main 19 | 20 | import ( 21 | "fmt" 22 | "strings" 23 | "time" 24 | 25 | "github.com/cloudwego/hertz/pkg/app/server" 26 | //"github.com/hertz-contrib/gzip" 27 | ) 28 | 29 | // 变量的位置不要更改!!!!!,实际是做初始化使用的,优先级高于init函数!!! 30 | 31 | // 是否已经安装过了 32 | var installed = isInstalled() 33 | 34 | // 加载配置文件 35 | var config, site = loadInstallConfig() 36 | 37 | // 服务器url路径 38 | var httpServerPath = "http://" 39 | 40 | // hertz对象,可以在其他地方使用 41 | var h = server.Default(server.WithHostPorts(config.ServerPort), server.WithBasePath(config.BasePath), server.WithMaxRequestBodySize(config.MaxRequestBodySize), server.WithKeepAliveTimeout(time.Second*time.Duration(180))) 42 | 43 | func init() { 44 | 45 | // 设置随机种子 46 | //rand.Seed(time.Now().UnixNano()) 47 | 48 | // gzip压缩文件,产生 xxx.html.gz 文件,https://www.cloudwego.io/zh/docs/hertz/tutorials/basic-feature/middleware/gzip/ 49 | // h.Use(gzip.Gzip(gzip.DefaultCompression)) 50 | } 51 | 52 | func main() { 53 | 54 | // 初始化语言包 55 | initLocale() 56 | 57 | //加载页面模板 58 | err := loadTemplate() 59 | if err != nil { // 初始化模板异常 60 | panic(funcT("Template initialization anomaly")) 61 | } 62 | 63 | message := funcT("Open the front-end in the browse") + ": " 64 | if strings.HasPrefix(config.ServerPort, ":") { 65 | httpServerPath += "127.0.0.1" 66 | } 67 | httpServerPath += config.ServerPort + config.BasePath 68 | message += httpServerPath 69 | message += "\n" + funcT("Open the back-end in the browser") + ": " + httpServerPath + "admin/login" 70 | fmt.Println(message) 71 | 72 | // 启动服务 73 | h.Spin() 74 | } 75 | -------------------------------------------------------------------------------- /minragdatadir/dict.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/dict.zip -------------------------------------------------------------------------------- /minragdatadir/extensions/libsimple.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/extensions/libsimple.dll -------------------------------------------------------------------------------- /minragdatadir/extensions/libsimple.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/extensions/libsimple.dylib -------------------------------------------------------------------------------- /minragdatadir/extensions/libsimple.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/extensions/libsimple.so -------------------------------------------------------------------------------- /minragdatadir/extensions/libsimple.so-aarch64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/extensions/libsimple.so-aarch64 -------------------------------------------------------------------------------- /minragdatadir/extensions/vec0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/extensions/vec0.dll -------------------------------------------------------------------------------- /minragdatadir/extensions/vec0.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/extensions/vec0.dylib -------------------------------------------------------------------------------- /minragdatadir/extensions/vec0.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/extensions/vec0.so -------------------------------------------------------------------------------- /minragdatadir/install_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "serverPort":":738", 3 | "timeout":7200, 4 | "jwttokenKey":"jwttoken", 5 | "jwtSecret":"", 6 | "basePath":"/", 7 | "proxy":"", 8 | "maxRequestBodySize":20971520, 9 | "locale":"zh-CN", 10 | "aiBaseURL":"https://ai.gitee.com/v1", 11 | "aiAPIkey":"A4FTACZVPGAIV8PZCKIBEUGV7ZBMXTIBEGUGNC11" 12 | } -------------------------------------------------------------------------------- /minragdatadir/libgcc_s_seh-1.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/libgcc_s_seh-1.dll -------------------------------------------------------------------------------- /minragdatadir/locales/en-US.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /minragdatadir/public/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/demo.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/000.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/001.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/002.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/003.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/004.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/005.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/005.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/006.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/006.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/007.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/007.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/008.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/008.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/009.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/009.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/010.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/010.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/011.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/011.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/012.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/012.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/013.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/013.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/014.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/014.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/015.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/015.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/016.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/016.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/017.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/017.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/018.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/018.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/019.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/019.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/020.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/020.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/021.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/021.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/022.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/022.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/023.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/023.png -------------------------------------------------------------------------------- /minragdatadir/public/doc/image/024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/doc/image/024.png -------------------------------------------------------------------------------- /minragdatadir/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/favicon.png -------------------------------------------------------------------------------- /minragdatadir/public/gongan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/gongan.png -------------------------------------------------------------------------------- /minragdatadir/public/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/index.png -------------------------------------------------------------------------------- /minragdatadir/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/logo.png -------------------------------------------------------------------------------- /minragdatadir/public/minrag-logo-1080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/minrag-logo-1080.png -------------------------------------------------------------------------------- /minragdatadir/public/minrag-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/public/minrag-logo.png -------------------------------------------------------------------------------- /minragdatadir/public/minrag-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | background 10 | 11 | 12 | 13 | Layer 1 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /minragdatadir/public/minrag-文字-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | background 10 | 11 | 12 | 13 | Layer 1 14 | 15 | 16 | 17 | minRAG 18 | 19 | -------------------------------------------------------------------------------- /minragdatadir/public/mqtt3.1.1.html: -------------------------------------------------------------------------------- 1 | 2 | 107 | 108 | 109 | 110 | 111 | 112 |
服务器地址:
113 | 114 |
  115 |                                  116 |
117 | 118 |
119 | 订阅消息  120 | QoS  125 |                                  126 |
127 |
128 | 发布消息:   129 |                                  130 |
131 |
132 | 订阅列表 133 |
134 |
135 | 136 | 137 | 138 | 139 | 140 | 141 |
订阅消息QoS操作
142 |
143 |
144 | 接收消息 145 |
146 |
147 | 148 |
149 | 150 | -------------------------------------------------------------------------------- /minragdatadir/template/admin/agent/list.html: -------------------------------------------------------------------------------- 1 | {{template "admin/header.html"}} 2 | {{T "Agent"}} - MINRAG 3 | {{template "admin/bodystart.html"}} 4 | 5 |
6 | 7 |
8 | 9 |
10 | 11 |
12 |
13 |      14 |
15 |
16 | +{{T "Add Agent"}} 17 |
18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {{ range $i,$v := .Data }} 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 51 | 67 | 68 | {{end }} 69 | 70 |
ID{{T "Name"}}{{T "Knowledge Base"}}{{T "Pipeline"}}{{T "Sort"}}{{T "Status"}}{{T "Actions"}}
{{ .Id }} {{ .Name }} {{ .KnowledgeBaseID }} {{ .PipelineID }} {{ .SortNo }} 43 | {{if eq .Status 0 }} 44 | {{T "Disable"}} 45 | {{else if eq .Status 1 }} 46 | {{T "Active"}} 47 | {{else}} 48 | {{T "Unknown"}} 49 | {{end}} 50 | 52 | 57 | 62 | 66 |
71 | 72 | 73 | {{template "admin/bodyend.html"}} 74 | 75 | 76 | -------------------------------------------------------------------------------- /minragdatadir/template/admin/agent/save.html: -------------------------------------------------------------------------------- 1 | {{template "admin/header.html"}} 2 | 10 | {{T "Add Agent"}} - MINRAG 11 | {{ $knowledgeBases := knowledgeBases }} 12 | {{ $pipelineIDs := pipelineIDs }} 13 | {{template "admin/bodystart.html"}} 14 |
15 |
16 | {{T "Add Agent"}} 17 |
18 |
19 |
20 |
21 | 22 |
23 | 24 |
25 |
26 |
27 | 28 |
29 | 30 |
31 |
32 |
33 | 34 |
35 | 36 |
37 |
38 |
39 | 40 |
41 | 42 |
43 |
44 |
45 | 46 |
47 | 48 |
49 |
50 | 51 |
52 | 53 |
54 | 60 |
61 |
62 | 63 |
64 | 65 |
66 | 72 |
73 |
74 | 75 |
76 | 77 |
78 | 79 |
80 |
81 | 82 |
83 | 84 |
85 | 86 |
87 |
88 | 89 |
90 | 91 |
92 | 96 |
97 |
98 | 99 | 100 | 101 |
102 |
103 | 104 |
105 |
106 |
107 |
108 |
109 | {{template "admin/bodyend.html"}} 110 | 111 | -------------------------------------------------------------------------------- /minragdatadir/template/admin/agent/update.html: -------------------------------------------------------------------------------- 1 | {{template "admin/header.html"}} 2 | 10 | {{T "Update Agent"}} - MINRAG 11 | {{ $knowledgeBases := knowledgeBases }} 12 | {{ $pipelineIDs := pipelineIDs }} 13 | {{template "admin/bodystart.html"}} 14 |
15 |
16 | {{T "Update Agent"}} 17 |
18 |
19 |
20 |
21 | 22 |
23 | 24 | 25 |
26 |
27 |
28 | 29 |
30 | 31 |
32 |
33 |
34 | 35 |
36 | 37 |
38 |
39 |
40 | 41 |
42 | 43 |
44 |
45 |
46 | 47 |
48 | 49 |
50 |
51 | 52 |
53 | 54 |
55 | 60 |
61 |
62 | 63 |
64 | 65 |
66 | 71 |
72 |
73 | 74 |
75 | 76 |
77 | 78 |
79 |
80 | 81 |
82 | 83 |
84 | 85 |
86 |
87 | 88 |
89 | 90 |
91 | 95 |
96 |
97 | 98 |
99 |
100 | 101 |
102 |
103 |
104 |
105 |
106 | {{template "admin/bodyend.html"}} 107 | 108 | -------------------------------------------------------------------------------- /minragdatadir/template/admin/bodyend.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /minragdatadir/template/admin/bodystart.html: -------------------------------------------------------------------------------- 1 | 43 | 44 | 45 | 46 |
47 | 48 | 116 |
117 | 118 |
-------------------------------------------------------------------------------- /minragdatadir/template/admin/component/list.html: -------------------------------------------------------------------------------- 1 | {{template "admin/header.html"}} 2 | {{T "Component"}} - MINRAG 3 | {{template "admin/bodystart.html"}} 4 | 5 |
6 | 7 |
8 | 9 |
10 | 11 |
12 |
13 |      14 |
15 | 18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {{ range $i,$v := .Data }} 33 | 34 | 35 | 36 | 37 | 38 | 47 | 58 | 59 | {{end }} 60 | 61 |
ID{{T "Component Type"}}{{T "Sort"}}{{T "Status"}}{{T "Actions"}}
{{ .Id }} {{ .ComponentType }} {{ .SortNo }} 39 | {{if eq .Status 0 }} 40 | {{T "Disable"}} 41 | {{else if eq .Status 1 }} 42 | {{T "Active"}} 43 | {{else}} 44 | {{T "Unknown"}} 45 | {{end}} 46 | 48 | 53 | 57 |
62 | 63 | 64 | {{template "admin/bodyend.html"}} 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /minragdatadir/template/admin/component/save.html: -------------------------------------------------------------------------------- 1 | {{template "admin/header.html"}} 2 | 10 | {{T "Add Component"}} - MINRAG 11 | {{ $componentType := componentType }} 12 | {{template "admin/bodystart.html"}} 13 |
14 |
15 | {{T "Add Component"}} 16 |
17 |
18 |
19 |
20 | 21 |
22 | 23 |
24 |
25 |
26 | 27 |
28 | 34 |
35 |
36 |
37 | 38 |
39 | 40 |
41 |
42 | 43 |
44 | 45 |
46 | 50 |
51 |
52 | 53 |
54 | 55 |
56 | 57 |
58 |
59 | 60 |
61 |
62 | 63 |
64 |
65 |
66 |
67 |
68 | {{template "admin/bodyend.html"}} 69 | 70 | -------------------------------------------------------------------------------- /minragdatadir/template/admin/component/update.html: -------------------------------------------------------------------------------- 1 | {{template "admin/header.html"}} 2 | 10 | {{T "Update Component"}} - MINRAG 11 | {{ $componentType := componentType }} 12 | {{template "admin/bodystart.html"}} 13 |
14 |
15 | {{T "Update Component"}} 16 |
17 |
18 |
19 |
20 | 21 |
22 | 23 | 24 |
25 |
26 |
27 | 28 |
29 | 34 |
35 |
36 | 37 |
38 | 39 |
40 | 41 |
42 |
43 | 44 |
45 | 46 |
47 | 51 |
52 |
53 | 54 |
55 | 56 |
57 | 58 |
59 |
60 | 61 |
62 |
63 | 64 |
65 |
66 |
67 |
68 |
69 | {{template "admin/bodyend.html"}} 70 | 71 | -------------------------------------------------------------------------------- /minragdatadir/template/admin/config/update.html: -------------------------------------------------------------------------------- 1 | {{template "admin/header.html"}} 2 | 10 | 11 | {{T "Update Settings"}} - MINRAG 12 | 13 | 14 | {{template "admin/bodystart.html"}} 15 |
16 |
17 | {{T "Update Settings"}} 18 |
19 |
20 |
21 |
22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 |
32 | 33 |
34 | 35 |
36 |
37 |
38 | 39 |
40 | 41 |
42 |
43 |
44 | 45 |
46 | 47 |
48 |
49 |
50 | 51 |
52 | 53 |
54 |
55 |
56 | 57 |
58 | 62 |
63 |
64 |
65 | 66 |
67 | 68 |
69 |
70 |
71 | 72 |
73 | 74 |
75 |
76 |
77 | 78 |
79 | 80 |
81 |
82 |
83 | 84 |
85 | 86 |
87 |
88 |
89 |
90 | 91 | 92 | {{T "Site Information"}} 93 | {{T "Theme Template"}} 94 |
95 |
96 |
97 |
98 |
99 | {{template "admin/bodyend.html"}} 100 | 101 | 173 | -------------------------------------------------------------------------------- /minragdatadir/template/admin/css/tree.css: -------------------------------------------------------------------------------- 1 | 2 | /* 参考文章 https://segmentfault.com/a/1190000043966941 */ 3 | .tree { 4 | flex: 1; 5 | overflow: auto; 6 | padding: 0px; 7 | position: relative; 8 | } 9 | 10 | .tree summary { 11 | outline: 0; 12 | padding-left: 22px; 13 | list-style: none; 14 | background: repeating-linear-gradient(90deg, #c0c4cc 0 1px, transparent 0px 2px) 0px 50%/22px 1px no-repeat; 15 | /* background: linear-gradient(#c0c4cc,#c0c4cc) 0px 50%/20px 1px no-repeat; */ 16 | } 17 | 18 | .tree details:last-child { 19 | background-size: 1px 14px; 20 | } 21 | 22 | .tree>details:not(:last-child)>details:last-child { 23 | background-size: 1px 100%; 24 | } 25 | 26 | .tree details { 27 | padding-left: 28px; 28 | background: repeating-linear-gradient(#c0c4cc 0 1px, transparent 0px 2px) 28px 0px/1px 100% no-repeat; 29 | /* background: linear-gradient(#c0c4cc, #c0c4cc) 40px 0px/1px 100% no-repeat; */ 30 | } 31 | 32 | .tree>details { 33 | background: none; 34 | padding-left: 0; 35 | } 36 | 37 | .tree>details>summary { 38 | background: none; 39 | } 40 | 41 | .tree summary { 42 | display: flex; 43 | align-items: center; 44 | height: 26px; 45 | font-size: 14px; 46 | line-height: 14px; 47 | color: #000; 48 | cursor: default; 49 | } 50 | 51 | .tree summary::after { 52 | content: ""; 53 | position: absolute; 54 | left: 16px; 55 | right: 16px; 56 | height: 26px; 57 | background: #eef2ff; 58 | border-radius: 8px; 59 | z-index: -1; 60 | opacity: 0; 61 | transition: 0.2s; 62 | } 63 | 64 | .tree summary:hover::after { 65 | opacity: 1; 66 | } 67 | 68 | .tree summary:not(:only-child)::before { 69 | content: ""; 70 | width: 12px; 71 | height: 12px; 72 | flex-shrink: 0; 73 | margin-right: 8px; 74 | border: 1px solid #c0c4cc; 75 | background: linear-gradient(#5f5f5f, #5f5f5f) 50%/1px 8px no-repeat, 76 | linear-gradient(#5f5f5f, #5f5f5f) 50%/8px 1px no-repeat; 77 | } 78 | 79 | .tree details[open]>summary::before { 80 | background: linear-gradient(#5f5f5f, #5f5f5f) 50%/8px 1px no-repeat; 81 | } 82 | 83 | .tree summary:hover .operate { 84 | opacity: 1; 85 | visibility: visible; 86 | } 87 | 88 | .operate{ 89 | padding-left: 5px; 90 | opacity: 0; 91 | visibility: hidden; 92 | transition: opacity 0.2s ease-in-out; 93 | } 94 | 95 | .tips-dropdown { 96 | cursor: pointer; 97 | padding-left: 5px; 98 | font-size: 12px; 99 | } 100 | 101 | .tips-dropdown:hover { 102 | color: #16baaa; 103 | } 104 | 105 | -------------------------------------------------------------------------------- /minragdatadir/template/admin/document/update.html: -------------------------------------------------------------------------------- 1 | {{template "admin/header.html"}} 2 | 11 | {{T "Edit Document"}} - MINRAG 12 | 13 | 14 | {{template "admin/bodystart.html"}} 15 | {{ $knowledgeBases := selectList "knowledgeBase" "" 1 1000 "* FROM knowledgeBase order by sortNo desc" }} 16 | 17 |
18 |
19 |
20 |
21 | 22 |
23 | 24 | 25 |
26 |
27 |
28 | 29 |
30 | 32 |
33 |
34 |
35 | 36 |
37 | 39 |
40 |
41 |
42 | 43 |
44 |
45 | 46 |
47 | 53 |
54 |
55 |
56 | 57 |
58 | 64 |
65 |
66 |
67 |
68 | 69 | 71 |
72 |
73 |
74 |
75 | 76 |
77 |
78 | {{template "admin/bodyend.html"}} 79 | 80 | -------------------------------------------------------------------------------- /minragdatadir/template/admin/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | error 10 | 11 | -------------------------------------------------------------------------------- /minragdatadir/template/admin/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | -------------------------------------------------------------------------------- /minragdatadir/template/admin/index.html: -------------------------------------------------------------------------------- 1 | {{template "admin/header.html"}} 2 | {{T "Backend management"}} - MINRAG 3 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /minragdatadir/template/admin/install.html: -------------------------------------------------------------------------------- 1 | {{template "admin/header.html"}} 2 | {{T "Install"}} - GPRESS 3 | 4 | 58 | 59 | 60 | 61 |
62 | 63 |

{{T "Install"}}

64 |
65 |
66 |
67 |
68 | 69 |
70 | 71 |
72 |
73 |
74 |
75 |
76 | 77 |
78 | 79 |
80 |
81 |
82 |
83 |
84 | 85 |
86 | 87 |
88 |
89 |
90 | 91 |
92 |
93 | 94 |
95 | 96 |
97 |
98 |
99 | 100 |
101 | 102 |
103 |
104 |
105 | 106 |
107 |
108 |
109 | 110 | 111 | 112 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /minragdatadir/template/admin/js/cherry-markdown/fonts/ch-icon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/template/admin/js/cherry-markdown/fonts/ch-icon.eot -------------------------------------------------------------------------------- /minragdatadir/template/admin/js/cherry-markdown/fonts/ch-icon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/template/admin/js/cherry-markdown/fonts/ch-icon.ttf -------------------------------------------------------------------------------- /minragdatadir/template/admin/js/cherry-markdown/fonts/ch-icon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/template/admin/js/cherry-markdown/fonts/ch-icon.woff -------------------------------------------------------------------------------- /minragdatadir/template/admin/js/cherry-markdown/fonts/ch-icon.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/template/admin/js/cherry-markdown/fonts/ch-icon.woff2 -------------------------------------------------------------------------------- /minragdatadir/template/admin/js/layui/font/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/template/admin/js/layui/font/iconfont.eot -------------------------------------------------------------------------------- /minragdatadir/template/admin/js/layui/font/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/template/admin/js/layui/font/iconfont.ttf -------------------------------------------------------------------------------- /minragdatadir/template/admin/js/layui/font/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/template/admin/js/layui/font/iconfont.woff -------------------------------------------------------------------------------- /minragdatadir/template/admin/js/layui/font/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/template/admin/js/layui/font/iconfont.woff2 -------------------------------------------------------------------------------- /minragdatadir/template/admin/js/layui/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 测试 - Layui 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 | 16 | 17 |
18 | 19 | 20 |
21 | 22 |
23 |
24 |
    25 |
  • 您当前预览的是:Layui-v
  • 26 |
  • Layui 是一套开源免费的 Web UI(界面)组件库。这是一个极其简洁的演示页面
  • 27 |
28 |
29 |
30 |
31 | 32 | 33 | 34 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /minragdatadir/template/admin/js/layui/免责声明.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 免责声明 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /minragdatadir/template/admin/js/layui/官方文档.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 官方文档 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /minragdatadir/template/admin/knowledgeBase/save.html: -------------------------------------------------------------------------------- 1 | {{template "admin/header.html"}} 2 | 10 | {{T "Add Knowledge Base"}} - MINRAG 11 | {{template "admin/bodystart.html"}} 12 | 13 | {{ $knowledgeBase := selectOne "knowledgeBase" "* from knowledgeBase WHERE id=?" .QueryStringMap.pid }} 14 | 15 | {{ $maxSortNo:= selectOne "knowledgeBase" "max(sortNo) as sortNo from knowledgeBase" }} 16 | 17 |
18 |
19 | {{T "Add Knowledge Base"}} 20 |
21 |
22 |
23 |
24 | 25 |
26 | 27 |
28 |
29 | 30 |
31 | 32 |
33 | 34 |
35 |
36 | 37 |
38 | 39 |
40 | 41 | 42 |
43 |
44 | 45 |
46 | 47 |
48 | 49 |
50 |
51 | 52 |
53 | 54 |
55 | 59 |
60 |
61 | 62 |
63 |
64 | 65 |
66 |
67 |
68 |
69 |
70 | {{template "admin/bodyend.html"}} 71 | 72 | -------------------------------------------------------------------------------- /minragdatadir/template/admin/knowledgeBase/update.html: -------------------------------------------------------------------------------- 1 | {{template "admin/header.html"}} 2 | 10 | {{T "Update Knowledge Base"}} - MINRAG 11 | {{template "admin/bodystart.html"}} 12 | 13 | {{ $knowledgeBase := selectOne "knowledgeBase" "* from knowledgeBase WHERE id=?" .Data.Pid }} 14 | 15 |
16 |
17 | {{T "Update Knowledge Base"}} 18 |
19 |
20 |
21 | 22 |
23 | 24 |
25 | 26 | 27 |
28 |
29 |
30 | 31 |
32 | 33 |
34 |
35 | 36 | 37 |
38 | 39 |
40 | 41 | 42 |
43 |
44 | 45 |
46 | 47 |
48 | 49 |
50 |
51 | 52 |
53 | 54 |
55 | 59 |
60 |
61 | 62 |
63 |
64 | 65 |
66 |
67 |
68 |
69 |
70 | {{template "admin/bodyend.html"}} 71 | 72 | -------------------------------------------------------------------------------- /minragdatadir/template/admin/login.html: -------------------------------------------------------------------------------- 1 | {{template "admin/header.html"}} 2 | {{T "Login"}} - MINRAG 3 | 4 | 59 | 60 | 61 | 62 | 109 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /minragdatadir/template/admin/site/update.html: -------------------------------------------------------------------------------- 1 | {{template "admin/header.html"}} 2 | 10 | {{T "Update Site Information"}} - MINRAG 11 | {{ $themeName := themeName }} 12 | {{template "admin/bodystart.html"}} 13 |
14 |
15 | {{T "Update Site Information"}} 16 |
17 |
18 |
19 |
20 | 21 |
22 | 23 | 24 | 25 | 26 |
27 |
28 |
29 | 30 |
31 | 32 |
33 |
34 |
35 | 36 |
37 | 38 |
39 |
40 |
41 | 42 |
43 | 44 |
45 |
46 |
47 | 48 |
49 | 54 |
55 |
56 |
57 | 58 |
59 | 64 |
65 |
66 |
67 | 68 |
69 | 74 |
75 |
76 |
77 | 78 |
79 | 84 |
85 |
86 |
87 | 88 |
89 | 90 |
91 |
92 |
93 | 94 |
95 | 96 |
97 |
98 |
99 | 100 |
101 |   102 |   105 | 106 |
107 |
108 |
109 | 110 |
111 |   112 |   115 | 116 |
117 |
118 |
119 | 120 |
121 | 122 |
123 |
124 |
125 |
126 | 127 |
128 |
129 |
130 |
131 |
132 | {{template "admin/bodyend.html"}} 133 | 134 | -------------------------------------------------------------------------------- /minragdatadir/template/admin/themeTemplate/list.html: -------------------------------------------------------------------------------- 1 | {{ $convertJson := convertType $.Data "object" "json" }} 2 | {{template "admin/header.html"}} 3 | {{T "Theme Template"}} - MINRAG 4 | 5 | 6 | {{template "admin/bodystart.html"}} 7 | 8 |
9 |
10 |
11 | 12 |
13 |
14 | 17 |
18 |
19 | 22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 | 31 |
32 |
33 |
34 |
35 | 36 |
37 |
38 | 39 |
40 | {{template "admin/bodyend.html"}} 41 | 42 | 258 | 259 | -------------------------------------------------------------------------------- /minragdatadir/template/admin/user/update.html: -------------------------------------------------------------------------------- 1 | {{template "admin/header.html"}} 2 | 10 | {{T "Update User Information"}} - MINRAG 11 | 12 | {{template "admin/bodystart.html"}} 13 |
14 |
15 | {{T "Update User Information"}} 16 |
17 |
18 |
19 |
20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
30 | 31 |
32 | 33 |
34 |
35 | 36 |
37 | 38 |
39 | 40 |
41 |
42 |
43 | 44 |
45 | 46 |
47 |
48 |
49 |
50 | 51 |
52 |
53 |
54 |
55 |
56 | {{template "admin/bodyend.html"}} 57 | 58 | -------------------------------------------------------------------------------- /minragdatadir/template/theme/default/css/agent.css: -------------------------------------------------------------------------------- 1 | /* styles.css */ 2 | body { 3 | font-family: Arial, sans-serif; 4 | margin: 0 auto; 5 | padding: 0; 6 | width: 61.8%; 7 | overflow-y: auto; /* 当内容超出视口时显示滚动条 */ 8 | background-color: #f4f4f4; 9 | } 10 | h1,h2,h3,h4,h5,p,ul,ol,li { 11 | display: block; /* 或者使用 inline-block */ 12 | margin: 2px; /* 移除上下间距 */ 13 | padding: 0; /* 移除内边距 */ 14 | } 15 | 16 | .chat-container { 17 | display: flex; 18 | background-color: #f4f4f4; 19 | min-height: 100vh; /* 可选:确保div至少占满屏幕 */ 20 | border-radius: 5px; 21 | overflow-y: auto; 22 | font-size: 14px; 23 | } 24 | 25 | .chat-panel { 26 | flex: 1; 27 | display: flex; 28 | flex-direction: column; 29 | 30 | } 31 | 32 | .chat-header { 33 | font-size: 18px; 34 | font-weight: bold; 35 | text-align: center; 36 | padding: 10px; 37 | /*border-bottom: 1px solid #ddd;*/ 38 | position: sticky; /* 固定位置 */ 39 | top: 0; /* 粘性定位的顶部偏移 */ 40 | z-index: 10; /* 确保在最上层 */ 41 | } 42 | 43 | .chat-messages { 44 | flex: 1; 45 | /*padding: 15px;*/ 46 | overflow-y: auto; 47 | display: flex; 48 | flex-direction: column; 49 | gap: 15px; 50 | font-size: 14px; 51 | overflow:visible; 52 | padding-bottom: 220px; /* 根据输入框高度调整该值 */ 53 | } 54 | 55 | .message { 56 | display: flex; 57 | flex-direction: column; /* 垂直排列子元素 */ 58 | /*margin-bottom: 10px;*/ 59 | padding: 10px; 60 | max-width: 100%; 61 | word-wrap: break-word; 62 | position: relative; 63 | font-size: 14px; 64 | } 65 | 66 | /* AI消息样式 */ 67 | .ai-message-think,.ai-message { 68 | align-self: flex-start; 69 | background: white; 70 | color: #000000; 71 | border-radius: 15px; 72 | position: relative; 73 | font-size: 14px; 74 | margin-right: auto; /* 确保靠左对齐 */ 75 | /*display: flex;*/ 76 | /*align-items: center; 内容垂直居中 */ 77 | text-align: left; 78 | width: fit-content; 79 | max-width: 100%; 80 | word-wrap: break-word; /* 长单词或 URL 自动换行 */ 81 | overflow-wrap: break-word; /* 新换行规则,适合现代浏览器 */ 82 | /*white-space: pre-wrap; 保留换行符,并允许长内容换行 */ 83 | } 84 | .ai-message { 85 | margin-top: 2px; 86 | } 87 | .ai-message-think { 88 | color: #8b8b8b; 89 | font-size: 14px; 90 | border-left: 2px solid #8b8b8b; /* 添加蓝色左侧边框,宽度为2px */ 91 | padding-left: 5px; /* 添加一些内边距,使内容不紧贴边框 */ 92 | border-radius: 0px; 93 | margin-bottom: 2px; 94 | } 95 | 96 | 97 | /* 用户消息样式 */ 98 | .user-message { 99 | background-color: #eff6ff; 100 | color: #000000; 101 | align-self: flex-end; 102 | border-radius: 15px; 103 | font-size: 14px; 104 | align-items: center; /* 内容垂直居中 */ 105 | width: fit-content; 106 | max-width: 100%; 107 | margin-left: auto; /* 确保靠右对齐 */ 108 | word-wrap: break-word; /* 长单词或 URL 自动换行 */ 109 | overflow-wrap: break-word; /* 新换行规则,适合现代浏览器 */ 110 | white-space: pre-wrap; /* 保留换行符,并允许长内容换行 */ 111 | } 112 | /* 包裹输入框的容器 */ 113 | .copyright,.footer,.input-container { 114 | position: fixed; /* 固定定位 */ 115 | bottom: 20px; /* 距离底部20px */ 116 | left: 50%; /* 向右偏移50% */ 117 | /* margin: 2px; 移除上下间距 */ 118 | transform: translateX(-50%); /* 只水平偏移自身宽度的50%,实现水平居中 */ 119 | box-sizing: border-box; /* 确保宽度包含边框和内边距 */ 120 | width: 100%; /* 与 body 宽度一致 */ 121 | /* max-width: 600px; 最大宽度限制 */ 122 | background: #fff; 123 | border: 1px solid #ddd; 124 | border-radius: 10px; 125 | font-size: 14px; 126 | overflow: hidden; 127 | box-shadow: 0 4px 8px #4166d5(0, 0, 0, 0.1); /* 添加阴影效果 */ 128 | } 129 | 130 | /* 输入框样式 */ 131 | #input-box { 132 | width: 100%; 133 | height: 144px; /*输入框有上边框,避免内容重叠无法分辨*/ 134 | border: none; 135 | padding: 15px; 136 | font-size: 14px; 137 | resize: none; 138 | box-sizing: border-box; 139 | } 140 | #input-box:focus { 141 | outline: none; /* 去掉默认的选中边框 */ 142 | } 143 | .footer{ 144 | height: 170px; 145 | width: 62%; 146 | bottom: 0px; /* 距离底部20px */ 147 | background-color: #f4f4f4; 148 | border:0px; 149 | border-radius: 0px; 150 | } 151 | .copyright{ 152 | height: 20px; 153 | color: #000000; 154 | bottom: 0px; 155 | border:0px; 156 | align-items: center; /* 内容垂直居中 */ 157 | align-content: center; 158 | text-align: center; 159 | background-color: #f4f4f4; 160 | font-size: 12px; 161 | } 162 | 163 | /* 发送按钮样式 */ 164 | #send-button { 165 | position: absolute; 166 | bottom: 10px; 167 | right: 10px; 168 | padding: 10px 20px; 169 | font-size: 16px; 170 | background-color: #007bff; 171 | color: #fff; 172 | border: none; 173 | border-radius: 5px; 174 | cursor: pointer; 175 | } 176 | -------------------------------------------------------------------------------- /minragdatadir/template/theme/default/error.html: -------------------------------------------------------------------------------- 1 | err -------------------------------------------------------------------------------- /minragdatadir/template/theme/default/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{T "Agent"}} - MINRAG 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /minragdatadir/upload/upload.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minrag/minRAG/029efa4b269fe7bd45fc5ba40ea6342a9fe4bf2d/minragdatadir/upload/upload.txt -------------------------------------------------------------------------------- /routeWeb.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 minRAG Authors. 2 | // 3 | // This file is part of minRAG. 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package main 19 | 20 | import ( 21 | "context" 22 | "encoding/json" 23 | "fmt" 24 | "net/http" 25 | "strings" 26 | 27 | "github.com/cloudwego/hertz/pkg/app" 28 | "github.com/cloudwego/hertz/pkg/protocol/http1/resp" 29 | ) 30 | 31 | // init 初始化函数 32 | func init() { 33 | 34 | //初始化静态文件 35 | initStaticFS() 36 | 37 | // 异常页面 38 | h.GET("/error", funcError) 39 | 40 | // 默认首页 41 | h.GET("/", funcIndex) 42 | 43 | // agent 页面 44 | h.GET("/agent/:agentID", funcAgentPre) 45 | 46 | // 兼容OpenAI模型接口,api_key是agentID,user是roomID 47 | h.POST("/v1/chat/completions", funcChatCompletions) 48 | } 49 | 50 | // funcIndex 模板首页 51 | func funcIndex(ctx context.Context, c *app.RequestContext) { 52 | data := warpRequestMap(c) 53 | cHtml(c, http.StatusOK, "index.html", data) 54 | } 55 | 56 | // funcError 错误页面 57 | func funcError(ctx context.Context, c *app.RequestContext) { 58 | cHtml(c, http.StatusOK, "error.html", nil) 59 | } 60 | 61 | // funcAgentPre 智能体 62 | func funcAgentPre(ctx context.Context, c *app.RequestContext) { 63 | data := warpRequestMap(c) 64 | agentID := c.Param("agentID") 65 | data["agentID"] = agentID 66 | cHtml(c, http.StatusOK, "agent.html", data) 67 | } 68 | 69 | // funcChatCompletions 兼容OpenAI模型接口,api_key是agentID,user是roomID 70 | func funcChatCompletions(ctx context.Context, c *app.RequestContext) { 71 | 72 | // 设置响应头 73 | c.SetStatusCode(http.StatusOK) 74 | 75 | accept := string(c.GetHeader("Accept")) 76 | stream := strings.Contains(strings.ToLower(accept), "text/event-stream") 77 | 78 | aByte := c.GetHeader("Authorization") 79 | if len(aByte) < 1 { 80 | errMsg := "Authorization is empty" 81 | if stream { 82 | c.WriteString(warpOpenAIJsonMessage(stream, errMsg)) 83 | c.Flush() 84 | c.WriteString("data: [DONE]\n\n") 85 | c.Flush() 86 | } else { 87 | c.WriteString(errMsg) 88 | c.Flush() 89 | } 90 | c.Abort() 91 | return 92 | } 93 | authorization := string(aByte) 94 | agentID := strings.TrimPrefix(authorization, "Bearer ") 95 | if agentID == "" { 96 | errMsg := "Authorization is empty" 97 | if stream { 98 | c.WriteString(warpOpenAIJsonMessage(stream, errMsg)) 99 | c.Flush() 100 | c.WriteString("data: [DONE]\n\n") 101 | c.Flush() 102 | } else { 103 | c.WriteString(errMsg) 104 | c.Flush() 105 | } 106 | c.Abort() 107 | return 108 | } 109 | 110 | agentRequestBody := &AgentRequestBody{} 111 | err := c.BindJSON(agentRequestBody) 112 | if err != nil { 113 | errMsg := fmt.Sprintf("body is error:%v", err) 114 | if stream { 115 | c.WriteString(warpOpenAIJsonMessage(stream, errMsg)) 116 | c.Flush() 117 | c.WriteString("data: [DONE]\n\n") 118 | c.Flush() 119 | } else { 120 | c.WriteString(errMsg) 121 | c.Flush() 122 | } 123 | } 124 | if len(agentRequestBody.Messages) < 1 { 125 | errMsg := "messages is empty" 126 | if stream { 127 | c.WriteString(warpOpenAIJsonMessage(stream, errMsg)) 128 | c.Flush() 129 | c.WriteString("data: [DONE]\n\n") 130 | c.Flush() 131 | } else { 132 | c.WriteString(errMsg) 133 | c.Flush() 134 | } 135 | c.Abort() 136 | return 137 | } 138 | input := make(map[string]interface{}, 0) 139 | // 用户发送的第一个消息 140 | input["query"] = agentRequestBody.Messages[0].Content 141 | // agentID 142 | input["agentID"] = agentID 143 | // 获取roomID,可能会空 144 | roomID := agentRequestBody.User 145 | if roomID != "" && len(roomID) == 32 { 146 | input["roomID"] = roomID 147 | } 148 | 149 | if stream { 150 | c.Header("Accept", "text/event-stream") 151 | c.Header("Cache-Control", "no-cache") 152 | c.Header("Connection", "keep-alive") 153 | writer := resp.NewChunkedBodyWriter(&c.Response, c.GetWriter()) 154 | c.Response.HijackWriter(writer) 155 | } 156 | input["c"] = c 157 | 158 | agent, err := findAgentByID(ctx, agentID) 159 | if err != nil || agent.Id == "" { 160 | errMsg := "agent is empty" 161 | if stream { 162 | c.WriteString(warpOpenAIJsonMessage(stream, errMsg)) 163 | c.Flush() 164 | c.WriteString("data: [DONE]\n\n") 165 | c.Flush() 166 | } else { 167 | c.WriteString(errMsg) 168 | c.Flush() 169 | } 170 | c.Abort() 171 | return 172 | } 173 | 174 | input["knowledgeBaseID"] = agent.KnowledgeBaseID 175 | pipeline := componentMap[agent.PipelineID] 176 | pipeline.Run(ctx, input) 177 | //choice := input["choice"] 178 | errObj := input[errorKey] 179 | if errObj != nil { 180 | errMsg := fmt.Sprintf("component run is error:%v", errObj) 181 | if stream { 182 | msg := warpOpenAIJsonMessage(stream, errMsg) 183 | c.WriteString(msg) 184 | c.Flush() 185 | c.WriteString("data: [DONE]\n\n") 186 | c.Flush() 187 | } else { 188 | msg := warpOpenAIJsonMessage(stream, errMsg) 189 | c.WriteString(msg) 190 | c.Flush() 191 | } 192 | c.Abort() 193 | return 194 | } 195 | 196 | //fmt.Println(choice) 197 | //c.JSON(http.StatusOK, ResponseData{StatusCode: 1, Data: choice}) 198 | } 199 | 200 | // warpRequestMap 包装请求参数为map 201 | func warpRequestMap(c *app.RequestContext) map[string]interface{} { 202 | data := make(map[string]interface{}, 0) 203 | jwttoken := string(c.Cookie(config.JwttokenKey)) 204 | userId, _ := userIdByToken(jwttoken) 205 | if userId != "" { 206 | data[userTypeKey] = 1 207 | } else { 208 | data[userTypeKey] = 0 209 | } 210 | return data 211 | } 212 | 213 | // warpOpenAIJsonMessage 包装需要返回的OpenAI json 格式的信息 214 | func warpOpenAIJsonMessage(stream bool, content string) string { 215 | rs := struct { 216 | Choices []Choice `json:"choices,omitempty"` 217 | }{Choices: make([]Choice, 1)} 218 | if stream { 219 | rs.Choices[0].Delta.Content = content 220 | } else { 221 | rs.Choices[0].Message.Content = content 222 | } 223 | //大模型返回的json对象,进行接受 224 | data, err := json.Marshal(rs) 225 | jsonStr := string(data) 226 | if err != nil { 227 | jsonStr = "" 228 | } 229 | if stream { 230 | if jsonStr == "" { 231 | jsonStr = "{}" 232 | } 233 | jsonStr = "data: " + jsonStr + "\n\n" 234 | } 235 | return jsonStr 236 | } 237 | -------------------------------------------------------------------------------- /serviceAgent.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 minRAG Authors. 2 | // 3 | // This file is part of minRAG. 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package main 19 | 20 | import ( 21 | "context" 22 | 23 | "gitee.com/chunanyong/zorm" 24 | ) 25 | 26 | // AgentRequestBody 请求Agent的参数 27 | type AgentRequestBody struct { 28 | Model string `json:"model,omitempty"` 29 | Messages []ChatMessage `json:"messages,omitempty"` 30 | Stream bool `json:"stream,omitempty"` 31 | User string `json:"user,omitempty"` 32 | } 33 | 34 | // findAllAgentList 查询所有的智能体 35 | func findAllAgentList(ctx context.Context) ([]Agent, error) { 36 | finder := zorm.NewSelectFinder(tableAgentName).Append("order by sortNo desc") 37 | list := make([]Agent, 0) 38 | err := zorm.Query(ctx, finder, &list, nil) 39 | return list, err 40 | } 41 | 42 | // findAgentByID 查询Agent 43 | func findAgentByID(ctx context.Context, agentID string) (*Agent, error) { 44 | finder := zorm.NewSelectFinder(tableAgentName).Append("WHERE id=? and status=1", agentID) 45 | agent := &Agent{} 46 | _, err := zorm.QueryRow(ctx, finder, agent) 47 | return agent, err 48 | } 49 | -------------------------------------------------------------------------------- /serviceConfig.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 minRAG Authors. 2 | // 3 | // This file is part of minRAG. 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package main 19 | 20 | import ( 21 | "context" 22 | "encoding/json" 23 | "errors" 24 | "io" 25 | "os" 26 | 27 | "gitee.com/chunanyong/zorm" 28 | ) 29 | 30 | // loadInstallConfig 加载配置文件,只有初始化安装时需要读取配置文件,读取后,就写入表,通过后台管理,然后重命名为 install_config.json_配置已失效_请通过后台设置管理 31 | func loadInstallConfig() (Config, Site) { 32 | var site = Site{Theme: "default"} 33 | defaultErr := errors.New(funcT("Failed to load install_config.json, using default configuration")) 34 | if installed { // 已经安装,需要表读取配置 35 | var err error 36 | site, err = funcSite() 37 | if err != nil { 38 | return defaultConfig, site 39 | } 40 | config, err := findConfig() 41 | if err != nil { 42 | return defaultConfig, site 43 | } 44 | return config, site 45 | } 46 | // 打开文件 47 | jsonFile, err := os.Open(datadir + "install_config.json") 48 | if err != nil { 49 | FuncLogError(nil, defaultErr) 50 | return defaultConfig, site 51 | } 52 | // 关闭文件 53 | defer jsonFile.Close() 54 | byteValue, err := io.ReadAll(jsonFile) 55 | if err != nil { 56 | FuncLogError(nil, defaultErr) 57 | return defaultConfig, site 58 | } 59 | configJson := Config{} 60 | // Decode从输入流读取下一个json编码值并保存在v指向的值里 61 | err = json.Unmarshal([]byte(byteValue), &configJson) 62 | if err != nil { 63 | FuncLogError(nil, defaultErr) 64 | return defaultConfig, site 65 | } 66 | 67 | if configJson.JwtSecret == "" { // 如果没有配置jwtSecret,产生随机字符串 68 | configJson.JwtSecret = randStr(32) 69 | } 70 | if configJson.BasePath == "" { 71 | configJson.BasePath = "/" 72 | } 73 | if configJson.Locale == "" { 74 | configJson.Locale = "zh-CN" 75 | } 76 | configJson.Id = defaultConfig.Id 77 | 78 | return configJson, site 79 | } 80 | 81 | var defaultConfig = Config{ 82 | Id: "minrag_config", 83 | BasePath: "/", 84 | // 默认的加密Secret 85 | // JwtSecret: "minrag+jwtSecret-2023", 86 | JwtSecret: randStr(32), 87 | //Theme: "default", 88 | MaxRequestBodySize: 20 * 1024 * 1024, 89 | JwttokenKey: "jwttoken", // jwt的key 90 | Timeout: 7200, // 两小时超时 91 | ServerPort: ":738", // minrag: 109 + 105 + 110 + 114 + 97 + 103 = 738 92 | Locale: "zh-CN", 93 | } 94 | 95 | // insertConfig 插入config 96 | func insertConfig(ctx context.Context) error { 97 | //数据库存在config,不更新数据库,更新config变量 98 | finder := zorm.NewSelectFinder(tableConfigName).Append("WHERE id=?", "minrag_config") 99 | c := Config{} 100 | has, err := zorm.QueryRow(ctx, finder, &c) 101 | if has && err == nil && c.Id != "" { 102 | config = c 103 | return err 104 | } 105 | 106 | // 清空配置,重新创建 107 | deleteAll(ctx, tableConfigName) 108 | _, err = zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { 109 | return zorm.Insert(ctx, &config) 110 | }) 111 | 112 | return err 113 | } 114 | 115 | // updateConfigAI 安装时更新AI配置 116 | func updateConfigAI(ctx context.Context, aiBaseURL string, aiAPIKey string) error { 117 | if aiBaseURL == "" || aiAPIKey == "" { 118 | return nil 119 | } 120 | _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { 121 | finder := zorm.NewUpdateFinder(tableConfigName).Append("aiBaseURL=?,aiAPIKey=? WHERE id=?", aiBaseURL, aiAPIKey, "minrag_config") 122 | return zorm.UpdateFinder(ctx, finder) 123 | }) 124 | if err != nil { 125 | return err 126 | } 127 | config, err = findConfig() 128 | return err 129 | } 130 | 131 | // findConfig 查询配置 132 | func findConfig() (Config, error) { 133 | 134 | finder := zorm.NewSelectFinder(tableConfigName) 135 | 136 | m, err := zorm.QueryRowMap(context.Background(), finder) 137 | 138 | config := defaultConfig 139 | if err != nil { 140 | return config, err 141 | } 142 | b, err := json.Marshal(m) 143 | if err != nil { 144 | return config, err 145 | } 146 | json.Unmarshal(b, &config) 147 | 148 | if config.BasePath == "" { 149 | config.BasePath = "/" 150 | } 151 | if config.MaxRequestBodySize == 0 { 152 | config.MaxRequestBodySize = 20 * 1024 * 1024 153 | } 154 | if config.Locale == "" { 155 | config.Locale = "zh-CN" 156 | } 157 | return config, nil 158 | } 159 | -------------------------------------------------------------------------------- /serviceDocument.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 minRAG Authors. 2 | // 3 | // This file is part of minRAG. 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package main 19 | 20 | import ( 21 | "context" 22 | "errors" 23 | "strings" 24 | 25 | "gitee.com/chunanyong/zorm" 26 | ) 27 | 28 | // updateDocumentChunk 运行indexPipeline流水线,更新Document,DocumentChunk,VecDocumentChunk 29 | func updateDocumentChunk(ctx context.Context, document *Document) (bool, error) { 30 | input := make(map[string]interface{}) 31 | input["document"] = document 32 | indexPipeline, has := componentMap["indexPipeline"] 33 | if !has || indexPipeline == nil { 34 | return false, errors.New("indexPipeline is empty") 35 | } 36 | err := indexPipeline.Run(ctx, input) 37 | if err != nil { 38 | return false, err 39 | } 40 | errObj, has := input[errorKey] 41 | if has || errObj != nil { 42 | return false, errObj.(error) 43 | } 44 | 45 | return true, nil 46 | 47 | } 48 | 49 | // findDocumentIdByFilePath 根据文档路径查询文档ID 50 | func findDocumentIdByFilePath(ctx context.Context, filePath string) (string, error) { 51 | finder := zorm.NewSelectFinder(tableDocumentName, "id").Append("WHERE filePath=?", filePath) 52 | id := "" 53 | _, err := zorm.QueryRow(ctx, finder, &id) 54 | return id, err 55 | } 56 | 57 | // findDocumentChunkMarkDown 查询DocumentChunk的markdown 58 | func findDocumentChunkMarkDown(ctx context.Context, documentChunks []DocumentChunk) ([]DocumentChunk, error) { 59 | for i := 0; i < len(documentChunks); i++ { 60 | documentChunk := documentChunks[i] 61 | finder := zorm.NewSelectFinder(tableDocumentChunkName, "markdown").Append("WHERE id=?", documentChunk.Id) 62 | markdown := "" 63 | _, err := zorm.QueryRow(ctx, finder, &markdown) 64 | if err != nil { 65 | return documentChunks, err 66 | } 67 | documentChunks[i].Markdown = markdown 68 | } 69 | 70 | return documentChunks, nil 71 | } 72 | 73 | // funcDeleteDocumentById 根据文档ID删除 Document,DocumentChunk,VecDocumentChunk 74 | func funcDeleteDocumentById(ctx context.Context, id string) error { 75 | _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { 76 | f1 := zorm.NewDeleteFinder(tableDocumentName).Append("WHERE id=?", id) 77 | count, err := zorm.UpdateFinder(ctx, f1) 78 | if err != nil { 79 | return count, err 80 | } 81 | f2 := zorm.NewDeleteFinder(tableDocumentChunkName).Append("WHERE documentID=?", id) 82 | count, err = zorm.UpdateFinder(ctx, f2) 83 | if err != nil { 84 | return count, err 85 | } 86 | f3 := zorm.NewDeleteFinder(tableVecDocumentChunkName).Append("WHERE documentID=?", id) 87 | return zorm.UpdateFinder(ctx, f3) 88 | }) 89 | return err 90 | } 91 | 92 | // recursiveSplit 递归分割实现 93 | func (component *DocumentSplitter) recursiveSplit(text string, depth int) []string { 94 | chunks := make([]string, 0) 95 | // 终止条件:处理完所有分隔符 96 | if depth >= len(component.SplitBy) { 97 | if text != "" { 98 | return append(chunks, text) 99 | } 100 | return chunks 101 | } 102 | 103 | currentSep := component.SplitBy[depth] 104 | parts := strings.Split(text, currentSep) 105 | for i, part := range parts { 106 | part = strings.TrimSpace(part) 107 | if part == "" { 108 | continue 109 | } 110 | partContent := part 111 | if i < len(parts)-1 { //不是最后一个 112 | partContent = partContent + currentSep 113 | } 114 | 115 | // 处理超长内容 116 | if len(part) >= component.SplitLength { 117 | partLeaf := component.recursiveSplit(partContent, depth+1) 118 | if len(partLeaf) > 0 { 119 | chunks = append(chunks, partLeaf...) 120 | } 121 | continue 122 | } else { 123 | chunks = append(chunks, partContent) 124 | } 125 | } 126 | return chunks 127 | } 128 | 129 | // mergeChunks 合并短内容 130 | func (component *DocumentSplitter) mergeChunks(chunks []string) []string { 131 | // 合并短内容 132 | for i := 0; i < len(chunks); i++ { 133 | chunk := chunks[i] 134 | if len(chunk) >= component.SplitLength || i+1 >= len(chunks) { 135 | continue 136 | } 137 | nextChunk := chunks[i+1] 138 | 139 | // 汉字字符占位3个长度 140 | if (len(chunk) + len(nextChunk)) > (component.SplitLength*18)/10 { 141 | continue 142 | } 143 | chunks[i] = chunk + nextChunk 144 | if i+2 >= len(chunks) { //倒数第二个元素,去掉最后一个 145 | chunks = chunks[:len(chunks)-1] 146 | } else { // 去掉 i+1 索引元素,合并到了 i 索引 147 | chunks = append(chunks[:i+1], chunks[i+2:]...) 148 | } 149 | } 150 | return chunks 151 | } 152 | -------------------------------------------------------------------------------- /serviceKnowledgeBase.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 minRAG Authors. 2 | // 3 | // This file is part of minRAG. 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package main 19 | 20 | import ( 21 | "context" 22 | "errors" 23 | 24 | "gitee.com/chunanyong/zorm" 25 | ) 26 | 27 | // validateIDExists 校验ID是否已经存在 28 | func validateIDExists(ctx context.Context, id string) bool { 29 | id = funcTrimSuffixSlash(id) 30 | if id == "" { 31 | return true 32 | } 33 | 34 | f1 := zorm.NewSelectFinder(tableDocumentName, "id").Append("Where id=?", id) 35 | cid := "" 36 | zorm.QueryRow(ctx, f1, &cid) 37 | if cid != "" { 38 | return true 39 | } 40 | id = id + "/" 41 | f2 := zorm.NewSelectFinder(tableKnowledgeBaseName, "id").Append("Where id=?", id) 42 | zorm.QueryRow(ctx, f2, &cid) 43 | return cid != "" 44 | 45 | } 46 | 47 | // findKnowledgeBaseNameById 根据知识库ID查找知识库名称 48 | func findKnowledgeBaseNameById(ctx context.Context, knowledgeBaseId string) (string, error) { 49 | if knowledgeBaseId == "" { 50 | return "", errors.New(funcT("Knowledge base cannot be empty")) 51 | } 52 | finder := zorm.NewSelectFinder(tableKnowledgeBaseName, "name").Append("WHERE id=?", knowledgeBaseId) 53 | knowledgeBaseName := "" 54 | _, err := zorm.QueryRow(ctx, finder, &knowledgeBaseName) 55 | return knowledgeBaseName, err 56 | } 57 | -------------------------------------------------------------------------------- /serviceUser.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 minRAG Authors. 2 | // 3 | // This file is part of minRAG. 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package main 19 | 20 | import ( 21 | "context" 22 | 23 | "gitee.com/chunanyong/zorm" 24 | ) 25 | 26 | // insertUser 插入用户 27 | func insertUser(ctx context.Context, user User) error { 28 | // 清空用户,只能有一个管理员 29 | deleteAll(ctx, tableUserName) 30 | // 初始化数据 31 | user.Id = "minrag_admin" 32 | user.SortNo = 1 33 | user.Status = 1 34 | _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { 35 | return zorm.Insert(ctx, &user) 36 | }) 37 | return err 38 | } 39 | 40 | // findUserId 查询用户ID 41 | func findUserId(ctx context.Context, account string, password string) (string, error) { 42 | finder := zorm.NewSelectFinder(tableUserName, "id").Append(" WHERE account=? and password=?", account, password) 43 | userId := "" 44 | _, err := zorm.QueryRow(ctx, finder, &userId) 45 | return userId, err 46 | } 47 | -------------------------------------------------------------------------------- /utilCaptcha.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 minRAG Authors. 2 | // 3 | // This file is part of minRAG. 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package main 19 | 20 | import ( 21 | "image/color" 22 | "math/rand" 23 | "sync" 24 | "sync/atomic" 25 | 26 | "github.com/mojocn/base64Captcha" 27 | ) 28 | 29 | const errCount = 3 30 | 31 | // 配置验证码的参数 32 | var driverString = base64Captcha.DriverString{ 33 | Height: 60, 34 | Width: 200, 35 | NoiseCount: 0, 36 | ShowLineOptions: 2 | 4, 37 | Length: 4, 38 | Source: "34678acdefghkmnprtuvwxy", 39 | BgColor: &color.RGBA{R: 3, G: 102, B: 214, A: 125}, 40 | Fonts: []string{"wqy-microhei.ttc"}, 41 | } 42 | 43 | // var driver = base64Captcha.NewDriverMath(60, 200, 0, base64Captcha.OptionShowHollowLine, nil, nil, []string{"wqy-microhei.ttc"}) 44 | var driver = driverString.ConvertFonts() 45 | 46 | var captchaQuestion, captchaAnswer, captchaBase64 string 47 | var captchaLock = &sync.Mutex{} 48 | 49 | var errorLoginCount atomic.Uint32 50 | 51 | // generateCaptcha 生成随机验证码 52 | func generateCaptcha() { 53 | captchaLock.Lock() 54 | defer captchaLock.Unlock() 55 | _, captchaQuestion, captchaAnswer = driver.GenerateIdQuestionAnswer() 56 | item, err := driver.DrawCaptcha(captchaQuestion) 57 | if err != nil { 58 | captchaBase64 = "" 59 | return 60 | } 61 | captchaBase64 = item.EncodeB64string() 62 | } 63 | 64 | // randStr 生成随机字符串 65 | func randStr(n int) string { 66 | //rand.Seed(time.Now().UnixNano()) 67 | b := make([]byte, n) 68 | for i := range b { 69 | b[i] = letters[rand.Intn(len(letters))] 70 | } 71 | return string(b) 72 | } 73 | -------------------------------------------------------------------------------- /utilFunctionCalling.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 minRAG Authors. 2 | // 3 | // This file is part of minRAG. 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package main 19 | 20 | import ( 21 | "context" 22 | "encoding/json" 23 | ) 24 | 25 | var functionCallingMap = make(map[string]IToolFunctionCalling, 0) 26 | 27 | func init() { 28 | fcWeather := FCWeather{} 29 | fc, err := fcWeather.Initialization(context.TODO(), get_weather_json) 30 | if err == nil { 31 | functionCallingMap["get_weather"] = fc 32 | } 33 | } 34 | 35 | // IToolFunctionCalling 函数调用接口 36 | type IToolFunctionCalling interface { 37 | // Initialization 初始化方法 38 | Initialization(ctx context.Context, descriptionJson string) (IToolFunctionCalling, error) 39 | //获取描述的Map 40 | Description(ctx context.Context) interface{} 41 | // Run 执行方法 42 | Run(ctx context.Context, arguments string) (string, error) 43 | } 44 | 45 | var get_weather_json = `{ 46 | "type": "function", 47 | "function": { 48 | "name": "get_weather", 49 | "description": "Get weather of an location, the user shoud supply a location first", 50 | "parameters": { 51 | "type": "object", 52 | "properties": { 53 | "location": { 54 | "type": "string", 55 | "description": "The city and state, e.g. San Francisco, CA" 56 | } 57 | }, 58 | "required": ["location"] 59 | } 60 | } 61 | }` 62 | 63 | // FCWeather 天气函数 64 | type FCWeather struct { 65 | //接受模型返回的 arguments 66 | Location string `json:"location,omitempty"` 67 | DescriptionMap map[string]interface{} `json:"-"` 68 | } 69 | 70 | func (fc FCWeather) Initialization(ctx context.Context, descriptionJson string) (IToolFunctionCalling, error) { 71 | dm := make(map[string]interface{}) 72 | if descriptionJson == "" { 73 | return fc, nil 74 | } 75 | err := json.Unmarshal([]byte(descriptionJson), &dm) 76 | if err != nil { 77 | return fc, err 78 | } 79 | fc.DescriptionMap = dm 80 | return fc, nil 81 | } 82 | 83 | // 获取描述的Map 84 | func (fc FCWeather) Description(ctx context.Context) interface{} { 85 | return fc.DescriptionMap 86 | } 87 | 88 | // Run 执行方法 89 | func (fc FCWeather) Run(ctx context.Context, arguments string) (string, error) { 90 | if arguments != "" { 91 | err := json.Unmarshal([]byte(arguments), &fc) 92 | if err != nil { 93 | return "", nil 94 | } 95 | } 96 | return fc.Location + "的气温是25度", nil 97 | } 98 | -------------------------------------------------------------------------------- /utilGenStaticFile.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 minRAG Authors. 2 | // 3 | // This file is part of minRAG. 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package main 19 | 20 | import ( 21 | "bufio" 22 | "bytes" 23 | "compress/gzip" 24 | "context" 25 | "encoding/hex" 26 | "io" 27 | "net/http" 28 | "os" 29 | "path/filepath" 30 | "strconv" 31 | "strings" 32 | "sync" 33 | 34 | "gitee.com/chunanyong/zorm" 35 | "golang.org/x/crypto/sha3" 36 | ) 37 | 38 | // onlyOnce控制并发 39 | // var onlyOnce = make(chan struct{}, 1) 40 | var genStaticHtmlLock = &sync.Mutex{} 41 | 42 | // genStaticFile 生成全站静态文件和gzip文件,包括静态的html和search-data.json 43 | func genStaticFile() error { 44 | genStaticHtmlLock.Lock() 45 | defer genStaticHtmlLock.Unlock() 46 | 47 | ctx := context.Background() 48 | documents := make([]Document, 0) 49 | 50 | f_post := zorm.NewSelectFinder(tableDocumentName, "id").Append(" WHERE status=1 order by status desc, sortNo desc") 51 | err := zorm.Query(ctx, f_post, &documents, nil) 52 | if err != nil { 53 | return err 54 | } 55 | //生成知识库的静态网页 56 | knowledgeBaseIDs := make([]string, 0) 57 | f_knowledgeBase := zorm.NewSelectFinder(tableKnowledgeBaseName, "id").Append(" WHERE status=1 order by status desc,sortNo desc") 58 | err = zorm.Query(ctx, f_knowledgeBase, &knowledgeBaseIDs, nil) 59 | if err != nil { 60 | return err 61 | } 62 | //删除整个目录 63 | os.RemoveAll(staticHtmlDir) 64 | 65 | // 生成 default,pc,wap,weixin 等平台的静态文件 66 | useThemes := map[string]bool{} 67 | useThemes[""] = true 68 | err = genStaticFileByTheme(documents, knowledgeBaseIDs, site.Theme, "") 69 | if err != nil { 70 | FuncLogError(ctx, err) 71 | //return err 72 | } 73 | useThemes[site.Theme] = true 74 | _, has := useThemes[site.ThemePC] 75 | //生成PC模板的静态网页 76 | if !has { 77 | err = genStaticFileByTheme(documents, knowledgeBaseIDs, site.ThemePC, "Mozilla/5.0 (Windows NT 10.0; Win64; x64)") 78 | if err != nil { 79 | FuncLogError(ctx, err) 80 | //return err 81 | } 82 | useThemes[site.ThemePC] = true 83 | } 84 | 85 | // 生成手机WAP模板的静态网页 86 | _, has = useThemes[site.ThemeWAP] 87 | if !has { 88 | err = genStaticFileByTheme(documents, knowledgeBaseIDs, site.ThemeWAP, "Mozilla/5.0 (Linux; Android 13;) Mobile") 89 | if err != nil { 90 | FuncLogError(ctx, err) 91 | //return err 92 | } 93 | useThemes[site.ThemeWAP] = true 94 | } 95 | //生成微信WX模板的静态网页 96 | _, has = useThemes[site.ThemeWX] 97 | if !has { 98 | err = genStaticFileByTheme(documents, knowledgeBaseIDs, site.ThemeWX, "Mozilla/5.0 (Linux; Android 13;) Mobile MicroMessenger WeChat Weixin") 99 | if err != nil { 100 | FuncLogError(ctx, err) 101 | //return err 102 | } 103 | useThemes[site.ThemeWX] = true 104 | } 105 | 106 | return err 107 | } 108 | 109 | // genStaticFileByTheme 根据主题模板,生成静态文件 110 | func genStaticFileByTheme(documents []Document, categories []string, theme string, userAgent string) error { 111 | //生成首页index网页 112 | fileHash, _, err := writeStaticHtml("", "", theme, userAgent) 113 | if fileHash == "" || err != nil { 114 | return err 115 | } 116 | 117 | //上一个分页 118 | prvePageFileHash := "" 119 | //生成文章的静态网页 120 | for i := 0; i < len(documents); i++ { 121 | //postURL := httpServerPath + "post/" + postId 122 | fileHash, _, err := writeStaticHtml(funcTrimPrefixSlash(documents[i].Id), "", theme, userAgent) 123 | if fileHash == "" || err != nil { 124 | continue 125 | } 126 | 127 | fileHash, _, err = writeStaticHtml("page/"+strconv.Itoa(i+1), prvePageFileHash, theme, userAgent) 128 | if fileHash == "" || err != nil { 129 | continue 130 | } 131 | 132 | //如果hash完全一致,认为是最后一页 133 | prvePageFileHash = fileHash 134 | } 135 | 136 | for i := 0; i < len(categories); i++ { 137 | //生成知识库首页index 138 | fileHash, _, err := writeStaticHtml(funcTrimSlash(categories[i]), "", theme, userAgent) 139 | if fileHash == "" || err != nil { 140 | return err 141 | } 142 | 143 | for j := 0; j < len(documents); j++ { 144 | fileHash, _, err := writeStaticHtml(funcTrimSlash(categories[i])+"/page/"+strconv.Itoa(j+1), prvePageFileHash, theme, userAgent) 145 | if fileHash == "" || err != nil { 146 | continue 147 | } 148 | 149 | //如果hash完全一致,认为是最后一页 150 | prvePageFileHash = fileHash 151 | } 152 | } 153 | //遍历当前使用的模板文件夹,压缩文本格式的文件 154 | err = filepath.Walk(templateDir+"theme/"+theme+"/", func(path string, info os.FileInfo, err error) error { 155 | if err != nil { 156 | return err 157 | } 158 | // 分隔符统一为 / 斜杠 159 | path = filepath.ToSlash(path) 160 | 161 | // 只处理 js 和 css 文件夹 162 | if !(strings.Contains(path, "/js/") || strings.Contains(path, "/css/")) { 163 | return nil 164 | } 165 | 166 | //获取文件后缀 167 | suffix := filepath.Ext(path) 168 | 169 | // 压缩 js,mjs,json,css,html 170 | // 压缩字体文件 ttf,otf,svg gzip_types font/ttf font/otf image/svg+xml 171 | if !(suffix == ".js" || suffix == ".mjs" || suffix == ".json" || suffix == ".css" || suffix == ".html" || suffix == ".ttf" || suffix == ".otf" || suffix == ".svg") { 172 | return nil 173 | } 174 | 175 | // 获取要打包的文件信息 176 | readFile, err := os.Open(path) 177 | if err != nil { 178 | return err 179 | } 180 | defer readFile.Close() 181 | reader := bufio.NewReader(readFile) 182 | //压缩文件 183 | err = doGzipFile(path+compressedFileSuffix, reader) 184 | 185 | return err 186 | }) 187 | return err 188 | } 189 | 190 | // writeStaticHtml 写入静态html 191 | func writeStaticHtml(urlFilePath string, fileHash string, theme string, userAgent string) (string, bool, error) { 192 | httpurl := httpServerPath + urlFilePath 193 | filePath := staticHtmlDir + theme + funcBasePath() + urlFilePath 194 | if urlFilePath != "" { 195 | filePath = filePath + "/" 196 | } 197 | client := &http.Client{} 198 | req, err := http.NewRequest("GET", httpurl, nil) 199 | if err != nil { 200 | 201 | return "", false, err 202 | } 203 | 204 | // 设置请求头 205 | if userAgent != "" { 206 | req.Header.Set("User-Agent", userAgent) 207 | } 208 | response, err := client.Do(req) 209 | if err != nil { 210 | return "", false, err 211 | } 212 | defer response.Body.Close() 213 | 214 | // 读取资源数据 body: []byte 215 | body, err := io.ReadAll(response.Body) 216 | // 关闭资源流 217 | response.Body.Close() 218 | if err != nil { 219 | return "", false, err 220 | } 221 | //计算hash 222 | bytehex := sha3.Sum256(body) 223 | bodyHash := hex.EncodeToString(bytehex[:]) 224 | if bodyHash == fileHash { //如果hash一致,不再生成文件 225 | return bodyHash, false, nil 226 | } 227 | // 写入文件 228 | os.MkdirAll(filePath, os.ModePerm) 229 | err = os.WriteFile(filePath+"index.html", body, os.ModePerm) 230 | if err != nil { 231 | return bodyHash, false, err 232 | } 233 | // 压缩gzip文件 234 | err = doGzipFile(filePath+"index.html"+compressedFileSuffix, bytes.NewReader(body)) 235 | if err != nil { 236 | return bodyHash, false, err 237 | } 238 | return bodyHash, true, nil 239 | } 240 | 241 | // doGzipFile 压缩gzip文件 242 | func doGzipFile(gzipFilePath string, reader io.Reader) error { 243 | 244 | //如果文件存在就删除 245 | if pathExist(gzipFilePath) { 246 | os.Remove(gzipFilePath) 247 | } 248 | //创建文件 249 | gzipFile, err := os.Create(gzipFilePath) 250 | if err != nil { 251 | return err 252 | } 253 | defer gzipFile.Close() 254 | 255 | gzipWrite, err := gzip.NewWriterLevel(gzipFile, gzip.BestCompression) 256 | if err != nil { 257 | return err 258 | } 259 | defer gzipWrite.Close() 260 | _, err = io.Copy(gzipWrite, reader) 261 | return err 262 | } 263 | -------------------------------------------------------------------------------- /utilHttp.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 minRAG Authors. 2 | // 3 | // This file is part of minRAG. 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package main 19 | 20 | import ( 21 | "bytes" 22 | "encoding/json" 23 | "errors" 24 | "fmt" 25 | "io" 26 | "net/http" 27 | "os" 28 | ) 29 | 30 | // httpPostJsonBody 使用Post发送Json请求 31 | func httpPostJsonBody(client *http.Client, authorization string, url string, header map[string]string, bodyMap map[string]interface{}) ([]byte, error) { 32 | resp, err := httpPostJsonResponse(client, authorization, url, header, bodyMap) 33 | if err != nil { 34 | return nil, err 35 | } 36 | defer resp.Body.Close() 37 | 38 | // 读取响应体内容 39 | body, err := io.ReadAll(resp.Body) 40 | if err != nil { 41 | return nil, err 42 | } 43 | if len(body) < 1 { 44 | return nil, errors.New("body is empty") 45 | } 46 | 47 | return body, nil 48 | } 49 | 50 | // httpPostJsonResponse post请求的response 51 | func httpPostJsonResponse(client *http.Client, authorization string, url string, header map[string]string, bodyMap map[string]interface{}) (*http.Response, error) { 52 | if client == nil { 53 | return nil, errors.New("httpClient is nil") 54 | } 55 | // 序列化请求体 56 | payloadBytes, err := json.Marshal(bodyMap) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | // 创建HTTP请求 62 | req, err := http.NewRequest("POST", url, bytes.NewBuffer(payloadBytes)) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | // 设置请求头 68 | if authorization != "" { 69 | req.Header.Set("Authorization", "Bearer "+authorization) 70 | } 71 | req.Header.Set("Content-Type", "application/json") 72 | if len(header) > 0 { 73 | for k, v := range header { 74 | req.Header.Set(k, v) 75 | } 76 | } 77 | resp, err := client.Do(req) 78 | if err != nil { 79 | return resp, err 80 | } 81 | // 保留错误信息 82 | //if resp.StatusCode >= 400 { 83 | // return nil, fmt.Errorf("HTTP error: %s", resp.Status) 84 | //} 85 | // 检查状态码 86 | if resp.StatusCode != http.StatusOK { 87 | bodyByte, _ := io.ReadAll(resp.Body) 88 | resp.Body.Close() 89 | return nil, errors.New(string(bodyByte)) 90 | } 91 | return resp, err 92 | } 93 | 94 | // httpUploadFile http上传附件 95 | func httpUploadFile(client *http.Client, method string, url string, filePath string, header map[string]string) ([]byte, error) { 96 | if client == nil { 97 | return nil, errors.New("httpClient is nil") 98 | } 99 | if filePath == "" { 100 | return nil, errors.New("filePath is empty") 101 | } 102 | if method == "" { 103 | method = "POST" 104 | } 105 | // 打开文件 106 | file, err := os.Open(filePath) 107 | if err != nil { 108 | return nil, err 109 | } 110 | defer file.Close() 111 | 112 | // 创建请求体 113 | bodyBuffer := &bytes.Buffer{} 114 | if _, err := io.Copy(bodyBuffer, file); err != nil { 115 | return nil, fmt.Errorf("read file error: %w", err) 116 | } 117 | 118 | // 创建请求 119 | req, err := http.NewRequest(method, url, bodyBuffer) 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | if len(header) > 0 { 125 | for k, v := range header { 126 | req.Header.Set(k, v) 127 | } 128 | } 129 | resp, err := client.Do(req) 130 | if err != nil { 131 | return nil, err 132 | } 133 | // 关闭resp.Body 134 | defer resp.Body.Close() 135 | // 保留错误信息 136 | //if resp.StatusCode >= 400 { 137 | // return nil, fmt.Errorf("HTTP error: %s", resp.Status) 138 | //} 139 | // 读取响应体内容 140 | body, err := io.ReadAll(resp.Body) 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | if len(body) < 1 { 146 | return nil, errors.New("body is empty") 147 | } 148 | // 保留错误信息 149 | //if resp.StatusCode >= 400 { 150 | // return nil, fmt.Errorf("HTTP error: %s", resp.Status) 151 | //} 152 | 153 | // 检查状态码 154 | if resp.StatusCode != http.StatusOK { 155 | return nil, errors.New(string(body)) 156 | } 157 | return body, nil 158 | } 159 | -------------------------------------------------------------------------------- /utilJWT.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 minRAG Authors. 2 | // 3 | // This file is part of minRAG. 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package main 19 | 20 | import ( 21 | "crypto/hmac" 22 | "crypto/sha512" 23 | "encoding/base64" 24 | "encoding/json" 25 | "errors" 26 | "fmt" 27 | "strings" 28 | "time" 29 | ) 30 | 31 | type Header struct { 32 | Alg string `json:"alg"` 33 | Typ string `json:"typ"` 34 | } 35 | 36 | type Payload struct { 37 | // 定义您需要加入的有效载荷字段 38 | UserId string `json:"userId"` 39 | Expires int64 `json:"exp"` 40 | } 41 | 42 | // 定义头部和有效载荷 43 | var defaultHeader = Header{ 44 | Alg: "HS512", 45 | Typ: "JWT", 46 | } 47 | 48 | // newJWTToken 创建一个jwtToken 49 | func newJWTToken(userId string) (string, error) { 50 | if userId == "" { 51 | return "", errors.New("userId is nil ") 52 | } 53 | 54 | payload := Payload{ 55 | UserId: userId, 56 | Expires: time.Now().Add(time.Duration(config.Timeout) * time.Second).Unix(), // 设置过期时间 57 | } 58 | 59 | // 序列化头部和有效载荷 60 | headerBytes, err := json.Marshal(defaultHeader) 61 | if err != nil { 62 | return "", err 63 | } 64 | 65 | payloadBytes, err := json.Marshal(payload) 66 | if err != nil { 67 | return "", err 68 | } 69 | 70 | // Base64编码头部和有效载荷 71 | headerString := base64.RawURLEncoding.EncodeToString(headerBytes) 72 | payloadString := base64.RawURLEncoding.EncodeToString(payloadBytes) 73 | 74 | // 计算签名 75 | hasher := hmac.New(sha512.New, []byte(config.JwtSecret)) 76 | hasher.Write([]byte(headerString + "." + payloadString)) 77 | signature := base64.RawURLEncoding.EncodeToString(hasher.Sum(nil)) 78 | 79 | // 拼接JWT字符串 80 | jwtString := fmt.Sprintf("%s.%s.%s", headerString, payloadString, signature) 81 | 82 | return jwtString, err 83 | } 84 | 85 | // userIdByToken 根据token字符串,查询UserId 86 | func userIdByToken(tokenString string) (string, error) { 87 | if tokenString == "" { 88 | return "", errors.New("token is nil") 89 | } 90 | 91 | // 分割JWT字符串 92 | parts := strings.Split(tokenString, ".") 93 | if len(parts) != 3 { 94 | return "", errors.New("invalid JWT format") 95 | } 96 | 97 | // 解码头部和有效载荷部分 98 | headerBytes, err := base64.RawURLEncoding.DecodeString(parts[0]) 99 | if err != nil { 100 | return "", err 101 | } 102 | 103 | payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[1]) 104 | if err != nil { 105 | return "", err 106 | } 107 | 108 | // 解析头部 109 | var header Header 110 | err = json.Unmarshal(headerBytes, &header) 111 | if err != nil { 112 | return "", err 113 | } 114 | 115 | // 验证算法 116 | if header.Alg != "HS512" { 117 | return "", err 118 | } 119 | 120 | // 计算签名 121 | hasher := hmac.New(sha512.New, []byte(config.JwtSecret)) 122 | hasher.Write([]byte(parts[0] + "." + parts[1])) 123 | expectedSignature := base64.RawURLEncoding.EncodeToString(hasher.Sum(nil)) 124 | 125 | // 比较签名 126 | if parts[2] != expectedSignature { 127 | return "", errors.New("JWT signature is invalid") 128 | } 129 | 130 | // 解析有效载荷 131 | var payload Payload 132 | err = json.Unmarshal(payloadBytes, &payload) 133 | if err != nil { 134 | return "", err 135 | } 136 | 137 | // 检查有效载荷中的过期时间 138 | if payload.Expires < time.Now().Unix() { 139 | return "", errors.New("JWT signature is expires") 140 | } 141 | return payload.UserId, nil 142 | } 143 | -------------------------------------------------------------------------------- /utilLocale.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 minRAG Authors. 2 | // 3 | // This file is part of minRAG. 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package main 19 | 20 | import ( 21 | "encoding/json" 22 | "errors" 23 | "io" 24 | "os" 25 | "strings" 26 | ) 27 | 28 | // localeMap 用于记录翻译的Map 29 | var localeMap = make(map[string]string) 30 | var minragLocate = "zh-CN" 31 | 32 | // initLocale 要在config初始化之后,需要获取config中的语言配置 33 | func initLocale() { 34 | minragLocate = config.Locale 35 | defaultErr := errors.New(minragLocate + ".json " + funcT("Load failed, using default") + " zh-CN.json") 36 | // 打开文件 37 | jsonFile, err := os.Open(datadir + "/locales/" + minragLocate + ".json") 38 | if err != nil { 39 | FuncLogError(nil, defaultErr) 40 | jsonFile, err = os.Open(datadir + "/locales/zh-CN.json") 41 | if err != nil { 42 | FuncLogError(nil, defaultErr) 43 | return 44 | } 45 | minragLocate = "zh-CN" 46 | } 47 | // 关闭文件 48 | defer jsonFile.Close() 49 | byteValue, err := io.ReadAll(jsonFile) 50 | if err != nil { 51 | FuncLogError(nil, defaultErr) 52 | return 53 | } 54 | localeMapTemp := make(map[string]string) 55 | // Decode从输入流读取下一个json编码值并保存在v指向的值里 56 | err = json.Unmarshal([]byte(byteValue), &localeMapTemp) 57 | if err != nil { 58 | FuncLogError(nil, defaultErr) 59 | return 60 | } 61 | //不区分大小写 62 | for key, value := range localeMapTemp { 63 | localeMap[strings.ToLower(key)] = value 64 | } 65 | localeMapTemp = nil 66 | } 67 | -------------------------------------------------------------------------------- /utilSQLite.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 minRAG Authors. 2 | // 3 | // This file is part of minRAG. 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package main 19 | 20 | import ( 21 | "bytes" 22 | "context" 23 | "database/sql" 24 | "encoding/binary" 25 | "fmt" 26 | "os" 27 | 28 | "gitee.com/chunanyong/zorm" 29 | 30 | // 00.引入数据库驱动 31 | "github.com/mattn/go-sqlite3" 32 | ) 33 | 34 | var dbDao *zorm.DBDao 35 | 36 | var dbDaoConfig = zorm.DataSourceConfig{ 37 | DSN: sqliteDBfile, 38 | DriverName: "sqlite3_simple", // 使用simple分词器会注册这个驱动名 39 | Dialect: "sqlite", 40 | MaxOpenConns: 1, 41 | MaxIdleConns: 1, 42 | ConnMaxLifetimeSecond: 600, 43 | SlowSQLMillis: -1, 44 | } 45 | 46 | // checkSQLiteStatus 初始化sqlite数据库,并检查是否成功 47 | func checkSQLiteStatus() bool { 48 | const failSuffix = ".fail" 49 | if failDB := datadir + "minrag.db" + failSuffix; pathExist(failDB) { 50 | FuncLogError(nil, fmt.Errorf(funcT("Please confirm if [%s] needs to be manually renamed to [minrag.db]. If not, please manually delete [%s]"), failDB, failDB)) 51 | return false 52 | } 53 | 54 | fts5File := datadir + "extensions/libsimple" 55 | vecFile := datadir + "extensions/vec0" 56 | //注册fts5的simple分词器,建议使用jieba分词 57 | //需要 --tags "fts5" 58 | sql.Register("sqlite3_simple", &sqlite3.SQLiteDriver{ 59 | Extensions: []string{ 60 | fts5File, //不要加后缀,它会自己处理,这样代码也统一 61 | vecFile, 62 | }, 63 | }) 64 | 65 | var err error 66 | dbDao, err = zorm.NewDBDao(&dbDaoConfig) 67 | if dbDao == nil || err != nil { //数据库初始化失败 68 | if db := datadir + "minrag.db"; pathExist(db) { 69 | _ = os.Rename(db, db+failSuffix) 70 | } 71 | return false 72 | } 73 | 74 | //初始化结巴分词的字典 75 | finder := zorm.NewFinder().Append("SELECT jieba_dict(?)", datadir+"dict") 76 | fts5jieba := "" 77 | _, err = zorm.QueryRow(context.Background(), finder, &fts5jieba) 78 | if err != nil { 79 | return false 80 | } 81 | 82 | finder = zorm.NewFinder().Append("select jieba_query(?)", "让数据自由一点点,让世界美好一点点") 83 | _, err = zorm.QueryRow(context.Background(), finder, &fts5jieba) 84 | if err != nil { 85 | return false 86 | } 87 | 88 | // 查询sqlite_vec版本 89 | var vecVersion string 90 | finder = zorm.NewFinder().Append("select vec_version()") 91 | _, err = zorm.QueryRow(context.Background(), finder, &vecVersion) 92 | if err != nil { 93 | panic(err) 94 | } 95 | //fmt.Println("vec_version:" + vecVersion) 96 | isInit := pathExist(datadir + "minrag.db") 97 | if !isInit { //需要初始化数据库 98 | return isInit 99 | } 100 | 101 | if tableExist(tableDocumentName) { 102 | return true 103 | } 104 | 105 | sqlByte, err := os.ReadFile("minragdatadir/minrag.sql") 106 | if err != nil { 107 | panic(err) 108 | } 109 | createTableSQL := string(sqlByte) 110 | if createTableSQL == "" { 111 | panic("minragdatadir/minrag.sql " + funcT("File anomaly")) 112 | } 113 | 114 | ctx := context.Background() 115 | _, err = execNativeSQL(ctx, createTableSQL) 116 | if err != nil { 117 | panic(err) 118 | } 119 | 120 | return true 121 | } 122 | 123 | // tableExist 数据表是否存在 124 | func tableExist(tableName string) bool { 125 | finder := zorm.NewSelectFinder("sqlite_master", "count(*)").Append("WHERE type=? and name=?", "table", tableName) 126 | count := 0 127 | zorm.QueryRow(context.Background(), finder, &count) 128 | return count > 0 129 | } 130 | 131 | // deleteById 根据Id删除数据 132 | func deleteById(ctx context.Context, tableName string, id string) error { 133 | finder := zorm.NewDeleteFinder(tableName).Append(" WHERE id=?", id) 134 | _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { 135 | _, err := zorm.UpdateFinder(ctx, finder) 136 | return nil, err 137 | }) 138 | 139 | return err 140 | } 141 | 142 | // deleteAll 删除所有数据 143 | func deleteAll(ctx context.Context, tableName string) error { 144 | finder := zorm.NewDeleteFinder(tableName) 145 | _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { 146 | _, err := zorm.UpdateFinder(ctx, finder) 147 | return nil, err 148 | }) 149 | 150 | return err 151 | } 152 | 153 | // execNativeSQL 执行SQL语句 154 | func execNativeSQL(ctx context.Context, nativeSQL string) (bool, error) { 155 | finder := zorm.NewFinder().Append(nativeSQL) 156 | finder.InjectionCheck = false 157 | _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { 158 | _, err := zorm.UpdateFinder(ctx, finder) 159 | return nil, err 160 | }) 161 | if err != nil { 162 | return false, err 163 | } 164 | return true, nil 165 | } 166 | 167 | // vecSerializeFloat64 Serializes a float64 list into a vector BLOB that sqlite-vec accepts 168 | func vecSerializeFloat64(vector []float64) ([]byte, error) { 169 | vector32 := make([]float32, len(vector)) 170 | for i, v := range vector { 171 | vector32[i] = float32(v) 172 | } 173 | buf := new(bytes.Buffer) 174 | err := binary.Write(buf, binary.LittleEndian, vector32) 175 | if err != nil { 176 | return nil, err 177 | } 178 | return buf.Bytes(), nil 179 | } 180 | -------------------------------------------------------------------------------- /utilSQLite_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 minRAG Authors. 2 | // 3 | // This file is part of minRAG. 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package main 19 | 20 | import ( 21 | "context" 22 | "fmt" 23 | "testing" 24 | 25 | "gitee.com/chunanyong/zorm" 26 | ) 27 | 28 | func TestVecLikeQuery(t *testing.T) { 29 | finder := zorm.NewSelectFinder(tableVecDocumentChunkName).Append("WHERE knowledgeBaseID like ? LIMIT 5", "%") 30 | list, _ := zorm.QueryMap(context.Background(), finder, nil) 31 | fmt.Println(len(list)) 32 | fmt.Println(list) 33 | } 34 | 35 | func TestVecQuery(t *testing.T) { 36 | ctx := context.Background() 37 | embedder := componentMap["OpenAITextEmbedder"] 38 | input := map[string]interface{}{"query": "I am a technical developer from China, primarily using Java, Go, and Python as my development languages."} 39 | err := embedder.Run(ctx, input) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | //需要使用bge-m3模型进行embedding 44 | embedding := input["embedding"].([]float64) 45 | query, _ := vecSerializeFloat64(embedding) 46 | finder := zorm.NewSelectFinder(tableVecDocumentChunkName, "rowid,distance as score,*").Append("WHERE embedding MATCH ? ORDER BY score LIMIT 5", query) 47 | datas := make([]DocumentChunk, 0) 48 | zorm.Query(ctx, finder, &datas, nil) 49 | 50 | for i := 0; i < len(datas); i++ { 51 | data := datas[i] 52 | fmt.Println(data.DocumentID, data.Score) 53 | } 54 | 55 | } 56 | 57 | func TestDocumentSplitter(t *testing.T) { 58 | ctx := context.Background() 59 | documentSplitter := componentMap["DocumentSplitter"] 60 | input := make(map[string]interface{}, 0) 61 | input["document"] = &Document{Markdown: "我是中国人,我爱中国。圣诞节,了大家安康金发傲娇考虑实际得分拉萨放假啊十六分是。1。2。3。"} 62 | err := documentSplitter.Run(ctx, input) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | ds := input["documentChunks"] 67 | documentChunks := ds.([]DocumentChunk) 68 | for i := 0; i < len(documentChunks); i++ { 69 | documentChunk := documentChunks[i] 70 | fmt.Println(documentChunk.Markdown) 71 | } 72 | 73 | } 74 | 75 | func TestFtsKeywordRetriever(t *testing.T) { 76 | ctx := context.Background() 77 | ftsKeywordRetriever := componentMap["FtsKeywordRetriever"] 78 | input := make(map[string]interface{}, 0) 79 | input["query"] = "马斯克" 80 | err := ftsKeywordRetriever.Run(ctx, input) 81 | if err != nil { 82 | t.Fatal(err) 83 | } 84 | ds := input["documentChunks"] 85 | documentChunks := ds.([]DocumentChunk) 86 | for i := 0; i < len(documentChunks); i++ { 87 | documentChunk := documentChunks[i] 88 | fmt.Println(documentChunk) 89 | } 90 | } 91 | 92 | func TestDocumentChunkReranker(t *testing.T) { 93 | ctx := context.Background() 94 | documentChunkReranker := componentMap["DocumentChunkReranker"] 95 | input := make(map[string]interface{}, 0) 96 | input["query"] = "你在哪里?" 97 | documentChunks := make([]DocumentChunk, 3) 98 | documentChunks[0] = DocumentChunk{Markdown: "我在郑州"} 99 | documentChunks[1] = DocumentChunk{Markdown: "今天晴天"} 100 | documentChunks[2] = DocumentChunk{Markdown: "我明天去旅游"} 101 | input["documentChunks"] = documentChunks 102 | err := documentChunkReranker.Run(ctx, input) 103 | if err != nil { 104 | t.Fatal(err) 105 | } 106 | ds := input["documentChunks"] 107 | documentChunks = ds.([]DocumentChunk) 108 | for i := 0; i < len(documentChunks); i++ { 109 | documentChunk := documentChunks[i] 110 | fmt.Println(documentChunk) 111 | } 112 | } 113 | 114 | func TestPromptBuilder(t *testing.T) { 115 | ctx := context.Background() 116 | promptBuilder := componentMap["PromptBuilder"] 117 | input := make(map[string]interface{}, 0) 118 | input["query"] = "你在哪里?" 119 | documentChunks := make([]DocumentChunk, 3) 120 | documentChunks[0] = DocumentChunk{Markdown: "我在郑州"} 121 | documentChunks[1] = DocumentChunk{Markdown: "今天晴天"} 122 | documentChunks[2] = DocumentChunk{Markdown: "我明天去旅游"} 123 | input["documentChunks"] = documentChunks 124 | err := promptBuilder.Run(ctx, input) 125 | if err != nil { 126 | t.Fatal(err) 127 | } 128 | fmt.Println(input["prompt"]) 129 | 130 | openAIChatMemory := componentMap["OpenAIChatMemory"] 131 | openAIChatMemory.Run(ctx, input) 132 | 133 | openAIChatGenerator := componentMap["OpenAIChatGenerator"] 134 | openAIChatGenerator.Run(ctx, input) 135 | choice := input["choice"] 136 | fmt.Println(choice) 137 | } 138 | 139 | func TestPipline(t *testing.T) { 140 | ctx := context.Background() 141 | defaultPipline := componentMap["default"] 142 | input := make(map[string]interface{}, 0) 143 | input["query"] = "你在哪里?" 144 | documentChunks := make([]DocumentChunk, 3) 145 | documentChunks[0] = DocumentChunk{Markdown: "我在郑州"} 146 | documentChunks[1] = DocumentChunk{Markdown: "今天晴天"} 147 | documentChunks[2] = DocumentChunk{Markdown: "我明天去旅游"} 148 | input["documentChunks"] = documentChunks 149 | err := defaultPipline.Run(ctx, input) 150 | if err != nil { 151 | t.Fatal(err) 152 | } 153 | choice := input["choice"] 154 | fmt.Println(choice) 155 | } 156 | -------------------------------------------------------------------------------- /utilZIP.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 minRAG Authors. 2 | // 3 | // This file is part of minRAG. 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package main 19 | 20 | import ( 21 | "archive/zip" 22 | "io" 23 | "os" 24 | "path/filepath" 25 | "sync" 26 | ) 27 | 28 | // unzip 用于解压ZIP文件到指定目录 29 | func unzip(zipPath, destDir string) (err error) { 30 | // 打开ZIP文件 31 | r, err := zip.OpenReader(zipPath) 32 | if err != nil { 33 | return err 34 | } 35 | defer r.Close() 36 | 37 | // 创建解压目录,如果不存在 38 | if _, err := os.Stat(destDir); os.IsNotExist(err) { 39 | if err := os.MkdirAll(destDir, 0644); err != nil { 40 | return err 41 | } 42 | } 43 | 44 | var ( 45 | once sync.Once 46 | dir string 47 | ) 48 | defer func() { 49 | if err != nil && dir != "" { 50 | _ = os.RemoveAll(dir) 51 | } 52 | }() 53 | // 遍历ZIP文件中的所有文件 54 | for _, f := range r.File { 55 | // 构造文件路径 56 | filePath := filepath.Join(destDir, f.Name) 57 | 58 | // 如果文件是目录,则创建目录 59 | if f.FileInfo().IsDir() { 60 | once.Do(func() { 61 | if _, err := os.Stat(filePath); err != nil { 62 | // 说明是本次解压创建的文件夹 63 | dir = filePath 64 | } 65 | }) 66 | os.MkdirAll(filePath, 0644) 67 | continue 68 | } 69 | 70 | // 打开ZIP文件中的文件 71 | if err := func(f *zip.File) error { 72 | rc, err := f.Open() 73 | if err != nil { 74 | return err 75 | } 76 | defer rc.Close() 77 | 78 | // 创建解压后的文件,不可执行 79 | outFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 80 | if err != nil { 81 | return err 82 | } 83 | defer outFile.Close() 84 | 85 | // 将文件内容写入到解压后的文件中 86 | if _, err := io.Copy(outFile, rc); err != nil { 87 | return err 88 | } 89 | return nil 90 | }(f); err != nil { 91 | return err 92 | } 93 | } 94 | 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /utilzip_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 minRAG Authors. 2 | // 3 | // This file is part of minRAG. 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package main 19 | 20 | import ( 21 | "os" 22 | "testing" 23 | ) 24 | 25 | func Test_unzip(t *testing.T) { 26 | type args struct { 27 | zipPath string 28 | destDir string 29 | } 30 | tests := []struct { 31 | name string 32 | args args 33 | wantExistDir bool 34 | wantDir string 35 | }{ 36 | { 37 | name: "解压成功 -> 创建或者覆盖目录和文件成功", 38 | args: args{ 39 | zipPath: "./minragdatadir/minrag-geekdoc-master.zip", 40 | destDir: "minragdatadir/", 41 | }, 42 | wantExistDir: true, 43 | wantDir: "./minragdatadir/minrag-geekdoc-master/", 44 | }, 45 | { 46 | name: "解压失败-原目录存在 -> 不进行任何处理", 47 | args: args{ 48 | zipPath: "./minragdatadir/minrag-geekdoc-master2.zip", 49 | destDir: "minragdatadir/", 50 | }, 51 | wantExistDir: true, 52 | wantDir: "./minragdatadir/minrag-geekdoc-master/", 53 | }, 54 | { 55 | name: "解压失败-原目录不存在 -> 删除原目录", 56 | args: args{ 57 | zipPath: "./minragdatadir/minrag-geekdoc-master_副本.zip", // 这个副本 把里面的一个.html后缀的改成.zip 然后压缩 58 | destDir: "tmp/", 59 | }, 60 | wantExistDir: false, 61 | wantDir: "./tmp/minrag-geekdoc-master/", 62 | }, 63 | } 64 | for _, tt := range tests { 65 | t.Run(tt.name, func(t *testing.T) { 66 | _ = unzip(tt.args.zipPath, tt.args.destDir) 67 | if _, err := os.Stat(tt.wantDir); (err == nil) != tt.wantExistDir { 68 | t.Errorf("unzip() error = %v, wantExistDir %v", err, tt.wantExistDir) 69 | } 70 | }) 71 | } 72 | } 73 | --------------------------------------------------------------------------------