本地 RAG 知识库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

12 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

项目定位

这是 AI 智能客服系统的前端 SDK 工程(chatbot-sdk),与仓库根目录的 Spring Boot 后端、src/main/resources/static/ 下的 Vue 3 管理后台是三个独立部分。SDK 产物是一行 <script> 标签即可嵌入第三方站点的悬浮客服窗口,IIFE 格式挂载到 window.ChatbotSDK。

根目录的 ../CLAUDE.md 描述后端架构,本文件只覆盖 client/ 工程。

构建与开发

cd client/

# 安装依赖
npm install

# 构建(输出 dist/chatbot-sdk.js 与 dist/chatbot-sdk.min.js)
npm run build

# 开发 watch 模式
npm run dev
  • 构建工具:Rollup + @rollup/plugin-typescript + @rollup/plugin-terser,配置见 rollup.config.js
  • TypeScript 配置:tsconfig.json,target: ES2017,strict: true,rootDir: ./src,不生成 .d.ts
  • 产物双份:chatbot-sdk.js(未压缩 + sourcemap,~5.6MB)和 chatbot-sdk.min.js(压缩,~4.0MB,gzip ~1.1MB)。体积主要由内置的 TDesign Web Components 组件库贡献(按需 tree-shaking 后打包进 IIFE)
  • 自动化测试:tests/ 下有 vitest 单测(npm test,node 环境,覆盖 config.ts 配置解析与 token.ts 的刷新竞态);另有后端 http://localhost:9090/sdk/test.html 的 10 个浏览器端核心用例(见 README 第十四节)

运行时依赖

