@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.
Files changed (222) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/LICENSE +6 -0
  3. package/README.md +55 -31
  4. package/dist/ai-functions.d.ts +42 -0
  5. package/dist/ai-functions.d.ts.map +1 -0
  6. package/dist/ai-functions.js +327 -0
  7. package/dist/ai-functions.js.map +1 -0
  8. package/dist/archive.d.ts +21 -0
  9. package/dist/archive.d.ts.map +1 -1
  10. package/dist/archive.js +65 -10
  11. package/dist/archive.js.map +1 -1
  12. package/dist/auth.d.ts +8 -1
  13. package/dist/auth.d.ts.map +1 -1
  14. package/dist/auth.js +60 -6
  15. package/dist/auth.js.map +1 -1
  16. package/dist/check.d.ts +74 -0
  17. package/dist/check.d.ts.map +1 -0
  18. package/dist/check.js +283 -0
  19. package/dist/check.js.map +1 -0
  20. package/dist/dependency-policy.d.ts +10 -0
  21. package/dist/dependency-policy.d.ts.map +1 -0
  22. package/dist/dependency-policy.js +162 -0
  23. package/dist/dependency-policy.js.map +1 -0
  24. package/dist/deploy.d.ts +1 -0
  25. package/dist/deploy.d.ts.map +1 -1
  26. package/dist/deploy.js +122 -175
  27. package/dist/deploy.js.map +1 -1
  28. package/dist/environment-variables.d.ts +52 -0
  29. package/dist/environment-variables.d.ts.map +1 -0
  30. package/dist/environment-variables.js +563 -0
  31. package/dist/environment-variables.js.map +1 -0
  32. package/dist/environment.d.ts +1 -1
  33. package/dist/environment.js +1 -1
  34. package/dist/environment.js.map +1 -1
  35. package/dist/function-input-types.d.ts +10 -0
  36. package/dist/function-input-types.d.ts.map +1 -0
  37. package/dist/function-input-types.js +160 -0
  38. package/dist/function-input-types.js.map +1 -0
  39. package/dist/function-schema.d.ts.map +1 -1
  40. package/dist/function-schema.js +89 -36
  41. package/dist/function-schema.js.map +1 -1
  42. package/dist/init-field-retry.d.ts +17 -0
  43. package/dist/init-field-retry.d.ts.map +1 -0
  44. package/dist/init-field-retry.js +127 -0
  45. package/dist/init-field-retry.js.map +1 -0
  46. package/dist/init-journal.d.ts +98 -0
  47. package/dist/init-journal.d.ts.map +1 -0
  48. package/dist/init-journal.js +492 -0
  49. package/dist/init-journal.js.map +1 -0
  50. package/dist/init-schema.d.ts +8 -0
  51. package/dist/init-schema.d.ts.map +1 -0
  52. package/dist/init-schema.js +423 -0
  53. package/dist/init-schema.js.map +1 -0
  54. package/dist/init.d.ts +14 -0
  55. package/dist/init.d.ts.map +1 -1
  56. package/dist/init.js +886 -243
  57. package/dist/init.js.map +1 -1
  58. package/dist/lark-cli-error.d.ts +46 -0
  59. package/dist/lark-cli-error.d.ts.map +1 -0
  60. package/dist/lark-cli-error.js +171 -0
  61. package/dist/lark-cli-error.js.map +1 -0
  62. package/dist/lark-cli.d.ts +2 -1
  63. package/dist/lark-cli.d.ts.map +1 -1
  64. package/dist/lark-cli.js +292 -29
  65. package/dist/lark-cli.js.map +1 -1
  66. package/dist/mutation-schema.d.ts +146 -0
  67. package/dist/mutation-schema.d.ts.map +1 -0
  68. package/dist/mutation-schema.js +153 -0
  69. package/dist/mutation-schema.js.map +1 -0
  70. package/dist/mutation.d.ts +10 -0
  71. package/dist/mutation.d.ts.map +1 -0
  72. package/dist/mutation.js +131 -0
  73. package/dist/mutation.js.map +1 -0
  74. package/dist/npm.d.ts +1 -0
  75. package/dist/npm.d.ts.map +1 -1
  76. package/dist/npm.js +2 -2
  77. package/dist/npm.js.map +1 -1
  78. package/dist/preview-receipt.d.ts.map +1 -1
  79. package/dist/preview-receipt.js +15 -5
  80. package/dist/preview-receipt.js.map +1 -1
  81. package/dist/process.d.ts +3 -1
  82. package/dist/process.d.ts.map +1 -1
  83. package/dist/process.js +16 -4
  84. package/dist/process.js.map +1 -1
  85. package/dist/program.d.ts.map +1 -1
  86. package/dist/program.js +241 -42
  87. package/dist/program.js.map +1 -1
  88. package/dist/project-build.d.ts +25 -0
  89. package/dist/project-build.d.ts.map +1 -0
  90. package/dist/project-build.js +229 -0
  91. package/dist/project-build.js.map +1 -0
  92. package/dist/project-context.d.ts +344 -0
  93. package/dist/project-context.d.ts.map +1 -0
  94. package/dist/project-context.js +485 -0
  95. package/dist/project-context.js.map +1 -0
  96. package/dist/project-layout.d.ts +5 -0
  97. package/dist/project-layout.d.ts.map +1 -0
  98. package/dist/project-layout.js +24 -0
  99. package/dist/project-layout.js.map +1 -0
  100. package/dist/public-sql-error.d.ts +25 -0
  101. package/dist/public-sql-error.d.ts.map +1 -0
  102. package/dist/public-sql-error.js +81 -0
  103. package/dist/public-sql-error.js.map +1 -0
  104. package/dist/pull.d.ts.map +1 -1
  105. package/dist/pull.js +54 -15
  106. package/dist/pull.js.map +1 -1
  107. package/dist/site-readme.d.ts +3 -0
  108. package/dist/site-readme.d.ts.map +1 -0
  109. package/dist/site-readme.js +83 -0
  110. package/dist/site-readme.js.map +1 -0
  111. package/dist/workspace-fingerprint.d.ts +21 -1
  112. package/dist/workspace-fingerprint.d.ts.map +1 -1
  113. package/dist/workspace-fingerprint.js +81 -2
  114. package/dist/workspace-fingerprint.js.map +1 -1
  115. package/package.json +2 -1
  116. package/skills/lark-base-database/SKILL.md +70 -53
  117. package/skills/lark-base-database/references/ai-function.md +471 -0
  118. package/skills/lark-base-database/references/base-capability-routing.md +3 -3
  119. package/skills/lark-base-database/references/database-function-limitations.md +46 -0
  120. package/skills/lark-base-database/references/database-function.md +349 -0
  121. package/skills/lark-base-database/references/design/components/actions-fields.md +16 -0
  122. package/skills/lark-base-database/references/design/components/code/controls.css +122 -0
  123. package/skills/lark-base-database/references/design/components/code/controls.tsx +73 -0
  124. package/skills/lark-base-database/references/design/components/code/date-picker.tsx +42 -0
  125. package/skills/lark-base-database/references/design/components/code/floating.tsx +41 -0
  126. package/skills/lark-base-database/references/design/components/code/overlays.tsx +19 -0
  127. package/skills/lark-base-database/references/design/components/code/select.tsx +34 -0
  128. package/skills/lark-base-database/references/design/components/code/styles.d.ts +1 -0
  129. package/skills/lark-base-database/references/design/components/code/theme.css +49 -0
  130. package/skills/lark-base-database/references/design/components/code/usage.tsx +38 -0
  131. package/skills/lark-base-database/references/design/components/date-time-number.md +9 -0
  132. package/skills/lark-base-database/references/design/components/feedback-navigation.md +9 -0
  133. package/skills/lark-base-database/references/design/components/index.md +18 -0
  134. package/skills/lark-base-database/references/design/components/overlays.md +9 -0
  135. package/skills/lark-base-database/references/design/components/patterns/flat-business.md +7 -0
  136. package/skills/lark-base-database/references/design/components/selection.md +24 -0
  137. package/skills/lark-base-database/references/design/components.md +91 -0
  138. package/skills/lark-base-database/references/design/composition.md +90 -0
  139. package/skills/lark-base-database/references/design/control-recipes.md +107 -0
  140. package/skills/lark-base-database/references/design/examples.md +172 -0
  141. package/skills/lark-base-database/references/design/foundations.md +126 -0
  142. package/skills/lark-base-database/references/design/review.md +82 -0
  143. package/skills/lark-base-database/references/design/scenes/analysis-report.md +66 -0
  144. package/skills/lark-base-database/references/design/scenes/brand-showcase.md +70 -0
  145. package/skills/lark-base-database/references/design/scenes/business-operations.md +85 -0
  146. package/skills/lark-base-database/references/design/scenes/campaign-report.md +66 -0
  147. package/skills/lark-base-database/references/design/scenes/dashboard.md +77 -0
  148. package/skills/lark-base-database/references/design/scenes/festive-campaign.md +69 -0
  149. package/skills/lark-base-database/references/design/theme.md +27 -0
  150. package/skills/lark-base-database/references/design/typography.md +56 -0
  151. package/skills/lark-base-database/references/development.md +165 -32
  152. package/skills/lark-base-database/references/environment-variables.md +55 -0
  153. package/skills/lark-base-database/references/faas.md +41 -48
  154. package/skills/lark-base-database/references/file-transfer.md +25 -25
  155. package/skills/lark-base-database/references/frontend-design-guide.md +33 -77
  156. package/skills/lark-base-database/references/mutations.md +295 -0
  157. package/skills/lark-base-database/references/permission-design.md +24 -24
  158. package/skills/lark-base-database/references/release.md +54 -27
  159. package/skills/lark-base-database/references/style-guide.md +10 -67
  160. package/skills/lark-base-database/references/verification.md +17 -13
  161. package/skills/lark-base-database/references/workspace.md +71 -22
  162. package/templates/database-starter/README.md +34 -4
  163. package/templates/database-starter/database-functions/README.md +8 -0
  164. package/templates/database-starter/mutations/README.md +19 -0
  165. package/templates/database-starter/page/README.md +25 -19
  166. package/templates/database-starter/page/package-lock.json +8 -16
  167. package/templates/database-starter/page/package.json +2 -3
  168. package/templates/database-starter/page/src/App.tsx +23 -3
  169. package/templates/database-starter/page/src/components/UserSelectorEditor/index.tsx +32 -4
  170. package/templates/database-starter/page/src/components/UserSelectorPanel/index.tsx +104 -34
  171. package/templates/database-starter/page/src/components/UserSelectorPanel/user-search.test.ts +55 -11
  172. package/templates/database-starter/page/src/components/UserSelectorPanel/user-search.ts +68 -25
  173. package/templates/database-starter/page/src/components/database-runtime-ready.tsx +1 -1
  174. package/templates/database-starter/page/src/components/database-user-menu/index.tsx +4 -7
  175. package/templates/database-starter/page/src/components/feedback/page-state.tsx +40 -5
  176. package/templates/database-starter/page/src/components/floating-button/assets/edit-active.svg +3 -0
  177. package/templates/database-starter/page/src/components/floating-button/assets/edit-default.svg +3 -0
  178. package/templates/database-starter/page/src/components/floating-button/index.tsx +75 -112
  179. package/templates/database-starter/page/src/components/floating-button/utils/project-meta.test.ts +107 -37
  180. package/templates/database-starter/page/src/components/floating-button/utils/project-meta.ts +13 -50
  181. package/templates/database-starter/page/src/components/ui-language.test.tsx +101 -0
  182. package/templates/database-starter/page/src/examples/ai-function/ImageTaskOperation.tsx +202 -0
  183. package/templates/database-starter/page/src/examples/ai-function/README.md +14 -0
  184. package/templates/database-starter/page/src/examples/ai-function/TextStreamOperation.tsx +184 -0
  185. package/templates/database-starter/page/src/examples/ai-function/ai-copy.ts +173 -0
  186. package/templates/database-starter/page/src/examples/ai-function/ai-delta.test.ts +30 -0
  187. package/templates/database-starter/page/src/examples/ai-function/ai-delta.ts +26 -0
  188. package/templates/database-starter/page/src/examples/ai-function/ai-language.test.tsx +86 -0
  189. package/templates/database-starter/page/src/examples/ai-function/ai-media-panel.test.ts +201 -0
  190. package/templates/database-starter/page/src/examples/ai-function/ai-media-panel.tsx +382 -0
  191. package/templates/database-starter/page/src/lib/attachment-image.tsx +144 -0
  192. package/templates/database-starter/page/src/lib/database-function-inputs.generated.ts +4 -0
  193. package/templates/database-starter/page/src/lib/database-runtime-attachment.test.ts +180 -76
  194. package/templates/database-starter/page/src/lib/database-runtime.test.ts +130 -10
  195. package/templates/database-starter/page/src/lib/database-runtime.ts +484 -51
  196. package/templates/database-starter/page/src/lib/date-time.ts +61 -0
  197. package/templates/database-starter/page/src/lib/ui-language.ts +86 -0
  198. package/templates/database-starter/page/src/main.tsx +1 -1
  199. package/templates/database-starter/page/tsconfig.json +2 -2
  200. package/templates/database-starter/page/vite.config.ts +1 -2
  201. package/templates/database-starter/prettierignore +1 -1
  202. package/templates/database-starter/server-functions/README.md +2 -2
  203. package/templates/database-starter/server-functions/package-lock.json +4 -4
  204. package/templates/database-starter/server-functions/package.json +1 -1
  205. package/dist/agent-guide.d.ts +0 -19
  206. package/dist/agent-guide.d.ts.map +0 -1
  207. package/dist/agent-guide.js +0 -31
  208. package/dist/agent-guide.js.map +0 -1
  209. package/dist/database-skill.d.ts +0 -24
  210. package/dist/database-skill.d.ts.map +0 -1
  211. package/dist/database-skill.js +0 -151
  212. package/dist/database-skill.js.map +0 -1
  213. package/skills/lark-base-database/references/base-sql-unsupported.md +0 -42
  214. package/skills/lark-base-database/references/database-capability-skill.md +0 -40
  215. package/skills/lark-base-database/references/sql-function.md +0 -289
  216. package/templates/database-capability/SKILL.md.tmpl +0 -58
  217. package/templates/database-starter/page/src/components/floating-button/chatbot-entry.test.ts +0 -23
  218. package/templates/database-starter/page/src/components/floating-button/chatbot-entry.tsx +0 -873
  219. package/templates/database-starter/page/src/components/floating-button/local-chat-panel.test.ts +0 -25
  220. package/templates/database-starter/page/src/components/floating-button/local-chat-panel.tsx +0 -197
  221. package/templates/database-starter/page/src/components/floating-button/utils/env.ts +0 -18
  222. 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
