@lark-base-open/base-database-cli 1.1.1 → 1.3.8

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.
Files changed (199) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/LICENSE +6 -0
  3. package/README.md +47 -27
  4. package/dist/ai-functions.d.ts +53 -0
  5. package/dist/ai-functions.d.ts.map +1 -0
  6. package/dist/ai-functions.js +436 -0
  7. package/dist/ai-functions.js.map +1 -0
  8. package/dist/archive.d.ts +21 -0
  9. package/dist/archive.d.ts.map +1 -1
  10. package/dist/archive.js +69 -10
  11. package/dist/archive.js.map +1 -1
  12. package/dist/auth.d.ts +5 -0
  13. package/dist/auth.d.ts.map +1 -1
  14. package/dist/auth.js +26 -3
  15. package/dist/auth.js.map +1 -1
  16. package/dist/check.d.ts +74 -0
  17. package/dist/check.d.ts.map +1 -0
  18. package/dist/check.js +283 -0
  19. package/dist/check.js.map +1 -0
  20. package/dist/dependency-policy.d.ts +10 -0
  21. package/dist/dependency-policy.d.ts.map +1 -0
  22. package/dist/dependency-policy.js +162 -0
  23. package/dist/dependency-policy.js.map +1 -0
  24. package/dist/deploy.d.ts +5 -0
  25. package/dist/deploy.d.ts.map +1 -1
  26. package/dist/deploy.js +141 -176
  27. package/dist/deploy.js.map +1 -1
  28. package/dist/environment-variables.d.ts +52 -0
  29. package/dist/environment-variables.d.ts.map +1 -0
  30. package/dist/environment-variables.js +563 -0
  31. package/dist/environment-variables.js.map +1 -0
  32. package/dist/environment.d.ts +1 -1
  33. package/dist/environment.js +1 -1
  34. package/dist/environment.js.map +1 -1
  35. package/dist/function-input-types.d.ts +10 -0
  36. package/dist/function-input-types.d.ts.map +1 -0
  37. package/dist/function-input-types.js +160 -0
  38. package/dist/function-input-types.js.map +1 -0
  39. package/dist/function-schema.d.ts.map +1 -1
  40. package/dist/function-schema.js +89 -36
  41. package/dist/function-schema.js.map +1 -1
  42. package/dist/init-field-retry.d.ts +17 -0
  43. package/dist/init-field-retry.d.ts.map +1 -0
  44. package/dist/init-field-retry.js +127 -0
  45. package/dist/init-field-retry.js.map +1 -0
  46. package/dist/init-journal.d.ts +98 -0
  47. package/dist/init-journal.d.ts.map +1 -0
  48. package/dist/init-journal.js +492 -0
  49. package/dist/init-journal.js.map +1 -0
  50. package/dist/init-schema.d.ts +8 -0
  51. package/dist/init-schema.d.ts.map +1 -0
  52. package/dist/init-schema.js +423 -0
  53. package/dist/init-schema.js.map +1 -0
  54. package/dist/init.d.ts +15 -1
  55. package/dist/init.d.ts.map +1 -1
  56. package/dist/init.js +884 -261
  57. package/dist/init.js.map +1 -1
  58. package/dist/lark-cli-error.d.ts +47 -0
  59. package/dist/lark-cli-error.d.ts.map +1 -0
  60. package/dist/lark-cli-error.js +201 -0
  61. package/dist/lark-cli-error.js.map +1 -0
  62. package/dist/lark-cli.d.ts +1 -1
  63. package/dist/lark-cli.d.ts.map +1 -1
  64. package/dist/lark-cli.js +78 -18
  65. package/dist/lark-cli.js.map +1 -1
  66. package/dist/mutation-schema.d.ts +146 -0
  67. package/dist/mutation-schema.d.ts.map +1 -0
  68. package/dist/mutation-schema.js +153 -0
  69. package/dist/mutation-schema.js.map +1 -0
  70. package/dist/mutation.d.ts +10 -0
  71. package/dist/mutation.d.ts.map +1 -0
  72. package/dist/mutation.js +131 -0
  73. package/dist/mutation.js.map +1 -0
  74. package/dist/npm.d.ts +1 -0
  75. package/dist/npm.d.ts.map +1 -1
  76. package/dist/npm.js +2 -2
  77. package/dist/npm.js.map +1 -1
  78. package/dist/preview-receipt.d.ts.map +1 -1
  79. package/dist/preview-receipt.js +15 -5
  80. package/dist/preview-receipt.js.map +1 -1
  81. package/dist/process.d.ts +3 -1
  82. package/dist/process.d.ts.map +1 -1
  83. package/dist/process.js +16 -4
  84. package/dist/process.js.map +1 -1
  85. package/dist/program.d.ts.map +1 -1
  86. package/dist/program.js +246 -42
  87. package/dist/program.js.map +1 -1
  88. package/dist/project-build.d.ts +25 -0
  89. package/dist/project-build.d.ts.map +1 -0
  90. package/dist/project-build.js +229 -0
  91. package/dist/project-build.js.map +1 -0
  92. package/dist/project-context.d.ts +344 -0
  93. package/dist/project-context.d.ts.map +1 -0
  94. package/dist/project-context.js +485 -0
  95. package/dist/project-context.js.map +1 -0
  96. package/dist/project-layout.d.ts +5 -0
  97. package/dist/project-layout.d.ts.map +1 -0
  98. package/dist/project-layout.js +24 -0
  99. package/dist/project-layout.js.map +1 -0
  100. package/dist/public-sql-error.d.ts +25 -0
  101. package/dist/public-sql-error.d.ts.map +1 -0
  102. package/dist/public-sql-error.js +65 -0
  103. package/dist/public-sql-error.js.map +1 -0
  104. package/dist/pull.d.ts +1 -0
  105. package/dist/pull.d.ts.map +1 -1
  106. package/dist/pull.js +67 -15
  107. package/dist/pull.js.map +1 -1
  108. package/dist/site-readme.d.ts +3 -0
  109. package/dist/site-readme.d.ts.map +1 -0
  110. package/dist/site-readme.js +83 -0
  111. package/dist/site-readme.js.map +1 -0
  112. package/dist/workspace-fingerprint.d.ts +21 -1
  113. package/dist/workspace-fingerprint.d.ts.map +1 -1
  114. package/dist/workspace-fingerprint.js +81 -2
  115. package/dist/workspace-fingerprint.js.map +1 -1
  116. package/package.json +2 -1
  117. package/skills/lark-base-database/SKILL.md +86 -52
  118. package/skills/lark-base-database/references/ai-function.md +479 -0
  119. package/skills/lark-base-database/references/base-capability-routing.md +7 -7
  120. package/skills/lark-base-database/references/base-sql-errors.generated.md +403 -0
  121. package/skills/lark-base-database/references/database-function-limitations.md +29 -0
  122. package/skills/lark-base-database/references/database-function.md +323 -0
  123. package/skills/lark-base-database/references/development.md +166 -33
  124. package/skills/lark-base-database/references/environment-variables.md +55 -0
  125. package/skills/lark-base-database/references/faas.md +46 -53
  126. package/skills/lark-base-database/references/file-transfer.md +25 -25
  127. package/skills/lark-base-database/references/frontend-design-guide.md +151 -65
  128. package/skills/lark-base-database/references/mutations.md +295 -0
  129. package/skills/lark-base-database/references/notifications.md +9 -9
  130. package/skills/lark-base-database/references/permission-design.md +24 -24
  131. package/skills/lark-base-database/references/release.md +57 -30
  132. package/skills/lark-base-database/references/style-guide.md +10 -67
  133. package/skills/lark-base-database/references/verification.md +19 -13
  134. package/skills/lark-base-database/references/workspace.md +57 -17
  135. package/templates/database-starter/README.md +34 -4
  136. package/templates/database-starter/database-functions/README.md +8 -0
  137. package/templates/database-starter/gitignore +7 -0
  138. package/templates/database-starter/mutations/README.md +19 -0
  139. package/templates/database-starter/page/README.md +25 -19
  140. package/templates/database-starter/page/package-lock.json +8 -16
  141. package/templates/database-starter/page/package.json +2 -3
  142. package/templates/database-starter/page/src/App.tsx +23 -3
  143. package/templates/database-starter/page/src/components/UserSelectorEditor/index.tsx +32 -4
  144. package/templates/database-starter/page/src/components/UserSelectorPanel/index.tsx +104 -34
  145. package/templates/database-starter/page/src/components/UserSelectorPanel/user-search.test.ts +55 -11
  146. package/templates/database-starter/page/src/components/UserSelectorPanel/user-search.ts +68 -25
  147. package/templates/database-starter/page/src/components/database-runtime-ready.tsx +1 -1
  148. package/templates/database-starter/page/src/components/database-user-menu/index.tsx +4 -7
  149. package/templates/database-starter/page/src/components/feedback/page-state.tsx +40 -5
  150. package/templates/database-starter/page/src/components/floating-button/assets/edit-active.svg +3 -0
  151. package/templates/database-starter/page/src/components/floating-button/assets/edit-default.svg +3 -0
  152. package/templates/database-starter/page/src/components/floating-button/assets/toast-close.svg +3 -0
  153. package/templates/database-starter/page/src/components/floating-button/assets/toast-info.svg +5 -0
  154. package/templates/database-starter/page/src/components/floating-button/assets/tooltip-caret.svg +3 -0
  155. package/templates/database-starter/page/src/components/floating-button/index.tsx +193 -113
  156. package/templates/database-starter/page/src/components/floating-button/utils/project-meta.test.ts +107 -37
  157. package/templates/database-starter/page/src/components/floating-button/utils/project-meta.ts +13 -50
  158. package/templates/database-starter/page/src/components/ui-language.test.tsx +101 -0
  159. package/templates/database-starter/page/src/examples/ai-function/ImageTaskOperation.tsx +202 -0
  160. package/templates/database-starter/page/src/examples/ai-function/README.md +14 -0
  161. package/templates/database-starter/page/src/examples/ai-function/TextStreamOperation.tsx +184 -0
  162. package/templates/database-starter/page/src/examples/ai-function/ai-copy.ts +173 -0
  163. package/templates/database-starter/page/src/examples/ai-function/ai-delta.test.ts +30 -0
  164. package/templates/database-starter/page/src/examples/ai-function/ai-delta.ts +26 -0
  165. package/templates/database-starter/page/src/examples/ai-function/ai-language.test.tsx +86 -0
  166. package/templates/database-starter/page/src/examples/ai-function/ai-media-panel.test.ts +201 -0
  167. package/templates/database-starter/page/src/examples/ai-function/ai-media-panel.tsx +382 -0
  168. package/templates/database-starter/page/src/lib/attachment-image.tsx +144 -0
  169. package/templates/database-starter/page/src/lib/database-function-inputs.generated.ts +4 -0
  170. package/templates/database-starter/page/src/lib/database-runtime-attachment.test.ts +180 -76
  171. package/templates/database-starter/page/src/lib/database-runtime.test.ts +130 -10
  172. package/templates/database-starter/page/src/lib/database-runtime.ts +484 -51
  173. package/templates/database-starter/page/src/lib/date-time.ts +61 -0
  174. package/templates/database-starter/page/src/lib/ui-language.ts +86 -0
  175. package/templates/database-starter/page/src/main.tsx +1 -1
  176. package/templates/database-starter/page/tsconfig.json +2 -2
  177. package/templates/database-starter/page/vite.config.ts +1 -2
  178. package/templates/database-starter/prettierignore +1 -1
  179. package/templates/database-starter/server-functions/README.md +2 -2
  180. package/templates/database-starter/server-functions/package-lock.json +4 -4
  181. package/templates/database-starter/server-functions/package.json +1 -1
  182. package/dist/agent-guide.d.ts +0 -19
  183. package/dist/agent-guide.d.ts.map +0 -1
  184. package/dist/agent-guide.js +0 -31
  185. package/dist/agent-guide.js.map +0 -1
  186. package/dist/database-skill.d.ts +0 -24
  187. package/dist/database-skill.d.ts.map +0 -1
  188. package/dist/database-skill.js +0 -148
  189. package/dist/database-skill.js.map +0 -1
  190. package/skills/lark-base-database/references/base-sql-unsupported.md +0 -42
  191. package/skills/lark-base-database/references/database-capability-skill.md +0 -40
  192. package/skills/lark-base-database/references/sql-function.md +0 -289
  193. package/templates/database-capability/SKILL.md.tmpl +0 -58
  194. package/templates/database-starter/page/src/components/floating-button/chatbot-entry.test.ts +0 -23
  195. package/templates/database-starter/page/src/components/floating-button/chatbot-entry.tsx +0 -873
  196. package/templates/database-starter/page/src/components/floating-button/local-chat-panel.test.ts +0 -25
  197. package/templates/database-starter/page/src/components/floating-button/local-chat-panel.tsx +0 -197
  198. package/templates/database-starter/page/src/components/floating-button/utils/env.ts +0 -18
  199. package/templates/database-starter/sql/README.md +0 -8
@@ -1,873 +0,0 @@
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'