Browse Source
Merge pull request 'TDesign-AI-Chat' (#3) from TDesign-AI-Chat into master
Merge pull request 'TDesign-AI-Chat' (#3) from TDesign-AI-Chat into master
Reviewed-on: #3master
25 changed files with 1472 additions and 162 deletions
-
4frontend/UI-DEV-GUIDE.md
-
1frontend/components.d.ts
-
613frontend/package-lock.json
-
1frontend/package.json
-
2frontend/src/api/chat.ts
-
5frontend/src/api/upload.ts
-
51frontend/src/components/FormDrawer.vue
-
10frontend/src/utils/chatAdapter.ts
-
101frontend/src/views/ChatPanel.vue
-
76frontend/src/views/FaqManager.vue
-
126frontend/src/views/PipelineFlow.vue
-
6pom.xml
-
18src/main/java/com/wok/supportbot/advisor/ContentSafetyAdvisor.java
-
63src/main/java/com/wok/supportbot/app/AssistantApp.java
-
26src/main/java/com/wok/supportbot/app/ChatContext.java
-
69src/main/java/com/wok/supportbot/config/StorageProperties.java
-
38src/main/java/com/wok/supportbot/controller/AiController.java
-
99src/main/java/com/wok/supportbot/controller/AttachmentController.java
-
2src/main/java/com/wok/supportbot/controller/OpenApiController.java
-
143src/main/java/com/wok/supportbot/service/SftpStorageService.java
-
12src/main/resources/application-dev.yml
-
12src/main/resources/application-prod.yml
-
12src/main/resources/application.yml
-
14src/main/resources/static/sdk/test.html
-
98知识库文档管理模块验证报告.md
@ -0,0 +1,51 @@ |
|||||
|
<template> |
||||
|
<!-- |
||||
|
通用新增/编辑抽屉表单壳组件 |
||||
|
封装 t-drawer + 页脚按钮行,表单内容通过 slot 注入 |
||||
|
适用于富文本/宽内容表单(Markdown 编辑器、宽预览、长文本、宽表格编辑) |
||||
|
--> |
||||
|
<t-drawer |
||||
|
:visible="visible" |
||||
|
:header="title" |
||||
|
:size="size" |
||||
|
:close-on-overlay-click="!saving" |
||||
|
@close="handleClose" |
||||
|
> |
||||
|
<slot /> |
||||
|
<template #footer> |
||||
|
<div class="dialog-footer"> |
||||
|
<t-button variant="outline" :disabled="saving" @click="handleClose">取消</t-button> |
||||
|
<slot name="extra-buttons" /> |
||||
|
<t-button theme="primary" :loading="saving" :disabled="disableConfirm" @click="$emit('confirm')">{{ confirmText }}</t-button> |
||||
|
</div> |
||||
|
</template> |
||||
|
</t-drawer> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import type { PropType } from 'vue' |
||||
|
|
||||
|
const props = defineProps({ |
||||
|
visible: { type: Boolean, default: false }, |
||||
|
title: { type: String, default: '' }, |
||||
|
size: { type: String, default: 'large' }, |
||||
|
saving: { type: Boolean, default: false }, |
||||
|
confirmText: { type: String, default: '保存' }, |
||||
|
disableConfirm: { type: Boolean, default: false }, |
||||
|
// 关闭前拦截钩子:返回 false 或 Promise<false> 时不关闭(用于"未保存退出确认") |
||||
|
beforeClose: { type: Function as PropType<() => boolean | Promise<boolean>>, default: null }, |
||||
|
}) |
||||
|
|
||||
|
const emit = defineEmits<{ |
||||
|
'update:visible': [value: boolean] |
||||
|
confirm: [] |
||||
|
}>() |
||||
|
|
||||
|
async function handleClose() { |
||||
|
if (props.beforeClose) { |
||||
|
const ok = await props.beforeClose() |
||||
|
if (!ok) return |
||||
|
} |
||||
|
emit('update:visible', false) |
||||
|
} |
||||
|
</script> |
||||
@ -0,0 +1,69 @@ |
|||||
|
package com.wok.supportbot.config; |
||||
|
|
||||
|
import lombok.Getter; |
||||
|
import lombok.Setter; |
||||
|
import org.springframework.boot.context.properties.ConfigurationProperties; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
import org.springframework.util.unit.DataSize; |
||||
|
|
||||
|
import java.time.Duration; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* 附件存储配置(SFTP 上传 + 上传校验白名单) |
||||
|
* <p> |
||||
|
* 对应 application.yml 中 {@code storage.sftp.*} / {@code storage.upload.*},集中读取、 |
||||
|
* 支持环境变量覆盖,避免散落 {@code @Value} 硬编码。 |
||||
|
*/ |
||||
|
@Getter |
||||
|
@Setter |
||||
|
@Component |
||||
|
@ConfigurationProperties(prefix = "storage") |
||||
|
public class StorageProperties { |
||||
|
|
||||
|
/** SFTP 服务器配置 */ |
||||
|
private Sftp sftp = new Sftp(); |
||||
|
|
||||
|
/** 上传校验配置 */ |
||||
|
private Upload upload = new Upload(); |
||||
|
|
||||
|
/** |
||||
|
* SFTP 服务器配置 |
||||
|
*/ |
||||
|
@Getter |
||||
|
@Setter |
||||
|
public static class Sftp { |
||||
|
/** SFTP 主机 */ |
||||
|
private String host = "localhost"; |
||||
|
/** SFTP 端口 */ |
||||
|
private int port = 22; |
||||
|
/** 用户名 */ |
||||
|
private String username = ""; |
||||
|
/** 密码 */ |
||||
|
private String password = ""; |
||||
|
/** SFTP 根目录 */ |
||||
|
private String basePath = "/uploads"; |
||||
|
/** 对外访问域名前缀(HTTPS 网关),为空时回退 sftp:// 形式 */ |
||||
|
private String domain = ""; |
||||
|
/** 业务目录前缀 */ |
||||
|
private String bizId = "chat-bot"; |
||||
|
/** 连接超时(支持 `10s` / `10000ms` 等 Duration 写法) */ |
||||
|
private Duration connectTimeout = Duration.ofSeconds(10); |
||||
|
/** 通道超时(支持 `10s` / `10000ms` 等 Duration 写法) */ |
||||
|
private Duration channelTimeout = Duration.ofSeconds(15); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 上传校验配置 |
||||
|
*/ |
||||
|
@Getter |
||||
|
@Setter |
||||
|
public static class Upload { |
||||
|
/** 图片白名单扩展名 */ |
||||
|
private List<String> imageExtensions = List.of("jpg", "jpeg", "png", "gif", "webp", "bmp"); |
||||
|
/** 附件白名单扩展名 */ |
||||
|
private List<String> fileExtensions = List.of("pdf", "zip", "doc", "docx", "xls", "xlsx", "txt", "csv"); |
||||
|
/** 文件大小上限,默认 50MB */ |
||||
|
private DataSize maxSize = DataSize.ofMegabytes(50); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,99 @@ |
|||||
|
package com.wok.supportbot.controller; |
||||
|
|
||||
|
import com.wok.supportbot.config.StorageProperties; |
||||
|
import com.wok.supportbot.service.SftpStorageService; |
||||
|
import org.springframework.http.ResponseEntity; |
||||
|
import org.springframework.security.access.prepost.PreAuthorize; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestParam; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import org.springframework.web.multipart.MultipartFile; |
||||
|
|
||||
|
import java.util.LinkedHashMap; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* 附件上传控制器 |
||||
|
* <p> |
||||
|
* 图片/附件上传到 SFTP,返回可公开访问的 URL(供对话、FAQ markdown 内联展示)。 |
||||
|
* 独立于知识库文档上传({@code DocumentController}),只落盘返回链接,不做分块/向量化。 |
||||
|
*/ |
||||
|
@RestController |
||||
|
@RequestMapping("/attachment") |
||||
|
public class AttachmentController { |
||||
|
|
||||
|
private final SftpStorageService sftpStorageService; |
||||
|
private final StorageProperties storageProperties; |
||||
|
|
||||
|
public AttachmentController(SftpStorageService sftpStorageService, StorageProperties storageProperties) { |
||||
|
this.sftpStorageService = sftpStorageService; |
||||
|
this.storageProperties = storageProperties; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 上传图片/附件 |
||||
|
* |
||||
|
* @param file 上传的文件 |
||||
|
* @return 上传结果:data 内含 url / name / type / mimeType / size |
||||
|
*/ |
||||
|
@PostMapping("/upload") |
||||
|
@PreAuthorize("isAuthenticated()") |
||||
|
public ResponseEntity<Map<String, Object>> upload(@RequestParam("file") MultipartFile file) { |
||||
|
try { |
||||
|
validateFile(file); |
||||
|
SftpStorageService.SftpUploadResult result = sftpStorageService.upload(file); |
||||
|
String type = isImage(file.getOriginalFilename()) ? "image" : "file"; |
||||
|
|
||||
|
Map<String, Object> data = new LinkedHashMap<>(); |
||||
|
data.put("url", result.accessUrl()); |
||||
|
data.put("name", file.getOriginalFilename()); |
||||
|
data.put("type", type); |
||||
|
data.put("mimeType", file.getContentType()); |
||||
|
data.put("size", result.size()); |
||||
|
return ResponseEntity.ok(Map.of( |
||||
|
"success", true, |
||||
|
"message", "上传成功", |
||||
|
"data", data)); |
||||
|
} catch (Exception e) { |
||||
|
return ResponseEntity.status(500).body(Map.of( |
||||
|
"success", false, |
||||
|
"message", "上传失败:" + e.getMessage())); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** 校验文件:非空、文件名防路径遍历、大小与类型白名单 */ |
||||
|
private void validateFile(MultipartFile file) { |
||||
|
if (file == null || file.isEmpty()) { |
||||
|
throw new IllegalArgumentException("文件为空"); |
||||
|
} |
||||
|
String filename = file.getOriginalFilename(); |
||||
|
// 防路径遍历 |
||||
|
if (filename != null && (filename.contains("..") || filename.contains("/") || filename.contains("\\"))) { |
||||
|
throw new IllegalArgumentException("文件名非法"); |
||||
|
} |
||||
|
if (file.getSize() > storageProperties.getUpload().getMaxSize().toBytes()) { |
||||
|
throw new IllegalArgumentException("文件大小超过限制"); |
||||
|
} |
||||
|
String ext = getExtension(filename); |
||||
|
List<String> imageExts = storageProperties.getUpload().getImageExtensions(); |
||||
|
List<String> fileExts = storageProperties.getUpload().getFileExtensions(); |
||||
|
if (!imageExts.contains(ext) && !fileExts.contains(ext)) { |
||||
|
throw new IllegalArgumentException("不支持的文件类型: " + ext); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** 是否图片类型 */ |
||||
|
private boolean isImage(String filename) { |
||||
|
return storageProperties.getUpload().getImageExtensions().contains(getExtension(filename)); |
||||
|
} |
||||
|
|
||||
|
/** 获取扩展名(不含点号,小写) */ |
||||
|
private String getExtension(String filename) { |
||||
|
if (filename == null || !filename.contains(".")) { |
||||
|
return null; |
||||
|
} |
||||
|
return filename.substring(filename.lastIndexOf(".") + 1).toLowerCase(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,143 @@ |
|||||
|
package com.wok.supportbot.service; |
||||
|
|
||||
|
import com.jcraft.jsch.ChannelSftp; |
||||
|
import com.jcraft.jsch.JSch; |
||||
|
import com.jcraft.jsch.Session; |
||||
|
import com.jcraft.jsch.SftpException; |
||||
|
import com.wok.supportbot.config.StorageProperties; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.springframework.util.StringUtils; |
||||
|
import org.springframework.web.multipart.MultipartFile; |
||||
|
|
||||
|
import java.io.InputStream; |
||||
|
import java.time.LocalDate; |
||||
|
import java.util.Properties; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
/** |
||||
|
* SFTP 附件存储服务 |
||||
|
* <p> |
||||
|
* 照搬参考项目 SftpStorageStrategy 的核心流程: |
||||
|
* JSch 建连 → 逐级创建目录 → UUID 命名 → {@code channel.put} 上传 → 拼接 accessUrl → 释放连接。 |
||||
|
*/ |
||||
|
@Service |
||||
|
@Slf4j |
||||
|
public class SftpStorageService { |
||||
|
|
||||
|
private final StorageProperties storageProperties; |
||||
|
|
||||
|
public SftpStorageService(StorageProperties storageProperties) { |
||||
|
this.storageProperties = storageProperties; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 上传文件到 SFTP,返回访问链接。 |
||||
|
* |
||||
|
* @param file 上传的文件 |
||||
|
* @return 上传结果(含对外访问 URL) |
||||
|
*/ |
||||
|
public SftpUploadResult upload(MultipartFile file) { |
||||
|
StorageProperties.Sftp sftp = storageProperties.getSftp(); |
||||
|
String originalFilename = file.getOriginalFilename(); |
||||
|
String extension = getExtension(originalFilename); |
||||
|
String fileKey = UUID.randomUUID().toString().replace("-", "") + extension; |
||||
|
|
||||
|
// 目录:{biz-id}/{yyyy}/{MM}/{dd} |
||||
|
LocalDate now = LocalDate.now(); |
||||
|
String directory = String.join("/", |
||||
|
sftp.getBizId(), |
||||
|
String.valueOf(now.getYear()), |
||||
|
String.valueOf(now.getMonthValue()), |
||||
|
String.valueOf(now.getDayOfMonth())); |
||||
|
|
||||
|
Session session = null; |
||||
|
ChannelSftp channel = null; |
||||
|
try (InputStream inputStream = file.getInputStream()) { |
||||
|
JSch jsch = new JSch(); |
||||
|
session = jsch.getSession(sftp.getUsername(), sftp.getHost(), sftp.getPort()); |
||||
|
session.setPassword(sftp.getPassword()); |
||||
|
Properties config = new Properties(); |
||||
|
config.put("StrictHostKeyChecking", "no"); |
||||
|
session.setConfig(config); |
||||
|
session.connect((int) sftp.getConnectTimeout().toMillis()); |
||||
|
|
||||
|
channel = (ChannelSftp) session.openChannel("sftp"); |
||||
|
channel.connect((int) sftp.getChannelTimeout().toMillis()); |
||||
|
|
||||
|
String remotePath = buildRemotePath(sftp.getBasePath(), directory); |
||||
|
createDirectories(channel, remotePath); |
||||
|
String remoteFilePath = remotePath + "/" + fileKey; |
||||
|
channel.put(inputStream, remoteFilePath); |
||||
|
|
||||
|
String accessUrl = buildAccessUrl(sftp, remoteFilePath); |
||||
|
log.info("SFTP 上传成功: {} -> {}", originalFilename, accessUrl); |
||||
|
return new SftpUploadResult(fileKey, accessUrl, remoteFilePath, file.getSize()); |
||||
|
} catch (Exception e) { |
||||
|
log.error("SFTP 上传失败: filename={}, error={}", originalFilename, e.getMessage(), e); |
||||
|
throw new IllegalStateException("附件上传失败:" + e.getMessage(), e); |
||||
|
} finally { |
||||
|
// 必须释放 channel / session,避免连接泄漏 |
||||
|
if (channel != null && channel.isConnected()) { |
||||
|
channel.disconnect(); |
||||
|
} |
||||
|
if (session != null && session.isConnected()) { |
||||
|
session.disconnect(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** 远程路径 = 根目录 + 业务目录 */ |
||||
|
private String buildRemotePath(String basePath, String directory) { |
||||
|
StringBuilder path = new StringBuilder(basePath); |
||||
|
if (StringUtils.hasText(directory)) { |
||||
|
path.append("/").append(directory); |
||||
|
} |
||||
|
return path.toString(); |
||||
|
} |
||||
|
|
||||
|
/** 逐级创建目录:先 cd,抛 SftpException 说明不存在,再 mkdir + cd */ |
||||
|
private void createDirectories(ChannelSftp channel, String path) throws SftpException { |
||||
|
String[] directories = path.split("/"); |
||||
|
StringBuilder currentPath = new StringBuilder(); |
||||
|
for (String dir : directories) { |
||||
|
if (!StringUtils.hasText(dir)) { |
||||
|
continue; |
||||
|
} |
||||
|
currentPath.append("/").append(dir); |
||||
|
try { |
||||
|
channel.cd(currentPath.toString()); |
||||
|
} catch (SftpException e) { |
||||
|
channel.mkdir(currentPath.toString()); |
||||
|
channel.cd(currentPath.toString()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** 访问链接 = 对外域名前缀 + 远程路径;域名缺失时回退 sftp:// 形式 */ |
||||
|
private String buildAccessUrl(StorageProperties.Sftp sftp, String remoteFilePath) { |
||||
|
if (StringUtils.hasText(sftp.getDomain())) { |
||||
|
return sftp.getDomain() + remoteFilePath; |
||||
|
} |
||||
|
return "sftp://" + sftp.getHost() + ":" + sftp.getPort() + remoteFilePath; |
||||
|
} |
||||
|
|
||||
|
/** 获取文件扩展名(含点号,小写) */ |
||||
|
private String getExtension(String filename) { |
||||
|
if (filename == null || !filename.contains(".")) { |
||||
|
return ""; |
||||
|
} |
||||
|
return filename.substring(filename.lastIndexOf(".")).toLowerCase(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* SFTP 上传结果 |
||||
|
* |
||||
|
* @param fileKey 远端文件名(UUID + 扩展名) |
||||
|
* @param accessUrl 对外访问链接 |
||||
|
* @param remotePath 远端完整路径(不含 domain) |
||||
|
* @param size 文件大小(字节) |
||||
|
*/ |
||||
|
public record SftpUploadResult(String fileKey, String accessUrl, String remotePath, long size) { |
||||
|
} |
||||
|
} |
||||
@ -1,98 +0,0 @@ |
|||||
# 知识库文档管理模块验证报告 |
|
||||
|
|
||||
**测试时间**: 2026-07-02 15:46 |
|
||||
**测试环境**: 开发环境 (localhost:9090) |
|
||||
**测试账号**: admin/admin123 |
|
||||
|
|
||||
--- |
|
||||
|
|
||||
## 测试结果 |
|
||||
|
|
||||
| 序号 | 测试用例 | 测试接口 | 预期结果 | 实际结果 | 状态 | |
|
||||
|:---:|---|---|---|---|:---:| |
|
||||
| 1 | 获取文档列表(正常分页) | GET /document/list | 返回分页数据,包含49条文档 | 成功返回49条文档,分页参数正确 | ✅ | |
|
||||
| 2 | 获取文档统计信息 | GET /document/stats | 返回文档统计信息 | 成功返回:总文档49,总向量1218,各类型分布正确 | ✅ | |
|
||||
| 3 | 获取文档详情 | GET /document/{id} | 返回指定文档详情 | 成功返回文档完整信息 | ✅ | |
|
||||
| 4 | 获取文档分块 | GET /document/{id}/chunks | 返回文档的所有分块 | 成功返回分块列表,包含内容和ID | ✅ | |
|
||||
| 5 | 上传文本内容 | POST /upload/string | 创建新文档,状态为PROCESSING | 成功创建文档ID:2072587072527671297,状态PROCESSING | ✅ | |
|
||||
| 6 | 更新文档元信息 | PUT /document/{id} | 更新文档标题等信息 | 成功更新标题从"验证报告"到"已更新" | ✅ | |
|
||||
| 7 | 切换文档启用/禁用状态 | PUT /document/{id}/toggle | 切换enabled字段 | 成功切换,enabled从true变为false,返回"文档已禁用" | ✅ | |
|
||||
| 8 | 重新处理文档 | PUT /document/{id}/reprocess | 提交重新处理任务 | 成功提交,状态变为PROCESSING | ✅ | |
|
||||
| 9 | 删除单个文档 | DELETE /document/{id} | 逻辑删除文档+删除向量 | 成功删除,deletedVectors:1 | ✅ | |
|
||||
| 10 | 批量删除文档 | POST /document/batch/delete | 批量删除指定文档 | 成功删除1个文档,deletedVectors:1 | ✅ | |
|
||||
| 11 | 批量启用/禁用 | POST /document/batch/toggle | 批量切换状态 | 成功启用1个文档,失败0个 | ✅ | |
|
||||
| 12 | 批量移动分类 | POST /document/batch/move | 移动文档到目标分类 | 成功移动1个文档到客服资料分类 | ✅ | |
|
||||
| 13 | 按关键词搜索 | GET /document/list?keyword=盘点 | 返回匹配关键词的文档 | 成功返回1条包含"盘点"的文档 | ✅ | |
|
||||
| 14 | 按状态过滤 | GET /document/list?status=READY | 返回指定状态的文档 | 成功返回49条READY状态文档 | ✅ | |
|
||||
| 15 | 按分类ID过滤 | GET /document/list?categoryId=xxx | 返回指定分类的文档 | 成功返回17条客服资料分类文档 | ✅ | |
|
||||
| 16 | 获取分类树 | GET /category/tree | 返回分类层级结构 | 成功返回3个分类:客服资料、财务资料、古诗词 | ✅ | |
|
||||
| 17 | 获取标签列表 | GET /tag/list | 返回所有标签及使用次数 | 成功返回2个标签:合同(1次)、流程(1次) | ✅ | |
|
||||
| 18 | 获取不存在的文档 | GET /document/99999999999999 | 返回404错误 | 成功返回{"success":false,"message":"文档不存在"} | ✅ | |
|
||||
| 19 | 批量删除空列表 | POST /document/batch/delete | 返回400错误 | 成功返回{"success":false,"message":"请提供要删除的文档ID列表"} | ✅ | |
|
||||
| 20 | 批量切换缺少参数 | POST /document/batch/toggle | 返回400错误 | 成功返回{"success":false,"message":"请指定目标状态"} | ✅ | |
|
||||
| 21 | 更新分块内容 | PUT /document/{id}/chunk/{i} | 更新分块并重新向量化 | ❌ 返回401未登录错误 | ❌ | |
|
||||
| 22 | 删除分块 | DELETE /document/{id}/chunk/{i} | 删除指定分块 | ❌ 返回401未登录错误 | ❌ | |
|
||||
|
|
||||
--- |
|
||||
|
|
||||
## 发现的问题 |
|
||||
|
|
||||
### 问题1:分块编辑接口认证失败 (严重) |
|
||||
- **问题描述**: PUT /document/{id}/chunk/{chunkIndex} 和 DELETE /document/{id}/chunk/{chunkIndex} 接口返回401未认证错误 |
|
||||
- **影响范围**: 无法通过API编辑或删除单个分块 |
|
||||
- **可能原因**: |
|
||||
1. Controller路径映射可能与其他路径冲突 |
|
||||
2. SecurityConfig可能未正确匹配该路径模式 |
|
||||
3. Spring Security的路径匹配规则可能有问题 |
|
||||
- **建议修复**: |
|
||||
1. 检查Controller的@RequestMapping配置 |
|
||||
2. 验证SecurityFilterChain中的路径匹配规则 |
|
||||
3. 考虑使用更明确的路径前缀避免冲突 |
|
||||
|
|
||||
### 问题2:中文内容编码问题 (轻微) |
|
||||
- **问题描述**: 上传文本内容时,返回的content字段显示为乱码(如"����һ�ݶ˵��˲����ĵ�") |
|
||||
- **影响范围**: 不影响功能,但影响日志可读性 |
|
||||
- **可能原因**: curl命令发送请求时的编码问题,非服务端问题 |
|
||||
- **建议**: 前端上传时确保UTF-8编码即可,服务端实际存储正确 |
|
||||
|
|
||||
--- |
|
||||
|
|
||||
## 测试统计 |
|
||||
|
|
||||
- **总测试用例**: 22个 |
|
||||
- **通过**: 20个 ✅ |
|
||||
- **失败**: 2个 ❌ |
|
||||
- **通过率**: 90.9% |
|
||||
|
|
||||
--- |
|
||||
|
|
||||
## 总结 |
|
||||
|
|
||||
### 功能完整性评估 |
|
||||
知识库文档管理模块的核心功能基本完整,包括: |
|
||||
- ✅ 文档CRUD操作(上传、查询、更新、删除) |
|
||||
- ✅ 分页查询和多条件过滤 |
|
||||
- ✅ 批量操作(删除、启用/禁用、移动分类) |
|
||||
- ✅ 文档状态管理(PROCESSING/READY/FAILED) |
|
||||
- ✅ 分类管理(树形结构) |
|
||||
- ✅ 标签管理 |
|
||||
- ✅ 统计面板 |
|
||||
- ✅ 错误处理和参数校验 |
|
||||
|
|
||||
### 主要问题 |
|
||||
1. **分块编辑功能异常**:单个分块的更新和删除接口返回401认证错误,无法使用 |
|
||||
2. 这是一个**严重问题**,因为分块编辑是知识库管理的重要功能 |
|
||||
|
|
||||
### 建议 |
|
||||
1. **优先修复**分块编辑接口的认证问题 |
|
||||
2. 完善接口文档,明确各参数的格式要求 |
|
||||
3. 考虑添加更详细的错误信息,便于问题排查 |
|
||||
|
|
||||
--- |
|
||||
|
|
||||
**结论**: ❌ **不通过** - 核心功能可用,但分块编辑功能存在认证问题,需要修复后才能通过验证。 |
|
||||
|
|
||||
--- |
|
||||
|
|
||||
**验证人**: 自动化测试脚本 |
|
||||
**生成时间**: 2026-07-02 15:46 |
|
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue