@lark-base-open/base-database-cli 1.1.1 → 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 +74 -0
  2. package/LICENSE +6 -0
  3. package/README.md +45 -27
  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 +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 +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 +883 -261
  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 +1 -1
  63. package/dist/lark-cli.d.ts.map +1 -1
  64. package/dist/lark-cli.js +66 -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 +240 -41
  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 +66 -50
  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 +51 -24
  159. package/skills/lark-base-database/references/style-guide.md +10 -67
  160. package/skills/lark-base-database/references/verification.md +15 -11
  161. package/skills/lark-base-database/references/workspace.md +46 -8
  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 -148
  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,184 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+ import { Button } from '../../components/ui/button'
3
+ import {
4
+ invokeSiteAIFunctionStream,
5
+ type SiteAIFunctionStreamHandle,
6
+ type SiteAIMessage,
7
+ } from '../../lib/database-runtime'
8
+ import { useUiLanguage } from '../../lib/ui-language'
9
+ import { AI_FUNCTION_COPY } from './ai-copy'
10
+ import { splitAIStreamDelta } from './ai-delta'
11
+
12
+ /** 业务型 text AI Function 示例:分区渲染 reasoning 与 answer,并从最终结果解析结构化列表。
13
+ *
14
+ * 契约要点(对应 ai-function.md):
15
+ * - 一次只维护一条活动 stream handle:新请求开始前 destroy 旧 handle。
16
+ * - 同时消费 reasoningContent 与 content,思考过程和最终回答分区展示。
17
+ * - onError 与 onDone 都收口 loading。
18
+ * - 结构化解析只基于最终 answer(content 的累积值),不基于 reasoning 内容。
19
+ * - 组件卸载时 destroy 活动 handle,避免流回调更新已卸载组件。
20
+ */
21
+ export interface TextStreamOperationProps {
22
+ // functionKey 指向后台配置好的 text/vision AI Function。
23
+ functionKey: string
24
+ // variables 渲染 AI Function System Prompt 里的 {{var}} 占位符(可选)。
25
+ variables?: Record<string, unknown>
26
+ title?: string
27
+ }
28
+
29
+ interface StreamState {
30
+ answer: string
31
+ error: string
32
+ reasoning: string
33
+ streaming: boolean
34
+ }
35
+
36
+ const INITIAL_STATE: StreamState = {
37
+ answer: '',
38
+ error: '',
39
+ reasoning: '',
40
+ streaming: false,
41
+ }
42
+
43
+ /** 把最终回答按行解析成结构化列表项,仅作演示:真实业务应替换为自己的解析规则。 */
44
+ export function parseAnswerItems(answer: string): string[] {
45
+ return answer
46
+ .split('\n')
47
+ .map((line) => line.replace(/^[-*\d.、\s]+/, '').trim())
48
+ .filter(Boolean)
49
+ }
50
+
51
+ function useTextStream(
52
+ functionKey: string,
53
+ variables: Record<string, unknown> | undefined,
54
+ unavailableMessage: string,
55
+ ) {
56
+ const streamRef = useRef<SiteAIFunctionStreamHandle | null>(null)
57
+ const [state, setState] = useState<StreamState>(INITIAL_STATE)
58
+
59
+ const stop = () => {
60
+ streamRef.current?.destroy()
61
+ streamRef.current = null
62
+ }
63
+
64
+ // 卸载时销毁活动流:回调不再触碰已卸载组件的 setState。
65
+ useEffect(() => stop, [])
66
+
67
+ const ask = (question: string) => {
68
+ const content = question.trim()
69
+ if (!content) {
70
+ return
71
+ }
72
+ // 新请求开始前,先释放上一条活动流。
73
+ stop()
74
+ setState({ ...INITIAL_STATE, streaming: true })
75
+
76
+ const messages: SiteAIMessage[] = [
77
+ { content: [{ type: 'text', text: content }], role: 'user' },
78
+ ]
79
+ const stream = invokeSiteAIFunctionStream({
80
+ functionKey,
81
+ messages,
82
+ ...(variables ? { variables } : {}),
83
+ })
84
+ stream.onMessage((delta) => {
85
+ const { answer, reasoning } = splitAIStreamDelta(delta)
86
+ setState((current) => ({
87
+ ...current,
88
+ answer: current.answer + answer,
89
+ reasoning: current.reasoning + reasoning,
90
+ }))
91
+ })
92
+ stream.onError((error) => {
93
+ setState((current) => ({
94
+ ...current,
95
+ error: error.message || unavailableMessage,
96
+ streaming: false,
97
+ }))
98
+ stop()
99
+ })
100
+ stream.onDone(() => {
101
+ setState((current) => ({ ...current, streaming: false }))
102
+ streamRef.current = null
103
+ })
104
+ streamRef.current = stream
105
+ }
106
+
107
+ return { ask, state, stop }
108
+ }
109
+
110
+ /** 文本流式业务操作示例组件。 */
111
+ export function TextStreamOperation({
112
+ functionKey,
113
+ variables,
114
+ title,
115
+ }: TextStreamOperationProps) {
116
+ const i18n = AI_FUNCTION_COPY[useUiLanguage()].text
117
+ const { ask, state, stop } = useTextStream(
118
+ functionKey,
119
+ variables,
120
+ i18n.unavailable,
121
+ )
122
+ const [draft, setDraft] = useState('')
123
+
124
+ const submit = () => {
125
+ if (state.streaming) {
126
+ return
127
+ }
128
+ ask(draft)
129
+ }
130
+
131
+ const items = state.streaming ? [] : parseAnswerItems(state.answer)
132
+
133
+ return (
134
+ <section className="flex flex-col gap-3 rounded-2xl border border-border bg-card p-5">
135
+ <h2 className="font-semibold text-card-foreground">
136
+ {title ?? i18n.title}
137
+ </h2>
138
+ <textarea
139
+ aria-label={i18n.question}
140
+ className="min-h-20 rounded-lg border border-border bg-background p-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
141
+ onChange={(event) => setDraft(event.target.value)}
142
+ placeholder={i18n.questionPlaceholder}
143
+ value={draft}
144
+ />
145
+ <div className="flex gap-2">
146
+ <Button disabled={state.streaming || !draft.trim()} onClick={submit}>
147
+ {state.streaming ? i18n.generating : i18n.send}
148
+ </Button>
149
+ {state.streaming ? (
150
+ <Button onClick={stop} variant="secondary">
151
+ {i18n.stop}
152
+ </Button>
153
+ ) : null}
154
+ </div>
155
+
156
+ {state.reasoning ? (
157
+ <details className="rounded-lg bg-secondary/40 p-3 text-xs text-muted-foreground">
158
+ <summary className="cursor-pointer select-none">
159
+ {i18n.reasoning}
160
+ </summary>
161
+ <p className="mt-2 whitespace-pre-wrap">{state.reasoning}</p>
162
+ </details>
163
+ ) : null}
164
+
165
+ {state.answer ? (
166
+ <div className="whitespace-pre-wrap rounded-lg bg-secondary p-3 text-sm text-secondary-foreground">
167
+ {state.answer}
168
+ </div>
169
+ ) : null}
170
+
171
+ {items.length > 0 ? (
172
+ <ul className="list-disc pl-5 text-sm text-card-foreground">
173
+ {items.map((item, index) => (
174
+ <li key={`${index}-${item}`}>{item}</li>
175
+ ))}
176
+ </ul>
177
+ ) : null}
178
+
179
+ {state.error ? (
180
+ <p className="text-sm text-danger">{state.error}</p>
181
+ ) : null}
182
+ </section>
183
+ )
184
+ }
@@ -0,0 +1,173 @@
1
+ import type { SiteAIMediaTask } from '../../lib/database-runtime'
2
+ import type { UiLanguage } from '../../lib/ui-language'
3
+
4
+ interface AiFunctionCopy {
5
+ text: {
6
+ title: string
7
+ question: string
8
+ questionPlaceholder: string
9
+ send: string
10
+ generating: string
11
+ stop: string
12
+ reasoning: string
13
+ unavailable: string
14
+ }
15
+ image: {
16
+ title: string
17
+ description: string
18
+ descriptionPlaceholder: string
19
+ generate: string
20
+ generating: string
21
+ generatedImage: string
22
+ failed: string
23
+ unavailable: string
24
+ timeout: string
25
+ }
26
+ video: {
27
+ title: string
28
+ description: string
29
+ descriptionPlaceholder: string
30
+ generate: string
31
+ processing: string
32
+ generatedVideo: string
33
+ unavailable: string
34
+ timeout: string
35
+ }
36
+ taskStatus: string
37
+ statuses: Record<SiteAIMediaTask['status'], string>
38
+ }
39
+
40
+ export const AI_FUNCTION_COPY = {
41
+ en: {
42
+ text: {
43
+ title: 'Generate text',
44
+ question: 'Question',
45
+ questionPlaceholder: 'Enter your question',
46
+ send: 'Send',
47
+ generating: 'Generating…',
48
+ stop: 'Stop',
49
+ reasoning: 'Reasoning',
50
+ unavailable: 'AI request is temporarily unavailable',
51
+ },
52
+ image: {
53
+ title: 'Generate an image',
54
+ description: 'Image description',
55
+ descriptionPlaceholder: 'Describe the image you want to create',
56
+ generate: 'Generate image',
57
+ generating: 'Generating…',
58
+ generatedImage: 'Generated image',
59
+ failed: 'Image generation failed',
60
+ unavailable: 'Image generation is temporarily unavailable',
61
+ timeout: 'Image generation timed out',
62
+ },
63
+ video: {
64
+ title: 'Generate a video',
65
+ description: 'Video description',
66
+ descriptionPlaceholder: 'Describe the video you want to create',
67
+ generate: 'Generate video',
68
+ processing: 'Processing…',
69
+ generatedVideo: 'Generated video',
70
+ unavailable: 'Video generation is temporarily unavailable',
71
+ timeout: 'Video generation timed out',
72
+ },
73
+ taskStatus: 'Task status: ',
74
+ statuses: {
75
+ queued: 'Queued',
76
+ running: 'Running',
77
+ succeeded: 'Completed',
78
+ failed: 'Failed',
79
+ cancelled: 'Cancelled',
80
+ },
81
+ },
82
+ zh: {
83
+ text: {
84
+ title: '生成文本',
85
+ question: '问题',
86
+ questionPlaceholder: '请输入问题',
87
+ send: '发送',
88
+ generating: '生成中…',
89
+ stop: '停止',
90
+ reasoning: '思考过程',
91
+ unavailable: 'AI 请求暂时不可用',
92
+ },
93
+ image: {
94
+ title: '生成图片',
95
+ description: '图片描述',
96
+ descriptionPlaceholder: '描述你想生成的图片',
97
+ generate: '生成图片',
98
+ generating: '生成中…',
99
+ generatedImage: '生成的图片',
100
+ failed: '图片生成失败',
101
+ unavailable: '图片生成暂时不可用',
102
+ timeout: '图片生成等待超时',
103
+ },
104
+ video: {
105
+ title: '生成视频',
106
+ description: '视频描述',
107
+ descriptionPlaceholder: '描述你想生成的视频',
108
+ generate: '生成视频',
109
+ processing: '处理中…',
110
+ generatedVideo: '生成的视频',
111
+ unavailable: '视频生成暂时不可用',
112
+ timeout: '视频生成等待超时',
113
+ },
114
+ taskStatus: '任务状态:',
115
+ statuses: {
116
+ queued: '排队中',
117
+ running: '运行中',
118
+ succeeded: '已完成',
119
+ failed: '失败',
120
+ cancelled: '已取消',
121
+ },
122
+ },
123
+ ja: {
124
+ text: {
125
+ title: 'テキストを生成',
126
+ question: '質問',
127
+ questionPlaceholder: '質問を入力してください',
128
+ send: '送信',
129
+ generating: '生成中…',
130
+ stop: '停止',
131
+ reasoning: '推論過程',
132
+ unavailable: 'AI リクエストを処理できませんでした',
133
+ },
134
+ image: {
135
+ title: '画像を生成',
136
+ description: '画像の説明',
137
+ descriptionPlaceholder: '生成したい画像を説明してください',
138
+ generate: '画像を生成',
139
+ generating: '生成中…',
140
+ generatedImage: '生成された画像',
141
+ failed: '画像の生成に失敗しました',
142
+ unavailable: '現在、画像を生成できません',
143
+ timeout: '画像生成の待機時間を超えました',
144
+ },
145
+ video: {
146
+ title: '動画を生成',
147
+ description: '動画の説明',
148
+ descriptionPlaceholder: '生成したい動画を説明してください',
149
+ generate: '動画を生成',
150
+ processing: '処理中…',
151
+ generatedVideo: '生成された動画',
152
+ unavailable: '現在、動画を生成できません',
153
+ timeout: '動画生成の待機時間を超えました',
154
+ },
155
+ taskStatus: 'タスクの状態:',
156
+ statuses: {
157
+ queued: '待機中',
158
+ running: '実行中',
159
+ succeeded: '完了',
160
+ failed: '失敗',
161
+ cancelled: 'キャンセル済み',
162
+ },
163
+ },
164
+ } satisfies Record<UiLanguage, AiFunctionCopy>
165
+
166
+ export function formatAiTaskStatus(
167
+ status: SiteAIMediaTask['status'],
168
+ progress: number | undefined,
169
+ language: UiLanguage,
170
+ ): string {
171
+ const copy = AI_FUNCTION_COPY[language]
172
+ return `${copy.taskStatus}${copy.statuses[status]}${typeof progress === 'number' ? ` (${progress}%)` : ''}`
173
+ }
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { splitAIStreamDelta } from './ai-delta'
3
+ import { parseAnswerItems } from './TextStreamOperation'
4
+
5
+ describe('splitAIStreamDelta', () => {
6
+ it('reads answer and reasoning increments separately', () => {
7
+ expect(
8
+ splitAIStreamDelta({ content: '答', reasoningContent: '想', raw: {} }),
9
+ ).toEqual({ answer: '答', reasoning: '想' })
10
+ })
11
+
12
+ it('falls back to empty strings when a field is missing', () => {
13
+ expect(
14
+ splitAIStreamDelta({ reasoningContent: '只有思考', raw: {} }),
15
+ ).toEqual({ answer: '', reasoning: '只有思考' })
16
+ expect(splitAIStreamDelta({ content: '只有回答', raw: {} })).toEqual({
17
+ answer: '只有回答',
18
+ reasoning: '',
19
+ })
20
+ })
21
+ })
22
+
23
+ describe('parseAnswerItems', () => {
24
+ it('splits the final answer into trimmed list items and drops bullets', () => {
25
+ expect(parseAnswerItems('- 宫保鸡丁\n2. 麻婆豆腐\n\n ')).toEqual([
26
+ '宫保鸡丁',
27
+ '麻婆豆腐',
28
+ ])
29
+ })
30
+ })
@@ -0,0 +1,26 @@
1
+ import type { SiteAIStreamDelta } from '../../lib/database-runtime'
2
+
3
+ /** text/vision 流式增量拆成「最终回答」与「思考过程」两段。
4
+ *
5
+ * reasoning 模型会先持续下发 reasoningContent、再下发 content。页面若只消费 content,
6
+ * 思考过程区域会一直为空,看起来像没有流式输出。用本 helper 统一取两段增量,
7
+ * 业务组件不必逐帧判空。SDK 新版本导出了同名 `readSiteAITextDelta`,
8
+ * 这里内置一份,保证 starter 在任意 channel 版本下都可用。
9
+ */
10
+ export interface AITextDelta {
11
+ /** 最终回答增量,进正式结果区。 */
12
+ answer: string
13
+ /** 模型思考过程增量,进较小的过程区域。 */
14
+ reasoning: string
15
+ }
16
+
17
+ /** 从单帧增量读出 { answer, reasoning };缺失字段回退为空串。
18
+ * @param delta SDK 归一化后的流式增量。
19
+ * @returns 本帧的回答与思考过程增量。
20
+ */
21
+ export function splitAIStreamDelta(delta: SiteAIStreamDelta): AITextDelta {
22
+ return {
23
+ answer: delta.content ?? '',
24
+ reasoning: delta.reasoningContent ?? '',
25
+ }
26
+ }
@@ -0,0 +1,86 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+
4
+ const locale = vi.hoisted(() => ({ language: 'en' as 'en' | 'zh' | 'ja' }))
5
+ vi.mock('../../lib/ui-language', () => ({
6
+ useUiLanguage: () => locale.language,
7
+ }))
8
+
9
+ import { formatAiTaskStatus } from './ai-copy'
10
+ import { TextStreamOperation } from './TextStreamOperation'
11
+ import { ImageTaskOperation } from './ImageTaskOperation'
12
+ import { AIImagePanel, AIVideoPanel } from './ai-media-panel'
13
+
14
+ afterEach(() => {
15
+ locale.language = 'en'
16
+ })
17
+
18
+ describe('AI Function example language', () => {
19
+ it.each([
20
+ {
21
+ language: 'en' as const,
22
+ textTitle: 'Generate text',
23
+ question: 'Enter your question',
24
+ imageTitle: 'Generate an image',
25
+ imagePrompt: 'Describe the image you want to create',
26
+ videoTitle: 'Generate a video',
27
+ videoPrompt: 'Describe the video you want to create',
28
+ status: 'Task status: Running (45%)',
29
+ },
30
+ {
31
+ language: 'zh' as const,
32
+ textTitle: '生成文本',
33
+ question: '请输入问题',
34
+ imageTitle: '生成图片',
35
+ imagePrompt: '描述你想生成的图片',
36
+ videoTitle: '生成视频',
37
+ videoPrompt: '描述你想生成的视频',
38
+ status: '任务状态:运行中 (45%)',
39
+ },
40
+ {
41
+ language: 'ja' as const,
42
+ textTitle: 'テキストを生成',
43
+ question: '質問を入力してください',
44
+ imageTitle: '画像を生成',
45
+ imagePrompt: '生成したい画像を説明してください',
46
+ videoTitle: '動画を生成',
47
+ videoPrompt: '生成したい動画を説明してください',
48
+ status: 'タスクの状態:実行中 (45%)',
49
+ },
50
+ ])('renders built-in copy in $language', (expected) => {
51
+ locale.language = expected.language
52
+ const text = renderToStaticMarkup(
53
+ <TextStreamOperation functionKey="text" />,
54
+ )
55
+ const image = renderToStaticMarkup(
56
+ <ImageTaskOperation functionKey="image" />,
57
+ )
58
+ const imagePanel = renderToStaticMarkup(
59
+ <AIImagePanel functionKey="image" />,
60
+ )
61
+ const videoPanel = renderToStaticMarkup(
62
+ <AIVideoPanel functionKey="video" />,
63
+ )
64
+
65
+ expect(text).toContain(expected.textTitle)
66
+ expect(text).toContain(expected.question)
67
+ expect(image).toContain(expected.imageTitle)
68
+ expect(image).toContain(expected.imagePrompt)
69
+ expect(imagePanel).toContain(expected.imageTitle)
70
+ expect(imagePanel).toContain(expected.imagePrompt)
71
+ expect(videoPanel).toContain(expected.videoTitle)
72
+ expect(videoPanel).toContain(expected.videoPrompt)
73
+ expect(formatAiTaskStatus('running', 45, expected.language)).toBe(
74
+ expected.status,
75
+ )
76
+ })
77
+
78
+ it('preserves a title supplied by the business Page', () => {
79
+ locale.language = 'ja'
80
+ expect(
81
+ renderToStaticMarkup(
82
+ <AIVideoPanel functionKey="video" title="Project video" />,
83
+ ),
84
+ ).toContain('Project video')
85
+ })
86
+ })
@@ -0,0 +1,201 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import {
3
+ activateMediaTaskRequestTracker,
4
+ createMediaTaskRequestTracker,
5
+ pollMediaTaskUntilTerminal,
6
+ type MediaTaskPollUpdate,
7
+ } from './ai-media-panel'
8
+ import type { SiteAIMediaTask } from '../../lib/database-runtime'
9
+
10
+ function videoTask(taskId: string, status: string): SiteAIMediaTask {
11
+ return {
12
+ type: 'video_task',
13
+ taskId,
14
+ status: status as SiteAIMediaTask['status'],
15
+ requestId: `req-${taskId}`,
16
+ }
17
+ }
18
+
19
+ function nowClock(start: number) {
20
+ let time = start
21
+ return {
22
+ advance: (ms: number) => (time += ms),
23
+ now: () => time,
24
+ }
25
+ }
26
+
27
+ describe('pollMediaTaskUntilTerminal', () => {
28
+ it('drops updates once isCurrent becomes false (旧任务晚到)', async () => {
29
+ const updates: MediaTaskPollUpdate[] = []
30
+ let current = true
31
+ const clock = nowClock(0)
32
+ const fetchTask = vi
33
+ .fn<() => Promise<SiteAIMediaTask>>()
34
+ .mockImplementationOnce(async () => {
35
+ current = false
36
+ return videoTask('t1', 'succeeded')
37
+ })
38
+
39
+ await pollMediaTaskUntilTerminal({
40
+ fetchTask,
41
+ isCurrent: () => current,
42
+ timing: {
43
+ deadline: 60_000,
44
+ intervalMs: 10,
45
+ now: clock.now,
46
+ sleep: async (ms) => {
47
+ clock.advance(ms)
48
+ },
49
+ },
50
+ onUpdate: (update) => updates.push(update),
51
+ timeoutMessage: 'timeout',
52
+ failureMessage: 'failed',
53
+ })
54
+
55
+ expect(fetchTask).toHaveBeenCalledTimes(1)
56
+ expect(updates).toEqual([])
57
+ })
58
+
59
+ it('emits a timeout finish and stops when the deadline passes', async () => {
60
+ const updates: MediaTaskPollUpdate[] = []
61
+ const clock = nowClock(0)
62
+ const fetchTask = vi
63
+ .fn<() => Promise<SiteAIMediaTask>>()
64
+ .mockResolvedValue(videoTask('t1', 'queued'))
65
+
66
+ await pollMediaTaskUntilTerminal({
67
+ fetchTask,
68
+ isCurrent: () => true,
69
+ timing: {
70
+ deadline: 25,
71
+ intervalMs: 10,
72
+ now: clock.now,
73
+ sleep: async (ms) => {
74
+ clock.advance(ms)
75
+ },
76
+ },
77
+ onUpdate: (update) => updates.push(update),
78
+ timeoutMessage: '视频不可用(轮询超时)',
79
+ failureMessage: '视频不可用',
80
+ })
81
+
82
+ expect(updates[updates.length - 1]).toMatchObject({
83
+ finished: true,
84
+ error: '视频不可用(轮询超时)',
85
+ })
86
+ })
87
+
88
+ it('stops on succeeded/failed/cancelled terminal states', async () => {
89
+ for (const terminal of ['succeeded', 'failed', 'cancelled']) {
90
+ const updates: MediaTaskPollUpdate[] = []
91
+ const clock = nowClock(0)
92
+ const fetchTask = vi
93
+ .fn<() => Promise<SiteAIMediaTask>>()
94
+ .mockResolvedValueOnce(videoTask('t1', 'queued'))
95
+ .mockResolvedValueOnce(videoTask('t1', terminal))
96
+
97
+ await pollMediaTaskUntilTerminal({
98
+ fetchTask,
99
+ isCurrent: () => true,
100
+ timing: {
101
+ deadline: 10_000,
102
+ intervalMs: 10,
103
+ now: clock.now,
104
+ sleep: async (ms) => {
105
+ clock.advance(ms)
106
+ },
107
+ },
108
+ onUpdate: (update) => updates.push(update),
109
+ timeoutMessage: 'timeout',
110
+ failureMessage: '视频不可用',
111
+ })
112
+
113
+ expect(fetchTask).toHaveBeenCalledTimes(2)
114
+ const last = updates[updates.length - 1]
115
+ expect(last).toMatchObject({ finished: true })
116
+ if (terminal === 'succeeded') {
117
+ expect(last?.error).toBeUndefined()
118
+ } else {
119
+ expect(last?.error).toBeTruthy()
120
+ }
121
+ }
122
+ })
123
+
124
+ it('surfaces fetch errors and stops polling', async () => {
125
+ const updates: MediaTaskPollUpdate[] = []
126
+ const clock = nowClock(0)
127
+ const fetchTask = vi
128
+ .fn<() => Promise<SiteAIMediaTask>>()
129
+ .mockRejectedValueOnce(new Error('network broken'))
130
+
131
+ await pollMediaTaskUntilTerminal({
132
+ fetchTask,
133
+ isCurrent: () => true,
134
+ timing: {
135
+ deadline: 10_000,
136
+ intervalMs: 10,
137
+ now: clock.now,
138
+ sleep: async (ms) => {
139
+ clock.advance(ms)
140
+ },
141
+ },
142
+ onUpdate: (update) => updates.push(update),
143
+ timeoutMessage: 'timeout',
144
+ failureMessage: '视频不可用',
145
+ })
146
+
147
+ expect(updates).toEqual([{ finished: true, error: 'network broken' }])
148
+ })
149
+
150
+ it('preserves a server-provided task error', async () => {
151
+ const updates: MediaTaskPollUpdate[] = []
152
+ const failed = {
153
+ ...videoTask('t1', 'failed'),
154
+ errMessage: 'Server error: quota exceeded',
155
+ }
156
+
157
+ await pollMediaTaskUntilTerminal({
158
+ fetchTask: async () => failed,
159
+ isCurrent: () => true,
160
+ timing: {
161
+ deadline: 10_000,
162
+ intervalMs: 10,
163
+ now: () => 0,
164
+ sleep: async () => {},
165
+ },
166
+ onUpdate: (update) => updates.push(update),
167
+ timeoutMessage: 'Localized timeout',
168
+ failureMessage: 'Localized fallback',
169
+ })
170
+
171
+ expect(updates).toEqual([
172
+ { finished: true, error: 'Server error: quota exceeded', task: failed },
173
+ ])
174
+ })
175
+ })
176
+
177
+ describe('media task request lifecycle', () => {
178
+ it('invalidates the previous request when a new request begins', () => {
179
+ const tracker = createMediaTaskRequestTracker()
180
+ const cleanup = activateMediaTaskRequestTracker(tracker)
181
+ const firstRequestId = tracker.begin()
182
+ const secondRequestId = tracker.begin()
183
+
184
+ expect(tracker.isCurrent(firstRequestId)).toBe(false)
185
+ expect(tracker.isCurrent(secondRequestId)).toBe(true)
186
+ cleanup()
187
+ })
188
+
189
+ it('reactivates after the StrictMode effect replay and invalidates on cleanup', () => {
190
+ const tracker = createMediaTaskRequestTracker()
191
+
192
+ const firstCleanup = activateMediaTaskRequestTracker(tracker)
193
+ firstCleanup()
194
+ const secondCleanup = activateMediaTaskRequestTracker(tracker)
195
+ const requestId = tracker.begin()
196
+
197
+ expect(tracker.isCurrent(requestId)).toBe(true)
198
+ secondCleanup()
199
+ expect(tracker.isCurrent(requestId)).toBe(false)
200
+ })
201
+ })