@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.
Files changed (149) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +169 -0
  3. package/changelog/CHANGELOG-0.x.md +103 -0
  4. package/config-schema.json +28 -0
  5. package/dist/agent-guide.d.ts +18 -0
  6. package/dist/agent-guide.d.ts.map +1 -0
  7. package/dist/agent-guide.js +30 -0
  8. package/dist/agent-guide.js.map +1 -0
  9. package/dist/archive.d.ts +16 -0
  10. package/dist/archive.d.ts.map +1 -0
  11. package/dist/archive.js +162 -0
  12. package/dist/archive.js.map +1 -0
  13. package/dist/assets.d.ts +4 -0
  14. package/dist/assets.d.ts.map +1 -0
  15. package/dist/assets.js +6 -0
  16. package/dist/assets.js.map +1 -0
  17. package/dist/auth.d.ts +17 -0
  18. package/dist/auth.d.ts.map +1 -0
  19. package/dist/auth.js +63 -0
  20. package/dist/auth.js.map +1 -0
  21. package/dist/cli.d.ts +3 -0
  22. package/dist/cli.d.ts.map +1 -0
  23. package/dist/cli.js +15 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/config.d.ts +5 -0
  26. package/dist/config.d.ts.map +1 -0
  27. package/dist/config.js +50 -0
  28. package/dist/config.js.map +1 -0
  29. package/dist/database-skill.d.ts +22 -0
  30. package/dist/database-skill.d.ts.map +1 -0
  31. package/dist/database-skill.js +135 -0
  32. package/dist/database-skill.js.map +1 -0
  33. package/dist/deploy.d.ts +28 -0
  34. package/dist/deploy.d.ts.map +1 -0
  35. package/dist/deploy.js +476 -0
  36. package/dist/deploy.js.map +1 -0
  37. package/dist/environment.d.ts +12 -0
  38. package/dist/environment.d.ts.map +1 -0
  39. package/dist/environment.js +27 -0
  40. package/dist/environment.js.map +1 -0
  41. package/dist/function-schema.d.ts +3 -0
  42. package/dist/function-schema.d.ts.map +1 -0
  43. package/dist/function-schema.js +310 -0
  44. package/dist/function-schema.js.map +1 -0
  45. package/dist/init.d.ts +77 -0
  46. package/dist/init.d.ts.map +1 -0
  47. package/dist/init.js +418 -0
  48. package/dist/init.js.map +1 -0
  49. package/dist/lark-cli.d.ts +8 -0
  50. package/dist/lark-cli.d.ts.map +1 -0
  51. package/dist/lark-cli.js +228 -0
  52. package/dist/lark-cli.js.map +1 -0
  53. package/dist/npm.d.ts +13 -0
  54. package/dist/npm.d.ts.map +1 -0
  55. package/dist/npm.js +93 -0
  56. package/dist/npm.js.map +1 -0
  57. package/dist/page-title.d.ts +3 -0
  58. package/dist/page-title.d.ts.map +1 -0
  59. package/dist/page-title.js +47 -0
  60. package/dist/page-title.js.map +1 -0
  61. package/dist/preview-receipt.d.ts +23 -0
  62. package/dist/preview-receipt.d.ts.map +1 -0
  63. package/dist/preview-receipt.js +133 -0
  64. package/dist/preview-receipt.js.map +1 -0
  65. package/dist/process.d.ts +17 -0
  66. package/dist/process.d.ts.map +1 -0
  67. package/dist/process.js +53 -0
  68. package/dist/process.js.map +1 -0
  69. package/dist/program.d.ts +12 -0
  70. package/dist/program.d.ts.map +1 -0
  71. package/dist/program.js +204 -0
  72. package/dist/program.js.map +1 -0
  73. package/dist/pull.d.ts +18 -0
  74. package/dist/pull.d.ts.map +1 -0
  75. package/dist/pull.js +324 -0
  76. package/dist/pull.js.map +1 -0
  77. package/dist/skill.d.ts +7 -0
  78. package/dist/skill.d.ts.map +1 -0
  79. package/dist/skill.js +28 -0
  80. package/dist/skill.js.map +1 -0
  81. package/dist/types.d.ts +32 -0
  82. package/dist/types.d.ts.map +1 -0
  83. package/dist/types.js +2 -0
  84. package/dist/types.js.map +1 -0
  85. package/dist/workspace-fingerprint.d.ts +8 -0
  86. package/dist/workspace-fingerprint.d.ts.map +1 -0
  87. package/dist/workspace-fingerprint.js +64 -0
  88. package/dist/workspace-fingerprint.js.map +1 -0
  89. package/package.json +54 -0
  90. package/skills/lark-base-database/SKILL.md +307 -0
  91. package/skills/lark-base-database/references/base-capability-routing.md +81 -0
  92. package/skills/lark-base-database/references/base-sql-unsupported.md +42 -0
  93. package/skills/lark-base-database/references/database-capability-skill.md +24 -0
  94. package/skills/lark-base-database/references/development.md +642 -0
  95. package/skills/lark-base-database/references/file-transfer.md +275 -0
  96. package/skills/lark-base-database/references/permission-design.md +138 -0
  97. package/skills/lark-base-database/references/release.md +81 -0
  98. package/skills/lark-base-database/references/sql-function.md +276 -0
  99. package/skills/lark-base-database/references/style-guide.md +88 -0
  100. package/templates/database-capability/SKILL.md.tmpl +41 -0
  101. package/templates/database-starter/README.md +11 -0
  102. package/templates/database-starter/gitignore +3 -0
  103. package/templates/database-starter/page/.prettierignore +4 -0
  104. package/templates/database-starter/page/.prettierrc.json +7 -0
  105. package/templates/database-starter/page/README.md +29 -0
  106. package/templates/database-starter/page/index.html +24 -0
  107. package/templates/database-starter/page/package-lock.json +2675 -0
  108. package/templates/database-starter/page/package.json +44 -0
  109. package/templates/database-starter/page/public/favicon.png +0 -0
  110. package/templates/database-starter/page/src/App.tsx +18 -0
  111. package/templates/database-starter/page/src/components/UserSelectorEditor/index.tsx +267 -0
  112. package/templates/database-starter/page/src/components/UserSelectorPanel/index.css +32 -0
  113. package/templates/database-starter/page/src/components/UserSelectorPanel/index.test.ts +24 -0
  114. package/templates/database-starter/page/src/components/UserSelectorPanel/index.tsx +478 -0
  115. package/templates/database-starter/page/src/components/UserSelectorPanel/user-search.test.ts +175 -0
  116. package/templates/database-starter/page/src/components/UserSelectorPanel/user-search.ts +201 -0
  117. package/templates/database-starter/page/src/components/database-runtime-ready.tsx +21 -0
  118. package/templates/database-starter/page/src/components/database-user-menu/index.tsx +238 -0
  119. package/templates/database-starter/page/src/components/feedback/page-state.tsx +66 -0
  120. package/templates/database-starter/page/src/components/floating-button/chatbot-entry.test.ts +23 -0
  121. package/templates/database-starter/page/src/components/floating-button/chatbot-entry.tsx +873 -0
  122. package/templates/database-starter/page/src/components/floating-button/index.tsx +196 -0
  123. package/templates/database-starter/page/src/components/floating-button/local-chat-panel.test.ts +25 -0
  124. package/templates/database-starter/page/src/components/floating-button/local-chat-panel.tsx +197 -0
  125. package/templates/database-starter/page/src/components/floating-button/utils/current-user.ts +56 -0
  126. package/templates/database-starter/page/src/components/floating-button/utils/env.ts +18 -0
  127. package/templates/database-starter/page/src/components/floating-button/utils/project-meta.test.ts +79 -0
  128. package/templates/database-starter/page/src/components/floating-button/utils/project-meta.ts +252 -0
  129. package/templates/database-starter/page/src/components/ui/button.tsx +56 -0
  130. package/templates/database-starter/page/src/index.css +67 -0
  131. package/templates/database-starter/page/src/lib/cn.test.ts +9 -0
  132. package/templates/database-starter/page/src/lib/cn.ts +7 -0
  133. package/templates/database-starter/page/src/lib/database-runtime-attachment.test.ts +168 -0
  134. package/templates/database-starter/page/src/lib/database-runtime-context.test.ts +111 -0
  135. package/templates/database-starter/page/src/lib/database-runtime.test.ts +103 -0
  136. package/templates/database-starter/page/src/lib/database-runtime.ts +389 -0
  137. package/templates/database-starter/page/src/main.tsx +34 -0
  138. package/templates/database-starter/page/src/vite-env.d.ts +1 -0
  139. package/templates/database-starter/page/tsconfig.json +23 -0
  140. package/templates/database-starter/page/vite.config.ts +13 -0
  141. package/templates/database-starter/prettierignore +1 -0
  142. package/templates/database-starter/server-functions/.prettierignore +3 -0
  143. package/templates/database-starter/server-functions/.prettierrc.json +7 -0
  144. package/templates/database-starter/server-functions/README.md +11 -0
  145. package/templates/database-starter/server-functions/package-lock.json +1687 -0
  146. package/templates/database-starter/server-functions/package.json +22 -0
  147. package/templates/database-starter/server-functions/scripts/run-tsc.mjs +50 -0
  148. package/templates/database-starter/server-functions/tsconfig.json +20 -0
  149. package/templates/database-starter/sql/README.md +8 -0
