@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,103 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
buildDatabaseInvokeFunctionPath,
|
|
4
|
+
buildDatabaseInvokeGetPath,
|
|
5
|
+
getDatabaseContext,
|
|
6
|
+
} from './database-runtime'
|
|
7
|
+
|
|
8
|
+
function stubPathname(pathname: string): void {
|
|
9
|
+
vi.stubGlobal('location', { pathname })
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.unstubAllGlobals()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('getDatabaseContext', () => {
|
|
17
|
+
it('reads token and prod env from the online artifact path', () => {
|
|
18
|
+
stubPathname('/app/bascnDemo/index.html')
|
|
19
|
+
expect(getDatabaseContext()).toEqual({
|
|
20
|
+
env: 'prod',
|
|
21
|
+
gatewayOrigin: '',
|
|
22
|
+
token: 'bascnDemo',
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('reads preview env from the preview artifact path', () => {
|
|
27
|
+
stubPathname('/preview/app/bascnDemo/')
|
|
28
|
+
expect(getDatabaseContext()).toEqual({
|
|
29
|
+
env: 'preview',
|
|
30
|
+
gatewayOrigin: '',
|
|
31
|
+
token: 'bascnDemo',
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('returns an empty token when the path is not an artifact path', () => {
|
|
36
|
+
stubPathname('/')
|
|
37
|
+
expect(getDatabaseContext()).toEqual({
|
|
38
|
+
env: 'prod',
|
|
39
|
+
gatewayOrigin: '',
|
|
40
|
+
token: '',
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('buildDatabaseInvokeGetPath', () => {
|
|
46
|
+
it('builds the prod path with the chatbot flag', () => {
|
|
47
|
+
expect(
|
|
48
|
+
buildDatabaseInvokeGetPath(
|
|
49
|
+
{ env: 'prod', gatewayOrigin: '', token: 'bascnDemo' },
|
|
50
|
+
true,
|
|
51
|
+
),
|
|
52
|
+
).toBe('/space/api/bitable/site/v1/bascnDemo/invoke_get?withChatbot=true')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('adds the preview env for draft artifacts', () => {
|
|
56
|
+
expect(
|
|
57
|
+
buildDatabaseInvokeGetPath(
|
|
58
|
+
{ env: 'preview', gatewayOrigin: '', token: 'bascnDemo' },
|
|
59
|
+
false,
|
|
60
|
+
),
|
|
61
|
+
).toBe('/space/api/bitable/site/v1/bascnDemo/invoke_get?env=preview')
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('buildDatabaseInvokeFunctionPath', () => {
|
|
66
|
+
// token 与 function_id 是 path 参数,env 走 query,prod 依赖服务端缺省。
|
|
67
|
+
it('puts token and functionId in the path and omits env for prod', () => {
|
|
68
|
+
expect(
|
|
69
|
+
buildDatabaseInvokeFunctionPath(
|
|
70
|
+
{ env: 'prod', gatewayOrigin: '', token: 'bascnDemo' },
|
|
71
|
+
'list_tasks',
|
|
72
|
+
),
|
|
73
|
+
).toBe('/space/api/bitable/site/v1/bascnDemo/invoke_function/list_tasks')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('adds the preview env query for draft artifacts', () => {
|
|
77
|
+
expect(
|
|
78
|
+
buildDatabaseInvokeFunctionPath(
|
|
79
|
+
{ env: 'preview', gatewayOrigin: '', token: 'bascnDemo' },
|
|
80
|
+
'list-tasks',
|
|
81
|
+
),
|
|
82
|
+
).toBe(
|
|
83
|
+
'/space/api/bitable/site/v1/bascnDemo/invoke_function/list-tasks?env=preview',
|
|
84
|
+
)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('rejects functionIds that cannot be a safe path segment', () => {
|
|
88
|
+
const context = {
|
|
89
|
+
env: 'prod',
|
|
90
|
+
gatewayOrigin: '',
|
|
91
|
+
token: 'bascnDemo',
|
|
92
|
+
} as const
|
|
93
|
+
expect(() => buildDatabaseInvokeFunctionPath(context, 'a/b')).toThrow(
|
|
94
|
+
TypeError,
|
|
95
|
+
)
|
|
96
|
+
expect(() => buildDatabaseInvokeFunctionPath(context, '../x')).toThrow(
|
|
97
|
+
TypeError,
|
|
98
|
+
)
|
|
99
|
+
expect(() => buildDatabaseInvokeFunctionPath(context, '')).toThrow(
|
|
100
|
+
TypeError,
|
|
101
|
+
)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import {
|
|
2
|
+
invokeHttp,
|
|
3
|
+
siteRuntime,
|
|
4
|
+
fetch as siteRuntimeFetch,
|
|
5
|
+
type SiteProjectEnv,
|
|
6
|
+
} from '@lark-base-open/base-site-channel/runtime'
|
|
7
|
+
|
|
8
|
+
/** 内置 Runtime Function(function.runtime.*,由宿主固定分发)。
|
|
9
|
+
* 随 channel 包发布,直接透传官方实现,不在 starter 侧包装。 */
|
|
10
|
+
export {
|
|
11
|
+
getCurrentUser,
|
|
12
|
+
logout,
|
|
13
|
+
requestEdit,
|
|
14
|
+
} from '@lark-base-open/base-site-channel/runtime'
|
|
15
|
+
export type { SiteRuntimeUserInfo } from '@lark-base-open/base-site-channel/runtime'
|
|
16
|
+
|
|
17
|
+
/** 应用产物在 BaseApp 页面 iframe 中的运行上下文。 */
|
|
18
|
+
export interface DatabaseContext {
|
|
19
|
+
env: DatabaseEnv
|
|
20
|
+
gatewayOrigin: string
|
|
21
|
+
token: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 附件在底表里的位置。四个值都来自 catalog.json 与 SQL 结果。 */
|
|
25
|
+
export interface DatabaseAttachmentRef {
|
|
26
|
+
tableId: string
|
|
27
|
+
fieldId: string
|
|
28
|
+
recordId: string
|
|
29
|
+
fileToken: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 产物环境沿用 site.v1 协议的规范类型,避免两侧各自声明后漂移。 */
|
|
33
|
+
export type DatabaseEnv = SiteProjectEnv
|
|
34
|
+
|
|
35
|
+
/** Function 调用的统一响应包裹结构,与运行态接口一致。 */
|
|
36
|
+
export interface DatabaseFunctionResponse<Data = unknown> {
|
|
37
|
+
code: number
|
|
38
|
+
data?: Data
|
|
39
|
+
msg?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 产物托管路径:线上为 /app/{token}/,预览为 /preview/app/{token}/。 */
|
|
43
|
+
const ARTIFACT_PATH_PATTERN = /^\/?(preview\/)?app\/([^/]+)(\/|$)/
|
|
44
|
+
|
|
45
|
+
/** functionId 会进入 URL 路径段,收口到 Base Database 的既有命名约束。 */
|
|
46
|
+
const FUNCTION_ID_PATTERN = /^[A-Za-z0-9_-]+$/
|
|
47
|
+
|
|
48
|
+
const DATABASE_RUNTIME_GATEWAY_PREFIX = '/space/api/bitable/site/v1'
|
|
49
|
+
const ATTACHMENT_COVER_PATH = '/space/api/box/stream/download/v2/cover/'
|
|
50
|
+
const ATTACHMENT_DOWNLOAD_URL_PATH =
|
|
51
|
+
'/space/api/box/file/temporary_download_url'
|
|
52
|
+
const ATTACHMENT_COVER_SIZES = [120, 360, 720, 1280] as const
|
|
53
|
+
const PREVIEW_ENV: DatabaseEnv = 'preview'
|
|
54
|
+
|
|
55
|
+
let databaseContextPromise: Promise<DatabaseContext> | null = null
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
invokeHttp,
|
|
59
|
+
siteRuntime,
|
|
60
|
+
/** 兼容原生 fetch 签名的父页面代理请求方法。 */
|
|
61
|
+
siteRuntimeFetch as fetch,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 资源请求不能用会落到产物域的相对地址;无法从 iframe 关系确定网关时返回空串。 */
|
|
65
|
+
function inferGatewayOrigin(): string {
|
|
66
|
+
const location =
|
|
67
|
+
typeof globalThis.location === 'undefined' ? undefined : globalThis.location
|
|
68
|
+
const ancestorOrigin = location?.ancestorOrigins?.[0]
|
|
69
|
+
|
|
70
|
+
if (ancestorOrigin) {
|
|
71
|
+
return ancestorOrigin
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const referrer =
|
|
75
|
+
typeof globalThis.document === 'undefined'
|
|
76
|
+
? ''
|
|
77
|
+
: globalThis.document.referrer
|
|
78
|
+
|
|
79
|
+
if (!referrer) {
|
|
80
|
+
return ''
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
return new URL(referrer).origin
|
|
85
|
+
} catch {
|
|
86
|
+
return ''
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 从产物托管路径推断当前应用 token 与运行环境。
|
|
91
|
+
*
|
|
92
|
+
* 这是宿主未下发上下文时的兜底:BaseApp 形态下 site 与 BaseApp 共用同一个 token,
|
|
93
|
+
* 线上产物挂在 /app/{token}/、预览产物挂在 /preview/app/{token}/。
|
|
94
|
+
* 该推断依赖部署侧的 CDN 路径约定,优先级低于宿主下发值。
|
|
95
|
+
*
|
|
96
|
+
* @returns 应用 token、运行环境与业务网关 origin;无法推断的值为空字符串。
|
|
97
|
+
*/
|
|
98
|
+
export function getDatabaseContext(): DatabaseContext {
|
|
99
|
+
const pathname =
|
|
100
|
+
typeof globalThis.location === 'undefined'
|
|
101
|
+
? ''
|
|
102
|
+
: globalThis.location.pathname
|
|
103
|
+
const matched = ARTIFACT_PATH_PATTERN.exec(pathname)
|
|
104
|
+
const gatewayOrigin = inferGatewayOrigin()
|
|
105
|
+
|
|
106
|
+
if (!matched) {
|
|
107
|
+
return { env: 'prod', gatewayOrigin, token: '' }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
env: matched[1] ? PREVIEW_ENV : 'prod',
|
|
112
|
+
gatewayOrigin,
|
|
113
|
+
token: decodeURIComponent(matched[2] ?? ''),
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 判断输入是否为普通对象。
|
|
118
|
+
* @param value 待判断的数据。
|
|
119
|
+
* @returns 是普通对象时返回 true。
|
|
120
|
+
*/
|
|
121
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
122
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 判断输入是否为受支持的产物环境。
|
|
126
|
+
* @param value 待判断的数据。
|
|
127
|
+
* @returns 是 preview 或 prod 时返回 true。
|
|
128
|
+
*/
|
|
129
|
+
function isDatabaseEnv(value: unknown): value is DatabaseEnv {
|
|
130
|
+
return value === PREVIEW_ENV || value === 'prod'
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 从宿主下发的 Runtime 上下文中提取应用定位信息。
|
|
134
|
+
*
|
|
135
|
+
* 协议把 siteToken 与 env 声明为可选,但这里刻意按 unknown 处理并逐项校验:
|
|
136
|
+
* 取值跨 iframe 边界、来自另一个 origin,声明类型不构成运行时保证;
|
|
137
|
+
* 旧版本宿主也可能完全不返回这两个字段(宿主实现见 base-app 的 SiteRuntimeHost)。
|
|
138
|
+
*
|
|
139
|
+
* @param context 宿主返回的 Runtime 上下文。
|
|
140
|
+
* @returns 宿主提供的 token 与环境;缺失或非法时对应项为 undefined。
|
|
141
|
+
*/
|
|
142
|
+
function readHostDatabaseContext(context: unknown): Partial<DatabaseContext> {
|
|
143
|
+
if (!isRecord(context)) {
|
|
144
|
+
return {}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const token =
|
|
148
|
+
typeof context.siteToken === 'string' ? context.siteToken.trim() : ''
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
...(token ? { token } : {}),
|
|
152
|
+
...(isDatabaseEnv(context.env) ? { env: context.env } : {}),
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** 解析当前应用上下文,宿主下发值优先、产物路径推断兜底。
|
|
157
|
+
*
|
|
158
|
+
* 结果按会话缓存;解析失败时清空缓存,允许后续调用重试。
|
|
159
|
+
*
|
|
160
|
+
* @returns 应用 token、运行环境与业务网关 origin。
|
|
161
|
+
*/
|
|
162
|
+
export async function resolveDatabaseContext(): Promise<DatabaseContext> {
|
|
163
|
+
if (databaseContextPromise) {
|
|
164
|
+
return databaseContextPromise
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
databaseContextPromise = (async (): Promise<DatabaseContext> => {
|
|
168
|
+
const fallback = getDatabaseContext()
|
|
169
|
+
let host: Partial<DatabaseContext> = {}
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
host = readHostDatabaseContext(await siteRuntime.getRuntimeContext())
|
|
173
|
+
} catch (error) {
|
|
174
|
+
// 宿主未实现或握手尚未完成时退回路径推断,不阻断业务。
|
|
175
|
+
console.warn('读取宿主 Runtime 上下文失败,回退产物路径推断', error)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const context: DatabaseContext = {
|
|
179
|
+
env: host.env ?? fallback.env,
|
|
180
|
+
gatewayOrigin: fallback.gatewayOrigin,
|
|
181
|
+
token: host.token ?? fallback.token,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!context.token) {
|
|
185
|
+
console.error(
|
|
186
|
+
'未能确定应用 token:宿主未下发 siteToken,且当前路径不符合产物托管规则',
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return context
|
|
191
|
+
})().catch((error: unknown) => {
|
|
192
|
+
databaseContextPromise = null
|
|
193
|
+
throw error
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
return databaseContextPromise
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** 位置声明的四个值缺一不可,缺任意一个服务端都无法核验归属。 */
|
|
200
|
+
function hasCompleteRef(ref: DatabaseAttachmentRef): boolean {
|
|
201
|
+
return Boolean(
|
|
202
|
+
ref.tableId.trim() &&
|
|
203
|
+
ref.fieldId.trim() &&
|
|
204
|
+
ref.recordId.trim() &&
|
|
205
|
+
ref.fileToken.trim(),
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** baseAppToken 把附件鉴权绑定到应用;省略会退回底表权限判定并导致访客被拒。 */
|
|
210
|
+
function buildAttachmentExtra(
|
|
211
|
+
context: DatabaseContext,
|
|
212
|
+
ref: DatabaseAttachmentRef,
|
|
213
|
+
): string {
|
|
214
|
+
return JSON.stringify({
|
|
215
|
+
bitablePerm: {
|
|
216
|
+
tableId: ref.tableId,
|
|
217
|
+
attachments: {
|
|
218
|
+
[ref.fieldId]: { [ref.recordId]: [ref.fileToken] },
|
|
219
|
+
},
|
|
220
|
+
baseAppToken: context.token,
|
|
221
|
+
},
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** 封面由浏览器直连网关获取,必须是绝对 URL;尺寸向上匹配服务端档位。
|
|
226
|
+
* 构造 URL 不会检查附件是否挂载成功,不能用于确认写入结果。
|
|
227
|
+
*
|
|
228
|
+
* @returns 可直接交给 img.src 的地址;上下文或附件位置不完整时返回空字符串。
|
|
229
|
+
*/
|
|
230
|
+
export function buildAttachmentCoverUrl(
|
|
231
|
+
context: DatabaseContext,
|
|
232
|
+
ref: DatabaseAttachmentRef,
|
|
233
|
+
size = 360,
|
|
234
|
+
): string {
|
|
235
|
+
if (!context.gatewayOrigin || !context.token || !hasCompleteRef(ref)) {
|
|
236
|
+
return ''
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const coverSize =
|
|
240
|
+
ATTACHMENT_COVER_SIZES.find((candidate) => candidate >= size) ??
|
|
241
|
+
ATTACHMENT_COVER_SIZES[ATTACHMENT_COVER_SIZES.length - 1]
|
|
242
|
+
const searchParams = new URLSearchParams({
|
|
243
|
+
extra: buildAttachmentExtra(context, ref),
|
|
244
|
+
width: String(coverSize),
|
|
245
|
+
height: String(coverSize),
|
|
246
|
+
policy: 'allow_up',
|
|
247
|
+
fallback_source: '1',
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
`${context.gatewayOrigin}${ATTACHMENT_COVER_PATH}` +
|
|
252
|
+
`${encodeURIComponent(ref.fileToken)}?${searchParams.toString()}`
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** 经父页面 bridge 取短时效自证链接;原文件由浏览器下载,不进入页面 JavaScript。
|
|
257
|
+
* 下载链接不代表附件增删已完成,也不提供并发或重试保证。
|
|
258
|
+
*
|
|
259
|
+
* @returns 可直接交给 a.href 的下载链接。
|
|
260
|
+
*/
|
|
261
|
+
export async function fetchAttachmentDownloadUrl(
|
|
262
|
+
context: DatabaseContext,
|
|
263
|
+
ref: DatabaseAttachmentRef,
|
|
264
|
+
): Promise<string> {
|
|
265
|
+
if (!context.token) {
|
|
266
|
+
throw new Error('应用 token 未确定,无法获取附件下载链接')
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!hasCompleteRef(ref)) {
|
|
270
|
+
throw new Error('附件位置不完整,无法获取下载链接')
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const response = await siteRuntimeFetch(ATTACHMENT_DOWNLOAD_URL_PATH, {
|
|
274
|
+
body: JSON.stringify({
|
|
275
|
+
extra: buildAttachmentExtra(context, ref),
|
|
276
|
+
file_token: ref.fileToken,
|
|
277
|
+
}),
|
|
278
|
+
headers: { 'content-type': 'application/json' },
|
|
279
|
+
method: 'POST',
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
if (!response.ok) {
|
|
283
|
+
throw new Error(`附件下载链接获取失败:HTTP ${response.status}`)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const body: unknown = await response.json()
|
|
287
|
+
const url =
|
|
288
|
+
isRecord(body) && isRecord(body.data) && typeof body.data.url === 'string'
|
|
289
|
+
? body.data.url.trim()
|
|
290
|
+
: ''
|
|
291
|
+
|
|
292
|
+
if (!url) {
|
|
293
|
+
throw new Error('附件下载链接获取失败:响应缺少 data.url')
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return url
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** 构造运行态读取应用信息的统一网关地址。
|
|
300
|
+
*
|
|
301
|
+
* @param context 当前应用上下文。
|
|
302
|
+
* @param withChatbot 是否附带项目绑定的 chatbot 列表。
|
|
303
|
+
* @returns GetInvokeProject 的同源相对路径。
|
|
304
|
+
*/
|
|
305
|
+
export function buildDatabaseInvokeGetPath(
|
|
306
|
+
context: DatabaseContext,
|
|
307
|
+
withChatbot: boolean,
|
|
308
|
+
): string {
|
|
309
|
+
const searchParams = new URLSearchParams()
|
|
310
|
+
|
|
311
|
+
if (withChatbot) {
|
|
312
|
+
searchParams.set('withChatbot', 'true')
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (context.env === PREVIEW_ENV) {
|
|
316
|
+
searchParams.set('env', PREVIEW_ENV)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const query = searchParams.toString()
|
|
320
|
+
const path = `${DATABASE_RUNTIME_GATEWAY_PREFIX}/${encodeURIComponent(context.token)}/invoke_get`
|
|
321
|
+
|
|
322
|
+
return query ? `${path}?${query}` : path
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** 构造运行态调用函数的统一网关地址。
|
|
326
|
+
*
|
|
327
|
+
* token 与 function_id 是 path 参数,env 走 query(prod 为服务端缺省,不显式下传)。
|
|
328
|
+
*
|
|
329
|
+
* @param context 当前应用上下文。
|
|
330
|
+
* @param functionId Base Database Function ID。
|
|
331
|
+
* @returns InvokeFunction 的同源相对路径。
|
|
332
|
+
* @throws functionId 不满足命名约束时抛出 TypeError。
|
|
333
|
+
*/
|
|
334
|
+
export function buildDatabaseInvokeFunctionPath(
|
|
335
|
+
context: DatabaseContext,
|
|
336
|
+
functionId: string,
|
|
337
|
+
): string {
|
|
338
|
+
if (!FUNCTION_ID_PATTERN.test(functionId)) {
|
|
339
|
+
throw new TypeError(
|
|
340
|
+
`functionId ${functionId} 不满足命名约束 ${String(FUNCTION_ID_PATTERN)}`,
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const path =
|
|
345
|
+
`${DATABASE_RUNTIME_GATEWAY_PREFIX}/${encodeURIComponent(context.token)}` +
|
|
346
|
+
`/invoke_function/${encodeURIComponent(functionId)}`
|
|
347
|
+
|
|
348
|
+
return context.env === PREVIEW_ENV
|
|
349
|
+
? `${path}?env=${encodeURIComponent(PREVIEW_ENV)}`
|
|
350
|
+
: path
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** 调用应用 Function。
|
|
354
|
+
*
|
|
355
|
+
* 请求经父页面同源代理发往统一网关;整个请求体即 payload,业务参数包在 params 下。
|
|
356
|
+
* 目标环境只走 query 参数 env:服务端 resolveProjectEnv 显式入参优先,且
|
|
357
|
+
* cloneBaseAppHeaders 会剥掉入站的 x-project-env 后按归一化 env 重写,
|
|
358
|
+
* 再带同名 Header 属于重复下发。
|
|
359
|
+
* 4xx/5xx 直接抛错,业务码由调用方按 operation 契约校验一次顶层 code。
|
|
360
|
+
*
|
|
361
|
+
* @param functionId Base Database Function ID。
|
|
362
|
+
* @param params 函数入参:SQL Function 用数组,FaaS 用业务对象。
|
|
363
|
+
* @returns 统一包裹的函数响应。
|
|
364
|
+
*/
|
|
365
|
+
export async function invokeDatabaseFunction<Data = unknown>(
|
|
366
|
+
functionId: string,
|
|
367
|
+
params?: unknown,
|
|
368
|
+
): Promise<DatabaseFunctionResponse<Data>> {
|
|
369
|
+
const context = await resolveDatabaseContext()
|
|
370
|
+
|
|
371
|
+
if (!context.token) {
|
|
372
|
+
throw new Error(`应用 token 未确定,无法调用 Function ${functionId}`)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const response = await invokeHttp({
|
|
376
|
+
url: buildDatabaseInvokeFunctionPath(context, functionId),
|
|
377
|
+
init: {
|
|
378
|
+
body: JSON.stringify({ params }),
|
|
379
|
+
headers: { 'content-type': 'application/json' },
|
|
380
|
+
method: 'POST',
|
|
381
|
+
},
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
if (!response.ok) {
|
|
385
|
+
throw new Error(`Function ${functionId} 调用失败:HTTP ${response.status}`)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return (await response.json()) as DatabaseFunctionResponse<Data>
|
|
389
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { StrictMode } from 'react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import App from './App'
|
|
4
|
+
import { DatabaseRuntimeReady } from './components/database-runtime-ready'
|
|
5
|
+
import { siteRuntime } from './lib/database-runtime'
|
|
6
|
+
import './index.css'
|
|
7
|
+
|
|
8
|
+
const APP_BOOT_FAILED_CODE = 'APP_BOOT_FAILED'
|
|
9
|
+
|
|
10
|
+
/** 挂载应用。 */
|
|
11
|
+
function bootstrapDatabaseApp(): void {
|
|
12
|
+
const rootElement = document.getElementById('root')
|
|
13
|
+
|
|
14
|
+
if (!rootElement) {
|
|
15
|
+
throw new Error('未找到应用挂载节点 #root')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
createRoot(rootElement).render(
|
|
19
|
+
<StrictMode>
|
|
20
|
+
<App />
|
|
21
|
+
<DatabaseRuntimeReady />
|
|
22
|
+
</StrictMode>,
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
bootstrapDatabaseApp()
|
|
28
|
+
} catch (error) {
|
|
29
|
+
// ready 之前的启动失败必须显式上报,否则宿主只能等到 ready 超时才展示错误态。
|
|
30
|
+
void siteRuntime.reportLoadFailed(APP_BOOT_FAILED_CODE).catch(() => {
|
|
31
|
+
// 上报本身失败时不掩盖原始启动错误。
|
|
32
|
+
})
|
|
33
|
+
throw error
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "ES2020",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"moduleResolution": "bundler",
|
|
10
|
+
"allowImportingTsExtensions": true,
|
|
11
|
+
"verbatimModuleSyntax": false,
|
|
12
|
+
"moduleDetection": "force",
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
"jsx": "react-jsx",
|
|
15
|
+
"strict": true,
|
|
16
|
+
"noUnusedLocals": true,
|
|
17
|
+
"noUnusedParameters": true,
|
|
18
|
+
"noFallthroughCasesInSwitch": true,
|
|
19
|
+
"noUncheckedSideEffectImports": true,
|
|
20
|
+
"forceConsistentCasingInFileNames": true
|
|
21
|
+
},
|
|
22
|
+
"include": ["src"]
|
|
23
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createArgusCsrfPlugin } from '@lark-base-open/base-site-js/vite'
|
|
2
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
3
|
+
import react from '@vitejs/plugin-react'
|
|
4
|
+
import { defineConfig } from 'vite'
|
|
5
|
+
|
|
6
|
+
// https://vite.dev/config/
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
base: './',
|
|
9
|
+
build: {
|
|
10
|
+
sourcemap: false,
|
|
11
|
+
},
|
|
12
|
+
plugins: [createArgusCsrfPlugin(), react(), tailwindcss()],
|
|
13
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sql/
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Server Functions
|
|
2
|
+
|
|
3
|
+
每个业务写入 operation 放在:
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
functions/<functionId>/
|
|
7
|
+
├── index.ts
|
|
8
|
+
└── meta.json
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
本目录是服务端 FaaS Functions 的源码与构建工作区,不是一个长期运行的服务。依赖与脚本以本目录 `package.json` 为准。Function ID、handler、`rawParams`、`context`、读写路径和返回结构见 `lark-base-database` Skill 的 `development.md`。
|