@lark-base-open/base-database-app-sdk 0.0.0-stage → 0.0.1-alpha.0

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 (55) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +30 -2
  3. package/capabilities.json +75 -0
  4. package/development-pairing.json +11 -0
  5. package/dist/capabilities-56e403e7.d.ts +22 -0
  6. package/dist/chunk-3JX3FIBG.js +1 -0
  7. package/dist/chunk-HEMWSWXI.js +1 -0
  8. package/dist/chunk-OHPYKFCM.js +1 -0
  9. package/dist/chunk-YDAVA3BB.js +1 -0
  10. package/dist/components.d.ts +5 -0
  11. package/dist/components.js +1 -0
  12. package/dist/index-64557073.d.ts +306 -0
  13. package/dist/page.d.ts +137 -0
  14. package/dist/page.js +1 -0
  15. package/dist/server.cjs +1 -0
  16. package/dist/server.d.cts +78 -0
  17. package/dist/server.d.ts +78 -0
  18. package/dist/server.js +1 -0
  19. package/dist/shared.cjs +1 -0
  20. package/dist/shared.d.cts +177 -0
  21. package/dist/shared.d.ts +177 -0
  22. package/dist/shared.js +1 -0
  23. package/dist/testing.d.ts +73 -0
  24. package/dist/testing.js +1 -0
  25. package/dist/theme.css +168 -0
  26. package/dist/vite.d.ts +15 -0
  27. package/dist/vite.js +3 -0
  28. package/docs/README.md +16 -0
  29. package/docs/capabilities.md +59 -0
  30. package/docs/compat.md +36 -0
  31. package/docs/components.md +34 -0
  32. package/docs/exports.md +39 -0
  33. package/docs/page.md +101 -0
  34. package/docs/server.md +92 -0
  35. package/docs/shared.md +21 -0
  36. package/docs/start-base-app.md +47 -0
  37. package/docs/testing.md +57 -0
  38. package/docs/upgrade.md +44 -0
  39. package/examples/capabilities.ts +7 -0
  40. package/examples/compat.ts +21 -0
  41. package/examples/components.tsx +11 -0
  42. package/examples/contract-types.ts +41 -0
  43. package/examples/development-context.ts +17 -0
  44. package/examples/http.ts +12 -0
  45. package/examples/link.ts +10 -0
  46. package/examples/mock.ts +8 -0
  47. package/examples/msw.ts +5 -0
  48. package/examples/page.ts +6 -0
  49. package/examples/server.ts +57 -0
  50. package/examples/shared.ts +10 -0
  51. package/examples/start-base-app.tsx +8 -0
  52. package/examples/upgrade.ts +7 -0
  53. package/examples/upload.ts +18 -0
  54. package/examples/vite.ts +2 -0
  55. package/package.json +105 -4
