@lark-base-open/base-database-cli 1.1.0 → 1.3.7
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 +86 -0
- package/LICENSE +6 -0
- package/README.md +55 -31
- package/dist/ai-functions.d.ts +42 -0
- package/dist/ai-functions.d.ts.map +1 -0
- package/dist/ai-functions.js +327 -0
- package/dist/ai-functions.js.map +1 -0
- package/dist/archive.d.ts +21 -0
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +65 -10
- package/dist/archive.js.map +1 -1
- package/dist/auth.d.ts +8 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +60 -6
- package/dist/auth.js.map +1 -1
- package/dist/check.d.ts +74 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +283 -0
- package/dist/check.js.map +1 -0
- package/dist/dependency-policy.d.ts +10 -0
- package/dist/dependency-policy.d.ts.map +1 -0
- package/dist/dependency-policy.js +162 -0
- package/dist/dependency-policy.js.map +1 -0
- package/dist/deploy.d.ts +1 -0
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +122 -175
- package/dist/deploy.js.map +1 -1
- package/dist/environment-variables.d.ts +52 -0
- package/dist/environment-variables.d.ts.map +1 -0
- package/dist/environment-variables.js +563 -0
- package/dist/environment-variables.js.map +1 -0
- package/dist/environment.d.ts +1 -1
- package/dist/environment.js +1 -1
- package/dist/environment.js.map +1 -1
- package/dist/function-input-types.d.ts +10 -0
- package/dist/function-input-types.d.ts.map +1 -0
- package/dist/function-input-types.js +160 -0
- package/dist/function-input-types.js.map +1 -0
- package/dist/function-schema.d.ts.map +1 -1
- package/dist/function-schema.js +89 -36
- package/dist/function-schema.js.map +1 -1
- package/dist/init-field-retry.d.ts +17 -0
- package/dist/init-field-retry.d.ts.map +1 -0
- package/dist/init-field-retry.js +127 -0
- package/dist/init-field-retry.js.map +1 -0
- package/dist/init-journal.d.ts +98 -0
- package/dist/init-journal.d.ts.map +1 -0
- package/dist/init-journal.js +492 -0
- package/dist/init-journal.js.map +1 -0
- package/dist/init-schema.d.ts +8 -0
- package/dist/init-schema.d.ts.map +1 -0
- package/dist/init-schema.js +423 -0
- package/dist/init-schema.js.map +1 -0
- package/dist/init.d.ts +14 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +886 -243
- package/dist/init.js.map +1 -1
- package/dist/lark-cli-error.d.ts +46 -0
- package/dist/lark-cli-error.d.ts.map +1 -0
- package/dist/lark-cli-error.js +171 -0
- package/dist/lark-cli-error.js.map +1 -0
- package/dist/lark-cli.d.ts +2 -1
- package/dist/lark-cli.d.ts.map +1 -1
- package/dist/lark-cli.js +292 -29
- package/dist/lark-cli.js.map +1 -1
- package/dist/mutation-schema.d.ts +146 -0
- package/dist/mutation-schema.d.ts.map +1 -0
- package/dist/mutation-schema.js +153 -0
- package/dist/mutation-schema.js.map +1 -0
- package/dist/mutation.d.ts +10 -0
- package/dist/mutation.d.ts.map +1 -0
- package/dist/mutation.js +131 -0
- package/dist/mutation.js.map +1 -0
- package/dist/npm.d.ts +1 -0
- package/dist/npm.d.ts.map +1 -1
- package/dist/npm.js +2 -2
- package/dist/npm.js.map +1 -1
- package/dist/preview-receipt.d.ts.map +1 -1
- package/dist/preview-receipt.js +15 -5
- package/dist/preview-receipt.js.map +1 -1
- package/dist/process.d.ts +3 -1
- package/dist/process.d.ts.map +1 -1
- package/dist/process.js +16 -4
- package/dist/process.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +241 -42
- package/dist/program.js.map +1 -1
- package/dist/project-build.d.ts +25 -0
- package/dist/project-build.d.ts.map +1 -0
- package/dist/project-build.js +229 -0
- package/dist/project-build.js.map +1 -0
- package/dist/project-context.d.ts +344 -0
- package/dist/project-context.d.ts.map +1 -0
- package/dist/project-context.js +485 -0
- package/dist/project-context.js.map +1 -0
- package/dist/project-layout.d.ts +5 -0
- package/dist/project-layout.d.ts.map +1 -0
- package/dist/project-layout.js +24 -0
- package/dist/project-layout.js.map +1 -0
- package/dist/public-sql-error.d.ts +25 -0
- package/dist/public-sql-error.d.ts.map +1 -0
- package/dist/public-sql-error.js +81 -0
- package/dist/public-sql-error.js.map +1 -0
- package/dist/pull.d.ts.map +1 -1
- package/dist/pull.js +54 -15
- package/dist/pull.js.map +1 -1
- package/dist/site-readme.d.ts +3 -0
- package/dist/site-readme.d.ts.map +1 -0
- package/dist/site-readme.js +83 -0
- package/dist/site-readme.js.map +1 -0
- package/dist/workspace-fingerprint.d.ts +21 -1
- package/dist/workspace-fingerprint.d.ts.map +1 -1
- package/dist/workspace-fingerprint.js +81 -2
- package/dist/workspace-fingerprint.js.map +1 -1
- package/package.json +2 -1
- package/skills/lark-base-database/SKILL.md +70 -53
- package/skills/lark-base-database/references/ai-function.md +471 -0
- package/skills/lark-base-database/references/base-capability-routing.md +3 -3
- package/skills/lark-base-database/references/database-function-limitations.md +46 -0
- package/skills/lark-base-database/references/database-function.md +349 -0
- package/skills/lark-base-database/references/design/components/actions-fields.md +16 -0
- package/skills/lark-base-database/references/design/components/code/controls.css +122 -0
- package/skills/lark-base-database/references/design/components/code/controls.tsx +73 -0
- package/skills/lark-base-database/references/design/components/code/date-picker.tsx +42 -0
- package/skills/lark-base-database/references/design/components/code/floating.tsx +41 -0
- package/skills/lark-base-database/references/design/components/code/overlays.tsx +19 -0
- package/skills/lark-base-database/references/design/components/code/select.tsx +34 -0
- package/skills/lark-base-database/references/design/components/code/styles.d.ts +1 -0
- package/skills/lark-base-database/references/design/components/code/theme.css +49 -0
- package/skills/lark-base-database/references/design/components/code/usage.tsx +38 -0
- package/skills/lark-base-database/references/design/components/date-time-number.md +9 -0
- package/skills/lark-base-database/references/design/components/feedback-navigation.md +9 -0
- package/skills/lark-base-database/references/design/components/index.md +18 -0
- package/skills/lark-base-database/references/design/components/overlays.md +9 -0
- package/skills/lark-base-database/references/design/components/patterns/flat-business.md +7 -0
- package/skills/lark-base-database/references/design/components/selection.md +24 -0
- package/skills/lark-base-database/references/design/components.md +91 -0
- package/skills/lark-base-database/references/design/composition.md +90 -0
- package/skills/lark-base-database/references/design/control-recipes.md +107 -0
- package/skills/lark-base-database/references/design/examples.md +172 -0
- package/skills/lark-base-database/references/design/foundations.md +126 -0
- package/skills/lark-base-database/references/design/review.md +82 -0
- package/skills/lark-base-database/references/design/scenes/analysis-report.md +66 -0
- package/skills/lark-base-database/references/design/scenes/brand-showcase.md +70 -0
- package/skills/lark-base-database/references/design/scenes/business-operations.md +85 -0
- package/skills/lark-base-database/references/design/scenes/campaign-report.md +66 -0
- package/skills/lark-base-database/references/design/scenes/dashboard.md +77 -0
- package/skills/lark-base-database/references/design/scenes/festive-campaign.md +69 -0
- package/skills/lark-base-database/references/design/theme.md +27 -0
- package/skills/lark-base-database/references/design/typography.md +56 -0
- package/skills/lark-base-database/references/development.md +165 -32
- package/skills/lark-base-database/references/environment-variables.md +55 -0
- package/skills/lark-base-database/references/faas.md +41 -48
- package/skills/lark-base-database/references/file-transfer.md +25 -25
- package/skills/lark-base-database/references/frontend-design-guide.md +33 -77
- package/skills/lark-base-database/references/mutations.md +295 -0
- package/skills/lark-base-database/references/permission-design.md +24 -24
- package/skills/lark-base-database/references/release.md +54 -27
- package/skills/lark-base-database/references/style-guide.md +10 -67
- package/skills/lark-base-database/references/verification.md +17 -13
- package/skills/lark-base-database/references/workspace.md +71 -22
- package/templates/database-starter/README.md +34 -4
- package/templates/database-starter/database-functions/README.md +8 -0
- package/templates/database-starter/mutations/README.md +19 -0
- package/templates/database-starter/page/README.md +25 -19
- package/templates/database-starter/page/package-lock.json +8 -16
- package/templates/database-starter/page/package.json +2 -3
- package/templates/database-starter/page/src/App.tsx +23 -3
- package/templates/database-starter/page/src/components/UserSelectorEditor/index.tsx +32 -4
- package/templates/database-starter/page/src/components/UserSelectorPanel/index.tsx +104 -34
- package/templates/database-starter/page/src/components/UserSelectorPanel/user-search.test.ts +55 -11
- package/templates/database-starter/page/src/components/UserSelectorPanel/user-search.ts +68 -25
- package/templates/database-starter/page/src/components/database-runtime-ready.tsx +1 -1
- package/templates/database-starter/page/src/components/database-user-menu/index.tsx +4 -7
- package/templates/database-starter/page/src/components/feedback/page-state.tsx +40 -5
- package/templates/database-starter/page/src/components/floating-button/assets/edit-active.svg +3 -0
- package/templates/database-starter/page/src/components/floating-button/assets/edit-default.svg +3 -0
- package/templates/database-starter/page/src/components/floating-button/index.tsx +75 -112
- package/templates/database-starter/page/src/components/floating-button/utils/project-meta.test.ts +107 -37
- package/templates/database-starter/page/src/components/floating-button/utils/project-meta.ts +13 -50
- package/templates/database-starter/page/src/components/ui-language.test.tsx +101 -0
- package/templates/database-starter/page/src/examples/ai-function/ImageTaskOperation.tsx +202 -0
- package/templates/database-starter/page/src/examples/ai-function/README.md +14 -0
- package/templates/database-starter/page/src/examples/ai-function/TextStreamOperation.tsx +184 -0
- package/templates/database-starter/page/src/examples/ai-function/ai-copy.ts +173 -0
- package/templates/database-starter/page/src/examples/ai-function/ai-delta.test.ts +30 -0
- package/templates/database-starter/page/src/examples/ai-function/ai-delta.ts +26 -0
- package/templates/database-starter/page/src/examples/ai-function/ai-language.test.tsx +86 -0
- package/templates/database-starter/page/src/examples/ai-function/ai-media-panel.test.ts +201 -0
- package/templates/database-starter/page/src/examples/ai-function/ai-media-panel.tsx +382 -0
- package/templates/database-starter/page/src/lib/attachment-image.tsx +144 -0
- package/templates/database-starter/page/src/lib/database-function-inputs.generated.ts +4 -0
- package/templates/database-starter/page/src/lib/database-runtime-attachment.test.ts +180 -76
- package/templates/database-starter/page/src/lib/database-runtime.test.ts +130 -10
- package/templates/database-starter/page/src/lib/database-runtime.ts +484 -51
- package/templates/database-starter/page/src/lib/date-time.ts +61 -0
- package/templates/database-starter/page/src/lib/ui-language.ts +86 -0
- package/templates/database-starter/page/src/main.tsx +1 -1
- package/templates/database-starter/page/tsconfig.json +2 -2
- package/templates/database-starter/page/vite.config.ts +1 -2
- package/templates/database-starter/prettierignore +1 -1
- package/templates/database-starter/server-functions/README.md +2 -2
- package/templates/database-starter/server-functions/package-lock.json +4 -4
- package/templates/database-starter/server-functions/package.json +1 -1
- package/dist/agent-guide.d.ts +0 -19
- package/dist/agent-guide.d.ts.map +0 -1
- package/dist/agent-guide.js +0 -31
- package/dist/agent-guide.js.map +0 -1
- package/dist/database-skill.d.ts +0 -24
- package/dist/database-skill.d.ts.map +0 -1
- package/dist/database-skill.js +0 -151
- package/dist/database-skill.js.map +0 -1
- package/skills/lark-base-database/references/base-sql-unsupported.md +0 -42
- package/skills/lark-base-database/references/database-capability-skill.md +0 -40
- package/skills/lark-base-database/references/sql-function.md +0 -289
- package/templates/database-capability/SKILL.md.tmpl +0 -58
- package/templates/database-starter/page/src/components/floating-button/chatbot-entry.test.ts +0 -23
- package/templates/database-starter/page/src/components/floating-button/chatbot-entry.tsx +0 -873
- package/templates/database-starter/page/src/components/floating-button/local-chat-panel.test.ts +0 -25
- package/templates/database-starter/page/src/components/floating-button/local-chat-panel.tsx +0 -197
- package/templates/database-starter/page/src/components/floating-button/utils/env.ts +0 -18
- package/templates/database-starter/sql/README.md +0 -8
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
fetchAttachmentCover,
|
|
4
|
+
type DatabaseAttachmentRef,
|
|
5
|
+
} from './database-runtime'
|
|
6
|
+
import { useUiLanguage, type UiLanguage } from './ui-language'
|
|
7
|
+
|
|
8
|
+
const ATTACHMENT_IMAGE_COPY = {
|
|
9
|
+
en: {
|
|
10
|
+
decodeFailed: 'Image could not be decoded',
|
|
11
|
+
loadFailed: 'Failed to load image',
|
|
12
|
+
loading: 'Loading image',
|
|
13
|
+
retry: 'Retry image',
|
|
14
|
+
},
|
|
15
|
+
zh: {
|
|
16
|
+
decodeFailed: '图片无法解码',
|
|
17
|
+
loadFailed: '图片加载失败',
|
|
18
|
+
loading: '图片加载中',
|
|
19
|
+
retry: '重试图片',
|
|
20
|
+
},
|
|
21
|
+
ja: {
|
|
22
|
+
decodeFailed: '画像を表示できませんでした',
|
|
23
|
+
loadFailed: '画像を読み込めませんでした',
|
|
24
|
+
loading: '画像を読み込み中',
|
|
25
|
+
retry: '画像を再読み込み',
|
|
26
|
+
},
|
|
27
|
+
} satisfies Record<
|
|
28
|
+
UiLanguage,
|
|
29
|
+
{
|
|
30
|
+
decodeFailed: string
|
|
31
|
+
loadFailed: string
|
|
32
|
+
loading: string
|
|
33
|
+
retry: string
|
|
34
|
+
}
|
|
35
|
+
>
|
|
36
|
+
|
|
37
|
+
/** 缩略图与大图共用;只在靠近可见区域时加载,组件持有并释放本地图片 URL。 */
|
|
38
|
+
export function AttachmentImage({
|
|
39
|
+
attachment,
|
|
40
|
+
size = 360,
|
|
41
|
+
alt,
|
|
42
|
+
className,
|
|
43
|
+
}: {
|
|
44
|
+
attachment: DatabaseAttachmentRef
|
|
45
|
+
size?: number
|
|
46
|
+
alt: string
|
|
47
|
+
className?: string
|
|
48
|
+
}) {
|
|
49
|
+
const uiLanguage = useUiLanguage()
|
|
50
|
+
const i18n = ATTACHMENT_IMAGE_COPY[uiLanguage]
|
|
51
|
+
const container = useRef<HTMLSpanElement>(null)
|
|
52
|
+
const [visible, setVisible] = useState(false)
|
|
53
|
+
const [attempt, setAttempt] = useState(0)
|
|
54
|
+
const [image, setImage] = useState<{
|
|
55
|
+
key: string
|
|
56
|
+
url: string
|
|
57
|
+
error: string
|
|
58
|
+
localError?: 'loadFailed' | 'decodeFailed'
|
|
59
|
+
}>({ key: '', url: '', error: '' })
|
|
60
|
+
const { tableId, fieldId, recordId, fileToken } = attachment
|
|
61
|
+
const key = JSON.stringify([
|
|
62
|
+
tableId,
|
|
63
|
+
fieldId,
|
|
64
|
+
recordId,
|
|
65
|
+
fileToken,
|
|
66
|
+
size,
|
|
67
|
+
attempt,
|
|
68
|
+
])
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
72
|
+
setVisible(true)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
const observer = new IntersectionObserver(
|
|
76
|
+
(entries) => {
|
|
77
|
+
if (entries.some((entry) => entry.isIntersecting)) {
|
|
78
|
+
setVisible(true)
|
|
79
|
+
observer.disconnect()
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{ rootMargin: '200px' },
|
|
83
|
+
)
|
|
84
|
+
if (container.current) observer.observe(container.current)
|
|
85
|
+
return () => observer.disconnect()
|
|
86
|
+
}, [])
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (!visible) return
|
|
90
|
+
const controller = new AbortController()
|
|
91
|
+
let url = ''
|
|
92
|
+
fetchAttachmentCover({ tableId, fieldId, recordId, fileToken }, size, {
|
|
93
|
+
signal: controller.signal,
|
|
94
|
+
})
|
|
95
|
+
.then((blob) => {
|
|
96
|
+
if (controller.signal.aborted) return
|
|
97
|
+
url = URL.createObjectURL(blob)
|
|
98
|
+
setImage({ key, url, error: '' })
|
|
99
|
+
})
|
|
100
|
+
.catch((cause: unknown) => {
|
|
101
|
+
if (!controller.signal.aborted) {
|
|
102
|
+
setImage({
|
|
103
|
+
key,
|
|
104
|
+
url: '',
|
|
105
|
+
error: cause instanceof Error ? cause.message : '',
|
|
106
|
+
localError: cause instanceof Error ? undefined : 'loadFailed',
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
return () => {
|
|
111
|
+
controller.abort()
|
|
112
|
+
if (url) URL.revokeObjectURL(url)
|
|
113
|
+
}
|
|
114
|
+
}, [visible, key, tableId, fieldId, recordId, fileToken, size])
|
|
115
|
+
|
|
116
|
+
const current = image.key === key ? image : undefined
|
|
117
|
+
return (
|
|
118
|
+
<span ref={container} className={className} aria-busy={!current}>
|
|
119
|
+
{current?.url ? (
|
|
120
|
+
<img
|
|
121
|
+
src={current.url}
|
|
122
|
+
alt={alt}
|
|
123
|
+
onError={() =>
|
|
124
|
+
setImage({ key, url: '', error: '', localError: 'decodeFailed' })
|
|
125
|
+
}
|
|
126
|
+
/>
|
|
127
|
+
) : current?.error || current?.localError ? (
|
|
128
|
+
<span>
|
|
129
|
+
<span role="status">
|
|
130
|
+
{current.localError ? i18n[current.localError] : current.error}
|
|
131
|
+
</span>
|
|
132
|
+
<button
|
|
133
|
+
type="button"
|
|
134
|
+
onClick={() => setAttempt((value) => value + 1)}
|
|
135
|
+
>
|
|
136
|
+
{i18n.retry}
|
|
137
|
+
</button>
|
|
138
|
+
</span>
|
|
139
|
+
) : (
|
|
140
|
+
<span role="status">{i18n.loading}</span>
|
|
141
|
+
)}
|
|
142
|
+
</span>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
@@ -2,10 +2,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
2
2
|
|
|
3
3
|
const getRuntimeContext = vi.fn()
|
|
4
4
|
const siteRuntimeFetch = vi.fn()
|
|
5
|
+
const invokeHttp = vi.fn()
|
|
5
6
|
|
|
6
7
|
vi.mock('@lark-base-open/base-site-channel/runtime', () => ({
|
|
7
8
|
fetch: siteRuntimeFetch,
|
|
8
|
-
invokeHttp
|
|
9
|
+
invokeHttp,
|
|
9
10
|
siteRuntime: { getRuntimeContext },
|
|
10
11
|
}))
|
|
11
12
|
|
|
@@ -25,6 +26,7 @@ async function loadDatabaseRuntime() {
|
|
|
25
26
|
beforeEach(() => {
|
|
26
27
|
getRuntimeContext.mockReset()
|
|
27
28
|
siteRuntimeFetch.mockReset()
|
|
29
|
+
invokeHttp.mockReset()
|
|
28
30
|
vi.stubGlobal('location', {
|
|
29
31
|
ancestorOrigins: ['https://ancestor.example'],
|
|
30
32
|
pathname: '/app/pathToken/',
|
|
@@ -38,89 +40,109 @@ afterEach(() => {
|
|
|
38
40
|
vi.unstubAllGlobals()
|
|
39
41
|
})
|
|
40
42
|
|
|
41
|
-
describe('
|
|
42
|
-
it('
|
|
43
|
-
getRuntimeContext.mockResolvedValue({ siteToken: '
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
expect(buildAttachmentCoverUrl(context, attachmentRef)).toMatch(
|
|
50
|
-
/^https:\/\/ancestor\.example\/space\/api\/box\/stream\/download\/v2\/cover\//,
|
|
43
|
+
describe('fetchAttachmentCover', () => {
|
|
44
|
+
it('builds an HTTP binary request with trusted runtime context and full coordinates', async () => {
|
|
45
|
+
getRuntimeContext.mockResolvedValue({ siteToken: 'siteToken', env: 'prod' })
|
|
46
|
+
invokeHttp.mockResolvedValue(
|
|
47
|
+
new Response(new Uint8Array([0, 128, 255]), {
|
|
48
|
+
headers: { 'content-type': 'image/png' },
|
|
49
|
+
}),
|
|
51
50
|
)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
await loadDatabaseRuntime()
|
|
60
|
-
const context = await resolveDatabaseContext()
|
|
61
|
-
|
|
62
|
-
expect(buildAttachmentCoverUrl(context, attachmentRef)).toMatch(
|
|
63
|
-
/^https:\/\/referrer\.example\/space\/api\/box\/stream\/download\/v2\/cover\//,
|
|
51
|
+
const controller = new AbortController()
|
|
52
|
+
const { fetchAttachmentCover } = await loadDatabaseRuntime()
|
|
53
|
+
const blob = await fetchAttachmentCover(attachmentRef, 480, {
|
|
54
|
+
signal: controller.signal,
|
|
55
|
+
})
|
|
56
|
+
expect(new Uint8Array(await blob.arrayBuffer())).toEqual(
|
|
57
|
+
new Uint8Array([0, 128, 255]),
|
|
64
58
|
)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const requested = new URL(
|
|
77
|
-
buildAttachmentCoverUrl(context, attachmentRef, 480),
|
|
59
|
+
const request = invokeHttp.mock.calls[0][0]
|
|
60
|
+
expect(request.init).toEqual({
|
|
61
|
+
method: 'GET',
|
|
62
|
+
responseType: 'arrayBuffer',
|
|
63
|
+
timeoutMs: 45000,
|
|
64
|
+
})
|
|
65
|
+
expect(request.signal).toBe(controller.signal)
|
|
66
|
+
const url = new URL(request.url, 'https://host.example')
|
|
67
|
+
expect(url.pathname).toBe(
|
|
68
|
+
'/space/api/box/stream/download/v2/cover/boxcnFileToken',
|
|
78
69
|
)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
70
|
+
expect(url.searchParams.get('width')).toBe('720')
|
|
71
|
+
expect(JSON.parse(url.searchParams.get('extra')!)).toEqual({
|
|
72
|
+
bitablePerm: {
|
|
73
|
+
siteToken: 'siteToken',
|
|
74
|
+
siteEnv: 'prod',
|
|
75
|
+
tableId: 'tblTickets',
|
|
76
|
+
attachments: { fldPhotos: { recTicket: ['boxcnFileToken'] } },
|
|
77
|
+
},
|
|
78
|
+
})
|
|
85
79
|
})
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
)
|
|
96
|
-
|
|
80
|
+
it.each([401, 403])(
|
|
81
|
+
'reports HTTP %i with LogID without retry',
|
|
82
|
+
async (status) => {
|
|
83
|
+
invokeHttp.mockResolvedValue(
|
|
84
|
+
new Response('denied', {
|
|
85
|
+
status,
|
|
86
|
+
headers: { 'x-tt-logid': 'test-log' },
|
|
87
|
+
}),
|
|
88
|
+
)
|
|
89
|
+
const { fetchAttachmentCover } = await loadDatabaseRuntime()
|
|
90
|
+
await expect(fetchAttachmentCover(attachmentRef)).rejects.toThrow(
|
|
91
|
+
`HTTP ${status}, LogID test-log`,
|
|
92
|
+
)
|
|
93
|
+
expect(invokeHttp).toHaveBeenCalledTimes(1)
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
it('rejects non-image and empty responses', async () => {
|
|
97
|
+
const { fetchAttachmentCover } = await loadDatabaseRuntime()
|
|
98
|
+
for (const response of [
|
|
99
|
+
new Response('html'),
|
|
100
|
+
new Response('', { headers: { 'content-type': 'image/png' } }),
|
|
101
|
+
]) {
|
|
102
|
+
invokeHttp.mockResolvedValue(response)
|
|
103
|
+
await expect(fetchAttachmentCover(attachmentRef)).rejects.toThrow(
|
|
104
|
+
'The host did not return a valid image',
|
|
105
|
+
)
|
|
106
|
+
}
|
|
97
107
|
})
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
attachmentRef,
|
|
110
|
-
),
|
|
108
|
+
it('shows an English error while preserving host bridge details as the cause', async () => {
|
|
109
|
+
const failure = { code: 'REQUEST_TIMEOUT', msg: 'HTTP 请求超时' }
|
|
110
|
+
invokeHttp.mockRejectedValue(failure)
|
|
111
|
+
const { fetchAttachmentCover } = await loadDatabaseRuntime()
|
|
112
|
+
await expect(fetchAttachmentCover(attachmentRef)).rejects.toMatchObject({
|
|
113
|
+
message: 'Attachment image request timed out (code REQUEST_TIMEOUT)',
|
|
114
|
+
code: 'REQUEST_TIMEOUT',
|
|
115
|
+
cause: failure,
|
|
116
|
+
})
|
|
117
|
+
await expect(fetchAttachmentCover(attachmentRef)).rejects.toBeInstanceOf(
|
|
118
|
+
Error,
|
|
111
119
|
)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
},
|
|
120
|
-
baseAppToken: 'siteToken',
|
|
120
|
+
})
|
|
121
|
+
it('preserves SDK errors and recognizes Channel timeout replies', async () => {
|
|
122
|
+
const sdkError = Object.assign(
|
|
123
|
+
new Error('当前宿主不支持 HTTP 二进制响应,请升级宿主'),
|
|
124
|
+
{
|
|
125
|
+
name: 'SiteRuntimeInvocationError',
|
|
126
|
+
code: 'HTTP_RESPONSE_TYPE_UNSUPPORTED',
|
|
121
127
|
},
|
|
128
|
+
)
|
|
129
|
+
invokeHttp.mockRejectedValue(sdkError)
|
|
130
|
+
const { fetchAttachmentCover } = await loadDatabaseRuntime()
|
|
131
|
+
await expect(fetchAttachmentCover(attachmentRef)).rejects.toBe(sdkError)
|
|
132
|
+
|
|
133
|
+
invokeHttp.mockRejectedValue({ code: 10005, msg: '超时' })
|
|
134
|
+
await expect(fetchAttachmentCover(attachmentRef)).rejects.toMatchObject({
|
|
135
|
+
message: 'Attachment image request timed out (code 10005)',
|
|
136
|
+
code: '10005',
|
|
122
137
|
})
|
|
123
138
|
})
|
|
139
|
+
it('does not request an already canceled image', async () => {
|
|
140
|
+
const { fetchAttachmentCover } = await loadDatabaseRuntime()
|
|
141
|
+
await expect(
|
|
142
|
+
fetchAttachmentCover(attachmentRef, 360, { signal: AbortSignal.abort() }),
|
|
143
|
+
).rejects.toMatchObject({ name: 'AbortError' })
|
|
144
|
+
expect(invokeHttp).not.toHaveBeenCalled()
|
|
145
|
+
})
|
|
124
146
|
})
|
|
125
147
|
|
|
126
148
|
describe('fetchAttachmentDownloadUrl', () => {
|
|
@@ -130,6 +152,7 @@ describe('fetchAttachmentDownloadUrl', () => {
|
|
|
130
152
|
siteRuntimeFetch.mockResolvedValue({
|
|
131
153
|
json: () =>
|
|
132
154
|
Promise.resolve({
|
|
155
|
+
code: 0,
|
|
133
156
|
data: { url: 'https://download.example/authcode/?code=signed' },
|
|
134
157
|
}),
|
|
135
158
|
ok: true,
|
|
@@ -161,11 +184,92 @@ describe('fetchAttachmentDownloadUrl', () => {
|
|
|
161
184
|
attachments: {
|
|
162
185
|
fldPhotos: { recTicket: ['boxcnFileToken'] },
|
|
163
186
|
},
|
|
164
|
-
|
|
187
|
+
siteToken: 'siteToken',
|
|
188
|
+
siteEnv: 'prod',
|
|
165
189
|
},
|
|
166
190
|
}),
|
|
167
191
|
file_token: 'boxcnFileToken',
|
|
168
192
|
})
|
|
169
193
|
},
|
|
170
194
|
)
|
|
195
|
+
|
|
196
|
+
it.each([200, 403])(
|
|
197
|
+
'reports the business error before checking data.url for HTTP %i',
|
|
198
|
+
async (status) => {
|
|
199
|
+
const body = { code: 800008201, msg: '对外返回' }
|
|
200
|
+
siteRuntimeFetch.mockResolvedValue(
|
|
201
|
+
new Response(JSON.stringify(body), {
|
|
202
|
+
status,
|
|
203
|
+
headers: { 'x-tt-logid': 'attachment-log' },
|
|
204
|
+
}),
|
|
205
|
+
)
|
|
206
|
+
const { fetchAttachmentDownloadUrl } = await loadDatabaseRuntime()
|
|
207
|
+
|
|
208
|
+
await expect(
|
|
209
|
+
fetchAttachmentDownloadUrl(
|
|
210
|
+
{ env: 'prod', gatewayOrigin: '', token: 'siteToken' },
|
|
211
|
+
attachmentRef,
|
|
212
|
+
),
|
|
213
|
+
).rejects.toMatchObject({
|
|
214
|
+
code: '800008201',
|
|
215
|
+
cause: body,
|
|
216
|
+
message:
|
|
217
|
+
'Attachment service could not provide a download URL (code 800008201, LogID attachment-log)',
|
|
218
|
+
})
|
|
219
|
+
},
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
it('reports malformed successful responses without claiming the URL alone is missing', async () => {
|
|
223
|
+
siteRuntimeFetch.mockResolvedValue(
|
|
224
|
+
new Response('not JSON', {
|
|
225
|
+
status: 200,
|
|
226
|
+
headers: { 'x-tt-logid': 'invalid-json-log' },
|
|
227
|
+
}),
|
|
228
|
+
)
|
|
229
|
+
const { fetchAttachmentDownloadUrl } = await loadDatabaseRuntime()
|
|
230
|
+
|
|
231
|
+
await expect(
|
|
232
|
+
fetchAttachmentDownloadUrl(
|
|
233
|
+
{ env: 'prod', gatewayOrigin: '', token: 'siteToken' },
|
|
234
|
+
attachmentRef,
|
|
235
|
+
),
|
|
236
|
+
).rejects.toThrow(
|
|
237
|
+
'Attachment download URL response is not valid JSON, LogID invalid-json-log',
|
|
238
|
+
)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('includes the gateway LogID when a successful response lacks data.url', async () => {
|
|
242
|
+
siteRuntimeFetch.mockResolvedValue(
|
|
243
|
+
new Response(JSON.stringify({ code: 0, data: {} }), {
|
|
244
|
+
status: 200,
|
|
245
|
+
headers: { 'x-tt-logid': 'missing-url-log' },
|
|
246
|
+
}),
|
|
247
|
+
)
|
|
248
|
+
const { fetchAttachmentDownloadUrl } = await loadDatabaseRuntime()
|
|
249
|
+
await expect(
|
|
250
|
+
fetchAttachmentDownloadUrl(
|
|
251
|
+
{ env: 'prod', gatewayOrigin: '', token: 'siteToken' },
|
|
252
|
+
attachmentRef,
|
|
253
|
+
),
|
|
254
|
+
).rejects.toThrow(
|
|
255
|
+
'Attachment download URL response is missing data.url, LogID missing-url-log',
|
|
256
|
+
)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('does not surface a host bridge message that may be untranslated', async () => {
|
|
260
|
+
const failure = { code: 'REQUEST_TIMEOUT', msg: 'HTTP 请求超时' }
|
|
261
|
+
siteRuntimeFetch.mockRejectedValue(failure)
|
|
262
|
+
const { fetchAttachmentDownloadUrl } = await loadDatabaseRuntime()
|
|
263
|
+
|
|
264
|
+
await expect(
|
|
265
|
+
fetchAttachmentDownloadUrl(
|
|
266
|
+
{ env: 'prod', gatewayOrigin: '', token: 'siteToken' },
|
|
267
|
+
attachmentRef,
|
|
268
|
+
),
|
|
269
|
+
).rejects.toMatchObject({
|
|
270
|
+
message:
|
|
271
|
+
'Attachment download URL request timed out (code REQUEST_TIMEOUT)',
|
|
272
|
+
cause: failure,
|
|
273
|
+
})
|
|
274
|
+
})
|
|
171
275
|
})
|
|
@@ -2,7 +2,10 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
2
2
|
import {
|
|
3
3
|
buildDatabaseInvokeFunctionPath,
|
|
4
4
|
buildDatabaseInvokeGetPath,
|
|
5
|
+
getSiteAIFunctionTask,
|
|
5
6
|
getDatabaseContext,
|
|
7
|
+
hasRunnableMediaTask,
|
|
8
|
+
normalizeSiteAIMediaTask,
|
|
6
9
|
} from './database-runtime'
|
|
7
10
|
|
|
8
11
|
function stubPathname(pathname: string): void {
|
|
@@ -43,21 +46,23 @@ describe('getDatabaseContext', () => {
|
|
|
43
46
|
})
|
|
44
47
|
|
|
45
48
|
describe('buildDatabaseInvokeGetPath', () => {
|
|
46
|
-
it('builds the prod path
|
|
49
|
+
it('builds the prod metadata path', () => {
|
|
47
50
|
expect(
|
|
48
|
-
buildDatabaseInvokeGetPath(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
buildDatabaseInvokeGetPath({
|
|
52
|
+
env: 'prod',
|
|
53
|
+
gatewayOrigin: '',
|
|
54
|
+
token: 'bascnDemo',
|
|
55
|
+
}),
|
|
56
|
+
).toBe('/space/api/bitable/site/v1/bascnDemo/invoke_get')
|
|
53
57
|
})
|
|
54
58
|
|
|
55
59
|
it('adds the preview env for draft artifacts', () => {
|
|
56
60
|
expect(
|
|
57
|
-
buildDatabaseInvokeGetPath(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
buildDatabaseInvokeGetPath({
|
|
62
|
+
env: 'preview',
|
|
63
|
+
gatewayOrigin: '',
|
|
64
|
+
token: 'bascnDemo',
|
|
65
|
+
}),
|
|
61
66
|
).toBe('/space/api/bitable/site/v1/bascnDemo/invoke_get?env=preview')
|
|
62
67
|
})
|
|
63
68
|
})
|
|
@@ -101,3 +106,118 @@ describe('buildDatabaseInvokeFunctionPath', () => {
|
|
|
101
106
|
)
|
|
102
107
|
})
|
|
103
108
|
})
|
|
109
|
+
|
|
110
|
+
describe('normalizeSiteAIMediaTask', () => {
|
|
111
|
+
it('unwraps the common OpenAPI response envelope', () => {
|
|
112
|
+
expect(
|
|
113
|
+
normalizeSiteAIMediaTask({
|
|
114
|
+
code: 0,
|
|
115
|
+
data: {
|
|
116
|
+
capability_type: 'image',
|
|
117
|
+
request_id: 'req-create',
|
|
118
|
+
status: 'queued',
|
|
119
|
+
task_id: 'task-1',
|
|
120
|
+
type: 'image_task',
|
|
121
|
+
},
|
|
122
|
+
msg: 'success',
|
|
123
|
+
}),
|
|
124
|
+
).toEqual({
|
|
125
|
+
capabilityType: 'image',
|
|
126
|
+
requestId: 'req-create',
|
|
127
|
+
status: 'queued',
|
|
128
|
+
taskId: 'task-1',
|
|
129
|
+
type: 'image_task',
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('surfaces OpenAPI business errors instead of returning an empty task', () => {
|
|
134
|
+
expect(() =>
|
|
135
|
+
normalizeSiteAIMediaTask({
|
|
136
|
+
code: 800008006,
|
|
137
|
+
data: null,
|
|
138
|
+
msg: 'record sink field mapping is invalid',
|
|
139
|
+
}),
|
|
140
|
+
).toThrow('record sink field mapping is invalid')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('surfaces thrift-style BaseResp business errors', () => {
|
|
144
|
+
expect(() =>
|
|
145
|
+
normalizeSiteAIMediaTask({
|
|
146
|
+
BaseResp: {
|
|
147
|
+
StatusCode: 800008006,
|
|
148
|
+
StatusMessage: 'record sink field mapping is invalid',
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
).toThrow('record sink field mapping is invalid')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('normalizes thrift-style task field names', () => {
|
|
155
|
+
expect(
|
|
156
|
+
normalizeSiteAIMediaTask({
|
|
157
|
+
BaseResp: { StatusCode: 0, StatusMessage: 'success' },
|
|
158
|
+
CapabilityType: 'image',
|
|
159
|
+
ImageAssets: [
|
|
160
|
+
{
|
|
161
|
+
SafetyStatus: 'pass',
|
|
162
|
+
Token: 'token-1',
|
|
163
|
+
URL: 'https://example.com/image.png',
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
Progress: 100,
|
|
167
|
+
RequestID: 'req-get',
|
|
168
|
+
Status: 'succeeded',
|
|
169
|
+
TaskID: 'task-1',
|
|
170
|
+
Type: 'image_task',
|
|
171
|
+
}),
|
|
172
|
+
).toEqual({
|
|
173
|
+
capabilityType: 'image',
|
|
174
|
+
imageAssets: [
|
|
175
|
+
{
|
|
176
|
+
safetyStatus: 'pass',
|
|
177
|
+
token: 'token-1',
|
|
178
|
+
url: 'https://example.com/image.png',
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
progress: 100,
|
|
182
|
+
requestId: 'req-get',
|
|
183
|
+
status: 'succeeded',
|
|
184
|
+
taskId: 'task-1',
|
|
185
|
+
type: 'image_task',
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('rejects a successful response without a taskId', () => {
|
|
190
|
+
expect(() =>
|
|
191
|
+
normalizeSiteAIMediaTask({
|
|
192
|
+
code: 0,
|
|
193
|
+
data: { requestId: 'req-create', status: 'queued', type: 'image_task' },
|
|
194
|
+
msg: 'success',
|
|
195
|
+
}),
|
|
196
|
+
).toThrow('AI Function task response has an invalid structure')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('only treats queued and running tasks with a taskId as pollable', () => {
|
|
200
|
+
expect(
|
|
201
|
+
hasRunnableMediaTask({
|
|
202
|
+
requestId: 'req-create',
|
|
203
|
+
status: 'queued',
|
|
204
|
+
taskId: 'task-1',
|
|
205
|
+
type: 'image_task',
|
|
206
|
+
}),
|
|
207
|
+
).toBe(true)
|
|
208
|
+
expect(
|
|
209
|
+
hasRunnableMediaTask({
|
|
210
|
+
requestId: 'req-create',
|
|
211
|
+
status: 'queued',
|
|
212
|
+
taskId: '',
|
|
213
|
+
type: 'image_task',
|
|
214
|
+
}),
|
|
215
|
+
).toBe(false)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('rejects polling before transport when taskId is empty', async () => {
|
|
219
|
+
await expect(getSiteAIFunctionTask('image', '')).rejects.toThrow(
|
|
220
|
+
'AI Function task ID must not be empty',
|
|
221
|
+
)
|
|
222
|
+
})
|
|
223
|
+
})
|