mirror of
				https://github.com/yangjian102621/geekai.git
				synced 2025-11-04 16:23:42 +08:00 
			
		
		
		
	feat: chat with file function is ready
This commit is contained in:
		@@ -323,20 +323,46 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
 | 
				
			|||||||
		reqMgs = append(reqMgs, m)
 | 
							reqMgs = append(reqMgs, m)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fullPrompt := prompt
 | 
				
			||||||
 | 
						text := prompt
 | 
				
			||||||
 | 
						// extract files in prompt
 | 
				
			||||||
 | 
						files := utils.ExtractFileURLs(prompt)
 | 
				
			||||||
 | 
						logger.Debugf("detected FILES: %+v", files)
 | 
				
			||||||
 | 
						if len(files) > 0 {
 | 
				
			||||||
 | 
							contents := make([]string, 0)
 | 
				
			||||||
 | 
							var file model.File
 | 
				
			||||||
 | 
							for _, v := range files {
 | 
				
			||||||
 | 
								h.DB.Where("url = ?", v).First(&file)
 | 
				
			||||||
 | 
								content, err := utils.ReadFileContent(v)
 | 
				
			||||||
 | 
								if err == nil {
 | 
				
			||||||
 | 
									contents = append(contents, fmt.Sprintf("%s 文件内容:%s", file.Name, content))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								text = strings.Replace(text, v, "", 1)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if len(contents) > 0 {
 | 
				
			||||||
 | 
								fullPrompt = fmt.Sprintf("请根据提供的文件内容信息回答问题(其中Excel 已转成 HTML):\n\n %s\n\n 问题:%s", strings.Join(contents, "\n"), text)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							tokens, _ := utils.CalcTokens(fullPrompt, req.Model)
 | 
				
			||||||
 | 
							if tokens > session.Model.MaxContext {
 | 
				
			||||||
 | 
								return fmt.Errorf("文件的长度超出模型允许的最大上下文长度,请减少文件内容数量或文件大小。")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						logger.Debug("最终Prompt:", fullPrompt)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if session.Model.Platform == types.QWen.Value {
 | 
						if session.Model.Platform == types.QWen.Value {
 | 
				
			||||||
		req.Input = make(map[string]interface{})
 | 
							req.Input = make(map[string]interface{})
 | 
				
			||||||
		reqMgs = append(reqMgs, types.Message{
 | 
							reqMgs = append(reqMgs, types.Message{
 | 
				
			||||||
			Role:    "user",
 | 
								Role:    "user",
 | 
				
			||||||
			Content: prompt,
 | 
								Content: fullPrompt,
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
		req.Input["messages"] = reqMgs
 | 
							req.Input["messages"] = reqMgs
 | 
				
			||||||
	} else if session.Model.Platform == types.OpenAI.Value || session.Model.Platform == types.Azure.Value { // extract image for gpt-vision model
 | 
						} else if session.Model.Platform == types.OpenAI.Value || session.Model.Platform == types.Azure.Value { // extract image for gpt-vision model
 | 
				
			||||||
		imgURLs := utils.ExtractImgURL(prompt)
 | 
							imgURLs := utils.ExtractImgURLs(prompt)
 | 
				
			||||||
		logger.Debugf("detected IMG: %+v", imgURLs)
 | 
							logger.Debugf("detected IMG: %+v", imgURLs)
 | 
				
			||||||
		var content interface{}
 | 
							var content interface{}
 | 
				
			||||||
		if len(imgURLs) > 0 {
 | 
							if len(imgURLs) > 0 {
 | 
				
			||||||
			data := make([]interface{}, 0)
 | 
								data := make([]interface{}, 0)
 | 
				
			||||||
			text := prompt
 | 
					 | 
				
			||||||
			for _, v := range imgURLs {
 | 
								for _, v := range imgURLs {
 | 
				
			||||||
				text = strings.Replace(text, v, "", 1)
 | 
									text = strings.Replace(text, v, "", 1)
 | 
				
			||||||
				data = append(data, gin.H{
 | 
									data = append(data, gin.H{
 | 
				
			||||||
@@ -352,7 +378,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
 | 
				
			|||||||
			})
 | 
								})
 | 
				
			||||||
			content = data
 | 
								content = data
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			content = prompt
 | 
								content = fullPrompt
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		req.Messages = append(reqMgs, map[string]interface{}{
 | 
							req.Messages = append(reqMgs, map[string]interface{}{
 | 
				
			||||||
			"role":    "user",
 | 
								"role":    "user",
 | 
				
			||||||
@@ -361,7 +387,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
 | 
				
			|||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		req.Messages = append(reqMgs, map[string]interface{}{
 | 
							req.Messages = append(reqMgs, map[string]interface{}{
 | 
				
			||||||
			"role":    "user",
 | 
								"role":    "user",
 | 
				
			||||||
			"content": prompt,
 | 
								"content": fullPrompt,
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -454,7 +480,7 @@ func (h *ChatHandler) StopGenerate(c *gin.Context) {
 | 
				
			|||||||
func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, session *types.ChatSession, apiKey *model.ApiKey) (*http.Response, error) {
 | 
					func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, session *types.ChatSession, apiKey *model.ApiKey) (*http.Response, error) {
 | 
				
			||||||
	// if the chat model bind a KEY, use it directly
 | 
						// if the chat model bind a KEY, use it directly
 | 
				
			||||||
	if session.Model.KeyId > 0 {
 | 
						if session.Model.KeyId > 0 {
 | 
				
			||||||
		h.DB.Debug().Where("id", session.Model.KeyId).Where("enabled", true).Find(apiKey)
 | 
							h.DB.Where("id", session.Model.KeyId).Find(apiKey)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	// use the last unused key
 | 
						// use the last unused key
 | 
				
			||||||
	if apiKey.Id == 0 {
 | 
						if apiKey.Id == 0 {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -79,7 +79,7 @@ func (h *ChatHandler) sendXunFeiMessage(
 | 
				
			|||||||
	var res *gorm.DB
 | 
						var res *gorm.DB
 | 
				
			||||||
	// use the bind key
 | 
						// use the bind key
 | 
				
			||||||
	if session.Model.KeyId > 0 {
 | 
						if session.Model.KeyId > 0 {
 | 
				
			||||||
		res = h.DB.Where("id", session.Model.KeyId).Where("enabled", true).Find(&apiKey)
 | 
							res = h.DB.Where("id", session.Model.KeyId).Find(&apiKey)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	// use the last unused key
 | 
						// use the last unused key
 | 
				
			||||||
	if apiKey.Id == 0 {
 | 
						if apiKey.Id == 0 {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -215,7 +215,7 @@ func (h *MarkMapHandler) doRequest(req types.ApiRequest, chatModel model.ChatMod
 | 
				
			|||||||
	// if the chat model bind a KEY, use it directly
 | 
						// if the chat model bind a KEY, use it directly
 | 
				
			||||||
	var res *gorm.DB
 | 
						var res *gorm.DB
 | 
				
			||||||
	if chatModel.KeyId > 0 {
 | 
						if chatModel.KeyId > 0 {
 | 
				
			||||||
		res = h.DB.Where("id", chatModel.KeyId).Where("enabled", true).Find(apiKey)
 | 
							res = h.DB.Where("id", chatModel.KeyId).Find(apiKey)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	// use the last unused key
 | 
						// use the last unused key
 | 
				
			||||||
	if apiKey.Id == 0 {
 | 
						if apiKey.Id == 0 {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func main() {
 | 
					func main() {
 | 
				
			||||||
	file := "http://nk.img.r9it.com/chatgpt-plus/1719389335351828.xlsx"
 | 
						file := "http://nk.img.r9it.com/chatgpt-plus/1719389335351828.xlsx"
 | 
				
			||||||
	content, err := utils.ReadPdf(file)
 | 
						content, err := utils.ReadFileContent(file)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		panic(err)
 | 
							panic(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,7 +13,8 @@ import (
 | 
				
			|||||||
	"github.com/google/go-tika/tika"
 | 
						"github.com/google/go-tika/tika"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func ReadPdf(filePath string) (string, error) {
 | 
					func ReadFileContent(filePath string) (string, error) {
 | 
				
			||||||
 | 
						// for remote file, download it first
 | 
				
			||||||
	if strings.HasPrefix(filePath, "http") {
 | 
						if strings.HasPrefix(filePath, "http") {
 | 
				
			||||||
		file, err := downloadFile(filePath)
 | 
							file, err := downloadFile(filePath)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
@@ -31,22 +32,34 @@ func ReadPdf(filePath string) (string, error) {
 | 
				
			|||||||
	defer file.Close()
 | 
						defer file.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 使用 Tika 提取 PDF 文件的文本内容
 | 
						// 使用 Tika 提取 PDF 文件的文本内容
 | 
				
			||||||
	html, err := client.Parse(context.TODO(), file)
 | 
						content, err := client.Parse(context.TODO(), file)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return "", fmt.Errorf("error with parse file: %v", err)
 | 
							return "", fmt.Errorf("error with parse file: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	fmt.Println(html)
 | 
						ext := filepath.Ext(filePath)
 | 
				
			||||||
 | 
						switch ext {
 | 
				
			||||||
	return cleanBlankLine(html), nil
 | 
						case ".doc", ".docx", ".pdf", ".pptx", "ppt":
 | 
				
			||||||
 | 
							return cleanBlankLine(cleanHtml(content, false)), nil
 | 
				
			||||||
 | 
						case ".xls", ".xlsx":
 | 
				
			||||||
 | 
							return cleanBlankLine(cleanHtml(content, true)), nil
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return cleanBlankLine(content), nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 清理文本内容
 | 
					// 清理文本内容
 | 
				
			||||||
func cleanHtml(html string) string {
 | 
					func cleanHtml(html string, keepTable bool) string {
 | 
				
			||||||
	// 清理 HTML 标签
 | 
						// 清理 HTML 标签
 | 
				
			||||||
	p := bluemonday.StrictPolicy()
 | 
						var policy *bluemonday.Policy
 | 
				
			||||||
	return p.Sanitize(html)
 | 
						if keepTable {
 | 
				
			||||||
 | 
							policy = bluemonday.NewPolicy()
 | 
				
			||||||
 | 
							policy.AllowElements("table", "thead", "tbody", "tfoot", "tr", "td", "th")
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							policy = bluemonday.StrictPolicy()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return policy.Sanitize(html)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func cleanBlankLine(content string) string {
 | 
					func cleanBlankLine(content string) string {
 | 
				
			||||||
@@ -57,6 +70,12 @@ func cleanBlankLine(content string) string {
 | 
				
			|||||||
		if len(line) < 2 {
 | 
							if len(line) < 2 {
 | 
				
			||||||
			continue
 | 
								continue
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							// discard image
 | 
				
			||||||
 | 
							if strings.HasSuffix(line, ".png") ||
 | 
				
			||||||
 | 
								strings.HasSuffix(line, ".jpg") ||
 | 
				
			||||||
 | 
								strings.HasSuffix(line, ".jpeg") {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		texts = append(texts, line)
 | 
							texts = append(texts, line)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -88,7 +88,7 @@ func GetImgExt(filename string) string {
 | 
				
			|||||||
	return ext
 | 
						return ext
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func ExtractImgURL(text string) []string {
 | 
					func ExtractImgURLs(text string) []string {
 | 
				
			||||||
	re := regexp.MustCompile(`(http[s]?:\/\/.*?\.(?:png|jpg|jpeg|gif))`)
 | 
						re := regexp.MustCompile(`(http[s]?:\/\/.*?\.(?:png|jpg|jpeg|gif))`)
 | 
				
			||||||
	matches := re.FindAllStringSubmatch(text, 10)
 | 
						matches := re.FindAllStringSubmatch(text, 10)
 | 
				
			||||||
	urls := make([]string, 0)
 | 
						urls := make([]string, 0)
 | 
				
			||||||
@@ -99,3 +99,15 @@ func ExtractImgURL(text string) []string {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	return urls
 | 
						return urls
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ExtractFileURLs(text string) []string {
 | 
				
			||||||
 | 
						re := regexp.MustCompile(`(http[s]?:\/\/.*?\.(?:docx?|pdf|pptx?|xlsx?|txt))`)
 | 
				
			||||||
 | 
						matches := re.FindAllStringSubmatch(text, 10)
 | 
				
			||||||
 | 
						urls := make([]string, 0)
 | 
				
			||||||
 | 
						if len(matches) > 0 {
 | 
				
			||||||
 | 
							for _, m := range matches {
 | 
				
			||||||
 | 
								urls = append(urls, m[1])
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return urls
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
  presets: [
 | 
					  presets: [
 | 
				
			||||||
    '@vue/cli-plugin-babel/preset'
 | 
					    '@vue.css/cli-plugin-babel/preset'
 | 
				
			||||||
  ]
 | 
					  ]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										237
									
								
								web/src/assets/css/markdown/vue.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								web/src/assets/css/markdown/vue.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,237 @@
 | 
				
			|||||||
 | 
					.chat-line {
 | 
				
			||||||
 | 
					    ol, ul {
 | 
				
			||||||
 | 
					        margin: 0.8em 0;
 | 
				
			||||||
 | 
					        list-style: normal;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    a {
 | 
				
			||||||
 | 
					        color: #42b983;
 | 
				
			||||||
 | 
					        font-weight: 600;
 | 
				
			||||||
 | 
					        padding: 0 2px;
 | 
				
			||||||
 | 
					        text-decoration: none;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    h1,
 | 
				
			||||||
 | 
					    h2,
 | 
				
			||||||
 | 
					    h3,
 | 
				
			||||||
 | 
					    h4,
 | 
				
			||||||
 | 
					    h5,
 | 
				
			||||||
 | 
					    h6 {
 | 
				
			||||||
 | 
					        position: relative;
 | 
				
			||||||
 | 
					        margin-top: 1rem;
 | 
				
			||||||
 | 
					        margin-bottom: 1rem;
 | 
				
			||||||
 | 
					        font-weight: bold;
 | 
				
			||||||
 | 
					        line-height: 1.4;
 | 
				
			||||||
 | 
					        cursor: text;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    h1:hover a.anchor,
 | 
				
			||||||
 | 
					    h2:hover a.anchor,
 | 
				
			||||||
 | 
					    h3:hover a.anchor,
 | 
				
			||||||
 | 
					    h4:hover a.anchor,
 | 
				
			||||||
 | 
					    h5:hover a.anchor,
 | 
				
			||||||
 | 
					    h6:hover a.anchor {
 | 
				
			||||||
 | 
					        text-decoration: none;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    h1 tt,
 | 
				
			||||||
 | 
					    h1 code {
 | 
				
			||||||
 | 
					        font-size: inherit !important;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    h2 tt,
 | 
				
			||||||
 | 
					    h2 code {
 | 
				
			||||||
 | 
					        font-size: inherit !important;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    h3 tt,
 | 
				
			||||||
 | 
					    h3 code {
 | 
				
			||||||
 | 
					        font-size: inherit !important;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    h4 tt,
 | 
				
			||||||
 | 
					    h4 code {
 | 
				
			||||||
 | 
					        font-size: inherit !important;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    h5 tt,
 | 
				
			||||||
 | 
					    h5 code {
 | 
				
			||||||
 | 
					        font-size: inherit !important;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    h6 tt,
 | 
				
			||||||
 | 
					    h6 code {
 | 
				
			||||||
 | 
					        font-size: inherit !important;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    h2 a,
 | 
				
			||||||
 | 
					    h3 a {
 | 
				
			||||||
 | 
					        color: #34495e;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    h1 {
 | 
				
			||||||
 | 
					        padding-bottom: .4rem;
 | 
				
			||||||
 | 
					        font-size: 2.2rem;
 | 
				
			||||||
 | 
					        line-height: 1.3;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    h2 {
 | 
				
			||||||
 | 
					        font-size: 1.75rem;
 | 
				
			||||||
 | 
					        line-height: 1.225;
 | 
				
			||||||
 | 
					        margin: 35px 0 15px;
 | 
				
			||||||
 | 
					        padding-bottom: 0.5em;
 | 
				
			||||||
 | 
					        border-bottom: 1px solid #ddd;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    h3 {
 | 
				
			||||||
 | 
					        font-size: 1.4rem;
 | 
				
			||||||
 | 
					        line-height: 1.43;
 | 
				
			||||||
 | 
					        margin: 20px 0 7px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    h4 {
 | 
				
			||||||
 | 
					        font-size: 1.2rem;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    h5 {
 | 
				
			||||||
 | 
					        font-size: 1rem;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    h6 {
 | 
				
			||||||
 | 
					        font-size: 1rem;
 | 
				
			||||||
 | 
					        color: #777;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    p,
 | 
				
			||||||
 | 
					    blockquote,
 | 
				
			||||||
 | 
					    ul,
 | 
				
			||||||
 | 
					    ol,
 | 
				
			||||||
 | 
					    dl,
 | 
				
			||||||
 | 
					    table {
 | 
				
			||||||
 | 
					        margin: 0.8em 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    li > ol,
 | 
				
			||||||
 | 
					    li > ul {
 | 
				
			||||||
 | 
					        margin: 0 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    hr {
 | 
				
			||||||
 | 
					        height: 2px;
 | 
				
			||||||
 | 
					        padding: 0;
 | 
				
			||||||
 | 
					        margin: 16px 0;
 | 
				
			||||||
 | 
					        background-color: #e7e7e7;
 | 
				
			||||||
 | 
					        border: 0 none;
 | 
				
			||||||
 | 
					        overflow: hidden;
 | 
				
			||||||
 | 
					        box-sizing: content-box;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    body > h2:first-child {
 | 
				
			||||||
 | 
					        margin-top: 0;
 | 
				
			||||||
 | 
					        padding-top: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    body > h1:first-child {
 | 
				
			||||||
 | 
					        margin-top: 0;
 | 
				
			||||||
 | 
					        padding-top: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    body > h1:first-child + h2 {
 | 
				
			||||||
 | 
					        margin-top: 0;
 | 
				
			||||||
 | 
					        padding-top: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    body > h3:first-child,
 | 
				
			||||||
 | 
					    body > h4:first-child,
 | 
				
			||||||
 | 
					    body > h5:first-child,
 | 
				
			||||||
 | 
					    body > h6:first-child {
 | 
				
			||||||
 | 
					        margin-top: 0;
 | 
				
			||||||
 | 
					        padding-top: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    a:first-child h1,
 | 
				
			||||||
 | 
					    a:first-child h2,
 | 
				
			||||||
 | 
					    a:first-child h3,
 | 
				
			||||||
 | 
					    a:first-child h4,
 | 
				
			||||||
 | 
					    a:first-child h5,
 | 
				
			||||||
 | 
					    a:first-child h6 {
 | 
				
			||||||
 | 
					        margin-top: 0;
 | 
				
			||||||
 | 
					        padding-top: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    h1 p,
 | 
				
			||||||
 | 
					    h2 p,
 | 
				
			||||||
 | 
					    h3 p,
 | 
				
			||||||
 | 
					    h4 p,
 | 
				
			||||||
 | 
					    h5 p,
 | 
				
			||||||
 | 
					    h6 p {
 | 
				
			||||||
 | 
					        margin-top: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    li p.first {
 | 
				
			||||||
 | 
					        display: inline-block;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ul,
 | 
				
			||||||
 | 
					    ol {
 | 
				
			||||||
 | 
					        padding-left: 30px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ul:first-child,
 | 
				
			||||||
 | 
					    ol:first-child {
 | 
				
			||||||
 | 
					        margin-top: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ul:last-child,
 | 
				
			||||||
 | 
					    ol:last-child {
 | 
				
			||||||
 | 
					        margin-bottom: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    blockquote {
 | 
				
			||||||
 | 
					        border-left: 4px solid #42b983;
 | 
				
			||||||
 | 
					        padding: 10px 15px;
 | 
				
			||||||
 | 
					        color: #777;
 | 
				
			||||||
 | 
					        background-color: rgba(66, 185, 131, .1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    table {
 | 
				
			||||||
 | 
					        padding: 0;
 | 
				
			||||||
 | 
					        word-break: initial;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    table tr {
 | 
				
			||||||
 | 
					        border-top: 1px solid #dfe2e5;
 | 
				
			||||||
 | 
					        margin: 0;
 | 
				
			||||||
 | 
					        padding: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    table tr:nth-child(2n),
 | 
				
			||||||
 | 
					    thead {
 | 
				
			||||||
 | 
					        background-color: #fafafa;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    table tr th {
 | 
				
			||||||
 | 
					        font-weight: bold;
 | 
				
			||||||
 | 
					        border: 1px solid #dfe2e5;
 | 
				
			||||||
 | 
					        border-bottom: 0;
 | 
				
			||||||
 | 
					        text-align: left;
 | 
				
			||||||
 | 
					        margin: 0;
 | 
				
			||||||
 | 
					        padding: 6px 13px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    table tr td {
 | 
				
			||||||
 | 
					        border: 1px solid #dfe2e5;
 | 
				
			||||||
 | 
					        text-align: left;
 | 
				
			||||||
 | 
					        margin: 0;
 | 
				
			||||||
 | 
					        padding: 6px 13px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    table tr th:first-child,
 | 
				
			||||||
 | 
					    table tr td:first-child {
 | 
				
			||||||
 | 
					        margin-top: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    table tr th:last-child,
 | 
				
			||||||
 | 
					    table tr td:last-child {
 | 
				
			||||||
 | 
					        margin-bottom: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -16,7 +16,9 @@
 | 
				
			|||||||
                <el-image :src="GetFileIcon(file.ext)" fit="cover"  />
 | 
					                <el-image :src="GetFileIcon(file.ext)" fit="cover"  />
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
              <div class="body">
 | 
					              <div class="body">
 | 
				
			||||||
                <div class="title">{{file.name}}</div>
 | 
					                <div class="title">
 | 
				
			||||||
 | 
					                  <el-link :href="file.url" target="_blank" style="--el-font-weight-primary:bold">{{file.name}}</el-link>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
                <div class="info">
 | 
					                <div class="info">
 | 
				
			||||||
                  <span>{{GetFileType(file.ext)}}</span>
 | 
					                  <span>{{GetFileType(file.ext)}}</span>
 | 
				
			||||||
                  <span>{{FormatFileSize(file.size)}}</span>
 | 
					                  <span>{{FormatFileSize(file.size)}}</span>
 | 
				
			||||||
@@ -121,6 +123,7 @@ onMounted(() => {
 | 
				
			|||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="stylus">
 | 
					<style lang="stylus">
 | 
				
			||||||
 | 
					@import '@/assets/css/markdown/vue.css';
 | 
				
			||||||
.chat-line-prompt {
 | 
					.chat-line-prompt {
 | 
				
			||||||
  background-color #ffffff;
 | 
					  background-color #ffffff;
 | 
				
			||||||
  justify-content: center;
 | 
					  justify-content: center;
 | 
				
			||||||
@@ -214,11 +217,6 @@ onMounted(() => {
 | 
				
			|||||||
          margin 10px 0
 | 
					          margin 10px 0
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
        a {
 | 
					 | 
				
			||||||
          color #20a0ff
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        p {
 | 
					        p {
 | 
				
			||||||
          line-height 1.5
 | 
					          line-height 1.5
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -97,6 +97,7 @@ const reGenerate = (prompt) => {
 | 
				
			|||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="stylus">
 | 
					<style lang="stylus">
 | 
				
			||||||
 | 
					@import '@/assets/css/markdown/vue.css';
 | 
				
			||||||
.common-layout {
 | 
					.common-layout {
 | 
				
			||||||
  .chat-line-reply {
 | 
					  .chat-line-reply {
 | 
				
			||||||
    justify-content: center;
 | 
					    justify-content: center;
 | 
				
			||||||
@@ -132,18 +133,12 @@ const reGenerate = (prompt) => {
 | 
				
			|||||||
        .content {
 | 
					        .content {
 | 
				
			||||||
          min-height 20px;
 | 
					          min-height 20px;
 | 
				
			||||||
          word-break break-word;
 | 
					          word-break break-word;
 | 
				
			||||||
          padding: 6px 10px;
 | 
					          padding: 0 10px;
 | 
				
			||||||
          color #374151;
 | 
					          color #374151;
 | 
				
			||||||
          font-size: var(--content-font-size);
 | 
					          font-size: var(--content-font-size);
 | 
				
			||||||
          border-radius: 5px;
 | 
					          border-radius: 5px;
 | 
				
			||||||
          overflow auto;
 | 
					          overflow auto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          a {
 | 
					 | 
				
			||||||
            color #20a0ff
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          // control the image size in content
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          img {
 | 
					          img {
 | 
				
			||||||
            max-width: 600px;
 | 
					            max-width: 600px;
 | 
				
			||||||
            border-radius: 10px;
 | 
					            border-radius: 10px;
 | 
				
			||||||
@@ -170,10 +165,11 @@ const reGenerate = (prompt) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
          .code-container {
 | 
					          .code-container {
 | 
				
			||||||
            position relative
 | 
					            position relative
 | 
				
			||||||
 | 
					            display flex
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            .hljs {
 | 
					            .hljs {
 | 
				
			||||||
              border-radius 10px
 | 
					              border-radius 10px
 | 
				
			||||||
              line-height 1.5
 | 
					              width 100%
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            .copy-code-btn {
 | 
					            .copy-code-btn {
 | 
				
			||||||
@@ -194,7 +190,7 @@ const reGenerate = (prompt) => {
 | 
				
			|||||||
          .lang-name {
 | 
					          .lang-name {
 | 
				
			||||||
            position absolute;
 | 
					            position absolute;
 | 
				
			||||||
            right 10px
 | 
					            right 10px
 | 
				
			||||||
            bottom 50px
 | 
					            bottom 20px
 | 
				
			||||||
            padding 2px 6px 4px 6px
 | 
					            padding 2px 6px 4px 6px
 | 
				
			||||||
            background-color #444444
 | 
					            background-color #444444
 | 
				
			||||||
            border-radius 10px
 | 
					            border-radius 10px
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,6 +20,7 @@
 | 
				
			|||||||
                  :auto-upload="true"
 | 
					                  :auto-upload="true"
 | 
				
			||||||
                  :show-file-list="false"
 | 
					                  :show-file-list="false"
 | 
				
			||||||
                  :http-request="afterRead"
 | 
					                  :http-request="afterRead"
 | 
				
			||||||
 | 
					                  accept=".doc,.docx,.jpg,.png,.jpeg,.xls,.xlsx,.ppt,.pptx,.pdf"
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
                <el-icon class="avatar-uploader-icon">
 | 
					                <el-icon class="avatar-uploader-icon">
 | 
				
			||||||
                  <Plus/>
 | 
					                  <Plus/>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -188,38 +188,7 @@ export function processContent(content) {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    return content
 | 
				
			||||||
    const lines = content.split("\n")
 | 
					 | 
				
			||||||
    if (lines.length <= 1) {
 | 
					 | 
				
			||||||
        return content
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    const texts = []
 | 
					 | 
				
			||||||
    // 定义匹配数学公式的正则表达式
 | 
					 | 
				
			||||||
    const formulaRegex = /^\s*[a-z|A-Z]+[^=]+\s*=\s*[^=]+$/;
 | 
					 | 
				
			||||||
    let hasCode = false
 | 
					 | 
				
			||||||
    for (let i = 0; i < lines.length; i++) {
 | 
					 | 
				
			||||||
        // 处理引用块换行
 | 
					 | 
				
			||||||
        if (lines[i].startsWith(">")) {
 | 
					 | 
				
			||||||
            texts.push(lines[i])
 | 
					 | 
				
			||||||
            texts.push("\n")
 | 
					 | 
				
			||||||
            continue
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        // 如果包含代码块则跳过公式检测
 | 
					 | 
				
			||||||
        if (lines[i].indexOf("```") !== -1) {
 | 
					 | 
				
			||||||
            texts.push(lines[i])
 | 
					 | 
				
			||||||
            hasCode = true
 | 
					 | 
				
			||||||
            continue
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        // 识别并处理数学公式,需要排除那些已经被识别出来的公式
 | 
					 | 
				
			||||||
        if (i > 0 && formulaRegex.test(lines[i]) && lines[i - 1].indexOf("$$") === -1 && !hasCode) {
 | 
					 | 
				
			||||||
            texts.push("$$")
 | 
					 | 
				
			||||||
            texts.push(lines[i])
 | 
					 | 
				
			||||||
            texts.push("$$")
 | 
					 | 
				
			||||||
            continue
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        texts.push(lines[i])
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return texts.join("\n")
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function processPrompt(prompt) {
 | 
					export function processPrompt(prompt) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,7 @@
 | 
				
			|||||||
          <div class="content" :style="{height: leftBoxHeight+'px'}">
 | 
					          <div class="content" :style="{height: leftBoxHeight+'px'}">
 | 
				
			||||||
            <el-row v-for="chat in chatList" :key="chat.chat_id">
 | 
					            <el-row v-for="chat in chatList" :key="chat.chat_id">
 | 
				
			||||||
              <div :class="chat.chat_id === activeChat.chat_id?'chat-list-item active':'chat-list-item'"
 | 
					              <div :class="chat.chat_id === activeChat.chat_id?'chat-list-item active':'chat-list-item'"
 | 
				
			||||||
                   @click="changeChat(chat)">
 | 
					                   @click="loadChat(chat)">
 | 
				
			||||||
                <el-image :src="chat.icon" class="avatar"/>
 | 
					                <el-image :src="chat.icon" class="avatar"/>
 | 
				
			||||||
                <span class="chat-title-input" v-if="chat.edit">
 | 
					                <span class="chat-title-input" v-if="chat.edit">
 | 
				
			||||||
                  <el-input v-model="tmpChatTitle" size="small" @keydown="titleKeydown($event, chat)"
 | 
					                  <el-input v-model="tmpChatTitle" size="small" @keydown="titleKeydown($event, chat)"
 | 
				
			||||||
@@ -424,12 +424,8 @@ const newChat = () => {
 | 
				
			|||||||
  connect(null, roleId.value)
 | 
					  connect(null, roleId.value)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 切换会话
 | 
					 | 
				
			||||||
const changeChat = (chat) => {
 | 
					 | 
				
			||||||
  localStorage.setItem("chat_id", chat.chat_id)
 | 
					 | 
				
			||||||
  loadChat(chat)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 切换会话
 | 
				
			||||||
const loadChat = function (chat) {
 | 
					const loadChat = function (chat) {
 | 
				
			||||||
  if (!isLogin.value) {
 | 
					  if (!isLogin.value) {
 | 
				
			||||||
    store.setShowLoginDialog(true)
 | 
					    store.setShowLoginDialog(true)
 | 
				
			||||||
@@ -753,7 +749,7 @@ const sendMessage = function () {
 | 
				
			|||||||
  // 如果携带了文件,则串上文件地址
 | 
					  // 如果携带了文件,则串上文件地址
 | 
				
			||||||
  let content = prompt.value
 | 
					  let content = prompt.value
 | 
				
			||||||
  if (files.value.length > 0) {
 | 
					  if (files.value.length > 0) {
 | 
				
			||||||
    content = files.value.map(file => file.url).join(" ") + " " +  content
 | 
					    content += files.value.map(file => file.url).join(" ")
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  // 追加消息
 | 
					  // 追加消息
 | 
				
			||||||
  chatData.value.push({
 | 
					  chatData.value.push({
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -162,7 +162,7 @@
 | 
				
			|||||||
        </el-form-item>
 | 
					        </el-form-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <el-form-item label="绑定API-KEY:" prop="apikey">
 | 
					        <el-form-item label="绑定API-KEY:" prop="apikey">
 | 
				
			||||||
          <el-select v-model="item.key_id" placeholder="请选择 API KEY" clearable>
 | 
					          <el-select v-model="item.key_id" placeholder="请选择 API KEY" filterable clearable>
 | 
				
			||||||
            <el-option v-for="v in apiKeys" :value="v.id" :label="v.name" :key="v.id">
 | 
					            <el-option v-for="v in apiKeys" :value="v.id" :label="v.name" :key="v.id">
 | 
				
			||||||
              {{ v.name }}
 | 
					              {{ v.name }}
 | 
				
			||||||
              <el-text type="info" size="small">{{ substr(v.api_url, 50) }}</el-text>
 | 
					              <el-text type="info" size="small">{{ substr(v.api_url, 50) }}</el-text>
 | 
				
			||||||
@@ -229,7 +229,7 @@ const platforms = ref([])
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// 获取 API KEY
 | 
					// 获取 API KEY
 | 
				
			||||||
const apiKeys = ref([])
 | 
					const apiKeys = ref([])
 | 
				
			||||||
httpGet('/api/admin/apikey/list?status=true&type=chat').then(res => {
 | 
					httpGet('/api/admin/apikey/list?type=chat').then(res => {
 | 
				
			||||||
  apiKeys.value = res.data
 | 
					  apiKeys.value = res.data
 | 
				
			||||||
}).catch(e => {
 | 
					}).catch(e => {
 | 
				
			||||||
  ElMessage.error("获取 API KEY 失败:" + e.message)
 | 
					  ElMessage.error("获取 API KEY 失败:" + e.message)
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user