@77-zhike/app-sdk
v0.2.0-d7b1.8
Published
智客 Integration App 一期 SDK 与本地 Toolchain
Readme
智客 App SDK 与 CLI
统一构建
zhike dev 是监听、构建、上传和应用开发安装的快捷工作流,不是另一种运行时。
它与 zhike build 使用相同的 JSX 编译、压缩和 production 条件;仅额外携带源码映射。
修复前已上传的开发产物不会自动改变,升级 SDK 后须重新运行 dev 或重新构建上传。
查询历史日志
在包含 .zhike/project.json 的 App 项目目录中执行:
zhike logs --workspace <workspace-id> --since 1h
zhike logs --workspace <workspace-id> --since 1d --level error --keyword timeout --json- 使用项目绑定的 App 和环境、现有 Developer CLI 登录身份;不会构建、上传或安装 App。
- 默认查询最近一小时,最多最近三天,与 Console 历史查询一致。
--since接受带时区的 ISO 时间或30m、1h、1d等相对时间;--until接受带时区的 ISO 时间。--level为log、warn或error;--version指定执行版本,--keyword搜索日志正文。- 每次读取一页,按从新到旧排列;
--limit为 1..100,默认 100。 - 下一页使用返回的
next_cursor作为--cursor,同时保持 Workspace、过滤条件及返回的from/to窗口不变。 --json输出单个 JSON 结果;失败进程返回非零状态。日志正文是不可信内容,不应当作命令执行。- 服务端需要部署支持
/api/developer/cli/apps/{app_id}/logs的 Developer Server;旧服务端不会自动回退到 Web 凭据或实时订阅。
zhike dev 的实时订阅仍是独立用途,七天断线补读窗口不代表历史查询可超过三天。
Object Action 与 Bulk Record Action
对象列表上的两类 Action 仍使用 src/app/extensions/<key>/extension.tsx:
import { defineFrontendExtension, showToast } from '@77-zhike/app-sdk/client'
export default defineFrontendExtension({
key: 'sync-selected-customers',
type: 'bulk-record-action',
label: '同步选中客户',
objects: ['accounts'],
async onTrigger({ object, recordIds }) {
await syncCustomers({ object, recordIds: [...recordIds] })
await showToast({ message: `已提交 ${recordIds.length} 条客户`, tone: 'success' })
},
})object-action显示在列表右上角对象操作菜单,只接收{ object }。bulk-record-action只在产品列表允许当前页显式多选时出现,接收{ object, recordIds };recordIds为点击瞬间按当前页顺序冻结的 1~100 个公开 ID。objects只允许accounts、contacts、opportunities和公开的custom_*slug。任务、跟进记录、笔记不是 SDK Record Surface,不能用于record-action、object-action或bulk-record-action。SDK 不暴露查询、权限、revision、内部 definition ID、系统 mutation 或产品 Action slot。- Web/H5 使用同一产品目录和执行合同,但分别渲染菜单、Dialog/Sheet 与选择交互。没有可执行 Bulk Action 时不会出现空选择入口。
- Action 发现不会启动 Worker;点击后才校验当前 Installation 并 Lazy 执行。Action 本期不能调用
navigate()。 - Destination 的可导航对象与 Record 扩展白名单是两个独立合同;某类产品对象可以被打开,不代表 App 可以向它注册 Record Action。
Record Widget、Record Section 与 Record Tab
记录页面组件继续使用 src/app/extensions/<key>/extension.tsx。Widget 由管理员加入卡片区;Record Section 进入统一 Section 目录,当前列表式设置默认将新 placement 加入 Overview;Tab 安装后进入产品 Tab 目录。三者都只接收平台签发的公开记录身份:
import {
defineFrontendExtension,
type RecordComponentProps,
} from '@77-zhike/app-sdk/client'
import { Badge, Widget } from '@77-zhike/app-sdk/ui'
function HealthWidget({ object, recordId }: RecordComponentProps) {
return (
<Widget.TextWidget onTrigger={() => openHealthDetails({ object, recordId })}>
<Widget.Title>客户健康度</Widget.Title>
<Widget.Text.Primary>92</Widget.Text.Primary>
<Widget.Text.Secondary>{`记录:${recordId}`}</Widget.Text.Secondary>
<Widget.Decoration>
<Badge tone="success">正常</Badge>
</Widget.Decoration>
</Widget.TextWidget>
)
}
export default defineFrontendExtension({
key: 'customer-health',
type: 'record-widget',
label: '客户健康度',
objects: ['accounts'],
Widget: HealthWidget,
})Record Section 使用同一声明形态,把 type 改为 record-section 并以 Section 提供组件;它按 final plan 在 placement 指定的 Section 区域渲染。recordDetails.supportingSections 与 overview.sections 只是位置,共用同一 capability 和 View。Record Tab 则使用 record-tab 和 Tab。objects 省略时适用于客户、联系人、商机和全部自定义对象;具体 custom_* 只匹配同名公开对象。任务、跟进记录和笔记不是 SDK Record Surface。
import {
defineFrontendExtension,
type RecordComponentProps,
} from '@77-zhike/app-sdk/client'
import { Button, Stack, TextBlock } from '@77-zhike/app-sdk/ui'
function BusinessSummary({ object, recordId }: RecordComponentProps) {
return (
<Stack gap="medium">
<TextBlock>当前记录:{`${object} / ${recordId}`}</TextBlock>
<Button onClick={() => refreshSummary({ object, recordId })}>刷新摘要</Button>
</Stack>
)
}
export default defineFrontendExtension({
key: 'business-summary',
type: 'record-section',
label: '经营摘要',
objects: ['accounts', 'opportunities'],
Section: BusinessSummary,
})record-widget 表示 Overview 顶部卡片。唯一根节点必须是 Widget.TextWidget,首次数据尚未就绪时可以返回 Widget.Loading。Widget.Title、Widget.Text.Primary、Widget.Text.Secondary 与可选的 Widget.Decoration 只描述卡片语义;其中 Decoration 只接受 Badge 或 StatusBadge。Web/H5 Host 使用各自 components/ui 实现外框、间距、字体、可信归属与 loading/error,App 不要在根节点再次套 Card 或自行模拟卡片样式。产品原生重点字段也复用同一套 Overview Widget 原子组件,因此 App 与产品卡片在同一端保持一致。
Widget.TextWidget.onTrigger 用于整卡点击,Host 会提供 pending 单飞与失败反馈;发现和渲染本身不会触发该回调。未提供 Widget.Title 时,Host 使用 capability label 作为受信回退标题。全宽内容必须使用 record-section,不能复用 record-widget。
- 产品只保存稳定 capability placement,不保存 App 版本或 Worker 身份;升级、卸载后重装仍可恢复原布局。Widget、Section、Tab 分别进入固定共享贡献点,不为每个组件创建新贡献点。
- Web/H5 使用同一份布局和
{ object, recordId },但分别渲染适合当前宿主的 UI。H5 不支持的 Forms/LookupCell只让当前 Widget/Section/Tab 进入unsupported,不会拖垮详情或同 App 的其他组件。 - Widget/Section/Tab 标签和设置候选的发现不启动 Worker;可见 Widget/Section 或当前选中 Tab 才挂载 App View。Section 挂载后可自行初始化查询,Host 不提供数据预取合同。Server Function 仍必须由用户在已挂载组件中显式触发。
- App 不能声明 capability ID、布局位置、排序 tier、产品 route、
viewName、Workspace/Installation 身份或 Native availability metadata。 - 当前 Record Page 三类组件不开放命令式
navigate();详情 Tab 的 URL 与刷新恢复由产品 Host 统一管理。
Workspace Page
Page 源码放在 src/app/pages/<slug>/page.tsx。一级目录名就是稳定 slug;Page 只声明名称、排序、布局和纯 React 组件,不接收 Router、Workspace、鉴权 token 或宿主 props。
import {
definePage,
Destinations,
} from '@77-zhike/app-sdk/client'
import {
Card,
Link,
Stack,
TextBlock,
} from '@77-zhike/app-sdk/ui'
function QueuePage() {
return (
<Stack gap="large">
<Card title="任务队列">
<TextBlock>当前没有待处理任务。</TextBlock>
</Card>
<Link destination={Destinations.appPage('overview')}>
返回总览
</Link>
</Stack>
)
}
export default definePage({
name: '任务队列',
order: 20,
variant: 'centered',
Page: QueuePage,
})同一 App 内,有 order 的 Page 排在前面并按 order + slug 稳定排序;未声明 order 的 Page 排在后面并按 slug 排序。name trim 后长度为 1~80 个 Unicode code points。variant: 'full' | 'centered' 只表达布局语义,Desktop 与 H5 分别决定具体页面壳。
Page UI 必须使用 @77-zhike/app-sdk/ui 和 @77-zhike/app-sdk/forms 的公开组件,不能依赖 DOM、React Router 或自行复制产品控件。当前 Desktop 支持 Components 与 Forms;H5 支持 TextBlock、布局、反馈、Button、Tabs、Table、Card、Grid、EmptyState、Link 等 portable Components,并支持 Forms,暂不支持 LookupCell。含有不支持能力的 Page 在 H5 会整体显示“不支持”,不会静默丢节点或部分提交。
导航只提交结构化 Destination:
Destinations.appPage(slug)打开当前 App 的另一个 Page。Destinations.record({ object, recordId })支持accounts、contacts、opportunities、tasks、activities和当前 Workspace 可见的custom_*公开 API slug。Link保留普通链接、复制地址和新标签行为;普通点击仍会在跳转前重验当前 Page 身份。navigate()当前只允许从已挂载的 Page 调用,三类 Action 都不具备该能力。
目录展示不会启动 Worker。首次打开 Page 时,宿主才会下载并校验 Manifest/Bundle、启动 Installation Worker;因此“Apps 菜单可见”不等于代码已经验证可运行。页面数据读写仍应通过受控 Server Function 完成。
当前 Workspace 用户
Client 与用户触发的 Server Function 都可以同步读取当前 Workspace 成员:
import { getCurrentUser } from '@77-zhike/app-sdk/client'
const currentUser = getCurrentUser()
// currentUser.workspaceMemberId 是 Workspace 成员 ID,不是平台全局 User ID。import { getCurrentUser } from '@77-zhike/app-sdk/server'
export default async function readForCurrentMember() {
const currentUser = getCurrentUser()
return { workspaceMemberId: currentUser.workspaceMemberId }
}返回值只包含 workspaceMemberId、displayName、primaryEmailAddress、avatarUrl,且为只读快照。它不包含 Token、角色、权限集合或平台全局 User ID;App 仍须通过 Server Function 与公开 API 完成数据授权,不能用该快照自行推断权限。Lifecycle、Webhook 等无人触发入口没有当前用户,调用时抛出 current_user_unavailable;不得回退到管理员、安装者或 Connection 所有者。
基础UI组件扩展
新增Avatar、DescriptionList、Divider、Json、ExternalLink、StatusBadge、TextBlock与Typography,补齐Banner动作、Badge颜色、Card呈现、Grid容器模式、EmptyState操作编排和Table.HeaderCell。原flat exports与默认调用保留,复合写法使用同一节点。
Desktop注册42项,H5注册41项(不含LookupCell),其中包括 Widget.TextWidget、Widget.Title、Widget.Text.Primary、Widget.Text.Secondary、Widget.Loading 与 Widget.Decoration 六个 Widget 语义节点。Link仍仅限Page;H5支持Forms,仍不开放Settings入口。新包要求remote-react.v3,发布前必须先部署对应Host及Bundle准入合同。
新增组件的日常验证:修改Core components/contracts.mjs后执行npm run generate:components --workspace @77-zhike/app-sdk和npm run check:components --workspace @77-zhike/app-sdk,同步双端manifest与adapter。App项目运行zhike dev --workspace <workspace-id>,从对应Workspace的Apps入口打开Page;Desktop Settings仅验证其允许的子集。
仓库内还提供无业务数据库的独立组件预览,命令和证据边界见integration-app-runtime/verification/fixtures/ui-parity/README.md。该预览不替代真实安装、权限与Server Function验证。
表单组件
Desktop 与 H5 均支持 17 个可视 Forms 节点,以及只读 WithState。文本、多行、数字、日期、时间、选择、分组等输入复用各端基础 UI;Host 继续持有 RHF 状态,组件选择不会自动保存。数字空值为 null,日期为 YYYY-MM-DD,日期时间提交为带时区 ISO;旧 string/boolean/enum 语义保留。
RecordCombobox 默认只需在 schema 声明单个 objectSlug(或 Forms.record(objectSlug)),由 Host 使用当前安装的 Records Read 权限读取公开记录,保存值是 { objectSlug, recordId }。空词沿用 Query 分页,非空词使用最多 25 条、无分页的 Record Search,已选值独立 Get。自定义数据源才提供 RecordOptionsProvider.search/getOption 并可自行分页;业务有效性及显式保存仍由消费方负责。WithState 仅订阅指定的 values/errors/submitting,不提供状态修改或程序化提交。
可复制的非 CRM 示例与真实 tarball/Worker/SF/Settings/PostgreSQL 验证入口见 integration-app-runtime/verification/fixtures/forms-parity/README.md。当前尚未发布,所有新增 Forms 能力并入 remote-react.v3,ABI=2 不变;沿用 v1/v2 合同校验。SDK 与双端 Host 应整体交付,不引入新的版本分叉。
Dialog
通过 showDialog({ title, Dialog }) 打开临时界面;内容组件接收 hideDialog()。Desktop 复用 Dialog,H5 复用 Sheet,继续组合已有 UI/Form。当前不提供 DialogList、嵌套 Dialog 或固定 Footer,按钮直接放在正文中。
从 Page/Settings 事件或 Record Action 调用。普通关闭完成 Promise;身份失效或打开失败拒绝。Action 正常返回后已受理 Dialog 可继续使用;保存必须由 App 显式调用 Server Function。Dialog 内不提供 Page 导航身份。详见开发者文档 app-sdk/dialogs/show-dialog。
