Browse Source

优化前端操作交互体验。

ai大模型配置错误不影响启动服务。
去除后台系统提示词,直接使用前端角色设定提示词。
dev-mcp
wanghanlin 4 weeks ago
parent
commit
cdaff49533
  1. 5
      CLAUDE.md
  2. 24
      client/README.md
  3. 21
      client/dist/chatbot-sdk.js
  4. 2
      client/dist/chatbot-sdk.js.map
  5. 2
      client/dist/chatbot-sdk.min.js
  6. 2
      client/dist/chatbot-sdk.min.js.map
  7. 4
      client/src/api.ts
  8. 5
      client/src/chat.ts
  9. 2
      client/src/config.ts
  10. 12
      client/src/types.ts
  11. 27
      src/main/java/com/wok/supportbot/app/AssistantApp.java
  12. 46
      src/main/java/com/wok/supportbot/config/ChatModelFactory.java
  13. 35
      src/main/java/com/wok/supportbot/config/EmbeddingModelFactory.java
  14. 28
      src/main/java/com/wok/supportbot/rag/config/QueryExpanderConfig.java
  15. 46
      src/main/java/com/wok/supportbot/rag/config/QueryTransformerConfig.java
  16. 15
      src/main/java/com/wok/supportbot/service/AiModelConfigService.java
  17. 6
      src/main/java/com/wok/supportbot/service/CustomerServiceRoleService.java
  18. 9
      src/main/resources/static/components/ModelConfigManager.js
  19. 21
      src/main/resources/static/sdk/chatbot-sdk.js
  20. 2
      src/main/resources/static/sdk/chatbot-sdk.js.map
  21. 2
      src/main/resources/static/sdk/chatbot-sdk.min.js
  22. 2
      src/main/resources/static/sdk/chatbot-sdk.min.js.map
  23. 9
      src/main/resources/static/sdk/test.html

5
CLAUDE.md

@ -46,13 +46,13 @@ AI 智能客服系统,基于 Spring AI Alibaba + 通义千问 + PGVector,支
### RAG 双模式
1. **QuestionAnswerAdvisor 模式**(生产使用): 预检索优化 + `QuestionAnswerAdvisor`
2. **RetrievalAugmentationAdvisor 模式**(实验性): `doChatWithRagEnhance()` `queryTransformers``multiQueryExpander` 未生效
2. **RetrievalAugmentationAdvisor 模式**(实验性): `doChatWithRagEnhance()` 仅做基础 RAG 检索,无查询增强
### 文档处理管道
`DocumentService.uploadDocument()` 统一流程:文档提取 → `MyTokenTextSplitter` 分块 → `MyKeywordEnricher` AI 关键词提取 → `pgVectorVectorStore.add()` 向量化存储。每个分块的 metadata 中注入 `documentId`、`chunkIndex`、`sourceName`、`title` 以关联 `knowledge_document` 表。
### 预检索查询优化
四种策略在 `rag/preretrieval/` 下,由 `AssistantApp.doChatWithRagStrategy()` 根据 `strategy` 参数动态选择:REWRITE / TRANSLATION / COMPRESSION / MULTI_QUERY。另存在 Bean 配置版本(`QueryTransformerConfig`、`QueryExpanderConfig`),但实际使用自定义 Rewriter 组件
四种策略在 `rag/preretrieval/` 下,由 `AssistantApp.doChatWithRagStrategy()` 根据 `strategy` 参数动态选择:REWRITE / TRANSLATION / COMPRESSION / MULTI_QUERY。查询重写器均为 `@Component`,仅在请求时懒加载 ChatModel,不影响启动
## 关键配置
@ -160,7 +160,6 @@ AI 智能客服系统,基于 Spring AI Alibaba + 通义千问 + PGVector,支
## 已知 TODO
- `AssistantApp.doChatWithRagEnhance()`: `queryTransformers` 未生效
- `DocumentService.updateDocumentMetadata()`: Spring AI 无直接更新 vector_store metadata 的 API,向量元数据同步留后续
- `DocumentService.searchDocuments()`: Spring AI 1.0.1 的 filter 支持有限,分类过滤暂未实现
- `CompressionQueryRewriter`: 当前传入空历史列表

24
client/README.md

@ -34,9 +34,10 @@
userId: 'zhangsan',
roleId: 1,
// 知识库联动(P1)
categoryId: 5,
showCategorySwitch: true,
// RAG 知识库检索
enableRag: true, // 启用 RAG 增强对话(自动走 /chat/rag/sse)
categoryId: 5, // 默认知识库分类
showCategorySwitch: true, // 显示知识库分类下拉
// UI 定制
title: 'HR 智能助手',
@ -77,6 +78,7 @@ SDK 产物位于 `client/dist/` 目录:
| `requestDomain` | `string` | ✅ | — | P0 | 后端 API 域名 |
| `userId` | `string` | ❌ | — | P0 | 宿主用户标识 → 后端 `accountId` |
| `roleId` | `number` | ❌ | — | P0 | 客服角色 ID |
| `enableRag` | `boolean` | ❌ | `false` | P1 | 启用 RAG 知识库检索对话(自动走 `/chat/rag/sse` 接口) |
| `categoryId` | `number` | ❌ | — | P1 | 默认知识库分类 |
| `showCategorySwitch` | `boolean` | ❌ | `false` | P1 | 是否显示知识库下拉切换 |
| `title` | `string` | ❌ | `"AI 智能助手"` | P0 | 弹窗标题 |
@ -133,18 +135,32 @@ AI 回复支持 Markdown 格式渲染,包括:
### 4.3 知识库联动
**方式一:直接启用 RAG(无需分类选择)**
```html
ChatbotSDK.init({
integrateId: 'my-app',
requestDomain: 'https://ai.example.com',
enableRag: true, // 所有对话自动走 RAG 增强接口
});
```
**方式二:配合知识库分类下拉框**
开启 `showCategorySwitch: true` 后,输入区上方出现知识库分类下拉框:
```html
ChatbotSDK.init({
integrateId: 'my-app',
requestDomain: 'https://ai.example.com',
enableRag: true, // 启用 RAG
showCategorySwitch: true, // 显示分类下拉框
categoryId: 5, // 默认选中分类
});
```
- 选择分类后,后续对话自动走 RAG 增强接口(`/ai/assistant_app/chat/rag/sse`)
- `enableRag: true` 时,所有对话自动走 RAG 增强接口(`/ai/assistant_app/chat/rag/sse`)
- 选择分类后,后续对话也走 RAG 增强接口
- 选择「全部分类」则走普通流式接口
- 分类数据从 `/category/tree` 接口动态加载,支持树形缩进显示

