@lark-base-open/base-database-cli 1.1.0 → 1.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -0
- package/LICENSE +6 -0
- package/README.md +55 -31
- package/dist/ai-functions.d.ts +42 -0
- package/dist/ai-functions.d.ts.map +1 -0
- package/dist/ai-functions.js +327 -0
- package/dist/ai-functions.js.map +1 -0
- package/dist/archive.d.ts +21 -0
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +65 -10
- package/dist/archive.js.map +1 -1
- package/dist/auth.d.ts +8 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +60 -6
- package/dist/auth.js.map +1 -1
- package/dist/check.d.ts +74 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +283 -0
- package/dist/check.js.map +1 -0
- package/dist/dependency-policy.d.ts +10 -0
- package/dist/dependency-policy.d.ts.map +1 -0
- package/dist/dependency-policy.js +162 -0
- package/dist/dependency-policy.js.map +1 -0
- package/dist/deploy.d.ts +1 -0
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +122 -175
- package/dist/deploy.js.map +1 -1
- package/dist/environment-variables.d.ts +52 -0
- package/dist/environment-variables.d.ts.map +1 -0
- package/dist/environment-variables.js +563 -0
- package/dist/environment-variables.js.map +1 -0
- package/dist/environment.d.ts +1 -1
- package/dist/environment.js +1 -1
- package/dist/environment.js.map +1 -1
- package/dist/function-input-types.d.ts +10 -0
- package/dist/function-input-types.d.ts.map +1 -0
- package/dist/function-input-types.js +160 -0
- package/dist/function-input-types.js.map +1 -0
- package/dist/function-schema.d.ts.map +1 -1
- package/dist/function-schema.js +89 -36
- package/dist/function-schema.js.map +1 -1
- package/dist/init-field-retry.d.ts +17 -0
- package/dist/init-field-retry.d.ts.map +1 -0
- package/dist/init-field-retry.js +127 -0
- package/dist/init-field-retry.js.map +1 -0
- package/dist/init-journal.d.ts +98 -0
- package/dist/init-journal.d.ts.map +1 -0
- package/dist/init-journal.js +492 -0
- package/dist/init-journal.js.map +1 -0
- package/dist/init-schema.d.ts +8 -0
- package/dist/init-schema.d.ts.map +1 -0
- package/dist/init-schema.js +423 -0
- package/dist/init-schema.js.map +1 -0
- package/dist/init.d.ts +14 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +886 -243
- package/dist/init.js.map +1 -1
- package/dist/lark-cli-error.d.ts +46 -0
- package/dist/lark-cli-error.d.ts.map +1 -0
- package/dist/lark-cli-error.js +171 -0
- package/dist/lark-cli-error.js.map +1 -0
- package/dist/lark-cli.d.ts +2 -1
- package/dist/lark-cli.d.ts.map +1 -1
- package/dist/lark-cli.js +292 -29
- package/dist/lark-cli.js.map +1 -1
- package/dist/mutation-schema.d.ts +146 -0
- package/dist/mutation-schema.d.ts.map +1 -0
- package/dist/mutation-schema.js +153 -0
- package/dist/mutation-schema.js.map +1 -0
- package/dist/mutation.d.ts +10 -0
- package/dist/mutation.d.ts.map +1 -0
- package/dist/mutation.js +131 -0
- package/dist/mutation.js.map +1 -0
- package/dist/npm.d.ts +1 -0
- package/dist/npm.d.ts.map +1 -1
- package/dist/npm.js +2 -2
- package/dist/npm.js.map +1 -1
- package/dist/preview-receipt.d.ts.map +1 -1
- package/dist/preview-receipt.js +15 -5
- package/dist/preview-receipt.js.map +1 -1
- package/dist/process.d.ts +3 -1
- package/dist/process.d.ts.map +1 -1
- package/dist/process.js +16 -4
- package/dist/process.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +241 -42
- package/dist/program.js.map +1 -1
- package/dist/project-build.d.ts +25 -0
- package/dist/project-build.d.ts.map +1 -0
- package/dist/project-build.js +229 -0
- package/dist/project-build.js.map +1 -0
- package/dist/project-context.d.ts +344 -0
- package/dist/project-context.d.ts.map +1 -0
- package/dist/project-context.js +485 -0
- package/dist/project-context.js.map +1 -0
- package/dist/project-layout.d.ts +5 -0
- package/dist/project-layout.d.ts.map +1 -0
- package/dist/project-layout.js +24 -0
- package/dist/project-layout.js.map +1 -0
- package/dist/public-sql-error.d.ts +25 -0
- package/dist/public-sql-error.d.ts.map +1 -0
- package/dist/public-sql-error.js +81 -0
- package/dist/public-sql-error.js.map +1 -0
- package/dist/pull.d.ts.map +1 -1
- package/dist/pull.js +54 -15
- package/dist/pull.js.map +1 -1
- package/dist/site-readme.d.ts +3 -0
- package/dist/site-readme.d.ts.map +1 -0
- package/dist/site-readme.js +83 -0
- package/dist/site-readme.js.map +1 -0
- package/dist/workspace-fingerprint.d.ts +21 -1
- package/dist/workspace-fingerprint.d.ts.map +1 -1
- package/dist/workspace-fingerprint.js +81 -2
- package/dist/workspace-fingerprint.js.map +1 -1
- package/package.json +2 -1
- package/skills/lark-base-database/SKILL.md +70 -53
- package/skills/lark-base-database/references/ai-function.md +471 -0
- package/skills/lark-base-database/references/base-capability-routing.md +3 -3
- package/skills/lark-base-database/references/database-function-limitations.md +46 -0
- package/skills/lark-base-database/references/database-function.md +349 -0
- package/skills/lark-base-database/references/design/components/actions-fields.md +16 -0
- package/skills/lark-base-database/references/design/components/code/controls.css +122 -0
- package/skills/lark-base-database/references/design/components/code/controls.tsx +73 -0
- package/skills/lark-base-database/references/design/components/code/date-picker.tsx +42 -0
- package/skills/lark-base-database/references/design/components/code/floating.tsx +41 -0
- package/skills/lark-base-database/references/design/components/code/overlays.tsx +19 -0
- package/skills/lark-base-database/references/design/components/code/select.tsx +34 -0
- package/skills/lark-base-database/references/design/components/code/styles.d.ts +1 -0
- package/skills/lark-base-database/references/design/components/code/theme.css +49 -0
- package/skills/lark-base-database/references/design/components/code/usage.tsx +38 -0
- package/skills/lark-base-database/references/design/components/date-time-number.md +9 -0
- package/skills/lark-base-database/references/design/components/feedback-navigation.md +9 -0
- package/skills/lark-base-database/references/design/components/index.md +18 -0
- package/skills/lark-base-database/references/design/components/overlays.md +9 -0
- package/skills/lark-base-database/references/design/components/patterns/flat-business.md +7 -0
- package/skills/lark-base-database/references/design/components/selection.md +24 -0
- package/skills/lark-base-database/references/design/components.md +91 -0
- package/skills/lark-base-database/references/design/composition.md +90 -0
- package/skills/lark-base-database/references/design/control-recipes.md +107 -0
- package/skills/lark-base-database/references/design/examples.md +172 -0
- package/skills/lark-base-database/references/design/foundations.md +126 -0
- package/skills/lark-base-database/references/design/review.md +82 -0
- package/skills/lark-base-database/references/design/scenes/analysis-report.md +66 -0
- package/skills/lark-base-database/references/design/scenes/brand-showcase.md +70 -0
- package/skills/lark-base-database/references/design/scenes/business-operations.md +85 -0
- package/skills/lark-base-database/references/design/scenes/campaign-report.md +66 -0
- package/skills/lark-base-database/references/design/scenes/dashboard.md +77 -0
- package/skills/lark-base-database/references/design/scenes/festive-campaign.md +69 -0
- package/skills/lark-base-database/references/design/theme.md +27 -0
- package/skills/lark-base-database/references/design/typography.md +56 -0
- package/skills/lark-base-database/references/development.md +165 -32
- package/skills/lark-base-database/references/environment-variables.md +55 -0
- package/skills/lark-base-database/references/faas.md +41 -48
- package/skills/lark-base-database/references/file-transfer.md +25 -25
- package/skills/lark-base-database/references/frontend-design-guide.md +33 -77
- package/skills/lark-base-database/references/mutations.md +295 -0
- package/skills/lark-base-database/references/permission-design.md +24 -24
- package/skills/lark-base-database/references/release.md +54 -27
- package/skills/lark-base-database/references/style-guide.md +10 -67
- package/skills/lark-base-database/references/verification.md +17 -13
- package/skills/lark-base-database/references/workspace.md +71 -22
- package/templates/database-starter/README.md +34 -4
- package/templates/database-starter/database-functions/README.md +8 -0
- package/templates/database-starter/mutations/README.md +19 -0
- package/templates/database-starter/page/README.md +25 -19
- package/templates/database-starter/page/package-lock.json +8 -16
- package/templates/database-starter/page/package.json +2 -3
- package/templates/database-starter/page/src/App.tsx +23 -3
- package/templates/database-starter/page/src/components/UserSelectorEditor/index.tsx +32 -4
- package/templates/database-starter/page/src/components/UserSelectorPanel/index.tsx +104 -34
- package/templates/database-starter/page/src/components/UserSelectorPanel/user-search.test.ts +55 -11
- package/templates/database-starter/page/src/components/UserSelectorPanel/user-search.ts +68 -25
- package/templates/database-starter/page/src/components/database-runtime-ready.tsx +1 -1
- package/templates/database-starter/page/src/components/database-user-menu/index.tsx +4 -7
- package/templates/database-starter/page/src/components/feedback/page-state.tsx +40 -5
- package/templates/database-starter/page/src/components/floating-button/assets/edit-active.svg +3 -0
- package/templates/database-starter/page/src/components/floating-button/assets/edit-default.svg +3 -0
- package/templates/database-starter/page/src/components/floating-button/index.tsx +75 -112
- package/templates/database-starter/page/src/components/floating-button/utils/project-meta.test.ts +107 -37
- package/templates/database-starter/page/src/components/floating-button/utils/project-meta.ts +13 -50
- package/templates/database-starter/page/src/components/ui-language.test.tsx +101 -0
- package/templates/database-starter/page/src/examples/ai-function/ImageTaskOperation.tsx +202 -0
- package/templates/database-starter/page/src/examples/ai-function/README.md +14 -0
- package/templates/database-starter/page/src/examples/ai-function/TextStreamOperation.tsx +184 -0
- package/templates/database-starter/page/src/examples/ai-function/ai-copy.ts +173 -0
- package/templates/database-starter/page/src/examples/ai-function/ai-delta.test.ts +30 -0
- package/templates/database-starter/page/src/examples/ai-function/ai-delta.ts +26 -0
- package/templates/database-starter/page/src/examples/ai-function/ai-language.test.tsx +86 -0
- package/templates/database-starter/page/src/examples/ai-function/ai-media-panel.test.ts +201 -0
- package/templates/database-starter/page/src/examples/ai-function/ai-media-panel.tsx +382 -0
- package/templates/database-starter/page/src/lib/attachment-image.tsx +144 -0
- package/templates/database-starter/page/src/lib/database-function-inputs.generated.ts +4 -0
- package/templates/database-starter/page/src/lib/database-runtime-attachment.test.ts +180 -76
- package/templates/database-starter/page/src/lib/database-runtime.test.ts +130 -10
- package/templates/database-starter/page/src/lib/database-runtime.ts +484 -51
- package/templates/database-starter/page/src/lib/date-time.ts +61 -0
- package/templates/database-starter/page/src/lib/ui-language.ts +86 -0
- package/templates/database-starter/page/src/main.tsx +1 -1
- package/templates/database-starter/page/tsconfig.json +2 -2
- package/templates/database-starter/page/vite.config.ts +1 -2
- package/templates/database-starter/prettierignore +1 -1
- package/templates/database-starter/server-functions/README.md +2 -2
- package/templates/database-starter/server-functions/package-lock.json +4 -4
- package/templates/database-starter/server-functions/package.json +1 -1
- package/dist/agent-guide.d.ts +0 -19
- package/dist/agent-guide.d.ts.map +0 -1
- package/dist/agent-guide.js +0 -31
- package/dist/agent-guide.js.map +0 -1
- package/dist/database-skill.d.ts +0 -24
- package/dist/database-skill.d.ts.map +0 -1
- package/dist/database-skill.js +0 -151
- package/dist/database-skill.js.map +0 -1
- package/skills/lark-base-database/references/base-sql-unsupported.md +0 -42
- package/skills/lark-base-database/references/database-capability-skill.md +0 -40
- package/skills/lark-base-database/references/sql-function.md +0 -289
- package/templates/database-capability/SKILL.md.tmpl +0 -58
- package/templates/database-starter/page/src/components/floating-button/chatbot-entry.test.ts +0 -23
- package/templates/database-starter/page/src/components/floating-button/chatbot-entry.tsx +0 -873
- package/templates/database-starter/page/src/components/floating-button/local-chat-panel.test.ts +0 -25
- package/templates/database-starter/page/src/components/floating-button/local-chat-panel.tsx +0 -197
- package/templates/database-starter/page/src/components/floating-button/utils/env.ts +0 -18
- package/templates/database-starter/sql/README.md +0 -8
|
@@ -0,0 +1,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
|
+
}
|