feat(ai): 完善知识库接口
This commit is contained in:
8
service-ai/.idea/GitCommitMessageStorage.xml
generated
Normal file
8
service-ai/.idea/GitCommitMessageStorage.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GitCommitMessageStorage">
|
||||
<option name="messageStorage">
|
||||
<MessageStorage />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
6
service-ai/bin/build-ai-chat.sh
Executable file
6
service-ai/bin/build-ai-chat.sh
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
root_path=$(dirname $(cd $(dirname $0);pwd))
|
||||
source $(realpath $root_path/..)/bin/library.sh
|
||||
deploy service-ai-core
|
||||
package service-ai-chat
|
||||
upload $root_path/service-ai-chat/target/service-ai-chat-1.0.0-SNAPSHOT.jar
|
||||
@@ -30,6 +30,10 @@
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-qdrant-store-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-markdown-document-reader</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
package com.lanyuanxiaoyao.service.ai.knowledge;
|
||||
|
||||
import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.net.MalformedURLException;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.model.Media;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
import org.springframework.core.io.FileUrlResource;
|
||||
import org.springframework.retry.annotation.EnableRetry;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
/**
|
||||
* @author lanyuanxiaoyao
|
||||
@@ -25,35 +19,11 @@ import org.springframework.util.MimeTypeUtils;
|
||||
@EnableEncryptableProperties
|
||||
@EnableRetry
|
||||
public class KnowledgeApplication implements ApplicationRunner {
|
||||
@Resource
|
||||
private ChatClient.Builder builder;
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(KnowledgeApplication.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) throws Exception {
|
||||
ChatClient client = builder.build();
|
||||
String content = client.prompt()
|
||||
.user(
|
||||
prompt -> {
|
||||
try {
|
||||
prompt
|
||||
.text("如实描述图片中的内容,不要加入自己的思考以及与图片内容无关的任何文本")
|
||||
.media(
|
||||
Media.builder()
|
||||
.mimeType(MimeTypeUtils.IMAGE_PNG)
|
||||
.data(new FileUrlResource("/Users/lanyuanxiaoyao/Pictures/数据使用合同签订.png"))
|
||||
.build()
|
||||
);
|
||||
} catch (MalformedURLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
)
|
||||
.call()
|
||||
.content();
|
||||
System.out.println(content);
|
||||
public void run(ApplicationArguments args) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
package com.lanyuanxiaoyao.service.ai.knowledge.controller;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import com.lanyuanxiaoyao.service.ai.knowledge.entity.CollectionVO;
|
||||
import com.lanyuanxiaoyao.service.ai.knowledge.entity.PointVO;
|
||||
import io.qdrant.client.QdrantClient;
|
||||
import io.qdrant.client.grpc.Collections;
|
||||
import io.qdrant.client.grpc.Points;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.stream.Collectors;
|
||||
import org.eclipse.collections.api.factory.Lists;
|
||||
import org.eclipse.collections.api.list.ImmutableList;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.embedding.EmbeddingModel;
|
||||
import org.springframework.ai.reader.markdown.MarkdownDocumentReader;
|
||||
import org.springframework.ai.reader.markdown.config.MarkdownDocumentReaderConfig;
|
||||
import org.springframework.ai.vectorstore.VectorStore;
|
||||
import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
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.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
@@ -15,10 +34,108 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
public class KnowledgeController {
|
||||
private static final Logger logger = LoggerFactory.getLogger(KnowledgeController.class);
|
||||
|
||||
public KnowledgeController() {
|
||||
private final QdrantClient client;
|
||||
private final EmbeddingModel embeddingModel;
|
||||
|
||||
public KnowledgeController(VectorStore vectorStore, EmbeddingModel embeddingModel) {
|
||||
client = (QdrantClient) vectorStore.getNativeClient().orElseThrow();
|
||||
this.embeddingModel = embeddingModel;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void initial() {
|
||||
@PostMapping("add")
|
||||
public void add(
|
||||
@RequestParam("name") String name,
|
||||
@RequestParam("strategy") String strategy
|
||||
) throws ExecutionException, InterruptedException {
|
||||
logger.info("Enter method: add[name, strategy]. name:{},strategy:{}", name, strategy);
|
||||
client.createCollectionAsync(
|
||||
name,
|
||||
Collections.VectorParams.newBuilder()
|
||||
.setDistance(Collections.Distance.valueOf(strategy))
|
||||
.setSize(embeddingModel.dimensions())
|
||||
.build()
|
||||
).get();
|
||||
}
|
||||
|
||||
@GetMapping("list")
|
||||
public ImmutableList<CollectionVO> list() throws ExecutionException, InterruptedException {
|
||||
return client.listCollectionsAsync()
|
||||
.get()
|
||||
.stream()
|
||||
.collect(Collectors.toCollection(Lists.mutable::empty))
|
||||
.collect(name -> {
|
||||
try {
|
||||
Collections.CollectionInfo info = client.getCollectionInfoAsync(name).get();
|
||||
CollectionVO vo = new CollectionVO();
|
||||
vo.setName(name);
|
||||
vo.setPoints(info.getPointsCount());
|
||||
vo.setSegments(info.getSegmentsCount());
|
||||
vo.setStatus(info.getStatus().name());
|
||||
Collections.VectorParams vectorParams = info.getConfig().getParams().getVectorsConfig().getParams();
|
||||
vo.setStrategy(vectorParams.getDistance().name());
|
||||
vo.setSize(vectorParams.getSize());
|
||||
return vo;
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.toImmutable();
|
||||
}
|
||||
|
||||
@GetMapping("list_points")
|
||||
public ImmutableList<PointVO> listPoints(@RequestParam("name") String name) throws ExecutionException, InterruptedException {
|
||||
Points.ScrollResponse response = client.scrollAsync(
|
||||
Points.ScrollPoints.newBuilder()
|
||||
.setCollectionName(name)
|
||||
// .setLimit(2)
|
||||
.setWithPayload(Points.WithPayloadSelector.newBuilder().setEnable(true).build())
|
||||
.setWithVectors(Points.WithVectorsSelector.newBuilder().setEnable(false).build())
|
||||
.build()
|
||||
)
|
||||
.get();
|
||||
return response.getResultList()
|
||||
.stream()
|
||||
.collect(Collectors.toCollection(Lists.mutable::empty))
|
||||
.collect(point -> {
|
||||
PointVO vo = new PointVO();
|
||||
vo.setId(point.getId().getUuid());
|
||||
vo.setText(point.getPayloadMap().get("doc_content").getStringValue());
|
||||
return vo;
|
||||
})
|
||||
.toImmutable();
|
||||
}
|
||||
|
||||
@GetMapping("delete")
|
||||
public void delete(@RequestParam("name") String name) throws ExecutionException, InterruptedException {
|
||||
client.deleteCollectionAsync(name).get();
|
||||
}
|
||||
|
||||
@PostMapping(value = "preview_text", consumes = "text/plain;charset=utf-8")
|
||||
public ImmutableList<String> previewText(
|
||||
@RequestParam("name") String name,
|
||||
@RequestParam(value = "mode", defaultValue = "normal") String mode,
|
||||
@RequestBody String text
|
||||
) {
|
||||
return Lists.immutable.empty();
|
||||
}
|
||||
|
||||
@PostMapping(value = "process_text", consumes = "text/plain;charset=utf-8")
|
||||
public void processText(
|
||||
@RequestParam("name") String name,
|
||||
@RequestBody String text
|
||||
) {
|
||||
VectorStore source = QdrantVectorStore.builder(client, embeddingModel)
|
||||
.collectionName(name)
|
||||
.initializeSchema(true)
|
||||
.build();
|
||||
MarkdownDocumentReader reader = new MarkdownDocumentReader(
|
||||
new ByteArrayResource(text.getBytes(StandardCharsets.UTF_8)),
|
||||
MarkdownDocumentReaderConfig.builder()
|
||||
.withHorizontalRuleCreateDocument(true)
|
||||
.withIncludeCodeBlock(false)
|
||||
.withIncludeBlockquote(false)
|
||||
.build()
|
||||
);
|
||||
source.add(reader.get());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.lanyuanxiaoyao.service.ai.knowledge.entity;
|
||||
|
||||
/**
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20250516
|
||||
*/
|
||||
public class CollectionVO {
|
||||
private String name;
|
||||
private String strategy;
|
||||
private Long size;
|
||||
private Long points;
|
||||
private Long segments;
|
||||
private String status;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getStrategy() {
|
||||
return strategy;
|
||||
}
|
||||
|
||||
public void setStrategy(String strategy) {
|
||||
this.strategy = strategy;
|
||||
}
|
||||
|
||||
public Long getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public void setSize(Long size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public Long getPoints() {
|
||||
return points;
|
||||
}
|
||||
|
||||
public void setPoints(Long points) {
|
||||
this.points = points;
|
||||
}
|
||||
|
||||
public Long getSegments() {
|
||||
return segments;
|
||||
}
|
||||
|
||||
public void setSegments(Long segments) {
|
||||
this.segments = segments;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CollectionVO{" +
|
||||
"name='" + name + '\'' +
|
||||
", strategy='" + strategy + '\'' +
|
||||
", size=" + size +
|
||||
", points=" + points +
|
||||
", segments=" + segments +
|
||||
", status='" + status + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.lanyuanxiaoyao.service.ai.knowledge.entity;
|
||||
|
||||
/**
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20250516
|
||||
*/
|
||||
public class PointVO {
|
||||
private String id;
|
||||
private String text;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PointVO{" +
|
||||
"id='" + id + '\'' +
|
||||
", text='" + text + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -24,18 +24,17 @@ spring:
|
||||
darkcode: ENC(0jzpQ7T6S+P7bZrENgYsUoLhlqGvw7DA2MN3BRqEOwq7plhtg72vuuiPQNnr3DaYz0CpyTvxInhpx11W3VZ1trD6NINh7O3LN70ZqO5pWXk=)
|
||||
ai:
|
||||
openai:
|
||||
base-url: http://localhost:3000
|
||||
api-key: '*XMySqV%>hR&v>>g*NwCs3tpQ5FVMFEF2VHVTj<MYQd$&@$sY7CgqNyea4giJi4'
|
||||
base-url: http://132.121.206.65:10086
|
||||
api-key: ENC(K+Hff9QGC+fcyi510VIDd9CaeK/IN5WBJ9rlkUsHEdDgIidW+stHHJlsK0lLPUXXREha+ToQZqqDXJrqSE+GUKCXklFhelD8bRHFXBIeP/ZzT2cxhzgKUXgjw3S0Qw2R)
|
||||
chat:
|
||||
options:
|
||||
model: '/models/InternVL3-1B-Instruct-Q8_0.gguf'
|
||||
model: 'Qwen3-1.7'
|
||||
embedding:
|
||||
options:
|
||||
model: 'Bge-m3'
|
||||
# vectorstore:
|
||||
# qdrant:
|
||||
# api-key: lanyuanxiaoyao
|
||||
# initialize-schema: true
|
||||
vectorstore:
|
||||
qdrant:
|
||||
api-key: lanyuanxiaoyao
|
||||
jasypt:
|
||||
encryptor:
|
||||
password: 'r#(R,P"Dp^A47>WSn:Wn].gs/+"v:q_Q*An~zF*g-@j@jtSTv5H/,S-3:R?r9R}.'
|
||||
|
||||
105
service-web/client/src/pages/ai/knowledge/DataDetail.tsx
Normal file
105
service-web/client/src/pages/ai/knowledge/DataDetail.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React from 'react'
|
||||
import {useParams} from 'react-router'
|
||||
import {amisRender, crudCommonOptions} from '../../../util/amis.tsx'
|
||||
|
||||
const DataDetail: React.FC = () => {
|
||||
const {name} = useParams()
|
||||
return (
|
||||
<div className="import-detail h-full">
|
||||
{amisRender(
|
||||
{
|
||||
className: 'h-full',
|
||||
type: 'page',
|
||||
title: `数据详情 (知识库:${name})`,
|
||||
size: 'lg',
|
||||
actions: [],
|
||||
body: [
|
||||
{
|
||||
type: 'crud',
|
||||
api: {
|
||||
url: 'http://127.0.0.1:8080/knowledge/list_points?name=${name}',
|
||||
headers: {
|
||||
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
|
||||
},
|
||||
},
|
||||
...crudCommonOptions(),
|
||||
headerToolbar: [
|
||||
'reload',
|
||||
],
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
name: 'text',
|
||||
label: '内容',
|
||||
},
|
||||
{
|
||||
type: 'operation',
|
||||
label: '操作',
|
||||
width: 100,
|
||||
buttons: [
|
||||
{
|
||||
type: 'action',
|
||||
label: '编辑',
|
||||
level: 'link',
|
||||
size: 'lg',
|
||||
actionType: 'dialog',
|
||||
dialog: {
|
||||
title: '编辑文段',
|
||||
size: 'md',
|
||||
body: {
|
||||
type: 'form',
|
||||
body: [
|
||||
{
|
||||
type: 'input-text',
|
||||
name: 'id',
|
||||
disabled: true,
|
||||
label: '文段ID',
|
||||
},
|
||||
{
|
||||
type: 'editor',
|
||||
label: '内容',
|
||||
name: 'text',
|
||||
language: 'plaintext',
|
||||
options: {
|
||||
lineNumbers: 'off',
|
||||
wordWrap: 'bounded',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'action',
|
||||
label: '删除',
|
||||
className: 'text-danger hover:text-red-600',
|
||||
level: 'link',
|
||||
size: 'xs',
|
||||
actionType: 'ajax',
|
||||
api: {
|
||||
method: 'get',
|
||||
headers: {
|
||||
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
|
||||
},
|
||||
},
|
||||
confirmText: '确认删除',
|
||||
confirmTitle: '删除',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: name,
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DataDetail
|
||||
144
service-web/client/src/pages/ai/knowledge/DataImport.tsx
Normal file
144
service-web/client/src/pages/ai/knowledge/DataImport.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import React from 'react'
|
||||
import {useParams} from 'react-router'
|
||||
import styled from 'styled-components'
|
||||
import {amisRender} from '../../../util/amis.tsx'
|
||||
|
||||
const ImportDataDiv = styled.div`
|
||||
.antd-EditorControl {
|
||||
min-height: 500px !important;
|
||||
}
|
||||
`
|
||||
|
||||
const DataImport: React.FC = () => {
|
||||
const {name} = useParams()
|
||||
return (
|
||||
<ImportDataDiv className="import-data h-full">
|
||||
{amisRender({
|
||||
type: 'page',
|
||||
title: `数据导入 (知识库:${name})`,
|
||||
body: [
|
||||
[
|
||||
{
|
||||
className: 'h-full',
|
||||
type: 'grid',
|
||||
columns: [
|
||||
{
|
||||
body: [
|
||||
{
|
||||
type: 'form',
|
||||
wrapWithPanel: false,
|
||||
mode: 'horizontal',
|
||||
actions: [],
|
||||
body: [
|
||||
{
|
||||
name: 'mode',
|
||||
type: 'radios',
|
||||
label: '解析模式',
|
||||
value: 'normal',
|
||||
options: [
|
||||
{
|
||||
value: 'normal',
|
||||
label: '常规模式',
|
||||
},
|
||||
{
|
||||
value: 'llm',
|
||||
label: '智能模式',
|
||||
},
|
||||
{
|
||||
value: 'qa',
|
||||
label: 'Q/A模式',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
type: 'radios',
|
||||
label: '数据形式',
|
||||
value: 'text',
|
||||
options: [
|
||||
{
|
||||
value: 'text',
|
||||
label: '文本',
|
||||
},
|
||||
{
|
||||
value: 'file',
|
||||
label: '文件',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
visibleOn: 'type === \'text\'',
|
||||
type: 'editor',
|
||||
label: '数据内容',
|
||||
name: 'content',
|
||||
language: 'plaintext',
|
||||
options: {
|
||||
lineNumbers: 'off',
|
||||
wordWrap: 'bounded',
|
||||
},
|
||||
},
|
||||
{
|
||||
visibleOn: 'type === \'file\'',
|
||||
type: 'input-file',
|
||||
name: 'files',
|
||||
label: '数据文件',
|
||||
accept: '.txt,.csv',
|
||||
autoUpload: false,
|
||||
drag: true,
|
||||
multiple: true,
|
||||
},
|
||||
{
|
||||
className: 'text-right',
|
||||
type: 'button-toolbar',
|
||||
buttons: [
|
||||
{
|
||||
type: 'action',
|
||||
label: '预览',
|
||||
},
|
||||
{
|
||||
type: 'submit',
|
||||
label: '提交',
|
||||
level: 'primary',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
body: [
|
||||
{
|
||||
type: 'card',
|
||||
className: 'h-full',
|
||||
header: {
|
||||
title: '解析预览',
|
||||
subTitle: '截取部份文本进行解析预览',
|
||||
},
|
||||
body: [
|
||||
{
|
||||
type: 'list',
|
||||
source: '${rows}',
|
||||
listItem: [
|
||||
{
|
||||
body: {
|
||||
type: 'tpl',
|
||||
tpl: '${content}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
})}
|
||||
</ImportDataDiv>
|
||||
)
|
||||
}
|
||||
|
||||
export default DataImport
|
||||
176
service-web/client/src/pages/ai/knowledge/Knowledge.tsx
Normal file
176
service-web/client/src/pages/ai/knowledge/Knowledge.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import React from 'react'
|
||||
import {useNavigate} from 'react-router'
|
||||
import {amisRender, crudCommonOptions, mappingField, mappingItem} from '../../../util/amis.tsx'
|
||||
|
||||
const strategyMapping = [
|
||||
mappingItem('文本', 'Cosine'),
|
||||
mappingItem('图片', 'Euclid'),
|
||||
]
|
||||
|
||||
const statusMapping = [
|
||||
mappingItem('正常', 'Green', 'label-success'),
|
||||
mappingItem('优化中', 'Yellow', 'label-warning'),
|
||||
mappingItem('错误', 'Red', 'label-danger'),
|
||||
mappingItem('等待中', 'Grey', 'label-primary'),
|
||||
]
|
||||
|
||||
const Knowledge: React.FC = () => {
|
||||
const navigate = useNavigate()
|
||||
return (
|
||||
<div className="knowledge">
|
||||
{amisRender(
|
||||
{
|
||||
type: 'page',
|
||||
title: '知识库',
|
||||
body: [
|
||||
{
|
||||
type: 'crud',
|
||||
api: {
|
||||
url: 'http://127.0.0.1:8080/knowledge/list',
|
||||
headers: {
|
||||
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
|
||||
},
|
||||
},
|
||||
...crudCommonOptions(),
|
||||
headerToolbar: [
|
||||
'reload',
|
||||
{
|
||||
type: 'action',
|
||||
label: '',
|
||||
icon: 'fa fa-plus',
|
||||
actionType: 'dialog',
|
||||
dialog: {
|
||||
title: '新增知识库',
|
||||
size: 'md',
|
||||
body: {
|
||||
type: 'form',
|
||||
api: {
|
||||
url: 'http://127.0.0.1:8080/knowledge/add',
|
||||
dataType: 'form',
|
||||
headers: {
|
||||
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
|
||||
},
|
||||
},
|
||||
body: [
|
||||
{
|
||||
type: 'input-text',
|
||||
name: 'name',
|
||||
label: '名称',
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'strategy',
|
||||
label: '类型',
|
||||
value: 'Cosine',
|
||||
options: [
|
||||
{
|
||||
label: '文本',
|
||||
value: 'Cosine',
|
||||
},
|
||||
{
|
||||
label: '图片',
|
||||
value: 'Euclid',
|
||||
disabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
columns: [
|
||||
{
|
||||
name: 'name',
|
||||
label: '名称',
|
||||
},
|
||||
{
|
||||
label: '类型',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
...mappingField('strategy', strategyMapping),
|
||||
},
|
||||
{
|
||||
name: 'points',
|
||||
label: '文本数',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
...mappingField('status', statusMapping),
|
||||
},
|
||||
{
|
||||
type: 'operation',
|
||||
label: '操作',
|
||||
width: 150,
|
||||
buttons: [
|
||||
{
|
||||
type: 'action',
|
||||
label: '详情',
|
||||
level: 'link',
|
||||
size: 'xs',
|
||||
onEvent: {
|
||||
click: {
|
||||
actions: [
|
||||
{
|
||||
actionType: 'custom',
|
||||
// @ts-ignore
|
||||
script: (context, action, event) => {
|
||||
navigate(`/ai/knowledge/detail/${context.props.data['name']}`)
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'action',
|
||||
label: '导入',
|
||||
level: 'link',
|
||||
size: 'xs',
|
||||
onEvent: {
|
||||
click: {
|
||||
actions: [
|
||||
{
|
||||
actionType: 'custom',
|
||||
// @ts-ignore
|
||||
script: (context, action, event) => {
|
||||
navigate(`/ai/knowledge/import/${context.props.data['name']}`)
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'action',
|
||||
label: '删除',
|
||||
className: 'text-danger hover:text-red-600',
|
||||
level: 'link',
|
||||
size: 'xs',
|
||||
actionType: 'ajax',
|
||||
api: {
|
||||
method: 'get',
|
||||
url: 'http://127.0.0.1:8080/knowledge/delete?name=${name}',
|
||||
headers: {
|
||||
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
|
||||
},
|
||||
},
|
||||
confirmText: '确认删除',
|
||||
confirmTitle: '删除',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Knowledge
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
serviceLogByAppName,
|
||||
serviceLogByAppNameAndHost,
|
||||
time,
|
||||
} from '../../util/amis.ts'
|
||||
} from '../../util/amis.tsx'
|
||||
|
||||
const cloudCrud = (title: string, path: string) => {
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import {amisRender, commonInfo, crudCommonOptions, readOnlyDialogOptions} from '../../util/amis.ts'
|
||||
import {amisRender, commonInfo, crudCommonOptions, readOnlyDialogOptions} from '../../util/amis.tsx'
|
||||
|
||||
const color = (number: number) => {
|
||||
let color = 'text-success'
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
paginationCommonOptions,
|
||||
time,
|
||||
yarnQueueCrud,
|
||||
} from '../../util/amis.ts'
|
||||
} from '../../util/amis.tsx'
|
||||
|
||||
const queueCrud = (name: string) => {
|
||||
return {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
tableMetaDialog,
|
||||
tableRunningStateMapping,
|
||||
timeAndFrom,
|
||||
} from '../../util/amis.ts'
|
||||
} from '../../util/amis.tsx'
|
||||
|
||||
function Table() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import {amisRender, commonInfo, paginationCommonOptions, serviceLogByAppName, yarnCrudColumns} from '../../util/amis.ts'
|
||||
import {amisRender, commonInfo, paginationCommonOptions, serviceLogByAppName, yarnCrudColumns} from '../../util/amis.tsx'
|
||||
|
||||
const Task: React.FC = () => {
|
||||
return (
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
paginationCommonOptions,
|
||||
readOnlyDialogOptions,
|
||||
timelineColumns,
|
||||
} from '../../util/amis.ts'
|
||||
} from '../../util/amis.tsx'
|
||||
|
||||
const Tool: React.FC = () => {
|
||||
return (
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
paginationCommonOptions,
|
||||
tableMetaDialog,
|
||||
versionUpdateStateMapping,
|
||||
} from '../../util/amis.ts'
|
||||
} from '../../util/amis.tsx'
|
||||
|
||||
function Version() {
|
||||
return (
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
paginationCommonOptions,
|
||||
yarnCrudColumns,
|
||||
yarnQueueCrud,
|
||||
} from '../../util/amis.ts'
|
||||
} from '../../util/amis.tsx'
|
||||
|
||||
const Yarn: React.FC = () => {
|
||||
const {clusters, queue, search} = useParams()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import {amisRender, commonInfo, yarnQueueCrud} from '../../util/amis.ts'
|
||||
import {amisRender, commonInfo, yarnQueueCrud} from '../../util/amis.tsx'
|
||||
|
||||
const YarnCluster: React.FC = () => {
|
||||
return (
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
CloudOutlined,
|
||||
ClusterOutlined,
|
||||
CompressOutlined,
|
||||
DatabaseOutlined,
|
||||
InfoCircleOutlined,
|
||||
OpenAIOutlined,
|
||||
QuestionOutlined,
|
||||
@@ -14,6 +15,9 @@ import {
|
||||
import {Navigate, type RouteObject} from 'react-router'
|
||||
import Conversation from './pages/ai/Conversation.tsx'
|
||||
import Inspection from './pages/ai/Inspection.tsx'
|
||||
import DataDetail from './pages/ai/knowledge/DataDetail.tsx'
|
||||
import DataImport from './pages/ai/knowledge/DataImport.tsx'
|
||||
import Knowledge from './pages/ai/knowledge/Knowledge.tsx'
|
||||
import App from './pages/App.tsx'
|
||||
import Cloud from './pages/overview/Cloud.tsx'
|
||||
import Overview from './pages/overview/Overview.tsx'
|
||||
@@ -24,7 +28,7 @@ import Tool from './pages/overview/Tool.tsx'
|
||||
import Version from './pages/overview/Version.tsx'
|
||||
import Yarn from './pages/overview/Yarn.tsx'
|
||||
import YarnCluster from './pages/overview/YarnCluster.tsx'
|
||||
import {commonInfo} from './util/amis.ts'
|
||||
import {commonInfo} from './util/amis.tsx'
|
||||
|
||||
export const routes: RouteObject[] = [
|
||||
{
|
||||
@@ -86,6 +90,18 @@ export const routes: RouteObject[] = [
|
||||
path: 'conversation',
|
||||
Component: Conversation,
|
||||
},
|
||||
{
|
||||
path: 'knowledge',
|
||||
Component: Knowledge,
|
||||
},
|
||||
{
|
||||
path: 'knowledge/import/:name',
|
||||
Component: DataImport,
|
||||
},
|
||||
{
|
||||
path: 'knowledge/detail/:name',
|
||||
Component: DataDetail,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -179,6 +195,11 @@ export const menus = {
|
||||
name: '智能巡检',
|
||||
icon: <CheckSquareOutlined/>,
|
||||
},
|
||||
{
|
||||
path: '/ai/knowledge',
|
||||
name: '知识库',
|
||||
icon: <DatabaseOutlined/>,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {attachmentAdpator, makeTranslator, render, type Schema} from 'amis'
|
||||
import {AlertComponent, attachmentAdpator, makeTranslator, render, type Schema, ToastComponent} from 'amis'
|
||||
|
||||
import 'amis/lib/themes/antd.css'
|
||||
import 'amis/lib/helper.css'
|
||||
@@ -58,85 +58,99 @@ const responseAdaptor = () => (response: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const amisRender = (schema: Schema) => {
|
||||
return render(
|
||||
schema,
|
||||
{
|
||||
theme: 'antd',
|
||||
},
|
||||
{
|
||||
fetcher: async (api: any) => {
|
||||
let {url, method, data, responseType, config, headers} = api
|
||||
config = config || {}
|
||||
config.url = url
|
||||
config.withCredentials = true
|
||||
responseType && (config.responseType = responseType)
|
||||
export const amisRender = (schema: Schema, data: Record<any, any> = {}) => {
|
||||
const theme = 'antd'
|
||||
const locale = 'zh-CN'
|
||||
return (
|
||||
<>
|
||||
<ToastComponent
|
||||
theme={theme}
|
||||
key="toast"
|
||||
position={'top-right'}
|
||||
locale={locale}
|
||||
/>
|
||||
<AlertComponent theme={theme} key="alert" locale={locale}/>
|
||||
{render(
|
||||
schema,
|
||||
{
|
||||
data: data,
|
||||
theme: theme,
|
||||
},
|
||||
{
|
||||
fetcher: async (api: any) => {
|
||||
let {url, method, data, responseType, config, headers} = api
|
||||
config = config || {}
|
||||
config.url = url
|
||||
config.withCredentials = true
|
||||
responseType && (config.responseType = responseType)
|
||||
|
||||
if (config.cancelExecutor) {
|
||||
config.cancelToken = new (axios as any).CancelToken(
|
||||
config.cancelExecutor,
|
||||
)
|
||||
}
|
||||
|
||||
config.headers = headers || {}
|
||||
config.method = method
|
||||
config.data = data
|
||||
|
||||
if (method === 'get' && data) {
|
||||
config.params = data
|
||||
} else if (data && data instanceof FormData) {
|
||||
// config.headers['Content-Type'] = 'multipart/form-data';
|
||||
} else if (
|
||||
data &&
|
||||
typeof data !== 'string' &&
|
||||
!(data instanceof Blob) &&
|
||||
!(data instanceof ArrayBuffer)
|
||||
) {
|
||||
data = JSON.stringify(data)
|
||||
config.headers['Content-Type'] = 'application/json'
|
||||
}
|
||||
|
||||
// 支持返回各种报错信息
|
||||
config.validateStatus = function () {
|
||||
return true
|
||||
}
|
||||
|
||||
let response = await axios(config)
|
||||
response = await attachmentAdpator(response, __, api)
|
||||
response = responseAdaptor()(response)
|
||||
|
||||
if (response.status >= 400) {
|
||||
if (response.data) {
|
||||
// 主要用于 raw: 模式下,后端自己校验登录,
|
||||
if (
|
||||
response.status === 401 &&
|
||||
response.data.location &&
|
||||
response.data.location.startsWith('http')
|
||||
) {
|
||||
location.href = response.data.location.replace(
|
||||
'{{redirect}}',
|
||||
encodeURIComponent(location.href),
|
||||
)
|
||||
return new Promise(() => {
|
||||
})
|
||||
} else if (response.data.msg) {
|
||||
throw new Error(response.data.msg)
|
||||
} else {
|
||||
throw new Error(
|
||||
'System.requestError' + JSON.stringify(response.data, null, 2),
|
||||
if (config.cancelExecutor) {
|
||||
config.cancelToken = new (axios as any).CancelToken(
|
||||
config.cancelExecutor,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
`${'System.requestErrorStatus'} ${response.status}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
},
|
||||
isCancel: (value: any) => (axios as any).isCancel(value),
|
||||
},
|
||||
config.headers = headers || {}
|
||||
config.method = method
|
||||
config.data = data
|
||||
|
||||
if (method === 'get' && data) {
|
||||
config.params = data
|
||||
} else if (data && data instanceof FormData) {
|
||||
// config.headers['Content-Type'] = 'multipart/form-data';
|
||||
} else if (
|
||||
data &&
|
||||
typeof data !== 'string' &&
|
||||
!(data instanceof Blob) &&
|
||||
!(data instanceof ArrayBuffer)
|
||||
) {
|
||||
data = JSON.stringify(data)
|
||||
config.headers['Content-Type'] = 'application/json'
|
||||
}
|
||||
|
||||
// 支持返回各种报错信息
|
||||
config.validateStatus = function () {
|
||||
return true
|
||||
}
|
||||
|
||||
let response = await axios(config)
|
||||
response = await attachmentAdpator(response, __, api)
|
||||
response = responseAdaptor()(response)
|
||||
|
||||
if (response.status >= 400) {
|
||||
if (response.data) {
|
||||
// 主要用于 raw: 模式下,后端自己校验登录,
|
||||
if (
|
||||
response.status === 401 &&
|
||||
response.data.location &&
|
||||
response.data.location.startsWith('http')
|
||||
) {
|
||||
location.href = response.data.location.replace(
|
||||
'{{redirect}}',
|
||||
encodeURIComponent(location.href),
|
||||
)
|
||||
return new Promise(() => {
|
||||
})
|
||||
} else if (response.data.msg) {
|
||||
throw new Error(response.data.msg)
|
||||
} else {
|
||||
throw new Error(
|
||||
'System.requestError' + JSON.stringify(response.data, null, 2),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
`${'System.requestErrorStatus'} ${response.status}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
},
|
||||
isCancel: (value: any) => (axios as any).isCancel(value),
|
||||
},
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user