Browse Source
SDK 新增 6 项功能完善(类型导出/窗口配置/错误回调/测试/分组/通知)
SDK 新增 6 项功能完善(类型导出/窗口配置/错误回调/测试/分组/通知)
1. 类型导出:tsconfig 开启 declaration,新增 build:types 脚本,index.ts re-export 公开类型 2. 窗口高度可配置 + 记忆位置:height 配置项(默认 520,最小 300),拖拽位置 localStorage 持久化 3. 错误监控回调:新增 onError/onReady/onMessage 三个生命周期回调 4. 自动化测试:引入 Vitest,4 个测试文件共 67 个用例(markdown/config/i18n/utils) 5. 会话分组:历史面板按今天/昨天/本周/更早分组渲染 6. 声音/桌面通知:Web Audio API 合成提示音 + Notification API 桌面通知dev-mcp
27 changed files with 3071 additions and 1220 deletions
-
558client/dist/chatbot-sdk.js
-
2client/dist/chatbot-sdk.js.map
-
2client/dist/chatbot-sdk.min.js
-
2client/dist/chatbot-sdk.min.js.map
-
1019client/icon-designs-preview.html
-
1136client/package-lock.json
-
11client/package.json
-
3client/rollup.config.js
-
2client/src/chat.ts
-
111client/src/config.ts
-
70client/src/dom.ts
-
8client/src/i18n.ts
-
130client/src/index.ts
-
19client/src/logger.ts
-
109client/src/styles.ts
-
32client/src/types.ts
-
134client/tests/config.test.ts
-
100client/tests/i18n.test.ts
-
150client/tests/markdown.test.ts
-
86client/tests/utils.test.ts
-
3client/tsconfig.json
-
9client/vitest.config.ts
-
558src/main/resources/static/sdk/chatbot-sdk.js
-
2src/main/resources/static/sdk/chatbot-sdk.js.map
-
2src/main/resources/static/sdk/chatbot-sdk.min.js
-
2src/main/resources/static/sdk/chatbot-sdk.min.js.map
-
31src/main/resources/static/sdk/test.html
2
client/dist/chatbot-sdk.js.map
File diff suppressed because it is too large
View File
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
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
File diff suppressed because it is too large
View File
1019
client/icon-designs-preview.html
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
1136
client/package-lock.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,134 @@ |
|||||
|
/** |
||||
|
* config.ts 配置解析模块单元测试 |
||||
|
*/ |
||||
|
import { describe, it, expect } from 'vitest'; |
||||
|
import { parseConfig } from '../src/config'; |
||||
|
|
||||
|
const validConfig = { |
||||
|
integrateId: '123', |
||||
|
requestDomain: 'https://api.example.com', |
||||
|
}; |
||||
|
|
||||
|
describe('parseConfig - 必传参数校验', () => { |
||||
|
it('缺少 integrateId 返回 null', () => { |
||||
|
expect(parseConfig({ requestDomain: 'https://api.example.com' } as never)).toBeNull(); |
||||
|
}); |
||||
|
|
||||
|
it('integrateId 为空字符串返回 null', () => { |
||||
|
expect(parseConfig({ integrateId: '', requestDomain: 'https://api.example.com' })).toBeNull(); |
||||
|
}); |
||||
|
|
||||
|
it('缺少 requestDomain 返回 null', () => { |
||||
|
expect(parseConfig({ integrateId: '123' } as never)).toBeNull(); |
||||
|
}); |
||||
|
|
||||
|
it('requestDomain 不是合法 URL 返回 null', () => { |
||||
|
expect(parseConfig({ integrateId: '123', requestDomain: 'not-a-url' })).toBeNull(); |
||||
|
}); |
||||
|
|
||||
|
it('合法配置正常解析', () => { |
||||
|
const result = parseConfig(validConfig); |
||||
|
expect(result).not.toBeNull(); |
||||
|
expect(result!.integrateId).toBe('123'); |
||||
|
expect(result!.requestDomain).toBe('https://api.example.com'); |
||||
|
}); |
||||
|
|
||||
|
it('数字 integrateId 转为字符串', () => { |
||||
|
const result = parseConfig({ integrateId: 42, requestDomain: 'https://api.example.com' }); |
||||
|
expect(result!.integrateId).toBe('42'); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('parseConfig - 默认值填充', () => { |
||||
|
it('width 默认 380', () => { |
||||
|
const result = parseConfig(validConfig); |
||||
|
expect(result!.width).toBe(380); |
||||
|
}); |
||||
|
|
||||
|
it('height 默认 520', () => { |
||||
|
const result = parseConfig(validConfig); |
||||
|
expect(result!.height).toBe(520); |
||||
|
}); |
||||
|
|
||||
|
it('height 最小值 300', () => { |
||||
|
const result = parseConfig({ ...validConfig, height: 100 }); |
||||
|
expect(result!.height).toBe(300); |
||||
|
}); |
||||
|
|
||||
|
it('title 默认 "AI 智能助手"', () => { |
||||
|
const result = parseConfig(validConfig); |
||||
|
expect(result!.title).toBe('AI 智能助手'); |
||||
|
}); |
||||
|
|
||||
|
it('theme 默认 light', () => { |
||||
|
const result = parseConfig(validConfig); |
||||
|
expect(result!.theme).toBe('light'); |
||||
|
}); |
||||
|
|
||||
|
it('theme dark 正常解析', () => { |
||||
|
const result = parseConfig({ ...validConfig, theme: 'dark' }); |
||||
|
expect(result!.theme).toBe('dark'); |
||||
|
}); |
||||
|
|
||||
|
it('streaming 默认 true', () => { |
||||
|
const result = parseConfig(validConfig); |
||||
|
expect(result!.streaming).toBe(true); |
||||
|
}); |
||||
|
|
||||
|
it('debug 默认 true', () => { |
||||
|
const result = parseConfig(validConfig); |
||||
|
expect(result!.debug).toBe(true); |
||||
|
}); |
||||
|
|
||||
|
it('sound 默认 false', () => { |
||||
|
const result = parseConfig(validConfig); |
||||
|
expect(result!.sound).toBe(false); |
||||
|
}); |
||||
|
|
||||
|
it('notification 默认 false', () => { |
||||
|
const result = parseConfig(validConfig); |
||||
|
expect(result!.notification).toBe(false); |
||||
|
}); |
||||
|
|
||||
|
it('quickReplies 默认空数组', () => { |
||||
|
const result = parseConfig(validConfig); |
||||
|
expect(result!.quickReplies).toEqual([]); |
||||
|
}); |
||||
|
|
||||
|
it('quickReplies 过滤空字符串', () => { |
||||
|
const result = parseConfig({ ...validConfig, quickReplies: ['你好', '', ' ', '世界'] }); |
||||
|
expect(result!.quickReplies).toEqual(['你好', '世界']); |
||||
|
}); |
||||
|
|
||||
|
it('requestDomain 去除末尾斜杠', () => { |
||||
|
const result = parseConfig({ ...validConfig, requestDomain: 'https://api.example.com///' }); |
||||
|
expect(result!.requestDomain).toBe('https://api.example.com'); |
||||
|
}); |
||||
|
|
||||
|
it('position 仅接受两个合法值', () => { |
||||
|
expect(parseConfig({ ...validConfig, position: 'left-bottom' })!.position).toBe('left-bottom'); |
||||
|
expect(parseConfig({ ...validConfig, position: 'right-bottom' })!.position).toBe('right-bottom'); |
||||
|
expect(parseConfig({ ...validConfig, position: 'top' as never })!.position).toBe('right-bottom'); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('parseConfig - 回调函数', () => { |
||||
|
it('onError 回调正常保留', () => { |
||||
|
const onError = () => {}; |
||||
|
const result = parseConfig({ ...validConfig, onError }); |
||||
|
expect(result!.onError).toBe(onError); |
||||
|
}); |
||||
|
|
||||
|
it('非函数的 onError 被忽略', () => { |
||||
|
const result = parseConfig({ ...validConfig, onError: 'not-a-function' as never }); |
||||
|
expect(result!.onError).toBeUndefined(); |
||||
|
}); |
||||
|
|
||||
|
it('onReady / onMessage 回调正常保留', () => { |
||||
|
const onReady = () => {}; |
||||
|
const onMessage = () => {}; |
||||
|
const result = parseConfig({ ...validConfig, onReady, onMessage }); |
||||
|
expect(result!.onReady).toBe(onReady); |
||||
|
expect(result!.onMessage).toBe(onMessage); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,100 @@ |
|||||
|
/** |
||||
|
* i18n.ts 国际化模块单元测试 |
||||
|
*/ |
||||
|
import { describe, it, expect, beforeEach } from 'vitest'; |
||||
|
import { t, setLocale, getLocale } from '../src/i18n'; |
||||
|
|
||||
|
describe('setLocale / getLocale', () => { |
||||
|
beforeEach(() => { |
||||
|
setLocale('zh-CN'); |
||||
|
}); |
||||
|
|
||||
|
it('默认语言为 zh-CN', () => { |
||||
|
expect(getLocale()).toBe('zh-CN'); |
||||
|
}); |
||||
|
|
||||
|
it('切换为英文', () => { |
||||
|
setLocale('en'); |
||||
|
expect(getLocale()).toBe('en'); |
||||
|
}); |
||||
|
|
||||
|
it('匹配语言前缀(zh → zh-CN)', () => { |
||||
|
setLocale('zh'); |
||||
|
expect(getLocale()).toBe('zh-CN'); |
||||
|
}); |
||||
|
|
||||
|
it('未知语言保持默认 zh-CN', () => { |
||||
|
setLocale('fr'); |
||||
|
expect(getLocale()).toBe('zh-CN'); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('t()', () => { |
||||
|
beforeEach(() => { |
||||
|
setLocale('zh-CN'); |
||||
|
}); |
||||
|
|
||||
|
it('返回中文翻译', () => { |
||||
|
expect(t('send')).toBe('发送'); |
||||
|
expect(t('close')).toBe('关闭'); |
||||
|
}); |
||||
|
|
||||
|
it('切换英文后返回英文翻译', () => { |
||||
|
setLocale('en'); |
||||
|
expect(t('send')).toBe('Send'); |
||||
|
expect(t('close')).toBe('Close'); |
||||
|
}); |
||||
|
|
||||
|
it('未知 key 返回 key 本身', () => { |
||||
|
expect(t('nonexistent_key')).toBe('nonexistent_key'); |
||||
|
}); |
||||
|
|
||||
|
it('支持插值参数替换', () => { |
||||
|
setLocale('zh-CN'); |
||||
|
expect(t('source_count', { n: 3 })).toBe('3 条参考来源'); |
||||
|
}); |
||||
|
|
||||
|
it('英文插值参数替换', () => { |
||||
|
setLocale('en'); |
||||
|
expect(t('source_count', { n: 5 })).toBe('5 source(s)'); |
||||
|
}); |
||||
|
|
||||
|
it('中英文字典 key 数量一致', () => { |
||||
|
// 验证中英文字典完整性:所有中文 key 在英文中都有对应
|
||||
|
setLocale('zh-CN'); |
||||
|
const zhKeys = Object.keys(getAllKeys('zh-CN')); |
||||
|
setLocale('en'); |
||||
|
const enKeys = Object.keys(getAllKeys('en')); |
||||
|
|
||||
|
// 每个中文 key 在英文字典中也应存在
|
||||
|
for (const key of zhKeys) { |
||||
|
setLocale('en'); |
||||
|
const translated = t(key); |
||||
|
expect(translated).not.toBe(key); // 不应回退到 key 本身
|
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
/** 辅助函数:获取指定语言的所有 key(通过遍历测试已知 key) */ |
||||
|
function getAllKeys(_locale: string): Record<string, string> { |
||||
|
const knownKeys = [ |
||||
|
'title', 'minimize', 'close', 'status_online', |
||||
|
'welcome_title', 'welcome_desc', |
||||
|
'placeholder', 'send', 'stop', 'new_message', |
||||
|
'copy', 'copied', 'retry', |
||||
|
'loading', 'stream_interrupted', |
||||
|
'category_all', 'source_count', |
||||
|
'clear', 'clear_confirm', |
||||
|
'history_title', 'history_empty', 'history_search', |
||||
|
'feedback_up', 'feedback_down', |
||||
|
'teaser_text', 'new_msg_announce', |
||||
|
'error_network', 'error_timeout', 'error_send', |
||||
|
'history_group_today', 'history_group_yesterday', |
||||
|
'history_group_week', 'history_group_earlier', |
||||
|
]; |
||||
|
const result: Record<string, string> = {}; |
||||
|
for (const key of knownKeys) { |
||||
|
result[key] = t(key); |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
@ -0,0 +1,150 @@ |
|||||
|
/** |
||||
|
* markdown.ts Markdown 渲染器单元测试 |
||||
|
* |
||||
|
* 重点覆盖: |
||||
|
* 1. 基础语法正确性(标题、粗体、斜体、列表、链接、引用、代码) |
||||
|
* 2. XSS 防护(先转义再转换、链接白名单 http/https) |
||||
|
*/ |
||||
|
import { describe, it, expect } from 'vitest'; |
||||
|
import { renderMarkdown } from '../src/markdown'; |
||||
|
|
||||
|
describe('renderMarkdown - 基础语法', () => { |
||||
|
it('空字符串返回空', () => { |
||||
|
expect(renderMarkdown('')).toBe(''); |
||||
|
}); |
||||
|
|
||||
|
it('null/undefined 返回空', () => { |
||||
|
expect(renderMarkdown(null as never)).toBe(''); |
||||
|
expect(renderMarkdown(undefined as never)).toBe(''); |
||||
|
}); |
||||
|
|
||||
|
it('普通文本渲染为段落', () => { |
||||
|
expect(renderMarkdown('你好世界')).toContain('<p class="csk-md-p">你好世界</p>'); |
||||
|
}); |
||||
|
|
||||
|
it('标题渲染 h1-h6', () => { |
||||
|
expect(renderMarkdown('# 标题一')).toContain('<h1 class="csk-md-h1">'); |
||||
|
expect(renderMarkdown('## 标题二')).toContain('<h2 class="csk-md-h2">'); |
||||
|
expect(renderMarkdown('### 标题三')).toContain('<h3 class="csk-md-h3">'); |
||||
|
}); |
||||
|
|
||||
|
it('粗体渲染', () => { |
||||
|
expect(renderMarkdown('**粗体文本**')).toContain('<strong>粗体文本</strong>'); |
||||
|
}); |
||||
|
|
||||
|
it('斜体渲染', () => { |
||||
|
expect(renderMarkdown('*斜体文本*')).toContain('<em>斜体文本</em>'); |
||||
|
}); |
||||
|
|
||||
|
it('删除线渲染', () => { |
||||
|
expect(renderMarkdown('~~删除文本~~')).toContain('<del>删除文本</del>'); |
||||
|
}); |
||||
|
|
||||
|
it('无序列表渲染', () => { |
||||
|
const result = renderMarkdown('- 项目一\n- 项目二'); |
||||
|
expect(result).toContain('<ul class="csk-md-ul">'); |
||||
|
expect(result).toContain('<li>'); |
||||
|
expect(result).toContain('项目一'); |
||||
|
expect(result).toContain('项目二'); |
||||
|
}); |
||||
|
|
||||
|
it('有序列表渲染', () => { |
||||
|
const result = renderMarkdown('1. 第一步\n2. 第二步'); |
||||
|
expect(result).toContain('<ol class="csk-md-ol">'); |
||||
|
expect(result).toContain('<li>'); |
||||
|
}); |
||||
|
|
||||
|
it('引用渲染', () => { |
||||
|
const result = renderMarkdown('> 这是引用'); |
||||
|
expect(result).toContain('<blockquote class="csk-md-blockquote">'); |
||||
|
expect(result).toContain('这是引用'); |
||||
|
}); |
||||
|
|
||||
|
it('链接渲染(http 协议)', () => { |
||||
|
const result = renderMarkdown('[百度](https://www.baidu.com)'); |
||||
|
expect(result).toContain('href="https://www.baidu.com"'); |
||||
|
expect(result).toContain('target="_blank"'); |
||||
|
expect(result).toContain('rel="noopener noreferrer"'); |
||||
|
}); |
||||
|
|
||||
|
it('水平线渲染', () => { |
||||
|
expect(renderMarkdown('---')).toContain('<hr class="csk-md-hr">'); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('renderMarkdown - 代码', () => { |
||||
|
it('行内代码渲染', () => { |
||||
|
const result = renderMarkdown('使用 `console.log` 调试'); |
||||
|
expect(result).toContain('<code class="csk-md-inline-code">console.log</code>'); |
||||
|
}); |
||||
|
|
||||
|
it('代码块渲染', () => { |
||||
|
const result = renderMarkdown('```js\nconsole.log("hello");\n```'); |
||||
|
expect(result).toContain('<pre class="csk-md-code-block">'); |
||||
|
expect(result).toContain('class="language-js"'); |
||||
|
expect(result).toContain('console.log'); |
||||
|
}); |
||||
|
|
||||
|
it('代码块内容不被 Markdown 处理', () => { |
||||
|
const result = renderMarkdown('```\n**粗体** *斜体*\n```'); |
||||
|
// 代码块内的 Markdown 语法不应被转换
|
||||
|
expect(result).toContain('**粗体**'); |
||||
|
expect(result).toContain('*斜体*'); |
||||
|
expect(result).not.toContain('<strong>'); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('renderMarkdown - XSS 防护', () => { |
||||
|
it('转义 HTML 标签', () => { |
||||
|
const result = renderMarkdown('<script>alert("xss")</script>'); |
||||
|
expect(result).not.toContain('<script>'); |
||||
|
expect(result).toContain('<script>'); |
||||
|
}); |
||||
|
|
||||
|
it('转义 img 标签的 onerror', () => { |
||||
|
const result = renderMarkdown('<img src=x onerror=alert(1)>'); |
||||
|
// < 被转义为 <,标签不会执行,onerror 变为纯文本
|
||||
|
expect(result).not.toContain('<img'); |
||||
|
expect(result).toContain('<img'); |
||||
|
}); |
||||
|
|
||||
|
it('链接只允许 http/https 协议', () => { |
||||
|
const result = renderMarkdown('[恶意](javascript:alert(1))'); |
||||
|
expect(result).toContain('href="#"'); |
||||
|
expect(result).not.toContain('javascript:'); |
||||
|
}); |
||||
|
|
||||
|
it('允许 http 链接', () => { |
||||
|
const result = renderMarkdown('[安全](http://example.com)'); |
||||
|
expect(result).toContain('href="http://example.com"'); |
||||
|
}); |
||||
|
|
||||
|
it('代码块内容同样被转义', () => { |
||||
|
const result = renderMarkdown('```\n<script>alert(1)</script>\n```'); |
||||
|
expect(result).not.toContain('<script>alert'); |
||||
|
expect(result).toContain('<script>'); |
||||
|
}); |
||||
|
|
||||
|
it('行内代码内容同样被转义', () => { |
||||
|
const result = renderMarkdown('`<img src=x>`'); |
||||
|
expect(result).toContain('<img'); |
||||
|
expect(result).not.toContain('<img'); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('renderMarkdown - 复合内容', () => { |
||||
|
it('混合标题和段落', () => { |
||||
|
const result = renderMarkdown('# 标题\n\n这是正文'); |
||||
|
expect(result).toContain('<h1'); |
||||
|
expect(result).toContain('<p class="csk-md-p">这是正文</p>'); |
||||
|
}); |
||||
|
|
||||
|
it('多段落正确分隔', () => { |
||||
|
const result = renderMarkdown('第一段\n\n第二段'); |
||||
|
expect(result).toContain('第一段'); |
||||
|
expect(result).toContain('第二段'); |
||||
|
// 应有两个 <p> 标签
|
||||
|
const pCount = (result.match(/<p class="csk-md-p">/g) || []).length; |
||||
|
expect(pCount).toBe(2); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,86 @@ |
|||||
|
/** |
||||
|
* utils.ts 工具函数单元测试 |
||||
|
*/ |
||||
|
import { describe, it, expect, vi } from 'vitest'; |
||||
|
import { escapeHtml, debounce, formatTime, uuid, shortUuid } from '../src/utils'; |
||||
|
|
||||
|
describe('escapeHtml', () => { |
||||
|
it('转义 HTML 特殊字符', () => { |
||||
|
expect(escapeHtml('<script>alert("xss")</script>')).toBe( |
||||
|
'<script>alert("xss")</script>' |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
it('转义所有五种危险字符', () => { |
||||
|
expect(escapeHtml('&<>"\'')).toBe('&<>"''); |
||||
|
}); |
||||
|
|
||||
|
it('不修改安全文本', () => { |
||||
|
expect(escapeHtml('Hello World 你好世界')).toBe('Hello World 你好世界'); |
||||
|
}); |
||||
|
|
||||
|
it('空字符串返回空字符串', () => { |
||||
|
expect(escapeHtml('')).toBe(''); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('uuid', () => { |
||||
|
it('生成符合 UUID v4 格式的字符串', () => { |
||||
|
const id = uuid(); |
||||
|
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); |
||||
|
}); |
||||
|
|
||||
|
it('两次生成不同的值', () => { |
||||
|
expect(uuid()).not.toBe(uuid()); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('shortUuid', () => { |
||||
|
it('生成长度为 8 的字符串', () => { |
||||
|
expect(shortUuid()).toHaveLength(8); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('formatTime', () => { |
||||
|
it('格式化为 HH:mm', () => { |
||||
|
// 2026-01-15 14:30:00 UTC
|
||||
|
const ts = new Date(2026, 0, 15, 14, 30, 0).getTime(); |
||||
|
expect(formatTime(ts)).toBe('14:30'); |
||||
|
}); |
||||
|
|
||||
|
it('补零处理', () => { |
||||
|
const ts = new Date(2026, 0, 1, 9, 5, 0).getTime(); |
||||
|
expect(formatTime(ts)).toBe('09:05'); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('debounce', () => { |
||||
|
it('延迟执行函数', async () => { |
||||
|
vi.useFakeTimers(); |
||||
|
const fn = vi.fn(); |
||||
|
const debounced = debounce(fn, 100); |
||||
|
|
||||
|
debounced(); |
||||
|
expect(fn).not.toHaveBeenCalled(); |
||||
|
|
||||
|
vi.advanceTimersByTime(100); |
||||
|
expect(fn).toHaveBeenCalledTimes(1); |
||||
|
|
||||
|
vi.useRealTimers(); |
||||
|
}); |
||||
|
|
||||
|
it('连续调用只执行最后一次', async () => { |
||||
|
vi.useFakeTimers(); |
||||
|
const fn = vi.fn(); |
||||
|
const debounced = debounce(fn, 100); |
||||
|
|
||||
|
debounced(); |
||||
|
debounced(); |
||||
|
debounced(); |
||||
|
|
||||
|
vi.advanceTimersByTime(100); |
||||
|
expect(fn).toHaveBeenCalledTimes(1); |
||||
|
|
||||
|
vi.useRealTimers(); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,9 @@ |
|||||
|
import { defineConfig } from 'vitest/config'; |
||||
|
|
||||
|
export default defineConfig({ |
||||
|
test: { |
||||
|
globals: true, |
||||
|
environment: 'node', |
||||
|
include: ['tests/**/*.test.ts'], |
||||
|
}, |
||||
|
}); |
||||
2
src/main/resources/static/sdk/chatbot-sdk.js.map
File diff suppressed because it is too large
View File
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
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
File diff suppressed because it is too large
View File
Write
Preview
Loading…
Cancel
Save
Reference in new issue