Browse Source

chatsdk窗口增加水印功能

ChatbotSDK瘦身
TDesign-Vue-Next-1.20.6
wanghanlin 3 weeks ago
parent
commit
03dd0ab557
  1. 69
      client/dist/chatbot-sdk.js
  2. 2
      client/dist/chatbot-sdk.min.js
  3. 31
      client/package-lock.json
  4. 1
      client/package.json
  5. 55
      client/rollup.config.js
  6. 6
      client/src/config.ts
  7. 52
      client/src/dom.ts
  8. 17
      client/src/styles.ts
  9. 6
      client/src/types.ts
  10. 69
      src/main/resources/static/sdk/chatbot-sdk.js
  11. 2
      src/main/resources/static/sdk/chatbot-sdk.min.js
  12. BIN
      src/main/resources/static/sdk/launcher-logo.png
  13. 19
      src/main/resources/static/sdk/test.html

69
client/dist/chatbot-sdk.js
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

31
client/package-lock.json

@ -8,7 +8,6 @@
"name": "chatbot-sdk", "name": "chatbot-sdk",
"version": "1.0.0", "version": "1.0.0",
"devDependencies": { "devDependencies": {
"@rollup/plugin-image": "^3.0.3",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.1.0", "@rollup/plugin-typescript": "^12.1.0",
"rollup": "^4.30.0", "rollup": "^4.30.0",
@ -509,27 +508,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@rollup/plugin-image": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/@rollup/plugin-image/-/plugin-image-3.0.3.tgz",
"integrity": "sha512-qXWQwsXpvD4trSb8PeFPFajp8JLpRtqqOeNYRUKnEQNHm7e5UP7fuSRcbjQAJ7wDZBbnJvSdY5ujNBQd9B1iFg==",
"dev": true,
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"mini-svg-data-uri": "^1.4.4"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-terser": { "node_modules/@rollup/plugin-terser": {
"version": "0.4.4", "version": "0.4.4",
"resolved": "https://registry.npmmirror.com/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", "resolved": "https://registry.npmmirror.com/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
@ -1377,15 +1355,6 @@
"@jridgewell/sourcemap-codec": "^1.5.5" "@jridgewell/sourcemap-codec": "^1.5.5"
} }
}, },
"node_modules/mini-svg-data-uri": {
"version": "1.4.4",
"resolved": "https://registry.npmmirror.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
"integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
"dev": true,
"bin": {
"mini-svg-data-uri": "cli.js"
}
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",

1
client/package.json

@ -14,7 +14,6 @@
"test:watch": "vitest" "test:watch": "vitest"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-image": "^3.0.3",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.1.0", "@rollup/plugin-typescript": "^12.1.0",
"rollup": "^4.30.0", "rollup": "^4.30.0",

55
client/rollup.config.js

@ -1,6 +1,52 @@
import typescript from '@rollup/plugin-typescript'; import typescript from '@rollup/plugin-typescript';
import terser from '@rollup/plugin-terser'; import terser from '@rollup/plugin-terser';
import image from '@rollup/plugin-image';
import { copyFileSync, existsSync, mkdirSync } from 'fs';
import { dirname, resolve } from 'path';
const ROOT = process.cwd();
const DIST_DIR = resolve(ROOT, 'dist');
const BACKEND_DIR = resolve(ROOT, '../src/main/resources/static/sdk');
const ASSETS = [
{ src: resolve(ROOT, 'assets/launcher-logo.png'), name: 'launcher-logo.png' },
];
const BUNDLE_FILES = ['chatbot-sdk.js', 'chatbot-sdk.min.js'];
/** 将构建产物与静态资源复制到指定目录 */
function copyFiles(srcFiles, targetDir, label) {
if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
for (const { src, name } of srcFiles) {
if (!existsSync(src)) {
console.warn(`[copy-assets] 未找到 ${src},跳过`);
continue;
}
copyFileSync(src, resolve(targetDir, name));
}
console.log(`[copy-assets] 已复制 ${label}${targetDir}`);
}
/** Rollup 插件:构建结束后同步产物与 logo 到后端静态资源目录 */
function copyAssets() {
return {
name: 'copy-assets',
closeBundle() {
// 1. 复制 logo 到 dist(便于独立分发)
copyFiles(
ASSETS.map(a => ({ src: a.src, name: a.name })),
DIST_DIR,
'logo 到 dist'
);
// 2. 复制 logo + JS 产物到后端 static/sdk/
const backendAssets = [
...ASSETS.map(a => ({ src: a.src, name: a.name })),
...BUNDLE_FILES.map(name => ({ src: resolve(DIST_DIR, name), name })),
];
copyFiles(backendAssets, BACKEND_DIR, 'SDK 产物与 logo 到后端 static/sdk');
},
};
}
export default { export default {
input: 'src/index.ts', input: 'src/index.ts',
@ -9,28 +55,23 @@ export default {
file: 'dist/chatbot-sdk.js', file: 'dist/chatbot-sdk.js',
format: 'iife', format: 'iife',
name: 'ChatbotSDK', name: 'ChatbotSDK',
// 生产构建不需要 sourcemap,开发模式用 npm run dev 单独生成
sourcemap: false, sourcemap: false,
}, },
{ {
file: 'dist/chatbot-sdk.min.js', file: 'dist/chatbot-sdk.min.js',
format: 'iife', format: 'iife',
name: 'ChatbotSDK', name: 'ChatbotSDK',
// 生产环境不需要 sourcemap,加速构建
sourcemap: false, sourcemap: false,
plugins: [terser()], plugins: [terser()],
}, },
], ],
// 全局关闭 sourcemap 生成开关后,压缩版 output.sourcemap=false 才不会触发插件警告
plugins: [ plugins: [
image(),
typescript({ typescript({
tsconfig: './tsconfig.json', tsconfig: './tsconfig.json',
// Rollup 构建不生成 .d.ts,由独立的 tsc --emitDeclarationOnly 负责
declaration: false, declaration: false,
declarationDir: undefined, declarationDir: undefined,
// 生产构建不生成 sourcemap,避免插件警告并加速
sourceMap: false, sourceMap: false,
}), }),
copyAssets(),
], ],
}; };

6
client/src/config.ts

@ -8,10 +8,9 @@
*/ */
import { SDKConfig, ResolvedConfig } from './types'; import { SDKConfig, ResolvedConfig } from './types';
import { logger } from './logger'; import { logger } from './logger';
import launcherLogo from '../assets/launcher-logo.png';
/** 默认悬浮按钮图标 — 使用 assets/launcher-logo.png 的内联 base64 */
const DEFAULT_LAUNCHER_ICON = `<img src="${launcherLogo}" alt="" draggable="false" />`;
/** 默认悬浮按钮图标 — 指向 SDK 同目录下的外置 logo 文件 */
const DEFAULT_LAUNCHER_ICON = '<img src="/sdk/launcher-logo.png" alt="" draggable="false" />';
/** /**
* *
@ -67,6 +66,7 @@ export function parseConfig(raw: SDKConfig): ResolvedConfig | null {
showTeaser: raw.showTeaser ?? true, showTeaser: raw.showTeaser ?? true,
teaserText: (typeof raw.teaserText === 'string' && raw.teaserText.trim()) || '', teaserText: (typeof raw.teaserText === 'string' && raw.teaserText.trim()) || '',
resizable: raw.resizable ?? true, resizable: raw.resizable ?? true,
watermark: (typeof raw.watermark === 'string' && raw.watermark.trim()) || undefined,
streaming: raw.streaming ?? true, streaming: raw.streaming ?? true,
enableRag: raw.enableRag ?? true, enableRag: raw.enableRag ?? true,
rewriteStrategy: raw.rewriteStrategy || 'REWRITE', rewriteStrategy: raw.rewriteStrategy || 'REWRITE',

52
client/src/dom.ts

@ -54,6 +54,47 @@ export function createLauncher(config: ResolvedConfig, onClick: () => void): HTM
return launcher; return launcher;
} }
// ==================== 水印生成 ====================
/** 生成水印纹理背景图(Canvas 离屏渲染,平铺用) */
function createWatermarkPattern(config: ResolvedConfig): string {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) return '';
// 组装水印文本:配置值 + 日期 时分
const now = new Date();
const y = now.getFullYear();
const m = String(now.getMonth() + 1).padStart(2, '0');
const d = String(now.getDate()).padStart(2, '0');
const h = String(now.getHours()).padStart(2, '0');
const min = String(now.getMinutes()).padStart(2, '0');
const dateStr = `${y}-${m}-${d} ${h}:${min}`;
const watermarkText = config.watermark ? `${config.watermark} ${dateStr}` : dateStr;
// 一个平铺单元的尺寸
const cellW = 280;
const cellH = 160;
canvas.width = cellW;
canvas.height = cellH;
// 按主题选择透明度
const isDark = config.theme === 'dark';
ctx.fillStyle = isDark ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.06)';
ctx.font = '13px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 倾斜绘制
ctx.save();
ctx.translate(cellW / 2, cellH / 2);
ctx.rotate(-22 * Math.PI / 180);
ctx.fillText(watermarkText, 0, 0);
ctx.restore();
return canvas.toDataURL('image/png');
}
// ==================== 聊天弹窗 ==================== // ==================== 聊天弹窗 ====================
/** 创建聊天弹窗完整结构,返回各区域引用 */ /** 创建聊天弹窗完整结构,返回各区域引用 */
@ -84,6 +125,17 @@ export function createChatWindow(config: ResolvedConfig): {
windowEl.setAttribute('aria-label', config.title); windowEl.setAttribute('aria-label', config.title);
windowEl.setAttribute('aria-modal', 'false'); windowEl.setAttribute('aria-modal', 'false');
// === 水印层(仅当配置了 watermark 时创建,绝对定位背景,不拦截交互) ===
if (config.watermark) {
const watermark = document.createElement('div');
watermark.className = 'csk-watermark';
const watermarkBg = createWatermarkPattern(config);
if (watermarkBg) {
watermark.style.backgroundImage = `url(${watermarkBg})`;
}
windowEl.appendChild(watermark);
}
// === 头部 === // === 头部 ===
const header = document.createElement('div'); const header = document.createElement('div');
header.className = 'csk-header'; header.className = 'csk-header';

17
client/src/styles.ts

@ -163,6 +163,20 @@ function getStyles(config: ResolvedConfig): string {
pointer-events: none; pointer-events: none;
} }
/* ========== 水印层 ========== */
.csk-watermark {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
user-select: none;
z-index: 0;
background-repeat: repeat;
opacity: 0.8;
}
/* ========== 头部 ========== */ /* ========== 头部 ========== */
.csk-header { .csk-header {
display: flex; display: flex;
@ -1186,6 +1200,9 @@ function getStyles(config: ResolvedConfig): string {
background: #1A1A2E; background: #1A1A2E;
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.4), 0 2px 8px rgba(0, 0, 0, 0.2); box-shadow: 0 12px 48px rgba(0, 0, 0, 0.4), 0 2px 8px rgba(0, 0, 0, 0.2);
} }
.csk-dark .csk-watermark {
opacity: 1;
}
.csk-dark .csk-header::after { background: linear-gradient(to bottom, rgba(0,0,0,0.12), transparent); } .csk-dark .csk-header::after { background: linear-gradient(to bottom, rgba(0,0,0,0.12), transparent); }
.csk-dark .csk-msg__avatar--ai { .csk-dark .csk-msg__avatar--ai {
background: var(--csk-bg-ai); background: var(--csk-bg-ai);

6
client/src/types.ts

@ -61,6 +61,10 @@ export interface SDKConfig {
/** 是否允许用户拖拽缩放窗口(右下角手柄),默认 true。移动端(≤480px)强制全屏时忽略 */ /** 是否允许用户拖拽缩放窗口(右下角手柄),默认 true。移动端(≤480px)强制全屏时忽略 */
resizable?: boolean; resizable?: boolean;
// === 水印 ===
/** 聊天窗口水印文字(浅色背景平铺),不传则不显示。建议传入用户ID、工号等标识信息 */
watermark?: string;
// === 行为配置 === // === 行为配置 ===
/** 是否启用流式输出,默认 true */ /** 是否启用流式输出,默认 true */
streaming?: boolean; streaming?: boolean;
@ -136,6 +140,8 @@ export interface ResolvedConfig {
teaserText: string; teaserText: string;
/** 是否允许用户拖拽缩放窗口 */ /** 是否允许用户拖拽缩放窗口 */
resizable: boolean; resizable: boolean;
/** 水印文字,不传则不显示 */
watermark?: string;
/** 流式输出 */ /** 流式输出 */
streaming: boolean; streaming: boolean;
/** 是否启用 RAG 知识库检索 */ /** 是否启用 RAG 知识库检索 */

69
src/main/resources/static/sdk/chatbot-sdk.js
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

BIN
src/main/resources/static/sdk/launcher-logo.png

After

Width: 1024  |  Height: 1024  |  Size: 1.3 MiB

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

@ -204,15 +204,15 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
</div> </div>
</div> </div>
<div class="fg-row"> <div class="fg-row">
<div class="fg">
<label>launcherIcon 悬浮按钮图标</label>
<input id="cfg-launcher-icon" type="text" placeholder="URL 或 SVG 字符串,留空使用默认极光粒子图标" style="width:100%;box-sizing:border-box;" />
<div class="hint">留空则使用默认极光粒子动画</div>
</div>
<div class="fg"> <div class="fg">
<label>showClear 清空按钮</label> <label>showClear 清空按钮</label>
<select id="cfg-clear"><option value="1" selected>显示</option><option value="0">隐藏</option></select> <select id="cfg-clear"><option value="1" selected>显示</option><option value="0">隐藏</option></select>
</div> </div>
<div class="fg">
<label>watermark 水印文字</label>
<input type="text" id="cfg-watermark" value="" placeholder="水印格式:日期 + 此值">
<div class="hint">留空则不显示水印</div>
</div>
</div> </div>
<div class="actions"> <div class="actions">
<button class="btn btn-primary btn-full" id="btn-init" onclick="doInit()">🚀 初始化 SDK</button> <button class="btn btn-primary btn-full" id="btn-init" onclick="doInit()">🚀 初始化 SDK</button>
@ -348,6 +348,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
debug: getEl('cfg-debug').value === '1', debug: getEl('cfg-debug').value === '1',
theme: getEl('cfg-theme-mode').value, theme: getEl('cfg-theme-mode').value,
showClear: getEl('cfg-clear').value === '1', showClear: getEl('cfg-clear').value === '1',
watermark: getEl('cfg-watermark').value.trim() || undefined,
}; };
// 有 Token 时传入鉴权参数 // 有 Token 时传入鉴权参数
if (token) { if (token) {
@ -446,7 +447,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
// SDK 可选参数的默认值(与 config.ts parseConfig 严格对齐),用于判断是否需要在生成代码中输出 // SDK 可选参数的默认值(与 config.ts parseConfig 严格对齐),用于判断是否需要在生成代码中输出
const SDK_DEFAULTS = { const SDK_DEFAULTS = {
title: 'AI 智能助手', title: 'AI 智能助手',
width: 380,
width: 500,
height: 520, height: 520,
position: 'right-bottom', position: 'right-bottom',
primaryColor: '#4F46E5', primaryColor: '#4F46E5',
@ -494,6 +495,8 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
if (cfg.showCategorySwitch !== SDK_DEFAULTS.showCategorySwitch) params.push(' showCategorySwitch: ' + cfg.showCategorySwitch + ', // 是否显示知识库切换'); 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.enableRag !== SDK_DEFAULTS.enableRag) params.push(' enableRag: ' + cfg.enableRag + ', // 启用 RAG 知识库检索');
if (cfg.showClear !== SDK_DEFAULTS.showClear) params.push(' showClear: ' + cfg.showClear + ', // 显示清空按钮'); if (cfg.showClear !== SDK_DEFAULTS.showClear) params.push(' showClear: ' + cfg.showClear + ', // 显示清空按钮');
const watermark = getEl('cfg-watermark').value.trim();
if (watermark) params.push(' watermark: ' + JSON.stringify(watermark) + ', // 聊天窗口水印文字');
// debug 强制 false(生产环境推荐),无论面板当前值 // debug 强制 false(生产环境推荐),无论面板当前值
params.push(' debug: false // 调试日志,生产环境建议关闭'); params.push(' debug: false // 调试日志,生产环境建议关闭');
const initBlock = 'ChatbotSDK.init({\n' + params.join('\n') + '\n});'; const initBlock = 'ChatbotSDK.init({\n' + params.join('\n') + '\n});';
@ -525,7 +528,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
// 语法高亮:先转义 HTML,再用单遍正则匹配 HTML 注释 / 字符串 / 行注释 / 数字 / 对象 key // 语法高亮:先转义 HTML,再用单遍正则匹配 HTML 注释 / 字符串 / 行注释 / 数字 / 对象 key
function highlightCode(code) { function highlightCode(code) {
const esc = code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); 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|enableRag|rewriteStrategy|debug|showClear|showAdminPanel|launcherIcon|token|roles)(?=\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|watermark|showAdminPanel|launcherIcon|token|roles)(?=\s*:)/g;
return esc.replace(re, (m, htmlCmt, str, cmt, num, key) => { return esc.replace(re, (m, htmlCmt, str, cmt, num, key) => {
if (htmlCmt) return '<span class="cm">' + htmlCmt + '</span>'; if (htmlCmt) return '<span class="cm">' + htmlCmt + '</span>';
if (str) return '<span class="st">' + str + '</span>'; if (str) return '<span class="st">' + str + '</span>';
@ -684,7 +687,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
setEl('footer-time',new Date().toLocaleTimeString()); setEl('footer-time',new Date().toLocaleTimeString());
if(typeof window.ChatbotSDK!=='undefined'){const tag=getEl('tag-sdk');tag.className='tag tag--pass';tag.textContent='✅ SDK 已加载';} if(typeof window.ChatbotSDK!=='undefined'){const tag=getEl('tag-sdk');tag.className='tag tag--pass';tag.textContent='✅ SDK 已加载';}
// 监听配置表单变化,实时刷新接入代码 // 监听配置表单变化,实时刷新接入代码
['cfg-iid','cfg-domain','cfg-uid','cfg-title','cfg-color','cfg-pos','cfg-width','cfg-height','cfg-resizable','cfg-stream','cfg-locale','cfg-cat','cfg-rag','cfg-clear','cfg-debug','cfg-theme','cfg-theme-mode','cfg-apikey','cfg-token'].forEach(function(id){
['cfg-iid','cfg-domain','cfg-uid','cfg-title','cfg-color','cfg-pos','cfg-width','cfg-height','cfg-resizable','cfg-stream','cfg-locale','cfg-cat','cfg-rag','cfg-clear','cfg-debug','cfg-theme-mode','cfg-apikey','cfg-token','cfg-watermark'].forEach(function(id){
const el=getEl(id); const el=getEl(id);
if(!el)return; if(!el)return;
el.addEventListener('input', generateCode); el.addEventListener('input', generateCode);

Loading…
Cancel
Save