@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,202 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+ import { Button } from '../../components/ui/button'
3
+ import {
4
+ createSiteAIFunctionTask,
5
+ getSiteAIFunctionTask,
6
+ hasRunnableMediaTask,
7
+ type SiteAIMediaTask,
8
+ } from '../../lib/database-runtime'
9
+ import { useUiLanguage } from '../../lib/ui-language'
10
+ import { AI_FUNCTION_COPY, formatAiTaskStatus } from './ai-copy'
11
+
12
+ /** 业务型 image AI Function 示例:create task + poll,处理全部终态,并防止旧任务覆盖新结果。
13
+ *
14
+ * 契约要点(对应 ai-function.md):
15
+ * - 文生图/文生视频统一走任务式:createSiteAIFunctionTask 提交,getSiteAIFunctionTask 轮询。
16
+ * - 处理 queued/running/succeeded/failed/cancelled 五种状态。
17
+ * - 用本地自增 requestSeq 作为「当前请求」标记:轮询回调先校验 seq,过期任务的结果直接丢弃,
18
+ * 避免用户快速多次提交时旧任务把新结果覆盖掉。
19
+ * - 卸载时清理 timer,回调不再触碰已卸载组件。
20
+ */
21
+ export interface ImageTaskOperationProps {
22
+ functionKey: string
23
+ // referenceImages 支持 1~14 张可访问 URL 或 data:image Base64。
24
+ referenceImages?: string[]
25
+ title?: string
26
+ // pollIntervalMs 轮询节奏,默认 3s;服务端也有轮询上限。
27
+ pollIntervalMs?: number
28
+ }
29
+
30
+ interface ImageTaskState {
31
+ error: string
32
+ loading: boolean
33
+ task: SiteAIMediaTask | null
34
+ }
35
+
36
+ const INITIAL_STATE: ImageTaskState = {
37
+ error: '',
38
+ loading: false,
39
+ task: null,
40
+ }
41
+
42
+ function useImageTask(
43
+ functionKey: string,
44
+ pollIntervalMs: number,
45
+ messages: { failed: string; unavailable: string },
46
+ ) {
47
+ const [state, setState] = useState<ImageTaskState>(INITIAL_STATE)
48
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
49
+ // requestSeq 单调递增,标识「当前有效请求」;轮询链只对最新一次提交生效。
50
+ const requestSeq = useRef(0)
51
+
52
+ const clearTimer = () => {
53
+ if (timerRef.current) {
54
+ clearTimeout(timerRef.current)
55
+ timerRef.current = null
56
+ }
57
+ }
58
+
59
+ // 卸载时清理 timer:过期回调不再 setState。
60
+ useEffect(() => clearTimer, [])
61
+
62
+ const poll = (taskId: string, seq: number) => {
63
+ timerRef.current = setTimeout(async () => {
64
+ try {
65
+ const next = await getSiteAIFunctionTask(functionKey, taskId)
66
+ // 期间已发起新请求:本次结果作废,什么都不做。
67
+ if (seq !== requestSeq.current) {
68
+ return
69
+ }
70
+ setState((current) => ({ ...current, task: next }))
71
+ if (next.status === 'succeeded') {
72
+ setState((current) => ({ ...current, loading: false }))
73
+ return
74
+ }
75
+ if (next.status === 'failed' || next.status === 'cancelled') {
76
+ setState((current) => ({
77
+ ...current,
78
+ error: next.errMessage ?? messages.failed,
79
+ loading: false,
80
+ }))
81
+ return
82
+ }
83
+ poll(taskId, seq)
84
+ } catch (cause) {
85
+ if (seq !== requestSeq.current) {
86
+ return
87
+ }
88
+ setState((current) => ({
89
+ ...current,
90
+ error: cause instanceof Error ? cause.message : messages.unavailable,
91
+ loading: false,
92
+ }))
93
+ }
94
+ }, pollIntervalMs)
95
+ }
96
+
97
+ const submit = async (prompt: string, referenceImages: string[]) => {
98
+ const value = prompt.trim()
99
+ if (!value) {
100
+ return
101
+ }
102
+ clearTimer()
103
+ const seq = requestSeq.current + 1
104
+ requestSeq.current = seq
105
+ setState({ ...INITIAL_STATE, loading: true })
106
+ try {
107
+ const created = await createSiteAIFunctionTask({
108
+ functionKey,
109
+ capability: 'image',
110
+ input: {
111
+ prompt: value,
112
+ ...(referenceImages.length ? { image: referenceImages } : {}),
113
+ },
114
+ })
115
+ if (seq !== requestSeq.current) {
116
+ return
117
+ }
118
+ setState((current) => ({ ...current, task: created }))
119
+ if (hasRunnableMediaTask(created)) {
120
+ poll(created.taskId, seq)
121
+ } else {
122
+ setState((current) => ({ ...current, loading: false }))
123
+ }
124
+ } catch (cause) {
125
+ if (seq !== requestSeq.current) {
126
+ return
127
+ }
128
+ setState((current) => ({
129
+ ...current,
130
+ error: cause instanceof Error ? cause.message : messages.unavailable,
131
+ loading: false,
132
+ }))
133
+ }
134
+ }
135
+
136
+ return { state, submit }
137
+ }
138
+
139
+ /** 图片生成业务操作示例组件,支持纯文本、单参考图和多参考图。 */
140
+ export function ImageTaskOperation({
141
+ functionKey,
142
+ pollIntervalMs = 3000,
143
+ referenceImages = [],
144
+ title,
145
+ }: ImageTaskOperationProps) {
146
+ const language = useUiLanguage()
147
+ const copy = AI_FUNCTION_COPY[language]
148
+ const i18n = copy.image
149
+ const { state, submit } = useImageTask(functionKey, pollIntervalMs, i18n)
150
+ const [prompt, setPrompt] = useState('')
151
+
152
+ const generate = () => {
153
+ if (state.loading) {
154
+ return
155
+ }
156
+ void submit(prompt, referenceImages)
157
+ }
158
+
159
+ const images =
160
+ state.task?.type === 'image_task'
161
+ ? (state.task.imageAssets ?? []).filter(
162
+ (asset): asset is typeof asset & { url: string } =>
163
+ Boolean(asset.url),
164
+ )
165
+ : []
166
+
167
+ return (
168
+ <section className="flex flex-col gap-3 rounded-2xl border border-border bg-card p-5">
169
+ <h2 className="font-semibold text-card-foreground">
170
+ {title ?? i18n.title}
171
+ </h2>
172
+ <textarea
173
+ aria-label={i18n.description}
174
+ 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"
175
+ onChange={(event) => setPrompt(event.target.value)}
176
+ placeholder={i18n.descriptionPlaceholder}
177
+ value={prompt}
178
+ />
179
+ <Button disabled={state.loading || !prompt.trim()} onClick={generate}>
180
+ {state.loading ? i18n.generating : i18n.generate}
181
+ </Button>
182
+ {state.task ? (
183
+ <p className="text-sm text-muted-foreground">
184
+ {formatAiTaskStatus(state.task.status, state.task.progress, language)}
185
+ </p>
186
+ ) : null}
187
+ {state.error ? (
188
+ <p className="text-sm text-danger">{state.error}</p>
189
+ ) : null}
190
+ <div className="grid grid-cols-2 gap-3">
191
+ {images.map((asset) => (
192
+ <img
193
+ alt={i18n.generatedImage}
194
+ className="w-full rounded-lg border border-border"
195
+ key={asset.url}
196
+ src={asset.url}
197
+ />
198
+ ))}
199
+ </div>
200
+ </section>
201
+ )
202
+ }
@@ -0,0 +1,14 @@
1
+ # AI Function examples
2
+
3
+ Reference implementations for business pages. Copy the relevant example into a page and adapt it to the task.
4
+
5
+ The examples' built-in titles, prompts, buttons, task statuses, and local fallback errors follow the host Runtime Context locale, then the browser language, then English. Supported languages are Chinese, English, and Japanese. A `title` prop supplied by the business Page overrides the built-in title and remains that Page's responsibility. Server-provided error messages and generated AI content are displayed as received.
6
+
7
+ The components share `ai-copy.ts`. Keep that import when reusing an example, or copy the dictionary alongside the component when moving it to another directory.
8
+
9
+ - `ai-media-panel.tsx`: `AIImagePanel` and `AIVideoPanel` call image and video AI Functions by `functionKey`. Their shared task hook ignores late results from superseded requests and limits total polling time with `pollDeadlineMs` (five minutes by default). `AIVideoPanel` renders `videoAsset`. The polling loop is exported as `pollMediaTaskUntilTerminal` for reuse and testing.
10
+ - `ai-delta.ts`: `splitAIStreamDelta(delta)` separates streamed output into `{ answer, reasoning }`. The SDK also provides `readSiteAITextDelta`; this example uses a local helper.
11
+ - `TextStreamOperation.tsx`: Text and image understanding stream. Shows how to keep one active stream handle, destroy the old handle before a new request, render `reasoningContent` and `content` separately, close loading state in `onError` and `onDone`, and parse a structured list from the final answer rather than reasoning.
12
+ - `ImageTaskOperation.tsx`: Image task flow using `capability: 'image'`. Shows submission with `createSiteAIFunctionTask`, polling with `getSiteAIFunctionTask`, handling every terminal state (`succeeded`, `failed`, `cancelled`), passing 1–14 reference images, preventing an old task from overwriting a new result with `requestSeq`, and clearing the timer on unmount. For video tasks, use `AIVideoPanel` in `ai-media-panel.tsx`, which renders `videoAsset`.
13
+
14
+ See the `lark-base-database` Skill's `ai-function.md` for the call contracts: `callSiteAIFunction` for a discriminated result, `invokeSiteAIFunctionStream` for explicit streaming, and `createSiteAIFunctionTask` / `getSiteAIFunctionTask` for media task submission and polling.
@@ -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
+ }