@lark-base-open/base-database-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +169 -0
- package/changelog/CHANGELOG-0.x.md +103 -0
- package/config-schema.json +28 -0
- package/dist/agent-guide.d.ts +18 -0
- package/dist/agent-guide.d.ts.map +1 -0
- package/dist/agent-guide.js +30 -0
- package/dist/agent-guide.js.map +1 -0
- package/dist/archive.d.ts +16 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +162 -0
- package/dist/archive.js.map +1 -0
- package/dist/assets.d.ts +4 -0
- package/dist/assets.d.ts.map +1 -0
- package/dist/assets.js +6 -0
- package/dist/assets.js.map +1 -0
- package/dist/auth.d.ts +17 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +63 -0
- package/dist/auth.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +50 -0
- package/dist/config.js.map +1 -0
- package/dist/database-skill.d.ts +22 -0
- package/dist/database-skill.d.ts.map +1 -0
- package/dist/database-skill.js +135 -0
- package/dist/database-skill.js.map +1 -0
- package/dist/deploy.d.ts +28 -0
- package/dist/deploy.d.ts.map +1 -0
- package/dist/deploy.js +476 -0
- package/dist/deploy.js.map +1 -0
- package/dist/environment.d.ts +12 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +27 -0
- package/dist/environment.js.map +1 -0
- package/dist/function-schema.d.ts +3 -0
- package/dist/function-schema.d.ts.map +1 -0
- package/dist/function-schema.js +310 -0
- package/dist/function-schema.js.map +1 -0
- package/dist/init.d.ts +77 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +418 -0
- package/dist/init.js.map +1 -0
- package/dist/lark-cli.d.ts +8 -0
- package/dist/lark-cli.d.ts.map +1 -0
- package/dist/lark-cli.js +228 -0
- package/dist/lark-cli.js.map +1 -0
- package/dist/npm.d.ts +13 -0
- package/dist/npm.d.ts.map +1 -0
- package/dist/npm.js +93 -0
- package/dist/npm.js.map +1 -0
- package/dist/page-title.d.ts +3 -0
- package/dist/page-title.d.ts.map +1 -0
- package/dist/page-title.js +47 -0
- package/dist/page-title.js.map +1 -0
- package/dist/preview-receipt.d.ts +23 -0
- package/dist/preview-receipt.d.ts.map +1 -0
- package/dist/preview-receipt.js +133 -0
- package/dist/preview-receipt.js.map +1 -0
- package/dist/process.d.ts +17 -0
- package/dist/process.d.ts.map +1 -0
- package/dist/process.js +53 -0
- package/dist/process.js.map +1 -0
- package/dist/program.d.ts +12 -0
- package/dist/program.d.ts.map +1 -0
- package/dist/program.js +204 -0
- package/dist/program.js.map +1 -0
- package/dist/pull.d.ts +18 -0
- package/dist/pull.d.ts.map +1 -0
- package/dist/pull.js +324 -0
- package/dist/pull.js.map +1 -0
- package/dist/skill.d.ts +7 -0
- package/dist/skill.d.ts.map +1 -0
- package/dist/skill.js +28 -0
- package/dist/skill.js.map +1 -0
- package/dist/types.d.ts +32 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/workspace-fingerprint.d.ts +8 -0
- package/dist/workspace-fingerprint.d.ts.map +1 -0
- package/dist/workspace-fingerprint.js +64 -0
- package/dist/workspace-fingerprint.js.map +1 -0
- package/package.json +54 -0
- package/skills/lark-base-database/SKILL.md +307 -0
- package/skills/lark-base-database/references/base-capability-routing.md +81 -0
- package/skills/lark-base-database/references/base-sql-unsupported.md +42 -0
- package/skills/lark-base-database/references/database-capability-skill.md +24 -0
- package/skills/lark-base-database/references/development.md +642 -0
- package/skills/lark-base-database/references/file-transfer.md +275 -0
- package/skills/lark-base-database/references/permission-design.md +138 -0
- package/skills/lark-base-database/references/release.md +81 -0
- package/skills/lark-base-database/references/sql-function.md +276 -0
- package/skills/lark-base-database/references/style-guide.md +88 -0
- package/templates/database-capability/SKILL.md.tmpl +41 -0
- package/templates/database-starter/README.md +11 -0
- package/templates/database-starter/gitignore +3 -0
- package/templates/database-starter/page/.prettierignore +4 -0
- package/templates/database-starter/page/.prettierrc.json +7 -0
- package/templates/database-starter/page/README.md +29 -0
- package/templates/database-starter/page/index.html +24 -0
- package/templates/database-starter/page/package-lock.json +2675 -0
- package/templates/database-starter/page/package.json +44 -0
- package/templates/database-starter/page/public/favicon.png +0 -0
- package/templates/database-starter/page/src/App.tsx +18 -0
- package/templates/database-starter/page/src/components/UserSelectorEditor/index.tsx +267 -0
- package/templates/database-starter/page/src/components/UserSelectorPanel/index.css +32 -0
- package/templates/database-starter/page/src/components/UserSelectorPanel/index.test.ts +24 -0
- package/templates/database-starter/page/src/components/UserSelectorPanel/index.tsx +478 -0
- package/templates/database-starter/page/src/components/UserSelectorPanel/user-search.test.ts +175 -0
- package/templates/database-starter/page/src/components/UserSelectorPanel/user-search.ts +201 -0
- package/templates/database-starter/page/src/components/database-runtime-ready.tsx +21 -0
- package/templates/database-starter/page/src/components/database-user-menu/index.tsx +238 -0
- package/templates/database-starter/page/src/components/feedback/page-state.tsx +66 -0
- package/templates/database-starter/page/src/components/floating-button/chatbot-entry.test.ts +23 -0
- package/templates/database-starter/page/src/components/floating-button/chatbot-entry.tsx +873 -0
- package/templates/database-starter/page/src/components/floating-button/index.tsx +196 -0
- package/templates/database-starter/page/src/components/floating-button/local-chat-panel.test.ts +25 -0
- package/templates/database-starter/page/src/components/floating-button/local-chat-panel.tsx +197 -0
- package/templates/database-starter/page/src/components/floating-button/utils/current-user.ts +56 -0
- package/templates/database-starter/page/src/components/floating-button/utils/env.ts +18 -0
- package/templates/database-starter/page/src/components/floating-button/utils/project-meta.test.ts +79 -0
- package/templates/database-starter/page/src/components/floating-button/utils/project-meta.ts +252 -0
- package/templates/database-starter/page/src/components/ui/button.tsx +56 -0
- package/templates/database-starter/page/src/index.css +67 -0
- package/templates/database-starter/page/src/lib/cn.test.ts +9 -0
- package/templates/database-starter/page/src/lib/cn.ts +7 -0
- package/templates/database-starter/page/src/lib/database-runtime-attachment.test.ts +168 -0
- package/templates/database-starter/page/src/lib/database-runtime-context.test.ts +111 -0
- package/templates/database-starter/page/src/lib/database-runtime.test.ts +103 -0
- package/templates/database-starter/page/src/lib/database-runtime.ts +389 -0
- package/templates/database-starter/page/src/main.tsx +34 -0
- package/templates/database-starter/page/src/vite-env.d.ts +1 -0
- package/templates/database-starter/page/tsconfig.json +23 -0
- package/templates/database-starter/page/vite.config.ts +13 -0
- package/templates/database-starter/prettierignore +1 -0
- package/templates/database-starter/server-functions/.prettierignore +3 -0
- package/templates/database-starter/server-functions/.prettierrc.json +7 -0
- package/templates/database-starter/server-functions/README.md +11 -0
- package/templates/database-starter/server-functions/package-lock.json +1687 -0
- package/templates/database-starter/server-functions/package.json +22 -0
- package/templates/database-starter/server-functions/scripts/run-tsc.mjs +50 -0
- package/templates/database-starter/server-functions/tsconfig.json +20 -0
- package/templates/database-starter/sql/README.md +8 -0
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
import {
|
|
2
|
+
memo,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
type PointerEvent as ReactPointerEvent,
|
|
8
|
+
} from 'react'
|
|
9
|
+
import { createPortal } from 'react-dom'
|
|
10
|
+
import { getBaseDatabaseEndpointConfig } from './utils/env'
|
|
11
|
+
import { LocalChatPanel } from './local-chat-panel'
|
|
12
|
+
|
|
13
|
+
interface ChatbotEntryProps {
|
|
14
|
+
chatbots: ChatbotInfo[]
|
|
15
|
+
/** 绑定的 Base token,面板新建会话时通知服务端重置需要。 */
|
|
16
|
+
baseId?: string
|
|
17
|
+
className?: string
|
|
18
|
+
fallbackName: string
|
|
19
|
+
onOpenChange?: (isOpen: boolean) => void
|
|
20
|
+
onSidebarWidthChange?: (width: number) => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ChatbotInfo {
|
|
24
|
+
id: string
|
|
25
|
+
name: string
|
|
26
|
+
iconInfo: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const IMG_DEFAULT_SIZE = 120
|
|
30
|
+
const RETRY_INTERVAL_TIME = 2000
|
|
31
|
+
const SUCCESS_STATUS = 200
|
|
32
|
+
const STATUS_TO_RETRY = [202, 204, 403]
|
|
33
|
+
const SIDEBAR_DEFAULT_WIDTH = 420
|
|
34
|
+
const SIDEBAR_MIN_WIDTH = 420
|
|
35
|
+
const SIDEBAR_MAX_WIDTH = 720
|
|
36
|
+
const SIDEBAR_VIEWPORT_MARGIN = 24
|
|
37
|
+
const SIDEBAR_WIDTH_STORAGE_KEY = 'base-site.chatbot-entry.sidebar-width'
|
|
38
|
+
/** 侧边栏开合状态按 chatbot 分开存:同一浏览器可能打开多个应用。 */
|
|
39
|
+
const SIDEBAR_OPEN_STORAGE_PREFIX = 'base-site.chatbot-entry.open:'
|
|
40
|
+
const PLACEHOLDER_CHATBOT_NAMES = new Set([
|
|
41
|
+
'untitled',
|
|
42
|
+
'untitled app',
|
|
43
|
+
'untitled chatbot',
|
|
44
|
+
'untitled site',
|
|
45
|
+
'unnamed',
|
|
46
|
+
'unnamed app',
|
|
47
|
+
'unnamed chatbot',
|
|
48
|
+
'unnamed site',
|
|
49
|
+
'未命名',
|
|
50
|
+
'未命名应用',
|
|
51
|
+
'未命名智能体',
|
|
52
|
+
'未命名站点',
|
|
53
|
+
])
|
|
54
|
+
|
|
55
|
+
type CoverPolicy = 'allow_up'
|
|
56
|
+
|
|
57
|
+
enum ChatbotIconType {
|
|
58
|
+
Icon = 0,
|
|
59
|
+
SystemIcon = 2,
|
|
60
|
+
CustomIcon = 5,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface ChatbotIconInfo {
|
|
64
|
+
key: string
|
|
65
|
+
type: ChatbotIconType
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface ChatbotIconProps {
|
|
69
|
+
iconInfo?: string
|
|
70
|
+
name?: string
|
|
71
|
+
className?: string
|
|
72
|
+
size?: number
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const systemIconKeyList = [
|
|
76
|
+
'purple_baseAgentBox.png',
|
|
77
|
+
'blue_baseAgentGlasses.png',
|
|
78
|
+
'yellow_baseAgentIdea.png',
|
|
79
|
+
'pink_baseAgentGift.png',
|
|
80
|
+
'green_baseAgentSprout.png',
|
|
81
|
+
'green_baseAgentChart.png',
|
|
82
|
+
'purple_baseAgentMagic.png',
|
|
83
|
+
'pink_baseAgentCat.png',
|
|
84
|
+
'blue_baseAgentSunglasses.png',
|
|
85
|
+
'purple_baseAgentCap.png',
|
|
86
|
+
'yellow_baseAgentStar.png',
|
|
87
|
+
'green_baseAgentMagnifier.png',
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
const oldSystemIconKey = [
|
|
91
|
+
'purple_baseAgentBase.png',
|
|
92
|
+
'yellow_baseAgentChat.png',
|
|
93
|
+
'pink_baseAgentData.png',
|
|
94
|
+
'pink_baseAgentEmail.png',
|
|
95
|
+
'purple_baseAgentFile.png',
|
|
96
|
+
'blue_baseAgentSearch.png',
|
|
97
|
+
'green_baseAgentTask.png',
|
|
98
|
+
'purple_defaultBaseAgent.png',
|
|
99
|
+
'blue_emailBaseAgent.png',
|
|
100
|
+
'blue_messageBaseAgent.png',
|
|
101
|
+
'blue_notebookBaseAgent.png',
|
|
102
|
+
'green_fourLeafBaseAgent.png',
|
|
103
|
+
'green_pluginBaseAgent.png',
|
|
104
|
+
'orange_notebookBaseAgent.png',
|
|
105
|
+
'orange_starBaseAgent.png',
|
|
106
|
+
'pink_tomatoBaseAgent.png',
|
|
107
|
+
'purple_dataBaseAgent.png',
|
|
108
|
+
'white_cloudsBaseAgent.png',
|
|
109
|
+
'yellow_boxBaseAgent.png',
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
const defaultSystemIcon = systemIconKeyList[0]
|
|
113
|
+
|
|
114
|
+
/** 渲染 Figma 设计中的 chatbot 浮动入口,并通过侧边栏 iframe 承载会话页。
|
|
115
|
+
*
|
|
116
|
+
* TODO(site-baseapp): 本组件仍在 Site 产物内嵌套第二层 feishu.cn iframe,
|
|
117
|
+
* 与「业务请求一律经父页面代理」的门禁冲突,属于待迁移项。目标形态是在 Site 内
|
|
118
|
+
* 直接渲染 Chatbot SDK,request / Poll 注入 base-site-channel/runtime 的 fetch,
|
|
119
|
+
* 长链注入 SiteChatbotStreamAdapter。迁移前置条件:Chatbot SDK 的 ChatbotAdapter
|
|
120
|
+
* 需先提供独立的 stream adapter 注入参数,宿主侧 StreamBroker 与 MsgBox 凭证链路就绪。
|
|
121
|
+
*
|
|
122
|
+
* @param props.chatbots 项目资源中的 chatbot 列表。
|
|
123
|
+
* @param props.className 入口按钮附加样式。
|
|
124
|
+
* @param props.fallbackName Chatbot 仍是默认占位名时使用的 Site 名称。
|
|
125
|
+
* @param props.onOpenChange 打开状态变化回调。
|
|
126
|
+
* @param props.onSidebarWidthChange 侧边栏宽度变化回调。
|
|
127
|
+
* @returns chatbot 入口按钮与打开后的侧边栏弹窗。
|
|
128
|
+
*/
|
|
129
|
+
export function ChatbotEntry({
|
|
130
|
+
chatbots,
|
|
131
|
+
baseId,
|
|
132
|
+
className,
|
|
133
|
+
fallbackName,
|
|
134
|
+
onOpenChange,
|
|
135
|
+
onSidebarWidthChange,
|
|
136
|
+
}: ChatbotEntryProps) {
|
|
137
|
+
const chatbot = chatbots[0] ?? null
|
|
138
|
+
const [isOpen, setIsOpen] = useState(() =>
|
|
139
|
+
readStoredSidebarOpen(chatbot?.id ?? ''),
|
|
140
|
+
)
|
|
141
|
+
/** 面板挂载后不再随关闭卸载:卸载会丢掉正在跑的那一轮,重开只能从历史重建。 */
|
|
142
|
+
const [panelMounted, setPanelMounted] = useState(isOpen)
|
|
143
|
+
const [sidebarWidth, setSidebarWidth] = useState(readStoredSidebarWidth)
|
|
144
|
+
const [isResizing, setIsResizing] = useState(false)
|
|
145
|
+
const triggerRef = useRef<HTMLButtonElement>(null)
|
|
146
|
+
const panelRef = useRef<HTMLElement>(null)
|
|
147
|
+
/** 拖拽期间的实时宽度。只写 DOM 不进 state,松手才提交。 */
|
|
148
|
+
const previewWidthRef = useRef(sidebarWidth)
|
|
149
|
+
const chatbotUrl = chatbot ? buildChatbotUrl(chatbot.id) : null
|
|
150
|
+
const chatbotDisplayName = chatbot
|
|
151
|
+
? resolveChatbotDisplayName(chatbot.name, fallbackName)
|
|
152
|
+
: ''
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
storeSidebarWidth(sidebarWidth)
|
|
156
|
+
onSidebarWidthChange?.(sidebarWidth)
|
|
157
|
+
}, [onSidebarWidthChange, sidebarWidth])
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
onOpenChange?.(isOpen)
|
|
161
|
+
}, [isOpen, onOpenChange])
|
|
162
|
+
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (isOpen) {
|
|
165
|
+
setPanelMounted(true)
|
|
166
|
+
}
|
|
167
|
+
storeSidebarOpen(chatbot?.id ?? '', isOpen)
|
|
168
|
+
}, [chatbot?.id, isOpen])
|
|
169
|
+
|
|
170
|
+
/** 关闭侧边栏。必须保持引用稳定:`LocalChatPanel` 用它作为挂载依赖,
|
|
171
|
+
* 每次渲染换一个新函数会让面板卸载重挂并重新拉取会话历史。 */
|
|
172
|
+
const handleClosePanel = useCallback(() => {
|
|
173
|
+
setIsOpen(false)
|
|
174
|
+
}, [])
|
|
175
|
+
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
if (!isOpen) {
|
|
178
|
+
return undefined
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
panelRef.current?.focus()
|
|
182
|
+
|
|
183
|
+
return () => {
|
|
184
|
+
triggerRef.current?.focus()
|
|
185
|
+
}
|
|
186
|
+
}, [isOpen])
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (!isResizing) {
|
|
189
|
+
return undefined
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const { body } = document
|
|
193
|
+
const previousCursor = body.style.cursor
|
|
194
|
+
const previousUserSelect = body.style.userSelect
|
|
195
|
+
let frameId: number | undefined
|
|
196
|
+
|
|
197
|
+
/** 每帧至多同步一次预览宽度。直接写 DOM,不经过 React。
|
|
198
|
+
*
|
|
199
|
+
* 拖拽期间 setState 会让整个入口重渲染,`LocalChatPanel` 随之重新挂载,
|
|
200
|
+
* 面板每帧重新拉一次会话历史(history/list)。预览宽度必须只落到 DOM。 */
|
|
201
|
+
function applyPreviewWidth(): void {
|
|
202
|
+
frameId = undefined
|
|
203
|
+
const panel = panelRef.current
|
|
204
|
+
if (panel) {
|
|
205
|
+
panel.style.width = `${previewWidthRef.current}px`
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** 根据指针位置计算右侧栏宽度。 */
|
|
210
|
+
function handlePointerMove(event: PointerEvent): void {
|
|
211
|
+
previewWidthRef.current = clampSidebarWidth(
|
|
212
|
+
window.innerWidth - event.clientX,
|
|
213
|
+
)
|
|
214
|
+
if (frameId === undefined) {
|
|
215
|
+
frameId = window.requestAnimationFrame(applyPreviewWidth)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** 结束拖动,提交本轮宽度并恢复页面交互。 */
|
|
220
|
+
function handlePointerUp(): void {
|
|
221
|
+
if (frameId !== undefined) {
|
|
222
|
+
window.cancelAnimationFrame(frameId)
|
|
223
|
+
frameId = undefined
|
|
224
|
+
}
|
|
225
|
+
setSidebarWidth(previewWidthRef.current)
|
|
226
|
+
setIsResizing(false)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
body.style.cursor = 'col-resize'
|
|
230
|
+
body.style.userSelect = 'none'
|
|
231
|
+
window.addEventListener('pointermove', handlePointerMove)
|
|
232
|
+
window.addEventListener('pointerup', handlePointerUp, { once: true })
|
|
233
|
+
window.addEventListener('pointercancel', handlePointerUp, { once: true })
|
|
234
|
+
|
|
235
|
+
return () => {
|
|
236
|
+
if (frameId !== undefined) {
|
|
237
|
+
window.cancelAnimationFrame(frameId)
|
|
238
|
+
}
|
|
239
|
+
body.style.cursor = previousCursor
|
|
240
|
+
body.style.userSelect = previousUserSelect
|
|
241
|
+
window.removeEventListener('pointermove', handlePointerMove)
|
|
242
|
+
window.removeEventListener('pointerup', handlePointerUp)
|
|
243
|
+
window.removeEventListener('pointercancel', handlePointerUp)
|
|
244
|
+
}
|
|
245
|
+
}, [isResizing])
|
|
246
|
+
usePageScrollLock(isOpen)
|
|
247
|
+
|
|
248
|
+
/** 启动侧边栏横向拖拽调整。
|
|
249
|
+
* @param event 拖拽手柄的 pointerdown 事件。
|
|
250
|
+
*/
|
|
251
|
+
function handleResizeStart(
|
|
252
|
+
event: ReactPointerEvent<HTMLButtonElement>,
|
|
253
|
+
): void {
|
|
254
|
+
event.preventDefault()
|
|
255
|
+
const nextWidth = clampSidebarWidth(window.innerWidth - event.clientX)
|
|
256
|
+
previewWidthRef.current = nextWidth
|
|
257
|
+
if (panelRef.current) {
|
|
258
|
+
panelRef.current.style.width = `${nextWidth}px`
|
|
259
|
+
}
|
|
260
|
+
setIsResizing(true)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!chatbot || !chatbotUrl) {
|
|
264
|
+
return null
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<>
|
|
269
|
+
{!isOpen ? (
|
|
270
|
+
<div className="group relative">
|
|
271
|
+
<span className="pointer-events-none absolute right-0 bottom-[calc(100%+10px)] rounded-md bg-[#1f2329] px-3 py-2 text-xs leading-5 font-normal whitespace-nowrap text-white opacity-0 shadow-[0_4px_8px_-8px_rgba(31,35,41,0.09),0_6px_12px_rgba(31,35,41,0.04),0_8px_24px_8px_rgba(31,35,41,0.04)] transition-opacity after:absolute after:right-4 after:bottom-[-4px] after:size-2 after:rotate-45 after:bg-[#1f2329] group-hover:opacity-100">
|
|
272
|
+
{chatbotDisplayName}
|
|
273
|
+
</span>
|
|
274
|
+
<button
|
|
275
|
+
ref={triggerRef}
|
|
276
|
+
type="button"
|
|
277
|
+
className={`relative m-0 flex size-12 cursor-pointer items-center justify-center overflow-hidden rounded-full border border-[#dee0e3] bg-[#cfd5ff] p-0 outline-none transition-[filter] hover:brightness-[0.98] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 active:brightness-95 ${className ?? ''}`}
|
|
278
|
+
aria-label={`打开 ${chatbotDisplayName}`}
|
|
279
|
+
aria-haspopup="dialog"
|
|
280
|
+
aria-expanded={isOpen}
|
|
281
|
+
onClick={() => setIsOpen(true)}
|
|
282
|
+
>
|
|
283
|
+
<ChatbotIcon
|
|
284
|
+
iconInfo={chatbot.iconInfo}
|
|
285
|
+
name={chatbotDisplayName}
|
|
286
|
+
size={48}
|
|
287
|
+
/>
|
|
288
|
+
</button>
|
|
289
|
+
</div>
|
|
290
|
+
) : null}
|
|
291
|
+
|
|
292
|
+
{panelMounted
|
|
293
|
+
? createPortal(
|
|
294
|
+
<div
|
|
295
|
+
className={`fixed inset-0 z-50 ${isOpen ? '' : 'hidden'}`}
|
|
296
|
+
aria-hidden={!isOpen}
|
|
297
|
+
>
|
|
298
|
+
<div
|
|
299
|
+
className="absolute inset-0 cursor-default bg-[#1f2329]/20"
|
|
300
|
+
aria-hidden="true"
|
|
301
|
+
/>
|
|
302
|
+
<aside
|
|
303
|
+
ref={panelRef}
|
|
304
|
+
tabIndex={-1}
|
|
305
|
+
className="absolute top-0 right-0 flex h-dvh max-w-[calc(100vw-24px)] flex-col overflow-hidden border-l border-border shadow-[0_16px_48px_rgba(31,35,41,0.18)] outline-none"
|
|
306
|
+
style={{
|
|
307
|
+
width: sidebarWidth,
|
|
308
|
+
backgroundColor: 'var(--card, #ffffff)',
|
|
309
|
+
}}
|
|
310
|
+
role="dialog"
|
|
311
|
+
aria-modal="true"
|
|
312
|
+
aria-label="Chatbot 侧边栏"
|
|
313
|
+
>
|
|
314
|
+
<button
|
|
315
|
+
aria-label="拖动调整 chatbot 侧边栏宽度"
|
|
316
|
+
className="absolute top-0 bottom-0 left-0 z-10 w-2 -translate-x-1/2 cursor-col-resize touch-none border-0 bg-transparent p-0 outline-none transition-colors hover:bg-primary/15 focus-visible:bg-primary/20 focus-visible:ring-2 focus-visible:ring-primary/30 active:bg-primary/25"
|
|
317
|
+
onPointerDown={handleResizeStart}
|
|
318
|
+
type="button"
|
|
319
|
+
/>
|
|
320
|
+
{isResizing ? (
|
|
321
|
+
<div
|
|
322
|
+
aria-hidden="true"
|
|
323
|
+
className="absolute inset-0 z-[1] cursor-col-resize bg-transparent"
|
|
324
|
+
/>
|
|
325
|
+
) : null}
|
|
326
|
+
<LocalChatPanel
|
|
327
|
+
chatbotId={chatbot.id}
|
|
328
|
+
chatbotName={chatbotDisplayName}
|
|
329
|
+
chatbotCount={chatbots.length}
|
|
330
|
+
chatbotIconUrl={getChatbotIconUrl(chatbot.iconInfo)}
|
|
331
|
+
baseId={baseId}
|
|
332
|
+
onClose={handleClosePanel}
|
|
333
|
+
fallback={
|
|
334
|
+
<iframe
|
|
335
|
+
className="min-h-0 flex-1 border-0 bg-background"
|
|
336
|
+
src={chatbotUrl}
|
|
337
|
+
title={chatbotDisplayName}
|
|
338
|
+
loading="lazy"
|
|
339
|
+
referrerPolicy="no-referrer"
|
|
340
|
+
allow="clipboard-read; clipboard-write"
|
|
341
|
+
/>
|
|
342
|
+
}
|
|
343
|
+
/>
|
|
344
|
+
</aside>
|
|
345
|
+
</div>,
|
|
346
|
+
document.body,
|
|
347
|
+
)
|
|
348
|
+
: null}
|
|
349
|
+
</>
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** 将未命名占位值替换为 Site 名称,避免浮动入口暴露无意义名称。 */
|
|
354
|
+
export function resolveChatbotDisplayName(
|
|
355
|
+
chatbotName: string,
|
|
356
|
+
fallbackName: string,
|
|
357
|
+
): string {
|
|
358
|
+
const normalizedChatbotName = chatbotName.trim()
|
|
359
|
+
if (
|
|
360
|
+
normalizedChatbotName &&
|
|
361
|
+
!PLACEHOLDER_CHATBOT_NAMES.has(normalizedChatbotName.toLowerCase())
|
|
362
|
+
) {
|
|
363
|
+
return normalizedChatbotName
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return fallbackName.trim()
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** 在弹窗打开时锁定页面滚动,关闭后恢复原滚动位置。
|
|
370
|
+
* @param isLocked 是否需要锁定页面滚动。
|
|
371
|
+
*/
|
|
372
|
+
function usePageScrollLock(isLocked: boolean): void {
|
|
373
|
+
useEffect(() => {
|
|
374
|
+
if (!isLocked) {
|
|
375
|
+
return undefined
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const { body, documentElement } = document
|
|
379
|
+
const scrollX = window.scrollX
|
|
380
|
+
const scrollY = window.scrollY
|
|
381
|
+
const previousHtmlOverflow = documentElement.style.overflow
|
|
382
|
+
const previousBodyOverflow = body.style.overflow
|
|
383
|
+
const previousBodyPosition = body.style.position
|
|
384
|
+
const previousBodyTop = body.style.top
|
|
385
|
+
const previousBodyWidth = body.style.width
|
|
386
|
+
|
|
387
|
+
// 固定 body 可以避免部分内嵌运行时仍把滚动传递到底层页面。
|
|
388
|
+
documentElement.style.overflow = 'hidden'
|
|
389
|
+
body.style.overflow = 'hidden'
|
|
390
|
+
body.style.position = 'fixed'
|
|
391
|
+
body.style.top = `-${scrollY}px`
|
|
392
|
+
body.style.width = '100%'
|
|
393
|
+
|
|
394
|
+
return () => {
|
|
395
|
+
documentElement.style.overflow = previousHtmlOverflow
|
|
396
|
+
body.style.overflow = previousBodyOverflow
|
|
397
|
+
body.style.position = previousBodyPosition
|
|
398
|
+
body.style.top = previousBodyTop
|
|
399
|
+
body.style.width = previousBodyWidth
|
|
400
|
+
window.scrollTo({ left: scrollX, top: scrollY })
|
|
401
|
+
}
|
|
402
|
+
}, [isLocked])
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** 将侧边栏宽度限制在可用视口范围内。
|
|
406
|
+
* @param width 拖拽计算出的目标宽度。
|
|
407
|
+
* @returns 经过最小值、最大值和视口边距约束后的宽度。
|
|
408
|
+
*/
|
|
409
|
+
function clampSidebarWidth(width: number): number {
|
|
410
|
+
const viewportMaxWidth = Math.max(
|
|
411
|
+
SIDEBAR_MIN_WIDTH,
|
|
412
|
+
window.innerWidth - SIDEBAR_VIEWPORT_MARGIN,
|
|
413
|
+
)
|
|
414
|
+
const maxWidth = Math.min(SIDEBAR_MAX_WIDTH, viewportMaxWidth)
|
|
415
|
+
|
|
416
|
+
return Math.min(Math.max(width, SIDEBAR_MIN_WIDTH), maxWidth)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** 读取本地保存的侧边栏宽度。
|
|
420
|
+
* @returns 合法宽度;不存在或异常时返回默认宽度。
|
|
421
|
+
*/
|
|
422
|
+
function readStoredSidebarWidth(): number {
|
|
423
|
+
if (typeof window === 'undefined') {
|
|
424
|
+
return SIDEBAR_DEFAULT_WIDTH
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
let storedValue: string | null = null
|
|
428
|
+
try {
|
|
429
|
+
storedValue = window.localStorage.getItem(SIDEBAR_WIDTH_STORAGE_KEY)
|
|
430
|
+
} catch {
|
|
431
|
+
return SIDEBAR_DEFAULT_WIDTH
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const parsedWidth = storedValue ? Number(storedValue) : Number.NaN
|
|
435
|
+
return Number.isFinite(parsedWidth)
|
|
436
|
+
? clampSidebarWidth(parsedWidth)
|
|
437
|
+
: SIDEBAR_DEFAULT_WIDTH
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** 读取侧边栏是否处于打开状态。
|
|
441
|
+
* @param chatbotId 当前 chatbot 标识。
|
|
442
|
+
* @returns 上次离开时侧边栏是否打开;读不到时按关闭处理。
|
|
443
|
+
*/
|
|
444
|
+
function readStoredSidebarOpen(chatbotId: string): boolean {
|
|
445
|
+
if (typeof window === 'undefined' || !chatbotId) {
|
|
446
|
+
return false
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
return (
|
|
451
|
+
window.localStorage.getItem(
|
|
452
|
+
`${SIDEBAR_OPEN_STORAGE_PREFIX}${chatbotId}`,
|
|
453
|
+
) === '1'
|
|
454
|
+
)
|
|
455
|
+
} catch {
|
|
456
|
+
return false
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** 保存侧边栏开合状态,供刷新后恢复。
|
|
461
|
+
* @param chatbotId 当前 chatbot 标识。
|
|
462
|
+
* @param isOpen 侧边栏是否打开。
|
|
463
|
+
*/
|
|
464
|
+
function storeSidebarOpen(chatbotId: string, isOpen: boolean): void {
|
|
465
|
+
if (typeof window === 'undefined' || !chatbotId) {
|
|
466
|
+
return
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
const key = `${SIDEBAR_OPEN_STORAGE_PREFIX}${chatbotId}`
|
|
471
|
+
if (isOpen) {
|
|
472
|
+
window.localStorage.setItem(key, '1')
|
|
473
|
+
} else {
|
|
474
|
+
window.localStorage.removeItem(key)
|
|
475
|
+
}
|
|
476
|
+
} catch {
|
|
477
|
+
// 本地存储不可用时不影响侧边栏本身的开合。
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** 保存侧边栏宽度,供刷新或重新进入页面时恢复。
|
|
482
|
+
* @param width 当前侧边栏宽度。
|
|
483
|
+
*/
|
|
484
|
+
function storeSidebarWidth(width: number): void {
|
|
485
|
+
if (typeof window === 'undefined') {
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
window.localStorage.setItem(SIDEBAR_WIDTH_STORAGE_KEY, String(width))
|
|
491
|
+
} catch {
|
|
492
|
+
// 本地存储不可用时保持本次会话内的宽度状态即可。
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** 拼接 chatbot iframe 地址。
|
|
497
|
+
* @param chatId chatbot id。
|
|
498
|
+
* @returns iframe 加载地址。
|
|
499
|
+
*/
|
|
500
|
+
function buildChatbotUrl(chatId: string): string {
|
|
501
|
+
const { chatbotOrigin } = getBaseDatabaseEndpointConfig()
|
|
502
|
+
|
|
503
|
+
return (
|
|
504
|
+
`${chatbotOrigin}/base/agent/${encodeURIComponent(chatId)}` +
|
|
505
|
+
`?readonly=true&hideCreate=1&hideHeader=true`
|
|
506
|
+
)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** 判断输入是否为普通对象。
|
|
510
|
+
* @param value 待判断的数据。
|
|
511
|
+
* @returns 是普通对象时返回 true。
|
|
512
|
+
*/
|
|
513
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
514
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** 读取固定的系统图标 CDN 前缀。
|
|
518
|
+
* @returns 系统图标资源前缀。
|
|
519
|
+
*/
|
|
520
|
+
function getCustomIconUri(): string {
|
|
521
|
+
return getBaseDatabaseEndpointConfig().customIconUri
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** 获取默认系统图标地址。
|
|
525
|
+
* @returns 默认 chatbot 系统图标地址。
|
|
526
|
+
*/
|
|
527
|
+
function getDefaultSystemIconUrl(): string {
|
|
528
|
+
return getSystemIconUrl(defaultSystemIcon)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** 拼接系统图标地址。
|
|
532
|
+
* @param key 系统图标 key。
|
|
533
|
+
* @returns 系统图标完整地址。
|
|
534
|
+
*/
|
|
535
|
+
function getSystemIconUrl(key: string): string {
|
|
536
|
+
return new URL(key, getCustomIconUri()).toString()
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** 解析 chatbot iconInfo。
|
|
540
|
+
* @param iconStr iconInfo JSON 字符串。
|
|
541
|
+
* @returns 解析后的图标信息;结构异常时返回 null。
|
|
542
|
+
*/
|
|
543
|
+
function parseChatbotIconInfo(iconStr: string): ChatbotIconInfo | null {
|
|
544
|
+
try {
|
|
545
|
+
const iconInfo: unknown = JSON.parse(iconStr)
|
|
546
|
+
if (!isRecord(iconInfo)) {
|
|
547
|
+
return null
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const { key, type } = iconInfo
|
|
551
|
+
if (
|
|
552
|
+
typeof key !== 'string' ||
|
|
553
|
+
(type !== ChatbotIconType.Icon &&
|
|
554
|
+
type !== ChatbotIconType.SystemIcon &&
|
|
555
|
+
type !== ChatbotIconType.CustomIcon)
|
|
556
|
+
) {
|
|
557
|
+
return null
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return {
|
|
561
|
+
key,
|
|
562
|
+
type,
|
|
563
|
+
}
|
|
564
|
+
} catch {
|
|
565
|
+
return null
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** 获取自定义上传图标地址。
|
|
570
|
+
* @param iconInfo 解析后的图标信息。
|
|
571
|
+
* @returns 自定义上传图标的 Drive cover 地址。
|
|
572
|
+
*/
|
|
573
|
+
function getCustomIconUrl(iconInfo: ChatbotIconInfo): string {
|
|
574
|
+
const { type, key } = iconInfo
|
|
575
|
+
if (type !== ChatbotIconType.CustomIcon || !key) {
|
|
576
|
+
return ''
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return getDriveImageUrl(key, {
|
|
580
|
+
width: IMG_DEFAULT_SIZE,
|
|
581
|
+
height: IMG_DEFAULT_SIZE,
|
|
582
|
+
policy: 'allow_up',
|
|
583
|
+
fallback_source: 1,
|
|
584
|
+
})
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
interface CoverOptions {
|
|
588
|
+
width?: number
|
|
589
|
+
height?: number
|
|
590
|
+
policy?: CoverPolicy
|
|
591
|
+
fallback_source?: number
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** 序列化 URL 查询参数,跳过空值。
|
|
595
|
+
* @param params 查询参数对象。
|
|
596
|
+
* @returns 带问号的查询字符串;没有有效参数时返回空字符串。
|
|
597
|
+
*/
|
|
598
|
+
function formatQuery(
|
|
599
|
+
params: Record<string, string | number | undefined>,
|
|
600
|
+
): string {
|
|
601
|
+
const searchParams = new URLSearchParams()
|
|
602
|
+
|
|
603
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
604
|
+
if (value === undefined || value === '') {
|
|
605
|
+
return
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
searchParams.set(key, String(value))
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
const queryString = searchParams.toString()
|
|
612
|
+
return queryString ? `?${queryString}` : ''
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** 按 bear-web 规则生成 Base Chatbot 自定义图标 cover 地址。
|
|
616
|
+
* @param fileToken 自定义上传图标 token。
|
|
617
|
+
* @param coverOptions cover 裁剪参数。
|
|
618
|
+
* @returns Drive cover 下载地址。
|
|
619
|
+
*/
|
|
620
|
+
function getDriveImageUrl(
|
|
621
|
+
fileToken: string,
|
|
622
|
+
coverOptions: CoverOptions,
|
|
623
|
+
): string {
|
|
624
|
+
if (!fileToken) {
|
|
625
|
+
return ''
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const { driveStreamOrigin } = getBaseDatabaseEndpointConfig()
|
|
629
|
+
const baseUrl =
|
|
630
|
+
`${driveStreamOrigin}/space/api/box/stream/download/v2/cover/` +
|
|
631
|
+
`${encodeURIComponent(fileToken)}/`
|
|
632
|
+
|
|
633
|
+
return (
|
|
634
|
+
baseUrl +
|
|
635
|
+
formatQuery({
|
|
636
|
+
mount_node_token: fileToken,
|
|
637
|
+
mount_point: 'base_chatbot',
|
|
638
|
+
width: coverOptions.width,
|
|
639
|
+
height: coverOptions.height,
|
|
640
|
+
policy: coverOptions.policy,
|
|
641
|
+
fallback_source: coverOptions.fallback_source,
|
|
642
|
+
})
|
|
643
|
+
)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/** 带重试地请求自定义图标资源。
|
|
647
|
+
* @param url 图标资源地址。
|
|
648
|
+
* @param maxRetry 最大重试次数。
|
|
649
|
+
* @param retryInterval 初始重试间隔。
|
|
650
|
+
* @returns 图标资源响应。
|
|
651
|
+
*/
|
|
652
|
+
async function fetchCoverWithRetry(
|
|
653
|
+
url: string,
|
|
654
|
+
maxRetry = 6,
|
|
655
|
+
retryInterval = RETRY_INTERVAL_TIME,
|
|
656
|
+
): Promise<Response> {
|
|
657
|
+
let retryTime = 0
|
|
658
|
+
|
|
659
|
+
async function doFetch(interval: number): Promise<Response> {
|
|
660
|
+
const response = await fetch(url, { credentials: 'include' })
|
|
661
|
+
const { status } = response
|
|
662
|
+
|
|
663
|
+
if (status === SUCCESS_STATUS) {
|
|
664
|
+
return response
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (STATUS_TO_RETRY.includes(status) && retryTime < maxRetry) {
|
|
668
|
+
retryTime += 1
|
|
669
|
+
await new Promise((resolve) => {
|
|
670
|
+
setTimeout(resolve, interval)
|
|
671
|
+
})
|
|
672
|
+
return doFetch(retryTime === 1 ? interval : interval * 2)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
throw new Error(`Fetch cover failed with status ${status}`)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return doFetch(retryInterval)
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** 获取自定义图标 blob 地址,失败时降级获取原 cover。
|
|
682
|
+
* @param token 自定义上传图标 token。
|
|
683
|
+
* @returns 可供 img 使用的 blob URL。
|
|
684
|
+
*/
|
|
685
|
+
async function fetchCoverWithFallback(token: string): Promise<string> {
|
|
686
|
+
try {
|
|
687
|
+
const coverUrl = getCustomIconUrl({
|
|
688
|
+
key: token,
|
|
689
|
+
type: ChatbotIconType.CustomIcon,
|
|
690
|
+
})
|
|
691
|
+
const coverRes = await fetchCoverWithRetry(coverUrl, 2, RETRY_INTERVAL_TIME)
|
|
692
|
+
const blob = await coverRes.blob()
|
|
693
|
+
return URL.createObjectURL(blob)
|
|
694
|
+
} catch {
|
|
695
|
+
const imageUrl = getDriveImageUrl(token, {
|
|
696
|
+
policy: 'allow_up',
|
|
697
|
+
fallback_source: 1,
|
|
698
|
+
})
|
|
699
|
+
const imgRes = await fetchCoverWithRetry(imageUrl, 3, RETRY_INTERVAL_TIME)
|
|
700
|
+
const imgBlob = await imgRes.blob()
|
|
701
|
+
return URL.createObjectURL(imgBlob)
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/** 管理自定义图标 blob URL 缓存,复用并发请求。
|
|
706
|
+
* @returns 自定义图标缓存管理器实例。
|
|
707
|
+
*/
|
|
708
|
+
class ChatbotIconBlobUrlCache {
|
|
709
|
+
private readonly blobUrlCache = new Map<string, string>()
|
|
710
|
+
|
|
711
|
+
private readonly fetchingPromiseCache = new Map<string, Promise<string>>()
|
|
712
|
+
|
|
713
|
+
/** 获取自定义上传图标 blob URL。
|
|
714
|
+
* @param token 自定义上传图标 token。
|
|
715
|
+
* @returns 可供 img 使用的 blob URL。
|
|
716
|
+
*/
|
|
717
|
+
fetch(token: string): Promise<string> {
|
|
718
|
+
const cachedBlobUrl = this.blobUrlCache.get(token)
|
|
719
|
+
if (cachedBlobUrl) {
|
|
720
|
+
return Promise.resolve(cachedBlobUrl)
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const fetchingPromise = this.fetchingPromiseCache.get(token)
|
|
724
|
+
if (fetchingPromise) {
|
|
725
|
+
return fetchingPromise
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const promise = fetchCoverWithFallback(token)
|
|
729
|
+
.then((blobUrl) => {
|
|
730
|
+
this.blobUrlCache.set(token, blobUrl)
|
|
731
|
+
return blobUrl
|
|
732
|
+
})
|
|
733
|
+
.finally(() => {
|
|
734
|
+
this.fetchingPromiseCache.delete(token)
|
|
735
|
+
})
|
|
736
|
+
|
|
737
|
+
this.fetchingPromiseCache.set(token, promise)
|
|
738
|
+
return promise
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/** 读取已缓存的自定义上传图标 blob URL。
|
|
742
|
+
* @param token 自定义上传图标 token。
|
|
743
|
+
* @returns 已缓存的 blob URL。
|
|
744
|
+
*/
|
|
745
|
+
get(token: string): string | undefined {
|
|
746
|
+
return this.blobUrlCache.get(token)
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/** 清理自定义上传图标缓存。
|
|
750
|
+
* @param token 指定 token;不传则清空全部缓存。
|
|
751
|
+
*/
|
|
752
|
+
clear(token?: string): void {
|
|
753
|
+
if (token) {
|
|
754
|
+
const blobUrl = this.blobUrlCache.get(token)
|
|
755
|
+
if (blobUrl) {
|
|
756
|
+
URL.revokeObjectURL(blobUrl)
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
this.blobUrlCache.delete(token)
|
|
760
|
+
this.fetchingPromiseCache.delete(token)
|
|
761
|
+
return
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
this.blobUrlCache.forEach((blobUrl) => {
|
|
765
|
+
URL.revokeObjectURL(blobUrl)
|
|
766
|
+
})
|
|
767
|
+
this.blobUrlCache.clear()
|
|
768
|
+
this.fetchingPromiseCache.clear()
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const chatbotIconBlobUrlManager = new ChatbotIconBlobUrlCache()
|
|
773
|
+
|
|
774
|
+
/** 根据 iconInfo 获取 chatbot 图标地址。
|
|
775
|
+
* @param iconStr iconInfo JSON 字符串。
|
|
776
|
+
* @returns 可用于 img 的图标地址。
|
|
777
|
+
*/
|
|
778
|
+
function getChatbotIconUrl(iconStr: string): string {
|
|
779
|
+
const iconInfo = parseChatbotIconInfo(iconStr)
|
|
780
|
+
if (!iconInfo) {
|
|
781
|
+
return getDefaultSystemIconUrl()
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const customIconUrl = getCustomIconUrl(iconInfo)
|
|
785
|
+
if (customIconUrl) {
|
|
786
|
+
return customIconUrl
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (
|
|
790
|
+
iconInfo.type === ChatbotIconType.SystemIcon &&
|
|
791
|
+
(systemIconKeyList.includes(iconInfo.key) ||
|
|
792
|
+
oldSystemIconKey.includes(iconInfo.key))
|
|
793
|
+
) {
|
|
794
|
+
return getSystemIconUrl(iconInfo.key)
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return getDefaultSystemIconUrl()
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/** 渲染 chatbot 图标。
|
|
801
|
+
* @param props.iconInfo chatbot iconInfo JSON 字符串。
|
|
802
|
+
* @param props.name chatbot 名称,用于图片 alt。
|
|
803
|
+
* @param props.className 图标容器附加类名。
|
|
804
|
+
* @param props.size 图标尺寸。
|
|
805
|
+
* @returns chatbot 图标节点。
|
|
806
|
+
*/
|
|
807
|
+
const ChatbotIcon = memo(
|
|
808
|
+
({ iconInfo, name, className, size = 18 }: ChatbotIconProps) => {
|
|
809
|
+
const style = { width: size, height: size }
|
|
810
|
+
const [hasError, setHasError] = useState(false)
|
|
811
|
+
const [iconBlobUrl, setIconBlobUrl] = useState('')
|
|
812
|
+
|
|
813
|
+
useEffect(() => {
|
|
814
|
+
let isCurrentRequest = true
|
|
815
|
+
setHasError(false)
|
|
816
|
+
if (!iconInfo) {
|
|
817
|
+
setIconBlobUrl(getDefaultSystemIconUrl())
|
|
818
|
+
return () => {
|
|
819
|
+
isCurrentRequest = false
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const iconInfoObj = parseChatbotIconInfo(iconInfo)
|
|
824
|
+
if (iconInfoObj?.type === ChatbotIconType.CustomIcon) {
|
|
825
|
+
chatbotIconBlobUrlManager
|
|
826
|
+
.fetch(iconInfoObj.key)
|
|
827
|
+
.then((coverUrl) => {
|
|
828
|
+
if (!isCurrentRequest) {
|
|
829
|
+
return
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
setIconBlobUrl(coverUrl)
|
|
833
|
+
})
|
|
834
|
+
.catch(() => {
|
|
835
|
+
if (!isCurrentRequest) {
|
|
836
|
+
return
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
setHasError(true)
|
|
840
|
+
setIconBlobUrl(getDefaultSystemIconUrl())
|
|
841
|
+
})
|
|
842
|
+
return () => {
|
|
843
|
+
isCurrentRequest = false
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
setIconBlobUrl(getChatbotIconUrl(iconInfo))
|
|
848
|
+
return () => {
|
|
849
|
+
isCurrentRequest = false
|
|
850
|
+
}
|
|
851
|
+
}, [iconInfo])
|
|
852
|
+
|
|
853
|
+
if (!iconBlobUrl) {
|
|
854
|
+
return null
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
return (
|
|
858
|
+
<span
|
|
859
|
+
className={`m-0 inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full p-0 ${className ?? ''}`}
|
|
860
|
+
style={style}
|
|
861
|
+
>
|
|
862
|
+
<img
|
|
863
|
+
className="h-full w-full object-cover"
|
|
864
|
+
src={hasError ? getDefaultSystemIconUrl() : iconBlobUrl}
|
|
865
|
+
alt={name}
|
|
866
|
+
onError={() => setHasError(true)}
|
|
867
|
+
/>
|
|
868
|
+
</span>
|
|
869
|
+
)
|
|
870
|
+
},
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
ChatbotIcon.displayName = 'ChatbotIcon'
|