@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.
- package/CHANGELOG.md +74 -0
- package/LICENSE +6 -0
- package/README.md +45 -27
- 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 +5 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +26 -3
- 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 +883 -261
- 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 +1 -1
- package/dist/lark-cli.d.ts.map +1 -1
- package/dist/lark-cli.js +66 -18
- 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 +240 -41
- 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 +66 -50
- 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 +51 -24
- package/skills/lark-base-database/references/style-guide.md +10 -67
- package/skills/lark-base-database/references/verification.md +15 -11
- package/skills/lark-base-database/references/workspace.md +46 -8
- 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 -148
- 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
package/templates/database-starter/page/src/components/floating-button/utils/project-meta.test.ts
CHANGED
|
@@ -1,16 +1,31 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { parseProjectMetaResponse } from './project-meta'
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { fetchProjectMeta, parseProjectMetaResponse } from './project-meta'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
const runtime = vi.hoisted(() => ({
|
|
5
|
+
fetch: vi.fn(),
|
|
6
|
+
resolveDatabaseContext: vi.fn(),
|
|
7
|
+
}))
|
|
8
|
+
|
|
9
|
+
vi.mock('../../../lib/database-runtime', async (importOriginal) => ({
|
|
10
|
+
...(await importOriginal<object>()),
|
|
11
|
+
...runtime,
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
vi.unstubAllGlobals()
|
|
16
|
+
vi.clearAllMocks()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
function projectResponse(isOwner: unknown, allowEdit?: unknown) {
|
|
5
20
|
return {
|
|
6
21
|
code: 0,
|
|
7
22
|
data: {
|
|
8
23
|
isOwner,
|
|
9
24
|
site: {
|
|
10
25
|
baseBindings: [],
|
|
11
|
-
chatbots: [],
|
|
12
26
|
description: 'Owner visibility test',
|
|
13
27
|
name: 'Owner visibility test',
|
|
28
|
+
...(allowEdit === undefined ? {} : { allowEdit }),
|
|
14
29
|
},
|
|
15
30
|
},
|
|
16
31
|
}
|
|
@@ -32,48 +47,103 @@ describe('project owner visibility', () => {
|
|
|
32
47
|
})
|
|
33
48
|
})
|
|
34
49
|
|
|
35
|
-
describe('
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
describe('edit entry permission', () => {
|
|
51
|
+
it('shows admins and owners when the server allows editing', () => {
|
|
52
|
+
expect(
|
|
53
|
+
parseProjectMetaResponse(projectResponse(false, true)).allowEdit,
|
|
54
|
+
).toBe(true)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('respects explicit denial and falls back to owner only for old responses', () => {
|
|
58
|
+
expect(
|
|
59
|
+
parseProjectMetaResponse(projectResponse(true, false)).allowEdit,
|
|
60
|
+
).toBe(false)
|
|
61
|
+
expect(parseProjectMetaResponse(projectResponse(true)).allowEdit).toBe(true)
|
|
62
|
+
expect(parseProjectMetaResponse(projectResponse(false)).allowEdit).toBe(
|
|
63
|
+
false,
|
|
64
|
+
)
|
|
65
|
+
expect(
|
|
66
|
+
parseProjectMetaResponse(projectResponse(false, 'true')).allowEdit,
|
|
67
|
+
).toBe(false)
|
|
68
|
+
expect(
|
|
69
|
+
parseProjectMetaResponse(projectResponse(true, 'true')).allowEdit,
|
|
70
|
+
).toBe(false)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('project metadata', () => {
|
|
75
|
+
it('preserves page identity and valid Base bindings', () => {
|
|
76
|
+
const meta = parseProjectMetaResponse({
|
|
39
77
|
code: 0,
|
|
40
78
|
data: {
|
|
41
79
|
isOwner: true,
|
|
42
80
|
site: {
|
|
43
|
-
|
|
44
|
-
chatbots,
|
|
81
|
+
name: ' 应用名称 ',
|
|
45
82
|
description: '应用描述',
|
|
46
|
-
|
|
83
|
+
organization: '所属组织',
|
|
84
|
+
baseBindings: [
|
|
85
|
+
null,
|
|
86
|
+
{ tableIds: ['missing-token'] },
|
|
87
|
+
{ baseToken: ' bascnBase ', tableIds: ['tbl_a', 42] },
|
|
88
|
+
],
|
|
47
89
|
},
|
|
48
90
|
},
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
91
|
+
})
|
|
92
|
+
expect(meta).toEqual({
|
|
93
|
+
isOwner: true,
|
|
94
|
+
allowEdit: true,
|
|
95
|
+
name: '应用名称',
|
|
96
|
+
description: '应用描述',
|
|
97
|
+
organization: '所属组织',
|
|
98
|
+
baseBindings: [{ baseToken: 'bascnBase', tableIds: ['tbl_a'] }],
|
|
99
|
+
})
|
|
100
|
+
})
|
|
56
101
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
expect(
|
|
102
|
+
it.each([
|
|
103
|
+
null,
|
|
104
|
+
{ code: 1 },
|
|
105
|
+
{ code: 0, data: { site: null, isOwner: true } },
|
|
106
|
+
])('fails closed for an invalid response', (response) => {
|
|
107
|
+
expect(parseProjectMetaResponse(response)).toEqual({
|
|
108
|
+
isOwner: false,
|
|
109
|
+
allowEdit: false,
|
|
110
|
+
name: '',
|
|
111
|
+
description: '',
|
|
112
|
+
organization: '',
|
|
113
|
+
baseBindings: [],
|
|
114
|
+
})
|
|
63
115
|
})
|
|
64
116
|
|
|
65
|
-
it('
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
117
|
+
it.each(['prod', 'preview'])(
|
|
118
|
+
'fetches only application metadata in %s',
|
|
119
|
+
async (env) => {
|
|
120
|
+
runtime.resolveDatabaseContext.mockResolvedValue({
|
|
121
|
+
env,
|
|
122
|
+
gatewayOrigin: '',
|
|
123
|
+
token: 'meta_test',
|
|
124
|
+
})
|
|
125
|
+
runtime.fetch.mockResolvedValue({
|
|
126
|
+
ok: true,
|
|
127
|
+
json: async () => projectResponse(true),
|
|
128
|
+
})
|
|
129
|
+
class MetaElement {
|
|
130
|
+
content = ''
|
|
131
|
+
}
|
|
132
|
+
const description = new MetaElement()
|
|
133
|
+
const document = { title: '', querySelector: vi.fn(() => description) }
|
|
134
|
+
vi.stubGlobal('document', document)
|
|
135
|
+
vi.stubGlobal('HTMLMetaElement', MetaElement)
|
|
73
136
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
137
|
+
const meta = await fetchProjectMeta()
|
|
138
|
+
expect(runtime.fetch).toHaveBeenCalledWith(
|
|
139
|
+
`/space/api/bitable/site/v1/meta_test/invoke_get${env === 'preview' ? '?env=preview' : ''}`,
|
|
140
|
+
{ method: 'GET' },
|
|
141
|
+
)
|
|
142
|
+
expect(meta.isOwner).toBe(true)
|
|
143
|
+
expect(document.title).toBe('Owner visibility test')
|
|
144
|
+
expect(description.content).toBe('Owner visibility test')
|
|
145
|
+
expect(await fetchProjectMeta()).toBe(meta)
|
|
146
|
+
expect(runtime.fetch).toHaveBeenCalledTimes(1)
|
|
147
|
+
},
|
|
148
|
+
)
|
|
79
149
|
})
|
package/templates/database-starter/page/src/components/floating-button/utils/project-meta.ts
CHANGED
|
@@ -5,12 +5,6 @@ import {
|
|
|
5
5
|
type DatabaseContext,
|
|
6
6
|
} from '../../../lib/database-runtime'
|
|
7
7
|
|
|
8
|
-
interface ProjectChat {
|
|
9
|
-
id: string
|
|
10
|
-
name: string
|
|
11
|
-
iconInfo: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
8
|
export interface ProjectBaseBinding {
|
|
15
9
|
baseToken: string
|
|
16
10
|
tableIds: string[]
|
|
@@ -18,11 +12,11 @@ export interface ProjectBaseBinding {
|
|
|
18
12
|
|
|
19
13
|
export interface ProjectMeta {
|
|
20
14
|
baseBindings: ProjectBaseBinding[]
|
|
21
|
-
chatList: ProjectChat[]
|
|
22
15
|
name: string
|
|
23
16
|
description: string
|
|
24
17
|
isOwner: boolean
|
|
25
|
-
|
|
18
|
+
allowEdit: boolean
|
|
19
|
+
/** 应用所属组织(企业)名,账号菜单展示用;服务端未下发时为空串。 */
|
|
26
20
|
organization: string
|
|
27
21
|
}
|
|
28
22
|
|
|
@@ -60,15 +54,15 @@ function readStringArray(value: unknown): string[] {
|
|
|
60
54
|
}
|
|
61
55
|
|
|
62
56
|
/** 构造空项目 meta。
|
|
63
|
-
* @returns
|
|
57
|
+
* @returns 不包含所有者权限的空项目 meta。
|
|
64
58
|
*/
|
|
65
59
|
function createEmptyProjectMeta(): ProjectMeta {
|
|
66
60
|
return {
|
|
67
61
|
baseBindings: [],
|
|
68
|
-
chatList: [],
|
|
69
62
|
name: '',
|
|
70
63
|
description: '',
|
|
71
64
|
isOwner: false,
|
|
65
|
+
allowEdit: false,
|
|
72
66
|
organization: '',
|
|
73
67
|
}
|
|
74
68
|
}
|
|
@@ -130,41 +124,9 @@ function parseProjectBaseBindings(
|
|
|
130
124
|
}, [])
|
|
131
125
|
}
|
|
132
126
|
|
|
133
|
-
/** 解析 Site 绑定的 chatbot 列表。
|
|
134
|
-
*
|
|
135
|
-
* 接口的 chatbot 字段全部可选:缺 id 的条目无法跳转,直接丢弃;名称与图标缺失时
|
|
136
|
-
* 留空交由展示层兜底。单个 chatbot 字段不全不能影响整个项目 meta 的解析。
|
|
137
|
-
*
|
|
138
|
-
* @param rawChatbots 接口返回的 chatbots 字段。
|
|
139
|
-
* @returns 可用于展示的 chatbot 列表。
|
|
140
|
-
*/
|
|
141
|
-
function parseProjectChats(rawChatbots: unknown): ProjectChat[] {
|
|
142
|
-
if (!Array.isArray(rawChatbots)) {
|
|
143
|
-
return []
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return rawChatbots.reduce<ProjectChat[]>((chats, chatbot) => {
|
|
147
|
-
if (!isRecord(chatbot)) {
|
|
148
|
-
return chats
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const id = readOptionalString(chatbot, 'id')
|
|
152
|
-
if (!id) {
|
|
153
|
-
return chats
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
chats.push({
|
|
157
|
-
id,
|
|
158
|
-
name: readOptionalString(chatbot, 'name'),
|
|
159
|
-
iconInfo: readOptionalString(chatbot, 'iconInfo'),
|
|
160
|
-
})
|
|
161
|
-
return chats
|
|
162
|
-
}, [])
|
|
163
|
-
}
|
|
164
|
-
|
|
165
127
|
/** 将资源接口响应转换为页面需要的项目 meta。
|
|
166
128
|
* @param rawResponse 接口原始响应。
|
|
167
|
-
* @returns
|
|
129
|
+
* @returns 提取后的应用名称、描述、所属组织、Base 绑定及所有者状态;接口或结构异常时返回空数据。
|
|
168
130
|
*/
|
|
169
131
|
export function parseProjectMetaResponse(rawResponse: unknown): ProjectMeta {
|
|
170
132
|
if (!isRecord(rawResponse)) {
|
|
@@ -187,10 +149,14 @@ export function parseProjectMetaResponse(rawResponse: unknown): ProjectMeta {
|
|
|
187
149
|
|
|
188
150
|
return {
|
|
189
151
|
baseBindings: parseProjectBaseBindings(site.baseBindings),
|
|
190
|
-
chatList: parseProjectChats(site.chatbots),
|
|
191
152
|
description: readOptionalString(site, 'description'),
|
|
192
153
|
name: readOptionalString(site, 'name'),
|
|
193
154
|
isOwner: data.isOwner === true,
|
|
155
|
+
// 老服务端尚未下发 allowEdit 时保留 Owner 入口;显式 false 必须优先。
|
|
156
|
+
allowEdit:
|
|
157
|
+
site.allowEdit === undefined
|
|
158
|
+
? data.isOwner === true
|
|
159
|
+
: site.allowEdit === true,
|
|
194
160
|
organization: readOptionalString(site, 'organization'),
|
|
195
161
|
}
|
|
196
162
|
}
|
|
@@ -206,12 +172,9 @@ async function requestProjectMeta(
|
|
|
206
172
|
context: DatabaseContext,
|
|
207
173
|
): Promise<ProjectMeta> {
|
|
208
174
|
try {
|
|
209
|
-
const response = await databaseFetch(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
method: 'GET',
|
|
213
|
-
},
|
|
214
|
-
)
|
|
175
|
+
const response = await databaseFetch(buildDatabaseInvokeGetPath(context), {
|
|
176
|
+
method: 'GET',
|
|
177
|
+
})
|
|
215
178
|
|
|
216
179
|
if (!response.ok) {
|
|
217
180
|
return createEmptyProjectMeta()
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
|
|
4
|
+
const runtime = vi.hoisted(() => ({ getRuntimeContext: vi.fn() }))
|
|
5
|
+
vi.mock('../lib/database-runtime', () => ({ siteRuntime: runtime }))
|
|
6
|
+
|
|
7
|
+
import UserSelectorEditor from './UserSelectorEditor'
|
|
8
|
+
import UserSelectorPanel from './UserSelectorPanel'
|
|
9
|
+
import { PageState } from './feedback/page-state'
|
|
10
|
+
import { AttachmentImage } from '../lib/attachment-image'
|
|
11
|
+
import App from '../App'
|
|
12
|
+
import {
|
|
13
|
+
getUiLanguage,
|
|
14
|
+
getUiLocale,
|
|
15
|
+
selectUiLanguage,
|
|
16
|
+
} from '../lib/ui-language'
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
vi.unstubAllGlobals()
|
|
20
|
+
runtime.getRuntimeContext.mockReset()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('built-in component language', () => {
|
|
24
|
+
it('prefers supported host locales, then browser locales, then English', () => {
|
|
25
|
+
expect(selectUiLanguage('ja-JP', 'zh-CN')).toBe('ja')
|
|
26
|
+
expect(selectUiLanguage('zh_CN', 'ja-JP')).toBe('zh')
|
|
27
|
+
expect(selectUiLanguage('zh-TW', 'ja-JP')).toBe('zh')
|
|
28
|
+
expect(getUiLocale('zh')).toBe('zh-CN')
|
|
29
|
+
expect(selectUiLanguage('fr-FR', 'ja-JP')).toBe('ja')
|
|
30
|
+
expect(selectUiLanguage(undefined, 'fr-FR')).toBe('en')
|
|
31
|
+
expect(selectUiLanguage(undefined, undefined)).toBe('en')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('uses the browser language before the host context resolves', () => {
|
|
35
|
+
vi.stubGlobal('navigator', { language: 'ja-JP' })
|
|
36
|
+
vi.stubGlobal('document', { documentElement: { lang: 'en' } })
|
|
37
|
+
|
|
38
|
+
expect(getUiLanguage()).toBe('ja')
|
|
39
|
+
expect(getUiLocale()).toBe('ja-JP')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('uses English on server render until the browser resolves the locale', () => {
|
|
43
|
+
expect(renderToStaticMarkup(<App />)).toContain(
|
|
44
|
+
'Start with a real business workflow',
|
|
45
|
+
)
|
|
46
|
+
expect(renderToStaticMarkup(<UserSelectorEditor value={null} />)).toContain(
|
|
47
|
+
'Select members',
|
|
48
|
+
)
|
|
49
|
+
expect(renderToStaticMarkup(<UserSelectorPanel />)).toContain(
|
|
50
|
+
'No recommended members yet',
|
|
51
|
+
)
|
|
52
|
+
expect(renderToStaticMarkup(<PageState status="error" />)).toContain(
|
|
53
|
+
'Please try again later',
|
|
54
|
+
)
|
|
55
|
+
expect(
|
|
56
|
+
renderToStaticMarkup(
|
|
57
|
+
<AttachmentImage
|
|
58
|
+
attachment={{
|
|
59
|
+
tableId: 'tblA',
|
|
60
|
+
fieldId: 'fldA',
|
|
61
|
+
recordId: 'recA',
|
|
62
|
+
fileToken: 'fileA',
|
|
63
|
+
}}
|
|
64
|
+
alt="attachment"
|
|
65
|
+
/>,
|
|
66
|
+
),
|
|
67
|
+
).toContain('Loading image')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('uses browser lang until the host locale arrives, then applies the host locale', async () => {
|
|
71
|
+
vi.resetModules()
|
|
72
|
+
vi.stubGlobal('navigator', { language: 'zh-CN' })
|
|
73
|
+
vi.stubGlobal('document', { documentElement: { lang: 'en' } })
|
|
74
|
+
let resolveContext!: (value: { locale: string }) => void
|
|
75
|
+
runtime.getRuntimeContext.mockReturnValue(
|
|
76
|
+
new Promise((resolve) => {
|
|
77
|
+
resolveContext = resolve
|
|
78
|
+
}),
|
|
79
|
+
)
|
|
80
|
+
const language = await import('../lib/ui-language')
|
|
81
|
+
const request = language.initializeUiLanguage()
|
|
82
|
+
|
|
83
|
+
expect(document.documentElement.lang).toBe('zh-CN')
|
|
84
|
+
resolveContext({ locale: 'ja-JP' })
|
|
85
|
+
expect(await request).toBe('ja')
|
|
86
|
+
expect(language.getUiLanguage()).toBe('ja')
|
|
87
|
+
expect(document.documentElement.lang).toBe('ja-JP')
|
|
88
|
+
expect(runtime.getRuntimeContext).toHaveBeenCalledTimes(1)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('falls back to the browser when the host request fails', async () => {
|
|
92
|
+
vi.resetModules()
|
|
93
|
+
vi.stubGlobal('navigator', { language: 'ja-JP' })
|
|
94
|
+
vi.stubGlobal('document', { documentElement: { lang: 'en' } })
|
|
95
|
+
runtime.getRuntimeContext.mockRejectedValue(new Error('unavailable'))
|
|
96
|
+
const language = await import('../lib/ui-language')
|
|
97
|
+
|
|
98
|
+
expect(await language.initializeUiLanguage()).toBe('ja')
|
|
99
|
+
expect(document.documentElement.lang).toBe('ja-JP')
|
|
100
|
+
})
|
|
101
|
+
})
|
|
@@ -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.
|