init
This commit is contained in:
		
						commit
						98d7406a99
					
				
							
								
								
									
										158
									
								
								knows-java/pom.xml
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										158
									
								
								knows-java/pom.xml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,158 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 | 
				
			||||||
 | 
					         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
 | 
				
			||||||
 | 
					    <modelVersion>4.0.0</modelVersion>
 | 
				
			||||||
 | 
					    <parent>
 | 
				
			||||||
 | 
					        <groupId>org.springframework.boot</groupId>
 | 
				
			||||||
 | 
					        <artifactId>spring-boot-starter-parent</artifactId>
 | 
				
			||||||
 | 
					        <version>3.3.2</version>
 | 
				
			||||||
 | 
					        <relativePath/> <!-- lookup parent from repository -->
 | 
				
			||||||
 | 
					    </parent>
 | 
				
			||||||
 | 
					    <groupId>com.zhych</groupId>
 | 
				
			||||||
 | 
					    <artifactId>knows</artifactId>
 | 
				
			||||||
 | 
					    <version>0.0.1-SNAPSHOT</version>
 | 
				
			||||||
 | 
					    <name>embeddings</name>
 | 
				
			||||||
 | 
					    <description>embeddings</description>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <properties>
 | 
				
			||||||
 | 
					        <java.version>17</java.version>
 | 
				
			||||||
 | 
					    </properties>
 | 
				
			||||||
 | 
					    <dependencies>
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>org.springframework.boot</groupId>
 | 
				
			||||||
 | 
					            <artifactId>spring-boot-starter-web</artifactId>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>org.apache.commons</groupId>
 | 
				
			||||||
 | 
					            <artifactId>commons-lang3</artifactId>
 | 
				
			||||||
 | 
					            <version>3.12.0</version>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>org.projectlombok</groupId>
 | 
				
			||||||
 | 
					            <artifactId>lombok</artifactId>
 | 
				
			||||||
 | 
					            <optional>true</optional>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>org.springframework.boot</groupId>
 | 
				
			||||||
 | 
					            <artifactId>spring-boot-starter-test</artifactId>
 | 
				
			||||||
 | 
					            <scope>test</scope>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>org.springframework.boot</groupId>
 | 
				
			||||||
 | 
					            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>com.alibaba</groupId>
 | 
				
			||||||
 | 
					            <artifactId>fastjson</artifactId>
 | 
				
			||||||
 | 
					            <version>2.0.15</version>
 | 
				
			||||||
 | 
					            <scope>compile</scope>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>cn.hutool</groupId>
 | 
				
			||||||
 | 
					            <artifactId>hutool-all</artifactId>
 | 
				
			||||||
 | 
					            <version>5.8.25</version>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>com.squareup.okhttp3</groupId>
 | 
				
			||||||
 | 
					            <artifactId>okhttp</artifactId>
 | 
				
			||||||
 | 
					            <version>5.0.0-alpha.3</version>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>org.apache.httpcomponents</groupId>
 | 
				
			||||||
 | 
					            <artifactId>httpclient</artifactId>
 | 
				
			||||||
 | 
					            <version>4.5.13</version>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>org.elasticsearch.client</groupId>
 | 
				
			||||||
 | 
					            <artifactId>elasticsearch-rest-high-level-client</artifactId>
 | 
				
			||||||
 | 
					            <version>7.17.23</version>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>co.elastic.clients</groupId>
 | 
				
			||||||
 | 
					            <artifactId>elasticsearch-java</artifactId>
 | 
				
			||||||
 | 
					            <version>8.13.4</version>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>com.fasterxml.jackson.core</groupId>
 | 
				
			||||||
 | 
					            <artifactId>jackson-databind</artifactId>
 | 
				
			||||||
 | 
					            <version>2.15.2</version>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>com.alibaba</groupId>
 | 
				
			||||||
 | 
					            <artifactId>dashscope-sdk-java</artifactId>
 | 
				
			||||||
 | 
					            <version>2.8.3</version>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>org.apache.pdfbox</groupId>
 | 
				
			||||||
 | 
					            <artifactId>pdfbox</artifactId>
 | 
				
			||||||
 | 
					            <version>2.0.24</version>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>net.sourceforge.tess4j</groupId>
 | 
				
			||||||
 | 
					            <artifactId>tess4j</artifactId>
 | 
				
			||||||
 | 
					            <version>5.7.0</version>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>org.bytedeco</groupId>
 | 
				
			||||||
 | 
					            <artifactId>opencv-platform</artifactId>
 | 
				
			||||||
 | 
					            <version>4.7.0-1.5.9</version>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <!-- Apache POI for Word documents -->
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>org.apache.poi</groupId>
 | 
				
			||||||
 | 
					            <artifactId>poi</artifactId>
 | 
				
			||||||
 | 
					            <version>5.2.3</version>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>org.apache.poi</groupId>
 | 
				
			||||||
 | 
					            <artifactId>poi-ooxml</artifactId>
 | 
				
			||||||
 | 
					            <version>5.2.3</version>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>org.apache.poi</groupId>
 | 
				
			||||||
 | 
					            <artifactId>poi-scratchpad</artifactId>
 | 
				
			||||||
 | 
					            <version>5.2.3</version>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					    </dependencies>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <build>
 | 
				
			||||||
 | 
					        <resources>
 | 
				
			||||||
 | 
					            <resource>
 | 
				
			||||||
 | 
					                <directory>src/main/resources</directory>
 | 
				
			||||||
 | 
					                <includes>
 | 
				
			||||||
 | 
					                    <include>**/*</include>
 | 
				
			||||||
 | 
					                </includes>
 | 
				
			||||||
 | 
					            </resource>
 | 
				
			||||||
 | 
					        </resources>
 | 
				
			||||||
 | 
					        <plugins>
 | 
				
			||||||
 | 
					            <plugin>
 | 
				
			||||||
 | 
					                <groupId>org.springframework.boot</groupId>
 | 
				
			||||||
 | 
					                <artifactId>spring-boot-maven-plugin</artifactId>
 | 
				
			||||||
 | 
					                <configuration>
 | 
				
			||||||
 | 
					                    <excludes>
 | 
				
			||||||
 | 
					                        <exclude>
 | 
				
			||||||
 | 
					                            <groupId>org.projectlombok</groupId>
 | 
				
			||||||
 | 
					                            <artifactId>lombok</artifactId>
 | 
				
			||||||
 | 
					                        </exclude>
 | 
				
			||||||
 | 
					                    </excludes>
 | 
				
			||||||
 | 
					                </configuration>
 | 
				
			||||||
 | 
					            </plugin>
 | 
				
			||||||
 | 
					            <plugin>
 | 
				
			||||||
 | 
					                <groupId>org.apache.maven.plugins</groupId>
 | 
				
			||||||
 | 
					                <artifactId>maven-surefire-plugin</artifactId>
 | 
				
			||||||
 | 
					                <version>3.0.0-M5</version>
 | 
				
			||||||
 | 
					                <configuration>
 | 
				
			||||||
 | 
					                    <argLine>
 | 
				
			||||||
 | 
					                        -Xmx2048m
 | 
				
			||||||
 | 
					                        -Djava.library.path=${project.basedir}/lib/opencv
 | 
				
			||||||
 | 
					                    </argLine>
 | 
				
			||||||
 | 
					                </configuration>
 | 
				
			||||||
 | 
					            </plugin>
 | 
				
			||||||
 | 
					        </plugins>
 | 
				
			||||||
 | 
					    </build>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</project>
 | 
				
			||||||
							
								
								
									
										15
									
								
								knows-java/src/main/java/cn/luckday/Application.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										15
									
								
								knows-java/src/main/java/cn/luckday/Application.java
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					package cn.luckday;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.springframework.boot.SpringApplication;
 | 
				
			||||||
 | 
					import org.springframework.boot.autoconfigure.SpringBootApplication;
 | 
				
			||||||
 | 
					import org.springframework.context.annotation.ComponentScan;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@SpringBootApplication
 | 
				
			||||||
 | 
					@ComponentScan(value = {"cn.luckday.*"})
 | 
				
			||||||
 | 
					public class Application {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static void main(String[] args) {
 | 
				
			||||||
 | 
					        SpringApplication.run(Application.class, args);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										21
									
								
								knows-java/src/main/java/cn/luckday/bean/KnowsIndex.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										21
									
								
								knows-java/src/main/java/cn/luckday/bean/KnowsIndex.java
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					package cn.luckday.bean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import lombok.Data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Data
 | 
				
			||||||
 | 
					public class KnowsIndex {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private String id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private String file_name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private String file_path;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private String file_type;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private String file_size;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private String content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private double[] content_vec;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										13
									
								
								knows-java/src/main/java/cn/luckday/bean/SearchResult.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										13
									
								
								knows-java/src/main/java/cn/luckday/bean/SearchResult.java
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					package cn.luckday.bean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import lombok.AllArgsConstructor;
 | 
				
			||||||
 | 
					import lombok.Data;
 | 
				
			||||||
 | 
					import lombok.NoArgsConstructor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Data
 | 
				
			||||||
 | 
					@AllArgsConstructor
 | 
				
			||||||
 | 
					@NoArgsConstructor
 | 
				
			||||||
 | 
					public class SearchResult {
 | 
				
			||||||
 | 
					    private KnowsIndex knowsIndex;
 | 
				
			||||||
 | 
					    private Double score;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,112 @@
 | 
				
			|||||||
 | 
					package cn.luckday.controller;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cn.hutool.core.collection.CollUtil;
 | 
				
			||||||
 | 
					import cn.luckday.llm.QwenClient;
 | 
				
			||||||
 | 
					import com.alibaba.dashscope.aigc.generation.GenerationResult;
 | 
				
			||||||
 | 
					import com.alibaba.dashscope.exception.InputRequiredException;
 | 
				
			||||||
 | 
					import com.alibaba.dashscope.exception.NoApiKeyException;
 | 
				
			||||||
 | 
					import com.alibaba.fastjson.JSON;
 | 
				
			||||||
 | 
					import com.alibaba.fastjson.JSONObject;
 | 
				
			||||||
 | 
					import cn.luckday.bean.SearchResult;
 | 
				
			||||||
 | 
					import cn.luckday.embed.EmbedClient;
 | 
				
			||||||
 | 
					import cn.luckday.embed.ReRankClient;
 | 
				
			||||||
 | 
					import cn.luckday.llm.OllamaClient;
 | 
				
			||||||
 | 
					import cn.luckday.service.EsDocumentService;
 | 
				
			||||||
 | 
					import jakarta.annotation.Resource;
 | 
				
			||||||
 | 
					import jakarta.servlet.http.HttpServletResponse;
 | 
				
			||||||
 | 
					import lombok.extern.slf4j.Slf4j;
 | 
				
			||||||
 | 
					import org.springframework.beans.factory.annotation.Value;
 | 
				
			||||||
 | 
					import org.springframework.web.bind.annotation.PostMapping;
 | 
				
			||||||
 | 
					import org.springframework.web.bind.annotation.RequestBody;
 | 
				
			||||||
 | 
					import org.springframework.web.bind.annotation.RequestMapping;
 | 
				
			||||||
 | 
					import org.springframework.web.bind.annotation.RestController;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.util.ArrayList;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Slf4j
 | 
				
			||||||
 | 
					@RestController
 | 
				
			||||||
 | 
					@RequestMapping("/knows")
 | 
				
			||||||
 | 
					public class KnowsController {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Value("${embedding.uri}")
 | 
				
			||||||
 | 
					    private String embeddingUri;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Value("${embedding.api-key}")
 | 
				
			||||||
 | 
					    private String embeddingApiKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Value("${re-rank.uri}")
 | 
				
			||||||
 | 
					    private String ReRankUri;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Value("${re-rank.api-key}")
 | 
				
			||||||
 | 
					    private String ReRankApiKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Value("${oll.uri}")
 | 
				
			||||||
 | 
					    private String ollUri;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Value("${qwen.api-key}")
 | 
				
			||||||
 | 
					    private static String apiKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Value("${qwen.model}")
 | 
				
			||||||
 | 
					    private static String model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Resource
 | 
				
			||||||
 | 
					    private EsDocumentService service;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @PostMapping("/process")
 | 
				
			||||||
 | 
					    public List<SearchResult> process(@RequestBody Map<String, String> dto) throws IOException {
 | 
				
			||||||
 | 
					        String keyword = dto.get("keyword");
 | 
				
			||||||
 | 
					        return service.searchVector(EmbedClient.getEmbedding(embeddingUri, embeddingApiKey, keyword));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @PostMapping("/generate")
 | 
				
			||||||
 | 
					    public void generate(HttpServletResponse response, @RequestBody Map<String, String> dto) throws IOException, NoApiKeyException, InputRequiredException {
 | 
				
			||||||
 | 
					        String keyword = dto.get("keyword");
 | 
				
			||||||
 | 
					        List<SearchResult> searchResults = service.searchVector(EmbedClient.getEmbedding(embeddingUri, embeddingApiKey, keyword));
 | 
				
			||||||
 | 
					        List<String> contents = searchResults.stream().map(searchResult -> searchResult.getKnowsIndex().getContent()).toList();
 | 
				
			||||||
 | 
					        log.info("搜索结果searchResults: {} ", contents);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Object reRankPassages = "";
 | 
				
			||||||
 | 
					        if (CollUtil.isNotEmpty(searchResults)) {
 | 
				
			||||||
 | 
					            // 重排处理
 | 
				
			||||||
 | 
					            List<String> contentList = new ArrayList<>();
 | 
				
			||||||
 | 
					            searchResults.forEach(searchResult -> contentList.add(searchResult.getKnowsIndex().getContent()));
 | 
				
			||||||
 | 
					            String reRank = ReRankClient.reRank(ReRankUri, ReRankApiKey, contentList, keyword);
 | 
				
			||||||
 | 
					            log.info("重排结果reRank: {} ", reRank);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            JSONObject jsonObject = JSON.parseObject(reRank, JSONObject.class);
 | 
				
			||||||
 | 
					            reRankPassages = jsonObject.get("rerank_passages");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // LLM总结回答
 | 
				
			||||||
 | 
					        OllamaClient.sendMsg(response, ollUri, keyword, reRankPassages.toString());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @PostMapping("/qwen-generate")
 | 
				
			||||||
 | 
					    public String qwen(@RequestBody Map<String, String> dto) throws IOException, NoApiKeyException, InputRequiredException {
 | 
				
			||||||
 | 
					        String keyword = dto.get("keyword");
 | 
				
			||||||
 | 
					        List<SearchResult> searchResults = service.searchVector(EmbedClient.getEmbedding(embeddingUri, embeddingApiKey, keyword));
 | 
				
			||||||
 | 
					        List<String> contents = searchResults.stream().map(searchResult -> searchResult.getKnowsIndex().getContent()).toList();
 | 
				
			||||||
 | 
					        log.info("搜索结果searchResults: {} ", contents);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Object reRankPassages = "";
 | 
				
			||||||
 | 
					        if (CollUtil.isNotEmpty(searchResults)) {
 | 
				
			||||||
 | 
					            // 重排处理
 | 
				
			||||||
 | 
					            List<String> contentList = new ArrayList<>();
 | 
				
			||||||
 | 
					            searchResults.forEach(searchResult -> contentList.add(searchResult.getKnowsIndex().getContent()));
 | 
				
			||||||
 | 
					            String reRank = ReRankClient.reRank(ReRankUri, ReRankApiKey, contentList, keyword);
 | 
				
			||||||
 | 
					            log.info("重排结果reRank: {} ", reRank);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            JSONObject jsonObject = JSON.parseObject(reRank, JSONObject.class);
 | 
				
			||||||
 | 
					            reRankPassages = jsonObject.get("rerank_passages");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // LLM总结回答
 | 
				
			||||||
 | 
					        GenerationResult result = QwenClient.sendMsg(model, apiKey, keyword, reRankPassages.toString());
 | 
				
			||||||
 | 
					        String content = result.getOutput().getChoices().get(0).getMessage().getContent();
 | 
				
			||||||
 | 
					        log.info("千问: {}", content);
 | 
				
			||||||
 | 
					        return content;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					package cn.luckday.controller;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cn.luckday.service.RedFileService;
 | 
				
			||||||
 | 
					import org.springframework.beans.factory.annotation.Autowired;
 | 
				
			||||||
 | 
					import org.springframework.http.ResponseEntity;
 | 
				
			||||||
 | 
					import org.springframework.web.bind.annotation.*;
 | 
				
			||||||
 | 
					import org.springframework.web.multipart.MultipartFile;
 | 
				
			||||||
 | 
					import java.util.*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@RestController
 | 
				
			||||||
 | 
					@RequestMapping("/api/file")
 | 
				
			||||||
 | 
					public class RedFileController {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Autowired
 | 
				
			||||||
 | 
					  private RedFileService redFileService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @PostMapping("/upload")
 | 
				
			||||||
 | 
					  public ResponseEntity<?> uploadFile(@RequestParam("file") MultipartFile file) {
 | 
				
			||||||
 | 
					    redFileService.uploadFile(file);
 | 
				
			||||||
 | 
					    return ResponseEntity.ok(Map.of("message", "文件上传并解析成功"));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										60
									
								
								knows-java/src/main/java/cn/luckday/document/Main.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										60
									
								
								knows-java/src/main/java/cn/luckday/document/Main.java
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,60 @@
 | 
				
			|||||||
 | 
					package cn.luckday.document;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.File;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.awt.image.BufferedImage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class Main {
 | 
				
			||||||
 | 
					    public static void main(String[] args) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // 验证文件是否存在
 | 
				
			||||||
 | 
					            String pdfPath = "D:\\小红书文档\\中频\\运营经验库\\方法论\\PDF\\评论区和私信的互动指引的方法论.pdf";
 | 
				
			||||||
 | 
					            File pdfFile = new File(pdfPath);
 | 
				
			||||||
 | 
					            if (!pdfFile.exists()) {
 | 
				
			||||||
 | 
					                System.err.println("PDF文件不存在: " + pdfPath);
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 初始化PDFParser时添加错误处理
 | 
				
			||||||
 | 
					            PDFParser parser = new PDFParser(pdfPath);
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                parser.parse();
 | 
				
			||||||
 | 
					            } catch (Exception e) {
 | 
				
			||||||
 | 
					                System.err.println("PDF解析失败: " + e.getMessage());
 | 
				
			||||||
 | 
					                e.printStackTrace();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 获取结果
 | 
				
			||||||
 | 
					            List<String> texts = parser.getExtractedText();
 | 
				
			||||||
 | 
					            List<BufferedImage> images = parser.getExtractedImages();
 | 
				
			||||||
 | 
					            List<Table> tables = parser.getExtractedTables();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // // 处理Word文档
 | 
				
			||||||
 | 
					            // String wordPath = "D:\\小红书文档\\高频\\平台知识库\\已处理word\\新模式开票流程及注意事项.docx";
 | 
				
			||||||
 | 
					            // WordProcessor wordProcessor = new WordProcessor(wordPath);
 | 
				
			||||||
 | 
					            // wordProcessor.process();
 | 
				
			||||||
 | 
					            //
 | 
				
			||||||
 | 
					            // // 获取提取的文本
 | 
				
			||||||
 | 
					            // List<String> textContent = wordProcessor.getExtractedText();
 | 
				
			||||||
 | 
					            // for (String text : textContent) {
 | 
				
			||||||
 | 
					            // System.out.println(text);
 | 
				
			||||||
 | 
					            // }
 | 
				
			||||||
 | 
					            //
 | 
				
			||||||
 | 
					            // // 处理表格
 | 
				
			||||||
 | 
					            // List<XWPFTable> tables = wordProcessor.getExtractedTables();
 | 
				
			||||||
 | 
					            // for (XWPFTable table : tables) {
 | 
				
			||||||
 | 
					            // List<List<String>> tableData = wordProcessor.convertTableToList(table);
 | 
				
			||||||
 | 
					            // System.out.println("表格数据:" + tableData);
 | 
				
			||||||
 | 
					            //
 | 
				
			||||||
 | 
					            // // 导出表格为CSV
 | 
				
			||||||
 | 
					            // wordProcessor.exportTableToCSV(table, "table_output.csv");
 | 
				
			||||||
 | 
					            // }
 | 
				
			||||||
 | 
					            //
 | 
				
			||||||
 | 
					            // // 保存图片
 | 
				
			||||||
 | 
					            // wordProcessor.saveImages("output_images");
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            System.err.println("程序执行出错: " + e.getMessage());
 | 
				
			||||||
 | 
					            e.printStackTrace();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										180
									
								
								knows-java/src/main/java/cn/luckday/document/OCRProcessor.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										180
									
								
								knows-java/src/main/java/cn/luckday/document/OCRProcessor.java
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,180 @@
 | 
				
			|||||||
 | 
					package cn.luckday.document;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import net.sourceforge.tess4j.Tesseract;
 | 
				
			||||||
 | 
					import org.opencv.core.CvType;
 | 
				
			||||||
 | 
					import org.opencv.core.Mat;
 | 
				
			||||||
 | 
					import org.opencv.core.Size;
 | 
				
			||||||
 | 
					import org.opencv.imgproc.Imgproc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.awt.image.BufferedImage;
 | 
				
			||||||
 | 
					import java.io.File;
 | 
				
			||||||
 | 
					import java.awt.image.DataBufferByte;
 | 
				
			||||||
 | 
					import org.apache.pdfbox.pdmodel.PDDocument;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class OCRProcessor {
 | 
				
			||||||
 | 
					    static {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // 从资源目录加载本地库
 | 
				
			||||||
 | 
					            String libraryPath = OCRProcessor.class
 | 
				
			||||||
 | 
					                    .getClassLoader()
 | 
				
			||||||
 | 
					                    .getResource("native/" + System.mapLibraryName("opencv_java4110"))
 | 
				
			||||||
 | 
					                    .getPath();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            System.load(libraryPath);
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            e.printStackTrace();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private final Tesseract tesseract;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public OCRProcessor() {
 | 
				
			||||||
 | 
					        tesseract = new Tesseract();
 | 
				
			||||||
 | 
					        initializeTesseract();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void initializeTesseract() {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // 设置Tesseract数据路径
 | 
				
			||||||
 | 
					            String tessdataPath = System.getenv("TESSDATA_PREFIX");
 | 
				
			||||||
 | 
					            if (tessdataPath == null || tessdataPath.isEmpty()) {
 | 
				
			||||||
 | 
					                tessdataPath ="D:\\study\\backend\\embeddingstoes-master\\src\\main\\resources\\ocr";
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            tesseract.setDatapath(tessdataPath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 修改:使用不依赖OSD的页面分割模式
 | 
				
			||||||
 | 
					            tesseract.setPageSegMode(3);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 设置语言包
 | 
				
			||||||
 | 
					            tesseract.setLanguage("chi_sim");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 性能优化配置
 | 
				
			||||||
 | 
					            tesseract.setTessVariable("tessedit_create_pdf", "0");
 | 
				
			||||||
 | 
					            tesseract.setTessVariable("tessedit_create_hocr", "0");
 | 
				
			||||||
 | 
					            tesseract.setTessVariable("tessedit_write_images", "0");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            throw new RuntimeException("Tesseract 初始化失败: " + e.getMessage(), e);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public String performOCR(BufferedImage image) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // 基本图像验证
 | 
				
			||||||
 | 
					            if (image == null || image.getWidth() < 10 || image.getHeight() < 10) {
 | 
				
			||||||
 | 
					                throw new IllegalArgumentException("无效的图像");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 预处理图像
 | 
				
			||||||
 | 
					            BufferedImage processedImage = preprocessImage(image);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 执行OCR
 | 
				
			||||||
 | 
					            return tesseract.doOCR(processedImage);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            System.err.println("OCR处理失败: " + e.getMessage());
 | 
				
			||||||
 | 
					            e.printStackTrace();
 | 
				
			||||||
 | 
					            return "";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private BufferedImage preprocessImage(BufferedImage image) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            Mat mat = bufferedImageToMat(image);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 调整预处理步骤
 | 
				
			||||||
 | 
					            // 1. 转换为灰度图
 | 
				
			||||||
 | 
					            Mat gray = new Mat();
 | 
				
			||||||
 | 
					            Imgproc.cvtColor(mat, gray, Imgproc.COLOR_BGR2GRAY);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 2. 使用OTSU二值化替代自适应阈值
 | 
				
			||||||
 | 
					            Mat binary = new Mat();
 | 
				
			||||||
 | 
					            Imgproc.threshold(gray, binary, 0, 255, Imgproc.THRESH_BINARY + Imgproc.THRESH_OTSU);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 3. 添加形态学操作
 | 
				
			||||||
 | 
					            Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3));
 | 
				
			||||||
 | 
					            Mat processed = new Mat();
 | 
				
			||||||
 | 
					            Imgproc.morphologyEx(binary, processed, Imgproc.MORPH_CLOSE, kernel);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 4. 边缘增强
 | 
				
			||||||
 | 
					            Mat enhanced = new Mat();
 | 
				
			||||||
 | 
					            Imgproc.GaussianBlur(processed, enhanced, new Size(3, 3), 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return matToBufferedImage(enhanced);
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            e.printStackTrace();
 | 
				
			||||||
 | 
					            return image;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private Mat bufferedImageToMat(BufferedImage image) {
 | 
				
			||||||
 | 
					        // 转换图像类型为 TYPE_3BYTE_BGR,如果需要的话
 | 
				
			||||||
 | 
					        BufferedImage convertedImage = image;
 | 
				
			||||||
 | 
					        if (image.getType() != BufferedImage.TYPE_3BYTE_BGR) {
 | 
				
			||||||
 | 
					            convertedImage = new BufferedImage(
 | 
				
			||||||
 | 
					                    image.getWidth(),
 | 
				
			||||||
 | 
					                    image.getHeight(),
 | 
				
			||||||
 | 
					                    BufferedImage.TYPE_3BYTE_BGR);
 | 
				
			||||||
 | 
					            convertedImage.getGraphics().drawImage(image, 0, 0, null);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 获取图像数据
 | 
				
			||||||
 | 
					        byte[] pixels = ((DataBufferByte) convertedImage.getRaster().getDataBuffer()).getData();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 创建Mat对象
 | 
				
			||||||
 | 
					        Mat mat = new Mat(
 | 
				
			||||||
 | 
					                convertedImage.getHeight(),
 | 
				
			||||||
 | 
					                convertedImage.getWidth(),
 | 
				
			||||||
 | 
					                CvType.CV_8UC3);
 | 
				
			||||||
 | 
					        mat.put(0, 0, pixels);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return mat;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private BufferedImage matToBufferedImage(Mat mat) {
 | 
				
			||||||
 | 
					        // 确保mat是8位3通道或单通道
 | 
				
			||||||
 | 
					        int type = BufferedImage.TYPE_3BYTE_BGR;
 | 
				
			||||||
 | 
					        if (mat.channels() == 1) {
 | 
				
			||||||
 | 
					            type = BufferedImage.TYPE_BYTE_GRAY;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 获取mat的数据
 | 
				
			||||||
 | 
					        byte[] pixels = new byte[mat.channels() * mat.cols() * mat.rows()];
 | 
				
			||||||
 | 
					        mat.get(0, 0, pixels);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 创建BufferedImage
 | 
				
			||||||
 | 
					        BufferedImage image = new BufferedImage(
 | 
				
			||||||
 | 
					                mat.cols(),
 | 
				
			||||||
 | 
					                mat.rows(),
 | 
				
			||||||
 | 
					                type);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 设置图像数据
 | 
				
			||||||
 | 
					        byte[] targetPixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
 | 
				
			||||||
 | 
					        System.arraycopy(pixels, 0, targetPixels, 0, pixels.length);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return image;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public void processPDF(String pdfPath) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // 添加内存使用监控
 | 
				
			||||||
 | 
					            Runtime runtime = Runtime.getRuntime();
 | 
				
			||||||
 | 
					            long maxMemory = runtime.maxMemory() / (1024 * 1024);
 | 
				
			||||||
 | 
					            System.out.println("最大可用内存: " + maxMemory + "MB");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 原有PDF处理代码
 | 
				
			||||||
 | 
					            PDDocument document = PDDocument.load(new File(pdfPath));
 | 
				
			||||||
 | 
					            // ... existing code ...
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 确保资源释放
 | 
				
			||||||
 | 
					            document.close();
 | 
				
			||||||
 | 
					        } catch (OutOfMemoryError e) {
 | 
				
			||||||
 | 
					            System.err.println("内存不足: " + e.getMessage());
 | 
				
			||||||
 | 
					            // TODO 日志记录
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            System.err.println("处理PDF时发生错误: " + e.getMessage());
 | 
				
			||||||
 | 
					            e.printStackTrace();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										137
									
								
								knows-java/src/main/java/cn/luckday/document/PDFParser.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										137
									
								
								knows-java/src/main/java/cn/luckday/document/PDFParser.java
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,137 @@
 | 
				
			|||||||
 | 
					package cn.luckday.document;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.apache.pdfbox.pdmodel.PDDocument;
 | 
				
			||||||
 | 
					import org.apache.pdfbox.pdmodel.PDPage;
 | 
				
			||||||
 | 
					import org.apache.pdfbox.text.PDFTextStripper;
 | 
				
			||||||
 | 
					import org.apache.pdfbox.cos.COSName;
 | 
				
			||||||
 | 
					import org.apache.pdfbox.pdmodel.graphics.PDXObject;
 | 
				
			||||||
 | 
					import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import javax.imageio.ImageIO;
 | 
				
			||||||
 | 
					import java.awt.image.BufferedImage;
 | 
				
			||||||
 | 
					import java.io.File;
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.util.ArrayList;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class PDFParser {
 | 
				
			||||||
 | 
					    private final String pdfPath;
 | 
				
			||||||
 | 
					    private PDDocument document;
 | 
				
			||||||
 | 
					    private final OCRProcessor ocrProcessor;
 | 
				
			||||||
 | 
					    private List<String> extractedText;
 | 
				
			||||||
 | 
					    private List<BufferedImage> extractedImages;
 | 
				
			||||||
 | 
					    private List<Table> extractedTables;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public PDFParser(String pdfPath) {
 | 
				
			||||||
 | 
					        this.pdfPath = pdfPath;
 | 
				
			||||||
 | 
					        this.ocrProcessor = new OCRProcessor();
 | 
				
			||||||
 | 
					        this.extractedText = new ArrayList<>();
 | 
				
			||||||
 | 
					        this.extractedImages = new ArrayList<>();
 | 
				
			||||||
 | 
					        this.extractedTables = new ArrayList<>();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public void parse() {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            document = PDDocument.load(new File(pdfPath));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 1. 解析文本内容
 | 
				
			||||||
 | 
					            System.out.println("=== 开始解析文本 ===");
 | 
				
			||||||
 | 
					            extractText();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 2. 解析图片
 | 
				
			||||||
 | 
					            System.out.println("\n=== 开始解析图片 ===");
 | 
				
			||||||
 | 
					            extractImages();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 3. 解析表格
 | 
				
			||||||
 | 
					//            System.out.println("\n=== 开始解析表格 ===");
 | 
				
			||||||
 | 
					//            extractTables();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            document.close();
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            System.err.println("PDF解析失败: " + e.getMessage());
 | 
				
			||||||
 | 
					            e.printStackTrace();
 | 
				
			||||||
 | 
					            if (document != null) {
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    document.close();
 | 
				
			||||||
 | 
					                } catch (IOException ignored) {
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void extractText() throws IOException {
 | 
				
			||||||
 | 
					        System.out.println("正在提取PDF文本...");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 只使用PDFTextStripper提取文本
 | 
				
			||||||
 | 
					        PDFTextStripper stripper = new PDFTextStripper();
 | 
				
			||||||
 | 
					        String text = stripper.getText(document);
 | 
				
			||||||
 | 
					        System.out.println("文本内容:\n" + text);
 | 
				
			||||||
 | 
					        extractedText.add(text);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void extractImages() throws IOException {
 | 
				
			||||||
 | 
					        System.out.println("正在提取并处理PDF图片...");
 | 
				
			||||||
 | 
					        int imageCounter = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (PDPage page : document.getPages()) {
 | 
				
			||||||
 | 
					            for (COSName name : page.getResources().getXObjectNames()) {
 | 
				
			||||||
 | 
					                PDXObject object = page.getResources().getXObject(name);
 | 
				
			||||||
 | 
					                if (object instanceof PDImageXObject) {
 | 
				
			||||||
 | 
					                    PDImageXObject image = (PDImageXObject) object;
 | 
				
			||||||
 | 
					                    BufferedImage bImage = image.getImage();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // 保存图片
 | 
				
			||||||
 | 
					                    String imagePath = "output_images/extracted_image_" + imageCounter + ".png";
 | 
				
			||||||
 | 
					                    ImageIO.write(bImage, "PNG", new File(imagePath));
 | 
				
			||||||
 | 
					                    System.out.println("已保存图片: " + imagePath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // OCR处理图片
 | 
				
			||||||
 | 
					                    try {
 | 
				
			||||||
 | 
					                        System.out.println("正在对图片 " + imageCounter + " 进行OCR处理...");
 | 
				
			||||||
 | 
					                        String imageText = ocrProcessor.performOCR(bImage);
 | 
				
			||||||
 | 
					                        if (!imageText.trim().isEmpty()) {
 | 
				
			||||||
 | 
					                            System.out.println("图片 " + imageCounter + " OCR结果:\n" + imageText);
 | 
				
			||||||
 | 
					                            extractedText.add("【图片" + imageCounter + "文本】\n" + imageText);
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            System.out.println("图片 " + imageCounter + " 未识别出文本");
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    } catch (Exception e) {
 | 
				
			||||||
 | 
					                        System.err.println("处理图片 " + imageCounter + " 时出错: " + e.getMessage());
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    extractedImages.add(bImage);
 | 
				
			||||||
 | 
					                    imageCounter++;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        System.out.println("共处理 " + imageCounter + " 张图片");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void extractTables() {
 | 
				
			||||||
 | 
					        System.out.println("正在提取PDF表格...");
 | 
				
			||||||
 | 
					        TableDetector detector = new TableDetector(document);
 | 
				
			||||||
 | 
					        extractedTables = detector.detectTables();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (extractedTables.isEmpty()) {
 | 
				
			||||||
 | 
					            System.out.println("未检测到表格");
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            System.out.println("共检测到 " + extractedTables.size() + " 个表格");
 | 
				
			||||||
 | 
					            for (int i = 0; i < extractedTables.size(); i++) {
 | 
				
			||||||
 | 
					                System.out.println("表格 " + (i + 1) + ":\n" + extractedTables.get(i));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Getter方法
 | 
				
			||||||
 | 
					    public List<String> getExtractedText() {
 | 
				
			||||||
 | 
					        return extractedText;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public List<BufferedImage> getExtractedImages() {
 | 
				
			||||||
 | 
					        return extractedImages;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public List<Table> getExtractedTables() {
 | 
				
			||||||
 | 
					        return extractedTables;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										43
									
								
								knows-java/src/main/java/cn/luckday/document/Table.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										43
									
								
								knows-java/src/main/java/cn/luckday/document/Table.java
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					package cn.luckday.document;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class Table {
 | 
				
			||||||
 | 
					    private String content;
 | 
				
			||||||
 | 
					    private int rows;
 | 
				
			||||||
 | 
					    private int columns;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public Table(String content) {
 | 
				
			||||||
 | 
					        this.content = content;
 | 
				
			||||||
 | 
					        analyzeStructure();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void analyzeStructure() {
 | 
				
			||||||
 | 
					        if (content == null || content.isEmpty()) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 按行分割内容
 | 
				
			||||||
 | 
					        String[] lines = content.split("\n");
 | 
				
			||||||
 | 
					        rows = lines.length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 分析列数(基于空格或制表符分隔)
 | 
				
			||||||
 | 
					        columns = 0;
 | 
				
			||||||
 | 
					        for (String line : lines) {
 | 
				
			||||||
 | 
					            String[] cells = line.trim().split("\\s+");
 | 
				
			||||||
 | 
					            columns = Math.max(columns, cells.length);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public int getRows() {
 | 
				
			||||||
 | 
					        return rows;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public int getColumns() {
 | 
				
			||||||
 | 
					        return columns;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public String toString() {
 | 
				
			||||||
 | 
					        return String.format("Table{rows=%d, columns=%d, content='%s'}",
 | 
				
			||||||
 | 
					                rows, columns, content);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										170
									
								
								knows-java/src/main/java/cn/luckday/document/TableDetector.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										170
									
								
								knows-java/src/main/java/cn/luckday/document/TableDetector.java
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,170 @@
 | 
				
			|||||||
 | 
					package cn.luckday.document;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.apache.pdfbox.pdmodel.PDDocument;
 | 
				
			||||||
 | 
					import org.apache.pdfbox.pdmodel.PDPage;
 | 
				
			||||||
 | 
					import org.apache.pdfbox.text.PDFTextStripperByArea;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.awt.Rectangle;
 | 
				
			||||||
 | 
					import java.util.ArrayList;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class TableDetector {
 | 
				
			||||||
 | 
					    private final PDDocument document;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public TableDetector(PDDocument document) {
 | 
				
			||||||
 | 
					        this.document = document;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public List<Table> detectTables() {
 | 
				
			||||||
 | 
					        List<Table> tables = new ArrayList<>();
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            for (PDPage page : document.getPages()) {
 | 
				
			||||||
 | 
					                // 使用文本位置分析来检测表格
 | 
				
			||||||
 | 
					                PDFTextStripperByArea stripper = new PDFTextStripperByArea();
 | 
				
			||||||
 | 
					                stripper.setSortByPosition(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // 检测表格边界
 | 
				
			||||||
 | 
					                List<Rectangle> tableRegions = detectTableRegions(page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                for (Rectangle region : tableRegions) {
 | 
				
			||||||
 | 
					                    stripper.addRegion("table", region);
 | 
				
			||||||
 | 
					                    stripper.extractRegions(page);
 | 
				
			||||||
 | 
					                    String tableContent = stripper.getTextForRegion("table");
 | 
				
			||||||
 | 
					                    tables.add(new Table(tableContent));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            e.printStackTrace();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return tables;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private List<Rectangle> detectTableRegions(PDPage page) {
 | 
				
			||||||
 | 
					        List<Rectangle> regions = new ArrayList<>();
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // 获取页面尺寸
 | 
				
			||||||
 | 
					            float pageHeight = page.getMediaBox().getHeight();
 | 
				
			||||||
 | 
					            float pageWidth = page.getMediaBox().getWidth();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 使用PDFTextStripperByArea进行文本分析
 | 
				
			||||||
 | 
					            PDFTextStripperByArea stripper = new PDFTextStripperByArea();
 | 
				
			||||||
 | 
					            stripper.setSortByPosition(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 将页面划分为网格进行分析
 | 
				
			||||||
 | 
					            int gridRows = 20;
 | 
				
			||||||
 | 
					            int gridCols = 20;
 | 
				
			||||||
 | 
					            float cellHeight = pageHeight / gridRows;
 | 
				
			||||||
 | 
					            float cellWidth = pageWidth / gridCols;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 存储每个网格单元的文本密度
 | 
				
			||||||
 | 
					            int[][] textDensity = new int[gridRows][gridCols];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 分析每个网格单元
 | 
				
			||||||
 | 
					            for (int row = 0; row < gridRows; row++) {
 | 
				
			||||||
 | 
					                for (int col = 0; col < gridCols; col++) {
 | 
				
			||||||
 | 
					                    Rectangle cell = new Rectangle(
 | 
				
			||||||
 | 
					                            (int) (col * cellWidth),
 | 
				
			||||||
 | 
					                            (int) (row * cellHeight),
 | 
				
			||||||
 | 
					                            (int) cellWidth,
 | 
				
			||||||
 | 
					                            (int) cellHeight);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    stripper.addRegion("cell_" + row + "_" + col, cell);
 | 
				
			||||||
 | 
					                    stripper.extractRegions(page);
 | 
				
			||||||
 | 
					                    String cellText = stripper.getTextForRegion("cell_" + row + "_" + col);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // 计算文本密度
 | 
				
			||||||
 | 
					                    textDensity[row][col] = cellText.trim().length();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 检测表格区域
 | 
				
			||||||
 | 
					            List<TableRegion> potentialTables = findPotentialTables(textDensity, gridRows, gridCols);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 转换检测到的区域为实际坐标
 | 
				
			||||||
 | 
					            for (TableRegion tableRegion : potentialTables) {
 | 
				
			||||||
 | 
					                Rectangle rect = new Rectangle(
 | 
				
			||||||
 | 
					                        (int) (tableRegion.startCol * cellWidth),
 | 
				
			||||||
 | 
					                        (int) (tableRegion.startRow * cellHeight),
 | 
				
			||||||
 | 
					                        (int) ((tableRegion.endCol - tableRegion.startCol + 1) * cellWidth),
 | 
				
			||||||
 | 
					                        (int) ((tableRegion.endRow - tableRegion.startRow + 1) * cellHeight));
 | 
				
			||||||
 | 
					                regions.add(rect);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            e.printStackTrace();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return regions;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private List<TableRegion> findPotentialTables(int[][] textDensity, int rows, int cols) {
 | 
				
			||||||
 | 
					        List<TableRegion> tables = new ArrayList<>();
 | 
				
			||||||
 | 
					        boolean[][] visited = new boolean[rows][cols];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 遍历网格寻找潜在的表格区域
 | 
				
			||||||
 | 
					        for (int i = 0; i < rows; i++) {
 | 
				
			||||||
 | 
					            for (int j = 0; j < cols; j++) {
 | 
				
			||||||
 | 
					                if (!visited[i][j] && isTableCell(textDensity, i, j)) {
 | 
				
			||||||
 | 
					                    TableRegion region = new TableRegion();
 | 
				
			||||||
 | 
					                    expandTableRegion(textDensity, visited, i, j, region);
 | 
				
			||||||
 | 
					                    if (isValidTable(region)) {
 | 
				
			||||||
 | 
					                        tables.add(region);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return tables;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private boolean isTableCell(int[][] density, int row, int col) {
 | 
				
			||||||
 | 
					        // 判断是否为表格单元格的条件
 | 
				
			||||||
 | 
					        // 1. 文本密度适中
 | 
				
			||||||
 | 
					        // 2. 周围有类似的文本密度分布
 | 
				
			||||||
 | 
					        int cellDensity = density[row][col];
 | 
				
			||||||
 | 
					        return cellDensity > 0 && cellDensity < 100; // 可调整阈值
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void expandTableRegion(int[][] density, boolean[][] visited,
 | 
				
			||||||
 | 
					                                   int row, int col, TableRegion region) {
 | 
				
			||||||
 | 
					        if (row < 0 || row >= density.length ||
 | 
				
			||||||
 | 
					                col < 0 || col >= density[0].length ||
 | 
				
			||||||
 | 
					                visited[row][col] ||
 | 
				
			||||||
 | 
					                !isTableCell(density, row, col)) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        visited[row][col] = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 更新表格区域的边界
 | 
				
			||||||
 | 
					        region.updateBounds(row, col);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 递归检查相邻单元格
 | 
				
			||||||
 | 
					        expandTableRegion(density, visited, row - 1, col, region); // 上
 | 
				
			||||||
 | 
					        expandTableRegion(density, visited, row + 1, col, region); // 下
 | 
				
			||||||
 | 
					        expandTableRegion(density, visited, row, col - 1, region); // 左
 | 
				
			||||||
 | 
					        expandTableRegion(density, visited, row, col + 1, region); // 右
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private boolean isValidTable(TableRegion region) {
 | 
				
			||||||
 | 
					        // 验证检测到的区域是否可能是表格
 | 
				
			||||||
 | 
					        int width = region.endCol - region.startCol + 1;
 | 
				
			||||||
 | 
					        int height = region.endRow - region.startRow + 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 表格至少应该有2x2的大小
 | 
				
			||||||
 | 
					        return width >= 2 && height >= 2;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 表格区域数据结构
 | 
				
			||||||
 | 
					    private static class TableRegion {
 | 
				
			||||||
 | 
					        int startRow = Integer.MAX_VALUE;
 | 
				
			||||||
 | 
					        int startCol = Integer.MAX_VALUE;
 | 
				
			||||||
 | 
					        int endRow = Integer.MIN_VALUE;
 | 
				
			||||||
 | 
					        int endCol = Integer.MIN_VALUE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        void updateBounds(int row, int col) {
 | 
				
			||||||
 | 
					            startRow = Math.min(startRow, row);
 | 
				
			||||||
 | 
					            startCol = Math.min(startCol, col);
 | 
				
			||||||
 | 
					            endRow = Math.max(endRow, row);
 | 
				
			||||||
 | 
					            endCol = Math.max(endCol, col);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										287
									
								
								knows-java/src/main/java/cn/luckday/document/WordProcessor.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										287
									
								
								knows-java/src/main/java/cn/luckday/document/WordProcessor.java
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,287 @@
 | 
				
			|||||||
 | 
					package cn.luckday.document;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.apache.poi.xwpf.usermodel.*;
 | 
				
			||||||
 | 
					import org.apache.poi.hwpf.HWPFDocument;
 | 
				
			||||||
 | 
					import org.apache.poi.hwpf.usermodel.Range;
 | 
				
			||||||
 | 
					import org.apache.poi.hwpf.usermodel.Table;
 | 
				
			||||||
 | 
					import org.apache.poi.hwpf.usermodel.TableRow;
 | 
				
			||||||
 | 
					import org.apache.poi.hwpf.usermodel.TableCell;
 | 
				
			||||||
 | 
					import org.apache.poi.poifs.filesystem.POIFSFileSystem;
 | 
				
			||||||
 | 
					import java.nio.file.Files;
 | 
				
			||||||
 | 
					import java.nio.file.Path;
 | 
				
			||||||
 | 
					import java.nio.file.Paths;
 | 
				
			||||||
 | 
					import org.apache.poi.common.usermodel.PictureType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.File;
 | 
				
			||||||
 | 
					import java.io.FileInputStream;
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.util.ArrayList;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class WordProcessor {
 | 
				
			||||||
 | 
					    private final String filePath;
 | 
				
			||||||
 | 
					    private List<String> extractedText;
 | 
				
			||||||
 | 
					    private List<XWPFTable> extractedTables;
 | 
				
			||||||
 | 
					    private List<XWPFPicture> extractedImages;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public WordProcessor(String filePath) {
 | 
				
			||||||
 | 
					        this.filePath = filePath;
 | 
				
			||||||
 | 
					        this.extractedText = new ArrayList<>();
 | 
				
			||||||
 | 
					        this.extractedTables = new ArrayList<>();
 | 
				
			||||||
 | 
					        this.extractedImages = new ArrayList<>();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public void process() {
 | 
				
			||||||
 | 
					        File file = new File(filePath);
 | 
				
			||||||
 | 
					        if (filePath.endsWith(".docx")) {
 | 
				
			||||||
 | 
					            processDocx(file);
 | 
				
			||||||
 | 
					        } else if (filePath.endsWith(".doc")) {
 | 
				
			||||||
 | 
					            processDoc(file);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            throw new IllegalArgumentException("不支持的文件格式:" + filePath);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void processDocx(File file) {
 | 
				
			||||||
 | 
					        try (FileInputStream fis = new FileInputStream(file);
 | 
				
			||||||
 | 
					             XWPFDocument document = new XWPFDocument(fis)) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 提取文本
 | 
				
			||||||
 | 
					            extractTextFromDocx(document);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 提取表格
 | 
				
			||||||
 | 
					            extractTablesFromDocx(document);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 提取图片
 | 
				
			||||||
 | 
					            extractImagesFromDocx(document);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } catch (IOException e) {
 | 
				
			||||||
 | 
					            e.printStackTrace();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void processDoc(File file) {
 | 
				
			||||||
 | 
					        try (FileInputStream fis = new FileInputStream(file);
 | 
				
			||||||
 | 
					             POIFSFileSystem fs = new POIFSFileSystem(fis)) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            HWPFDocument document = new HWPFDocument(fs);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 提取文本
 | 
				
			||||||
 | 
					            Range range = document.getRange();
 | 
				
			||||||
 | 
					            extractTextFromDoc(range);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 提取表格
 | 
				
			||||||
 | 
					            extractTablesFromDoc(range);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 提取图片(如果可能)
 | 
				
			||||||
 | 
					            extractImagesFromDoc(document);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } catch (IOException e) {
 | 
				
			||||||
 | 
					            e.printStackTrace();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void extractTextFromDocx(XWPFDocument document) {
 | 
				
			||||||
 | 
					        // 提取段落文本
 | 
				
			||||||
 | 
					        for (XWPFParagraph paragraph : document.getParagraphs()) {
 | 
				
			||||||
 | 
					            String text = paragraph.getText().trim();
 | 
				
			||||||
 | 
					            if (!text.isEmpty()) {
 | 
				
			||||||
 | 
					                extractedText.add(text);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void extractTablesFromDocx(XWPFDocument document) {
 | 
				
			||||||
 | 
					        // 提取表格
 | 
				
			||||||
 | 
					        for (XWPFTable table : document.getTables()) {
 | 
				
			||||||
 | 
					            extractedTables.add(table);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 处理表格内容
 | 
				
			||||||
 | 
					            for (XWPFTableRow row : table.getRows()) {
 | 
				
			||||||
 | 
					                StringBuilder rowContent = new StringBuilder();
 | 
				
			||||||
 | 
					                for (XWPFTableCell cell : row.getTableCells()) {
 | 
				
			||||||
 | 
					                    rowContent.append(cell.getText()).append("\t");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                extractedText.add("表格行:" + rowContent.toString().trim());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void extractImagesFromDocx(XWPFDocument document) {
 | 
				
			||||||
 | 
					        // 提取图片
 | 
				
			||||||
 | 
					        for (XWPFParagraph paragraph : document.getParagraphs()) {
 | 
				
			||||||
 | 
					            for (XWPFRun run : paragraph.getRuns()) {
 | 
				
			||||||
 | 
					                List<XWPFPicture> pictures = run.getEmbeddedPictures();
 | 
				
			||||||
 | 
					                extractedImages.addAll(pictures);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void extractTextFromDoc(Range range) {
 | 
				
			||||||
 | 
					        String text = range.text();
 | 
				
			||||||
 | 
					        // 按段落分割
 | 
				
			||||||
 | 
					        String[] paragraphs = text.split("\\r?\\n");
 | 
				
			||||||
 | 
					        for (String paragraph : paragraphs) {
 | 
				
			||||||
 | 
					            if (!paragraph.trim().isEmpty()) {
 | 
				
			||||||
 | 
					                extractedText.add(paragraph.trim());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void extractTablesFromDoc(Range range) {
 | 
				
			||||||
 | 
					        for (int i = 0; i < range.numParagraphs(); i++) {
 | 
				
			||||||
 | 
					            if (range.getParagraph(i).isInTable()) {
 | 
				
			||||||
 | 
					                Table table = range.getTable(range.getParagraph(i));
 | 
				
			||||||
 | 
					                processDocTable(table);
 | 
				
			||||||
 | 
					                // 跳过表格中的其他段落
 | 
				
			||||||
 | 
					                i += table.numParagraphs() - 1;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void processDocTable(Table table) {
 | 
				
			||||||
 | 
					        List<List<String>> tableData = new ArrayList<>();
 | 
				
			||||||
 | 
					        for (int rowIdx = 0; rowIdx < table.numRows(); rowIdx++) {
 | 
				
			||||||
 | 
					            TableRow row = table.getRow(rowIdx);
 | 
				
			||||||
 | 
					            List<String> rowData = new ArrayList<>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for (int colIdx = 0; colIdx < row.numCells(); colIdx++) {
 | 
				
			||||||
 | 
					                TableCell cell = row.getCell(colIdx);
 | 
				
			||||||
 | 
					                String cellText = cell.text().trim();
 | 
				
			||||||
 | 
					                if (cellText.endsWith("\u0007")) {
 | 
				
			||||||
 | 
					                    cellText = cellText.substring(0, cellText.length() - 1);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                rowData.add(cellText);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            tableData.add(rowData);
 | 
				
			||||||
 | 
					            extractedText.add("表格行:" + String.join("\t", rowData));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void extractImagesFromDoc(HWPFDocument document) {
 | 
				
			||||||
 | 
					        // 注意:HWPF对图片的支持有限
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            List<org.apache.poi.hwpf.usermodel.Picture> pictures = document.getPicturesTable().getAllPictures();
 | 
				
			||||||
 | 
					            File outputDir = new File("output_images");
 | 
				
			||||||
 | 
					            if (!outputDir.exists()) {
 | 
				
			||||||
 | 
					                outputDir.mkdirs();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            int imageCounter = 0;
 | 
				
			||||||
 | 
					            for (org.apache.poi.hwpf.usermodel.Picture picture : pictures) {
 | 
				
			||||||
 | 
					                String extension = picture.suggestFileExtension();
 | 
				
			||||||
 | 
					                String filename = String.format("doc_image_%d.%s", imageCounter++, extension);
 | 
				
			||||||
 | 
					                Path outputPath = Paths.get(outputDir.getPath(), filename);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // 保存图片数据
 | 
				
			||||||
 | 
					                Files.write(outputPath, picture.getContent());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            System.out.println("警告:提取.doc文件中的图片时出错:" + e.getMessage());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public void saveImages(String outputDir) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            File dir = new File(outputDir);
 | 
				
			||||||
 | 
					            if (!dir.exists()) {
 | 
				
			||||||
 | 
					                dir.mkdirs();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            int imageCounter = 0;
 | 
				
			||||||
 | 
					            for (XWPFPicture picture : extractedImages) {
 | 
				
			||||||
 | 
					                // 获取图片数据
 | 
				
			||||||
 | 
					                byte[] pictureData = picture.getPictureData().getData();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // 确定图片扩展名
 | 
				
			||||||
 | 
					                String extension = getImageExtension(picture.getPictureData().getPictureType());
 | 
				
			||||||
 | 
					                String filename = String.format("image_%d.%s", imageCounter++, extension);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // 保存图片
 | 
				
			||||||
 | 
					                Path outputPath = Paths.get(dir.getPath(), filename);
 | 
				
			||||||
 | 
					                Files.write(outputPath, pictureData);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (IOException e) {
 | 
				
			||||||
 | 
					            e.printStackTrace();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private String getImageExtension(int pictureType) {
 | 
				
			||||||
 | 
					        // 使用PictureType的常量来处理图片类型
 | 
				
			||||||
 | 
					        if (pictureType == PictureType.PNG.getOoxmlId()) {
 | 
				
			||||||
 | 
					            return "png";
 | 
				
			||||||
 | 
					        } else if (pictureType == PictureType.JPEG.getOoxmlId()) {
 | 
				
			||||||
 | 
					            return "jpg";
 | 
				
			||||||
 | 
					        } else if (pictureType == PictureType.GIF.getOoxmlId()) {
 | 
				
			||||||
 | 
					            return "gif";
 | 
				
			||||||
 | 
					        } else if (pictureType == PictureType.TIFF.getOoxmlId()) {
 | 
				
			||||||
 | 
					            return "tiff";
 | 
				
			||||||
 | 
					        } else if (pictureType == PictureType.BMP.getOoxmlId()) {
 | 
				
			||||||
 | 
					            return "bmp";
 | 
				
			||||||
 | 
					        } else if (pictureType == PictureType.EMF.getOoxmlId()) {
 | 
				
			||||||
 | 
					            return "emf";
 | 
				
			||||||
 | 
					        } else if (pictureType == PictureType.WMF.getOoxmlId()) {
 | 
				
			||||||
 | 
					            return "wmf";
 | 
				
			||||||
 | 
					        } else if (pictureType == PictureType.PICT.getOoxmlId()) {
 | 
				
			||||||
 | 
					            return "pict";
 | 
				
			||||||
 | 
					        } else if (pictureType == PictureType.DIB.getOoxmlId()) {
 | 
				
			||||||
 | 
					            return "dib";
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            return "unknown";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public List<String> getExtractedText() {
 | 
				
			||||||
 | 
					        return extractedText;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public List<XWPFTable> getExtractedTables() {
 | 
				
			||||||
 | 
					        return extractedTables;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public List<XWPFPicture> getExtractedImages() {
 | 
				
			||||||
 | 
					        return extractedImages;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 将表格转换为结构化数据
 | 
				
			||||||
 | 
					    public List<List<String>> convertTableToList(XWPFTable table) {
 | 
				
			||||||
 | 
					        List<List<String>> tableData = new ArrayList<>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (XWPFTableRow row : table.getRows()) {
 | 
				
			||||||
 | 
					            List<String> rowData = new ArrayList<>();
 | 
				
			||||||
 | 
					            for (XWPFTableCell cell : row.getTableCells()) {
 | 
				
			||||||
 | 
					                rowData.add(cell.getText().trim());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            tableData.add(rowData);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return tableData;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 导出表格为CSV格式
 | 
				
			||||||
 | 
					    public void exportTableToCSV(XWPFTable table, String outputPath) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            StringBuilder csv = new StringBuilder();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for (XWPFTableRow row : table.getRows()) {
 | 
				
			||||||
 | 
					                List<String> rowData = new ArrayList<>();
 | 
				
			||||||
 | 
					                for (XWPFTableCell cell : row.getTableCells()) {
 | 
				
			||||||
 | 
					                    // 处理CSV中的特殊字符
 | 
				
			||||||
 | 
					                    String cellText = cell.getText().trim()
 | 
				
			||||||
 | 
					                            .replace("\"", "\"\"")
 | 
				
			||||||
 | 
					                            .replace(",", "\",\"");
 | 
				
			||||||
 | 
					                    rowData.add("\"" + cellText + "\"");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                csv.append(String.join(",", rowData)).append("\n");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            java.nio.file.Files.write(
 | 
				
			||||||
 | 
					                    new File(outputPath).toPath(),
 | 
				
			||||||
 | 
					                    csv.toString().getBytes());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } catch (IOException e) {
 | 
				
			||||||
 | 
					            e.printStackTrace();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										83
									
								
								knows-java/src/main/java/cn/luckday/embed/EmbedClient.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										83
									
								
								knows-java/src/main/java/cn/luckday/embed/EmbedClient.java
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,83 @@
 | 
				
			|||||||
 | 
					package cn.luckday.embed;
 | 
				
			||||||
 | 
					import com.alibaba.fastjson.JSON;
 | 
				
			||||||
 | 
					import com.alibaba.fastjson.JSONObject;
 | 
				
			||||||
 | 
					import okhttp3.*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.util.*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class EmbedClient {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static double[] getEmbedding(String uri, String apiKey, String inputText) throws IOException {
 | 
				
			||||||
 | 
					        OkHttpClient client = new OkHttpClient();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 创建请求体
 | 
				
			||||||
 | 
					        JSONObject requestBody = new JSONObject();
 | 
				
			||||||
 | 
					        requestBody.put("input", Collections.singletonList(inputText));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 创建请求
 | 
				
			||||||
 | 
					        MediaType mediaType = MediaType.parse("application/json; charset=utf-8");
 | 
				
			||||||
 | 
					        RequestBody body = RequestBody.Companion.create(requestBody.toJSONString(), mediaType);
 | 
				
			||||||
 | 
					        Request request = new Request.Builder()
 | 
				
			||||||
 | 
					                .url(uri)
 | 
				
			||||||
 | 
					                .addHeader("Authorization", "Bearer " + apiKey)
 | 
				
			||||||
 | 
					                .addHeader("Content-Type", "application/json")
 | 
				
			||||||
 | 
					                .post(body)
 | 
				
			||||||
 | 
					                .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 发送请求
 | 
				
			||||||
 | 
					        Response response = client.newCall(request).execute();
 | 
				
			||||||
 | 
					        if (!response.isSuccessful()) {
 | 
				
			||||||
 | 
					            throw new IOException("Unexpected code " + response);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 解析JSON响应
 | 
				
			||||||
 | 
					        String responseBody = response.body().string();
 | 
				
			||||||
 | 
					        EmbeddingResponse embeddingResponse = JSON.parseObject(responseBody, EmbeddingResponse.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 返回嵌入向量
 | 
				
			||||||
 | 
					        return embeddingResponse.getData().get(0).getEmbedding();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    static class EmbeddingResponse {
 | 
				
			||||||
 | 
					        private List<Data> data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public List<Data> getData() {
 | 
				
			||||||
 | 
					            return data;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public void setData(List<Data> data) {
 | 
				
			||||||
 | 
					            this.data = data;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    static class Data {
 | 
				
			||||||
 | 
					        private double[] embedding;
 | 
				
			||||||
 | 
					        private int index;
 | 
				
			||||||
 | 
					        private String object;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public double[] getEmbedding() {
 | 
				
			||||||
 | 
					            return embedding;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public void setEmbedding(double[] embedding) {
 | 
				
			||||||
 | 
					            this.embedding = embedding;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public int getIndex() {
 | 
				
			||||||
 | 
					            return index;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public void setIndex(int index) {
 | 
				
			||||||
 | 
					            this.index = index;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public String getObject() {
 | 
				
			||||||
 | 
					            return object;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public void setObject(String object) {
 | 
				
			||||||
 | 
					            this.object = object;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										34
									
								
								knows-java/src/main/java/cn/luckday/embed/ReRankClient.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										34
									
								
								knows-java/src/main/java/cn/luckday/embed/ReRankClient.java
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					package cn.luckday.embed;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.alibaba.fastjson.JSONObject;
 | 
				
			||||||
 | 
					import okhttp3.*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class ReRankClient {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static String reRank(String uri, String apiKey, List<String> textsList, String query) throws IOException {
 | 
				
			||||||
 | 
					        OkHttpClient client = new OkHttpClient();
 | 
				
			||||||
 | 
					        JSONObject requestBody = new JSONObject();
 | 
				
			||||||
 | 
					        String[] texts = textsList.toArray(new String[0]);
 | 
				
			||||||
 | 
					        requestBody.put("textList", texts);
 | 
				
			||||||
 | 
					        requestBody.put("query", query);
 | 
				
			||||||
 | 
					        // 创建请求
 | 
				
			||||||
 | 
					        MediaType mediaType = MediaType.parse("application/json; charset=utf-8");
 | 
				
			||||||
 | 
					        RequestBody body = RequestBody.Companion.create(requestBody.toJSONString(), mediaType);
 | 
				
			||||||
 | 
					        Request request = new Request.Builder()
 | 
				
			||||||
 | 
					                .url(uri)
 | 
				
			||||||
 | 
					                .addHeader("Authorization", "Bearer " + apiKey)
 | 
				
			||||||
 | 
					                .addHeader("Content-Type", "application/json")
 | 
				
			||||||
 | 
					                .post(body)
 | 
				
			||||||
 | 
					                .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 发送请求
 | 
				
			||||||
 | 
					        Response response = client.newCall(request).execute();
 | 
				
			||||||
 | 
					        if (!response.isSuccessful()) {
 | 
				
			||||||
 | 
					            throw new IOException("Unexpected code " + response);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return response.body().string();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					package cn.luckday.filter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import jakarta.servlet.*;
 | 
				
			||||||
 | 
					import jakarta.servlet.annotation.WebFilter;
 | 
				
			||||||
 | 
					import jakarta.servlet.http.HttpServletRequest;
 | 
				
			||||||
 | 
					import jakarta.servlet.http.HttpServletResponse;
 | 
				
			||||||
 | 
					import org.springframework.core.annotation.Order;
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Component;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component
 | 
				
			||||||
 | 
					@WebFilter(urlPatterns = "/*", asyncSupported = true)
 | 
				
			||||||
 | 
					@Order(1)
 | 
				
			||||||
 | 
					public class AccessControlFilter implements Filter {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void init(FilterConfig filterConfig) throws ServletException {
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
 | 
				
			||||||
 | 
					        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
 | 
				
			||||||
 | 
					        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 获取源站
 | 
				
			||||||
 | 
					        String origin = httpServletRequest.getHeader("origin");
 | 
				
			||||||
 | 
					        httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
 | 
				
			||||||
 | 
					        httpServletResponse.setHeader("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With,cors, content-type, luck-token, userId, user, type");
 | 
				
			||||||
 | 
					        httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
 | 
				
			||||||
 | 
					        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,PATCH,OPTIONS");
 | 
				
			||||||
 | 
					        httpServletResponse.setHeader("Access-Control-Max-Age", "3600");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ("OPTIONS".equals(httpServletRequest.getMethod())) {
 | 
				
			||||||
 | 
					            httpServletResponse.setStatus(HttpServletResponse.SC_OK);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            chain.doFilter(request, response);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void destroy() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										85
									
								
								knows-java/src/main/java/cn/luckday/llm/OllamaClient.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										85
									
								
								knows-java/src/main/java/cn/luckday/llm/OllamaClient.java
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,85 @@
 | 
				
			|||||||
 | 
					package cn.luckday.llm;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.alibaba.fastjson2.JSON;
 | 
				
			||||||
 | 
					import jakarta.servlet.http.HttpServletResponse;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.*;
 | 
				
			||||||
 | 
					import java.net.HttpURLConnection;
 | 
				
			||||||
 | 
					import java.net.URL;
 | 
				
			||||||
 | 
					import java.nio.charset.StandardCharsets;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					import java.util.HashMap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class OllamaClient {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static Map<String, Object> PARAMS = new HashMap<>();
 | 
				
			||||||
 | 
					    private static Map<String, Object> OPTIONS = new HashMap<>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    static {
 | 
				
			||||||
 | 
					        OPTIONS.put("temperature", 0.3);     // # 控制随机性(0-1,值越大越随机)
 | 
				
			||||||
 | 
					        OPTIONS.put("top_p", 0.5);           // # 采样策略(0-1,值越小越集中)
 | 
				
			||||||
 | 
					        OPTIONS.put("max_tokens", 1024);     // # 生成的最大 token 数
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        PARAMS.put("model", "deepseek-r1:32b");
 | 
				
			||||||
 | 
					        PARAMS.put("stream", true);
 | 
				
			||||||
 | 
					        PARAMS.put("options", OPTIONS);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static String PROMPT = "你是一个知识库,必须严格按照知识库检索的内容做最精简的回答,只回答关键信息,坚决杜绝胡编乱造,注意字数。" +
 | 
				
			||||||
 | 
					            "当所有知识库内容都与产品问题无关时,或者知识库检索到任何相关信息时,你的回答必须是“没有找到”这句话。" +
 | 
				
			||||||
 | 
					            "        以下是知识库:\n" +
 | 
				
			||||||
 | 
					            "        { %content% }\n" +
 | 
				
			||||||
 | 
					            "        以上是知识库。 \n 以下是提问:";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static void sendMsg(HttpServletResponse response, String uri, String query, String content) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // 设置SSE必要的响应头
 | 
				
			||||||
 | 
					            response.setContentType("text/event-stream");
 | 
				
			||||||
 | 
					            response.setCharacterEncoding("UTF-8");
 | 
				
			||||||
 | 
					            response.setHeader("Cache-Control", "no-cache");
 | 
				
			||||||
 | 
					            response.setHeader("Connection", "keep-alive");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            URL url = new URL(uri);
 | 
				
			||||||
 | 
					            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
 | 
				
			||||||
 | 
					            conn.setRequestMethod("POST");
 | 
				
			||||||
 | 
					            conn.setRequestProperty("Accept", "text/event-stream");
 | 
				
			||||||
 | 
					            conn.setRequestProperty("Content-Type", "application/json");
 | 
				
			||||||
 | 
					            conn.setDoOutput(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            PARAMS.put("prompt", PROMPT.replace("%content%", content) + query);
 | 
				
			||||||
 | 
					            String json = JSON.toJSONString(PARAMS);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            try (OutputStream os = conn.getOutputStream()) {
 | 
				
			||||||
 | 
					                os.write(json.getBytes(StandardCharsets.UTF_8));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            int responseCode = conn.getResponseCode();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (responseCode >= HttpURLConnection.HTTP_OK && responseCode < HttpURLConnection.HTTP_USE_PROXY) {
 | 
				
			||||||
 | 
					                try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
 | 
				
			||||||
 | 
					                     PrintWriter writer = response.getWriter()) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    String line;
 | 
				
			||||||
 | 
					                    while ((line = br.readLine()) != null) {
 | 
				
			||||||
 | 
					                        if (!line.trim().isEmpty()) {
 | 
				
			||||||
 | 
					                            // 构造SSE消息格式
 | 
				
			||||||
 | 
					                            writer.write("data: " + line + "\n\n");
 | 
				
			||||||
 | 
					                            writer.flush();
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                throw new RuntimeException("Failed : HTTP error code : " + responseCode);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
 | 
				
			||||||
 | 
					                PrintWriter writer = response.getWriter();
 | 
				
			||||||
 | 
					                writer.write("data: {\"error\": \"" + e.getMessage() + "\"}\n\n");
 | 
				
			||||||
 | 
					                writer.flush();
 | 
				
			||||||
 | 
					            } catch (IOException ioe) {
 | 
				
			||||||
 | 
					                e.printStackTrace();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										45
									
								
								knows-java/src/main/java/cn/luckday/llm/QwenClient.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										45
									
								
								knows-java/src/main/java/cn/luckday/llm/QwenClient.java
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					package cn.luckday.llm;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.Arrays;
 | 
				
			||||||
 | 
					import com.alibaba.dashscope.aigc.generation.Generation;
 | 
				
			||||||
 | 
					import com.alibaba.dashscope.aigc.generation.GenerationParam;
 | 
				
			||||||
 | 
					import com.alibaba.dashscope.aigc.generation.GenerationResult;
 | 
				
			||||||
 | 
					import com.alibaba.dashscope.common.Message;
 | 
				
			||||||
 | 
					import com.alibaba.dashscope.common.Role;
 | 
				
			||||||
 | 
					import com.alibaba.dashscope.exception.ApiException;
 | 
				
			||||||
 | 
					import com.alibaba.dashscope.exception.InputRequiredException;
 | 
				
			||||||
 | 
					import com.alibaba.dashscope.exception.NoApiKeyException;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class QwenClient {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static GenerationResult sendMsg(String model, String apiKey, String query, String content) throws ApiException, NoApiKeyException, InputRequiredException {
 | 
				
			||||||
 | 
					        Generation gen = new Generation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Message systemMsg = Message.builder()
 | 
				
			||||||
 | 
					                .role(Role.SYSTEM.getValue())
 | 
				
			||||||
 | 
					                .content("你是一个知识库,必须严格按照知识库检索的内容做最精简的回答,只回答关键信息,坚决杜绝胡编乱造,注意数字。" +
 | 
				
			||||||
 | 
					                        "当所有知识库内容都与产品问题无关时,或者知识库检索到任何相关信息时,你的回答必须是“没有找到”这句话。" +
 | 
				
			||||||
 | 
					                        "        以下是知识库:\n" +
 | 
				
			||||||
 | 
					                        "        {" + content + "}\n" +
 | 
				
			||||||
 | 
					                        "        以上是知识库。")
 | 
				
			||||||
 | 
					                .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Message userMsg = Message.builder()
 | 
				
			||||||
 | 
					                .role(Role.USER.getValue())
 | 
				
			||||||
 | 
					                .content(query)
 | 
				
			||||||
 | 
					                .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        GenerationParam param = GenerationParam.builder()
 | 
				
			||||||
 | 
					                .model(model)
 | 
				
			||||||
 | 
					                .messages(Arrays.asList(systemMsg, userMsg))
 | 
				
			||||||
 | 
					                .resultFormat(GenerationParam.ResultFormat.MESSAGE)
 | 
				
			||||||
 | 
					                .apiKey(apiKey)
 | 
				
			||||||
 | 
					                .topK(50)
 | 
				
			||||||
 | 
					                .temperature(0.1f)
 | 
				
			||||||
 | 
					                .topP(0.8)
 | 
				
			||||||
 | 
					                .seed(1234)
 | 
				
			||||||
 | 
					                .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return gen.call(param);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,143 @@
 | 
				
			|||||||
 | 
					package cn.luckday.service;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import co.elastic.clients.elasticsearch.ElasticsearchClient;
 | 
				
			||||||
 | 
					import co.elastic.clients.elasticsearch._types.Script;
 | 
				
			||||||
 | 
					import co.elastic.clients.elasticsearch._types.query_dsl.*;
 | 
				
			||||||
 | 
					import co.elastic.clients.elasticsearch.core.IndexResponse;
 | 
				
			||||||
 | 
					import co.elastic.clients.elasticsearch.core.SearchResponse;
 | 
				
			||||||
 | 
					import co.elastic.clients.elasticsearch.indices.CreateIndexRequest;
 | 
				
			||||||
 | 
					import co.elastic.clients.elasticsearch.indices.CreateIndexResponse;
 | 
				
			||||||
 | 
					import co.elastic.clients.json.JsonData;
 | 
				
			||||||
 | 
					import cn.luckday.bean.SearchResult;
 | 
				
			||||||
 | 
					import cn.luckday.bean.KnowsIndex;
 | 
				
			||||||
 | 
					import cn.luckday.embed.EmbedClient;
 | 
				
			||||||
 | 
					import jakarta.annotation.Resource;
 | 
				
			||||||
 | 
					import lombok.extern.slf4j.Slf4j;
 | 
				
			||||||
 | 
					import org.springframework.beans.factory.annotation.Value;
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Service;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.util.Comparator;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					import java.util.Objects;
 | 
				
			||||||
 | 
					import java.util.stream.Collectors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Slf4j
 | 
				
			||||||
 | 
					@Service
 | 
				
			||||||
 | 
					public class EsDocumentService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Value("${embedding.uri}")
 | 
				
			||||||
 | 
					    private String embeddingUri;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Value("${embedding.api-key}")
 | 
				
			||||||
 | 
					    private String embeddingApiKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Resource
 | 
				
			||||||
 | 
					    private ElasticsearchClient client;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static final String INDEX_NAME = "knows_index";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static final float SIMILARITY_THRESHOLD  = 0.2f;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 创建索引
 | 
				
			||||||
 | 
					     * @throws IOException 异常
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public void createIndex() throws IOException {
 | 
				
			||||||
 | 
					        CreateIndexRequest request = new CreateIndexRequest.Builder()
 | 
				
			||||||
 | 
					                .index(INDEX_NAME)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                .mappings(m -> m
 | 
				
			||||||
 | 
					                        .properties("file_name", p -> p.keyword(k -> k))
 | 
				
			||||||
 | 
					                        .properties("file_path", p -> p.keyword(k -> k))
 | 
				
			||||||
 | 
					                        .properties("file_type", p -> p.keyword(k -> k))
 | 
				
			||||||
 | 
					                        .properties("file_size", p -> p.keyword(k -> k))
 | 
				
			||||||
 | 
					                        .properties("remark_vec", p -> p
 | 
				
			||||||
 | 
					                                .denseVector(dv -> dv
 | 
				
			||||||
 | 
					                                        .dims(1024)
 | 
				
			||||||
 | 
					                                        .index(true)
 | 
				
			||||||
 | 
					                                        .similarity("cosine")
 | 
				
			||||||
 | 
					                                )
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        .properties("remark", p -> p
 | 
				
			||||||
 | 
					                                .text(t -> t)
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					//                        .properties("remark", p -> p
 | 
				
			||||||
 | 
					//                                .text(t -> t.searchAnalyzer("ik_smart")
 | 
				
			||||||
 | 
					//                                        .analyzer("ik_smart") // 使用 IK 分词器
 | 
				
			||||||
 | 
					//                                )
 | 
				
			||||||
 | 
					//                        )
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        CreateIndexResponse createIndexResponse = client.indices().create(request);
 | 
				
			||||||
 | 
					        log.info("Index created: {}", createIndexResponse.acknowledged());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 添加数据
 | 
				
			||||||
 | 
					     * @param knowsIndexList 数据
 | 
				
			||||||
 | 
					     * @throws IOException 异常
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public void indexSellList(List<KnowsIndex> knowsIndexList) throws IOException {
 | 
				
			||||||
 | 
					        for (KnowsIndex knowsIndex : knowsIndexList) {
 | 
				
			||||||
 | 
					            knowsIndex.setContent_vec(EmbedClient.getEmbedding(embeddingUri, embeddingApiKey, knowsIndex.getContent()));
 | 
				
			||||||
 | 
					            IndexResponse response = client.index(i -> i
 | 
				
			||||||
 | 
					                    .index(INDEX_NAME)
 | 
				
			||||||
 | 
					                    .id(knowsIndex.getId())
 | 
				
			||||||
 | 
					                    .document(knowsIndex)
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            log.info("Sell indexed: {}", response.id());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 检索
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param queryVector 向量
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public List<SearchResult> searchVector(double[] queryVector) throws IOException {
 | 
				
			||||||
 | 
					        // 创建向量相似度查询
 | 
				
			||||||
 | 
					        ScriptScoreQuery scriptScoreQuery = ScriptScoreQuery.of(q -> q
 | 
				
			||||||
 | 
					                .query(QueryBuilders.matchAll().build()._toQuery())
 | 
				
			||||||
 | 
					                .script(Script.of(s -> s.inline(i -> i
 | 
				
			||||||
 | 
					                        .source("double score = cosineSimilarity(params.query_vector, 'content_vec'); " +
 | 
				
			||||||
 | 
					                                "score = Math.min(1.0, Math.max(0.0, score)); " + // 确保评分在[0, 1]之间
 | 
				
			||||||
 | 
					                                "if (score < params.threshold) { return 0; } else { return score; }")
 | 
				
			||||||
 | 
					                        .params(Map.of(
 | 
				
			||||||
 | 
					                                "query_vector", JsonData.of(queryVector),
 | 
				
			||||||
 | 
					                                "threshold", JsonData.of(SIMILARITY_THRESHOLD) // 将阈值作为参数传递给脚本
 | 
				
			||||||
 | 
					                        ))))));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 创建bool查询,向量相似度查询作为should子句
 | 
				
			||||||
 | 
					        Query boolQuery = QueryBuilders.bool(b -> b
 | 
				
			||||||
 | 
					                .should(scriptScoreQuery._toQuery())
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Query functionScoreQuery = QueryBuilders.functionScore(fs -> fs
 | 
				
			||||||
 | 
					                .query(boolQuery)
 | 
				
			||||||
 | 
					                .scoreMode(FunctionScoreMode.Max)
 | 
				
			||||||
 | 
					                .boostMode(FunctionBoostMode.Replace)
 | 
				
			||||||
 | 
					                .minScore((double) SIMILARITY_THRESHOLD)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 执行合并后的查询
 | 
				
			||||||
 | 
					        SearchResponse<KnowsIndex> combinedSearchResponse = client.search(s -> s
 | 
				
			||||||
 | 
					                        .index(INDEX_NAME)
 | 
				
			||||||
 | 
					                        .query(functionScoreQuery),
 | 
				
			||||||
 | 
					                KnowsIndex.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 处理查询的结果
 | 
				
			||||||
 | 
					        return combinedSearchResponse.hits().hits().stream()
 | 
				
			||||||
 | 
					                .map(hit -> {
 | 
				
			||||||
 | 
					                    double finalScore = Objects.nonNull(hit.score()) ? hit.score() : 0.0;
 | 
				
			||||||
 | 
					                    return finalScore >= SIMILARITY_THRESHOLD ? new SearchResult(hit.source(), finalScore) : null;
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                .filter(Objects::nonNull)
 | 
				
			||||||
 | 
					                .sorted(Comparator.comparingDouble(SearchResult::getScore).reversed())
 | 
				
			||||||
 | 
					                .collect(Collectors.toList());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										135
									
								
								knows-java/src/main/java/cn/luckday/service/RedFileService.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										135
									
								
								knows-java/src/main/java/cn/luckday/service/RedFileService.java
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,135 @@
 | 
				
			|||||||
 | 
					package cn.luckday.service;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cn.hutool.core.util.IdUtil;
 | 
				
			||||||
 | 
					import cn.luckday.bean.KnowsIndex;
 | 
				
			||||||
 | 
					import cn.luckday.embed.EmbedClient;
 | 
				
			||||||
 | 
					import cn.luckday.document.PDFParser;
 | 
				
			||||||
 | 
					import cn.luckday.document.WordProcessor;
 | 
				
			||||||
 | 
					import jakarta.annotation.Resource;
 | 
				
			||||||
 | 
					import org.apache.poi.xwpf.usermodel.XWPFPicture;
 | 
				
			||||||
 | 
					import org.springframework.beans.factory.annotation.Value;
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Service;
 | 
				
			||||||
 | 
					import org.springframework.web.multipart.MultipartFile;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.awt.image.BufferedImage;
 | 
				
			||||||
 | 
					import java.io.File;
 | 
				
			||||||
 | 
					import java.nio.file.Files;
 | 
				
			||||||
 | 
					import java.nio.file.Path;
 | 
				
			||||||
 | 
					import java.nio.file.Paths;
 | 
				
			||||||
 | 
					import java.time.LocalDateTime;
 | 
				
			||||||
 | 
					import java.time.format.DateTimeFormatter;
 | 
				
			||||||
 | 
					import java.util.Arrays;
 | 
				
			||||||
 | 
					import java.util.HashMap;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Service
 | 
				
			||||||
 | 
					public class RedFileService {
 | 
				
			||||||
 | 
					    private static final String TEMP_DIR = "src/main/resources/temp_uploads";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Value("${embedding.uri}")
 | 
				
			||||||
 | 
					    private String embeddingUri;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Value("${embedding.api-key}")
 | 
				
			||||||
 | 
					    private String embeddingApiKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Resource
 | 
				
			||||||
 | 
					    private EsDocumentService esDocumentService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public void uploadFile(MultipartFile file) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            String projectPath = System.getProperty("user.dir");
 | 
				
			||||||
 | 
					            Path tempDirPath = Paths.get(projectPath, TEMP_DIR);
 | 
				
			||||||
 | 
					            if (!Files.exists(tempDirPath)) {
 | 
				
			||||||
 | 
					                Files.createDirectories(tempDirPath);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 获取文件名和扩展名
 | 
				
			||||||
 | 
					            String originalFilename = file.getOriginalFilename();
 | 
				
			||||||
 | 
					            String fileExtension = getFileExtension(originalFilename);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 生成临时文件路径
 | 
				
			||||||
 | 
					            String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
 | 
				
			||||||
 | 
					            String tempFileName = timestamp + "_" + originalFilename;
 | 
				
			||||||
 | 
					            Path tempFilePath = Paths.get(projectPath, TEMP_DIR, tempFileName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 保存上传的文件
 | 
				
			||||||
 | 
					            file.transferTo(tempFilePath.toFile());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 解析文件内容
 | 
				
			||||||
 | 
					            Map<String, Object> parsedContent = parseFile(tempFilePath.toString(), fileExtension);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 保存到 Elasticsearch
 | 
				
			||||||
 | 
					            String text = parsedContent.get("text").toString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            KnowsIndex knowsIndex = new KnowsIndex();
 | 
				
			||||||
 | 
					            knowsIndex.setId(String.valueOf(IdUtil.getSnowflakeNextId()));
 | 
				
			||||||
 | 
					            knowsIndex.setContent(text);
 | 
				
			||||||
 | 
					            knowsIndex.setContent_vec(EmbedClient.getEmbedding(embeddingUri, embeddingApiKey, text));
 | 
				
			||||||
 | 
					            esDocumentService.indexSellList(Arrays.asList(knowsIndex));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 清理临时文件
 | 
				
			||||||
 | 
					            Files.deleteIfExists(tempFilePath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            e.printStackTrace();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private String getFileExtension(String filename) {
 | 
				
			||||||
 | 
					        if (filename == null)
 | 
				
			||||||
 | 
					            return "";
 | 
				
			||||||
 | 
					        int lastDotIndex = filename.lastIndexOf('.');
 | 
				
			||||||
 | 
					        return (lastDotIndex == -1) ? "" : filename.substring(lastDotIndex + 1).toLowerCase();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private Map<String, Object> parseFile(String filePath, String extension) throws Exception {
 | 
				
			||||||
 | 
					        Map<String, Object> content = new HashMap<>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        switch (extension) {
 | 
				
			||||||
 | 
					            case "pdf":
 | 
				
			||||||
 | 
					                PDFParser pdfParser = new PDFParser(filePath);
 | 
				
			||||||
 | 
					                pdfParser.parse();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // 获取解析结果
 | 
				
			||||||
 | 
					                List<String> texts = pdfParser.getExtractedText();
 | 
				
			||||||
 | 
					                List<BufferedImage> images = pdfParser.getExtractedImages();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // 合并所有文本
 | 
				
			||||||
 | 
					                StringBuilder fullText = new StringBuilder();
 | 
				
			||||||
 | 
					                for (String text : texts) {
 | 
				
			||||||
 | 
					                    fullText.append(text).append("\n");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                content.put("text", fullText.toString());
 | 
				
			||||||
 | 
					                content.put("imageCount", images.size());
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            case "docx":
 | 
				
			||||||
 | 
					                WordProcessor wordProcessor = new WordProcessor(filePath);
 | 
				
			||||||
 | 
					                wordProcessor.process();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                List<String> extractedText = wordProcessor.getExtractedText();
 | 
				
			||||||
 | 
					                // 合并所有文本
 | 
				
			||||||
 | 
					                StringBuilder docxFullText = new StringBuilder();
 | 
				
			||||||
 | 
					                for (String text : extractedText) {
 | 
				
			||||||
 | 
					                    docxFullText.append(text).append("\n");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                List<XWPFPicture> extractedImages = wordProcessor.getExtractedImages();
 | 
				
			||||||
 | 
					                content.put("text", docxFullText.toString());
 | 
				
			||||||
 | 
					                content.put("imageCount", extractedImages.size());
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            default:
 | 
				
			||||||
 | 
					                throw new IllegalArgumentException("不支持的文件类型: " + extension);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 添加元数据
 | 
				
			||||||
 | 
					        content.put("filename", new File(filePath).getName());
 | 
				
			||||||
 | 
					        content.put("uploadTime", LocalDateTime.now().toString());
 | 
				
			||||||
 | 
					        content.put("fileType", extension);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return content;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										32
									
								
								knows-java/src/main/resources/application.yml
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										32
									
								
								knows-java/src/main/resources/application.yml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					server:
 | 
				
			||||||
 | 
					  port: 8899
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					spring:
 | 
				
			||||||
 | 
					  servlet:
 | 
				
			||||||
 | 
					    multipart:
 | 
				
			||||||
 | 
					      max-file-size: 10MB
 | 
				
			||||||
 | 
					      max-request-size: 10MB
 | 
				
			||||||
 | 
					  main:
 | 
				
			||||||
 | 
					    allow-bean-definition-overriding: true
 | 
				
			||||||
 | 
					  application:
 | 
				
			||||||
 | 
					    name: knows
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  elasticsearch:
 | 
				
			||||||
 | 
					    uris: 172.16.100.47:9200
 | 
				
			||||||
 | 
					#    username: elastic
 | 
				
			||||||
 | 
					#    password: 123456
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					qwen:
 | 
				
			||||||
 | 
					  api-key: sk-**********************
 | 
				
			||||||
 | 
					  model: qwen-plus
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					oll:
 | 
				
			||||||
 | 
					  uri: http://172.16.90.4:11434/api/generate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					embedding:
 | 
				
			||||||
 | 
					  uri: http://172.16.90.4:6009/v1/embed
 | 
				
			||||||
 | 
					  api-key: sk-abcdefg1234567
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					re-rank:
 | 
				
			||||||
 | 
					  uri: http://172.16.90.4:6010/v1/reRank
 | 
				
			||||||
 | 
					  api-key: sk-abcdefg1234567
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								knows-java/src/main/resources/native/opencv_java4110.dll
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										
											BIN
										
									
								
								knows-java/src/main/resources/native/opencv_java4110.dll
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								knows-java/src/main/resources/ocr/chi_sim.traineddata
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										
											BIN
										
									
								
								knows-java/src/main/resources/ocr/chi_sim.traineddata
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								knows-java/src/main/resources/ocr/eng.traineddata
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										
											BIN
										
									
								
								knows-java/src/main/resources/ocr/eng.traineddata
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								knows-java/src/main/resources/ocr/osd.traineddata
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										
											BIN
										
									
								
								knows-java/src/main/resources/ocr/osd.traineddata
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										24
									
								
								knows-java/src/test/java/cn/luckday/ApplicationTests.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										24
									
								
								knows-java/src/test/java/cn/luckday/ApplicationTests.java
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					package cn.luckday;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cn.luckday.service.EsDocumentService;
 | 
				
			||||||
 | 
					import jakarta.annotation.Resource;
 | 
				
			||||||
 | 
					import org.junit.jupiter.api.Test;
 | 
				
			||||||
 | 
					import org.springframework.boot.test.context.SpringBootTest;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@SpringBootTest
 | 
				
			||||||
 | 
					class ApplicationTests {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Test
 | 
				
			||||||
 | 
					    void contextLoads() {
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Resource
 | 
				
			||||||
 | 
					    private EsDocumentService service;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Test
 | 
				
			||||||
 | 
					    void create() throws IOException {
 | 
				
			||||||
 | 
					        service.createIndex();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										18
									
								
								konws-python/embed/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										18
									
								
								konws-python/embed/Dockerfile
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# 使用官方Python运行时作为父镜像
 | 
				
			||||||
 | 
					FROM python:3.10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 设置工作目录
 | 
				
			||||||
 | 
					WORKDIR /app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 将当前目录内容复制到容器的/app中
 | 
				
			||||||
 | 
					ADD . /app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RUN pip install --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple
 | 
				
			||||||
 | 
					# 安装程序需要的包
 | 
				
			||||||
 | 
					RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 运行时监听的端口
 | 
				
			||||||
 | 
					EXPOSE 6009
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 运行app.py时的命令及其参数
 | 
				
			||||||
 | 
					CMD ["uvicorn", "embed:app", "--host", "0.0.0.0", "--port", "6009"]
 | 
				
			||||||
							
								
								
									
										76
									
								
								konws-python/embed/embed.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										76
									
								
								konws-python/embed/embed.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,76 @@
 | 
				
			|||||||
 | 
					import os
 | 
				
			||||||
 | 
					from typing import List
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import numpy as np
 | 
				
			||||||
 | 
					import uvicorn
 | 
				
			||||||
 | 
					from fastapi import FastAPI, Depends, HTTPException, status
 | 
				
			||||||
 | 
					from fastapi.middleware.cors import CORSMiddleware
 | 
				
			||||||
 | 
					from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
 | 
				
			||||||
 | 
					from pydantic import BaseModel
 | 
				
			||||||
 | 
					from sentence_transformers import SentenceTransformer, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 环境变量传入
 | 
				
			||||||
 | 
					sk_key = os.environ.get('sk-key', 'sk-aaabbbcccdddeeefffggghhhiiijjjkkk')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 创建一个FastAPI实例
 | 
				
			||||||
 | 
					app = FastAPI()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.add_middleware(
 | 
				
			||||||
 | 
					    CORSMiddleware,
 | 
				
			||||||
 | 
					    allow_origins=["*"],
 | 
				
			||||||
 | 
					    allow_credentials=True,
 | 
				
			||||||
 | 
					    allow_methods=["*"],
 | 
				
			||||||
 | 
					    allow_headers=["*"],
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 创建一个HTTPBearer实例
 | 
				
			||||||
 | 
					security = HTTPBearer()
 | 
				
			||||||
 | 
					# 加载预训练的 Transformer 模型
 | 
				
			||||||
 | 
					transformer_model = models.Transformer('./m3e-large', cache_dir='./cache')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 创建 Mean Pooling 层
 | 
				
			||||||
 | 
					pooling_model = models.Pooling(transformer_model.get_word_embedding_dimension(), pooling_mode='mean')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 构建 SentenceTransformer 模型
 | 
				
			||||||
 | 
					model = SentenceTransformer(modules=[transformer_model, pooling_model])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class EmbeddingRequest(BaseModel):
 | 
				
			||||||
 | 
					    input: List[str]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class EmbeddingResponse(BaseModel):
 | 
				
			||||||
 | 
					    data: list
 | 
				
			||||||
 | 
					    dimension: int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.post("/v1/embed", response_model=EmbeddingResponse)
 | 
				
			||||||
 | 
					async def get_embed(request: EmbeddingRequest, credentials: HTTPAuthorizationCredentials = Depends(security)):
 | 
				
			||||||
 | 
					    if credentials.credentials != sk_key:
 | 
				
			||||||
 | 
					        raise HTTPException(
 | 
				
			||||||
 | 
					            status_code=status.HTTP_401_UNAUTHORIZED,
 | 
				
			||||||
 | 
					            detail="Invalid authorization code",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # 计算嵌入向量和tokens数量
 | 
				
			||||||
 | 
					    embeddings = [model.encode(text) for text in request.input]
 | 
				
			||||||
 | 
					    # 归一化处理
 | 
				
			||||||
 | 
					    embeddings = [embedding / np.linalg.norm(embedding) for embedding in embeddings]
 | 
				
			||||||
 | 
					    # 将numpy数组转换为列表
 | 
				
			||||||
 | 
					    embeddings = [embedding.tolist() for embedding in embeddings]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    response = {
 | 
				
			||||||
 | 
					        "data": [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "embedding": embedding,
 | 
				
			||||||
 | 
					                "index": index
 | 
				
			||||||
 | 
					            } for index, embedding in enumerate(embeddings)
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "dimension": len(embeddings[0])
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 | 
					    uvicorn.run("embed:app", host='0.0.0.0', port=6009, workers=2)
 | 
				
			||||||
							
								
								
									
										10
									
								
								konws-python/embed/requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										10
									
								
								konws-python/embed/requirements.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					fastapi==0.99.1
 | 
				
			||||||
 | 
					pydantic==1.10.7
 | 
				
			||||||
 | 
					sentence-transformers==3.3.1
 | 
				
			||||||
 | 
					uvicorn==0.23.1
 | 
				
			||||||
 | 
					numpy==1.24.4
 | 
				
			||||||
 | 
					scipy==1.10.1
 | 
				
			||||||
 | 
					scikit-learn==1.3.0
 | 
				
			||||||
 | 
					torchvision
 | 
				
			||||||
 | 
					torchaudio
 | 
				
			||||||
 | 
					torch
 | 
				
			||||||
							
								
								
									
										18
									
								
								konws-python/rerank/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										18
									
								
								konws-python/rerank/Dockerfile
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# 使用官方Python运行时作为父镜像
 | 
				
			||||||
 | 
					FROM python:3.10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 设置工作目录
 | 
				
			||||||
 | 
					WORKDIR /app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 将当前目录内容复制到容器的/app中
 | 
				
			||||||
 | 
					ADD . /app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RUN pip install --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple
 | 
				
			||||||
 | 
					# 安装程序需要的包
 | 
				
			||||||
 | 
					RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 运行时监听的端口
 | 
				
			||||||
 | 
					EXPOSE 6010
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 运行app.py时的命令及其参数
 | 
				
			||||||
 | 
					CMD ["uvicorn", "rerank:app", "--host", "0.0.0.0", "--port", "6010"]
 | 
				
			||||||
							
								
								
									
										12
									
								
								konws-python/rerank/requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										12
									
								
								konws-python/rerank/requirements.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					fastapi==0.99.1
 | 
				
			||||||
 | 
					pydantic==1.10.7
 | 
				
			||||||
 | 
					uvicorn==0.23.1 
 | 
				
			||||||
 | 
					tiktoken==0.4.0
 | 
				
			||||||
 | 
					numpy==1.24.4 
 | 
				
			||||||
 | 
					scipy==1.10.1 
 | 
				
			||||||
 | 
					scikit-learn==1.5.0
 | 
				
			||||||
 | 
					torchvision
 | 
				
			||||||
 | 
					torchaudio
 | 
				
			||||||
 | 
					torch
 | 
				
			||||||
 | 
					BCEmbedding==0.1.5
 | 
				
			||||||
 | 
					starlette~=0.27.0
 | 
				
			||||||
							
								
								
									
										58
									
								
								konws-python/rerank/rerank.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										58
									
								
								konws-python/rerank/rerank.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,58 @@
 | 
				
			|||||||
 | 
					import os
 | 
				
			||||||
 | 
					from typing import List
 | 
				
			||||||
 | 
					import uvicorn
 | 
				
			||||||
 | 
					from BCEmbedding import RerankerModel
 | 
				
			||||||
 | 
					from fastapi import FastAPI, Depends, HTTPException, status
 | 
				
			||||||
 | 
					from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
 | 
				
			||||||
 | 
					from pydantic import BaseModel
 | 
				
			||||||
 | 
					from starlette.middleware.cors import CORSMiddleware
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 环境变量传入
 | 
				
			||||||
 | 
					sk_key = os.environ.get('sk-key', 'sk-aaabbbcccdddeeefffggghhhiiijjjkkk...')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 创建一个FastAPI实例
 | 
				
			||||||
 | 
					app = FastAPI()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 添加CORS中间件
 | 
				
			||||||
 | 
					app.add_middleware(
 | 
				
			||||||
 | 
					    CORSMiddleware,
 | 
				
			||||||
 | 
					    allow_origins=["*"],  # 允许所有来源
 | 
				
			||||||
 | 
					    allow_credentials=True,
 | 
				
			||||||
 | 
					    allow_methods=["*"],  # 允许所有方法
 | 
				
			||||||
 | 
					    allow_headers=["*"],  # 允许所有头部
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 创建一个HTTPBearer实例
 | 
				
			||||||
 | 
					security = HTTPBearer()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 初始化模型
 | 
				
			||||||
 | 
					model = RerankerModel(model_name_or_path="./bce-reranker-base_v1")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ReRankRequest(BaseModel):
 | 
				
			||||||
 | 
					    textList: List[str]
 | 
				
			||||||
 | 
					    query: str
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ReRankResponse(BaseModel):
 | 
				
			||||||
 | 
					    rerank_passages: List[str]
 | 
				
			||||||
 | 
					    rerank_scores: List[float]
 | 
				
			||||||
 | 
					    rerank_ids: List[int]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 定义路由,处理rerank请求
 | 
				
			||||||
 | 
					@app.post("/v1/reRank", response_model=ReRankResponse)
 | 
				
			||||||
 | 
					async def get_embeddings(request: ReRankRequest, credentials: HTTPAuthorizationCredentials = Depends(security)):
 | 
				
			||||||
 | 
					    if credentials.credentials != sk_key:
 | 
				
			||||||
 | 
					        raise HTTPException(
 | 
				
			||||||
 | 
					            status_code=status.HTTP_401_UNAUTHORIZED,
 | 
				
			||||||
 | 
					            detail="Invalid authorization code",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    query = request.query
 | 
				
			||||||
 | 
					    passages = request.textList
 | 
				
			||||||
 | 
					    return model.rerank(query, passages)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 运行应用
 | 
				
			||||||
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 | 
					    uvicorn.run("rerank:app", host='0.0.0.0', port=6010, workers=2)
 | 
				
			||||||
							
								
								
									
										296
									
								
								konws-web/chatbox.html
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										296
									
								
								konws-web/chatbox.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,296 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="zh-CN">
 | 
				
			||||||
 | 
					  <head>
 | 
				
			||||||
 | 
					    <meta charset="UTF-8" />
 | 
				
			||||||
 | 
					    <title>DeepSeek 32B Chat</title>
 | 
				
			||||||
 | 
					    <script src="js/marked.min.js"></script>
 | 
				
			||||||
 | 
					    <link rel="stylesheet" href="css/main.css" />
 | 
				
			||||||
 | 
					  </head>
 | 
				
			||||||
 | 
					  <body>
 | 
				
			||||||
 | 
					    <div id="chatBox">
 | 
				
			||||||
 | 
					      <div class="messages-container"></div>
 | 
				
			||||||
 | 
					      <div id="inputArea">
 | 
				
			||||||
 | 
					        <input type="text" id="userInput" placeholder="输入消息..." />
 | 
				
			||||||
 | 
					        <button onclick="sendMessage()" id="sendBtn">
 | 
				
			||||||
 | 
					          <svg class="icon" viewBox="0 0 1057 1024" xmlns="http://www.w3.org/2000/svg" width="20" height="20">
 | 
				
			||||||
 | 
					            <path
 | 
				
			||||||
 | 
					              d="M891.904 825.782857L462.482286 693.613714l429.421714-495.396571-561.517714 495.469714L0.073143 561.590857 1057.133714 0.073143 891.904 825.782857zM462.482286 1024v-231.058286l132.096 65.828572-132.096 165.156571z"
 | 
				
			||||||
 | 
					              fill="#ffffff"
 | 
				
			||||||
 | 
					            ></path>
 | 
				
			||||||
 | 
					          </svg>
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- 添加上传按钮 -->
 | 
				
			||||||
 | 
					    <div class="upload-button-container">
 | 
				
			||||||
 | 
					      <button onclick="showUploadDialog()" class="upload-btn">
 | 
				
			||||||
 | 
					        <svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="20" height="20">
 | 
				
			||||||
 | 
					          <path
 | 
				
			||||||
 | 
					            d="M1024 736s-3.4048-10.24-10.24-20.5056l-150.1696-300.3392c-6.8352-10.24-20.48-20.5056-34.1248-20.5056H706.56c-13.6448 0-23.8848 10.24-23.8848 23.9104v40.96c0 13.6448 10.2144 23.9104 23.8848 23.9104h40.96c13.6448 0 27.2896 10.24 34.1248 20.48l105.8304 215.0656c6.8352 10.1888 0 20.4544-13.6704 20.4544H706.56c-13.6448 0-23.8848 10.24-23.8848 23.9104v122.9056c0 13.6448-10.24 23.9104-23.9104 23.9104H365.2352a23.3216 23.3216 0 0 1-23.8848-23.9104v-122.9056c0-13.6448-10.24-23.9104-23.9104-23.9104H146.7648c-13.6448 0-17.0752-10.24-13.6448-20.4544l109.2352-215.0656c6.8352-10.2144 20.48-20.48 34.1248-20.48h37.5552c13.6448 0 23.9104-10.24 23.9104-23.9104v-37.5552c0-13.6448-10.24-23.8848-23.9104-23.8848h-122.88c-13.6704 0-27.3152 10.2144-34.1248 20.48L10.24 715.4944c-6.8352 10.2656-10.24 20.5056-10.24 20.5056v235.4944c0 13.6448 10.24 23.9104 23.8848 23.9104h976.2048c13.6448 0 23.9104-10.24 23.9104-23.9104V736zM300.3648 292.2752h126.2848v358.4h170.6752v-358.4h133.12c13.6448 0 17.0752-6.8352 6.8352-17.0496l-211.6352-238.9504c-6.8352-10.2144-23.8848-10.2144-30.72 0l-204.8 238.9504c-6.8096 10.2144-3.4048 17.0496 10.24 17.0496z"
 | 
				
			||||||
 | 
					            fill="#fff"
 | 
				
			||||||
 | 
					          ></path>
 | 
				
			||||||
 | 
					        </svg>
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- 文件上传弹窗 -->
 | 
				
			||||||
 | 
					    <div id="uploadDialog" class="upload-dialog">
 | 
				
			||||||
 | 
					      <div class="upload-dialog-content">
 | 
				
			||||||
 | 
					        <span class="close-btn" onclick="closeUploadDialog()">×</span>
 | 
				
			||||||
 | 
					        <h2>上传文件</h2>
 | 
				
			||||||
 | 
					        <div class="upload-area" id="dropZone">
 | 
				
			||||||
 | 
					          <input type="file" id="fileInput" style="display: none" onchange="handleFileSelect(event)" />
 | 
				
			||||||
 | 
					          <div class="upload-placeholder" onclick="document.getElementById('fileInput').click()">
 | 
				
			||||||
 | 
					            <i class="fas fa-cloud-upload-alt"></i>
 | 
				
			||||||
 | 
					            <p>点击或拖拽文件到此处上传</p>
 | 
				
			||||||
 | 
					            <p class="supported-formats">支持的格式: PDF, DOC, DOCX</p>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div id="uploadProgress" class="upload-progress" style="display: none">
 | 
				
			||||||
 | 
					          <div class="progress-bar">
 | 
				
			||||||
 | 
					            <div class="progress-fill"></div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <span class="progress-text">0%</span>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div id="uploadStatus" class="upload-status"></div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <script>
 | 
				
			||||||
 | 
					      const url = "http://localhost:8899";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      let currentBotMessage = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 添加消息到聊天框
 | 
				
			||||||
 | 
					      function addMessage(content, isUser = false) {
 | 
				
			||||||
 | 
					        const messagesContainer = document.querySelector(".messages-container");
 | 
				
			||||||
 | 
					        const messageDiv = document.createElement("div");
 | 
				
			||||||
 | 
					        messageDiv.className = `message ${isUser ? "user-message" : "bot-message"}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 创建头像元素
 | 
				
			||||||
 | 
					        const avatar = document.createElement("img");
 | 
				
			||||||
 | 
					        avatar.className = "avatar";
 | 
				
			||||||
 | 
					        avatar.src = isUser ? "./images/user-avatar.png" : "/images/bot-avatar.png";
 | 
				
			||||||
 | 
					        avatar.alt = isUser ? "User Avatar" : "Bot Avatar";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 创建消息内容容器
 | 
				
			||||||
 | 
					        const messageContent = document.createElement("div");
 | 
				
			||||||
 | 
					        messageContent.className = "message-content";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (isUser) {
 | 
				
			||||||
 | 
					          messageContent.textContent = content;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          messageContent.innerHTML = marked.parse(content);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 组装消息元素
 | 
				
			||||||
 | 
					        messageDiv.appendChild(avatar);
 | 
				
			||||||
 | 
					        messageDiv.appendChild(messageContent);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        messagesContainer.appendChild(messageDiv);
 | 
				
			||||||
 | 
					        messagesContainer.scrollTop = messagesContainer.scrollHeight;
 | 
				
			||||||
 | 
					        return messageDiv;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 修改处理流式响应的部分
 | 
				
			||||||
 | 
					      async function streamResponse(prompt) {
 | 
				
			||||||
 | 
					        const btn = document.getElementById("sendBtn");
 | 
				
			||||||
 | 
					        btn.disabled = true;
 | 
				
			||||||
 | 
					        let accumulatedContent = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          const response = await fetch(url + "/knows/generate", {
 | 
				
			||||||
 | 
					            method: "POST",
 | 
				
			||||||
 | 
					            headers: {
 | 
				
			||||||
 | 
					              "Content-Type": "application/json",
 | 
				
			||||||
 | 
					              Accept: "text/event-stream"
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            body: JSON.stringify({
 | 
				
			||||||
 | 
					              keyword: prompt
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (!response.ok) {
 | 
				
			||||||
 | 
					            throw new Error(`HTTP error! status: ${response.status}`);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          const reader = response.body.getReader();
 | 
				
			||||||
 | 
					          const decoder = new TextDecoder();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (!currentBotMessage) {
 | 
				
			||||||
 | 
					            // 创建完整的消息结构,包括头像
 | 
				
			||||||
 | 
					            const messageDiv = document.createElement("div");
 | 
				
			||||||
 | 
					            messageDiv.className = "message bot-message";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 创建头像元素
 | 
				
			||||||
 | 
					            const avatar = document.createElement("img");
 | 
				
			||||||
 | 
					            avatar.className = "avatar";
 | 
				
			||||||
 | 
					            avatar.src = "./images/bot-avatar.png";
 | 
				
			||||||
 | 
					            avatar.alt = "Bot Avatar";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 创建消息内容容器
 | 
				
			||||||
 | 
					            const messageContent = document.createElement("div");
 | 
				
			||||||
 | 
					            messageContent.className = "message-content";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 组装消息元素
 | 
				
			||||||
 | 
					            messageDiv.appendChild(avatar);
 | 
				
			||||||
 | 
					            messageDiv.appendChild(messageContent);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            document.querySelector(".messages-container").appendChild(messageDiv);
 | 
				
			||||||
 | 
					            currentBotMessage = messageContent; // 更新 currentBotMessage 为消息内容容器
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          let thinkContent = true;
 | 
				
			||||||
 | 
					          while (true) {
 | 
				
			||||||
 | 
					            const { done, value } = await reader.read();
 | 
				
			||||||
 | 
					            if (done) break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const chunk = decoder.decode(value);
 | 
				
			||||||
 | 
					            const lines = chunk.split("\n").filter((line) => line.trim().startsWith("data: "));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for (const line of lines) {
 | 
				
			||||||
 | 
					              try {
 | 
				
			||||||
 | 
					                // 移除 "data: " 前缀并解析JSON
 | 
				
			||||||
 | 
					                const jsonData = JSON.parse(line.substring(6));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (jsonData.response) {
 | 
				
			||||||
 | 
					                  let content = jsonData.response;
 | 
				
			||||||
 | 
					                  // if (content.includes("\u003c/think\u003e")) {
 | 
				
			||||||
 | 
					                  //   thinkContent = false;
 | 
				
			||||||
 | 
					                  // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  // if (!thinkContent) {
 | 
				
			||||||
 | 
					                  //   accumulatedContent += content;
 | 
				
			||||||
 | 
					                  //   currentBotMessage.innerHTML = marked.parse(accumulatedContent);
 | 
				
			||||||
 | 
					                  // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  accumulatedContent += content;
 | 
				
			||||||
 | 
					                  currentBotMessage.innerHTML = marked.parse(accumulatedContent);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (jsonData.done) {
 | 
				
			||||||
 | 
					                  currentBotMessage = null;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              } catch (error) {
 | 
				
			||||||
 | 
					                console.error("解析数据失败:", error, "原始数据:", line);
 | 
				
			||||||
 | 
					                continue;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            document.querySelector(".messages-container").scrollTop = document.querySelector(".messages-container").scrollHeight;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					          console.error("请求失败:", error);
 | 
				
			||||||
 | 
					          addMessage(`[错误] ${error.message}`, false);
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					          btn.disabled = false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 发送消息
 | 
				
			||||||
 | 
					      async function sendMessage() {
 | 
				
			||||||
 | 
					        const input = document.getElementById("userInput");
 | 
				
			||||||
 | 
					        const userMessage = input.value.trim();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!userMessage) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        addMessage(userMessage, true);
 | 
				
			||||||
 | 
					        input.value = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await streamResponse(userMessage);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 回车键发送
 | 
				
			||||||
 | 
					      document.getElementById("userInput").addEventListener("keypress", (e) => {
 | 
				
			||||||
 | 
					        if (e.key === "Enter" && !e.shiftKey) {
 | 
				
			||||||
 | 
					          e.preventDefault();
 | 
				
			||||||
 | 
					          sendMessage();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      function showUploadDialog() {
 | 
				
			||||||
 | 
					        document.getElementById("uploadDialog").style.display = "block";
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      function closeUploadDialog() {
 | 
				
			||||||
 | 
					        document.getElementById("uploadDialog").style.display = "none";
 | 
				
			||||||
 | 
					        resetUploadDialog();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      function resetUploadDialog() {
 | 
				
			||||||
 | 
					        document.getElementById("fileInput").value = "";
 | 
				
			||||||
 | 
					        document.getElementById("uploadProgress").style.display = "none";
 | 
				
			||||||
 | 
					        document.getElementById("uploadStatus").innerHTML = "";
 | 
				
			||||||
 | 
					        document.getElementById("uploadStatus").className = "upload-status";
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      function handleFileSelect(event) {
 | 
				
			||||||
 | 
					        const file = event.target.files[0];
 | 
				
			||||||
 | 
					        if (file) {
 | 
				
			||||||
 | 
					          uploadFile(file);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      function updateProgress(percent) {
 | 
				
			||||||
 | 
					        const progressBar = document.querySelector(".progress-fill");
 | 
				
			||||||
 | 
					        const progressText = document.querySelector(".progress-text");
 | 
				
			||||||
 | 
					        progressBar.style.width = `${percent}%`;
 | 
				
			||||||
 | 
					        progressText.textContent = `${percent}%`;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      function uploadFile(file) {
 | 
				
			||||||
 | 
					        const formData = new FormData();
 | 
				
			||||||
 | 
					        formData.append("file", file);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const progressDiv = document.getElementById("uploadProgress");
 | 
				
			||||||
 | 
					        const statusDiv = document.getElementById("uploadStatus");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        progressDiv.style.display = "block";
 | 
				
			||||||
 | 
					        statusDiv.innerHTML = "正在上传...";
 | 
				
			||||||
 | 
					        statusDiv.className = "upload-status";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        fetch(url + "/api/file/upload", {
 | 
				
			||||||
 | 
					          method: "POST",
 | 
				
			||||||
 | 
					          body: formData
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					          .then((response) => response.json())
 | 
				
			||||||
 | 
					          .then((data) => {
 | 
				
			||||||
 | 
					            statusDiv.innerHTML = data.message;
 | 
				
			||||||
 | 
					            statusDiv.className = "upload-status success";
 | 
				
			||||||
 | 
					            updateProgress(100);
 | 
				
			||||||
 | 
					            setTimeout(() => {
 | 
				
			||||||
 | 
					              closeUploadDialog();
 | 
				
			||||||
 | 
					            }, 2000);
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					          .catch((error) => {
 | 
				
			||||||
 | 
					            statusDiv.innerHTML = "上传失败: " + error.message;
 | 
				
			||||||
 | 
					            statusDiv.className = "upload-status error";
 | 
				
			||||||
 | 
					            updateProgress(0);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 添加拖拽上传支持
 | 
				
			||||||
 | 
					      const dropZone = document.getElementById("dropZone");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      dropZone.addEventListener("dragover", (e) => {
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        dropZone.style.borderColor = "#4CAF50";
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      dropZone.addEventListener("dragleave", (e) => {
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        dropZone.style.borderColor = "#ccc";
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      dropZone.addEventListener("drop", (e) => {
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        dropZone.style.borderColor = "#ccc";
 | 
				
			||||||
 | 
					        const file = e.dataTransfer.files[0];
 | 
				
			||||||
 | 
					        if (file) {
 | 
				
			||||||
 | 
					          uploadFile(file);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    </script>
 | 
				
			||||||
 | 
					  </body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										337
									
								
								konws-web/css/main.css
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										337
									
								
								konws-web/css/main.css
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,337 @@
 | 
				
			|||||||
 | 
					body {
 | 
				
			||||||
 | 
					  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
 | 
				
			||||||
 | 
					  max-width: 800px;
 | 
				
			||||||
 | 
					  margin: 0 auto;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  background-color: #f5f5f5;
 | 
				
			||||||
 | 
					  height: 100vh;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#chatBox {
 | 
				
			||||||
 | 
					  flex: 1;
 | 
				
			||||||
 | 
					  background: #ededed;
 | 
				
			||||||
 | 
					  padding: 20px;
 | 
				
			||||||
 | 
					  overflow-y: auto;
 | 
				
			||||||
 | 
					  margin: 50px;
 | 
				
			||||||
 | 
					  border-radius: 18px;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  padding-bottom: 80px;
 | 
				
			||||||
 | 
					  scrollbar-width: none;
 | 
				
			||||||
 | 
					  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
 | 
					  /* Firefox */
 | 
				
			||||||
 | 
					  -ms-overflow-style: none;
 | 
				
			||||||
 | 
					  /* IE and Edge */
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#chatBox::-webkit-scrollbar {
 | 
				
			||||||
 | 
					  display: none;
 | 
				
			||||||
 | 
					  /* Chrome, Safari, Opera */
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.messages-container {
 | 
				
			||||||
 | 
					  flex: 1;
 | 
				
			||||||
 | 
					  overflow-y: auto;
 | 
				
			||||||
 | 
					  padding: 20px;
 | 
				
			||||||
 | 
					  scrollbar-width: none;
 | 
				
			||||||
 | 
					  /* Firefox */
 | 
				
			||||||
 | 
					  -ms-overflow-style: none;
 | 
				
			||||||
 | 
					  /* IE and Edge */
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.messages-container::-webkit-scrollbar {
 | 
				
			||||||
 | 
					  display: none;
 | 
				
			||||||
 | 
					  /* Chrome, Safari, Opera */
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.message {
 | 
				
			||||||
 | 
					  margin: 10px 0;
 | 
				
			||||||
 | 
					  padding: 10px 15px;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  max-width: 70%;
 | 
				
			||||||
 | 
					  word-wrap: break-word;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  line-height: 1.5;
 | 
				
			||||||
 | 
					  font-size: 15px;
 | 
				
			||||||
 | 
					  width: max-content;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: flex-start;
 | 
				
			||||||
 | 
					  gap: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.avatar {
 | 
				
			||||||
 | 
					  width: 40px;
 | 
				
			||||||
 | 
					  height: 40px;
 | 
				
			||||||
 | 
					  border-radius: 50%;
 | 
				
			||||||
 | 
					  flex-shrink: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.message-content {
 | 
				
			||||||
 | 
					  padding: 10px 15px;
 | 
				
			||||||
 | 
					  border-radius: 15px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.user-message {
 | 
				
			||||||
 | 
					  width: max-content;
 | 
				
			||||||
 | 
					  margin-left: auto;
 | 
				
			||||||
 | 
					  flex-direction: row-reverse;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.user-message .message-content {
 | 
				
			||||||
 | 
					  background: #95ec69;
 | 
				
			||||||
 | 
					  border-radius: 15px 0 15px 15px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.bot-message {
 | 
				
			||||||
 | 
					  background: white;
 | 
				
			||||||
 | 
					  margin-right: auto;
 | 
				
			||||||
 | 
					  border-radius: 0 15px 15px 15px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#inputArea {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  bottom: 20px;
 | 
				
			||||||
 | 
					  left: 50%;
 | 
				
			||||||
 | 
					  transform: translateX(-50%);
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  gap: 10px;
 | 
				
			||||||
 | 
					  padding: 15px;
 | 
				
			||||||
 | 
					  background: white;
 | 
				
			||||||
 | 
					  border-radius: 20px;
 | 
				
			||||||
 | 
					  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
 | 
					  width: calc(100% - 100px);
 | 
				
			||||||
 | 
					  max-width: 600px;
 | 
				
			||||||
 | 
					  box-sizing: border-box;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#userInput {
 | 
				
			||||||
 | 
					  flex-grow: 1;
 | 
				
			||||||
 | 
					  padding: 8px 12px;
 | 
				
			||||||
 | 
					  border: 1px solid #ddd;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  font-size: 15px;
 | 
				
			||||||
 | 
					  background: white;
 | 
				
			||||||
 | 
					  outline: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#userInput:focus {
 | 
				
			||||||
 | 
					  border-color: #07c160;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  background: #07c160;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  transition: background 0.2s;
 | 
				
			||||||
 | 
					  font-size: 15px;
 | 
				
			||||||
 | 
					  border-radius: 50%;
 | 
				
			||||||
 | 
					  width: 40px;
 | 
				
			||||||
 | 
					  height: 40px;
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  right: 18px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button:hover {
 | 
				
			||||||
 | 
					  background: #06ae56;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button:disabled {
 | 
				
			||||||
 | 
					  background: #9fd7b5;
 | 
				
			||||||
 | 
					  cursor: not-allowed;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Markdown 样式优化 */
 | 
				
			||||||
 | 
					.message pre {
 | 
				
			||||||
 | 
					  background: #f8f9fa;
 | 
				
			||||||
 | 
					  padding: 12px;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  overflow-x: auto;
 | 
				
			||||||
 | 
					  margin: 8px 0;
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.message code {
 | 
				
			||||||
 | 
					  font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
 | 
				
			||||||
 | 
					  background: rgba(0, 0, 0, 0.05);
 | 
				
			||||||
 | 
					  padding: 2px 4px;
 | 
				
			||||||
 | 
					  border-radius: 3px;
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.message p {
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.message p+p {
 | 
				
			||||||
 | 
					  margin-top: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 滚动条样式 */
 | 
				
			||||||
 | 
					#chatBox::-webkit-scrollbar {
 | 
				
			||||||
 | 
					  width: 6px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#chatBox::-webkit-scrollbar-track {
 | 
				
			||||||
 | 
					  background: #f1f1f1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#chatBox::-webkit-scrollbar-thumb {
 | 
				
			||||||
 | 
					  background: #c1c1c1;
 | 
				
			||||||
 | 
					  border-radius: 3px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#chatBox::-webkit-scrollbar-thumb:hover {
 | 
				
			||||||
 | 
					  background: #a8a8a8;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 适配移动端 */
 | 
				
			||||||
 | 
					@media (max-width: 768px) {
 | 
				
			||||||
 | 
					  body {
 | 
				
			||||||
 | 
					    max-width: 100%;
 | 
				
			||||||
 | 
					    height: 100vh;
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .message {
 | 
				
			||||||
 | 
					    max-width: 85%;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  #inputArea {
 | 
				
			||||||
 | 
					    padding: 10px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.upload-button-container {
 | 
				
			||||||
 | 
					  position: fixed;
 | 
				
			||||||
 | 
					  bottom: 20px;
 | 
				
			||||||
 | 
					  right: 20px;
 | 
				
			||||||
 | 
					  z-index: 1000;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.upload-btn {
 | 
				
			||||||
 | 
					  background-color: #000;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  border-radius: 50%;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  font-size: 16px;
 | 
				
			||||||
 | 
					  transition: background-color 0.3s;
 | 
				
			||||||
 | 
					  bottom: 50px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.upload-btn:hover {
 | 
				
			||||||
 | 
					  background-color: #1a1a1a;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.upload-dialog {
 | 
				
			||||||
 | 
					  display: none;
 | 
				
			||||||
 | 
					  position: fixed;
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					  left: 0;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  background-color: rgba(0, 0, 0, 0.5);
 | 
				
			||||||
 | 
					  z-index: 1001;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.upload-dialog-content {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  background-color: #fefefe;
 | 
				
			||||||
 | 
					  margin: 15% auto;
 | 
				
			||||||
 | 
					  padding: 20px;
 | 
				
			||||||
 | 
					  border-radius: 5px;
 | 
				
			||||||
 | 
					  width: 60%;
 | 
				
			||||||
 | 
					  max-width: 500px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.close-btn {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  right: 10px;
 | 
				
			||||||
 | 
					  top: 5px;
 | 
				
			||||||
 | 
					  font-size: 24px;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  color: #888;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.close-btn:hover {
 | 
				
			||||||
 | 
					  color: #555;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.upload-area {
 | 
				
			||||||
 | 
					  border: 2px dashed #ccc;
 | 
				
			||||||
 | 
					  border-radius: 5px;
 | 
				
			||||||
 | 
					  padding: 20px;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  margin: 20px 0;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  transition: border-color 0.3s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.upload-area:hover {
 | 
				
			||||||
 | 
					  border-color: #4caf50;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.upload-placeholder {
 | 
				
			||||||
 | 
					  color: #666;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.upload-placeholder i {
 | 
				
			||||||
 | 
					  font-size: 48px;
 | 
				
			||||||
 | 
					  color: #4caf50;
 | 
				
			||||||
 | 
					  margin-bottom: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.supported-formats {
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					  color: #888;
 | 
				
			||||||
 | 
					  margin-top: 5px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.upload-progress {
 | 
				
			||||||
 | 
					  margin: 15px 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.progress-bar {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 20px;
 | 
				
			||||||
 | 
					  background-color: #f0f0f0;
 | 
				
			||||||
 | 
					  border-radius: 10px;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.progress-fill {
 | 
				
			||||||
 | 
					  width: 0%;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  background-color: #4caf50;
 | 
				
			||||||
 | 
					  transition: width 0.3s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.progress-text {
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  margin-top: 5px;
 | 
				
			||||||
 | 
					  color: #666;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.upload-status {
 | 
				
			||||||
 | 
					  margin-top: 10px;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  color: #666;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.upload-status.success {
 | 
				
			||||||
 | 
					  color: #4caf50;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.upload-status.error {
 | 
				
			||||||
 | 
					  color: #f44336;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								konws-web/images/bot-avatar.png
									
									
									
									
									
										Normal file
									
								
							
							
								
									
								
								
								
								
								
									
									
								
							
						
						
									
										
											BIN
										
									
								
								konws-web/images/bot-avatar.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 3.5 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								konws-web/images/user-avatar.png
									
									
									
									
									
										Normal file
									
								
							
							
								
									
								
								
								
								
								
									
									
								
							
						
						
									
										
											BIN
										
									
								
								konws-web/images/user-avatar.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 4.7 KiB  | 
							
								
								
									
										6
									
								
								konws-web/js/marked.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										6
									
								
								konws-web/js/marked.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
		Loading…
	
		Reference in New Issue
	
	Block a user