+ }
@@ -0,0 +1,4 @@
1
+ /* Generated by base-database-cli from Function metadata. Do not edit. */
2
+ // prettier-ignore
3
+ export type DatabaseFunctionInputMap = {
4
+ }
@@ -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: vi.fn(),
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('buildAttachmentCoverUrl', () => {
42
- it('uses the ancestor origin before the referrer', async () => {
43
- getRuntimeContext.mockResolvedValue({ siteToken: 'hostToken' })
44
-
45
- const { buildAttachmentCoverUrl, resolveDatabaseContext } =
46
- await loadDatabaseRuntime()
47
- const context = await resolveDatabaseContext()
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
- it('falls back to the referrer origin when ancestorOrigins is unavailable', async () => {
55
- vi.stubGlobal('location', { pathname: '/app/pathToken/' })
56
- getRuntimeContext.mockResolvedValue({ siteToken: 'hostToken' })
57
-
58
- const { buildAttachmentCoverUrl, resolveDatabaseContext } =
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
- it('snaps the requested size up and defaults to 360', async () => {
68
- getRuntimeContext.mockResolvedValue({})
69
- const { buildAttachmentCoverUrl } = await loadDatabaseRuntime()
70
- const context = {
71
- env: 'prod',
72
- gatewayOrigin: 'https://gateway.example',
73
- token: 'siteToken',
74
- } as const
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
- const defaulted = new URL(buildAttachmentCoverUrl(context, attachmentRef))
80
-
81
- expect(requested.searchParams.get('width')).toBe('720')
82
- expect(requested.searchParams.get('height')).toBe('720')
83
- expect(defaulted.searchParams.get('width')).toBe('360')
84
- expect(defaulted.searchParams.get('height')).toBe('360')
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
- it('returns an empty string when the gateway origin is missing', async () => {
88
- getRuntimeContext.mockResolvedValue({})
89
- const { buildAttachmentCoverUrl } = await loadDatabaseRuntime()
90
-
91
- expect(
92
- buildAttachmentCoverUrl(
93
- { env: 'prod', gatewayOrigin: '', token: 'siteToken' },
94
- attachmentRef,
95
- ),
96
- ).toBe('')
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
- it('puts the complete attachment location and site token in extra', async () => {
100
- getRuntimeContext.mockResolvedValue({})
101
- const { buildAttachmentCoverUrl } = await loadDatabaseRuntime()
102
- const url = new URL(
103
- buildAttachmentCoverUrl(
104
- {
105
- env: 'prod',
106
- gatewayOrigin: 'https://gateway.example',
107
- token: 'siteToken',
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
- const extra = JSON.parse(String(url.searchParams.get('extra'))) as unknown
113
-
114
- expect(extra).toEqual({
115
- bitablePerm: {
116
- tableId: 'tblTickets',
117
- attachments: {
118
- fldPhotos: { recTicket: ['boxcnFileToken'] },
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
- baseAppToken: 'siteToken',
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 with the chatbot flag', () => {
49
+ it('builds the prod metadata path', () => {
47
50
  expect(
48
- buildDatabaseInvokeGetPath(
49
- { env: 'prod', gatewayOrigin: '', token: 'bascnDemo' },
50
- true,
51
- ),
52
- ).toBe('/space/api/bitable/site/v1/bascnDemo/invoke_get?withChatbot=true')
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
- { env: 'preview', gatewayOrigin: '', token: 'bascnDemo' },
59
- false,
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
+ })