@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,201 @@
|
|
|
1
|
+
import { fetch as databaseFetch } from '../../lib/database-runtime'
|
|
2
|
+
import { fetchProjectMeta } from '../floating-button/utils/project-meta'
|
|
3
|
+
|
|
4
|
+
const RECOMMEND_USERS_API_PATH = '/space/api/mention/recommend.v2/'
|
|
5
|
+
const BITABLE_OBJECT_TYPE = '8'
|
|
6
|
+
const BITABLE_MENTION_SOURCE = '4'
|
|
7
|
+
const USER_MENTION_TYPE = '0'
|
|
8
|
+
const SEARCH_PAGE_SIZE = 20
|
|
9
|
+
|
|
10
|
+
export interface UserAvatar {
|
|
11
|
+
avatar_72: string
|
|
12
|
+
avatar_240: string
|
|
13
|
+
avatar_640: string
|
|
14
|
+
avatar_origin: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface User {
|
|
18
|
+
avatar: UserAvatar
|
|
19
|
+
department_ids: string[]
|
|
20
|
+
description?: string
|
|
21
|
+
is_external?: boolean
|
|
22
|
+
name: string
|
|
23
|
+
user_id: string
|
|
24
|
+
open_id?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SearchUsersData {
|
|
28
|
+
users: User[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SearchUsersResponse {
|
|
32
|
+
code: number
|
|
33
|
+
msg: string
|
|
34
|
+
data: SearchUsersData
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type SearchUsers = (keyword: string) => Promise<SearchUsersResponse>
|
|
38
|
+
|
|
39
|
+
export async function requestUsers(
|
|
40
|
+
keyword: string,
|
|
41
|
+
): Promise<SearchUsersResponse> {
|
|
42
|
+
const projectMeta = await fetchProjectMeta()
|
|
43
|
+
const baseToken = projectMeta.baseBindings[0]?.baseToken
|
|
44
|
+
if (!baseToken) {
|
|
45
|
+
throw new Error('应用未绑定 Base,无法搜索成员')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const body = new URLSearchParams({
|
|
49
|
+
config_source: '0',
|
|
50
|
+
obj_type: BITABLE_OBJECT_TYPE,
|
|
51
|
+
source: BITABLE_MENTION_SOURCE,
|
|
52
|
+
token: baseToken,
|
|
53
|
+
type: USER_MENTION_TYPE,
|
|
54
|
+
user_type: '0',
|
|
55
|
+
content: keyword.trim().slice(0, 50),
|
|
56
|
+
}).toString()
|
|
57
|
+
|
|
58
|
+
// 与 GetInvokeProject 完全复用同一条 Channel Runtime fetch 链路:
|
|
59
|
+
// Site iframe -> site.v1.http.request -> BaseApp 宿主同源请求。
|
|
60
|
+
const response = await databaseFetch(RECOMMEND_USERS_API_PATH, {
|
|
61
|
+
body,
|
|
62
|
+
headers: {
|
|
63
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
64
|
+
},
|
|
65
|
+
method: 'POST',
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
throw new Error(`搜索用户请求失败,HTTP 状态码:${response.status}`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const payload: unknown = await response.json()
|
|
73
|
+
return {
|
|
74
|
+
code: 0,
|
|
75
|
+
msg: 'ok',
|
|
76
|
+
data: normalizeRecommendUsersResponse(payload),
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function normalizeRecommendUsersResponse(
|
|
81
|
+
value: unknown,
|
|
82
|
+
locale = getLocale(),
|
|
83
|
+
): SearchUsersData {
|
|
84
|
+
const payload = unwrapRecommendPayload(value)
|
|
85
|
+
if (!payload) {
|
|
86
|
+
throw new Error('搜索用户返回结构不正确')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const entities = payload.entities
|
|
90
|
+
const users = isRecord(entities) ? entities.users : undefined
|
|
91
|
+
if (!isRecord(users) || !Array.isArray(payload.result_list)) {
|
|
92
|
+
throw new Error('搜索用户返回结构不正确')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const result: User[] = []
|
|
96
|
+
const seenUserIds = new Set<string>()
|
|
97
|
+
for (const item of payload.result_list) {
|
|
98
|
+
if (!isRecord(item) || Number(item.type) !== 0) continue
|
|
99
|
+
const token = readString(item.token)
|
|
100
|
+
if (!token) continue
|
|
101
|
+
|
|
102
|
+
const entity = users[token]
|
|
103
|
+
if (!isRecord(entity)) continue
|
|
104
|
+
const userId = readString(entity.user_id) || readString(entity.id) || token
|
|
105
|
+
const name = readUserName(entity, locale)
|
|
106
|
+
if (!userId || !name || seenUserIds.has(userId)) continue
|
|
107
|
+
|
|
108
|
+
seenUserIds.add(userId)
|
|
109
|
+
const avatarUrl = readString(entity.avatar_url)
|
|
110
|
+
const openId = readString(entity.open_id)
|
|
111
|
+
result.push({
|
|
112
|
+
avatar: {
|
|
113
|
+
avatar_72: avatarUrl,
|
|
114
|
+
avatar_240: avatarUrl,
|
|
115
|
+
avatar_640: avatarUrl,
|
|
116
|
+
avatar_origin: avatarUrl,
|
|
117
|
+
},
|
|
118
|
+
department_ids: readStringArray(entity.department_ids),
|
|
119
|
+
description: readUserDescription(entity, locale),
|
|
120
|
+
is_external: entity.is_external === true,
|
|
121
|
+
name,
|
|
122
|
+
user_id: userId,
|
|
123
|
+
...(openId ? { open_id: openId } : {}),
|
|
124
|
+
})
|
|
125
|
+
if (result.length >= SEARCH_PAGE_SIZE) break
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { users: result }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function unwrapRecommendPayload(
|
|
132
|
+
value: unknown,
|
|
133
|
+
): Record<string, unknown> | undefined {
|
|
134
|
+
if (!isRecord(value)) return undefined
|
|
135
|
+
|
|
136
|
+
if (typeof value.code === 'number' && value.code !== 0) {
|
|
137
|
+
throw new Error(readString(value.msg) || '搜索成员失败')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if ('entities' in value && 'result_list' in value) return value
|
|
141
|
+
return isRecord(value.data) ? value.data : undefined
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readUserName(entity: Record<string, unknown>, locale: string): string {
|
|
145
|
+
return (
|
|
146
|
+
readLocalizedString(entity.display_name, locale) ||
|
|
147
|
+
(locale.toLowerCase().startsWith('en')
|
|
148
|
+
? readString(entity.en_name)
|
|
149
|
+
: readString(entity.cn_name)) ||
|
|
150
|
+
readString(entity.name) ||
|
|
151
|
+
readString(entity.cn_name) ||
|
|
152
|
+
readString(entity.en_name)
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function readUserDescription(
|
|
157
|
+
entity: Record<string, unknown>,
|
|
158
|
+
locale: string,
|
|
159
|
+
): string {
|
|
160
|
+
const localized =
|
|
161
|
+
entity.is_external === true ? entity.i18n_tenant : entity.i18n_department
|
|
162
|
+
return readLocalizedString(localized, locale) || readString(entity.department)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function readLocalizedString(value: unknown, locale: string): string {
|
|
166
|
+
if (typeof value === 'string') return value.trim()
|
|
167
|
+
if (!isRecord(value)) return ''
|
|
168
|
+
|
|
169
|
+
const normalizedLocale = locale.toLowerCase().replace('-', '_')
|
|
170
|
+
const language = normalizedLocale.split('_')[0] ?? ''
|
|
171
|
+
return (
|
|
172
|
+
readString(value[normalizedLocale]) ||
|
|
173
|
+
readString(value[language]) ||
|
|
174
|
+
readString(value.zh_cn) ||
|
|
175
|
+
readString(value.en_us)
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function readString(value: unknown): string {
|
|
180
|
+
return typeof value === 'string' ? value.trim() : ''
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function readStringArray(value: unknown): string[] {
|
|
184
|
+
return Array.isArray(value)
|
|
185
|
+
? value.filter((item): item is string => typeof item === 'string')
|
|
186
|
+
: []
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function getLocale(): string {
|
|
190
|
+
if (typeof document !== 'undefined' && document.documentElement.lang) {
|
|
191
|
+
return document.documentElement.lang
|
|
192
|
+
}
|
|
193
|
+
if (typeof navigator !== 'undefined' && navigator.language) {
|
|
194
|
+
return navigator.language
|
|
195
|
+
}
|
|
196
|
+
return 'zh-CN'
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
200
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
201
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
import { siteRuntime } from '../lib/database-runtime'
|
|
3
|
+
|
|
4
|
+
/** 在首屏挂载完成后向 BaseApp 宿主上报应用 Runtime 就绪。
|
|
5
|
+
*
|
|
6
|
+
* 宿主从 Channel 握手完成起等待该事件,超时(当前 10 秒)会展示加载失败态。
|
|
7
|
+
* 必须放在应用根节点内、且在首屏内容之后渲染,不要在模块顶层或数据初始化前调用。
|
|
8
|
+
*
|
|
9
|
+
* @returns 不渲染任何内容。
|
|
10
|
+
*/
|
|
11
|
+
export function DatabaseRuntimeReady(): null {
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
void siteRuntime.ready().catch((error: unknown) => {
|
|
14
|
+
console.error('Application Runtime ready 上报失败', error)
|
|
15
|
+
})
|
|
16
|
+
}, [])
|
|
17
|
+
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default DatabaseRuntimeReady
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
fetchCurrentUser,
|
|
4
|
+
type CurrentUser,
|
|
5
|
+
} from '../floating-button/utils/current-user'
|
|
6
|
+
import { fetchProjectMeta } from '../floating-button/utils/project-meta'
|
|
7
|
+
import { logout } from '../../lib/database-runtime'
|
|
8
|
+
|
|
9
|
+
export type DatabaseUserMenuLanguage = 'en' | 'zh'
|
|
10
|
+
|
|
11
|
+
/** 应用面向的使用场景。
|
|
12
|
+
*
|
|
13
|
+
* 企业级系统会被本人以外的人打开,导航里需要交代当前身份(头像、姓名、组织、退出登录);
|
|
14
|
+
* 个人系统只有自己用,这些信息是噪音,整块不渲染。该判定在应用开发时由 AI 一次性给出并
|
|
15
|
+
* 写死在调用处,运行时不做推断——产物里没有可靠信号(实测应用数据模型普遍不建模
|
|
16
|
+
* 「用户」这一维),而且忽隐忽现比判错更糟。判错时改这里一行重新部署即可。 */
|
|
17
|
+
export type DatabaseAudience = 'enterprise' | 'personal'
|
|
18
|
+
|
|
19
|
+
interface DatabaseUserMenuProps {
|
|
20
|
+
/** 界面语言,默认 en。 */
|
|
21
|
+
language?: DatabaseUserMenuLanguage
|
|
22
|
+
/** 默认 enterprise:拿不准时宁可多显示一个头像,也不要让用户找不到退出登录。 */
|
|
23
|
+
audience?: DatabaseAudience
|
|
24
|
+
/** 弹层相对触发器的对齐方向,默认 right(触发器在右上角时用)。 */
|
|
25
|
+
align?: 'left' | 'right'
|
|
26
|
+
/** 弹层展开方向,默认 down(触发器在顶部时用);侧边栏底部的入口用 up。 */
|
|
27
|
+
side?: 'up' | 'down'
|
|
28
|
+
/** 是否在头像右侧显示姓名。窄侧栏或顶栏空间紧张时传 false。 */
|
|
29
|
+
showName?: boolean
|
|
30
|
+
className?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface DatabaseUserMenuI18n {
|
|
34
|
+
account: string
|
|
35
|
+
logout: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const I18N: Record<DatabaseUserMenuLanguage, DatabaseUserMenuI18n> = {
|
|
39
|
+
en: { account: 'Account', logout: 'Log out' },
|
|
40
|
+
zh: { account: '账号', logout: '退出登录' },
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 应用自身导航里的当前用户入口:头像 + 姓名,点开是组织名与退出登录。
|
|
44
|
+
*
|
|
45
|
+
* 这块信息原先挂在右下角悬浮「更多」菜单里。悬浮控件是应用之外的一层浮层,
|
|
46
|
+
* 用户不会去那里找账号;放进应用自己的导航(顶栏右上角、侧边栏左下角等)才符合直觉。
|
|
47
|
+
* 位置由应用开发时按页面布局决定,组件只负责渲染与对齐。
|
|
48
|
+
*
|
|
49
|
+
* 数据自己拉:`function.runtime.getCurrentUser` 拿姓名与头像,组织名来自
|
|
50
|
+
* GetInvokeProject。两者都拿不到(例如应用被独立访问、无宿主)时整个组件不渲染,
|
|
51
|
+
* 不占位、不显示空头像。
|
|
52
|
+
*/
|
|
53
|
+
export function DatabaseUserMenu({
|
|
54
|
+
language = 'en',
|
|
55
|
+
audience = 'enterprise',
|
|
56
|
+
align = 'right',
|
|
57
|
+
side = 'down',
|
|
58
|
+
showName = true,
|
|
59
|
+
className = '',
|
|
60
|
+
}: DatabaseUserMenuProps) {
|
|
61
|
+
const [currentUser, setCurrentUser] = useState<CurrentUser | null>(null)
|
|
62
|
+
const [organization, setOrganization] = useState('')
|
|
63
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
64
|
+
const rootRef = useRef<HTMLDivElement>(null)
|
|
65
|
+
const i18n = I18N[language]
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (audience !== 'enterprise') {
|
|
69
|
+
return undefined
|
|
70
|
+
}
|
|
71
|
+
let isCurrentRequest = true
|
|
72
|
+
|
|
73
|
+
/** 读取当前用户与组织名。 */
|
|
74
|
+
async function load(): Promise<void> {
|
|
75
|
+
const [user, meta] = await Promise.all([
|
|
76
|
+
fetchCurrentUser(),
|
|
77
|
+
fetchProjectMeta().catch(() => null),
|
|
78
|
+
])
|
|
79
|
+
if (!isCurrentRequest) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
setCurrentUser(user)
|
|
83
|
+
setOrganization(user?.tenantName || meta?.organization || '')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
void load()
|
|
87
|
+
return () => {
|
|
88
|
+
isCurrentRequest = false
|
|
89
|
+
}
|
|
90
|
+
}, [audience])
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (!isOpen) {
|
|
94
|
+
return undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 点击组件外部时收起弹层。 */
|
|
98
|
+
function handlePointerDown(event: PointerEvent): void {
|
|
99
|
+
const target = event.target
|
|
100
|
+
if (target instanceof Node && !rootRef.current?.contains(target)) {
|
|
101
|
+
setIsOpen(false)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 按 Escape 收起弹层。 */
|
|
106
|
+
function handleKeyDown(event: KeyboardEvent): void {
|
|
107
|
+
if (event.key === 'Escape') {
|
|
108
|
+
setIsOpen(false)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
window.addEventListener('pointerdown', handlePointerDown)
|
|
113
|
+
window.addEventListener('keydown', handleKeyDown)
|
|
114
|
+
return () => {
|
|
115
|
+
window.removeEventListener('pointerdown', handlePointerDown)
|
|
116
|
+
window.removeEventListener('keydown', handleKeyDown)
|
|
117
|
+
}
|
|
118
|
+
}, [isOpen])
|
|
119
|
+
|
|
120
|
+
// 个人系统整块不渲染;企业级但拿不到用户(应用被独立访问、无宿主)时同样不占位
|
|
121
|
+
if (audience !== 'enterprise' || !currentUser) {
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const initial = currentUser.name.slice(0, 1)
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div ref={rootRef} className={`relative ${className}`}>
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
aria-label={i18n.account}
|
|
132
|
+
aria-expanded={isOpen}
|
|
133
|
+
aria-haspopup="menu"
|
|
134
|
+
className="flex max-w-full cursor-pointer items-center gap-2 rounded-full bg-transparent p-1 pr-2 text-sm outline-none transition-colors hover:bg-[#1f2329]/5 focus-visible:ring-2 focus-visible:ring-ring active:bg-[#1f2329]/10"
|
|
135
|
+
onClick={() => setIsOpen((next) => !next)}
|
|
136
|
+
>
|
|
137
|
+
{currentUser.avatarUrl ? (
|
|
138
|
+
<img
|
|
139
|
+
alt=""
|
|
140
|
+
aria-hidden="true"
|
|
141
|
+
className="size-7 shrink-0 rounded-full object-cover"
|
|
142
|
+
referrerPolicy="no-referrer"
|
|
143
|
+
src={currentUser.avatarUrl}
|
|
144
|
+
/>
|
|
145
|
+
) : (
|
|
146
|
+
<span
|
|
147
|
+
aria-hidden="true"
|
|
148
|
+
className="flex size-7 shrink-0 items-center justify-center rounded-full bg-[#1f2329]/8 text-xs font-medium"
|
|
149
|
+
>
|
|
150
|
+
{initial}
|
|
151
|
+
</span>
|
|
152
|
+
)}
|
|
153
|
+
{showName ? (
|
|
154
|
+
<span className="min-w-0 truncate font-medium">
|
|
155
|
+
{currentUser.name}
|
|
156
|
+
</span>
|
|
157
|
+
) : null}
|
|
158
|
+
</button>
|
|
159
|
+
|
|
160
|
+
{isOpen ? (
|
|
161
|
+
<div
|
|
162
|
+
role="menu"
|
|
163
|
+
className={`absolute z-50 w-[240px] rounded-[10px] border border-black/10 bg-white p-1 text-sm text-[#1f2329] shadow-[0_2px_4px_rgba(0,0,0,0.02),0_4px_10px_rgba(0,0,0,0.03),0_4px_16px_rgba(0,0,0,0.03)] ${
|
|
164
|
+
align === 'right' ? 'right-0' : 'left-0'
|
|
165
|
+
} ${side === 'up' ? 'bottom-[calc(100%+8px)]' : 'top-[calc(100%+8px)]'}`}
|
|
166
|
+
>
|
|
167
|
+
<div className="flex items-center gap-2.5 px-2.5 py-3">
|
|
168
|
+
{currentUser.avatarUrl ? (
|
|
169
|
+
<img
|
|
170
|
+
alt=""
|
|
171
|
+
aria-hidden="true"
|
|
172
|
+
className="size-8 shrink-0 rounded-full object-cover"
|
|
173
|
+
referrerPolicy="no-referrer"
|
|
174
|
+
src={currentUser.avatarUrl}
|
|
175
|
+
/>
|
|
176
|
+
) : null}
|
|
177
|
+
<div className="min-w-0 flex-1 leading-[22px]">
|
|
178
|
+
<div className="truncate font-medium">{currentUser.name}</div>
|
|
179
|
+
{organization ? (
|
|
180
|
+
<div className="truncate text-xs leading-[18px] text-[#646a73]">
|
|
181
|
+
{organization}
|
|
182
|
+
</div>
|
|
183
|
+
) : null}
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
<div
|
|
187
|
+
className="mx-2.5 my-1 border-t border-black/10"
|
|
188
|
+
aria-hidden="true"
|
|
189
|
+
/>
|
|
190
|
+
<button
|
|
191
|
+
type="button"
|
|
192
|
+
role="menuitem"
|
|
193
|
+
className="flex min-h-[38px] w-full cursor-pointer items-center gap-2 rounded-md px-2.5 py-1 text-left outline-none transition-colors hover:bg-[#1f2329]/5 focus-visible:bg-[#1f2329]/5 active:bg-[#1f2329]/10"
|
|
194
|
+
onClick={logoutDatabase}
|
|
195
|
+
>
|
|
196
|
+
<LogoutIcon />
|
|
197
|
+
<span className="min-w-0 flex-1 truncate">{i18n.logout}</span>
|
|
198
|
+
</button>
|
|
199
|
+
</div>
|
|
200
|
+
) : null}
|
|
201
|
+
</div>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** 退出登录:优先协议内置 Runtime Function(function.runtime.logout,由宿主执行
|
|
206
|
+
* 真正的登出);旧版宿主未实现时回落清应用自身会话 cookie 后重载。 */
|
|
207
|
+
function logoutDatabase(): void {
|
|
208
|
+
void logout().catch(() => {
|
|
209
|
+
const expire = 'Thu, 01 Jan 1970 00:00:00 GMT'
|
|
210
|
+
for (const name of ['base-site-session', 'session', 'sl_session']) {
|
|
211
|
+
document.cookie = `${name}=; expires=${expire}; path=/`
|
|
212
|
+
}
|
|
213
|
+
window.location.reload()
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** 退出登录图标。 */
|
|
218
|
+
function LogoutIcon() {
|
|
219
|
+
return (
|
|
220
|
+
<svg
|
|
221
|
+
aria-hidden="true"
|
|
222
|
+
width="16"
|
|
223
|
+
height="16"
|
|
224
|
+
viewBox="0 0 24 24"
|
|
225
|
+
fill="none"
|
|
226
|
+
stroke="currentColor"
|
|
227
|
+
strokeWidth="1.8"
|
|
228
|
+
strokeLinecap="round"
|
|
229
|
+
strokeLinejoin="round"
|
|
230
|
+
>
|
|
231
|
+
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
|
232
|
+
<path d="m16 17 5-5-5-5" />
|
|
233
|
+
<path d="M21 12H9" />
|
|
234
|
+
</svg>
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export default DatabaseUserMenu
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { CircleAlert, Inbox, LoaderCircle } from 'lucide-react'
|
|
2
|
+
import { Button } from '../ui/button'
|
|
3
|
+
|
|
4
|
+
interface PageStateProps {
|
|
5
|
+
status: 'loading' | 'empty' | 'error'
|
|
6
|
+
title?: string
|
|
7
|
+
description?: string
|
|
8
|
+
onRetry?: () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** 展示页面级加载、空数据或错误状态。 */
|
|
12
|
+
export function PageState({
|
|
13
|
+
status,
|
|
14
|
+
title,
|
|
15
|
+
description,
|
|
16
|
+
onRetry,
|
|
17
|
+
}: PageStateProps) {
|
|
18
|
+
if (status === 'loading') {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
className="flex min-h-48 flex-col items-center justify-center gap-3 text-center text-muted-foreground"
|
|
22
|
+
role="status"
|
|
23
|
+
>
|
|
24
|
+
<LoaderCircle
|
|
25
|
+
aria-hidden="true"
|
|
26
|
+
className="size-6 motion-safe:animate-spin"
|
|
27
|
+
/>
|
|
28
|
+
<p className="text-sm font-medium">{title ?? '正在加载'}</p>
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (status === 'empty') {
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex min-h-48 flex-col items-center justify-center gap-3 text-center">
|
|
36
|
+
<Inbox aria-hidden="true" className="size-7 text-muted-foreground" />
|
|
37
|
+
<div>
|
|
38
|
+
<p className="font-semibold">{title ?? '暂无数据'}</p>
|
|
39
|
+
{description ? (
|
|
40
|
+
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
|
|
41
|
+
) : null}
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
className="flex min-h-48 flex-col items-center justify-center gap-4 text-center"
|
|
50
|
+
role="alert"
|
|
51
|
+
>
|
|
52
|
+
<CircleAlert aria-hidden="true" className="size-7 text-danger" />
|
|
53
|
+
<div>
|
|
54
|
+
<p className="font-semibold">{title ?? '加载失败'}</p>
|
|
55
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
56
|
+
{description ?? '请稍后重试'}
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
{onRetry ? (
|
|
60
|
+
<Button variant="secondary" onClick={onRetry}>
|
|
61
|
+
重试
|
|
62
|
+
</Button>
|
|
63
|
+
) : null}
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { resolveChatbotDisplayName } from './chatbot-entry'
|
|
3
|
+
|
|
4
|
+
describe('resolveChatbotDisplayName', () => {
|
|
5
|
+
it.each(['未命名站点', '未命名智能体', 'Untitled site', 'Unnamed chatbot'])(
|
|
6
|
+
'uses the Site name for placeholder chatbot name %s',
|
|
7
|
+
(chatbotName) => {
|
|
8
|
+
expect(
|
|
9
|
+
resolveChatbotDisplayName(chatbotName, '客户工单与 SLA 系统'),
|
|
10
|
+
).toBe('客户工单与 SLA 系统')
|
|
11
|
+
},
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
it('keeps an explicitly configured chatbot name', () => {
|
|
15
|
+
expect(resolveChatbotDisplayName('SLA 助手', '客户工单系统')).toBe(
|
|
16
|
+
'SLA 助手',
|
|
17
|
+
)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('uses the generic caller fallback when Site metadata is unnamed', () => {
|
|
21
|
+
expect(resolveChatbotDisplayName('未命名站点', '智能助手')).toBe('智能助手')
|
|
22
|
+
})
|
|
23
|
+
})
|