Browse Source

增加项目数据库初始化sql

增加前端模型配置校验
设置默认向量维度为1024(豆包模型低向量维度)
dev-mcp
wanghanlin 4 weeks ago
parent
commit
26d6f9823b
  1. 4
      CLAUDE.md
  2. 10
      DEPLOY.md
  3. 4
      README.md
  4. 18
      src/main/java/com/wok/supportbot/config/ChatModelFactory.java
  5. 2
      src/main/java/com/wok/supportbot/config/EmbeddingConfigFixer.java
  6. 4
      src/main/java/com/wok/supportbot/config/EmbeddingModelFactory.java
  7. 4
      src/main/java/com/wok/supportbot/rag/load/PgVectorStoreConfig.java
  8. 32
      src/main/java/com/wok/supportbot/service/AiModelConfigService.java
  9. 2
      src/main/resources/add-comments.sql
  10. 4
      src/main/resources/application.yml
  11. 350
      src/main/resources/init-database.sql
  12. 10
      src/main/resources/static/components/ModelConfigManager.js
  13. 4
      src/main/resources/support-bot.sql

4
CLAUDE.md

@ -62,7 +62,7 @@ AI 智能客服系统,基于 Spring AI Alibaba + 通义千问 + PGVector,支
- MyBatis Plus 逻辑删除字段: `isDelete`,主键策略: `assign_id`(雪花算法) - MyBatis Plus 逻辑删除字段: `isDelete`,主键策略: `assign_id`(雪花算法)
- **雪花 ID 精度问题**: `KnowledgeDocument.id`、`categoryId` 和 `KnowledgeCategory.id`、`parentId` 已添加 `@JsonSerialize(using = ToStringSerializer.class)`,序列化为字符串避免前端 JS 精度丢失。新增 Long ID 字段时务必加上此注解 - **雪花 ID 精度问题**: `KnowledgeDocument.id`、`categoryId` 和 `KnowledgeCategory.id`、`parentId` 已添加 `@JsonSerialize(using = ToStringSerializer.class)`,序列化为字符串避免前端 JS 精度丢失。新增 Long ID 字段时务必加上此注解
- PostgreSQL JSONB 字段使用自定义 `PostgresJsonTypeHandler`(期望 JSON 对象 `'{}'`,非数组 `'[]'` - PostgreSQL JSONB 字段使用自定义 `PostgresJsonTypeHandler`(期望 JSON 对象 `'{}'`,非数组 `'[]'`
- **向量维度**: 由 `knowledge.vector.dimension` 配置(默认 1536)。修改后需执行 `DROP TABLE IF EXISTS vector_store CASCADE` 重建向量表,并重新上传知识库文档。距离类型: COSINE_DISTANCE,索引: HNSW
- **向量维度**: 由 `knowledge.vector.dimension` 配置(默认 1024)。修改后需执行 `DROP TABLE IF EXISTS vector_store CASCADE` 重建向量表,并重新上传知识库文档。距离类型: COSINE_DISTANCE,索引: HNSW
- **分块配置**: `knowledge.chunk.*` 配置项(`ChunkConfig`),默认 chunkSize=200, overlap=100, minChunkSizeChars=10, maxNumChunks=5000, keepSeparator=true - **分块配置**: `knowledge.chunk.*` 配置项(`ChunkConfig`),默认 chunkSize=200, overlap=100, minChunkSizeChars=10, maxNumChunks=5000, keepSeparator=true
- **上传校验**: `ALLOWED_EXTENSIONS` 白名单 + 50MB 大小限制(`spring.servlet.multipart` 配置),前后端双重校验 - **上传校验**: `ALLOWED_EXTENSIONS` 白名单 + 50MB 大小限制(`spring.servlet.multipart` 配置),前后端双重校验
- **文档去重**: `KnowledgeDocument.contentHash` 字段(SHA-256),上传时自动计算并查重 - **文档去重**: `KnowledgeDocument.contentHash` 字段(SHA-256),上传时自动计算并查重
@ -132,4 +132,4 @@ AI 智能客服系统,基于 Spring AI Alibaba + 通义千问 + PGVector,支
- `DocumentService.searchDocuments()`: Spring AI 1.0.1 的 filter 支持有限,分类过滤暂未实现 - `DocumentService.searchDocuments()`: Spring AI 1.0.1 的 filter 支持有限,分类过滤暂未实现
- `CompressionQueryRewriter`: 当前传入空历史列表 - `CompressionQueryRewriter`: 当前传入空历史列表
- MyBatis Plus 3.5.12 的 `mybatis-plus-spring-boot3-starter` 不含 `PaginationInnerInterceptor`,分页通过 SQL `LIMIT/OFFSET` 手动实现 - MyBatis Plus 3.5.12 的 `mybatis-plus-spring-boot3-starter` 不含 `PaginationInnerInterceptor`,分页通过 SQL `LIMIT/OFFSET` 手动实现
- `PgVectorStoreConfig.dimensions(1536)` 硬编码了向量维度,切换非 1536 维的 Embedding 模型时需修改并重建 vector_store 表 → **已修复:维度由 `knowledge.vector.dimension` 配置,启动时自动检测不匹配并告警**
- `PgVectorStoreConfig.dimensions(1024)` 硬编码了向量维度,切换非 1024 维的 Embedding 模型时需修改并重建 vector_store 表 → **已修复:维度由 `knowledge.vector.dimension` 配置,启动时自动检测不匹配并告警**

10
DEPLOY.md

@ -337,8 +337,8 @@ knowledge:
keep-separator: true keep-separator: true
vector: vector:
# 向量维度,需与 Embedding 模型输出维度一致 # 向量维度,需与 Embedding 模型输出维度一致
# 千问 text-embedding-v2: 1536 | 豆包 doubao-embedding-text-240515: 2048
dimension: 1536
# 千问 text-embedding-v2: 1024 | 豆包 doubao-embedding-text-240515: 2048
dimension: 1024
role: role:
strict-isolation: false strict-isolation: false
@ -468,9 +468,9 @@ tail -f /opt/support-bot/logs/startup.out
- **提供商**:dashscope - **提供商**:dashscope
- **API Key**:在 [DashScope 控制台](https://dashscope.console.aliyun.com/) 获取,格式 `sk-xxxx` - **API Key**:在 [DashScope 控制台](https://dashscope.console.aliyun.com/) 获取,格式 `sk-xxxx`
- **Base URL**:留空(DashScope 内置) - **Base URL**:留空(DashScope 内置)
- **EMBEDDING 类型**需在「向量维度」填 `1536`(与 `knowledge.vector.dimension` 一致)
- **EMBEDDING 类型**需在「向量维度」填 `1024`(与 `knowledge.vector.dimension` 一致)
> 配置后**热切换生效,无需重启**。若切换非 1536 维的 Embedding 模型,需修改 `application.yml``knowledge.vector.dimension` 并重建 `vector_store` 表(见运维章节)。
> 配置后**热切换生效,无需重启**。若切换非 1024 维的 Embedding 模型,需修改 `application.yml``knowledge.vector.dimension` 并重建 `vector_store` 表(见运维章节)。
### 9.4 其他提供商(可选) ### 9.4 其他提供商(可选)
@ -607,7 +607,7 @@ echo "0 3 * * * docker exec support-bot-postgres pg_dump -U postgres support_bot
### 12.3 切换 Embedding 模型 / 重建向量表 ### 12.3 切换 Embedding 模型 / 重建向量表
若更换非 1536 维的 Embedding 模型:
若更换非 1024 维的 Embedding 模型:
```bash ```bash
# 1. 修改 application.yml 中 knowledge.vector.dimension # 1. 修改 application.yml 中 knowledge.vector.dimension

4
README.md

@ -32,7 +32,7 @@
| **后端框架** | Spring Boot | 3.4.4 | 主框架,提供依赖注入和自动配置 | | **后端框架** | Spring Boot | 3.4.4 | 主框架,提供依赖注入和自动配置 |
| **AI框架** | Spring AI Alibaba | 1.0.0-M6.1 | AI集成框架,简化大模型调用 | | **AI框架** | Spring AI Alibaba | 1.0.0-M6.1 | AI集成框架,简化大模型调用 |
| **大语言模型** | 阿里云通义千问 | qwen-turbo | 对话生成和文本理解 | | **大语言模型** | 阿里云通义千问 | qwen-turbo | 对话生成和文本理解 |
| **Embedding模型** | 阿里云DashScope | text-embedding-v2 | 文本向量化(1536维) |
| **Embedding模型** | 阿里云DashScope | text-embedding-v2 | 文本向量化(1024维) |
| **数据库** | PostgreSQL + PGVector | 12+ | 关系数据存储 + 向量存储 | | **数据库** | PostgreSQL + PGVector | 12+ | 关系数据存储 + 向量存储 |
| **ORM框架** | MyBatis Plus | 3.5.12 | 数据库操作和对象映射 | | **ORM框架** | MyBatis Plus | 3.5.12 | 数据库操作和对象映射 |
| **API文档** | Knife4j | 4.4.0 | Swagger UI增强版 | | **API文档** | Knife4j | 4.4.0 | Swagger UI增强版 |
@ -65,7 +65,7 @@ CREATE TABLE vector_store (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, -- 向量记录ID id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, -- 向量记录ID
content TEXT NOT NULL, -- 原始文档内容 content TEXT NOT NULL, -- 原始文档内容
metadata JSONB DEFAULT '{}', -- 文档元数据 metadata JSONB DEFAULT '{}', -- 文档元数据
embedding VECTOR(1536) NOT NULL, -- 1536维向量嵌入
embedding VECTOR(1024) NOT NULL, -- 1024维向量嵌入
create_time TIMESTAMP DEFAULT NOW(), -- 创建时间 create_time TIMESTAMP DEFAULT NOW(), -- 创建时间
update_time TIMESTAMP DEFAULT NOW() -- 更新时间 update_time TIMESTAMP DEFAULT NOW() -- 更新时间
); );

18
src/main/java/com/wok/supportbot/config/ChatModelFactory.java

@ -92,8 +92,12 @@ public class ChatModelFactory {
* 创建 ChatModel 实例 * 创建 ChatModel 实例
* - dashscope手动构造 DashScopeApi + DashScopeChatModel DB 配置指定 model/temperature/maxTokens * - dashscope手动构造 DashScopeApi + DashScopeChatModel DB 配置指定 model/temperature/maxTokens
* - 其他提供商通过 OpenAI 兼容 API 创建 * - 其他提供商通过 OpenAI 兼容 API 创建
*
* 会校验模型名称防止 Embedding 模型被误用为 Chat 模型会导致 API 返回 404
*/ */
private ChatModel createChatModel(AiModelConfig config) { private ChatModel createChatModel(AiModelConfig config) {
// 校验Embedding 模型不应作为 Chat 模型使用
validateNotEmbeddingModel(config);
if ("dashscope".equalsIgnoreCase(config.getProvider())) { if ("dashscope".equalsIgnoreCase(config.getProvider())) {
log.info("创建 DashScope ChatModel: model={}, temperature={}, maxTokens={}", log.info("创建 DashScope ChatModel: model={}, temperature={}, maxTokens={}",
config.getModelName(), config.getTemperature(), config.getMaxTokens()); config.getModelName(), config.getTemperature(), config.getMaxTokens());
@ -179,4 +183,18 @@ public class ChatModelFactory {
chatModelCache.clear(); chatModelCache.clear();
log.info("ChatModel 缓存已清除"); log.info("ChatModel 缓存已清除");
} }
/**
* 校验模型名称防止 Embedding 模型被误用为 Chat 模型
* Embedding 模型不支持 /chat/completions 端点调用会导致 API 返回 404
*/
private void validateNotEmbeddingModel(AiModelConfig config) {
String modelName = config.getModelName();
if (modelName != null && modelName.toLowerCase().contains("embedding")) {
throw new IllegalArgumentException(
"模型 [" + modelName + "] 是 Embedding 模型,不支持对话功能。"
+ "请在「AI 大模型配置管理」页面将该模型的应用类型改为 EMBEDDING,"
+ "并为 CHAT 类型选择一个对话模型(如 doubao-pro、doubao-lite 等)");
}
}
} }

2
src/main/java/com/wok/supportbot/config/EmbeddingConfigFixer.java

@ -31,7 +31,7 @@ public class EmbeddingConfigFixer implements ApplicationListener<ApplicationRead
@Value("${spring.ai.dashscope.api-key:}") @Value("${spring.ai.dashscope.api-key:}")
private String dashscopeApiKey; private String dashscopeApiKey;
@Value("${knowledge.vector.dimension:1536}")
@Value("${knowledge.vector.dimension:1024}")
private int fallbackDimension; private int fallbackDimension;
@Override @Override

4
src/main/java/com/wok/supportbot/config/EmbeddingModelFactory.java

@ -170,7 +170,7 @@ public class EmbeddingModelFactory {
} }
/** /**
* extraConfig 中解析向量维度无配置时回退到默认值 1536
* extraConfig 中解析向量维度无配置时回退到默认值 1024
*/ */
private int resolveDimensions(AiModelConfig config) { private int resolveDimensions(AiModelConfig config) {
if (config.getExtraConfig() != null) { if (config.getExtraConfig() != null) {
@ -182,7 +182,7 @@ public class EmbeddingModelFactory {
} }
} }
} }
return 1536;
return 1024;
} }
/** /**

4
src/main/java/com/wok/supportbot/rag/load/PgVectorStoreConfig.java

@ -18,7 +18,7 @@ import static org.springframework.ai.vectorstore.pgvector.PgVectorStore.PgIndexT
/** /**
* 向量数据库配置初始化基于pgsql的向量数据库 Bean * 向量数据库配置初始化基于pgsql的向量数据库 Bean
* 使用 DynamicEmbeddingModel 代理支持运行时切换向量化模型无需重启 * 使用 DynamicEmbeddingModel 代理支持运行时切换向量化模型无需重启
* 向量维度优先级DB ai_model_config.extra_config.dimensions用户前端配置 > application.yml knowledge.vector.dimension默认 1536
* 向量维度优先级DB ai_model_config.extra_config.dimensions用户前端配置 > application.yml knowledge.vector.dimension默认 1024
*/ */
@Configuration @Configuration
public class PgVectorStoreConfig { public class PgVectorStoreConfig {
@ -29,7 +29,7 @@ public class PgVectorStoreConfig {
@Autowired @Autowired
private AiModelConfigService configService; private AiModelConfigService configService;
@Value("${knowledge.vector.dimension:1536}")
@Value("${knowledge.vector.dimension:1024}")
private int defaultVectorDimension; private int defaultVectorDimension;
@Bean @Bean

32
src/main/java/com/wok/supportbot/service/AiModelConfigService.java

@ -143,6 +143,8 @@ public class AiModelConfigService {
*/ */
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public AiModelConfig createConfig(AiModelConfig config) { public AiModelConfig createConfig(AiModelConfig config) {
// 校验Embedding 模型不能用于非 EMBEDDING 类型
validateModelTypeMatch(config);
// 如果新配置标记为活跃先禁用同类型的其他配置 // 如果新配置标记为活跃先禁用同类型的其他配置
if (Boolean.TRUE.equals(config.getIsActive())) { if (Boolean.TRUE.equals(config.getIsActive())) {
deactivateByAppType(config.getAppType()); deactivateByAppType(config.getAppType());
@ -172,6 +174,15 @@ public class AiModelConfigService {
throw new RuntimeException("配置不存在"); throw new RuntimeException("配置不存在");
} }
// 校验Embedding 模型不能用于非 EMBEDDING 类型
// 合并已有配置和更新内容进行校验
AiModelConfig merged = AiModelConfig.builder()
.appType(config.getAppType() != null ? config.getAppType() : existing.getAppType())
.modelName(config.getModelName() != null ? config.getModelName() : existing.getModelName())
.provider(config.getProvider() != null ? config.getProvider() : existing.getProvider())
.build();
validateModelTypeMatch(merged);
// 如果要激活此配置先禁用同类型的其他配置 // 如果要激活此配置先禁用同类型的其他配置
if (Boolean.TRUE.equals(config.getIsActive())) { if (Boolean.TRUE.equals(config.getIsActive())) {
deactivateByAppType(existing.getAppType()); deactivateByAppType(existing.getAppType());
@ -201,6 +212,7 @@ public class AiModelConfigService {
/** /**
* 激活指定配置 app_type 互斥 * 激活指定配置 app_type 互斥
* 校验Embedding 模型不能作为 CHAT / PRODUCT_EXTRACT / RAG_REWRITE 类型激活
* *
* @param id 配置ID * @param id 配置ID
*/ */
@ -211,6 +223,9 @@ public class AiModelConfigService {
throw new RuntimeException("配置不存在"); throw new RuntimeException("配置不存在");
} }
// 校验Embedding 模型不能用于非 EMBEDDING 类型
validateModelTypeMatch(config);
// 先禁用同类型的所有配置 // 先禁用同类型的所有配置
deactivateByAppType(config.getAppType()); deactivateByAppType(config.getAppType());
@ -275,6 +290,23 @@ public class AiModelConfigService {
return apiKey.substring(0, 4) + "****" + apiKey.substring(apiKey.length() - 4); return apiKey.substring(0, 4) + "****" + apiKey.substring(apiKey.length() - 4);
} }
/**
* 校验模型名称与应用类型的匹配关系
* Embedding 模型模型名含 "embedding"不能用于 CHAT / PRODUCT_EXTRACT / RAG_REWRITE 类型
* 否则调用 /chat/completions 端点会返回 404
*/
private void validateModelTypeMatch(AiModelConfig config) {
String modelName = config.getModelName();
String appType = config.getAppType();
if (modelName != null && appType != null
&& modelName.toLowerCase().contains("embedding")
&& !"EMBEDDING".equals(appType)) {
throw new IllegalArgumentException(
"模型 [" + modelName + "] 是 Embedding 模型,不能用于 [" + appType + "] 类型。"
+ "请选择一个对话模型(如 doubao-pro、doubao-lite、qwen-turbo 等)");
}
}
/** /**
* 显式持久化 extraConfig JSONB 字段 * 显式持久化 extraConfig JSONB 字段
* MyBatis Plus insert/updateById 对带 typeHandler JSONB 字段可能不触发写入 * MyBatis Plus insert/updateById 对带 typeHandler JSONB 字段可能不触发写入

2
src/main/resources/add-comments.sql

@ -16,6 +16,6 @@ COMMENT ON TABLE vector_store IS '向量存储表(存储文档的向量表示
COMMENT ON COLUMN vector_store.id IS '主键ID(UUID格式的唯一标识符)'; COMMENT ON COLUMN vector_store.id IS '主键ID(UUID格式的唯一标识符)';
COMMENT ON COLUMN vector_store.content IS '文档内容(原始的文本内容)'; COMMENT ON COLUMN vector_store.content IS '文档内容(原始的文本内容)';
COMMENT ON COLUMN vector_store.metadata IS '元数据(JSON格式,存储文档来源、标题、标签等附加信息)'; COMMENT ON COLUMN vector_store.metadata IS '元数据(JSON格式,存储文档来源、标题、标签等附加信息)';
COMMENT ON COLUMN vector_store.embedding IS '向量嵌入(1536维向量表示,适配OpenAI embedding模型)';
COMMENT ON COLUMN vector_store.embedding IS '向量嵌入(1024维向量表示,适配Embedding模型)';
COMMENT ON COLUMN vector_store.create_time IS '创建时间'; COMMENT ON COLUMN vector_store.create_time IS '创建时间';
COMMENT ON COLUMN vector_store.update_time IS '更新时间'; COMMENT ON COLUMN vector_store.update_time IS '更新时间';

4
src/main/resources/application.yml

@ -62,8 +62,8 @@ knowledge:
keep-separator: true keep-separator: true
vector: vector:
# 向量维度,需与 Embedding 模型输出维度一致 # 向量维度,需与 Embedding 模型输出维度一致
# 千问 text-embedding-v2: 1536 | 豆包 doubao-embedding-text-240515: 2048 | OpenAI text-embedding-3-small: 1536
dimension: 1536
# 千问 text-embedding-v2: 1024 | 豆包 doubao-embedding-text-240515: 2048 | OpenAI text-embedding-3-small: 1536
dimension: 1024
role: role:
# 严格隔离:true=角色未绑定知识库分类时禁止检索任何内容;false=可检索全部知识库 # 严格隔离:true=角色未绑定知识库分类时禁止检索任何内容;false=可检索全部知识库
strict-isolation: false strict-isolation: false

350
src/main/resources/init-database.sql

@ -0,0 +1,350 @@
-- ============================================================
-- AI 智能客服系统 - 数据库初始化脚本
-- 适用环境: PostgreSQL 12+ 且已安装 pgvector 扩展
-- 使用方法: psql -U postgres -f init-database.sql
-- ============================================================
-- 1. 创建数据库(需在 postgres 默认库下执行)
-- 如果已在目标库中,注释掉下面这行
CREATE DATABASE support_bot ENCODING 'UTF8' LC_COLLATE 'en_US.UTF-8' LC_CTYPE 'en_US.UTF-8' TEMPLATE template0;
-- 切换到 support_bot 数据库(psql 命令,非 SQL)
\c support_bot
-- 2. 安装必要扩展
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS vector;
-- ============================================================
-- 表 1: chat_message — 聊天消息表
-- ============================================================
CREATE TABLE IF NOT EXISTS chat_message (
id BIGINT PRIMARY KEY,
conversation_id VARCHAR(64) NOT NULL,
message_type VARCHAR(20) NOT NULL,
content TEXT NOT NULL,
metadata JSONB NOT NULL DEFAULT '{}',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
is_delete BOOLEAN NOT NULL DEFAULT FALSE,
CONSTRAINT chk_message_type CHECK (message_type IN ('USER', 'ASSISTANT', 'SYSTEM'))
);
CREATE INDEX IF NOT EXISTS idx_chat_message_conversation_id ON chat_message (conversation_id);
CREATE INDEX IF NOT EXISTS idx_chat_message_create_time ON chat_message (create_time DESC);
CREATE INDEX IF NOT EXISTS idx_chat_message_type ON chat_message (message_type);
CREATE INDEX IF NOT EXISTS idx_chat_message_not_deleted ON chat_message (conversation_id) WHERE is_delete = FALSE;
COMMENT ON TABLE chat_message IS '聊天消息表';
COMMENT ON COLUMN chat_message.id IS '主键(雪花算法生成)';
COMMENT ON COLUMN chat_message.conversation_id IS '会话ID';
COMMENT ON COLUMN chat_message.message_type IS '消息类型: USER / ASSISTANT / SYSTEM';
COMMENT ON COLUMN chat_message.content IS '消息内容';
COMMENT ON COLUMN chat_message.metadata IS '元数据(JSON)';
COMMENT ON COLUMN chat_message.create_time IS '创建时间';
COMMENT ON COLUMN chat_message.update_time IS '更新时间';
COMMENT ON COLUMN chat_message.is_delete IS '逻辑删除: FALSE=正常 TRUE=已删除';
-- ============================================================
-- 表 2: knowledge_category — 知识库分类表
-- ============================================================
CREATE TABLE IF NOT EXISTS knowledge_category (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
parent_id BIGINT NOT NULL DEFAULT 0,
sort_order INTEGER NOT NULL DEFAULT 0,
document_count INTEGER NOT NULL DEFAULT 0,
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
is_delete BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX IF NOT EXISTS idx_knowledge_category_parent ON knowledge_category (parent_id);
COMMENT ON TABLE knowledge_category IS '知识库分类表';
COMMENT ON COLUMN knowledge_category.id IS '主键';
COMMENT ON COLUMN knowledge_category.name IS '分类名称';
COMMENT ON COLUMN knowledge_category.description IS '分类描述';
COMMENT ON COLUMN knowledge_category.parent_id IS '父分类ID(0 表示顶级分类)';
COMMENT ON COLUMN knowledge_category.sort_order IS '排序权重';
COMMENT ON COLUMN knowledge_category.document_count IS '关联文档数(冗余字段)';
COMMENT ON COLUMN knowledge_category.create_time IS '创建时间';
COMMENT ON COLUMN knowledge_category.is_delete IS '逻辑删除';
-- ============================================================
-- 表 3: knowledge_document — 知识文档表
-- ============================================================
CREATE TABLE IF NOT EXISTS knowledge_document (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(500) NOT NULL,
source_name VARCHAR(500),
file_type VARCHAR(20) NOT NULL,
file_size BIGINT NOT NULL DEFAULT 0,
content TEXT,
category_id BIGINT NOT NULL DEFAULT 0,
tags JSONB NOT NULL DEFAULT '{}',
chunk_count INTEGER NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'PROCESSING',
error_message TEXT,
content_hash VARCHAR(64),
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
is_delete BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX IF NOT EXISTS idx_knowledge_document_category ON knowledge_document (category_id);
CREATE INDEX IF NOT EXISTS idx_knowledge_document_status ON knowledge_document (status);
CREATE INDEX IF NOT EXISTS idx_knowledge_document_create_time ON knowledge_document (create_time DESC);
CREATE INDEX IF NOT EXISTS idx_knowledge_document_content_hash ON knowledge_document (content_hash);
COMMENT ON TABLE knowledge_document IS '知识文档表';
COMMENT ON COLUMN knowledge_document.id IS '主键';
COMMENT ON COLUMN knowledge_document.title IS '文档标题';
COMMENT ON COLUMN knowledge_document.source_name IS '原始文件名';
COMMENT ON COLUMN knowledge_document.file_type IS '文件类型';
COMMENT ON COLUMN knowledge_document.file_size IS '文件大小(字节)';
COMMENT ON COLUMN knowledge_document.content IS '原文内容(截断预览)';
COMMENT ON COLUMN knowledge_document.category_id IS '所属分类ID';
COMMENT ON COLUMN knowledge_document.tags IS '标签(JSON)';
COMMENT ON COLUMN knowledge_document.chunk_count IS '分块数量';
COMMENT ON COLUMN knowledge_document.status IS '状态: PROCESSING / READY / FAILED';
COMMENT ON COLUMN knowledge_document.error_message IS '处理失败时的错误信息';
COMMENT ON COLUMN knowledge_document.content_hash IS '内容SHA-256哈希(用于去重)';
COMMENT ON COLUMN knowledge_document.create_time IS '创建时间';
COMMENT ON COLUMN knowledge_document.update_time IS '更新时间';
COMMENT ON COLUMN knowledge_document.is_delete IS '逻辑删除';
-- ============================================================
-- 表 4: customer_service_role — 客服角色表
-- ============================================================
CREATE TABLE IF NOT EXISTS customer_service_role (
id BIGSERIAL PRIMARY KEY,
role_key VARCHAR(64) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
description TEXT,
prompt TEXT,
sort_order INTEGER NOT NULL DEFAULT 0,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
is_delete BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX IF NOT EXISTS idx_customer_service_role_enabled ON customer_service_role (enabled, sort_order);
COMMENT ON TABLE customer_service_role IS '客服角色表';
COMMENT ON COLUMN customer_service_role.id IS '主键';
COMMENT ON COLUMN customer_service_role.role_key IS '角色标识符(唯一)';
COMMENT ON COLUMN customer_service_role.name IS '角色名称';
COMMENT ON COLUMN customer_service_role.description IS '角色描述';
COMMENT ON COLUMN customer_service_role.prompt IS '系统提示词';
COMMENT ON COLUMN customer_service_role.sort_order IS '排序权重';
COMMENT ON COLUMN customer_service_role.enabled IS '是否启用';
COMMENT ON COLUMN customer_service_role.create_time IS '创建时间';
COMMENT ON COLUMN customer_service_role.update_time IS '更新时间';
COMMENT ON COLUMN customer_service_role.is_delete IS '逻辑删除';
-- 默认角色种子数据
INSERT INTO customer_service_role (role_key, name, description, sort_order, enabled)
VALUES ('general', '客服', '通用客服角色', 0, TRUE)
ON CONFLICT (role_key) DO NOTHING;
INSERT INTO customer_service_role (role_key, name, description, sort_order, enabled)
VALUES ('finance', '财务', '财务相关客服角色', 1, TRUE)
ON CONFLICT (role_key) DO NOTHING;
INSERT INTO customer_service_role (role_key, name, description, sort_order, enabled)
VALUES ('administration', '行政', '行政相关客服角色', 2, TRUE)
ON CONFLICT (role_key) DO NOTHING;
-- ============================================================
-- 表 5: customer_service_role_category — 角色知识库关联表
-- ============================================================
CREATE TABLE IF NOT EXISTS customer_service_role_category (
id BIGSERIAL PRIMARY KEY,
role_id BIGINT NOT NULL,
category_id BIGINT NOT NULL,
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
is_delete BOOLEAN NOT NULL DEFAULT FALSE,
CONSTRAINT uq_role_category UNIQUE (role_id, category_id)
);
CREATE INDEX IF NOT EXISTS idx_customer_service_role_category_role ON customer_service_role_category (role_id);
CREATE INDEX IF NOT EXISTS idx_customer_service_role_category_category ON customer_service_role_category (category_id);
COMMENT ON TABLE customer_service_role_category IS '客服角色知识库关联表';
COMMENT ON COLUMN customer_service_role_category.id IS '主键';
COMMENT ON COLUMN customer_service_role_category.role_id IS '角色ID';
COMMENT ON COLUMN customer_service_role_category.category_id IS '分类ID';
COMMENT ON COLUMN customer_service_role_category.create_time IS '创建时间';
COMMENT ON COLUMN customer_service_role_category.is_delete IS '逻辑删除';
-- ============================================================
-- 表 6: customer_account — 客服账号表
-- ============================================================
CREATE TABLE IF NOT EXISTS customer_account (
id BIGSERIAL PRIMARY KEY,
account_key VARCHAR(64) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
description TEXT,
role_id BIGINT,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
is_delete BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX IF NOT EXISTS idx_customer_account_role ON customer_account (role_id);
CREATE INDEX IF NOT EXISTS idx_customer_account_enabled ON customer_account (enabled, id);
COMMENT ON TABLE customer_account IS '客服账号表';
COMMENT ON COLUMN customer_account.id IS '主键';
COMMENT ON COLUMN customer_account.account_key IS '账号标识符(唯一)';
COMMENT ON COLUMN customer_account.name IS '账号名称';
COMMENT ON COLUMN customer_account.description IS '账号描述';
COMMENT ON COLUMN customer_account.role_id IS '关联角色ID';
COMMENT ON COLUMN customer_account.enabled IS '是否启用';
COMMENT ON COLUMN customer_account.create_time IS '创建时间';
COMMENT ON COLUMN customer_account.update_time IS '更新时间';
COMMENT ON COLUMN customer_account.is_delete IS '逻辑删除';
-- 默认账号种子数据(引用角色表的 ID)
INSERT INTO customer_account (account_key, name, description, role_id, enabled)
SELECT 'service', '客服账号', '通用客服账号', id, TRUE
FROM customer_service_role WHERE role_key = 'general'
ON CONFLICT (account_key) DO NOTHING;
INSERT INTO customer_account (account_key, name, description, role_id, enabled)
SELECT 'finance', '财务账号', '财务客服账号', id, TRUE
FROM customer_service_role WHERE role_key = 'finance'
ON CONFLICT (account_key) DO NOTHING;
INSERT INTO customer_account (account_key, name, description, role_id, enabled)
SELECT 'administration', '行政账号', '行政客服账号', id, TRUE
FROM customer_service_role WHERE role_key = 'administration'
ON CONFLICT (account_key) DO NOTHING;
-- ============================================================
-- 表 7: conversation_session — 会话归属表
-- ============================================================
CREATE TABLE IF NOT EXISTS conversation_session (
conversation_id VARCHAR(64) PRIMARY KEY,
account_id BIGINT,
role_id BIGINT,
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_conversation_session_account ON conversation_session (account_id);
CREATE INDEX IF NOT EXISTS idx_conversation_session_role ON conversation_session (role_id);
COMMENT ON TABLE conversation_session IS '会话归属表';
COMMENT ON COLUMN conversation_session.conversation_id IS '会话ID(主键)';
COMMENT ON COLUMN conversation_session.account_id IS '归属账号ID';
COMMENT ON COLUMN conversation_session.role_id IS '归属角色ID';
COMMENT ON COLUMN conversation_session.create_time IS '创建时间';
COMMENT ON COLUMN conversation_session.update_time IS '更新时间';
-- ============================================================
-- 表 8: ai_model_config — AI 大模型配置表
-- ============================================================
CREATE TABLE IF NOT EXISTS ai_model_config (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
app_type VARCHAR(50) NOT NULL,
provider VARCHAR(50) NOT NULL DEFAULT 'dashscope',
api_key VARCHAR(512) NOT NULL,
model_name VARCHAR(100) NOT NULL,
temperature DOUBLE PRECISION DEFAULT 0.7,
max_tokens INTEGER DEFAULT 2000,
base_url VARCHAR(512),
extra_config JSONB NOT NULL DEFAULT '{}',
is_active BOOLEAN NOT NULL DEFAULT FALSE,
priority INTEGER NOT NULL DEFAULT 0,
description TEXT,
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
is_delete BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX IF NOT EXISTS idx_ai_model_config_app_type ON ai_model_config (app_type);
CREATE INDEX IF NOT EXISTS idx_ai_model_config_is_active ON ai_model_config (is_active);
CREATE INDEX IF NOT EXISTS idx_ai_model_config_provider ON ai_model_config (provider);
COMMENT ON TABLE ai_model_config IS 'AI 大模型配置表';
COMMENT ON COLUMN ai_model_config.id IS '主键';
COMMENT ON COLUMN ai_model_config.name IS '配置名称';
COMMENT ON COLUMN ai_model_config.app_type IS '应用类型: CHAT / PRODUCT_EXTRACT / EMBEDDING / RAG_REWRITE';
COMMENT ON COLUMN ai_model_config.provider IS '模型提供商';
COMMENT ON COLUMN ai_model_config.api_key IS 'API Key';
COMMENT ON COLUMN ai_model_config.model_name IS '模型名称';
COMMENT ON COLUMN ai_model_config.temperature IS '温度参数';
COMMENT ON COLUMN ai_model_config.max_tokens IS '最大Token数';
COMMENT ON COLUMN ai_model_config.base_url IS 'API 基础地址';
COMMENT ON COLUMN ai_model_config.extra_config IS '扩展配置(JSON,如向量维度等)';
COMMENT ON COLUMN ai_model_config.is_active IS '是否活跃(同类型互斥)';
COMMENT ON COLUMN ai_model_config.priority IS '优先级';
COMMENT ON COLUMN ai_model_config.description IS '描述';
COMMENT ON COLUMN ai_model_config.create_time IS '创建时间';
COMMENT ON COLUMN ai_model_config.update_time IS '更新时间';
COMMENT ON COLUMN ai_model_config.is_delete IS '逻辑删除';
-- 默认模型配置种子数据(api_key 占位,部署后在前端修改)
INSERT INTO ai_model_config (name, app_type, provider, api_key, model_name, temperature, max_tokens, is_active, description)
VALUES ('Chat Default', 'CHAT', 'dashscope', 'sk-placeholder', 'qwen-turbo', 0.7, 2000, TRUE, '默认对话模型配置')
ON CONFLICT DO NOTHING;
INSERT INTO ai_model_config (name, app_type, provider, api_key, model_name, temperature, max_tokens, is_active, description)
VALUES ('Product Extract Default', 'PRODUCT_EXTRACT', 'dashscope', 'sk-placeholder', 'qwen-turbo', 0.1, 2000, TRUE, '默认产品信息提取模型配置')
ON CONFLICT DO NOTHING;
INSERT INTO ai_model_config (name, app_type, provider, api_key, model_name, temperature, max_tokens, is_active, extra_config, description)
VALUES ('Embedding Default', 'EMBEDDING', 'dashscope', 'sk-placeholder', 'text-embedding-v2', 0.0, 2000, TRUE, '{"dimensions": 1024}', '默认向量化模型配置')
ON CONFLICT DO NOTHING;
INSERT INTO ai_model_config (name, app_type, provider, api_key, model_name, temperature, max_tokens, is_active, description)
VALUES ('RAG Rewrite Default', 'RAG_REWRITE', 'dashscope', 'sk-placeholder', 'qwen-turbo', 0.3, 2000, TRUE, '默认 RAG 查询重写模型配置')
ON CONFLICT DO NOTHING;
-- ============================================================
-- 表 9: vector_store — 向量存储表
-- 注意: Spring AI PgVectorStore 配置了 initializeSchema(true),
-- 应用启动时会自动建表。此处手动建表作为备份方案。
-- 向量维度默认 1024,如使用不同维度的 Embedding 模型
-- 需修改下面的 VECTOR(1024) 并重建。
-- ============================================================
CREATE TABLE IF NOT EXISTS vector_store (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
content TEXT NOT NULL,
metadata JSONB NOT NULL DEFAULT '{}',
embedding VECTOR(1024) NOT NULL,
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- HNSW 向量索引(余弦距离)
CREATE INDEX IF NOT EXISTS idx_vector_store_embedding ON vector_store
USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);
CREATE INDEX IF NOT EXISTS idx_vector_store_create_time ON vector_store (create_time DESC);
-- JSONB 元数据 GIN 索引
CREATE INDEX IF NOT EXISTS idx_vector_store_metadata ON vector_store USING gin (metadata);
COMMENT ON TABLE vector_store IS '向量存储表(RAG 知识库向量化数据)';
COMMENT ON COLUMN vector_store.id IS '主键(UUID)';
COMMENT ON COLUMN vector_store.content IS '文档内容';
COMMENT ON COLUMN vector_store.metadata IS '元数据(JSON)';
COMMENT ON COLUMN vector_store.embedding IS '向量嵌入(维度 1024)';
COMMENT ON COLUMN vector_store.create_time IS '创建时间';
COMMENT ON COLUMN vector_store.update_time IS '更新时间';
-- ============================================================
-- 完成
-- ============================================================
\echo '=========================================='
\echo '数据库 support_bot 初始化完成!'
\echo '共创建 9 张表及对应索引。'
\echo '默认 api-key 为占位符,请在前端'
\echo '「AI 大模型配置管理」页面修改为真实值。'
\echo '=========================================='

10
src/main/resources/static/components/ModelConfigManager.js

@ -242,10 +242,10 @@ export default {
<!-- 向量维度 EMBEDDING 类型显示 --> <!-- 向量维度 EMBEDDING 类型显示 -->
<div v-if="editModal.form.app_type === 'EMBEDDING'" style="padding:10px 14px;background:#fefce8;border:1px solid #fde68a;border-radius:8px;"> <div v-if="editModal.form.app_type === 'EMBEDDING'" style="padding:10px 14px;background:#fefce8;border:1px solid #fde68a;border-radius:8px;">
<label style="font-size:13px;font-weight:600;display:block;margin-bottom:6px;">📐 向量维度 <span style="color:#dc2626;">*</span></label> <label style="font-size:13px;font-weight:600;display:block;margin-bottom:6px;">📐 向量维度 <span style="color:#dc2626;">*</span></label>
<input type="number" class="input" v-model.number="editModal.form.embeddingDimensions" min="1" max="8192" placeholder="1536" style="max-width:200px;">
<input type="number" class="input" v-model.number="editModal.form.embeddingDimensions" min="1" max="8192" placeholder="1024" style="max-width:200px;">
<div style="font-size:11px;color:#92400e;margin-top:4px;"> <div style="font-size:11px;color:#92400e;margin-top:4px;">
修改维度后需重建向量表<code style="background:#fde68a;padding:1px 4px;border-radius:3px;">DROP TABLE IF EXISTS vector_store CASCADE</code> 修改维度后需重建向量表<code style="background:#fde68a;padding:1px 4px;border-radius:3px;">DROP TABLE IF EXISTS vector_store CASCADE</code>
常用维度千问 text-embedding-v2=1536 | 豆包 doubao-embedding-text=2048 | OpenAI text-embedding-3-small=1536
常用维度千问 text-embedding-v2=1024 | 豆包 doubao-embedding-text=2048 | OpenAI text-embedding-3-small=1536
</div> </div>
</div> </div>
@ -309,7 +309,7 @@ export default {
temperature: 0.7, temperature: 0.7,
max_tokens: 2000, max_tokens: 2000,
base_url: '', base_url: '',
embeddingDimensions: 1536,
embeddingDimensions: 1024,
priority: 0, priority: 0,
is_active: false, is_active: false,
description: '' description: ''
@ -407,7 +407,7 @@ export default {
temperature: config.temperature, temperature: config.temperature,
max_tokens: config.max_tokens, max_tokens: config.max_tokens,
base_url: config.base_url || '', base_url: config.base_url || '',
embeddingDimensions: extraConfig.dimensions || 1536,
embeddingDimensions: extraConfig.dimensions || 1024,
priority: config.priority || 0, priority: config.priority || 0,
is_active: config.is_active || false, is_active: config.is_active || false,
description: config.description || '' description: config.description || ''
@ -444,7 +444,7 @@ export default {
// EMBEDDING 类型:将向量维度写入 extraConfig // EMBEDDING 类型:将向量维度写入 extraConfig
if (form.app_type === 'EMBEDDING') { if (form.app_type === 'EMBEDDING') {
data.extraConfig = { data.extraConfig = {
dimensions: form.embeddingDimensions || 1536
dimensions: form.embeddingDimensions || 1024
} }
} }
return data return data

4
src/main/resources/support-bot.sql

@ -56,7 +56,7 @@ CREATE TABLE vector_store (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
content TEXT NOT NULL, content TEXT NOT NULL,
metadata JSONB NOT NULL DEFAULT '{}', metadata JSONB NOT NULL DEFAULT '{}',
embedding VECTOR(1536) NOT NULL,
embedding VECTOR(1024) NOT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
); );
@ -68,7 +68,7 @@ COMMENT ON TABLE vector_store IS '向量存储表 - 存储文档内容的向量
COMMENT ON COLUMN vector_store.id IS '主键ID - UUID格式的唯一标识符'; COMMENT ON COLUMN vector_store.id IS '主键ID - UUID格式的唯一标识符';
COMMENT ON COLUMN vector_store.content IS '文档内容 - 原始的文本内容'; COMMENT ON COLUMN vector_store.content IS '文档内容 - 原始的文本内容';
COMMENT ON COLUMN vector_store.metadata IS '元数据 - 文档的附加信息(来源、标题、标签等)'; COMMENT ON COLUMN vector_store.metadata IS '元数据 - 文档的附加信息(来源、标题、标签等)';
COMMENT ON COLUMN vector_store.embedding IS '向量嵌入 - 1536维的向量表示(适配OpenAI embedding模型)';
COMMENT ON COLUMN vector_store.embedding IS '向量嵌入 - 1024维的向量表示(适配Embedding模型)';
COMMENT ON COLUMN vector_store.create_time IS '创建时间'; COMMENT ON COLUMN vector_store.create_time IS '创建时间';
COMMENT ON COLUMN vector_store.update_time IS '更新时间'; COMMENT ON COLUMN vector_store.update_time IS '更新时间';

Loading…
Cancel
Save