21
client/dist/chatbot-sdk.js

@ -186,7 +186,7 @@ var ChatbotSDK = (function () {
* 解析并校验用户传入的配置填充默认值
*/
function parseConfig(raw) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
// 校验必传参数:integrateId(对应后端 roleId)
if (!raw.integrateId || (typeof raw.integrateId !== 'string' && typeof raw.integrateId !== 'number')
|| (typeof raw.integrateId === 'string' && raw.integrateId.trim() === '')) {
@ -202,7 +202,7 @@ var ChatbotSDK = (function () {
try {
new URL(raw.requestDomain);
}
catch (_l) {
catch (_m) {
logger.error(`requestDomain 不是合法的 URL 格式:${raw.requestDomain}。请提供完整的域名,如 https://api.example.com`);
return null;
}
@ -249,10 +249,12 @@ var ChatbotSDK = (function () {
showTeaser: (_f = raw.showTeaser) !== null && _f !== void 0 ? _f : true,
teaserText: (typeof raw.teaserText === 'string' && raw.teaserText.trim()) || '',
streaming: (_g = raw.streaming) !== null && _g !== void 0 ? _g : true,
enableRag: (_h = raw.enableRag) !== null && _h !== void 0 ? _h : false,
rewriteStrategy: raw.rewriteStrategy || 'REWRITE',
locale: raw.locale || 'zh-CN',
debug: (_h = raw.debug) !== null && _h !== void 0 ? _h : true,
sound: (_j = raw.sound) !== null && _j !== void 0 ? _j : false,
notification: (_k = raw.notification) !== null && _k !== void 0 ? _k : false,
debug: (_j = raw.debug) !== null && _j !== void 0 ? _j : true,
sound: (_k = raw.sound) !== null && _k !== void 0 ? _k : false,
notification: (_l = raw.notification) !== null && _l !== void 0 ? _l : false,
onError: typeof raw.onError === 'function' ? raw.onError : undefined,
onReady: typeof raw.onReady === 'function' ? raw.onReady : undefined,
onMessage: typeof raw.onMessage === 'function' ? raw.onMessage : undefined,
@ -506,7 +508,7 @@ var ChatbotSDK = (function () {
const params = new URLSearchParams();
params.set('message', message);
params.set('chatId', currentConfig.chatId);
params.set('rewriteStrategy', 'REWRITE');
params.set('rewriteStrategy', currentConfig.rewriteStrategy || 'REWRITE');
setIfPresent(params, 'roleId', currentConfig.integrateId);
setIfPresent(params, 'accountId', currentConfig.userId);
setIfPresent(params, 'categoryId', categoryId !== null && categoryId !== void 0 ? categoryId : currentConfig.categoryId);
@ -519,7 +521,7 @@ var ChatbotSDK = (function () {
const params = new URLSearchParams();
params.set('message', message);
params.set('chatId', currentConfig.chatId);
params.set('rewriteStrategy', 'REWRITE');
params.set('rewriteStrategy', currentConfig.rewriteStrategy || 'REWRITE');
setIfPresent(params, 'roleId', currentConfig.integrateId);
setIfPresent(params, 'accountId', currentConfig.userId);
setIfPresent(params, 'categoryId', categoryId !== null && categoryId !== void 0 ? categoryId : currentConfig.categoryId);
@ -3470,7 +3472,7 @@ var ChatbotSDK = (function () {
hideLoadingFn$1 = dom.hideLoading;
// 初始化知识库分类
currentCategoryId = cfg.categoryId;
useRag = !!cfg.categoryId || !!cfg.showCategorySwitch;
useRag = !!cfg.enableRag || !!cfg.categoryId || !!cfg.showCategorySwitch;
// 绑定发送事件
bindSendEvents();
// 绑定滚动监听(智能滚动 + 新消息提示)
@ -3782,7 +3784,8 @@ var ChatbotSDK = (function () {
await initChatId();
}
const aiTimestamp = now();
const shouldUseRag = useRag && (currentCategoryId !== undefined || config$1.categoryId !== undefined);
// RAG 启用条件:显式开启 enableRag,或已选择知识库分类
const shouldUseRag = useRag && (config$1.enableRag || currentCategoryId !== undefined || config$1.categoryId !== undefined);
// 显示 loading
if (showLoadingFn$1)
showLoadingFn$1();

2
client/dist/chatbot-sdk.js.map
File diff suppressed because it is too large
View File

2
client/dist/chatbot-sdk.min.js
File diff suppressed because it is too large
View File

2
client/dist/chatbot-sdk.min.js.map
File diff suppressed because it is too large
View File

4
client/src/api.ts

@ -94,7 +94,7 @@ function buildChatRAGSSEUrl(message: string, categoryId?: number): string {
const params = new URLSearchParams();
params.set('message', message);
params.set('chatId', currentConfig!.chatId);
params.set('rewriteStrategy', 'REWRITE');
params.set('rewriteStrategy', currentConfig!.rewriteStrategy || 'REWRITE');
setIfPresent(params, 'roleId', currentConfig!.integrateId);
setIfPresent(params, 'accountId', currentConfig!.userId);
@ -110,7 +110,7 @@ function buildRagSourcesUrl(message: string, categoryId?: number): string {
const params = new URLSearchParams();
params.set('message', message);
params.set('chatId', currentConfig!.chatId);
params.set('rewriteStrategy', 'REWRITE');
params.set('rewriteStrategy', currentConfig!.rewriteStrategy || 'REWRITE');
setIfPresent(params, 'roleId', currentConfig!.integrateId);
setIfPresent(params, 'accountId', currentConfig!.userId);

5
client/src/chat.ts

@ -111,7 +111,7 @@ export function initChat(
// 初始化知识库分类
currentCategoryId = cfg.categoryId;
useRag = !!cfg.categoryId || !!cfg.showCategorySwitch;
useRag = !!cfg.enableRag || !!cfg.categoryId || !!cfg.showCategorySwitch;
// 绑定发送事件
bindSendEvents();
@ -442,7 +442,8 @@ async function produceAIReply(userText: string): Promise<void> {
}
const aiTimestamp = now();
const shouldUseRag = useRag && (currentCategoryId !== undefined || config.categoryId !== undefined);
// RAG 启用条件:显式开启 enableRag,或已选择知识库分类
const shouldUseRag = useRag && (config.enableRag || currentCategoryId !== undefined || config.categoryId !== undefined);
// 显示 loading
if (showLoadingFn) showLoadingFn();

2
client/src/config.ts

@ -179,6 +179,8 @@ export function parseConfig(raw: SDKConfig): ResolvedConfig | null {
showTeaser: raw.showTeaser ?? true,
teaserText: (typeof raw.teaserText === 'string' && raw.teaserText.trim()) || '',
streaming: raw.streaming ?? true,
enableRag: raw.enableRag ?? false,
rewriteStrategy: raw.rewriteStrategy || 'REWRITE',
locale: raw.locale || 'zh-CN',
debug: raw.debug ?? true,
sound: raw.sound ?? false,

12
client/src/types.ts

@ -56,6 +56,14 @@ export interface SDKConfig {
// === 行为配置 ===
/** 是否启用流式输出,默认 true */
streaming?: boolean;
/**
* RAG false
* 使 /chat/rag/sse
*
*/
enableRag?: boolean;
/** RAG 查询重写策略,默认 "REWRITE"(仅在 enableRag=true 时生效) */
rewriteStrategy?: 'NONE' | 'REWRITE' | 'TRANSLATION' | 'COMPRESSION' | 'MULTI_QUERY';
/** 界面语言,默认 "zh-CN" */
locale?: string;
/** 是否输出调试日志,默认 true */
@ -116,6 +124,10 @@ export interface ResolvedConfig {
teaserText: string;
/** 流式输出 */
streaming: boolean;
/** 是否启用 RAG 知识库检索 */
enableRag: boolean;
/** RAG 查询重写策略 */
rewriteStrategy: string;
/** 界面语言 */
locale: string;
/** 调试日志 */

27
src/main/java/com/wok/supportbot/app/AssistantApp.java

@ -22,14 +22,11 @@ import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.document.Document;
import org.springframework.ai.rag.advisor.RetrievalAugmentationAdvisor;
import org.springframework.ai.rag.generation.augmentation.ContextualQueryAugmenter;
import org.springframework.ai.rag.preretrieval.query.expansion.MultiQueryExpander;
import org.springframework.ai.rag.preretrieval.query.transformation.QueryTransformer;
import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.filter.Filter;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
@ -71,7 +68,7 @@ public class AssistantApp {
private final ConcurrentHashMap<String, ChatClient> chatClientCache = new ConcurrentHashMap<>();
private static final String SYSTEM_PROMPT = "\u4f60\u662f\u83dc\u5927\u738b\u5185\u90e8\u77e5\u8bc6\u52a9\u624b\uff0c\u4e3b\u8981\u670d\u52a1\u5bf9\u8c61\u662f\u5ba2\u670d\u3001\u8d22\u52a1\u3001\u884c\u653f\u7b49\u5185\u90e8\u4eba\u5458\uff0c\u4e0d\u662f\u76f4\u63a5\u9762\u5411\u7ec8\u7aef\u5ba2\u6237\u7684\u673a\u5668\u4eba\u3002\n\n\u9ed8\u8ba4\u4e1a\u52a1\u4e0a\u4e0b\u6587\uff1a\n- \u7528\u6237\u672a\u7279\u522b\u8bf4\u660e\u65f6\uff0c\u5e73\u53f0\u3001APP\u3001\u540e\u53f0\u3001\u8ba2\u5355\u3001\u5ba2\u6237\u3001\u4f9b\u5e94\u5546\u5747\u9ed8\u8ba4\u6307\u83dc\u5927\u738b\u76f8\u5173\u7cfb\u7edf\u548c\u4e1a\u52a1\u3002\n- \u4e0d\u8981\u53cd\u95ee\u201c\u54ea\u4e2a\u5e73\u53f0/\u7cfb\u7edf\u201d\uff0c\u9664\u975e\u7528\u6237\u660e\u786e\u63d0\u5230\u5916\u90e8\u5e73\u53f0\u3001\u591a\u4e1a\u52a1\u7ebf\u51b2\u7a81\uff0c\u6216\u95ee\u9898\u672c\u8eab\u5fc5\u987b\u533a\u5206\u7cfb\u7edf\u3002\n\n\u56de\u7b54\u65b9\u5f0f\uff1a\n- \u5185\u90e8\u4eba\u5458\u8be2\u95ee\u5ba2\u6237\u54a8\u8be2\u7c7b\u95ee\u9898\u65f6\uff0c\u5148\u7ed9\u201c\u53ef\u8f6c\u8ff0\u7ed9\u5ba2\u6237\u7684\u7b54\u590d\u53e3\u5f84\u201d\uff0c\u518d\u7ed9\u5185\u90e8\u5904\u7406\u8981\u70b9\u3002\n- \u5bf9\u6d41\u7a0b\u7c7b\u95ee\u9898\uff0c\u4f18\u5148\u76f4\u63a5\u7ed9\u5165\u53e3\u3001\u6b65\u9aa4\u3001\u6761\u4ef6\u548c\u5f02\u5e38\u5904\u7406\uff0c\u4e0d\u8981\u4e00\u5f00\u59cb\u53ea\u8ffd\u95ee\u4fe1\u606f\u3002\n- \u53ea\u6709\u5728\u7f3a\u5c11\u8ba2\u5355\u53f7\u3001\u8ba2\u5355\u72b6\u6001\u3001\u53d1\u7968\u7c7b\u578b\u3001\u8d26\u53f7\u6743\u9650\u7b49\u4f1a\u5f71\u54cd\u529e\u7406\u7ed3\u679c\u7684\u5173\u952e\u4fe1\u606f\u65f6\uff0c\u624d\u8865\u5145\u9700\u8981\u786e\u8ba4\u7684\u4e8b\u9879\u3002\n- \u6d89\u53ca\u8d22\u52a1\u3001\u884c\u653f\u5236\u5ea6\u6216\u654f\u611f\u5185\u90e8\u4fe1\u606f\u65f6\uff0c\u53ea\u8f93\u51fa\u5f53\u524d\u89d2\u8272\u548c\u77e5\u8bc6\u5e93\u5141\u8bb8\u4f7f\u7528\u7684\u5185\u5bb9\uff1b\u8d44\u6599\u4e0d\u8db3\u65f6\u5982\u5b9e\u8bf4\u660e\uff0c\u4e0d\u8981\u7f16\u9020\u3002\n\n\u4fdd\u6301\u4e13\u4e1a\u3001\u7b80\u6d01\u3001\u53ef\u6267\u884c\u3002";
private static final String SYSTEM_PROMPT = "";//""\u4f60\u662f\u83dc\u5927\u738b\u5185\u90e8\u77e5\u8bc6\u52a9\u624b\uff0c\u4e3b\u8981\u670d\u52a1\u5bf9\u8c61\u662f\u5ba2\u670d\u3001\u8d22\u52a1\u3001\u884c\u653f\u7b49\u5185\u90e8\u4eba\u5458\uff0c\u4e0d\u662f\u76f4\u63a5\u9762\u5411\u7ec8\u7aef\u5ba2\u6237\u7684\u673a\u5668\u4eba\u3002\n\n\u9ed8\u8ba4\u4e1a\u52a1\u4e0a\u4e0b\u6587\uff1a\n- \u7528\u6237\u672a\u7279\u522b\u8bf4\u660e\u65f6\uff0c\u5e73\u53f0\u3001APP\u3001\u540e\u53f0\u3001\u8ba2\u5355\u3001\u5ba2\u6237\u3001\u4f9b\u5e94\u5546\u5747\u9ed8\u8ba4\u6307\u83dc\u5927\u738b\u76f8\u5173\u7cfb\u7edf\u548c\u4e1a\u52a1\u3002\n- \u4e0d\u8981\u53cd\u95ee\u201c\u54ea\u4e2a\u5e73\u53f0/\u7cfb\u7edf\u201d\uff0c\u9664\u975e\u7528\u6237\u660e\u786e\u63d0\u5230\u5916\u90e8\u5e73\u53f0\u3001\u591a\u4e1a\u52a1\u7ebf\u51b2\u7a81\uff0c\u6216\u95ee\u9898\u672c\u8eab\u5fc5\u987b\u533a\u5206\u7cfb\u7edf\u3002\n\n\u56de\u7b54\u65b9\u5f0f\uff1a\n- \u5185\u90e8\u4eba\u5458\u8be2\u95ee\u5ba2\u6237\u54a8\u8be2\u7c7b\u95ee\u9898\u65f6\uff0c\u5148\u7ed9\u201c\u53ef\u8f6c\u8ff0\u7ed9\u5ba2\u6237\u7684\u7b54\u590d\u53e3\u5f84\u201d\uff0c\u518d\u7ed9\u5185\u90e8\u5904\u7406\u8981\u70b9\u3002\n- \u5bf9\u6d41\u7a0b\u7c7b\u95ee\u9898\uff0c\u4f18\u5148\u76f4\u63a5\u7ed9\u5165\u53e3\u3001\u6b65\u9aa4\u3001\u6761\u4ef6\u548c\u5f02\u5e38\u5904\u7406\uff0c\u4e0d\u8981\u4e00\u5f00\u59cb\u53ea\u8ffd\u95ee\u4fe1\u606f\u3002\n- \u53ea\u6709\u5728\u7f3a\u5c11\u8ba2\u5355\u53f7\u3001\u8ba2\u5355\u72b6\u6001\u3001\u53d1\u7968\u7c7b\u578b\u3001\u8d26\u53f7\u6743\u9650\u7b49\u4f1a\u5f71\u54cd\u529e\u7406\u7ed3\u679c\u7684\u5173\u952e\u4fe1\u606f\u65f6\uff0c\u624d\u8865\u5145\u9700\u8981\u786e\u8ba4\u7684\u4e8b\u9879\u3002\n- \u6d89\u53ca\u8d22\u52a1\u3001\u884c\u653f\u5236\u5ea6\u6216\u654f\u611f\u5185\u90e8\u4fe1\u606f\u65f6\uff0c\u53ea\u8f93\u51fa\u5f53\u524d\u89d2\u8272\u548c\u77e5\u8bc6\u5e93\u5141\u8bb8\u4f7f\u7528\u7684\u5185\u5bb9\uff1b\u8d44\u6599\u4e0d\u8db3\u65f6\u5982\u5b9e\u8bf4\u660e\uff0c\u4e0d\u8981\u7f16\u9020\u3002\n\n\u4fdd\u6301\u4e13\u4e1a\u3001\u7b80\u6d01\u3001\u53ef\u6267\u884c\u3002";
/**
* 初始化 ChatClient
@ -86,8 +83,12 @@ public class AssistantApp {
private ChatClient getChatClient(String appType) {
return chatClientCache.computeIfAbsent(appType, type -> {
ChatModel chatModel = chatModelFactory.getChatModel(type);
return ChatClient.builder(chatModel)
.defaultSystem(SYSTEM_PROMPT)
var builder = ChatClient.builder(chatModel);
// 仅在系统提示词非空时设置避免 Spring AI Assert.hasText 校验抛异常
if (StringUtils.hasText(SYSTEM_PROMPT)) {
builder.defaultSystem(SYSTEM_PROMPT);
}
return builder
.defaultAdvisors(
contentSafetyAdvisor,
MessageChatMemoryAdvisor.builder(chatMemory).build(),
@ -284,6 +285,7 @@ public class AssistantApp {
if (strategy == null || strategy.isEmpty()) {
return message;
}
try {
switch (strategy.toUpperCase()) {
case "REWRITE":
return rewriteQueryRewriter.doQueryRewrite(message);
@ -298,6 +300,11 @@ public class AssistantApp {
default:
return message;
}
} catch (Exception e) {
// 查询重写失败时降级为原始查询避免 RAG_REWRITE 配置异常 InvalidApiKey导致整次对话不可用
log.warn("查询重写失败 [strategy={}, chatId={}],降级使用原始查询: {}", strategy, chatId, e.getMessage());
return message;
}
}
/**
@ -531,11 +538,6 @@ public class AssistantApp {
.toList();
}
@Autowired
private List<QueryTransformer> queryTransformers;
@Autowired
private MultiQueryExpander multiQueryExpander;
/**
* RAG 知识库进行对话(另外一种使用方式)
*
@ -545,9 +547,6 @@ public class AssistantApp {
*/
public String doChatWithRagEnhance(String message, String chatId) {
Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
// todo 不生效
//.queryTransformers(queryTransformers)
//.queryExpander(multiQueryExpander)
.documentRetriever(VectorStoreDocumentRetriever.builder()
.vectorStore(pgVectorVectorStore)
.similarityThreshold(0.5)

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

@ -36,7 +36,7 @@ public class ChatModelFactory {
private ToolCallingManager toolCallingManager;
/**
* ChatModel 缓存key = "provider:apiKey:modelName"
* ChatModel 缓存key = "provider:apiKey:modelName:baseUrl"
*/
private final ConcurrentHashMap<String, ChatModel> chatModelCache = new ConcurrentHashMap<>();
@ -81,30 +81,48 @@ public class ChatModelFactory {
/**
* 获取或创建 ChatModel带缓存
* 缓存 key = provider:apiKey:modelName配置不变则复用实例
* 缓存 key = provider:apiKey:modelName:baseUrl配置不变则复用实例
*/
private ChatModel getOrCreateChatModel(AiModelConfig config) {
String cacheKey = config.getProvider() + ":" + config.getApiKey() + ":" + config.getModelName();
return chatModelCache.computeIfAbsent(cacheKey, k -> createChatModel(config));
String baseUrl = resolveBaseUrl(config);
String cacheKey = config.getProvider() + ":" + config.getApiKey() + ":" + config.getModelName() + ":" + baseUrl;
return chatModelCache.computeIfAbsent(cacheKey, k -> createChatModel(config, baseUrl));
}
/**
* 创建 ChatModel 实例
* - dashscope手动构造 DashScopeApi + DashScopeChatModel DB 配置指定 model/temperature/maxTokens
* - dashscope手动构造 DashScopeApi + DashScopeChatModel DB 配置指定 model/temperature/maxTokens/baseUrl
* - 其他提供商通过 OpenAI 兼容 API 创建
*
* 会校验模型名称防止 Embedding 模型被误用为 Chat 模型会导致 API 返回 404
*
* @param config 模型配置
* @param baseUrl 已解析的 API 基础地址来自 resolveBaseUrl
*/
private ChatModel createChatModel(AiModelConfig config) {
private ChatModel createChatModel(AiModelConfig config, String baseUrl) {
// 校验Embedding 模型不应作为 Chat 模型使用
validateNotEmbeddingModel(config);
// 诊断日志记录 API Key 长度和前缀便于排查 InvalidApiKey 错误来源
String apiKey = config.getApiKey();
int keyLen = apiKey != null ? apiKey.length() : 0;
String keyHint = keyLen > 8
? apiKey.substring(0, 4) + "****" + apiKey.substring(keyLen - 4)
: (keyLen == 0 ? "(空)" : "(过短)");
log.info("创建 ChatModel: appType={}, provider={}, model={}, apiKey=[len={}, hint={}]",
config.getAppType(), config.getProvider(), config.getModelName(), keyLen, keyHint);
if ("dashscope".equalsIgnoreCase(config.getProvider())) {
log.info("创建 DashScope ChatModel: model={}, temperature={}, maxTokens={}",
config.getModelName(), config.getTemperature(), config.getMaxTokens());
log.info("创建 DashScope ChatModel: temperature={}, maxTokens={}, baseUrl={}",
config.getTemperature(), config.getMaxTokens(), baseUrl);
DashScopeApi api = DashScopeApi.builder()
.apiKey(config.getApiKey())
.build();
DashScopeApi.Builder apiBuilder = DashScopeApi.builder()
.apiKey(config.getApiKey());
// 团队版/私有化部署使用自定义 baseUrl
if (baseUrl != null && !baseUrl.isBlank()) {
apiBuilder.baseUrl(baseUrl);
}
DashScopeApi api = apiBuilder.build();
DashScopeChatOptions.DashscopeChatOptionsBuilder optionsBuilder = DashScopeChatOptions.builder()
.withModel(config.getModelName());
@ -123,7 +141,6 @@ public class ChatModelFactory {
}
// OpenAI 兼容提供商
String baseUrl = resolveBaseUrl(config);
String completionsPath = resolveCompletionsPath(config);
log.info("创建 OpenAI 兼容 ChatModel: provider={}, baseUrl={}, completionsPath={}, model={}",
config.getProvider(), baseUrl, completionsPath, config.getModelName());
@ -154,11 +171,16 @@ public class ChatModelFactory {
/**
* 解析 API 基础地址优先使用 DB 配置的 baseUrl否则使用提供商默认值
* DashScope 使用自己的 SDK 内置默认地址不需要在此配置
*/
private String resolveBaseUrl(AiModelConfig config) {
if (config.getBaseUrl() != null && !config.getBaseUrl().isBlank()) {
return config.getBaseUrl();
}
// DashScope 使用 SDK 内置默认地址无需指定
if ("dashscope".equalsIgnoreCase(config.getProvider())) {
return null;
}
String defaultUrl = DEFAULT_BASE_URLS.get(config.getProvider());
if (defaultUrl != null) {
return defaultUrl;

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

@ -33,7 +33,7 @@ public class EmbeddingModelFactory {
private AiModelConfigService configService;
/**
* EmbeddingModel 缓存key = "provider:apiKey:modelName"
* EmbeddingModel 缓存key = "provider:apiKey:modelName:baseUrl"
*/
private final ConcurrentHashMap<String, EmbeddingModel> cache = new ConcurrentHashMap<>();
@ -82,28 +82,36 @@ public class EmbeddingModelFactory {
/**
* 获取或创建 EmbeddingModel带缓存
* 缓存 key = provider:apiKey:modelName配置不变则复用实例
* 缓存 key = provider:apiKey:modelName:baseUrl配置不变则复用实例
*/
private EmbeddingModel getOrCreateEmbeddingModel(AiModelConfig config) {
String cacheKey = config.getProvider() + ":" + config.getApiKey() + ":" + config.getModelName();
return cache.computeIfAbsent(cacheKey, k -> createEmbeddingModel(config));
String baseUrl = resolveBaseUrl(config);
String cacheKey = config.getProvider() + ":" + config.getApiKey() + ":" + config.getModelName() + ":" + baseUrl;
return cache.computeIfAbsent(cacheKey, k -> createEmbeddingModel(config, baseUrl));
}
/**
* 创建 EmbeddingModel 实例
* - dashscope手动构造 DashScopeApi + DashScopeEmbeddingModel
* - dashscope手动构造 DashScopeApi + DashScopeEmbeddingModel支持自定义 baseUrl
* - volcengine 豆包多模态模型手写 VolcengineMultimodalEmbeddingModel 直调 /embeddings/multimodal
* - 其他提供商通过 OpenAI 兼容 API 创建
*
* @param config 模型配置
* @param baseUrl 已解析的 API 基础地址来自 resolveBaseUrl
*/
private EmbeddingModel createEmbeddingModel(AiModelConfig config) {
private EmbeddingModel createEmbeddingModel(AiModelConfig config, String baseUrl) {
RetryTemplate retryTemplate = createRetryTemplate();
// DashScope通义千问
if ("dashscope".equalsIgnoreCase(config.getProvider())) {
log.info("创建 DashScope EmbeddingModel: model={}", config.getModelName());
DashScopeApi api = DashScopeApi.builder()
.apiKey(config.getApiKey())
.build();
log.info("创建 DashScope EmbeddingModel: model={}, baseUrl={}", config.getModelName(), baseUrl);
DashScopeApi.Builder apiBuilder = DashScopeApi.builder()
.apiKey(config.getApiKey());
// 团队版/私有化部署使用自定义 baseUrl
if (baseUrl != null && !baseUrl.isBlank()) {
apiBuilder.baseUrl(baseUrl);
}
DashScopeApi api = apiBuilder.build();
DashScopeEmbeddingOptions options = DashScopeEmbeddingOptions.builder()
.withModel(config.getModelName())
.build();
@ -114,7 +122,6 @@ public class EmbeddingModelFactory {
if ("volcengine".equalsIgnoreCase(config.getProvider())
&& config.getModelName() != null
&& config.getModelName().contains("vision")) {
String baseUrl = resolveBaseUrl(config);
int dimensions = resolveDimensions(config);
log.info("创建豆包多模态 EmbeddingModel: model={}, baseUrl={}, dimensions={}",
config.getModelName(), baseUrl, dimensions);
@ -123,7 +130,6 @@ public class EmbeddingModelFactory {
}
// OpenAI 兼容提供商
String baseUrl = resolveBaseUrl(config);
String embeddingsPath = resolveEmbeddingsPath(config);
log.info("创建 OpenAI 兼容 EmbeddingModel: provider={}, baseUrl={}, embeddingsPath={}, model={}",
config.getProvider(), baseUrl, embeddingsPath, config.getModelName());
@ -148,11 +154,16 @@ public class EmbeddingModelFactory {
/**
* 解析 API 基础地址优先使用 DB 配置的 baseUrl否则使用提供商默认值
* DashScope 使用自己的 SDK 内置默认地址不需要在此配置
*/
private String resolveBaseUrl(AiModelConfig config) {
if (config.getBaseUrl() != null && !config.getBaseUrl().isBlank()) {
return config.getBaseUrl();
}
// DashScope 使用 SDK 内置默认地址无需指定
if ("dashscope".equalsIgnoreCase(config.getProvider())) {
return null;
}
String defaultUrl = DEFAULT_BASE_URLS.get(config.getProvider());
if (defaultUrl != null) {
return defaultUrl;

28
src/main/java/com/wok/supportbot/rag/config/QueryExpanderConfig.java

@ -1,28 +0,0 @@
package com.wok.supportbot.rag.config;
import com.wok.supportbot.config.ChatModelFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.rag.preretrieval.query.expansion.MultiQueryExpander;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 查询扩展器 Bean 配置
* 通过 ChatModelFactory 获取 ChatModel支持多提供商动态切换
*/
@Configuration
public class QueryExpanderConfig {
@Autowired
private ChatModelFactory chatModelFactory;
@Bean
public MultiQueryExpander multiQueryExpander() {
return MultiQueryExpander.builder()
.chatClientBuilder(ChatClient.builder(chatModelFactory.getChatModel("RAG_REWRITE")))
.numberOfQueries(3)
.includeOriginal(true)
.build();
}
}

46
src/main/java/com/wok/supportbot/rag/config/QueryTransformerConfig.java

@ -1,46 +0,0 @@
package com.wok.supportbot.rag.config;
import com.wok.supportbot.config.ChatModelFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.rag.preretrieval.query.transformation.CompressionQueryTransformer;
import org.springframework.ai.rag.preretrieval.query.transformation.QueryTransformer;
import org.springframework.ai.rag.preretrieval.query.transformation.RewriteQueryTransformer;
import org.springframework.ai.rag.preretrieval.query.transformation.TranslationQueryTransformer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 查询转换器 Bean 配置
* 通过 ChatModelFactory 获取 ChatModel支持多提供商动态切换
* 注意这些 Bean doChatWithRagEnhance() 中被注入但未生效TODO
* 实际使用的查询重写逻辑在 preretrieval/ 包下的 Rewriter 组件中
*/
@Configuration
public class QueryTransformerConfig {
@Autowired
private ChatModelFactory chatModelFactory;
@Bean
public QueryTransformer rewriteQueryTransformer() {
return RewriteQueryTransformer.builder()
.chatClientBuilder(ChatClient.builder(chatModelFactory.getChatModel("RAG_REWRITE")))
.build();
}
@Bean
public QueryTransformer translationQueryTransformer() {
return TranslationQueryTransformer.builder()
.chatClientBuilder(ChatClient.builder(chatModelFactory.getChatModel("RAG_REWRITE")))
.targetLanguage("chinese")
.build();
}
@Bean
public QueryTransformer compressionQueryTransformer() {
return CompressionQueryTransformer.builder()
.chatClientBuilder(ChatClient.builder(chatModelFactory.getChatModel("RAG_REWRITE")))
.build();
}
}

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

@ -129,7 +129,12 @@ public class AiModelConfigService {
wrapper.eq(AiModelConfig::getAppType, appType)
.eq(AiModelConfig::getIsActive, true)
.last("LIMIT 1");
return aiModelConfigMapper.selectOne(wrapper);
AiModelConfig config = aiModelConfigMapper.selectOne(wrapper);
// 防御trim 掉复制粘贴可能带入的前后空白避免 DashScope 返回 InvalidApiKey
if (config != null && config.getApiKey() != null) {
config.setApiKey(config.getApiKey().trim());
}
return config;
}
// ==================== 新建配置 ====================
@ -143,6 +148,10 @@ public class AiModelConfigService {
*/
@Transactional(rollbackFor = Exception.class)
public AiModelConfig createConfig(AiModelConfig config) {
// 防御trim API Key 前后空白避免复制粘贴引入不可见字符导致 InvalidApiKey
if (config.getApiKey() != null) {
config.setApiKey(config.getApiKey().trim());
}
// 校验Embedding 模型不能用于非 EMBEDDING 类型
validateModelTypeMatch(config);
// 如果新配置标记为活跃先禁用同类型的其他配置
@ -196,6 +205,10 @@ public class AiModelConfigService {
// 设置 ID 确保更新正确
config.setId(id);
// 防御trim API Key 前后空白
if (config.getApiKey() != null) {
config.setApiKey(config.getApiKey().trim());
}
aiModelConfigMapper.updateById(config);
// JSONB 字段 MyBatis Plus typeHandler 可能不触发 JdbcTemplate 显式写入保证可靠
persistExtraConfig(id, config.getExtraConfig());

6
src/main/java/com/wok/supportbot/service/CustomerServiceRoleService.java

@ -188,9 +188,9 @@ public class CustomerServiceRoleService {
return null;
}
StringBuilder sb = new StringBuilder();
if (!trimmedName.isEmpty()) {
sb.append("当前客服角色:").append(trimmedName).append("。");
}
// if (!trimmedName.isEmpty()) {
// sb.append("当前客服角色:").append(trimmedName).append("。");
// }
if (!trimmedPrompt.isEmpty()) {
sb.append(trimmedPrompt);
}

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

@ -34,7 +34,7 @@ const PROVIDER_DEFAULTS = {
embeddingModels: ['text-embedding-v2'],
defaultChatModel: 'qwen-turbo',
defaultEmbeddingModel: 'text-embedding-v2',
tip: '通义千问使用 DashScope 自动配置,Base URL 留空即可'
tip: '通义千问团队版/私有化部署需填写专属 Base URL,公共版可留空'
},
deepseek: {
baseUrl: 'https://api.deepseek.com',
@ -232,6 +232,9 @@ export default {
<div class="model-form-full">
<label style="font-size:13px;font-weight:600;display:block;margin-bottom:4px;">API 基础地址</label>
<input type="text" class="input" v-model="editModal.form.base_url" :placeholder="providerBaseUrlPlaceholder">
<div style="font-size:11px;color:#6b7280;margin-top:4px;">
💡 团队版/私有化部署/第三方代理需填写自定义地址留空则使用提供商官方默认地址
</div>
</div>
<!-- 向量维度 EMBEDDING 类型跨列 -->
@ -317,8 +320,10 @@ export default {
const defaults = PROVIDER_DEFAULTS[provider] || PROVIDER_DEFAULTS.other
const isEmbedding = editModal.value.form.app_type === 'EMBEDDING'
// 自动填充 Base URL
// 仅在新建模式下自动填充 Base URL,编辑模式保留用户已配置的值
if (editModal.value.mode !== 'edit') {
editModal.value.form.base_url = defaults.baseUrl
}
// 按 app_type 选择模型列表和默认模型
const models = isEmbedding ? (defaults.embeddingModels || []) : (defaults.chatModels || [])

21
src/main/resources/static/sdk/chatbot-sdk.js

@ -186,7 +186,7 @@ var ChatbotSDK = (function () {
* 解析并校验用户传入的配置填充默认值
*/
function parseConfig(raw) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
// 校验必传参数:integrateId(对应后端 roleId)
if (!raw.integrateId || (typeof raw.integrateId !== 'string' && typeof raw.integrateId !== 'number')
|| (typeof raw.integrateId === 'string' && raw.integrateId.trim() === '')) {
@ -202,7 +202,7 @@ var ChatbotSDK = (function () {
try {
new URL(raw.requestDomain);
}
catch (_l) {
catch (_m) {
logger.error(`requestDomain 不是合法的 URL 格式:${raw.requestDomain}。请提供完整的域名,如 https://api.example.com`);
return null;
}
@ -249,10 +249,12 @@ var ChatbotSDK = (function () {
showTeaser: (_f = raw.showTeaser) !== null && _f !== void 0 ? _f : true,
teaserText: (typeof raw.teaserText === 'string' && raw.teaserText.trim()) || '',
streaming: (_g = raw.streaming) !== null && _g !== void 0 ? _g : true,
enableRag: (_h = raw.enableRag) !== null && _h !== void 0 ? _h : false,
rewriteStrategy: raw.rewriteStrategy || 'REWRITE',
locale: raw.locale || 'zh-CN',
debug: (_h = raw.debug) !== null && _h !== void 0 ? _h : true,
sound: (_j = raw.sound) !== null && _j !== void 0 ? _j : false,
notification: (_k = raw.notification) !== null && _k !== void 0 ? _k : false,
debug: (_j = raw.debug) !== null && _j !== void 0 ? _j : true,
sound: (_k = raw.sound) !== null && _k !== void 0 ? _k : false,
notification: (_l = raw.notification) !== null && _l !== void 0 ? _l : false,
onError: typeof raw.onError === 'function' ? raw.onError : undefined,
onReady: typeof raw.onReady === 'function' ? raw.onReady : undefined,
onMessage: typeof raw.onMessage === 'function' ? raw.onMessage : undefined,
@ -506,7 +508,7 @@ var ChatbotSDK = (function () {
const params = new URLSearchParams();
params.set('message', message);
params.set('chatId', currentConfig.chatId);
params.set('rewriteStrategy', 'REWRITE');
params.set('rewriteStrategy', currentConfig.rewriteStrategy || 'REWRITE');
setIfPresent(params, 'roleId', currentConfig.integrateId);
setIfPresent(params, 'accountId', currentConfig.userId);
setIfPresent(params, 'categoryId', categoryId !== null && categoryId !== void 0 ? categoryId : currentConfig.categoryId);
@ -519,7 +521,7 @@ var ChatbotSDK = (function () {
const params = new URLSearchParams();
params.set('message', message);
params.set('chatId', currentConfig.chatId);
params.set('rewriteStrategy', 'REWRITE');
params.set('rewriteStrategy', currentConfig.rewriteStrategy || 'REWRITE');
setIfPresent(params, 'roleId', currentConfig.integrateId);
setIfPresent(params, 'accountId', currentConfig.userId);
setIfPresent(params, 'categoryId', categoryId !== null && categoryId !== void 0 ? categoryId : currentConfig.categoryId);
@ -3470,7 +3472,7 @@ var ChatbotSDK = (function () {
hideLoadingFn$1 = dom.hideLoading;
// 初始化知识库分类
currentCategoryId = cfg.categoryId;
useRag = !!cfg.categoryId || !!cfg.showCategorySwitch;
useRag = !!cfg.enableRag || !!cfg.categoryId || !!cfg.showCategorySwitch;
// 绑定发送事件
bindSendEvents();
// 绑定滚动监听(智能滚动 + 新消息提示)
@ -3782,7 +3784,8 @@ var ChatbotSDK = (function () {
await initChatId();
}
const aiTimestamp = now();
const shouldUseRag = useRag && (currentCategoryId !== undefined || config$1.categoryId !== undefined);
// RAG 启用条件:显式开启 enableRag,或已选择知识库分类
const shouldUseRag = useRag && (config$1.enableRag || currentCategoryId !== undefined || config$1.categoryId !== undefined);
// 显示 loading
if (showLoadingFn$1)
showLoadingFn$1();

2
src/main/resources/static/sdk/chatbot-sdk.js.map
File diff suppressed because it is too large
View File

2
src/main/resources/static/sdk/chatbot-sdk.min.js
File diff suppressed because it is too large
View File

2
src/main/resources/static/sdk/chatbot-sdk.min.js.map
File diff suppressed because it is too large
View File

9
src/main/resources/static/sdk/test.html

@ -161,8 +161,8 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
<select id="cfg-locale"><option value="zh-CN" selected>中文</option><option value="en">English</option></select>
</div>
<div class="fg">
<label>showCategorySwitch</label>
<select id="cfg-cat"><option value="0" selected>关闭</option><option value="1">开启</option></select>
<label>enableRag 知识库检索</label>
<select id="cfg-rag"><option value="0" selected>关闭</option><option value="1">开启</option></select>
</div>
</div>
<div class="fg-row">
@ -307,6 +307,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
streaming: getEl('cfg-stream').value === '1',
locale: getEl('cfg-locale').value,
showCategorySwitch: getEl('cfg-cat').value === '1',
enableRag: getEl('cfg-rag').value === '1',
debug: getEl('cfg-debug').value === '1',
showClear: true,
launcherTheme: getEl('cfg-theme').value || undefined,
@ -329,6 +330,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
streaming: true,
locale: 'zh-CN',
showCategorySwitch: false,
enableRag: false,
};
// 生成 Script 片段代码(纯文本)
@ -351,6 +353,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
if (cfg.streaming !== SDK_DEFAULTS.streaming) params.push(' streaming: ' + cfg.streaming + ', // 是否启用流式输出');
if (cfg.locale !== SDK_DEFAULTS.locale) params.push(' locale: ' + JSON.stringify(cfg.locale) + ', // 界面语言');
if (cfg.showCategorySwitch !== SDK_DEFAULTS.showCategorySwitch) params.push(' showCategorySwitch: ' + cfg.showCategorySwitch + ', // 是否显示知识库切换');
if (cfg.enableRag !== SDK_DEFAULTS.enableRag) params.push(' enableRag: ' + cfg.enableRag + ', // 启用 RAG 知识库检索');
if (cfg.launcherTheme) params.push(' launcherTheme: ' + JSON.stringify(cfg.launcherTheme) + ', // 悬浮按钮主题(磨砂玻璃质感)');
// debug 强制 false(生产环境推荐),无论面板当前值
params.push(' debug: false // 调试日志,生产环境建议关闭');
@ -383,7 +386,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
// 语法高亮:先转义 HTML,再用单遍正则匹配 HTML 注释 / 字符串 / 行注释 / 数字 / 对象 key
function highlightCode(code) {
const esc = code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const re = /(&lt;!--[\s\S]*?--&gt;)|('[^']*'|"[^"]*")|(\/\/[^\n]*)|\b(\d+)\b|\b(integrateId|requestDomain|userId|categoryId|title|primaryColor|position|width|streaming|locale|showCategorySwitch|debug|showClear|showAdminPanel|launcherIcon|launcherTheme)(?=\s*:)/g;
const re = /(&lt;!--[\s\S]*?--&gt;)|('[^']*'|"[^"]*")|(\/\/[^\n]*)|\b(\d+)\b|\b(integrateId|requestDomain|userId|categoryId|title|primaryColor|position|width|streaming|locale|showCategorySwitch|enableRag|rewriteStrategy|debug|showClear|showAdminPanel|launcherIcon|launcherTheme)(?=\s*:)/g;
return esc.replace(re, (m, htmlCmt, str, cmt, num, key) => {
if (htmlCmt) return '<span class="cm">' + htmlCmt + '</span>';
if (str) return '<span class="st">' + str + '</span>';

Loading…
Cancel
Save