@@ -0,0 +1,196 @@
1
+ import { useCallback, useEffect, useState } from 'react'
2
+ import { fetchProjectMeta, type ProjectMeta } from './utils/project-meta'
3
+ import { ChatbotEntry, resolveChatbotDisplayName } from './chatbot-entry'
4
+ import {
5
+ getDatabaseContext,
6
+ resolveDatabaseContext,
7
+ siteRuntime,
8
+ } from '../../lib/database-runtime'
9
+
10
+ export type FloatingButtonLanguage = 'en' | 'zh'
11
+
12
+ interface FloatingButtonProps {
13
+ language?: FloatingButtonLanguage
14
+ showChatbotEntry?: boolean
15
+ }
16
+
17
+ interface FloatingButtonI18n {
18
+ editSite: string
19
+ }
20
+
21
+ const BASE_DATABASE_OVERVIEW_URL = 'https://feishu.cn/base/site/detail/overview'
22
+ const CHATBOT_SIDEBAR_DEFAULT_WIDTH = 420
23
+ const FLOATING_BUTTON_PAGE_GAP = 24
24
+ const EMPTY_PROJECT_META: ProjectMeta = {
25
+ baseBindings: [],
26
+ chatList: [],
27
+ description: '',
28
+ isOwner: false,
29
+ name: '',
30
+ organization: '',
31
+ }
32
+ const FLOATING_BUTTON_I18N: Record<FloatingButtonLanguage, FloatingButtonI18n> =
33
+ {
34
+ en: {
35
+ editSite: 'Edit application',
36
+ },
37
+ zh: {
38
+ editSite: '编辑应用',
39
+ },
40
+ }
41
+
42
+ /** 渲染 Base Database 悬浮按钮入口:编辑、Chatbot。
43
+ *
44
+ * 原来这里还有一个「三个点」菜单,承载当前账号 / 组织 / 退出登录。它已经下掉了:
45
+ * 悬浮控件是应用之外的一层浮层,用户不会去那里找账号。身份信息改由应用自己的导航
46
+ * 承载,见 `page/src/components/database-user-menu`。
47
+ *
48
+ * @param props.language 界面语言,支持 en 和 zh,默认 en。
49
+ * @param props.showChatbotEntry 是否显示 Chatbot 入口,默认 true。
50
+ * @returns 悬浮按钮组。
51
+ */
52
+ export function FloatingButton({
53
+ language = 'en',
54
+ showChatbotEntry = true,
55
+ }: FloatingButtonProps) {
56
+ const [projectMeta, setProjectMeta] = useState<ProjectMeta | null>(null)
57
+ const [isFloatingDataLoaded, setIsFloatingDataLoaded] = useState(false)
58
+ const [isChatbotOpen, setIsChatbotOpen] = useState(false)
59
+ const [chatbotSidebarWidth, setChatbotSidebarWidth] = useState(
60
+ CHATBOT_SIDEBAR_DEFAULT_WIDTH,
61
+ )
62
+ // 编辑只在线上给 owner。Preview 本身就只有 owner 进得来,isOwner 恒为 true,
63
+ // 靠它永远隐藏不掉,必须显式按产物环境判断。
64
+ const shouldShowEditButton =
65
+ projectMeta?.isOwner === true && getDatabaseContext().env === 'prod'
66
+ const shouldOffsetForChatbot = showChatbotEntry && isChatbotOpen
67
+ const shouldShowFloatingControls =
68
+ shouldShowEditButton || (showChatbotEntry && !isChatbotOpen)
69
+ const floatingButtonRight = shouldOffsetForChatbot
70
+ ? chatbotSidebarWidth + FLOATING_BUTTON_PAGE_GAP
71
+ : FLOATING_BUTTON_PAGE_GAP
72
+ const i18n = FLOATING_BUTTON_I18N[language]
73
+ const genericChatbotName = language === 'zh' ? '智能助手' : 'AI assistant'
74
+ const chatbotFallbackName = resolveChatbotDisplayName(
75
+ projectMeta?.name ?? '',
76
+ genericChatbotName,
77
+ )
78
+
79
+ useEffect(() => {
80
+ let isCurrentRequest = true
81
+ setIsFloatingDataLoaded(false)
82
+
83
+ /** 读取悬浮控件所需的项目资源数据。 */
84
+ async function loadFloatingData(): Promise<void> {
85
+ const nextProjectMeta = await fetchProjectMeta().catch(
86
+ () => EMPTY_PROJECT_META,
87
+ )
88
+
89
+ if (!isCurrentRequest) {
90
+ return
91
+ }
92
+
93
+ setProjectMeta(nextProjectMeta)
94
+ setIsFloatingDataLoaded(true)
95
+ }
96
+
97
+ void loadFloatingData()
98
+
99
+ return () => {
100
+ isCurrentRequest = false
101
+ }
102
+ }, [])
103
+
104
+ useEffect(() => {
105
+ if (showChatbotEntry) {
106
+ return
107
+ }
108
+
109
+ setIsChatbotOpen(false)
110
+ }, [showChatbotEntry])
111
+
112
+ const handleChatbotOpenChange = useCallback((nextIsOpen: boolean) => {
113
+ setIsChatbotOpen(nextIsOpen)
114
+ }, [])
115
+
116
+ // 编辑按钮走 site.v1 契约:应用发 request-edit,宿主切编辑态后广播 mode-changed。
117
+ // 应用被独立访问(无宿主)时事件无人消费,回落为打开 Site 管理页。
118
+ const requestEdit = useCallback(() => {
119
+ void siteRuntime.requestEdit().catch(async () => {
120
+ const { token } = await resolveDatabaseContext()
121
+ const url = new URL(BASE_DATABASE_OVERVIEW_URL)
122
+ url.searchParams.set('token', token)
123
+ openExternalUrl(url.toString())
124
+ })
125
+ }, [])
126
+
127
+ if (!isFloatingDataLoaded || !projectMeta) {
128
+ return null
129
+ }
130
+
131
+ return (
132
+ <div
133
+ className={`fixed bottom-10 flex items-center pr-0 ${shouldOffsetForChatbot ? 'z-[80]' : 'z-40'}`}
134
+ style={{ right: floatingButtonRight }}
135
+ >
136
+ <div
137
+ className={`${shouldShowFloatingControls ? 'relative z-[80] flex' : 'hidden'} items-center gap-1.5 rounded-full bg-white p-0.5 shadow-[0_4px_4px_rgba(31,35,41,0.06),0_6px_6px_rgba(31,35,41,0.04),0_8px_12px_rgba(31,35,41,0.04)]`}
138
+ data-floating-controls="true"
139
+ >
140
+ {shouldShowEditButton ? (
141
+ <button
142
+ type="button"
143
+ aria-label={i18n.editSite}
144
+ title={i18n.editSite}
145
+ className="flex size-12 cursor-pointer items-center justify-center rounded-full bg-transparent text-[#2b2f36] outline-none transition-colors hover:bg-[#1f2329]/5 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 active:bg-[#1f2329]/10"
146
+ onClick={requestEdit}
147
+ >
148
+ <EditIcon />
149
+ </button>
150
+ ) : null}
151
+
152
+ {showChatbotEntry ? (
153
+ <ChatbotEntry
154
+ chatbots={projectMeta.chatList}
155
+ baseId={projectMeta.baseBindings[0]?.baseToken}
156
+ className="shrink-0"
157
+ fallbackName={chatbotFallbackName}
158
+ onOpenChange={handleChatbotOpenChange}
159
+ onSidebarWidthChange={setChatbotSidebarWidth}
160
+ />
161
+ ) : null}
162
+ </div>
163
+ </div>
164
+ )
165
+ }
166
+
167
+ /** 使用新标签页打开外部链接。
168
+ * @param url 待打开的外部链接。
169
+ */
170
+ function openExternalUrl(url: string): void {
171
+ window.open(url, '_blank', 'noopener,noreferrer')
172
+ }
173
+
174
+ /** 编辑应用图标。
175
+ * @returns 铅笔图标节点。
176
+ */
177
+ function EditIcon() {
178
+ return (
179
+ <svg
180
+ aria-hidden="true"
181
+ width="20"
182
+ height="20"
183
+ viewBox="0 0 24 24"
184
+ fill="none"
185
+ stroke="currentColor"
186
+ strokeWidth="1.8"
187
+ strokeLinecap="round"
188
+ strokeLinejoin="round"
189
+ >
190
+ <path d="M12 20h9" />
191
+ <path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z" />
192
+ </svg>
193
+ )
194
+ }
195
+
196
+ export default FloatingButton
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { resolvePanelSessionKey } from './local-chat-panel'
3
+
4
+ describe('resolvePanelSessionKey', () => {
5
+ it('只由 chatbotId 决定会话身份', () => {
6
+ expect(resolvePanelSessionKey({ chatbotId: 'cb-1' })).toBe('cb-1')
7
+ expect(resolvePanelSessionKey({ chatbotId: 'cb-2' })).not.toBe(
8
+ resolvePanelSessionKey({ chatbotId: 'cb-1' }),
9
+ )
10
+ })
11
+
12
+ // 回归:拖拽侧边栏时父组件每帧重渲染,展示属性(含每次新建的 onClose)
13
+ // 若进入挂载依赖,面板会逐帧 unmount + mount 并重复拉取 history/list。
14
+ it('展示属性变化不改变会话身份', () => {
15
+ const base = resolvePanelSessionKey({ chatbotId: 'cb-1' })
16
+ const withOtherDisplayProps = resolvePanelSessionKey({
17
+ chatbotId: 'cb-1',
18
+ chatbotName: '换了名字',
19
+ chatbotIconUrl: 'https://example.com/other.png',
20
+ baseId: 'base-token',
21
+ onClose: () => undefined,
22
+ } as { chatbotId: string })
23
+ expect(withOtherDisplayProps).toBe(base)
24
+ })
25
+ })
@@ -0,0 +1,197 @@
1
+ import React, { useEffect, useRef, useState } from 'react'
2
+ import { getDatabaseContext } from '../../lib/database-runtime'
3
+
4
+ /** 面板 IIFE 产物暴露的全局挂载接口(base-site-chat-panel 包)。 */
5
+ interface ChatPanelMountProps {
6
+ projectId: string
7
+ chatbotId?: string
8
+ chatbotName?: string
9
+ chatbotCount?: number
10
+ /** chatbot 自己的图标地址,面板 header 与欢迎页使用。 */
11
+ chatbotIconUrl?: string
12
+ /** 绑定的 Base token,面板新建会话时通知服务端重置需要。 */
13
+ baseId?: string
14
+ /** 面板 header 的关闭按钮回调(Figma 95-195885 右上角功能区)。 */
15
+ onClose?: () => void
16
+ }
17
+
18
+ interface ChatPanelSdk {
19
+ mount: (container: Element, props: ChatPanelMountProps) => void
20
+ /** 就地更新展示属性,不重建会话。旧版本产物可能没有这个方法。 */
21
+ update?: (container: Element, props: ChatPanelMountProps) => void
22
+ unmount: (container: Element) => void
23
+ }
24
+
25
+ declare global {
26
+ interface Window {
27
+ __SITE_CHATBOT_SDK__?: ChatPanelSdk
28
+ }
29
+ }
30
+
31
+ /** 面板产物(base-site-chat-panel)的 CDN 目录。
32
+ *
33
+ * 面板包按「构建产物发 CDN + 应用静态引用」分发:产物一份托管在 Eden CDN 的
34
+ * base-site 团队空间,所有应用共享,应用 Artifact 不再携带 ~4MB 产物。
35
+ * 更新产物时上传到新的版本目录并同步修改此常量。 */
36
+ const PANEL_CDN_BASE =
37
+ 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lapz-lzhi/ljhwZthlaukjlkulzlp/base-site-chat-panel/0.0.1-alpha.45'
38
+
39
+ let sdkPromise: Promise<ChatPanelSdk | null> | null = null
40
+
41
+ /** 惰性加载面板产物。
42
+ *
43
+ * 面板自带 React 18 与全部 chatbot-sdk 依赖(隔离 bundle),应用只提供容器 DOM;
44
+ * 产物统一从 CDN 加载,所有应用共享同一份,与应用自身的托管路径无关。
45
+ */
46
+ function loadChatPanelSdk(): Promise<ChatPanelSdk | null> {
47
+ if (window.__SITE_CHATBOT_SDK__) {
48
+ return Promise.resolve(window.__SITE_CHATBOT_SDK__)
49
+ }
50
+ if (!sdkPromise) {
51
+ sdkPromise = new Promise((resolve) => {
52
+ const stylesheet = document.createElement('link')
53
+ stylesheet.rel = 'stylesheet'
54
+ stylesheet.href = `${PANEL_CDN_BASE}/panel.css`
55
+ document.head.append(stylesheet)
56
+
57
+ const script = document.createElement('script')
58
+ script.src = `${PANEL_CDN_BASE}/panel.js`
59
+ script.onload = () => resolve(window.__SITE_CHATBOT_SDK__ ?? null)
60
+ script.onerror = () => resolve(null)
61
+ document.head.append(script)
62
+ })
63
+ }
64
+ return sdkPromise
65
+ }
66
+
67
+ interface LocalChatPanelProps {
68
+ chatbotId: string
69
+ chatbotName: string
70
+ /** 应用一共绑定了几个 chatbot。面板据此决定名称右边要不要出切换三角。 */
71
+ chatbotCount?: number
72
+ /** 面板产物缺失或加载失败时的回落渲染(如旧版 agent 页 iframe)。 */
73
+ fallback?: React.ReactNode
74
+ /** chatbot 自己的图标地址(由 iconInfo 解析)。 */
75
+ chatbotIconUrl?: string
76
+ /** 绑定的 Base token(projectMeta.baseBindings[0].baseToken)。 */
77
+ baseId?: string
78
+ /** 关闭侧边栏。面板 header 的关闭按钮会调用它(Figma 95-195885)。 */
79
+ onClose?: () => void
80
+ }
81
+
82
+ /** 面板会话身份。只有它变化才需要卸载重挂面板。
83
+ *
84
+ * 名称、图标、baseId、onClose 属于展示属性,一律不参与 —— 把它们放进挂载
85
+ * effect 的依赖会让父组件每次重渲染都重建面板,会话历史随之重新拉取一遍。
86
+ * @param props 当前面板属性。
87
+ * @returns 用作挂载 effect 唯一依赖的会话身份字符串。
88
+ */
89
+ export function resolvePanelSessionKey(props: { chatbotId: string }): string {
90
+ return props.chatbotId
91
+ }
92
+
93
+ /** 应用内本地化对话面板:加载隔离 bundle 并挂载到容器。
94
+ *
95
+ * 产物来源:ai-friendly-base-site/engineering/base-site-chat-panel 的 dist/panel.{js,css},
96
+ * 构建后上传到 Eden CDN 的 base-site 团队空间。加载失败时按 fallback 渲染。
97
+ *
98
+ * 会话请求与轮询由面板经 site.v1 Runtime 走父页面同源代发;应用被独立访问
99
+ * (无宿主)时面板自行回落 BFF 纯文本模式,本组件不感知传输细节。
100
+ */
101
+ export function LocalChatPanel({
102
+ chatbotId,
103
+ chatbotName,
104
+ chatbotCount = 1,
105
+ chatbotIconUrl,
106
+ baseId,
107
+ fallback,
108
+ onClose,
109
+ }: LocalChatPanelProps) {
110
+ const containerRef = useRef<HTMLDivElement>(null)
111
+ const [loadFailed, setLoadFailed] = useState(false)
112
+ const sessionKey = resolvePanelSessionKey({ chatbotId })
113
+
114
+ // 面板挂载后靠 ref 读取最新展示属性。挂载 effect 只认 sessionKey,
115
+ // 展示属性的引用变化不会 unmount + mount 面板,会话历史也就不会重拉
116
+ // (典型触发:侧边栏拖拽时父组件每帧重渲染)。
117
+ const displayPropsRef = useRef({
118
+ chatbotName,
119
+ chatbotCount,
120
+ chatbotIconUrl,
121
+ baseId,
122
+ })
123
+ displayPropsRef.current = {
124
+ chatbotName,
125
+ chatbotCount,
126
+ chatbotIconUrl,
127
+ baseId,
128
+ }
129
+ const onCloseRef = useRef(onClose)
130
+ onCloseRef.current = onClose
131
+
132
+ useEffect(() => {
133
+ const container = containerRef.current
134
+ if (!container) {
135
+ return undefined
136
+ }
137
+
138
+ let disposed = false
139
+ void loadChatPanelSdk().then((sdk) => {
140
+ if (disposed) {
141
+ return
142
+ }
143
+ if (!sdk) {
144
+ setLoadFailed(true)
145
+ return
146
+ }
147
+ sdk.mount(container, {
148
+ projectId: getDatabaseContext().token,
149
+ chatbotId: sessionKey,
150
+ ...displayPropsRef.current,
151
+ onClose: () => onCloseRef.current?.(),
152
+ })
153
+ })
154
+
155
+ return () => {
156
+ disposed = true
157
+ window.__SITE_CHATBOT_SDK__?.unmount(container)
158
+ }
159
+ }, [sessionKey])
160
+
161
+ // 展示属性(名称、图标、baseId)晚到时就地更新,不重建会话。
162
+ useEffect(() => {
163
+ const container = containerRef.current
164
+ const sdk = window.__SITE_CHATBOT_SDK__
165
+ if (!container || !sdk?.update) {
166
+ return
167
+ }
168
+ sdk.update(container, {
169
+ projectId: getDatabaseContext().token,
170
+ chatbotId,
171
+ chatbotName,
172
+ chatbotCount,
173
+ chatbotIconUrl,
174
+ baseId,
175
+ onClose: () => onCloseRef.current?.(),
176
+ })
177
+ }, [chatbotId, chatbotName, chatbotCount, chatbotIconUrl, baseId])
178
+
179
+ if (loadFailed) {
180
+ return (
181
+ fallback ?? (
182
+ <div className="flex min-h-0 flex-1 items-center justify-center text-sm text-muted-foreground">
183
+ 对话面板加载失败,请刷新页面重试
184
+ </div>
185
+ )
186
+ )
187
+ }
188
+
189
+ // 面板根节点 .site-chat-root 是 flex:1,挂载容器必须自己是有界高度的 flex column,
190
+ // 否则消息列表按内容自然高度撑开、输入框被顶出视口。
191
+ return (
192
+ <div
193
+ ref={containerRef}
194
+ className="flex min-h-0 flex-1 flex-col overflow-hidden"
195
+ />
196
+ )
197
+ }
@@ -0,0 +1,56 @@
1
+ import {
2
+ fetch as siteRuntimeFetch,
3
+ getCurrentUser,
4
+ } from '../../../lib/database-runtime'
5
+
6
+ /** 浮层菜单展示用的当前登录用户信息。 */
7
+ export interface CurrentUser {
8
+ name: string
9
+ avatarUrl: string
10
+ /** 组织名。协议的 getCurrentUser 不含该字段,仅宿主用户接口回落路径提供。 */
11
+ tenantName: string
12
+ }
13
+
14
+ /** 读取当前登录用户的展示信息。
15
+ *
16
+ * 优先走协议内置 Runtime Function(function.runtime.getCurrentUser,Host 固定分发);
17
+ * 旧版宿主未实现时回落父页面代理的 /space/api/user/。两条路都不可用(如应用被
18
+ * 独立访问)时返回 null,菜单退化为只展示组织名。
19
+ */
20
+ export async function fetchCurrentUser(): Promise<CurrentUser | null> {
21
+ try {
22
+ // 内置 Runtime Function(对齐 MR 29):协议保证 name / avatarUrl 稳定存在。
23
+ const user = await getCurrentUser()
24
+ if (typeof user?.name === 'string' && user.name) {
25
+ return {
26
+ name: user.name,
27
+ avatarUrl: typeof user.avatarUrl === 'string' ? user.avatarUrl : '',
28
+ tenantName: '',
29
+ }
30
+ }
31
+ } catch {
32
+ // 继续走宿主用户接口回落
33
+ }
34
+ try {
35
+ const response = await siteRuntimeFetch('/space/api/user/', {
36
+ headers: { accept: 'application/json' },
37
+ })
38
+ if (!response.ok) {
39
+ return null
40
+ }
41
+ const body = (await response.json()) as {
42
+ data?: { name?: unknown; avatar_url?: unknown; tenant_name?: unknown }
43
+ }
44
+ const data = body.data
45
+ if (!data || typeof data.name !== 'string' || !data.name) {
46
+ return null
47
+ }
48
+ return {
49
+ name: data.name,
50
+ avatarUrl: typeof data.avatar_url === 'string' ? data.avatar_url : '',
51
+ tenantName: typeof data.tenant_name === 'string' ? data.tenant_name : '',
52
+ }
53
+ } catch {
54
+ return null
55
+ }
56
+ }
@@ -0,0 +1,18 @@
1
+ export interface BaseDatabaseEndpointConfig {
2
+ chatbotOrigin: string
3
+ customIconUri: string
4
+ driveStreamOrigin: string
5
+ }
6
+
7
+ // 仅剩 Chatbot 第二层 iframe 与其图标资源仍依赖这些外部域名,
8
+ // 业务 HTTP 一律经父页面代理,不再配置 OpenAPI 直连域名。
9
+ const ONLINE_ENDPOINT_CONFIG: BaseDatabaseEndpointConfig = {
10
+ chatbotOrigin: 'https://feishu.cn',
11
+ customIconUri: 'https://sf1-scmcdn-cn.feishucdn.com/obj/lark-reaction-cn/',
12
+ driveStreamOrigin: 'https://internal-api-drive-stream.feishu.cn',
13
+ }
14
+
15
+ /** 读取 Base Database 线上环境的外部服务域名配置。 */
16
+ export function getBaseDatabaseEndpointConfig(): BaseDatabaseEndpointConfig {
17
+ return ONLINE_ENDPOINT_CONFIG
18
+ }
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { parseProjectMetaResponse } from './project-meta'
3
+
4
+ function projectResponse(isOwner: unknown) {
5
+ return {
6
+ code: 0,
7
+ data: {
8
+ isOwner,
9
+ site: {
10
+ baseBindings: [],
11
+ chatbots: [],
12
+ description: 'Owner visibility test',
13
+ name: 'Owner visibility test',
14
+ },
15
+ },
16
+ }
17
+ }
18
+
19
+ describe('project owner visibility', () => {
20
+ it('uses the backend isOwner verdict', () => {
21
+ expect(parseProjectMetaResponse(projectResponse(true)).isOwner).toBe(true)
22
+ expect(parseProjectMetaResponse(projectResponse(false)).isOwner).toBe(false)
23
+ })
24
+
25
+ it('fails closed when isOwner is missing or has the wrong type', () => {
26
+ expect(parseProjectMetaResponse(projectResponse(undefined)).isOwner).toBe(
27
+ false,
28
+ )
29
+ expect(parseProjectMetaResponse(projectResponse('true')).isOwner).toBe(
30
+ false,
31
+ )
32
+ })
33
+ })
34
+
35
+ describe('project chatbot parsing', () => {
36
+ // 接口的 chatbot 字段全部可选,缺字段不能连带丢掉 isOwner 与应用名称。
37
+ function responseWithChatbots(chatbots: unknown) {
38
+ return {
39
+ code: 0,
40
+ data: {
41
+ isOwner: true,
42
+ site: {
43
+ baseBindings: [{ baseToken: 'bascnBase', tableIds: ['tbl_a'] }],
44
+ chatbots,
45
+ description: '应用描述',
46
+ name: '应用名称',
47
+ },
48
+ },
49
+ }
50
+ }
51
+
52
+ it('keeps the rest of the meta when a chatbot omits optional fields', () => {
53
+ const meta = parseProjectMetaResponse(
54
+ responseWithChatbots([{ id: 'chat_1' }]),
55
+ )
56
+
57
+ expect(meta.isOwner).toBe(true)
58
+ expect(meta.name).toBe('应用名称')
59
+ expect(meta.baseBindings).toEqual([
60
+ { baseToken: 'bascnBase', tableIds: ['tbl_a'] },
61
+ ])
62
+ expect(meta.chatList).toEqual([{ id: 'chat_1', name: '', iconInfo: '' }])
63
+ })
64
+
65
+ it('drops entries without a usable id and keeps the valid ones', () => {
66
+ const meta = parseProjectMetaResponse(
67
+ responseWithChatbots([
68
+ { name: '没有 id' },
69
+ 'not-an-object',
70
+ { id: 'chat_2', name: '有效', iconInfo: '{"key":"k","type":2}' },
71
+ ]),
72
+ )
73
+
74
+ expect(meta.isOwner).toBe(true)
75
+ expect(meta.chatList).toEqual([
76
+ { id: 'chat_2', name: '有效', iconInfo: '{"key":"k","type":2}' },
77
+ ])
78
+ })
79
+ })