Browse Source

二期-修复文件上传bug

master
wanghanlin 1 day ago
parent
commit
e1fc0b023a
  1. 15
      CLAUDE.md
  2. 37
      src/main/resources/static/components/DocUpload.js

15
CLAUDE.md

@ -27,6 +27,10 @@ AI 智能客服系统,基于 Spring AI Alibaba + 通义千问 + PGVector,支
**前提条件**: PostgreSQL 12+ 需运行且安装 PGVector 扩展,数据库 `support_bot` 需存在。`knowledge_category` 和 `knowledge_document` 表由 `DatabaseInitConfig` 自动创建,无需手动建表。
**测试说明**: 所有测试均为集成测试(`@SpringBootTest`),需要运行中的 PostgreSQL 和有效的 DashScope API Key。测试类:`SupportBotApplicationTests`(对话/RAG)、`PgVectorVectorStoreConfigTest`(向量存储)、`QueryTransformerTests`(查询重写策略)。无单元测试。
**访问地址**: 前端管理页面 `http://localhost:9090/index.html`,API 文档 `http://localhost:9090/doc.html`(Knife4j)
## 核心架构决策
### 主启动类排除了 PgVectorStoreAutoConfiguration
@ -54,13 +58,18 @@ AI 智能客服系统,基于 Spring AI Alibaba + 通义千问 + PGVector,支
## 关键配置
- `application.yml` 含 API Key,已被 `.gitignore` 排除
- 对话模型: `qwen-turbo`,temperature: 0.7;Embedding: `text-embedding-v2`(1536维)
- MyBatis Plus 逻辑删除字段: `isDelete`,主键策略: `assign_id`(雪花算法)
- **雪花 ID 精度问题**: `KnowledgeDocument.id`、`categoryId` 和 `KnowledgeCategory.id`、`parentId` 已添加 `@JsonSerialize(using = ToStringSerializer.class)`,序列化为字符串避免前端 JS 精度丢失
- **雪花 ID 精度问题**: `KnowledgeDocument.id`、`categoryId` 和 `KnowledgeCategory.id`、`parentId` 已添加 `@JsonSerialize(using = ToStringSerializer.class)`,序列化为字符串避免前端 JS 精度丢失。新增 Long ID 字段时务必加上此注解
- PostgreSQL JSONB 字段使用自定义 `PostgresJsonTypeHandler`(期望 JSON 对象 `'{}'`,非数组 `'[]'`
- 向量维度: 1536,距离类型: COSINE_DISTANCE,索引: HNSW
- **分块配置**: `knowledge.chunk.*` 配置项(`ChunkConfig`),默认 chunkSize=200, overlap=100
- **上传校验**: `ALLOWED_EXTENSIONS` 白名单 + 50MB 大小限制,前后端双重校验
- **分块配置**: `knowledge.chunk.*` 配置项(`ChunkConfig`),默认 chunkSize=200, overlap=100, minChunkSizeChars=10, maxNumChunks=5000, keepSeparator=true
- **上传校验**: `ALLOWED_EXTENSIONS` 白名单 + 50MB 大小限制(`spring.servlet.multipart` 配置),前后端双重校验
- **文档去重**: `KnowledgeDocument.contentHash` 字段(SHA-256),上传时自动计算并查重
- **数据库自动初始化**: `DatabaseInitConfig` 在启动时检查并创建 `knowledge_category`/`knowledge_document` 表,对已存在的 `knowledge_document` 表会自动补加 `content_hash` 列。注意 `knowledge-base.sql` 脚本为早期版本,缺少此列,实际以 `DatabaseInitConfig` 为准
### 依赖版本注意
Spring AI 相关依赖版本混合:`spring-ai-alibaba-starter` 1.0.0-M6.1、`spring-ai-pgvector-store` 1.0.0-M6、`spring-ai-tika-document-reader` 1.0.0(正式版),可能存在 API 不兼容风险。pom.xml 中注释掉了 `spring-ai-starter-vector-store-pgvector` 1.0.0-M7(自动整合版本,未启用)。
## 前端架构

37
src/main/resources/static/components/DocUpload.js

@ -66,13 +66,13 @@ export default {
<!-- 通用文件 -->
<div v-show="activeSubTab === 'file'">
<div class="endpoint-info"><span class="badge badge-post">POST</span><code style="font-size:13px;">/upload/file</code><span style="font-size:12px;color:var(--sub);">Tika </span></div>
<div class="upload-zone" @click="$refs.fileInput.click()" @dragover.prevent="$event.currentTarget.classList.add('drag-over')" @dragleave="$event.currentTarget.classList.remove('drag-over')" @drop.prevent="handleDrop($event, 'file')">
<div class="upload-zone" @click="fileInput && fileInput.click()" @dragover.prevent="$event.currentTarget.classList.add('drag-over')" @dragleave="$event.currentTarget.classList.remove('drag-over')" @drop.prevent="handleDrop($event, 'file')">
<div class="icon">📎</div><p>点击或拖拽上传,支持多文件(PDF / Word / Excel / PPT / TXT </p>
<input type="file" ref="fileInput" style="display:none" multiple accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.csv,.html,.xml,.rtf" @change="handleFileSelect($event, 'file')">
</div>
<div v-html="fileInfo.file" style="margin-bottom:8px;font-size:13px;color:var(--sub);"></div>
<div v-if="validationErrors.file" style="margin-bottom:8px;font-size:13px;color:var(--danger);"> {{ validationErrors.file }}</div>
<button class="btn btn-primary" @click="doUpload('file')" :disabled="!fileData.file || validationErrors.file">🚀 上传并向量化</button>
<button class="btn btn-primary" @click="doUpload('file')" :disabled="getButtonState('file')">🚀 上传并向量化</button>
<div v-html="results.file"></div>
</div>
@ -95,7 +95,7 @@ export default {
</div>
<div v-html="fileInfo.markdown" style="margin-bottom:8px;font-size:13px;color:var(--sub);"></div>
<div v-if="validationErrors.markdown" style="margin-bottom:8px;font-size:13px;color:var(--danger);"> {{ validationErrors.markdown }}</div>
<button class="btn btn-primary" @click="doUpload('markdown')" :disabled="!fileData.markdown || validationErrors.markdown">🚀 上传并向量化</button>
<button class="btn btn-primary" @click="doUpload('markdown')" :disabled="getButtonState('markdown')">🚀 上传并向量化</button>
<div v-html="results.markdown"></div>
</div>
@ -108,7 +108,7 @@ export default {
</div>
<div v-html="fileInfo.jsonBasic" style="margin-bottom:8px;font-size:13px;color:var(--sub);"></div>
<div v-if="validationErrors.jsonBasic" style="margin-bottom:8px;font-size:13px;color:var(--danger);"> {{ validationErrors.jsonBasic }}</div>
<button class="btn btn-primary" @click="doUpload('jsonBasic')" :disabled="!fileData.jsonBasic || validationErrors.jsonBasic">🚀 上传并向量化</button>
<button class="btn btn-primary" @click="doUpload('jsonBasic')" :disabled="getButtonState('jsonBasic')">🚀 上传并向量化</button>
<div v-html="results.jsonBasic"></div>
</div>
@ -122,7 +122,7 @@ export default {
<div v-html="fileInfo.jsonFields" style="margin-bottom:8px;font-size:13px;color:var(--sub);"></div>
<div v-if="validationErrors.jsonFields" style="margin-bottom:8px;font-size:13px;color:var(--danger);"> {{ validationErrors.jsonFields }}</div>
<input class="input" v-model="jsonFieldsStr" placeholder="输入要提取的字段名(逗号分隔),如:title,description,content" style="margin-bottom:12px;">
<button class="btn btn-primary" @click="doUpload('jsonFields')" :disabled="!fileData.jsonFields || validationErrors.jsonFields">🚀 上传并向量化</button>
<button class="btn btn-primary" @click="doUpload('jsonFields')" :disabled="getButtonState('jsonFields')">🚀 上传并向量化</button>
<div v-html="results.jsonFields"></div>
</div>
@ -136,7 +136,7 @@ export default {
<div v-html="fileInfo.jsonPointer" style="margin-bottom:8px;font-size:13px;color:var(--sub);"></div>
<div v-if="validationErrors.jsonPointer" style="margin-bottom:8px;font-size:13px;color:var(--danger);"> {{ validationErrors.jsonPointer }}</div>
<input class="input" v-model="jsonPointerStr" placeholder="输入 JSON Pointer 路径,如:/data/items 或 /products" style="margin-bottom:12px;">
<button class="btn btn-primary" @click="doUpload('jsonPointer')" :disabled="!fileData.jsonPointer || validationErrors.jsonPointer">🚀 上传并向量化</button>
<button class="btn btn-primary" @click="doUpload('jsonPointer')" :disabled="getButtonState('jsonPointer')">🚀 上传并向量化</button>
<div v-html="results.jsonPointer"></div>
</div>
</div>
@ -154,6 +154,13 @@ export default {
const overlapOverride = ref(null)
const uploadProgress = ref(-1)
// 文件 input 的 ref 引用(Composition API 需要声明才能在模板中使用)
const fileInput = ref(null)
const mdInput = ref(null)
const jsonBInput = ref(null)
const jsonFInput = ref(null)
const jsonPInput = ref(null)
const fileData = reactive({
file: null, markdown: null, jsonBasic: null, jsonFields: null, jsonPointer: null
})
@ -170,6 +177,14 @@ export default {
file: '', markdown: '', jsonBasic: '', jsonFields: '', jsonPointer: ''
})
// 添加调试用的计算属性,检查按钮状态
const getButtonState = (type) => {
const hasFile = !!fileData[type]
const hasError = !!validationErrors[type]
console.log(`[BUTTON STATE] type=${type}, hasFile=${hasFile}, hasError=${hasError}, disabled=${!hasFile || hasError}`)
return !hasFile || hasError
}
const subTabs = [
{ key: 'file', icon: '📎', label: '通用文件' },
{ key: 'string', icon: '📝', label: '文本内容' },
@ -202,8 +217,12 @@ export default {
if (!input.files || input.files.length === 0) return
const files = Array.from(input.files)
console.log('[DEBUG] handleFileSelect - type:', type, 'files:', files.length)
// 前端校验
const error = validateFiles(files)
console.log('[DEBUG] validation error:', error)
if (error) {
// 校验不通过:清空数据,保持按钮禁用
validationErrors[type] = error
@ -221,6 +240,10 @@ export default {
? `已选择 <strong>${files.length}</strong> 个文件(共 ${formatBytes(totalSize)}`
: `已选择:<strong>${files[0].name}</strong> (${formatBytes(files[0].size)})`
fileInfo[type] = label
console.log('[DEBUG] fileData[type] set to:', fileData[type])
console.log('[DEBUG] validationErrors[type] set to:', validationErrors[type])
console.log('[DEBUG] Button should be enabled:', !!fileData[type] && !validationErrors[type])
}
function handleDrop(event, type) {
@ -374,7 +397,9 @@ export default {
stringTitle, stringContent, jsonFieldsStr, jsonPointerStr,
showAdvanced, chunkSizeOverride, overlapOverride,
uploadProgress,
fileInput, mdInput, jsonBInput, jsonFInput, jsonPInput,
fileData, fileInfo, results, validationErrors, store,
getButtonState,
handleFileSelect, handleDrop, doUpload, formatBytes
}
}

Loading…
Cancel
Save