diff --git a/service-ai/.idea/GitCommitMessageStorage.xml b/service-ai/.idea/GitCommitMessageStorage.xml new file mode 100644 index 0000000..e4fd56a --- /dev/null +++ b/service-ai/.idea/GitCommitMessageStorage.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/service-ai/bin/build-ai-chat.sh b/service-ai/bin/build-ai-chat.sh new file mode 100755 index 0000000..73ad775 --- /dev/null +++ b/service-ai/bin/build-ai-chat.sh @@ -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 \ No newline at end of file diff --git a/service-ai/service-ai-knowledge/pom.xml b/service-ai/service-ai-knowledge/pom.xml index a1cfa33..65487e6 100644 --- a/service-ai/service-ai-knowledge/pom.xml +++ b/service-ai/service-ai-knowledge/pom.xml @@ -30,6 +30,10 @@ org.springframework.ai spring-ai-qdrant-store-spring-boot-starter + + org.springframework.ai + spring-ai-markdown-document-reader + diff --git a/service-ai/service-ai-knowledge/src/main/java/com/lanyuanxiaoyao/service/ai/knowledge/KnowledgeApplication.java b/service-ai/service-ai-knowledge/src/main/java/com/lanyuanxiaoyao/service/ai/knowledge/KnowledgeApplication.java index 57f9c20..06cda45 100644 --- a/service-ai/service-ai-knowledge/src/main/java/com/lanyuanxiaoyao/service/ai/knowledge/KnowledgeApplication.java +++ b/service-ai/service-ai-knowledge/src/main/java/com/lanyuanxiaoyao/service/ai/knowledge/KnowledgeApplication.java @@ -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) { } } diff --git a/service-ai/service-ai-knowledge/src/main/java/com/lanyuanxiaoyao/service/ai/knowledge/controller/KnowledgeController.java b/service-ai/service-ai-knowledge/src/main/java/com/lanyuanxiaoyao/service/ai/knowledge/controller/KnowledgeController.java index fa6c426..7ece6a9 100644 --- a/service-ai/service-ai-knowledge/src/main/java/com/lanyuanxiaoyao/service/ai/knowledge/controller/KnowledgeController.java +++ b/service-ai/service-ai-knowledge/src/main/java/com/lanyuanxiaoyao/service/ai/knowledge/controller/KnowledgeController.java @@ -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 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 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 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()); } } diff --git a/service-ai/service-ai-knowledge/src/main/java/com/lanyuanxiaoyao/service/ai/knowledge/entity/CollectionVO.java b/service-ai/service-ai-knowledge/src/main/java/com/lanyuanxiaoyao/service/ai/knowledge/entity/CollectionVO.java new file mode 100644 index 0000000..5301b50 --- /dev/null +++ b/service-ai/service-ai-knowledge/src/main/java/com/lanyuanxiaoyao/service/ai/knowledge/entity/CollectionVO.java @@ -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 + '\'' + + '}'; + } +} diff --git a/service-ai/service-ai-knowledge/src/main/java/com/lanyuanxiaoyao/service/ai/knowledge/entity/PointVO.java b/service-ai/service-ai-knowledge/src/main/java/com/lanyuanxiaoyao/service/ai/knowledge/entity/PointVO.java new file mode 100644 index 0000000..a8ff85e --- /dev/null +++ b/service-ai/service-ai-knowledge/src/main/java/com/lanyuanxiaoyao/service/ai/knowledge/entity/PointVO.java @@ -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 + '\'' + + '}'; + } +} diff --git a/service-ai/service-ai-knowledge/src/main/resources/application.yml b/service-ai/service-ai-knowledge/src/main/resources/application.yml index 4026d67..b390e70 100644 --- a/service-ai/service-ai-knowledge/src/main/resources/application.yml +++ b/service-ai/service-ai-knowledge/src/main/resources/application.yml @@ -24,18 +24,17 @@ spring: darkcode: ENC(0jzpQ7T6S+P7bZrENgYsUoLhlqGvw7DA2MN3BRqEOwq7plhtg72vuuiPQNnr3DaYz0CpyTvxInhpx11W3VZ1trD6NINh7O3LN70ZqO5pWXk=) ai: openai: - base-url: http://localhost:3000 - api-key: '*XMySqV%>hR&v>>g*NwCs3tpQ5FVMFEF2VHVTjWSn:Wn].gs/+"v:q_Q*An~zF*g-@j@jtSTv5H/,S-3:R?r9R}.' diff --git a/service-web/client/src/pages/ai/knowledge/DataDetail.tsx b/service-web/client/src/pages/ai/knowledge/DataDetail.tsx new file mode 100644 index 0000000..8f60a39 --- /dev/null +++ b/service-web/client/src/pages/ai/knowledge/DataDetail.tsx @@ -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 ( +
+ {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, + }, + )} +
+ ) +} + +export default DataDetail \ No newline at end of file diff --git a/service-web/client/src/pages/ai/knowledge/DataImport.tsx b/service-web/client/src/pages/ai/knowledge/DataImport.tsx new file mode 100644 index 0000000..91a3307 --- /dev/null +++ b/service-web/client/src/pages/ai/knowledge/DataImport.tsx @@ -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 ( + + {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}', + }, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + ], + })} + + ) +} + +export default DataImport \ No newline at end of file diff --git a/service-web/client/src/pages/ai/knowledge/Knowledge.tsx b/service-web/client/src/pages/ai/knowledge/Knowledge.tsx new file mode 100644 index 0000000..9643f81 --- /dev/null +++ b/service-web/client/src/pages/ai/knowledge/Knowledge.tsx @@ -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 ( +
+ {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: '删除', + }, + ], + }, + ], + }, + ], + }, + )} +
+ ) +} + +export default Knowledge \ No newline at end of file diff --git a/service-web/client/src/pages/overview/Cloud.tsx b/service-web/client/src/pages/overview/Cloud.tsx index d7c6583..2ae64e3 100644 --- a/service-web/client/src/pages/overview/Cloud.tsx +++ b/service-web/client/src/pages/overview/Cloud.tsx @@ -6,7 +6,7 @@ import { serviceLogByAppName, serviceLogByAppNameAndHost, time, -} from '../../util/amis.ts' +} from '../../util/amis.tsx' const cloudCrud = (title: string, path: string) => { return { diff --git a/service-web/client/src/pages/overview/Overview.tsx b/service-web/client/src/pages/overview/Overview.tsx index 1baf313..9180985 100644 --- a/service-web/client/src/pages/overview/Overview.tsx +++ b/service-web/client/src/pages/overview/Overview.tsx @@ -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' diff --git a/service-web/client/src/pages/overview/Queue.tsx b/service-web/client/src/pages/overview/Queue.tsx index 7d0581e..dea1640 100644 --- a/service-web/client/src/pages/overview/Queue.tsx +++ b/service-web/client/src/pages/overview/Queue.tsx @@ -6,7 +6,7 @@ import { paginationCommonOptions, time, yarnQueueCrud, -} from '../../util/amis.ts' +} from '../../util/amis.tsx' const queueCrud = (name: string) => { return { diff --git a/service-web/client/src/pages/overview/Table.tsx b/service-web/client/src/pages/overview/Table.tsx index 01b1903..5d8b299 100644 --- a/service-web/client/src/pages/overview/Table.tsx +++ b/service-web/client/src/pages/overview/Table.tsx @@ -14,7 +14,7 @@ import { tableMetaDialog, tableRunningStateMapping, timeAndFrom, -} from '../../util/amis.ts' +} from '../../util/amis.tsx' function Table() { return ( diff --git a/service-web/client/src/pages/overview/Task.tsx b/service-web/client/src/pages/overview/Task.tsx index 62a61c6..ef5fb64 100644 --- a/service-web/client/src/pages/overview/Task.tsx +++ b/service-web/client/src/pages/overview/Task.tsx @@ -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 ( diff --git a/service-web/client/src/pages/overview/Tool.tsx b/service-web/client/src/pages/overview/Tool.tsx index af86bb1..b33dafd 100644 --- a/service-web/client/src/pages/overview/Tool.tsx +++ b/service-web/client/src/pages/overview/Tool.tsx @@ -10,7 +10,7 @@ import { paginationCommonOptions, readOnlyDialogOptions, timelineColumns, -} from '../../util/amis.ts' +} from '../../util/amis.tsx' const Tool: React.FC = () => { return ( diff --git a/service-web/client/src/pages/overview/Version.tsx b/service-web/client/src/pages/overview/Version.tsx index b04a443..61cc835 100644 --- a/service-web/client/src/pages/overview/Version.tsx +++ b/service-web/client/src/pages/overview/Version.tsx @@ -10,7 +10,7 @@ import { paginationCommonOptions, tableMetaDialog, versionUpdateStateMapping, -} from '../../util/amis.ts' +} from '../../util/amis.tsx' function Version() { return ( diff --git a/service-web/client/src/pages/overview/Yarn.tsx b/service-web/client/src/pages/overview/Yarn.tsx index 3803b3d..9c54aac 100644 --- a/service-web/client/src/pages/overview/Yarn.tsx +++ b/service-web/client/src/pages/overview/Yarn.tsx @@ -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() diff --git a/service-web/client/src/pages/overview/YarnCluster.tsx b/service-web/client/src/pages/overview/YarnCluster.tsx index bdabe67..3c4e959 100644 --- a/service-web/client/src/pages/overview/YarnCluster.tsx +++ b/service-web/client/src/pages/overview/YarnCluster.tsx @@ -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 ( diff --git a/service-web/client/src/route.tsx b/service-web/client/src/route.tsx index b9ad255..288632b 100644 --- a/service-web/client/src/route.tsx +++ b/service-web/client/src/route.tsx @@ -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: , }, + { + path: '/ai/knowledge', + name: '知识库', + icon: , + }, ], }, ], diff --git a/service-web/client/src/util/amis.ts b/service-web/client/src/util/amis.tsx similarity index 96% rename from service-web/client/src/util/amis.ts rename to service-web/client/src/util/amis.tsx index 0584098..a41397b 100644 --- a/service-web/client/src/util/amis.ts +++ b/service-web/client/src/util/amis.tsx @@ -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 = {}) => { + const theme = 'antd' + const locale = 'zh-CN' + return ( + <> + + + {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), + }, + )} + ) }