SDK 本身不独立运行,需要后端在 requestDomain(通常 http://localhost:9090)提供以下接口:

  • 基础对话:GET /ai/chat、GET /ai/chat/stream
  • RAG:GET /ai/chat/stream(enableRag=true)、GET /ai/chat/sources
  • 分类:GET /category/tree、GET /category/list
  • 会话:GET /conversation/list、GET /conversation/{id}/messages、DELETE /conversation/{id}、GET /conversation/{id}/export

完整接口契约见 README.md 第六节。修改 api.ts 时务必与后端 AiController / ConversationController 保持参数一致。

架构

模块职责与数据流

入口 src/index.ts 是单例,按固定顺序串联各模块(init() 中的 12 个步骤即生命周期):

init(rawConfig)
  → logger.ts    setErrorCallback()   先注入 onError(早于配置解析:控制台日志已关闭,配置错误只能靠它上报)
  → config.ts    parseConfig()        解析 + 校验,返回 ResolvedConfig(失败返回 null,不抛异常)
  → i18n.ts      setLocale()          设置语言字典(zh-CN / en)
  → logger.ts    setDebug()           控制日志级别
  → api.ts       setApiConfig()       注入 requestDomain 等,供后续 HTTP/SSE 调用复用
  → token.ts     configureTokenManager()  注入 token/expiresIn/getToken,启动 Token 自动刷新
  → styles.ts    injectStyles()       注入 <style data-csk-sdk>,按 primaryColor 生成主题
  → dom.ts       createLauncher() / createChatWindow()  构建 DOM,返回元素引用与 loading 控制函数
  → dom.ts       enableDrag()         弹窗头部拖拽,返回 cleanup 函数
  → chat.ts      initChat()           绑定事件、注入 DOM 引用,建立对话核心状态
  → chat.ts      initChatHistory()    异步加载 chatId 与历史(不阻塞 UI)

模块间通过两种方式通信:

  1. 闭包状态:index.ts、chat.ts、api.ts、token.ts 各自持有模块级变量,靠 init* 函数注入
  2. CustomEvent:DOM 层(dom.ts)派发 csk:categoryChange、csk:loadHistory,index.ts 监听后转发给 chat.ts

关键模块

  • chat.ts(最大、最核心):对话状态机。处理发送、SSE 流式追加、RAG 来源展示、流中断兜底、无流内容降级为同步、chatId 自动管理逻辑、清空会话。Markdown 渲染由 t-chat-item 内置 cherry-markdown 处理。修改对话行为优先改这里。
  • api.ts:HTTP 封装 + SSE 流解析。三种 SSE 接口(普通流式、RAG 流式、同步)的解析逻辑都在这里,注意流式解析的边界处理。
  • token.ts:Token 管理。getToken() 是运行时读取 Token 的唯一出口(ResolvedConfig.token 只是 init 时的输入值,自动刷新后不会跟着变)。取 Token 的「源头」有两种形式(getToken 回调优先,tokenUrl 会用 createUrlProvider() 包成同样的 provider),触发方式有四条:① 临期主动刷新(setTimeout 排到「过期时刻 - 临期阈值」);② 每请求前取(tokenStrategy: 'always',ensureFresh 无条件取,不推算过期时间);③ 401 兜底重试(safeFetch 调 refreshNow() 换新后重试一次原请求);④ 手动入口 setHostToken() / forceRefresh()。
    • tokenStrategy 两种模式的分工:'expiry'(默认)由 SDK 推算过期时间,只在临期 / 401 时取;'always' 每请求前都取且不排定时器(scheduleProactive early return)、不认 staleProviderUntil。后者是因为该模式下宿主返回同一个仍有效的缓存 Token 是常态,若沿用过期判断会把它误当成 provider 故障,让模式退化成 401 驱动。
    • tokenUrl 用 credentials: 'same-origin':同源自动带 Cookie,跨域不带(避免把宿主站点 Cookie 泄露给别的域)。需跨域带 Cookie 的场景请宿主改用 getToken。响应兼容 {token,expiresIn} 与 {success,token,expiresIn,roles}。
    • isProtectedPath() 必须与后端 SdkAuthFilter.shouldNotFilter() 逐字对齐(受保护 = /ai/** 除 /ai/system-config/** + /feedback + /attachment/upload)。改 api.ts 里鉴权相关逻辑时必须同步核对后端过滤器,否则会出现「该带 Token 的没带」或「把 Token 泄露给公开端点」。用 URL.pathname 判定,不要用整串 includes。
    • 临期阈值必须自适应(min(总寿命 10%, 5min)):后端 clampExpirationMillis 允许的最短 ttl 恰好是 300s,固定 5 分钟会让 300s 的 Token 永远处于临期态 → 定时器延迟算成 0 → 秒级无限刷新循环。
    • expiresIn 只做上界钳制(24h),绝不做下界:钳下界会把「只剩 20s 的 Token」当成 300s,导致带着过期 Token 发请求。
    • setToken 不得清 refreshPromise(会破坏 single-flight),但必须自增 generation(否则在途刷新返回的旧 Token 会覆盖宿主刚推来的新值)。refreshPromise 只能由 refreshNow 的 finally 无条件清空 —— 宿主 getToken 回调若永不 settle,挂起会传染给后续所有受保护请求,所以 provider 调用自带 10s 超时。
    • 刷新失败后进 60s 退避窗口(backoffUntil,判断在 refreshNow 顶部,不在 ensureFresh):窗口内所有自动触发(临期检查 / 定时器 / 401 兜底)都不再尝试,否则宿主取 Token 挂死时每个请求都要多等一个 PROVIDER_TIMEOUT。请求照常发出、由 401 兜底;连续 3 次失败还会停掉定时器链。forceRefresh() 显式绕过该窗口。
    • Token 只存内存,不落 localStorage;destroy() 走 resetTokenManager()。
  • dom.ts:纯 DOM 构建与事件绑定,含知识库下拉、RAG 来源卡片、历史会话面板的渲染。
  • styles.ts:所有 CSS 字符串模板,按 primaryColor 动态着色,并通过 --td-* CSS 变量透传给 TDesign 组件。改动 UI 视觉改这里,不要在 dom.ts 里写内联样式。
    • 主题/聊天变量自包含约定:TDesign 的变量分为两类,都必须自包含声明在 .csk-root 上(tdThemeVars() 负责主题变量 --td-brand/--td-gray/--td-bg/...,tdChatVars() 负责聊天/markdown/input 变量 --td-chat-*/--td-chat-md-*/--td-chat-input-*)。原因:这些变量在 tdesign-web-components 里定义于 globalCSS 的 constructable stylesheet、选择器为 :root,但 adoptedStyleSheets 里的 :root 不匹配 shadow root,无法进入 shadow DOM;直接声明在 .csk-root 上才能经 CSS 自定义属性继承穿透 shadow DOM。升级 tdesign-web-components 版本时需同步核对 style/index.js 的 css$2/css/css$1 三个变量块。
    • light-DOM 组件样式补注入:t-tag 是 tdesign-web-components 的 light DOM 组件(isLightDOM: true),其 .t-tag 样式在 globalCSS 里只对 shadow DOM 生效、进不了 light DOM,需在 getStyles() 里以 .csk-root .t-tag 前缀手动补注入(来源 _chunks/dep-5245073d.js)。新增 light DOM 组件时同样排查是否需要补样式。
  • markdown.ts:已废弃并移除。原基于 marked 的 Markdown 渲染器改由 TDesign Chat 组件(t-chat-item)内置 cherry-markdown 承担,无需额外加载 marked.min.js。cherry-markdown 的 XSS 防护由 TDesign 组件保证;如后续需要自定义渲染,仍需维持白名单/转义等安全策略。

单例与生命周期

index.ts 用模块级变量持有 config 和所有 DOM 引用,isInitialized 防止重复初始化。destroy() 必须清理:移除 DOM、调用 dragCleanup()、removeStyles()、clearApiConfig() + resetTokenManager()、置空所有引用。新增 DOM 引用时记得在 destroy() 中同步置空。

关键约定

参数映射(SDK → 后端)

SDK 入参 后端参数 说明
integrateId roleId 客服角色 ID,决定 AI 人设和知识库范围(必传)
userId accountId 客户账号 ID,账号绑定角色后服务端会覆盖 roleId
token Authorization: Bearer SDK JWT,仅注入到受保护路径(见 token.ts);由 getToken 回调自动续期
(自动管理) chatId 从 /conversation/list 取或生成 sdk_时间戳_随机串

chatId 缓存在 localStorage(key: csk_chatId_{integrateId}_{userId}),clearHistory() 会重新生成。改 chat.ts 的 chatId 逻辑时注意与后端 ConversationController 的会话匹配规则一致。

CSS 命名空间

所有 class/id 用 csk- 前缀,样式注入到 <style data-csk-sdk>,z-index:悬浮按钮 9998,弹窗 9999。新增 DOM 元素必须带前缀,避免污染宿主页面。

localStorage

  • 消息历史 key:csk_history_{integrateId},上限 200 条,超出裁剪最早 50 条
  • chatId key:csk_chatId_{integrateId}_{userId}
  • 窗口尺寸 / 位置:csk_size_{integrateId} 等
  • 不同 integrateId 隔离,互不影响
  • Token 不在此列:只存内存,不写 localStorage / sessionStorage(避免 XSS 或同域页面读取),页面刷新后由宿主重新 init() 传入或靠 getToken 自动重取

错误处理

所有错误不抛异常、不阻塞宿主页面。控制台日志已全量关闭(logger.ts 的 info/warn/error 都不输出),错误只通过宿主的 onError 回调上报 —— 因此新增错误路径时必须给 logger.error 传可识别的 code(第三个参数,如 auth_expired / auth_refresh_failed / config_invalid),否则宿主拿到的 code 永远是 'error'。新增异步逻辑要 try/catch 包裹,失败走 logger.error。

部署

构建脚本(rollup.config.js 的 copyAssets 插件)会自动把 chatbot-sdk.js / chatbot-sdk.min.js 和 launcher-logo.png 复制到后端 src/main/resources/static/sdk/,宿主页面通过 <script src="/sdk/chatbot-sdk.min.js"></script> 引入即可;CDN 分发时同样只需部署这三类文件(chatbot-sdk.js / chatbot-sdk.min.js / launcher-logo.png)。README.md 有完整接入示例与全部配置参数表,文档是 SDK 对外契约的一部分,改公开 API(init / destroy / open / close / toggle / clearHistory / setToken / refreshToken)或 SDKConfig 字段时同步更新 README.md 与根目录 SDK-INTEGRATION.md。