@@ -0,0 +1,177 @@
1
+ interface SDKErrorOptions {
2
+ wireCode?: number;
3
+ retryable?: boolean;
4
+ outcome?: "failed" | "unknown";
5
+ paths?: readonly (readonly (string | number)[])[];
6
+ }
7
+ /** message 必须是开发者提供的安全文案,不能包含原始错误或参数。 */
8
+ declare const errorBrand: unique symbol;
9
+ declare class SDKError extends Error {
10
+ readonly code: string;
11
+ private readonly [errorBrand];
12
+ static [Symbol.hasInstance](value: unknown): boolean;
13
+ readonly wireCode: number;
14
+ readonly safeMessage: string;
15
+ readonly retryable: boolean;
16
+ readonly outcome: "failed" | "unknown";
17
+ readonly paths: readonly (readonly (string | number)[])[];
18
+ readonly upstreamCode?: string;
19
+ constructor(code: string, message: string, options?: SDKErrorOptions | string, legacyRetryable?: boolean);
20
+ }
21
+
22
+ type JsonValue = null | boolean | number | string | JsonValue[] | {
23
+ [key: string]: JsonValue;
24
+ };
25
+ interface FunctionEnvelope<T = unknown> {
26
+ code: number;
27
+ msg: string;
28
+ data?: T;
29
+ }
30
+ type IgnoredField = {
31
+ id: string;
32
+ name: string;
33
+ reason: string;
34
+ };
35
+ type WriteResult = {
36
+ status: "success";
37
+ appliedIds: string[];
38
+ notFoundIds: [];
39
+ ignoredFields: [];
40
+ attachments?: JsonValue;
41
+ noop?: boolean;
42
+ } | {
43
+ status: "partial";
44
+ appliedIds: string[];
45
+ notFoundIds: string[];
46
+ ignoredFields: IgnoredField[];
47
+ attachments?: JsonValue;
48
+ } | {
49
+ status: "failed";
50
+ code: string;
51
+ upstreamCode?: number;
52
+ retryable: false;
53
+ appliedIds: [];
54
+ } | {
55
+ status: "unknown";
56
+ code: string;
57
+ retryable: false;
58
+ requestedIds?: string[];
59
+ logid?: string;
60
+ };
61
+
62
+ type CapabilityDescriptor = Readonly<{
63
+ version: number;
64
+ }>;
65
+ type CapabilityMap = Readonly<Record<string, CapabilityDescriptor>>;
66
+ type RuntimeCapabilities = CapabilityMap;
67
+ type CapabilityUnavailableReason = "permission-denied" | "region-unavailable" | "temporarily-unavailable";
68
+ interface CapabilityContext {
69
+ capabilities?: CapabilityMap;
70
+ capabilityUnavailable?: Readonly<Record<string, {
71
+ reason: CapabilityUnavailableReason;
72
+ retryable: boolean;
73
+ }>>;
74
+ }
75
+ declare const CAPABILITY_CATALOG: {
76
+ readonly "runtime.context": {
77
+ readonly kind: "critical";
78
+ readonly hostBaseline84afb07: true;
79
+ };
80
+ readonly "http.request": {
81
+ readonly kind: "critical";
82
+ readonly hostBaseline84afb07: true;
83
+ };
84
+ readonly "http.binary": {
85
+ readonly kind: "critical";
86
+ readonly hostBaseline84afb07: true;
87
+ };
88
+ readonly "http.cancel": {
89
+ readonly kind: "optional";
90
+ readonly hostBaseline84afb07: true;
91
+ };
92
+ readonly "function.invoke": {
93
+ readonly kind: "critical";
94
+ readonly hostBaseline84afb07: true;
95
+ };
96
+ readonly "runtime.user": {
97
+ readonly kind: "optional";
98
+ readonly hostBaseline84afb07: true;
99
+ };
100
+ readonly "runtime.logout": {
101
+ readonly kind: "optional";
102
+ readonly hostBaseline84afb07: true;
103
+ };
104
+ readonly "runtime.requestEdit": {
105
+ readonly kind: "optional";
106
+ readonly hostBaseline84afb07: true;
107
+ };
108
+ readonly "attachment.upload": {
109
+ readonly kind: "critical";
110
+ readonly hostBaseline84afb07: true;
111
+ };
112
+ readonly "attachment.cancel": {
113
+ readonly kind: "optional";
114
+ readonly hostBaseline84afb07: true;
115
+ };
116
+ readonly "chatbot.stream": {
117
+ readonly kind: "critical";
118
+ readonly hostBaseline84afb07: true;
119
+ };
120
+ readonly "ai.function.stream": {
121
+ readonly kind: "critical";
122
+ readonly hostBaseline84afb07: true;
123
+ };
124
+ readonly "runtime.lifecycle": {
125
+ readonly kind: "optional";
126
+ readonly hostBaseline84afb07: true;
127
+ };
128
+ readonly "runtime.mode": {
129
+ readonly kind: "optional";
130
+ readonly hostBaseline84afb07: true;
131
+ };
132
+ readonly "attachment.progress": {
133
+ readonly kind: "optional";
134
+ readonly hostBaseline84afb07: true;
135
+ };
136
+ readonly "link.open": {
137
+ readonly kind: "optional";
138
+ readonly hostBaseline84afb07: true;
139
+ };
140
+ };
141
+ type CapabilityName = keyof typeof CAPABILITY_CATALOG;
142
+ interface CapabilityQueryResult {
143
+ name: string;
144
+ requiredVersion: number;
145
+ version: number | null;
146
+ supported: boolean;
147
+ source: "advertised" | "host-baseline-84afb07";
148
+ availability: "available" | "unknown" | CapabilityUnavailableReason | "unsupported";
149
+ }
150
+ declare const DiagnosticCode: {
151
+ readonly HostUnsupported: "CAPABILITY_HOST_UNSUPPORTED";
152
+ readonly PermissionDenied: "CAPABILITY_PERMISSION_DENIED";
153
+ readonly RegionUnavailable: "CAPABILITY_REGION_UNAVAILABLE";
154
+ readonly TemporarilyUnavailable: "CAPABILITY_TEMPORARILY_UNAVAILABLE";
155
+ readonly ContextInvalid: "CAPABILITY_CONTEXT_INVALID";
156
+ readonly UnavailableUnknown: "CAPABILITY_UNAVAILABLE_UNKNOWN";
157
+ };
158
+ type DiagnosticCategory = "host-unsupported" | "permission-denied" | "region-unavailable" | "temporary" | "protocol" | "unknown";
159
+ interface RuntimeDiagnostic {
160
+ source: "sdk";
161
+ code: string;
162
+ upstreamCode?: string;
163
+ category: DiagnosticCategory;
164
+ retryable: boolean;
165
+ message: string;
166
+ redaction: "原始错误消息和业务参数已省略";
167
+ }
168
+
169
+ declare class CapabilityUnavailableError extends SDKError {
170
+ readonly capability: string;
171
+ readonly minimumVersion: number;
172
+ constructor(capability: string, minimumVersion: number);
173
+ }
174
+ /** 不解析 message 猜测原因;可恢复不代表写入可安全重试。 */
175
+ declare function classifyRuntimeError(error: unknown): RuntimeDiagnostic;
176
+
177
+ export { CAPABILITY_CATALOG, CapabilityContext, CapabilityDescriptor, CapabilityMap, CapabilityName, CapabilityQueryResult, CapabilityUnavailableError, CapabilityUnavailableReason, DiagnosticCategory, DiagnosticCode, FunctionEnvelope, JsonValue, RuntimeCapabilities, RuntimeDiagnostic, SDKError, SDKErrorOptions, WriteResult, classifyRuntimeError };
package/dist/shared.js ADDED
@@ -0,0 +1 @@
1
+ import{a as b,b as c,c as d,d as e}from"./chunk-YDAVA3BB.js";import{a}from"./chunk-3JX3FIBG.js";export{b as CAPABILITY_CATALOG,d as CapabilityUnavailableError,c as DiagnosticCode,a as SDKError,e as classifyRuntimeError};
@@ -0,0 +1,73 @@
1
+ import { SiteRuntimeApi } from '@lark-base-open/base-site-channel/runtime-contract';
2
+ import { RuntimeChannel } from '@lark-base-open/base-site-channel/runtime';
3
+ import { S as SiteRuntimeContext } from './capabilities-56e403e7.js';
4
+ import { RuntimeCapabilities } from './shared.js';
5
+ import { HttpHandler } from 'msw';
6
+
7
+ interface SiteHttpBridgeRoute {
8
+ method: string;
9
+ path: string;
10
+ }
11
+ interface SiteHttpBridgeAllowlistConfig {
12
+ routes: SiteHttpBridgeRoute[];
13
+ }
14
+ /**
15
+ * Runtime 当前使用的全部父页面 HTTP bridge 路由。AppSettings 未配置或配置非法时回退到这里,
16
+ * 避免把 bridge 重新退化为任意同源请求代理。
17
+ */
18
+ declare const DEFAULT_SITE_HTTP_BRIDGE_ALLOWLIST: SiteHttpBridgeAllowlistConfig;
19
+
20
+ interface HttpFixture {
21
+ method?: "get" | "post" | "put" | "patch" | "delete";
22
+ url: string;
23
+ status?: number;
24
+ body?: unknown;
25
+ headers?: Record<string, string>;
26
+ }
27
+ /** 仅替换真正的 HTTP;channel 的 postMessage 必须使用 createMockHost。 */
28
+ declare function createHttpHandlers(fixtures: readonly HttpFixture[]): HttpHandler[];
29
+
30
+ type MockApi = (typeof SiteRuntimeApi)[keyof typeof SiteRuntimeApi];
31
+ interface MockCall {
32
+ api: string;
33
+ params: unknown;
34
+ }
35
+ interface MockHandlerContext {
36
+ signal: AbortSignal;
37
+ emit: (event: string, payload: unknown) => Promise<void>;
38
+ }
39
+ interface MockHostOptions {
40
+ context?: Partial<SiteRuntimeContext>;
41
+ /** undefined 模拟无能力字段的 84afb07 宿主;{} 模拟明确无能力的新宿主。 */
42
+ capabilities?: RuntimeCapabilities;
43
+ bindings?: readonly {
44
+ baseToken: string;
45
+ tableIds: readonly string[];
46
+ }[];
47
+ allowedRoutes?: readonly SiteHttpBridgeRoute[];
48
+ origin?: string;
49
+ secureLinkAvailable?: boolean;
50
+ functions?: Record<string, (params: unknown) => unknown | Promise<unknown>>;
51
+ handlers?: Partial<Record<MockApi, (params: unknown, context: MockHandlerContext) => unknown | Promise<unknown>>>;
52
+ failures?: Partial<Record<MockApi, {
53
+ code: string;
54
+ message?: string;
55
+ }>>;
56
+ }
57
+ /** 无网络、无真实身份。校验始终先于配置的失败和 handler。 */
58
+ declare function createMockHost(options?: MockHostOptions): {
59
+ channel: RuntimeChannel;
60
+ context: SiteRuntimeContext;
61
+ calls: MockCall[];
62
+ events: {
63
+ event: string;
64
+ payload: unknown;
65
+ }[];
66
+ emit: (event: string, payload: unknown) => Promise<void>;
67
+ /** 真实 postMessage 传输使用 channel Host 的 origin/source 校验,非法消息无回包。 */
68
+ mount(container: HTMLElement, url: string, allowedOrigin?: string): () => void;
69
+ emitStreamMessage(subscriptionId: string, data: unknown, seq: number, ai?: boolean): Promise<void>;
70
+ dispose(): void;
71
+ };
72
+
73
+ export { DEFAULT_SITE_HTTP_BRIDGE_ALLOWLIST, HttpFixture, MockApi, MockCall, MockHandlerContext, MockHostOptions, createHttpHandlers, createMockHost };
@@ -0,0 +1 @@
1
+ import{Host as me,ChannelCallbackError as P}from"@lark-base-open/base-site-channel";import{SiteRuntimeApi as s,SiteRuntimeEvent as C,SiteRuntimeFunction as D,isValidSiteFunctionName as he}from"@lark-base-open/base-site-channel/runtime-contract";import{ChannelCallbackError as K}from"@lark-base-open/base-site-channel";import{SiteRuntimeErrorCode as b}from"@lark-base-open/base-site-channel/runtime-contract";var X=3e4,Z=6e4,Y=4;var J=32,Q=127,ee=new Set(["DELETE","GET","PATCH","POST","PUT"]),te=new Set(["authorization","connection","content-length","cookie","host","origin","proxy-authorization","referer","transfer-encoding"]);var ne=/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/,E=(e,n)=>{throw new K(e,n)},oe=e=>{let n=e;for(let r=0;r<Y;r+=1){let a;try{a=decodeURIComponent(n)}catch{return E(b.InvalidHttpUrl,"\u8BF7\u6C42\u8DEF\u5F84\u7F16\u7801\u65E0\u6548")}if(a===n)return n;n=a}return n},B=(e,n)=>{if(typeof e!="string"||!e.startsWith("/")||e.startsWith("//"))return E(b.InvalidHttpUrl,"\u8BF7\u6C42\u8DEF\u5F84\u5FC5\u987B\u4EE5\u5355\u4E2A / \u5F00\u5934");if(e.includes("\\")||e.includes("#"))return E(b.InvalidHttpUrl,"\u8BF7\u6C42\u8DEF\u5F84\u5305\u542B\u975E\u6CD5\u5B57\u7B26");let r=oe(e),a=r.split("?")[0]??"";if(r.startsWith("//")||r.includes("\\")||Array.from(r).some(f=>{let m=f.charCodeAt(0);return m<J||m===Q})||a.split("/").some(f=>f===".."))return E(b.InvalidHttpUrl,"\u8BF7\u6C42\u8DEF\u5F84\u4E0D\u5141\u8BB8\u8DE8\u76EE\u5F55\u6216\u8DE8\u57DF");let c=new URL(e,n);return c.origin!==n?E(b.InvalidHttpUrl,"\u8BF7\u6C42\u8DEF\u5F84\u4E0D\u5141\u8BB8\u8DE8\u57DF"):`${c.pathname}${c.search}`},ie=e=>{if(e===void 0)return{};if(e===null||typeof e!="object"||Array.isArray(e))return E(b.InvalidHttpHeader,"\u8BF7\u6C42\u5934\u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u952E\u503C\u5BF9\u8C61");let n={};for(let[r,a]of Object.entries(e)){let c=r.toLowerCase();if(typeof a!="string"||!ne.test(r)||te.has(c)||c.startsWith("sec-")||c.startsWith("x-forwarded-")||c.startsWith("rpc-"))return E(b.InvalidHttpHeader,`\u8BF7\u6C42\u5934 ${r} \u4E0D\u5141\u8BB8\u900F\u4F20`);n[r]=a}return n},F=e=>{let n=e?.method?.toUpperCase()??"GET";if(!ee.has(n))return E(b.InvalidHttpUrl,"\u8BF7\u6C42\u65B9\u6CD5\u4E0D\u53D7\u652F\u6301");if(e?.body!==void 0&&typeof e.body!="string")return E(b.UnsupportedHttpBody,"\u8BF7\u6C42\u4F53\u4EC5\u652F\u6301\u5B57\u7B26\u4E32");if((n==="GET"||n==="HEAD")&&e?.body!==void 0)return E(b.UnsupportedHttpBody,"GET \u8BF7\u6C42\u4E0D\u652F\u6301\u8BF7\u6C42\u4F53");let r=e?.timeoutMs??X;if(typeof r!="number"||!Number.isFinite(r)||r<=0)return E(b.InvalidHttpUrl,"\u8BF7\u6C42\u8D85\u65F6\u65F6\u95F4\u65E0\u6548");let a=Math.min(Math.max(r,1),Z);return{body:e?.body,credentials:"same-origin",headers:ie(e?.headers),method:n,timeoutMs:a}};var re=/^:[A-Za-z][A-Za-z0-9_]*$/;var v={routes:[{method:"GET",path:"/space/api/box/stream/download/v2/cover/:fileToken"},{method:"GET",path:"/space/api/bitable/site/v1/:siteToken/invoke_get"},{method:"POST",path:"/space/api/bitable/site/v1/:siteToken/invoke_function/:functionId"},{method:"POST",path:"/space/api/bitable/site/v1/:siteToken/ai_functions/:functionKey/tasks"},{method:"GET",path:"/space/api/bitable/site/v1/:siteToken/ai_functions/:functionKey/tasks/:taskId"},{method:"POST",path:"/space/api/bitable/site/v1/:siteToken/bases/:baseToken/tables/:tableId/records/batch_create"},{method:"POST",path:"/space/api/bitable/site/v1/:siteToken/bases/:baseToken/tables/:tableId/records/batch_update"},{method:"POST",path:"/space/api/bitable/site/v1/:siteToken/bases/:baseToken/tables/:tableId/records/batch_delete"},{method:"POST",path:"/space/api/box/file/temporary_download_url"},{method:"POST",path:"/space/api/mention/recommend.v2"},{method:"GET",path:"/space/api/user"},{method:"POST",path:"/space/api/bitable/chatbot/chat"},{method:"GET",path:"/space/api/bitable/chatbot/poll"},{method:"POST",path:"/space/api/bitable/chatbot/:chatbotId/history/list"},{method:"POST",path:"/space/api/bitable/chatbot/chatbot_session/reset"},{method:"GET",path:"/space/api/bitable/chatbot/:chatbotId/run_log/session"},{method:"POST",path:"/space/api/bitable/chatbot/:chatbotId/run_log/stop"}]},q=e=>e.length>1&&e.endsWith("/")?e.slice(0,-1):e;var se=(e,n)=>{let r=q(e).split("/"),a=q(n).split("/");return r.length===a.length&&a.every((c,f)=>{let m=r[f];return re.test(c)?!!m:c===m})},z=(e,n,r)=>{let a=e.split("?")[0]??"";return r.some(c=>c.method===n&&se(a,c.path))};import{ChannelCallbackError as j}from"@lark-base-open/base-site-channel";import{SiteRuntimeErrorCode as ae}from"@lark-base-open/base-site-channel/runtime-contract";var G="/space/api/box/stream/download/v2/cover/",ce="/space/api/box/file/temporary_download_url",le=new Set(["120","360","720","1280"]),de=/^[A-Za-z0-9_-]{1,256}$/,w=e=>typeof e=="string"&&de.test(e),y=()=>{throw new j(ae.InvalidAttachmentTarget,"\u9644\u4EF6\u76EE\u6807\u4E0D\u5C5E\u4E8E\u5F53\u524D Site \u6216\u9644\u4EF6\u5750\u6807\u65E0\u6548")},$=(e,n,r)=>{let a=e.pathname.startsWith(G),c=e.pathname.replace(/\/+$/,"")===ce;if(!a&&!c)return()=>{};try{let f=c&&typeof r?.body=="string"?JSON.parse(r.body):void 0,m=a?e.pathname.slice(G.length):f?.file_token;if(a&&(e.searchParams.getAll("extra").length!==1||!["width","height"].every(o=>e.searchParams.getAll(o).length===1&&le.has(e.searchParams.get(o)??""))))return y();let R=JSON.parse(a?e.searchParams.get("extra")??"null":f?.extra??"null"),p=R?.bitablePerm;if(!n||!w(m)||!w(p?.tableId))return y();let _=Object.entries(p.attachments??{});if(_.length!==1||!w(_[0][0]))return y();let A=Object.entries(_[0][1]);if(A.length!==1||!w(A[0][0]))return y();let I=A[0][1];if(!Array.isArray(I)||I.length!==1||I[0]!==m)return y();let k=n.getSiteToken(),H=n.getSiteEnv?.()??"prod";if(!k||p.siteToken!==void 0&&p.baseAppToken!==void 0||(p.siteToken??p.baseAppToken)!==k||p.siteEnv!==void 0&&p.siteEnv!==H||c&&(r?.method!=="POST"||e.searchParams.has("extra"))||Object.keys(R).some(o=>o!=="bitablePerm"))return y();let O=JSON.stringify({bitablePerm:{tableId:p.tableId,attachments:p.attachments,siteToken:k,siteEnv:H}});a?e.searchParams.set("extra",O):r&&(r.body=JSON.stringify({file_token:m,extra:O}));let t=()=>{(k!==n.getSiteToken()||H!==(n.getSiteEnv?.()??"prod")||!n.isBoundTable(p.tableId))&&y()};return t(),t}catch(f){if(f instanceof j)throw f;return y()}};import{http as pe,HttpResponse as ue}from"msw";function fe(e){return e.map(n=>pe[n.method??"get"](n.url,()=>ue.json(n.body??null,{status:n.status??200,headers:n.headers})))}var u=(e,n=e)=>{throw new P(e,n)},Te=new Set(["cache-control","content-language","content-type","etag","last-modified","x-request-id","x-tt-logid","x-tt-trace-id"]);function Oe(e={}){let n={protocolVersion:"site.v1",colorScheme:"light",locale:"zh-CN",mode:"reading",env:"preview",siteToken:"mock-site",...e.context,...e.capabilities===void 0?{}:{capabilities:e.capabilities}},r=e.origin??"https://host.example",a=[],c=[],f=new Map,m=new Map,R=new Set,p=new Set,_=!1,A=0,I=async(t,o)=>{_||(c.push({event:t,payload:o}),await Promise.all([...[...f.get(t)??[]].map(l=>l(o)),...[...p].map(l=>l.emitEvent(t,o))]))},k=(t,o)=>(e.bindings??[]).some(l=>l.baseToken===t&&l.tableIds.includes(o)),H=async(t,o)=>{if(_&&u("CHANNEL_DESTROYED"),Object.values(s).includes(t)||u("10004"),a.push({api:t,params:o}),t!==s.RuntimeContext&&n.capabilities!==void 0){let h={[s.HttpRequestCancel]:"http.cancel",[s.AttachmentUploadCancel]:"attachment.cancel",[s.StreamOpen]:"chatbot.stream",[s.StreamClose]:"chatbot.stream",[s.AIFunctionStreamOpen]:"ai.function.stream",[s.AIFunctionStreamClose]:"ai.function.stream"}[t]??t.replace("site.v1.",""),d=Object.prototype.hasOwnProperty.call(n.capabilities,h)?n.capabilities[h]?.version:void 0;(!Number.isInteger(d)||(d??0)<1)&&u("10004")}let l,T=()=>{},g,S;if(t===s.RuntimeContext)l=structuredClone(n);else if(t===s.OpenLink){let i;try{if(typeof o?.url!="string"||!o.url.trim())throw 0;i=new URL(o.url)}catch{return u("INVALID_LINK_URL")}["http:","https:","mailto:","tel:"].includes(i.protocol)||u("INVALID_LINK_URL"),["http:","https:"].includes(i.protocol)&&e.secureLinkAvailable===!1&&u("LINK_CHECK_UNAVAILABLE"),l={opened:!0}}else if(t===s.HttpRequest){(!o||typeof o!="object")&&u("INVALID_HTTP_URL");let i=o,h=B(i.url,r);S=F(i.init),z(h,S.method??"GET",e.allowedRoutes??v.routes)||u("INVALID_HTTP_URL"),["text","arrayBuffer"].includes(i.init?.responseType??"text")||u("HTTP_RESPONSE_TYPE_UNSUPPORTED");let d=i.requestId??`legacy-${++A}`;(typeof d!="string"||!d||d.length>256||m.has(`http:${d}`))&&u("HTTP_REQUEST_FAILED"),g=`http:${d}`,T=$(new URL(h,r),{getSiteToken:()=>n.siteToken,getSiteEnv:()=>n.env??"prod",isBoundTable:U=>(e.bindings??[]).some(V=>V.tableIds.includes(U))},S),l={body:i.init?.responseType==="arrayBuffer"?new ArrayBuffer(0):"{}",...i.init?.responseType==="arrayBuffer"?{bodyType:"arrayBuffer"}:{},headers:{},status:200,statusText:"OK"}}else if(t===s.AttachmentUpload){let i=o,h=typeof i?.baseToken=="string"?i.baseToken.trim():"",d=typeof i?.tableId=="string"?i.tableId.trim():"";(!h||!d||typeof File>"u"||!(i.file instanceof File)||i.file.size<=0||i.file.size>2*1024*1024*1024||!k(h,d))&&u("INVALID_ATTACHMENT_TARGET"),n.siteToken?.trim()||u("ATTACHMENT_UPLOAD_FAILED"),g=`upload:${i.uploadId??++A}`,l={fileToken:"mock-file",name:i.file.name,size:i.file.size,mimeType:i.file.type}}else if(t===s.HttpRequestCancel||t===s.AttachmentUploadCancel){let i=t===s.HttpRequestCancel?`http:${o?.requestId}`:`upload:${o?.uploadId}`,h=m.get(i);h?.controller.abort(),h?.reject(new P(t===s.HttpRequestCancel?"HTTP_REQUEST_CANCELED":"ATTACHMENT_UPLOAD_CANCELED","\u6D4B\u8BD5\u8C03\u7528\u5DF2\u53D6\u6D88"))}else if(t===s.FunctionInvoke){let i=o?.functionName;(typeof i!="string"||!he(i))&&u("INVALID_FUNCTION_NAME"),i===D.GetCurrentUser?l={name:"\u6D4B\u8BD5\u7528\u6237",avatarUrl:""}:[D.Logout,D.RequestEdit].includes(i)||(!e.functions||!Object.prototype.hasOwnProperty.call(e.functions,i))&&u("FUNCTION_NOT_ALLOWED")}else t===s.AIFunctionStreamOpen||t===s.StreamOpen?(o?.subscriptionId||u("STREAM_UNAVAILABLE"),t===s.AIFunctionStreamOpen&&(!n.siteToken||!o.functionKey)&&u("STREAM_UNAVAILABLE")):(t===s.AIFunctionStreamClose||t===s.StreamClose)&&R.delete(o?.subscriptionId);let L=e.failures?.[t];L&&u(L.code,L.message);let x=new AbortController,M,N=()=>{},W=new Promise((i,h)=>{N=h});g??=`call:${++A}`,m.set(g,{controller:x,reject:N}),S&&(M=setTimeout(()=>{x.abort(),N(new P("REQUEST_TIMEOUT","HTTP \u8BF7\u6C42\u8D85\u65F6"))},S.timeoutMs));try{T();let i=e.handlers?.[t],h=t===s.FunctionInvoke?e.functions?.[o.functionName]:void 0,d=await Promise.race([Promise.resolve().then(()=>i?i(o,{signal:x.signal,emit:I}):h?h(o.params):l),W]);return T(),(t===s.AIFunctionStreamOpen||t===s.StreamOpen)&&R.add(o.subscriptionId),t===s.HttpRequest&&d&&typeof d=="object"&&"headers"in d?{...d,headers:Object.fromEntries(Object.entries(d.headers).filter(([U])=>Te.has(U.toLowerCase())))}:d}finally{clearTimeout(M),g&&m.delete(g)}};return{channel:{invokeApi:H,emitEvent:async(t,o)=>{c.push({event:t,payload:o})},subscribeEvent:(t,o)=>{let l=f.get(t)??new Set;return l.add(o),f.set(t,l),()=>{l.delete(o)}}},context:n,calls:a,events:c,emit:I,mount(t,o,l=new URL(o).origin){let T=new me(t,()=>{},`mock-${++A}`,[l],{sandbox:"allow-scripts allow-same-origin allow-downloads"},{enableBinaryFetch:!1});for(let g of Object.values(s))T.registerApi(g,S=>H(g,S));for(let g of[C.LifecycleReady,C.LifecycleLoadFailed])T.subscribeEvent(g,S=>{c.push({event:g,payload:S})});return p.add(T),T.setClientUrl(o),()=>{T.destroy(),p.delete(T)}},emitStreamMessage(t,o,l,T=!1){return R.has(t)?I(T?C.AIFunctionStreamMessage:C.StreamMessage,{subscriptionId:t,data:o,seq:l}):Promise.resolve()},dispose(){_=!0;for(let t of m.values())t.controller.abort(),t.reject(new P("CHANNEL_DESTROYED","\u6D4B\u8BD5\u5BBF\u4E3B\u5DF2\u9500\u6BC1"));m.clear(),f.clear(),R.clear();for(let t of p)t.destroy();p.clear()}}}export{v as DEFAULT_SITE_HTTP_BRIDGE_ALLOWLIST,fe as createHttpHandlers,Oe as createMockHost};
package/dist/theme.css ADDED
@@ -0,0 +1,168 @@
1
+ @layer base-app {
2
+ .base-app-platform {
3
+ --base-app-color-bg: #fff;
4
+ --base-app-color-fg: #1f2329;
5
+ --base-app-color-muted: #646a73;
6
+ --base-app-color-accent: #1456f0;
7
+ --base-app-color-danger: #c6262e;
8
+ --base-app-color-border: #dee0e3;
9
+ --base-app-radius: 8px;
10
+ --base-app-font-family: system-ui, sans-serif;
11
+ --base-app-font-size: 14px;
12
+ --base-app-z-popover: 1000;
13
+ --base-app-z-floating: 900;
14
+ --base-app-shadow: 0 4px 20px #0002;
15
+ color: var(--base-app-color-fg);
16
+ font: var(--base-app-font-size) / 1.5 var(--base-app-font-family);
17
+ box-sizing: border-box;
18
+ }
19
+ .base-app-platform[data-theme="dark"],
20
+ [data-theme="dark"] .base-app-platform {
21
+ --base-app-color-bg: #22252b;
22
+ --base-app-color-fg: #f4f5f7;
23
+ --base-app-color-muted: #b3b7c0;
24
+ --base-app-color-border: #515660;
25
+ --base-app-color-accent: #7da6ff;
26
+ }
27
+ @media (prefers-color-scheme: dark) {
28
+ .base-app-platform[data-theme="host"] {
29
+ --base-app-color-bg: #22252b;
30
+ --base-app-color-fg: #f4f5f7;
31
+ --base-app-color-muted: #b3b7c0;
32
+ --base-app-color-border: #515660;
33
+ --base-app-color-accent: #7da6ff;
34
+ }
35
+ }
36
+ .base-app-platform button {
37
+ font: inherit;
38
+ color: inherit;
39
+ background: var(--base-app-color-bg);
40
+ border: 1px solid var(--base-app-color-border);
41
+ border-radius: var(--base-app-radius);
42
+ padding: 6px 10px;
43
+ cursor: pointer;
44
+ }
45
+ .base-app-platform button:focus-visible,
46
+ .base-app-platform input:focus-visible {
47
+ outline: 2px solid var(--base-app-color-accent);
48
+ outline-offset: 2px;
49
+ }
50
+ .base-app-popover {
51
+ background: var(--base-app-color-bg);
52
+ border: 1px solid var(--base-app-color-border);
53
+ border-radius: var(--base-app-radius);
54
+ box-shadow: var(--base-app-shadow);
55
+ padding: 8px;
56
+ width: 280px;
57
+ max-width: calc(100vw - 16px);
58
+ max-height: var(--radix-popover-content-available-height);
59
+ overflow: auto;
60
+ z-index: var(--base-app-z-popover);
61
+ }
62
+ .base-app-editor {
63
+ display: flex;
64
+ gap: 4px;
65
+ align-items: center;
66
+ }
67
+ .base-app-editor > button:first-child {
68
+ flex: 1;
69
+ text-align: start;
70
+ }
71
+ .base-app-editor span + span {
72
+ margin-inline-start: 8px;
73
+ }
74
+ .base-app-selector {
75
+ background: var(--base-app-color-bg);
76
+ min-width: 0;
77
+ }
78
+ .base-app-selector [cmdk-input] {
79
+ box-sizing: border-box;
80
+ width: 100%;
81
+ padding: 8px;
82
+ border: 1px solid var(--base-app-color-border);
83
+ border-radius: var(--base-app-radius);
84
+ background: var(--base-app-color-bg);
85
+ color: inherit;
86
+ font: inherit;
87
+ }
88
+ .base-app-selector [cmdk-list] {
89
+ max-height: 240px;
90
+ overflow: auto;
91
+ margin-top: 6px;
92
+ }
93
+ .base-app-selector [cmdk-item] {
94
+ display: flex;
95
+ align-items: center;
96
+ gap: 8px;
97
+ padding: 8px;
98
+ border-radius: var(--base-app-radius);
99
+ cursor: pointer;
100
+ }
101
+ .base-app-selector [cmdk-item][data-selected="true"] {
102
+ background: color-mix(
103
+ in srgb,
104
+ var(--base-app-color-accent) 16%,
105
+ transparent
106
+ );
107
+ }
108
+ .base-app-selector small {
109
+ color: var(--base-app-color-muted);
110
+ }
111
+ .base-app-selector [role="status"] {
112
+ padding: 12px;
113
+ color: var(--base-app-color-muted);
114
+ }
115
+ .base-app-floating {
116
+ position: fixed;
117
+ bottom: 24px;
118
+ right: 24px;
119
+ z-index: var(--base-app-z-floating);
120
+ display: flex;
121
+ gap: 4px;
122
+ }
123
+ .base-app-floating[data-position="bottom-left"] {
124
+ left: 24px;
125
+ right: auto;
126
+ }
127
+ .base-app-floating [role="tooltip"],
128
+ .base-app-floating [role="alert"] {
129
+ position: absolute;
130
+ bottom: 100%;
131
+ width: 220px;
132
+ background: var(--base-app-color-bg);
133
+ padding: 8px;
134
+ border-radius: var(--base-app-radius);
135
+ box-shadow: var(--base-app-shadow);
136
+ }
137
+ .base-app-notice {
138
+ position: fixed;
139
+ top: 20px;
140
+ left: 50%;
141
+ transform: translateX(-50%);
142
+ background: var(--base-app-color-bg);
143
+ padding: 12px;
144
+ z-index: var(--base-app-z-floating);
145
+ }
146
+ .base-app-user-menu {
147
+ display: inline-flex;
148
+ align-items: center;
149
+ gap: 8px;
150
+ }
151
+ .base-app-user-menu img {
152
+ width: 28px;
153
+ height: 28px;
154
+ border-radius: 50%;
155
+ }
156
+ .base-app-attachment img {
157
+ max-width: 100%;
158
+ }
159
+ .base-app-platform [role="alert"] {
160
+ color: var(--base-app-color-danger);
161
+ }
162
+ @media (prefers-reduced-motion: reduce) {
163
+ .base-app-platform * {
164
+ animation: none !important;
165
+ transition: none !important;
166
+ }
167
+ }
168
+ }
package/dist/vite.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { Server, IncomingMessage, ServerResponse } from 'node:http';
2
+
3
+ /** Only server-side environment receives the token. Never inject it into Vite defines. */
4
+ declare function baseAppDevelopmentPairingPlugin(): {
5
+ name: string;
6
+ apply: "serve";
7
+ configureServer(server: {
8
+ httpServer: Server | null;
9
+ middlewares: {
10
+ use(handler: (req: IncomingMessage, res: ServerResponse, next: () => void) => void): void;
11
+ };
12
+ }): void;
13
+ };
14
+
15
+ export { baseAppDevelopmentPairingPlugin };
package/dist/vite.js ADDED
@@ -0,0 +1,3 @@
1
+ import{a as r}from"./chunk-OHPYKFCM.js";import{createHmac as h}from"crypto";function I(){let i=process.env[r.tokenEnv];return{name:"base-app-development-pairing",apply:"serve",configureServer(c){if(i){if(!/^[A-Za-z0-9_-]{43}$/.test(i))throw new Error("Invalid development pairing token");c.middlewares.use((e,o,m)=>{if(e.url?.split("?")[0]!==r.endpoint)return m();let n=(t,f)=>{for(let g of o.getHeaderNames())g.toLowerCase().startsWith("access-control-")&&o.removeHeader(g);o.setHeader("Cache-Control","no-store"),o.setHeader("Content-Type","application/json"),o.statusCode=t,o.end(JSON.stringify(f))},a=c.httpServer?.address(),s=a&&typeof a!="string"?a.port:0,l=`http://${r.host}:${s}`,u=e.headers.origin===void 0?e.headers["sec-fetch-site"]==="same-origin":e.headers.origin===l;if(s<r.portMin||s>r.portMax||e.headers.host!==`${r.host}:${s}`||!u)return n(403,{error:"PAIRING_ORIGIN_REJECTED"});if(e.method!=="POST")return n(405,{error:"PAIRING_METHOD_REJECTED"});let p="",d=!1;e.on("data",t=>{d||(p+=t.toString(),p.length>1024&&(d=!0,n(413,{error:"PAIRING_BODY_TOO_LARGE"})))}),e.on("end",()=>{if(!d)try{let{challenge:t}=JSON.parse(p);if(typeof t!="string"||!/^[A-Za-z0-9_-]{43}$/.test(t))return n(400,{error:"PAIRING_CHALLENGE_INVALID"});let f=h("sha256",i).update(`${r.proofPrefix}
2
+ ${l}
3
+ ${t}`).digest("base64url");n(200,{proof:f})}catch{n(400,{error:"PAIRING_CHALLENGE_INVALID"})}})})}}}}export{I as baseAppDevelopmentPairingPlugin};
package/docs/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # SDK 文档索引
2
+
3
+ 本版本提供页面与服务端 SDK、平台组件、启动管理及测试替身。没有根入口;以 [完整导出清单](exports.md) 和已安装包的 dist 声明为准。
4
+
5
+ - [page:页面运行时、附件与兼容调用](page.md)
6
+ - [server:defineFunction、ctx.records 与结果语义](server.md)
7
+ - [shared:通用类型与诊断](shared.md)
8
+ - [testing:模拟宿主与 MSW](testing.md)
9
+ - [components:平台组件、主题与插槽](components.md)
10
+ - [start-base-app:启动、清理与 Vite 开发插件](start-base-app.md)
11
+ - [capabilities:能力协商与诊断](capabilities.md)
12
+ - [compat:老模板兼容](compat.md)
13
+ - [upgrade:codemod 与模板 1→2 迁移](upgrade.md)
14
+ - [可编译示例](../examples)
15
+
16
+ SDK 文档说明本版本 API、组件、兼容语义与可编译示例。业务规则、服务端契约、Mutation 结果、CLI 命令与发布流程,请查阅 [@lark-base-open/base-database-cli](https://www.npmjs.com/package/@lark-base-open/base-database-cli) 随包安装的 lark-base-database Skill 及其 references。升级 CLI 时以该版本随包 Skill 为准。
@@ -0,0 +1,59 @@
1
+ # 能力与诊断
2
+
3
+ 宿主字段形状为 `capabilities: { [name: string]: { version: number } }`。只有缺字段才使用下面的 `host-baseline-84afb07` 宿主基线;显式空表不回退。null、数组或非正安全整数版本抛 CAPABILITY_CONTEXT_INVALID;未知能力和过低版本不支持。能力声明不代表用户有权限,也不保证依赖服务当前可用。
4
+
5
+ | 能力 | 分类 | 缺字段时 host-baseline-84afb07(版本 1) |
6
+ |---|---|---|
7
+ | runtime.context | 关键 | 是 |
8
+ | http.request | 关键 | 是 |
9
+ | http.binary | 关键 | 是 |
10
+ | http.cancel | 可选 | 是 |
11
+ | function.invoke | 关键 | 是 |
12
+ | runtime.user | 可选 | 是 |
13
+ | runtime.[`logout`](exports.md#page) | 可选 | 是 |
14
+ | runtime.[`requestEdit`](exports.md#page) | 可选 | 是 |
15
+ | attachment.upload | 关键 | 是 |
16
+ | attachment.cancel | 可选 | 是 |
17
+ | chatbot.stream | 关键 | 是 |
18
+ | ai.function.stream | 关键 | 是 |
19
+ | runtime.lifecycle | 可选 | 是 |
20
+ | runtime.mode | 可选 | 是 |
21
+ | attachment.progress | 可选 | 是 |
22
+ | link.open | 可选 | 是 |
23
+
24
+ 宿主集中部署;缺少 capabilities 字段仅代表能力协商尚未上线,不代表缺少外链或进度能力。基线来自 base-site-web `84afb07a7a4c05c09f7791cb67c4fe82bac23df8` 的实际注册和事件实现(见下表),不是按 channel 版本猜测。服务端 ctx.records 不属于页面能力协商;未知能力不在基线。[`queryCapability`](exports.md#page) 的 source 此时为 `host-baseline-84afb07`;显式能力表为 `advertised`。
25
+ `readCapabilities(context)`、`hasCapability(context,name,minVersion)` 是纯查询;[`getCapabilities`](exports.md#page) 为标准查询入口,[`getRuntimeCapabilities`](exports.md#page) 是别名;[`queryCapability`](exports.md#page) 返回 supported/version/source/availability,未明确健康状态时 availability 为 unknown;`getRuntimeCapabilities(runtime?)` 每次读宿主,不跨应用、环境或用户缓存。[`requireCapability`](exports.md#page) 查询失败或缺失时抛错。所有公开业务入口、兼容 helper 与组件通过同一调用边界:等待配对 → 自动能力预检 → 发送。显式缺失能力时本地抛结构化能力错误,不发送业务请求;RuntimeContext 是协商引导请求,不对自身递归预检。显式 [`requireCapability`](exports.md#page) 仍可用于界面判断,宿主是最终授权方。
26
+
27
+ <!-- example: capabilities.ts -->
28
+ ```ts
29
+ import { getRuntimeCapabilities, requireCapability, classifyRuntimeError } from '@lark-base-open/base-database-app-sdk/page';
30
+ export async function inspectHost() {
31
+ const capabilities = await getRuntimeCapabilities();
32
+ try { await requireCapability('function.invoke'); }
33
+ catch (error) { return { capabilities, diagnostic: classifyRuntimeError(error) }; }
34
+ return { capabilities };
35
+ }
36
+ ```
37
+
38
+ 诊断分类:host-unsupported(CAPABILITY_HOST_UNSUPPORTED)、permission-denied(CAPABILITY_PERMISSION_DENIED)、region-unavailable(CAPABILITY_REGION_UNAVAILABLE)、temporary(CAPABILITY_TEMPORARILY_UNAVAILABLE)、protocol(CAPABILITY_CONTEXT_INVALID)、unknown(CAPABILITY_UNAVAILABLE_UNKNOWN)。
39
+ 分类只用稳定错误码,不解析含业务内容的 message。LINK_CHECK_UNAVAILABLE 表示服务不可用,分类为 unknown,不能据此断言地域政策。宿主可用 companion 字段 capabilityUnavailable 明确 permission-denied/region-unavailable/temporarily-unavailable;[`requireCapability`](exports.md#page) 会根据该原因拒绝。unknown 不猜测也不自动重试;retryable 提示不保证非幂等写入重试安全。
40
+ 输出含 source/code/upstreamCode/category/retryable/message/redaction;不附原始异常、请求参数、URL、身份、token。应用、环境、制品版本与 requestId/logid 由持有可信上下文的诊断汇总端补齐,本模块不伪造关联信息。
41
+
42
+ 结构化清单位于包内 capabilities.json,测试保证它与共享 [`CAPABILITY_CATALOG`](exports.md#page) 一致。无缓存实现每次读取当前 channel 上下文,避免跨 mount/用户/应用泄漏旧状态。基线包含 link.open 与 attachment.progress;支持进度事件不代表每次上传都必定出现回调或保证服务当前可用。
43
+
44
+ ## 基线来源
45
+
46
+ 以下均引用 base-site-web 84afb07 的 `apps/site-web/src/view/preview/` 下文件;固定源码保存在 test/fixtures/host-84afb07。
47
+
48
+ | 能力 | 源码证据 |
49
+ |---|---|
50
+ | link.open / attachment.upload / attachment.cancel | runtime-host.ts:175、176、178 |
51
+ | http.request / http.cancel / function.invoke / runtime.context | runtime-host.ts:179、180、181、182 |
52
+ | chatbot.stream 的 open / close | chatbot-stream-broker.ts:201、204 |
53
+ | ai.function.stream 的 open / close | ai-function-broker.ts:476、479 |
54
+ | attachment.progress | runtime-host.ts:81–86 接入上传进度回调;attachment-proxy.ts:367 派发进度 |
55
+ | http.binary | http-proxy.ts:212–215、230–245 支持 arrayBuffer |
56
+ | runtime.user / runtime.[`logout`](exports.md#page) / runtime.[`requestEdit`](exports.md#page) | function-proxy.ts:145–152 的运行时 Function 路由 |
57
+ | runtime.lifecycle / runtime.mode | runtime-host.ts:196–203 订阅生命周期;162–165 派发 mode |
58
+
59
+ 上述注册合计 11 个 site.v1 API,测试从固定源码提取注册列表并与基线逐项核对。无字段时安装 guard;显式非空表缺 link.open 或显式空表时均不安装,且不回退到基线。
package/docs/compat.md ADDED
@@ -0,0 +1,36 @@
1
+ # 老模板兼容
2
+
3
+ 基准是 base-site `b131d55909e78ee88a1fe07e64c9ed9c56798e7a` 的 `templates/database-starter/page/src/lib/database-runtime.ts`。page 保留其全部 36 个值/类型导出;测试夹具保存原始文件与 SHA256。兼容长期维护,暂不设截止日期。
4
+
5
+ 数据库与附件实现位于 page/database.ts,AI helper 位于 page/ai-compat.ts。替换 import 后,Function 调用仍返回原 envelope;HTTP 失败仍抛错;Mutation batch 接受 1–200 个命名参数对象,与统一 Function 路由一致;旧位置数组必须按已发布的冻结声明人工迁移,不能直接替换 import 后发送。[`resolveDatabaseContext`](exports.md#page) 保持宿主优先、路径兜底和原会话缓存语义。媒体任务保留原 helper 的宽容归一化及错误名/码,流式 destroy 仍取消。
6
+
7
+ [`DatabaseFunctionInputMap`](exports.md#page) 可通过模块扩充,[`FunctionInputMap`](exports.md#page) 也可作为生成器的基础扩充接口;两者未提供函数时拒绝任意函数名和参数。不使用 Record<string,unknown> 兜底伪造类型安全。Database Function 输出保持 unknown。对自定义过 helper 的工程应先比对差异,不直接覆盖。
8
+
9
+ <!-- example: compat.ts -->
10
+ ```ts
11
+ import {
12
+ invokeDatabaseFunction,
13
+ invokeDatabaseMutationBatch,
14
+ type DatabaseFunctionResponse,
15
+ } from "@lark-base-open/base-database-app-sdk/page";
16
+ // CLI 生成到工程文件;空 map 不接受任意 string。
17
+ declare module "@lark-base-open/base-database-app-sdk/page" {
18
+ interface DatabaseFunctionInputMap {
19
+ list_orders: { status: string };
20
+ }
21
+ }
22
+ export async function loadOrders(): Promise<DatabaseFunctionResponse> {
23
+ return invokeDatabaseFunction("list_orders", { status: "open" });
24
+ }
25
+ export async function writeOrders() {
26
+ const result = await invokeDatabaseMutationBatch("update_order", [
27
+ { recordId: "rec1", status: "done" },
28
+ ]);
29
+ // 兼容 API 保留 envelope,不能把 code/data/msg 再次解包。
30
+ return result;
31
+ }
32
+ ```
33
+
34
+ 兼容目录:[`getCurrentUser`](exports.md#page)/[`logout`](exports.md#page)/[`requestEdit`](exports.md#page)、[`SiteRuntimeUserInfo`](exports.md#page)、[`DatabaseContext`](exports.md#page)、[`DatabaseAttachmentRef`](exports.md#page)、[`DatabaseEnv`](exports.md#page)、[`SiteAIMessageContentPart`](exports.md#page)、[`SiteAIMessage`](exports.md#page)、[`CallSiteAIFunctionParams`](exports.md#page)、[`InvokeSiteAIFunctionStreamParams`](exports.md#page)、[`SiteAIStreamDelta`](exports.md#page)、[`SiteAIFunctionStreamHandle`](exports.md#page)、[`SiteAIMediaTask`](exports.md#page)、[`SiteAIFunctionResult`](exports.md#page)、[`SiteAIImageGenerationInput`](exports.md#page)、[`SiteAIVideoTask`](exports.md#page)、[`DatabaseFunctionResponse`](exports.md#page)、[`invokeHttp`](exports.md#page)/[`siteRuntime`](exports.md#page)/[`fetch`](exports.md#page)、[`callSiteAIFunction`](exports.md#page)、[`invokeSiteAIFunctionStream`](exports.md#page)、[`normalizeSiteAIMediaTask`](exports.md#page)、[`hasRunnableMediaTask`](exports.md#page)、[`createSiteAIFunctionTask`](exports.md#page)、[`getSiteAIFunctionTask`](exports.md#page)、[`getDatabaseContext`](exports.md#page)、[`resolveDatabaseContext`](exports.md#page)、[`fetchAttachmentCover`](exports.md#page)、[`fetchAttachmentDownloadUrl`](exports.md#page)、[`buildDatabaseInvokeGetPath`](exports.md#page)、[`buildDatabaseInvokeFunctionPath`](exports.md#page)、[`DatabaseFunctionInvocation`](exports.md#page)、[`invokeDatabaseFunction`](exports.md#page)、[`invokeDatabaseMutationBatch`](exports.md#page)。
35
+
36
+ [服务端 ABI](server.md) · [升级](upgrade.md)
@@ -0,0 +1,34 @@
1
+ # 平台组件
2
+
3
+ 组件可从 `/components` 或 `/page` 导入;样式从 `/page/styles.css` 导入。React/react-dom 19.2.7、TanStack Query 5.104.1 为固定 peer;cmdk 1.1.1、Radix Popover 1.2.0 为固定依赖。server/shared 不导入 UI。
4
+
5
+ [`BaseAppProvider`](exports.md#components) 要求显式传 runtime、theme、locale,可选 mode、onDiagnostic,[`useBaseApp`](exports.md#components) 在 Provider 外明确报错。[`startBaseApp`](exports.md#page) 自动提供这些值。独立组件保留原 starter 的浏览器语言回退与 host zh/en/ja 初始化;显式 Provider locale 优先。
6
+
7
+ - [`FloatingButton`](exports.md#components) 保留 prod + allowEdit 权限显隐、隐藏直到重挂载、编辑失败提示。position 为 bottom-right/bottom-left;className 和 slots.trigger/tooltip 可覆盖展示,点击仍走宿主 request-edit/OpenLink。
8
+ - [`DatabaseUserMenu`](exports.md#components) 保留 audience(personal 不渲染)、align、side、showName、className;slots.trigger 收到规范化 user,slots.organization 收到 name。弹层由 Radix 处理;登出清理平台缓存。
9
+ - [`UserSelectorPanel`](exports.md#components) 保留 value/defaultValue、onChange(value,{option,action})、onSelect、onClose、onSelectRecordCreator、getUserDescription、onSearch、loading、mode、onKeyDown。不传 mode 为单选,multiple 为多选;已选项再次选择触发 Remove,选择后搜索 AutoClear。初始化后不应改变受控/非受控模式。
10
+ - [`UserSelectorEditor`](exports.md#components) 保留 value、onChange/onSelect/onClear/onRemove、onDropdownVisibleChange、onFocus/onBlur。增加 className、slots.trigger/chip、portalContainer。原生 dialog 自动作为 Portal 容器,其他焦点受限的弹窗可显式传 portalContainer。默认同 iframe document.body,不跨宿主 DOM。
11
+ - [`AttachmentImage`](exports.md#components) 保留 attachment/size/alt/className;slots.loading 和 slots.error 可自定义,error 收到安全 code 与 retry。组件管理懒加载、AbortSignal 和 Blob URL 释放。
12
+
13
+ Panel 的 slots.empty/loading/option/footer 均为 ComponentType;option 收到 user/selected,外层 cmdk Item 的键盘与角色不由插槽替换。cmdk 关闭本地过滤;400ms 服务端搜索 debounce,IME 期间不搜索或选择,过期请求通过 TanStack Query 的 key 与 AbortSignal 隔离。每次挂载独立 QueryClient,无跨应用或跨身份的成员缓存;卸载/登出撤销并清理。项目元信息缓存键包含应用、环境和当前 userId,无可靠身份时不缓存,TTL 30 秒。宿主仍负责每次请求鉴权。
14
+
15
+ CSS 只作用 `.base-app-platform`,位于 `@layer base-app`。可覆盖 `--base-app-color-bg/fg/muted/accent/danger/border`、`--base-app-radius`、`--base-app-font-family/font-size`、`--base-app-z-popover/z-floating` 和 `--base-app-shadow`。Portal 保留 Provider 的主题,支持 light/dark、文档方向与 reduced-motion。不依赖用户工程的 Tailwind 编译。
16
+
17
+ 来源为 base-site b131d55909e78ee88a1fe07e64c9ed9c56798e7a 的附录 C 组件。旧成员推荐接口、返回归一化、zh/en/ja 文案和权限语义保留;交互骨架与主题 CSS 重写。旧 ui-language 测试中的业务 App 和 PageState 属模板而非 SDK,迁移保留该用例中全部 SDK 断言。
18
+
19
+ 体积由 scripts/measure-size.mjs 对生产 ESM bundle 测 gzip,包含 channel/cmdk/Radix,排除 React/react-dom/TanStack Query;测试阈值在 test/size-budget.json,为本次实测值 +10%。CSS 单独计量。浏览器夹具与 mock host 证明组件行为,不证明 BOE 权限或真实业务请求。
20
+
21
+ <!-- example: components.tsx -->
22
+ ```tsx
23
+ import { useState } from 'react';
24
+ import { AttachmentImage, BaseAppProvider, DatabaseUserMenu, UserSelectorEditor, type UserSelectorEditorValue } from '@lark-base-open/base-database-app-sdk/components';
25
+ import { siteRuntime, type DatabaseAttachmentRef } from '@lark-base-open/base-database-app-sdk/page';
26
+ export function Details({ attachment }: { attachment: DatabaseAttachmentRef }) {
27
+ const [value, setValue] = useState<UserSelectorEditorValue>();
28
+ return <BaseAppProvider runtime={siteRuntime} locale="zh" theme="light">
29
+ <DatabaseUserMenu audience="enterprise" />
30
+ <UserSelectorEditor value={value} onChange={setValue} />
31
+ <AttachmentImage attachment={attachment} alt="订单附件" size={360} />
32
+ </BaseAppProvider>;
33
+ }
34
+ ```