@lark-base-open/base-database-cli 1.1.1 → 1.3.8

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 (199) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/LICENSE +6 -0
  3. package/README.md +47 -27
  4. package/dist/ai-functions.d.ts +53 -0
  5. package/dist/ai-functions.d.ts.map +1 -0
  6. package/dist/ai-functions.js +436 -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 +69 -10
  11. package/dist/archive.js.map +1 -1
  12. package/dist/auth.d.ts +5 -0
  13. package/dist/auth.d.ts.map +1 -1
  14. package/dist/auth.js +26 -3
  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 +5 -0
  25. package/dist/deploy.d.ts.map +1 -1
  26. package/dist/deploy.js +141 -176
  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 +15 -1
  55. package/dist/init.d.ts.map +1 -1
  56. package/dist/init.js +884 -261
  57. package/dist/init.js.map +1 -1
  58. package/dist/lark-cli-error.d.ts +47 -0
  59. package/dist/lark-cli-error.d.ts.map +1 -0
  60. package/dist/lark-cli-error.js +201 -0
  61. package/dist/lark-cli-error.js.map +1 -0
  62. package/dist/lark-cli.d.ts +1 -1
  63. package/dist/lark-cli.d.ts.map +1 -1
  64. package/dist/lark-cli.js +78 -18
  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 +246 -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 +65 -0
  103. package/dist/public-sql-error.js.map +1 -0
  104. package/dist/pull.d.ts +1 -0
  105. package/dist/pull.d.ts.map +1 -1
  106. package/dist/pull.js +67 -15
  107. package/dist/pull.js.map +1 -1
  108. package/dist/site-readme.d.ts +3 -0
  109. package/dist/site-readme.d.ts.map +1 -0
  110. package/dist/site-readme.js +83 -0
  111. package/dist/site-readme.js.map +1 -0
  112. package/dist/workspace-fingerprint.d.ts +21 -1
  113. package/dist/workspace-fingerprint.d.ts.map +1 -1
  114. package/dist/workspace-fingerprint.js +81 -2
  115. package/dist/workspace-fingerprint.js.map +1 -1
  116. package/package.json +2 -1
  117. package/skills/lark-base-database/SKILL.md +86 -52
  118. package/skills/lark-base-database/references/ai-function.md +479 -0
  119. package/skills/lark-base-database/references/base-capability-routing.md +7 -7
  120. package/skills/lark-base-database/references/base-sql-errors.generated.md +403 -0
  121. package/skills/lark-base-database/references/database-function-limitations.md +29 -0
  122. package/skills/lark-base-database/references/database-function.md +323 -0
  123. package/skills/lark-base-database/references/development.md +166 -33
  124. package/skills/lark-base-database/references/environment-variables.md +55 -0
  125. package/skills/lark-base-database/references/faas.md +46 -53
  126. package/skills/lark-base-database/references/file-transfer.md +25 -25
  127. package/skills/lark-base-database/references/frontend-design-guide.md +151 -65
  128. package/skills/lark-base-database/references/mutations.md +295 -0
  129. package/skills/lark-base-database/references/notifications.md +9 -9
  130. package/skills/lark-base-database/references/permission-design.md +24 -24
  131. package/skills/lark-base-database/references/release.md +57 -30
  132. package/skills/lark-base-database/references/style-guide.md +10 -67
  133. package/skills/lark-base-database/references/verification.md +19 -13
  134. package/skills/lark-base-database/references/workspace.md +57 -17
  135. package/templates/database-starter/README.md +34 -4
  136. package/templates/database-starter/database-functions/README.md +8 -0
  137. package/templates/database-starter/gitignore +7 -0
  138. package/templates/database-starter/mutations/README.md +19 -0
  139. package/templates/database-starter/page/README.md +25 -19
  140. package/templates/database-starter/page/package-lock.json +8 -16
  141. package/templates/database-starter/page/package.json +2 -3
  142. package/templates/database-starter/page/src/App.tsx +23 -3
  143. package/templates/database-starter/page/src/components/UserSelectorEditor/index.tsx +32 -4
  144. package/templates/database-starter/page/src/components/UserSelectorPanel/index.tsx +104 -34
  145. package/templates/database-starter/page/src/components/UserSelectorPanel/user-search.test.ts +55 -11
  146. package/templates/database-starter/page/src/components/UserSelectorPanel/user-search.ts +68 -25
  147. package/templates/database-starter/page/src/components/database-runtime-ready.tsx +1 -1
  148. package/templates/database-starter/page/src/components/database-user-menu/index.tsx +4 -7
  149. package/templates/database-starter/page/src/components/feedback/page-state.tsx +40 -5
  150. package/templates/database-starter/page/src/components/floating-button/assets/edit-active.svg +3 -0
  151. package/templates/database-starter/page/src/components/floating-button/assets/edit-default.svg +3 -0
  152. package/templates/database-starter/page/src/components/floating-button/assets/toast-close.svg +3 -0
  153. package/templates/database-starter/page/src/components/floating-button/assets/toast-info.svg +5 -0
  154. package/templates/database-starter/page/src/components/floating-button/assets/tooltip-caret.svg +3 -0
  155. package/templates/database-starter/page/src/components/floating-button/index.tsx +193 -113
  156. package/templates/database-starter/page/src/components/floating-button/utils/project-meta.test.ts +107 -37
  157. package/templates/database-starter/page/src/components/floating-button/utils/project-meta.ts +13 -50
  158. package/templates/database-starter/page/src/components/ui-language.test.tsx +101 -0
  159. package/templates/database-starter/page/src/examples/ai-function/ImageTaskOperation.tsx +202 -0
  160. package/templates/database-starter/page/src/examples/ai-function/README.md +14 -0
  161. package/templates/database-starter/page/src/examples/ai-function/TextStreamOperation.tsx +184 -0
  162. package/templates/database-starter/page/src/examples/ai-function/ai-copy.ts +173 -0
  163. package/templates/database-starter/page/src/examples/ai-function/ai-delta.test.ts +30 -0
  164. package/templates/database-starter/page/src/examples/ai-function/ai-delta.ts +26 -0
  165. package/templates/database-starter/page/src/examples/ai-function/ai-language.test.tsx +86 -0
  166. package/templates/database-starter/page/src/examples/ai-function/ai-media-panel.test.ts +201 -0
  167. package/templates/database-starter/page/src/examples/ai-function/ai-media-panel.tsx +382 -0
  168. package/templates/database-starter/page/src/lib/attachment-image.tsx +144 -0
  169. package/templates/database-starter/page/src/lib/database-function-inputs.generated.ts +4 -0
  170. package/templates/database-starter/page/src/lib/database-runtime-attachment.test.ts +180 -76
  171. package/templates/database-starter/page/src/lib/database-runtime.test.ts +130 -10
  172. package/templates/database-starter/page/src/lib/database-runtime.ts +484 -51
  173. package/templates/database-starter/page/src/lib/date-time.ts +61 -0
  174. package/templates/database-starter/page/src/lib/ui-language.ts +86 -0
  175. package/templates/database-starter/page/src/main.tsx +1 -1
  176. package/templates/database-starter/page/tsconfig.json +2 -2
  177. package/templates/database-starter/page/vite.config.ts +1 -2
  178. package/templates/database-starter/prettierignore +1 -1
  179. package/templates/database-starter/server-functions/README.md +2 -2
  180. package/templates/database-starter/server-functions/package-lock.json +4 -4
  181. package/templates/database-starter/server-functions/package.json +1 -1
  182. package/dist/agent-guide.d.ts +0 -19
  183. package/dist/agent-guide.d.ts.map +0 -1
  184. package/dist/agent-guide.js +0 -31
  185. package/dist/agent-guide.js.map +0 -1
  186. package/dist/database-skill.d.ts +0 -24
  187. package/dist/database-skill.d.ts.map +0 -1
  188. package/dist/database-skill.js +0 -148
  189. package/dist/database-skill.js.map +0 -1
  190. package/skills/lark-base-database/references/base-sql-unsupported.md +0 -42
  191. package/skills/lark-base-database/references/database-capability-skill.md +0 -40
  192. package/skills/lark-base-database/references/sql-function.md +0 -289
  193. package/templates/database-capability/SKILL.md.tmpl +0 -58
  194. package/templates/database-starter/page/src/components/floating-button/chatbot-entry.test.ts +0 -23
  195. package/templates/database-starter/page/src/components/floating-button/chatbot-entry.tsx +0 -873
  196. package/templates/database-starter/page/src/components/floating-button/local-chat-panel.test.ts +0 -25
  197. package/templates/database-starter/page/src/components/floating-button/local-chat-panel.tsx +0 -197
  198. package/templates/database-starter/page/src/components/floating-button/utils/env.ts +0 -18
  199. package/templates/database-starter/sql/README.md +0 -8
@@ -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
+ })