Browse Source

SDK 聊天窗口底部的保密声明

master
wanghanlin 2 days ago
parent
commit
4eb726a056
  1. 71
      client/dist/chatbot-sdk.js
  2. 2
      client/dist/chatbot-sdk.js.map
  3. 2
      client/dist/chatbot-sdk.min.js
  4. 2
      client/dist/chatbot-sdk.min.js.map
  5. 21
      client/src/api.ts
  6. 1
      client/src/config.ts
  7. 17
      client/src/dom.ts
  8. 19
      client/src/index.ts
  9. 17
      client/src/styles.ts
  10. 4
      client/src/types.ts
  11. 53
      src/main/java/com/wok/supportbot/config/DatabaseInitConfig.java
  12. 30
      src/main/java/com/wok/supportbot/controller/AiController.java
  13. 134
      src/main/java/com/wok/supportbot/controller/SystemConfigController.java
  14. 12
      src/main/java/com/wok/supportbot/dao/SystemConfigMapper.java
  15. 58
      src/main/java/com/wok/supportbot/entity/SystemConfig.java
  16. 2
      src/main/java/com/wok/supportbot/security/SecurityConfig.java
  17. 106
      src/main/java/com/wok/supportbot/service/SystemConfigService.java
  18. 273
      src/main/resources/static/components/SystemConfigManager.js
  19. 22
      src/main/resources/static/js/api.js
  20. 6
      src/main/resources/static/js/app.js
  21. 4
      src/main/resources/static/js/store.js
  22. 71
      src/main/resources/static/sdk/chatbot-sdk.js
  23. 2
      src/main/resources/static/sdk/chatbot-sdk.js.map
  24. 2
      src/main/resources/static/sdk/chatbot-sdk.min.js
  25. 2
      src/main/resources/static/sdk/chatbot-sdk.min.js.map

71
client/dist/chatbot-sdk.js

@ -163,6 +163,7 @@ var ChatbotSDK = (function () {
onMessage: typeof raw.onMessage === 'function' ? raw.onMessage : undefined, onMessage: typeof raw.onMessage === 'function' ? raw.onMessage : undefined,
token: raw.token, token: raw.token,
roles: raw.roles, roles: raw.roles,
disclaimer: raw.disclaimer, // undefined = 从后端拉取,'' = 隐藏,'...' = 直接用
chatId: '', // 初始为空,由 chatId 初始化流程填充 chatId: '', // 初始为空,由 chatId 初始化流程填充
}; };
logger.info(`配置解析完成 integrateId(=roleId)=${config.integrateId} userId(=accountId)=${config.userId || '(未设置)'} requestDomain=${config.requestDomain}`); logger.info(`配置解析完成 integrateId(=roleId)=${config.integrateId} userId(=accountId)=${config.userId || '(未设置)'} requestDomain=${config.requestDomain}`);
@ -930,6 +931,28 @@ var ChatbotSDK = (function () {
// localStorage 不可用则忽略 // localStorage 不可用则忽略
} }
} }
// ==================== 系统配置(SDK 公开接口) ====================
/**
* 获取系统配置值 SDK 动态拉取声明等配置
* 路径 /ai/system-config/{key}safeFetch 自动注入 Bearer Token
* Token SDK 环境将被 SdkAuthFilter 拦截返回 401此处静默降级为空字符串
*/
async function fetchSystemConfig(key) {
if (!currentConfig)
return '';
try {
const url = buildUrl(`/ai/system-config/${encodeURIComponent(key)}`);
const response = await safeFetch(url);
if (!response.ok)
return '';
const json = await response.json();
return json.success && json.data ? (json.data.configValue || '') : '';
}
catch (_a) {
logger.warn(`获取系统配置 ${key} 失败,降级为空`);
return '';
}
}
let styleElement = null; let styleElement = null;
/** 将 hex 颜色解析为 "r, g, b" 字符串,用于 rgba() 拼接 */ /** 将 hex 颜色解析为 "r, g, b" 字符串,用于 rgba() 拼接 */
@ -1467,6 +1490,23 @@ var ChatbotSDK = (function () {
} }
.csk-send-btn svg { width: 18px; height: 18px; } .csk-send-btn svg { width: 18px; height: 18px; }
/* ========== 保密声明脚注 ========== */
.csk-disclaimer {
margin-top: 8px;
padding: 6px 8px 4px;
font-size: 10px;
color: #9CA3AF;
line-height: 1.5;
user-select: none;
}
.csk-disclaimer__title {
font-weight: 600;
margin-bottom: 2px;
color: #9CA3AF;
}
.csk-dark .csk-disclaimer { color: #6B7280; }
.csk-dark .csk-disclaimer__title { color: #6B7280; }
/* ========== 清空按钮 ========== */ /* ========== 清空按钮 ========== */
.csk-clear-btn { .csk-clear-btn {
display: inline-flex; display: inline-flex;
@ -2658,6 +2698,21 @@ var ChatbotSDK = (function () {
inputWrap.appendChild(inputEl); inputWrap.appendChild(inputEl);
inputWrap.appendChild(sendBtn); inputWrap.appendChild(sendBtn);
inputArea.appendChild(inputWrap); inputArea.appendChild(inputWrap);
// === 保密声明脚注 ===
const disclaimer = document.createElement('div');
disclaimer.className = 'csk-disclaimer';
if (config.disclaimer !== undefined) {
// 用户自定义或传空串隐藏
disclaimer.innerHTML = config.disclaimer;
if (!config.disclaimer) {
disclaimer.style.display = 'none';
}
}
else {
// 未传值,等异步从后端拉取,先隐藏
disclaimer.style.display = 'none';
}
inputArea.appendChild(disclaimer);
windowEl.appendChild(inputArea); windowEl.appendChild(inputArea);
// 清空按钮(可选) // 清空按钮(可选)
let clearBtn = null; let clearBtn = null;
@ -2734,6 +2789,7 @@ var ChatbotSDK = (function () {
searchInput, searchInput,
ariaLiveEl, ariaLiveEl,
roleSelect, roleSelect,
disclaimer,
showLoading, showLoading,
hideLoading, hideLoading,
}; };
@ -4649,6 +4705,8 @@ var ChatbotSDK = (function () {
let badgeEl = null; let badgeEl = null;
/** 音频上下文(提示音用) */ /** 音频上下文(提示音用) */
let audioCtx = null; let audioCtx = null;
/** 保密声明 DOM 引用 */
let disclaimerEl = null;
// ==================== 公开 API ==================== // ==================== 公开 API ====================
/** 初始化 SDK */ /** 初始化 SDK */
function init(rawConfig) { function init(rawConfig) {
@ -4708,6 +4766,7 @@ var ChatbotSDK = (function () {
historyPanel = dom.historyPanel; historyPanel = dom.historyPanel;
showLoadingFn = dom.showLoading; showLoadingFn = dom.showLoading;
hideLoadingFn = dom.hideLoading; hideLoadingFn = dom.hideLoading;
disclaimerEl = dom.disclaimer;
document.body.appendChild(windowEl); document.body.appendChild(windowEl);
// 8. 安置提示气泡(挂在 launcher 同层,由配置控制显示) // 8. 安置提示气泡(挂在 launcher 同层,由配置控制显示)
teaserEl = dom.teaserEl; teaserEl = dom.teaserEl;
@ -4810,6 +4869,17 @@ var ChatbotSDK = (function () {
initChatHistory().catch(err => { initChatHistory().catch(err => {
logger.warn('chatId 初始化失败,将在发送消息时重试', err); logger.warn('chatId 初始化失败,将在发送消息时重试', err);
}); });
// 20. 异步拉取保密声明(config.disclaimer undefined = 从后端拉取,有值/空串已在 dom.ts 处理)
if (config.disclaimer === undefined) {
fetchSystemConfig('disclaimer').then(html => {
if (html && disclaimerEl) {
disclaimerEl.innerHTML = html;
disclaimerEl.style.display = '';
}
}).catch(() => {
// 拉取失败静默降级,声明保持隐藏状态
});
}
} }
// ==================== 提示气泡控制 ==================== // ==================== 提示气泡控制 ====================
/** 根据 launcher 位置定位 teaser 气泡 */ /** 根据 launcher 位置定位 teaser 气泡 */
@ -4977,6 +5047,7 @@ var ChatbotSDK = (function () {
hideLoadingFn = null; hideLoadingFn = null;
badgeEl = null; badgeEl = null;
audioCtx = null; audioCtx = null;
disclaimerEl = null;
clearApiConfig(); clearApiConfig();
logger.lifecycleDestroy(oldIntegrateId || ''); logger.lifecycleDestroy(oldIntegrateId || '');
} }

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

21
client/src/api.ts

@ -678,3 +678,24 @@ export function saveCachedChatId(integrateId: string, userId?: string, chatId?:
// localStorage 不可用则忽略 // localStorage 不可用则忽略
} }
} }
// ==================== 系统配置(SDK 公开接口) ====================
/**
* SDK
* /ai/system-config/{key}safeFetch Bearer Token
* Token SDK SdkAuthFilter 401
*/
export async function fetchSystemConfig(key: string): Promise<string> {
if (!currentConfig) return '';
try {
const url = buildUrl(`/ai/system-config/${encodeURIComponent(key)}`);
const response = await safeFetch(url);
if (!response.ok) return '';
const json: ApiResponse<{ configKey: string; configValue: string }> = await response.json();
return json.success && json.data ? (json.data.configValue || '') : '';
} catch {
logger.warn(`获取系统配置 ${key} 失败,降级为空`);
return '';
}
}

1
client/src/config.ts

@ -79,6 +79,7 @@ export function parseConfig(raw: SDKConfig): ResolvedConfig | null {
onMessage: typeof raw.onMessage === 'function' ? raw.onMessage : undefined, onMessage: typeof raw.onMessage === 'function' ? raw.onMessage : undefined,
token: raw.token, token: raw.token,
roles: raw.roles, roles: raw.roles,
disclaimer: raw.disclaimer, // undefined = 从后端拉取,'' = 隐藏,'...' = 直接用
chatId: '', // 初始为空,由 chatId 初始化流程填充 chatId: '', // 初始为空,由 chatId 初始化流程填充
}; };

17
client/src/dom.ts

@ -72,6 +72,7 @@ export function createChatWindow(config: ResolvedConfig): {
resizeHandle: HTMLElement; resizeHandle: HTMLElement;
searchInput: HTMLInputElement | null; searchInput: HTMLInputElement | null;
ariaLiveEl: HTMLElement; ariaLiveEl: HTMLElement;
disclaimer: HTMLElement;
showLoading: () => HTMLElement; showLoading: () => HTMLElement;
hideLoading: () => void; hideLoading: () => void;
} { } {
@ -318,6 +319,21 @@ export function createChatWindow(config: ResolvedConfig): {
inputWrap.appendChild(inputEl); inputWrap.appendChild(inputEl);
inputWrap.appendChild(sendBtn); inputWrap.appendChild(sendBtn);
inputArea.appendChild(inputWrap); inputArea.appendChild(inputWrap);
// === 保密声明脚注 ===
const disclaimer = document.createElement('div');
disclaimer.className = 'csk-disclaimer';
if (config.disclaimer !== undefined) {
// 用户自定义或传空串隐藏
disclaimer.innerHTML = config.disclaimer;
if (!config.disclaimer) {
disclaimer.style.display = 'none';
}
} else {
// 未传值,等异步从后端拉取,先隐藏
disclaimer.style.display = 'none';
}
inputArea.appendChild(disclaimer);
windowEl.appendChild(inputArea); windowEl.appendChild(inputArea);
// 清空按钮(可选) // 清空按钮(可选)
@ -403,6 +419,7 @@ export function createChatWindow(config: ResolvedConfig): {
searchInput, searchInput,
ariaLiveEl, ariaLiveEl,
roleSelect, roleSelect,
disclaimer,
showLoading, showLoading,
hideLoading, hideLoading,
}; };

19
client/src/index.ts

@ -10,7 +10,7 @@
import { SDKConfig, ResolvedConfig, ChatbotSDKInstance } from './types'; import { SDKConfig, ResolvedConfig, ChatbotSDKInstance } from './types';
import { parseConfig } from './config'; import { parseConfig } from './config';
import { setDebug, logger, setErrorCallback } from './logger'; import { setDebug, logger, setErrorCallback } from './logger';
import { setApiConfig, clearApiConfig } from './api';
import { setApiConfig, clearApiConfig, fetchSystemConfig } from './api';
import { injectStyles, removeStyles } from './styles'; import { injectStyles, removeStyles } from './styles';
import { createLauncher, createChatWindow, enableDrag, enableLauncherDrag, enableResize } from './dom'; import { createLauncher, createChatWindow, enableDrag, enableLauncherDrag, enableResize } from './dom';
import { initChat, initChatHistory, setCategory, loadHistoryConversations, sendQuickReply, retryFromMessage, handleFeedback, switchRole } from './chat'; import { initChat, initChatHistory, setCategory, loadHistoryConversations, sendQuickReply, retryFromMessage, handleFeedback, switchRole } from './chat';
@ -61,6 +61,9 @@ let badgeEl: HTMLElement | null = null;
/** 音频上下文(提示音用) */ /** 音频上下文(提示音用) */
let audioCtx: AudioContext | null = null; let audioCtx: AudioContext | null = null;
/** 保密声明 DOM 引用 */
let disclaimerEl: HTMLElement | null = null;
// ==================== 公开 API ==================== // ==================== 公开 API ====================
/** 初始化 SDK */ /** 初始化 SDK */
@ -132,6 +135,7 @@ function init(rawConfig: SDKConfig): void {
historyPanel = dom.historyPanel; historyPanel = dom.historyPanel;
showLoadingFn = dom.showLoading; showLoadingFn = dom.showLoading;
hideLoadingFn = dom.hideLoading; hideLoadingFn = dom.hideLoading;
disclaimerEl = dom.disclaimer;
document.body.appendChild(windowEl); document.body.appendChild(windowEl);
// 8. 安置提示气泡(挂在 launcher 同层,由配置控制显示) // 8. 安置提示气泡(挂在 launcher 同层,由配置控制显示)
@ -244,6 +248,18 @@ function init(rawConfig: SDKConfig): void {
initChatHistory().catch(err => { initChatHistory().catch(err => {
logger.warn('chatId 初始化失败,将在发送消息时重试', err); logger.warn('chatId 初始化失败,将在发送消息时重试', err);
}); });
// 20. 异步拉取保密声明(config.disclaimer undefined = 从后端拉取,有值/空串已在 dom.ts 处理)
if (config.disclaimer === undefined) {
fetchSystemConfig('disclaimer').then(html => {
if (html && disclaimerEl) {
disclaimerEl.innerHTML = html;
disclaimerEl.style.display = '';
}
}).catch(() => {
// 拉取失败静默降级,声明保持隐藏状态
});
}
} }
// ==================== 提示气泡控制 ==================== // ==================== 提示气泡控制 ====================
@ -399,6 +415,7 @@ function destroy(): void {
hideLoadingFn = null; hideLoadingFn = null;
badgeEl = null; badgeEl = null;
audioCtx = null; audioCtx = null;
disclaimerEl = null;
clearApiConfig(); clearApiConfig();
logger.lifecycleDestroy(oldIntegrateId || ''); logger.lifecycleDestroy(oldIntegrateId || '');

17
client/src/styles.ts

@ -546,6 +546,23 @@ function getStyles(config: ResolvedConfig): string {
} }
.csk-send-btn svg { width: 18px; height: 18px; } .csk-send-btn svg { width: 18px; height: 18px; }
/* ========== 保密声明脚注 ========== */
.csk-disclaimer {
margin-top: 8px;
padding: 6px 8px 4px;
font-size: 10px;
color: #9CA3AF;
line-height: 1.5;
user-select: none;
}
.csk-disclaimer__title {
font-weight: 600;
margin-bottom: 2px;
color: #9CA3AF;
}
.csk-dark .csk-disclaimer { color: #6B7280; }
.csk-dark .csk-disclaimer__title { color: #6B7280; }
/* ========== 清空按钮 ========== */ /* ========== 清空按钮 ========== */
.csk-clear-btn { .csk-clear-btn {
display: inline-flex; display: inline-flex;

4
client/src/types.ts

@ -38,6 +38,8 @@ export interface SDKConfig {
width?: number; width?: number;
/** 弹窗高度(px),默认 520,最小 300 */ /** 弹窗高度(px),默认 520,最小 300 */
height?: number; height?: number;
/** 保密声明 HTML 内容。提供后直接渲染(不走后端拉取),传空串 '' 彻底隐藏,传 undefined 则从后端拉取 */
disclaimer?: string;
/** 悬浮按钮位置,默认 "right-bottom" */ /** 悬浮按钮位置,默认 "right-bottom" */
position?: 'left-bottom' | 'right-bottom'; position?: 'left-bottom' | 'right-bottom';
/** 主色调,默认 "#4F46E5" */ /** 主色调,默认 "#4F46E5" */
@ -112,6 +114,8 @@ export interface ResolvedConfig {
width: number; width: number;
/** 弹窗高度 */ /** 弹窗高度 */
height: number; height: number;
/** 保密声明 HTML 内容(来自 SDKConfig 或后端动态拉取) */
disclaimer?: string;
/** 位置 */ /** 位置 */
position: 'left-bottom' | 'right-bottom'; position: 'left-bottom' | 'right-bottom';
/** 主色调 */ /** 主色调 */

53
src/main/java/com/wok/supportbot/config/DatabaseInitConfig.java

@ -207,6 +207,14 @@ public class DatabaseInitConfig {
// ==================== API Key 角色绑定 ==================== // ==================== API Key 角色绑定 ====================
safeInit("api_key 新增 role_ids 列", this::addApiKeyRoleIdsColumn); safeInit("api_key 新增 role_ids 列", this::addApiKeyRoleIdsColumn);
// ==================== 系统配置 ====================
safeInit("创建系统配置表 system_config", () -> {
if (!checkTableExists("system_config")) {
createSystemConfigTable();
}
});
safeInit("同步默认系统配置", this::syncDefaultSystemConfigs);
// 为所有表添加注释幂等可重复执行 // 为所有表添加注释幂等可重复执行
safeInit("应用数据库表注释", this::applyTableComments); safeInit("应用数据库表注释", this::applyTableComments);
@ -240,7 +248,8 @@ public class DatabaseInitConfig {
"sys_user", "sys_role", "sys_permission", "sys_user_role", "sys_user", "sys_role", "sys_permission", "sys_user_role",
"rag_hit_log", "dashboard_snapshot", "rag_hit_log", "dashboard_snapshot",
"api_key", "webhook_config", "api_key", "webhook_config",
"mcp_server_config"
"mcp_server_config",
"system_config"
}; };
java.util.List<String> missingTables = new java.util.ArrayList<>(); java.util.List<String> missingTables = new java.util.ArrayList<>();
@ -1086,6 +1095,48 @@ public class DatabaseInitConfig {
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_mcp_server_config_active ON mcp_server_config (is_active) WHERE is_delete = FALSE"); jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_mcp_server_config_active ON mcp_server_config (is_active) WHERE is_delete = FALSE");
} }
/**
* 创建系统配置表 system_configkey-value 模式支持灵活扩展配置项
*/
private void createSystemConfigTable() {
String sql = """
CREATE TABLE IF NOT EXISTS system_config (
id BIGINT PRIMARY KEY,
config_key VARCHAR(128) NOT NULL UNIQUE,
config_value TEXT NOT NULL DEFAULT '',
description VARCHAR(500),
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
is_delete BOOLEAN NOT NULL DEFAULT FALSE
)
""";
jdbcTemplate.execute(sql);
// 创建序列仅当不存在时供种子数据/手动 INSERT 生成雪花 ID 兜底
jdbcTemplate.execute("CREATE SEQUENCE IF NOT EXISTS system_config_id_seq START 1000 NO CYCLE");
jdbcTemplate.execute("ALTER TABLE system_config ALTER COLUMN id SET DEFAULT nextval('system_config_id_seq')");
}
/**
* 同步默认系统配置种子数据
* <p>
* 使用 INSERT ... ON CONFLICT DO NOTHING首次启动写入默认值已存在不覆盖
*/
private void syncDefaultSystemConfigs() {
String defaultDisclaimer = """
<p class="csk-disclaimer__title">特别声明 Claim of Confidential</p>\
<p> 本文件内容为 内部公开 请勿外传禁止未授权的内部第三方人员使用与访问</p>\
<p> 非专项必要的业务与项目负责人收到此内容请立即删除</p>\
<p> 严禁在未经过主管领导审批发送给无关业务团队与人员</p>\
""";
jdbcTemplate.update("""
INSERT INTO system_config (config_key, config_value, description)
VALUES (?, ?, ?)
ON CONFLICT (config_key) DO NOTHING
""", "disclaimer", defaultDisclaimer, "SDK 聊天窗口底部的保密声明");
}
/** /**
* 为所有自动创建的表添加注释COMMENT ON * 为所有自动创建的表添加注释COMMENT ON
* 所有语句均为幂等操作可安全重复执行 * 所有语句均为幂等操作可安全重复执行

30
src/main/java/com/wok/supportbot/controller/AiController.java

@ -12,6 +12,7 @@ import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document; import org.springframework.ai.document.Document;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@ -47,6 +48,8 @@ public class AiController {
private CategoryFilter categoryFilter; private CategoryFilter categoryFilter;
@Resource @Resource
private ChatPipeline chatPipeline; private ChatPipeline chatPipeline;
@Resource
private com.wok.supportbot.service.SystemConfigService systemConfigService;
/** /**
@ -295,4 +298,31 @@ public class AiController {
private record AccountRoleContext(String accountId, Long roleId) { private record AccountRoleContext(String accountId, Long roleId) {
} }
// ==================== SDK 公开接口 ====================
/**
* 获取系统配置值 SDK 动态拉取保密声明等配置
* <p>
* 路径在 /ai/ SdkAuthFilter 守卫 Token SDK 请求会被拦截返回 401
*
* @param key 配置键 "disclaimer"
* @return { success: true, data: { configKey, configValue } }
*/
@GetMapping("/system-config/{key}")
public ResponseEntity<Map<String, Object>> getSdkSystemConfig(@PathVariable String key) {
try {
String value = systemConfigService.getValueByKey(key);
return ResponseEntity.ok(Map.of(
"success", true,
"data", Map.of("configKey", key, "configValue", value != null ? value : "")
));
} catch (Exception e) {
log.error("SDK 获取系统配置失败, key={}", key, e);
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "获取配置失败:" + e.getMessage()
));
}
}
} }

134
src/main/java/com/wok/supportbot/controller/SystemConfigController.java

@ -0,0 +1,134 @@
package com.wok.supportbot.controller;
import com.wok.supportbot.entity.SystemConfig;
import com.wok.supportbot.service.SystemConfigService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 系统配置管理接口
* <p>
* 管理后台编辑系统级配置项SDK 保密声明等
* 管理接口需要 admin 角色
*/
@Slf4j
@RestController
@RequestMapping("/system-config")
public class SystemConfigController {
@Autowired
private SystemConfigService systemConfigService;
/**
* 查询所有系统配置列表
*/
@GetMapping("/list")
@PreAuthorize("hasRole('admin')")
public ResponseEntity<Map<String, Object>> list() {
try {
List<SystemConfig> configs = systemConfigService.listAll();
return ResponseEntity.ok(Map.of(
"success", true,
"data", configs
));
} catch (Exception e) {
log.error("查询系统配置列表失败", e);
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "查询失败:" + e.getMessage()
));
}
}
/**
* 查询单条系统配置
*
* @param key 配置键 "disclaimer"
*/
@GetMapping("/{key}")
@PreAuthorize("hasRole('admin')")
public ResponseEntity<Map<String, Object>> getByKey(@PathVariable String key) {
try {
SystemConfig config = systemConfigService.getByKey(key);
if (config == null) {
return ResponseEntity.status(404).body(Map.of(
"success", false,
"message", "配置不存在:" + key
));
}
return ResponseEntity.ok(Map.of(
"success", true,
"data", config
));
} catch (Exception e) {
log.error("查询系统配置失败, key={}", key, e);
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "查询失败:" + e.getMessage()
));
}
}
/**
* 更新系统配置
*
* @param key 配置键 "disclaimer"
* @param body 请求体 { "configValue": "...", "description": "..." }
*/
@PutMapping("/{key}")
@PreAuthorize("hasRole('admin')")
public ResponseEntity<Map<String, Object>> update(
@PathVariable String key,
@RequestBody Map<String, String> body) {
try {
String configValue = body.getOrDefault("configValue", "");
String description = body.getOrDefault("description", "");
systemConfigService.saveOrUpdate(key, configValue, description);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "保存成功"
));
} catch (Exception e) {
log.error("更新系统配置失败", e);
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "保存失败:" + e.getMessage()
));
}
}
/**
* 删除系统配置逻辑删除
*
* @param key 配置键 "disclaimer"
*/
@DeleteMapping("/{key}")
@PreAuthorize("hasRole('admin')")
public ResponseEntity<Map<String, Object>> delete(@PathVariable String key) {
try {
boolean deleted = systemConfigService.deleteByKey(key);
if (!deleted) {
return ResponseEntity.status(404).body(Map.of(
"success", false,
"message", "配置不存在:" + key
));
}
return ResponseEntity.ok(Map.of(
"success", true,
"message", "删除成功"
));
} catch (Exception e) {
log.error("删除系统配置失败, key={}", key, e);
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "删除失败:" + e.getMessage()
));
}
}
}

12
src/main/java/com/wok/supportbot/dao/SystemConfigMapper.java

@ -0,0 +1,12 @@
package com.wok.supportbot.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.wok.supportbot.entity.SystemConfig;
import org.apache.ibatis.annotations.Mapper;
/**
* 系统配置 Mapper
*/
@Mapper
public interface SystemConfigMapper extends BaseMapper<SystemConfig> {
}

58
src/main/java/com/wok/supportbot/entity/SystemConfig.java

@ -0,0 +1,58 @@
package com.wok.supportbot.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 系统配置实体key-value 模式支持灵活扩展
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName("system_config")
public class SystemConfig implements Serializable {
@Serial
@TableField(exist = false)
private static final long serialVersionUID = 1L;
/** 主键ID(雪花算法) */
@TableId(value = "id", type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/** 配置键(唯一标识,如 "disclaimer") */
@TableField("config_key")
private String configKey;
/** 配置值 */
@TableField("config_value")
private String configValue;
/** 描述说明 */
@TableField("description")
private String description;
/** 创建时间 */
@TableField(value = "create_time", fill = FieldFill.INSERT)
private Date createTime;
/** 更新时间 */
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
/** 逻辑删除标识 */
@TableField("is_delete")
@TableLogic
private boolean isDelete;
}

2
src/main/java/com/wok/supportbot/security/SecurityConfig.java

@ -55,6 +55,8 @@ public class SecurityConfig {
// SDK 需要的接口feedback分类会话查询等 // SDK 需要的接口feedback分类会话查询等
.requestMatchers("/feedback").permitAll() .requestMatchers("/feedback").permitAll()
.requestMatchers("/category/tree", "/category/list").permitAll() .requestMatchers("/category/tree", "/category/list").permitAll()
// 系统配置SDK 拉取声明内容
.requestMatchers("/ai/system-config/**").permitAll()
// SDK 专属会话接口 /ai/** 路径下含账户归属校验 // SDK 专属会话接口 /ai/** 路径下含账户归属校验
// Knife4j / Swagger // Knife4j / Swagger
.requestMatchers("/doc.html", "/swagger-ui/**", "/v3/api-docs/**", "/webjars/**").permitAll() .requestMatchers("/doc.html", "/swagger-ui/**", "/v3/api-docs/**", "/webjars/**").permitAll()

106
src/main/java/com/wok/supportbot/service/SystemConfigService.java

@ -0,0 +1,106 @@
package com.wok.supportbot.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.wok.supportbot.dao.SystemConfigMapper;
import com.wok.supportbot.entity.SystemConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
/**
* 系统配置 CRUD 服务
* <p>
* 使用 key-value 模式存储系统级配置项 SDK 保密声明等
* 便于管理后台灵活编辑SDK 端动态拉取
*/
@Slf4j
@Service
public class SystemConfigService {
@Autowired
private SystemConfigMapper systemConfigMapper;
/**
* 根据 key 查询配置值仅返回 configValue 字符串
*
* @param key 配置键
* @return configValue不存在返回 null
*/
public String getValueByKey(String key) {
SystemConfig config = getByKey(key);
return config != null ? config.getConfigValue() : null;
}
/**
* 根据 key 查询完整配置实体
*
* @param key 配置键
* @return 配置实体不存在返回 null
*/
public SystemConfig getByKey(String key) {
LambdaQueryWrapper<SystemConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SystemConfig::getConfigKey, key);
return systemConfigMapper.selectOne(wrapper);
}
/**
* 保存或更新配置upsert 语义
*
* @param key 配置键
* @param value 配置值
* @param description 描述说明
*/
public void saveOrUpdate(String key, String value, String description) {
SystemConfig existing = getByKey(key);
if (existing != null) {
// 更新 configValuedescription updateTime
LambdaUpdateWrapper<SystemConfig> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(SystemConfig::getId, existing.getId())
.set(SystemConfig::getConfigValue, value)
.set(SystemConfig::getDescription, description)
.set(SystemConfig::getUpdateTime, new Date());
systemConfigMapper.update(null, wrapper);
log.info("更新系统配置:{}", key);
} else {
// 新建
SystemConfig config = SystemConfig.builder()
.configKey(key)
.configValue(value)
.description(description)
.build();
systemConfigMapper.insert(config);
log.info("新建系统配置:{}", key);
}
}
/**
* 查询所有未删除的配置列表
*
* @return 配置实体列表
*/
public List<SystemConfig> listAll() {
LambdaQueryWrapper<SystemConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.orderByAsc(SystemConfig::getConfigKey);
return systemConfigMapper.selectList(wrapper);
}
/**
* 根据 key 删除配置逻辑删除
*
* @param key 配置键
* @return true 删除成功false 配置不存在
*/
public boolean deleteByKey(String key) {
SystemConfig existing = getByKey(key);
if (existing == null) {
return false;
}
systemConfigMapper.deleteById(existing.getId());
log.info("删除系统配置:{}", key);
return true;
}
}

273
src/main/resources/static/components/SystemConfigManager.js

@ -0,0 +1,273 @@
/**
* 系统配置管理组件
* <p>
* 管理后台编辑系统级配置项SDK 保密声明等
* 支持 HTML 编辑 + 实时预览
*/
import { getSystemConfig, listSystemConfigs, updateSystemConfig, deleteSystemConfig } from '../js/api.js'
import { toast } from '../js/utils.js'
export default {
template: `
<div class="card">
<h3>🔧 系统配置</h3>
<p style="color: var(--text-secondary); margin-bottom: 16px; font-size: 14px;">
管理系统级配置项修改后实时生效SDK 窗口刷新即拉取最新内容
</p>
<!-- 配置列表 -->
<div v-if="!editing" style="display: flex; flex-direction: column; gap: 12px;">
<!-- 操作栏 -->
<div style="display: flex; gap: 8px; margin-bottom: 4px;">
<button @click="startAdd"
style="padding: 6px 14px; background: var(--primary); color: #fff; border: none;
border-radius: 6px; cursor: pointer; font-size: 13px;">
新建配置
</button>
<button @click="load" style="padding: 6px 14px;
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
cursor: pointer; font-size: 13px; color: var(--text-secondary);">
🔄 刷新
</button>
</div>
<div v-if="loading" style="text-align: center; padding: 40px; color: var(--text-secondary);">
加载中...
</div>
<div v-else-if="configs.length === 0" style="text-align: center; padding: 40px; color: var(--text-secondary);">
暂无配置项点击新建配置开始
</div>
<div v-else v-for="item in configs" :key="item.configKey"
style="display: flex; align-items: center; justify-content: space-between;
padding: 14px 16px; border: 1px solid var(--border); border-radius: 8px;
cursor: pointer; transition: border-color 0.2s, box-shadow 0.2s;"
@click="edit(item)"
@mouseenter="e => e.currentTarget.style.borderColor = 'var(--primary)'"
@mouseleave="e => e.currentTarget.style.borderColor = 'var(--border)'">
<div style="flex: 1; min-width: 0;">
<div style="font-weight: 600; font-size: 14px; margin-bottom: 2px;">
{{ item.configKey }}
</div>
<div style="font-size: 12px; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
{{ item.description || '无描述' }}
</div>
</div>
<div style="font-size: 12px; color: var(--text-secondary); margin-left: 16px; white-space: nowrap;">
{{ item.updateTime ? formatTime(item.updateTime) : '' }}
</div>
<button @click.stop="deleteItem(item)"
style="margin-left: 10px; padding: 4px 10px; background: #fee2e2; color: #dc2626;
border: none; border-radius: 6px; cursor: pointer; font-size: 12px;"
title="删除配置">
🗑 删除
</button>
</div>
</div>
<!-- 编辑区 -->
<div v-else>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px;">
<button @click="cancelEdit"
style="padding: 4px 10px; background: var(--bg); border: 1px solid var(--border);
border-radius: 6px; cursor: pointer; font-size: 13px;">
返回列表
</button>
<span style="font-weight: 600; font-size: 15px;">{{ isAdd ? '新建' : '编辑' }} {{ editingKey }}</span>
<span v-if="!isAdd && editingItem.description" style="font-size: 12px; color: var(--text-secondary);">
{{ editingItem.description }}
</span>
</div>
<!-- 配置键和描述新建和编辑均显示 -->
<div style="display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap;">
<div style="flex: 1; min-width: 180px;">
<label style="display: block; font-size: 12px; font-weight: 600; margin-bottom: 4px; color: var(--text-secondary);">
配置键 <span style="color: #ef4444;">*</span>
</label>
<input v-model="form.configKey" :disabled="!isAdd"
:placeholder="isAdd ? '如 disclaimer、welcome_text' : ''"
:style="'width: 100%; padding: 6px 10px; border: 1px solid var(--border); ' +
'border-radius: 6px; font-size: 13px; background: ' + (isAdd ? 'var(--bg)' : 'var(--border)') + '; color: var(--text);'" />
</div>
<div style="flex: 1; min-width: 180px;">
<label style="display: block; font-size: 12px; font-weight: 600; margin-bottom: 4px; color: var(--text-secondary);">
描述说明
</label>
<input v-model="form.description" placeholder="用于说明此配置的用途"
style="width: 100%; padding: 6px 10px; border: 1px solid var(--border);
border-radius: 6px; font-size: 13px; background: var(--bg); color: var(--text);" />
</div>
</div>
<div style="display: flex; gap: 16px; flex-wrap: wrap;">
<!-- 编辑区 -->
<div style="flex: 1; min-width: 300px;">
<label style="display: block; font-size: 13px; font-weight: 600; margin-bottom: 6px; color: var(--text-secondary);">
HTML 内容
</label>
<textarea v-model="form.configValue" rows="14"
style="width: 100%; padding: 10px 12px; border: 1px solid var(--border);
border-radius: 8px; font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px; line-height: 1.5; resize: vertical;
background: var(--bg); color: var(--text);"
placeholder="输入 HTML 内容..."></textarea>
</div>
<!-- 预览区 -->
<div style="flex: 1; min-width: 300px;">
<label style="display: block; font-size: 13px; font-weight: 600; margin-bottom: 6px; color: var(--text-secondary);">
👁 实时预览
</label>
<div style="border: 1px solid var(--border); border-radius: 8px; padding: 12px;
min-height: 200px; background: #f9fafb; font-size: 10px; color: #9CA3AF;
line-height: 1.5; overflow: auto;">
<div v-html="form.configValue" style="margin: 0;"></div>
<div v-if="!form.configValue" style="color: #ccc; font-style: italic;">
预览区域输入内容后实时显示效果
</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div style="display: flex; gap: 8px; margin-top: 16px;">
<button @click="save"
:disabled="saving"
style="padding: 8px 20px; background: var(--primary); color: #fff; border: none;
border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500;">
{{ saving ? '保存中...' : '💾 保存' }}
</button>
<button @click="cancelEdit"
style="padding: 8px 20px; background: var(--bg); border: 1px solid var(--border);
border-radius: 8px; cursor: pointer; font-size: 14px;">
取消
</button>
</div>
</div>
</div>
`,
data() {
return {
configs: [],
loading: false,
saving: false,
editing: false,
isAdd: false,
editingKey: '',
editingItem: {},
form: { configKey: '', configValue: '', description: '' }
}
},
mounted() {
this.load()
},
methods: {
async load() {
this.loading = true
try {
const json = await listSystemConfigs()
if (json.success) {
this.configs = json.data || []
}
} catch (e) {
toast(e.message || '加载失败', 'error')
} finally {
this.loading = false
}
},
edit(item) {
this.isAdd = false
this.editingKey = item.configKey
this.editingItem = item
this.form.configKey = item.configKey
this.form.configValue = item.configValue || ''
this.form.description = item.description || ''
this.editing = true
},
startAdd() {
this.isAdd = true
this.editingKey = ''
this.editingItem = {}
this.form.configKey = ''
this.form.configValue = ''
this.form.description = ''
this.editing = true
},
cancelEdit() {
this.editing = false
this.isAdd = false
this.editingKey = ''
this.editingItem = {}
this.form.configKey = ''
this.form.configValue = ''
this.form.description = ''
},
async save() {
if (this.saving) return
// 新建时需要校验 configKey
if (this.isAdd) {
const key = (this.form.configKey || '').trim()
if (!key) {
toast('请输入配置键', 'error')
return
}
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(key)) {
toast('配置键只能包含字母、数字、下划线和连字符,且以字母开头', 'error')
return
}
this.editingKey = key
}
this.saving = true
try {
const json = await updateSystemConfig(
this.editingKey,
this.form.configValue,
this.form.description || this.editingItem.description || ''
)
if (json.success) {
toast('保存成功', 'success')
this.cancelEdit()
this.load()
} else {
toast(json.message || '保存失败', 'error')
}
} catch (e) {
toast(e.message || '保存失败', 'error')
} finally {
this.saving = false
}
},
async deleteItem(item) {
if (!confirm(`确定删除配置「${item.configKey}」?`)) return
try {
const json = await deleteSystemConfig(item.configKey)
if (json.success) {
toast('删除成功', 'success')
this.load()
} else {
toast(json.message || '删除失败', 'error')
}
} catch (e) {
toast(e.message || '删除失败', 'error')
}
},
/** 简单时间格式化 */
formatTime(dateStr) {
if (!dateStr) return ''
const d = new Date(dateStr)
const pad = n => String(n).padStart(2, '0')
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) +
' ' + pad(d.getHours()) + ':' + pad(d.getMinutes())
}
}
}

22
src/main/resources/static/js/api.js

@ -1124,6 +1124,28 @@ export function refreshMcpServers() {
return postJSON('/mcp-server/refresh', {}) return postJSON('/mcp-server/refresh', {})
} }
// ==================== 系统配置管理 ====================
/** 获取单个系统配置(SDK 端 + 管理后台共用) */
export function getSystemConfig(key) {
return getJSON(`/system-config/${key}`)
}
/** 获取所有系统配置列表(管理后台) */
export function listSystemConfigs() {
return getJSON('/system-config/list')
}
/** 更新系统配置(管理后台,upsert 语义) */
export function updateSystemConfig(key, configValue, description) {
return putJSONWithBody(`/system-config/${key}`, { configValue, description })
}
/** 删除系统配置(管理后台,逻辑删除) */
export function deleteSystemConfig(key) {
return deleteJSON(`/system-config/${key}`)
}
// ==================== API Key 角色绑定 ==================== // ==================== API Key 角色绑定 ====================
/** /**

6
src/main/resources/static/js/app.js

@ -28,6 +28,7 @@ import DashboardPanel from '../components/DashboardPanel.js'
import ApiKeyManager from '../components/ApiKeyManager.js' import ApiKeyManager from '../components/ApiKeyManager.js'
import WebhookManager from '../components/WebhookManager.js' import WebhookManager from '../components/WebhookManager.js'
import McpServerManager from '../components/McpServerManager.js' import McpServerManager from '../components/McpServerManager.js'
import SystemConfigManager from '../components/SystemConfigManager.js'
const app = createApp({ const app = createApp({
setup() { setup() {
@ -307,6 +308,10 @@ const app = createApp({
<mcp-server-manager></mcp-server-manager> <mcp-server-manager></mcp-server-manager>
</div> </div>
<div v-if="store.activePage === 'system-config'" class="page-pane" style="animation: fadeIn .3s ease;">
<system-config-manager></system-config-manager>
</div>
<!-- 文档详情弹窗 --> <!-- 文档详情弹窗 -->
<doc-detail></doc-detail> <doc-detail></doc-detail>
</main> </main>
@ -338,5 +343,6 @@ app.component('dashboard-panel', DashboardPanel)
app.component('api-key-manager', ApiKeyManager) app.component('api-key-manager', ApiKeyManager)
app.component('webhook-manager', WebhookManager) app.component('webhook-manager', WebhookManager)
app.component('mcp-server-manager', McpServerManager) app.component('mcp-server-manager', McpServerManager)
app.component('system-config-manager', SystemConfigManager)
app.mount('#app') app.mount('#app')

4
src/main/resources/static/js/store.js

@ -48,7 +48,8 @@ export const MENU_ITEMS = [
{ id: 'user-manage', label: '用户管理', icon: '👥', roles: ['admin'] }, { id: 'user-manage', label: '用户管理', icon: '👥', roles: ['admin'] },
{ id: 'api-key', label: 'API Key 管理', icon: '🔑', roles: ['admin'] }, { id: 'api-key', label: 'API Key 管理', icon: '🔑', roles: ['admin'] },
{ id: 'webhook', label: 'Webhook 管理', icon: '🔔', roles: ['admin'] }, { id: 'webhook', label: 'Webhook 管理', icon: '🔔', roles: ['admin'] },
{ id: 'mcp-server', label: 'MCP 服务管理', icon: '🔌', roles: ['admin'] }
{ id: 'mcp-server', label: 'MCP 服务管理', icon: '🔌', roles: ['admin'] },
{ id: 'system-config',label: '系统配置', icon: '🔧', roles: ['admin'] }
] ]
} }
] ]
@ -246,6 +247,7 @@ export const store = reactive({
break break
case 'webhook': case 'webhook':
case 'mcp-server': case 'mcp-server':
case 'system-config':
// 新页面无额外数据预加载 // 新页面无额外数据预加载
break break
} }

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

@ -163,6 +163,7 @@ var ChatbotSDK = (function () {
onMessage: typeof raw.onMessage === 'function' ? raw.onMessage : undefined, onMessage: typeof raw.onMessage === 'function' ? raw.onMessage : undefined,
token: raw.token, token: raw.token,
roles: raw.roles, roles: raw.roles,
disclaimer: raw.disclaimer, // undefined = 从后端拉取,'' = 隐藏,'...' = 直接用
chatId: '', // 初始为空,由 chatId 初始化流程填充 chatId: '', // 初始为空,由 chatId 初始化流程填充
}; };
logger.info(`配置解析完成 integrateId(=roleId)=${config.integrateId} userId(=accountId)=${config.userId || '(未设置)'} requestDomain=${config.requestDomain}`); logger.info(`配置解析完成 integrateId(=roleId)=${config.integrateId} userId(=accountId)=${config.userId || '(未设置)'} requestDomain=${config.requestDomain}`);
@ -930,6 +931,28 @@ var ChatbotSDK = (function () {
// localStorage 不可用则忽略 // localStorage 不可用则忽略
} }
} }
// ==================== 系统配置(SDK 公开接口) ====================
/**
* 获取系统配置值 SDK 动态拉取声明等配置
* 路径 /ai/system-config/{key}safeFetch 自动注入 Bearer Token
* Token SDK 环境将被 SdkAuthFilter 拦截返回 401此处静默降级为空字符串
*/
async function fetchSystemConfig(key) {
if (!currentConfig)
return '';
try {
const url = buildUrl(`/ai/system-config/${encodeURIComponent(key)}`);
const response = await safeFetch(url);
if (!response.ok)
return '';
const json = await response.json();
return json.success && json.data ? (json.data.configValue || '') : '';
}
catch (_a) {
logger.warn(`获取系统配置 ${key} 失败,降级为空`);
return '';
}
}
let styleElement = null; let styleElement = null;
/** 将 hex 颜色解析为 "r, g, b" 字符串,用于 rgba() 拼接 */ /** 将 hex 颜色解析为 "r, g, b" 字符串,用于 rgba() 拼接 */
@ -1467,6 +1490,23 @@ var ChatbotSDK = (function () {
} }
.csk-send-btn svg { width: 18px; height: 18px; } .csk-send-btn svg { width: 18px; height: 18px; }
/* ========== 保密声明脚注 ========== */
.csk-disclaimer {
margin-top: 8px;
padding: 6px 8px 4px;
font-size: 10px;
color: #9CA3AF;
line-height: 1.5;
user-select: none;
}
.csk-disclaimer__title {
font-weight: 600;
margin-bottom: 2px;
color: #9CA3AF;
}
.csk-dark .csk-disclaimer { color: #6B7280; }
.csk-dark .csk-disclaimer__title { color: #6B7280; }
/* ========== 清空按钮 ========== */ /* ========== 清空按钮 ========== */
.csk-clear-btn { .csk-clear-btn {
display: inline-flex; display: inline-flex;
@ -2658,6 +2698,21 @@ var ChatbotSDK = (function () {
inputWrap.appendChild(inputEl); inputWrap.appendChild(inputEl);
inputWrap.appendChild(sendBtn); inputWrap.appendChild(sendBtn);
inputArea.appendChild(inputWrap); inputArea.appendChild(inputWrap);
// === 保密声明脚注 ===
const disclaimer = document.createElement('div');
disclaimer.className = 'csk-disclaimer';
if (config.disclaimer !== undefined) {
// 用户自定义或传空串隐藏
disclaimer.innerHTML = config.disclaimer;
if (!config.disclaimer) {
disclaimer.style.display = 'none';
}
}
else {
// 未传值,等异步从后端拉取,先隐藏
disclaimer.style.display = 'none';
}
inputArea.appendChild(disclaimer);
windowEl.appendChild(inputArea); windowEl.appendChild(inputArea);
// 清空按钮(可选) // 清空按钮(可选)
let clearBtn = null; let clearBtn = null;
@ -2734,6 +2789,7 @@ var ChatbotSDK = (function () {
searchInput, searchInput,
ariaLiveEl, ariaLiveEl,
roleSelect, roleSelect,
disclaimer,
showLoading, showLoading,
hideLoading, hideLoading,
}; };
@ -4649,6 +4705,8 @@ var ChatbotSDK = (function () {
let badgeEl = null; let badgeEl = null;
/** 音频上下文(提示音用) */ /** 音频上下文(提示音用) */
let audioCtx = null; let audioCtx = null;
/** 保密声明 DOM 引用 */
let disclaimerEl = null;
// ==================== 公开 API ==================== // ==================== 公开 API ====================
/** 初始化 SDK */ /** 初始化 SDK */
function init(rawConfig) { function init(rawConfig) {
@ -4708,6 +4766,7 @@ var ChatbotSDK = (function () {
historyPanel = dom.historyPanel; historyPanel = dom.historyPanel;
showLoadingFn = dom.showLoading; showLoadingFn = dom.showLoading;
hideLoadingFn = dom.hideLoading; hideLoadingFn = dom.hideLoading;
disclaimerEl = dom.disclaimer;
document.body.appendChild(windowEl); document.body.appendChild(windowEl);
// 8. 安置提示气泡(挂在 launcher 同层,由配置控制显示) // 8. 安置提示气泡(挂在 launcher 同层,由配置控制显示)
teaserEl = dom.teaserEl; teaserEl = dom.teaserEl;
@ -4810,6 +4869,17 @@ var ChatbotSDK = (function () {
initChatHistory().catch(err => { initChatHistory().catch(err => {
logger.warn('chatId 初始化失败,将在发送消息时重试', err); logger.warn('chatId 初始化失败,将在发送消息时重试', err);
}); });
// 20. 异步拉取保密声明(config.disclaimer undefined = 从后端拉取,有值/空串已在 dom.ts 处理)
if (config.disclaimer === undefined) {
fetchSystemConfig('disclaimer').then(html => {
if (html && disclaimerEl) {
disclaimerEl.innerHTML = html;
disclaimerEl.style.display = '';
}
}).catch(() => {
// 拉取失败静默降级,声明保持隐藏状态
});
}
} }
// ==================== 提示气泡控制 ==================== // ==================== 提示气泡控制 ====================
/** 根据 launcher 位置定位 teaser 气泡 */ /** 根据 launcher 位置定位 teaser 气泡 */
@ -4977,6 +5047,7 @@ var ChatbotSDK = (function () {
hideLoadingFn = null; hideLoadingFn = null;
badgeEl = null; badgeEl = null;
audioCtx = null; audioCtx = null;
disclaimerEl = null;
clearApiConfig(); clearApiConfig();
logger.lifecycleDestroy(oldIntegrateId || ''); logger.lifecycleDestroy(oldIntegrateId || '');
} }

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

Loading…
Cancel
Save