@77-zhike/app-sdk 0.2.0-d7b1.10
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/README.md +251 -0
- package/package.json +71 -0
- package/src/bin.js +15 -0
- package/src/cli/api-client.js +249 -0
- package/src/cli/browser-auth.js +212 -0
- package/src/cli/credential-store.js +101 -0
- package/src/cli/development-app.js +77 -0
- package/src/cli/development-logs.js +183 -0
- package/src/cli/development-watch.js +205 -0
- package/src/cli/index.js +256 -0
- package/src/cli/local-commands.js +80 -0
- package/src/cli/logs.js +56 -0
- package/src/cli/project-init.js +259 -0
- package/src/cli/upload-bundle.js +73 -0
- package/src/client/analysis.d.ts +4 -0
- package/src/client/analysis.js +12 -0
- package/src/client/destinations.d.ts +25 -0
- package/src/client/destinations.js +70 -0
- package/src/client/external-url.js +9 -0
- package/src/client/index.d.ts +249 -0
- package/src/client/index.js +72 -0
- package/src/client/interactions.d.ts +23 -0
- package/src/client/interactions.js +62 -0
- package/src/client/points.generated.js +67 -0
- package/src/config.js +68 -0
- package/src/development-workspace.js +154 -0
- package/src/errors.js +20 -0
- package/src/forms/array-contract.d.ts +32 -0
- package/src/forms/array-contract.js +138 -0
- package/src/forms/components.generated.js +4 -0
- package/src/forms/factory.js +41 -0
- package/src/forms/field-array.d.ts +16 -0
- package/src/forms/field-array.js +52 -0
- package/src/forms/index.d.ts +118 -0
- package/src/forms/index.js +5 -0
- package/src/forms/normalize-options.js +19 -0
- package/src/forms/options.d.ts +30 -0
- package/src/forms/options.js +55 -0
- package/src/forms/record-options.d.ts +28 -0
- package/src/forms/record-options.js +92 -0
- package/src/forms/schema.d.ts +82 -0
- package/src/forms/schema.js +128 -0
- package/src/forms/state.d.ts +30 -0
- package/src/forms/state.js +82 -0
- package/src/forms/validation.d.ts +9 -0
- package/src/forms/validation.js +81 -0
- package/src/forms/values.d.ts +5 -0
- package/src/forms/values.js +38 -0
- package/src/index.d.ts +13 -0
- package/src/index.js +2 -0
- package/src/jsx-dev-runtime.d.ts +2 -0
- package/src/jsx-dev-runtime.js +1 -0
- package/src/jsx-runtime.d.ts +2 -0
- package/src/jsx-runtime.js +1 -0
- package/src/project-binding.js +84 -0
- package/src/server/index.d.ts +318 -0
- package/src/server/index.js +242 -0
- package/src/server/steps/index.d.ts +71 -0
- package/src/server/steps/index.js +15 -0
- package/src/server/triggers/index.d.ts +68 -0
- package/src/server/triggers/index.js +27 -0
- package/src/server/workflow-blocks/index.d.ts +72 -0
- package/src/server/workflow-blocks/index.js +32 -0
- package/src/toolchain/allowed-packages.json +7 -0
- package/src/toolchain/canonical-json.js +17 -0
- package/src/toolchain/dependency-policy.js +181 -0
- package/src/toolchain/index.js +15 -0
- package/src/toolchain/real-build.js +459 -0
- package/src/toolchain/source-discovery.js +919 -0
- package/src/toolchain/transport-tar.js +168 -0
- package/src/toolchain/webhook-response.js +50 -0
- package/src/toolchain/workflow-blocks/discovery.js +133 -0
- package/src/toolchain/workflow-steps/manifest.js +99 -0
- package/src/toolchain/workflow-triggers/manifest.js +47 -0
- package/src/ui/components.generated.js +5 -0
- package/src/ui/events.d.ts +10 -0
- package/src/ui/factory.js +70 -0
- package/src/ui/icons.generated.js +2 -0
- package/src/ui/index.d.ts +343 -0
- package/src/ui/index.js +4 -0
- package/src/ui/jsx.generated.d.ts +359 -0
- package/src/ui/native-dom.d.ts +49 -0
- package/src/ui/native-dom.js +44 -0
- package/src/ui/style.d.ts +5 -0
- package/src/ui/style.generated.js +2 -0
- package/src/ui/style.js +15 -0
- package/src/ui/validation.js +86 -0
package/README.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# 智客 App SDK 与 CLI
|
|
2
|
+
|
|
3
|
+
## 获准依赖
|
|
4
|
+
|
|
5
|
+
工具链从 `src/toolchain/allowed-packages.json` 读取平台维护的包白名单。目前允许 `@77-zhike/qiqi-openapi@0.1.0` 的服务端包根入口 `.`;前端不可导入。项目安装依赖后直接使用静态 ESM import,真实解析的包名、精确版本、入口及间接模块在打包时核对,代码合并进 Artifact。App 的 Manifest、Settings 或 `package.json` 不能扩展名单。
|
|
6
|
+
|
|
7
|
+
Node builtin、CommonJS、动态 import、原生模块、未获准包和路径越界仍禁止。项目内 `file:` 包可用,但同名同版本不是来源证明。白名单只控制构建兼容性;不替代运行时沙箱、网络策略、租户隔离及 API 权限,不进行运行时 npm 安装。
|
|
8
|
+
|
|
9
|
+
## 统一构建
|
|
10
|
+
|
|
11
|
+
`zhike dev` 是监听、构建、上传和应用开发安装的快捷工作流,不是另一种运行时。
|
|
12
|
+
它与 `zhike build` 使用相同的 JSX 编译、压缩和 production 条件;仅额外携带源码映射。
|
|
13
|
+
修复前已上传的开发产物不会自动改变,升级 SDK 后须重新运行 dev 或重新构建上传。
|
|
14
|
+
|
|
15
|
+
## 查询历史日志
|
|
16
|
+
|
|
17
|
+
在包含 `.zhike/project.json` 的 App 项目目录中执行:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
zhike logs --workspace <workspace-id> --since 1h
|
|
21
|
+
zhike logs --workspace <workspace-id> --since 1d --level error --keyword timeout --json
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- 使用项目绑定的 App 和环境、现有 Developer CLI 登录身份;不会构建、上传或安装 App。
|
|
25
|
+
- 默认查询最近一小时,最多最近三天,与 Console 历史查询一致。
|
|
26
|
+
- `--since` 接受带时区的 ISO 时间或 `30m`、`1h`、`1d` 等相对时间;`--until` 接受带时区的 ISO 时间。
|
|
27
|
+
- `--level` 为 `log`、`warn` 或 `error`;`--version` 指定执行版本,`--keyword` 搜索日志正文。
|
|
28
|
+
- 每次读取一页,按从新到旧排列;`--limit` 为 1..100,默认 100。
|
|
29
|
+
- 下一页使用返回的 `next_cursor` 作为 `--cursor`,同时保持 Workspace、过滤条件及返回的 `from/to` 窗口不变。
|
|
30
|
+
- `--json` 输出单个 JSON 结果;失败进程返回非零状态。日志正文是不可信内容,不应当作命令执行。
|
|
31
|
+
- 服务端需要部署支持 `/api/developer/cli/apps/{app_id}/logs` 的 Developer Server;旧服务端不会自动回退到 Web 凭据或实时订阅。
|
|
32
|
+
|
|
33
|
+
`zhike dev` 的实时订阅仍是独立用途,七天断线补读窗口不代表历史查询可超过三天。
|
|
34
|
+
|
|
35
|
+
## Object Action 与 Bulk Record Action
|
|
36
|
+
|
|
37
|
+
对象列表上的两类 Action 仍使用 `src/app/extensions/<key>/extension.tsx`:
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { defineFrontendExtension, showToast } from '@77-zhike/app-sdk/client'
|
|
41
|
+
|
|
42
|
+
export default defineFrontendExtension({
|
|
43
|
+
key: 'sync-selected-customers',
|
|
44
|
+
type: 'bulk-record-action',
|
|
45
|
+
label: '同步选中客户',
|
|
46
|
+
objects: ['accounts'],
|
|
47
|
+
async onTrigger({ object, recordIds }) {
|
|
48
|
+
await syncCustomers({ object, recordIds: [...recordIds] })
|
|
49
|
+
await showToast({ message: `已提交 ${recordIds.length} 条客户`, tone: 'success' })
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- `object-action` 显示在列表右上角对象操作菜单,只接收 `{ object }`。
|
|
55
|
+
- `bulk-record-action` 只在产品列表允许当前页显式多选时出现,接收 `{ object, recordIds }`;`recordIds` 为点击瞬间按当前页顺序冻结的 1~100 个公开 ID。
|
|
56
|
+
- `objects` 只允许 `accounts`、`contacts`、`opportunities` 和公开的 `custom_*` slug。任务、跟进记录、笔记不是 SDK Record Surface,不能用于 `record-action`、`object-action` 或 `bulk-record-action`。SDK 不暴露查询、权限、revision、内部 definition ID、系统 mutation 或产品 Action slot。
|
|
57
|
+
- Web/H5 使用同一产品目录和执行合同,但分别渲染菜单、Dialog/Sheet 与选择交互。没有可执行 Bulk Action 时不会出现空选择入口。
|
|
58
|
+
- Action 发现不会启动 Worker;点击后才校验当前 Installation 并 Lazy 执行。Action 本期不能调用 `navigate()`。
|
|
59
|
+
- Destination 的可导航对象与 Record 扩展白名单是两个独立合同;某类产品对象可以被打开,不代表 App 可以向它注册 Record Action。
|
|
60
|
+
|
|
61
|
+
## Record Widget、Record Section 与 Record Tab
|
|
62
|
+
|
|
63
|
+
记录页面组件继续使用 `src/app/extensions/<key>/extension.tsx`。Widget 由管理员加入卡片区;Record Section 进入统一 Section 目录,当前列表式设置默认将新 placement 加入 Overview;Tab 安装后进入产品 Tab 目录。三者都只接收平台签发的公开记录身份:
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import {
|
|
67
|
+
defineFrontendExtension,
|
|
68
|
+
type RecordComponentProps,
|
|
69
|
+
} from '@77-zhike/app-sdk/client'
|
|
70
|
+
import { Badge, Widget } from '@77-zhike/app-sdk/ui'
|
|
71
|
+
|
|
72
|
+
function HealthWidget({ object, recordId }: RecordComponentProps) {
|
|
73
|
+
return (
|
|
74
|
+
<Widget.TextWidget onTrigger={() => openHealthDetails({ object, recordId })}>
|
|
75
|
+
<Widget.Title>客户健康度</Widget.Title>
|
|
76
|
+
<Widget.Text.Primary>92</Widget.Text.Primary>
|
|
77
|
+
<Widget.Text.Secondary>{`记录:${recordId}`}</Widget.Text.Secondary>
|
|
78
|
+
<Widget.Decoration>
|
|
79
|
+
<Badge tone="success">正常</Badge>
|
|
80
|
+
</Widget.Decoration>
|
|
81
|
+
</Widget.TextWidget>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export default defineFrontendExtension({
|
|
86
|
+
key: 'customer-health',
|
|
87
|
+
type: 'record-widget',
|
|
88
|
+
label: '客户健康度',
|
|
89
|
+
objects: ['accounts'],
|
|
90
|
+
Widget: HealthWidget,
|
|
91
|
+
})
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
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。
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
import {
|
|
98
|
+
defineFrontendExtension,
|
|
99
|
+
type RecordComponentProps,
|
|
100
|
+
} from '@77-zhike/app-sdk/client'
|
|
101
|
+
import { Button, Stack, TextBlock } from '@77-zhike/app-sdk/ui'
|
|
102
|
+
|
|
103
|
+
function BusinessSummary({ object, recordId }: RecordComponentProps) {
|
|
104
|
+
return (
|
|
105
|
+
<Stack gap="medium">
|
|
106
|
+
<TextBlock>当前记录:{`${object} / ${recordId}`}</TextBlock>
|
|
107
|
+
<Button onClick={() => refreshSummary({ object, recordId })}>刷新摘要</Button>
|
|
108
|
+
</Stack>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default defineFrontendExtension({
|
|
113
|
+
key: 'business-summary',
|
|
114
|
+
type: 'record-section',
|
|
115
|
+
label: '经营摘要',
|
|
116
|
+
objects: ['accounts', 'opportunities'],
|
|
117
|
+
Section: BusinessSummary,
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`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 与产品卡片在同一端保持一致。
|
|
122
|
+
|
|
123
|
+
`Widget.TextWidget.onTrigger` 用于整卡点击,Host 会提供 pending 单飞与失败反馈;发现和渲染本身不会触发该回调。未提供 `Widget.Title` 时,Host 使用 capability label 作为受信回退标题。全宽内容必须使用 `record-section`,不能复用 `record-widget`。
|
|
124
|
+
|
|
125
|
+
- 产品只保存稳定 capability placement,不保存 App 版本或 Worker 身份;升级、卸载后重装仍可恢复原布局。Widget、Section、Tab 分别进入固定共享贡献点,不为每个组件创建新贡献点。
|
|
126
|
+
- Web/H5 使用同一份布局和 `{ object, recordId }`,但分别渲染适合当前宿主的 UI。H5 不支持的 Forms/`LookupCell` 只让当前 Widget/Section/Tab 进入 `unsupported`,不会拖垮详情或同 App 的其他组件。
|
|
127
|
+
- Widget/Section/Tab 标签和设置候选的发现不启动 Worker;可见 Widget/Section 或当前选中 Tab 才挂载 App View。Section 挂载后可自行初始化查询,Host 不提供数据预取合同。Server Function 仍必须由用户在已挂载组件中显式触发。
|
|
128
|
+
- App 不能声明 capability ID、布局位置、排序 tier、产品 route、`viewName`、Workspace/Installation 身份或 Native availability metadata。
|
|
129
|
+
- 当前 Record Page 三类组件不开放命令式 `navigate()`;详情 Tab 的 URL 与刷新恢复由产品 Host 统一管理。
|
|
130
|
+
|
|
131
|
+
## Workspace Page
|
|
132
|
+
|
|
133
|
+
Page 源码放在 `src/app/pages/<slug>/page.tsx`。一级目录名就是稳定 slug;Page 只声明名称、排序、布局和纯 React 组件,不接收 Router、Workspace、鉴权 token 或宿主 props。
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import {
|
|
137
|
+
definePage,
|
|
138
|
+
Destinations,
|
|
139
|
+
} from '@77-zhike/app-sdk/client'
|
|
140
|
+
import {
|
|
141
|
+
Card,
|
|
142
|
+
ZhikeLink,
|
|
143
|
+
Stack,
|
|
144
|
+
TextBlock,
|
|
145
|
+
} from '@77-zhike/app-sdk/ui'
|
|
146
|
+
|
|
147
|
+
function QueuePage() {
|
|
148
|
+
return (
|
|
149
|
+
<Stack gap="large">
|
|
150
|
+
<Card title="任务队列">
|
|
151
|
+
<TextBlock>当前没有待处理任务。</TextBlock>
|
|
152
|
+
</Card>
|
|
153
|
+
<ZhikeLink destination={Destinations.appPage('overview')}>
|
|
154
|
+
返回总览
|
|
155
|
+
</ZhikeLink>
|
|
156
|
+
</Stack>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export default definePage({
|
|
161
|
+
name: '任务队列',
|
|
162
|
+
order: 20,
|
|
163
|
+
variant: 'centered',
|
|
164
|
+
Page: QueuePage,
|
|
165
|
+
})
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
同一 App 内,有 `order` 的 Page 排在前面并按 `order + slug` 稳定排序;未声明 `order` 的 Page 排在后面并按 slug 排序。`name` trim 后长度为 1~80 个 Unicode code points。`variant: 'full' | 'centered'` 只表达布局语义,Desktop 与 H5 分别决定具体页面壳。
|
|
169
|
+
|
|
170
|
+
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、ZhikeLink 等 portable Components,并支持 Forms,暂不支持 `LookupCell`。含有不支持能力的 Page 在 H5 会整体显示“不支持”,不会静默丢节点或部分提交。
|
|
171
|
+
|
|
172
|
+
导航只提交结构化 Destination:
|
|
173
|
+
|
|
174
|
+
- `Destinations.appPage(slug)` 打开当前 App 的另一个 Page。
|
|
175
|
+
- `Destinations.record({ object, recordId })` 支持 `accounts`、`contacts`、`opportunities`、`tasks`、`activities` 和当前 Workspace 可见的 `custom_*` 公开 API slug。
|
|
176
|
+
- `ZhikeLink` 保留普通链接、复制地址和新标签行为;普通点击仍会在跳转前重验当前 Page 身份。外部 HTTPS 使用 `Link`。
|
|
177
|
+
- `navigate()` 当前只允许从已挂载的 Page 调用,三类 Action 都不具备该能力。
|
|
178
|
+
|
|
179
|
+
目录展示不会启动 Worker。首次打开 Page 时,宿主才会下载并校验 Manifest/Bundle、启动 Installation Worker;因此“Apps 菜单可见”不等于代码已经验证可运行。页面数据读写仍应通过受控 Server Function 完成。
|
|
180
|
+
|
|
181
|
+
## 当前 Workspace 用户
|
|
182
|
+
|
|
183
|
+
Client 与用户触发的 Server Function 都可以同步读取当前 Workspace 成员:
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
import { getCurrentUser } from '@77-zhike/app-sdk/client'
|
|
187
|
+
|
|
188
|
+
const currentUser = getCurrentUser()
|
|
189
|
+
// currentUser.workspaceMemberId 是 Workspace 成员 ID,不是平台全局 User ID。
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import { getCurrentUser } from '@77-zhike/app-sdk/server'
|
|
194
|
+
|
|
195
|
+
export default async function readForCurrentMember() {
|
|
196
|
+
const currentUser = getCurrentUser()
|
|
197
|
+
return { workspaceMemberId: currentUser.workspaceMemberId }
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
返回值只包含 `workspaceMemberId`、`displayName`、`primaryEmailAddress`、`avatarUrl`,且为只读快照。它不包含 Token、角色、权限集合或平台全局 User ID;App 仍须通过 Server Function 与公开 API 完成数据授权,不能用该快照自行推断权限。Lifecycle、Webhook 等无人触发入口没有当前用户,调用时抛出 `current_user_unavailable`;不得回退到管理员、安装者或 Connection 所有者。
|
|
202
|
+
|
|
203
|
+
## 基础UI组件扩展
|
|
204
|
+
|
|
205
|
+
新增Avatar、DescriptionList、Divider、Json、外部Link、StatusBadge、TextBlock与Typography,补齐Banner动作、Badge颜色、Card呈现、Grid容器模式、EmptyState操作编排和Table.HeaderCell。复合写法使用同一节点。
|
|
206
|
+
|
|
207
|
+
Desktop与H5注册的组件合同中,`ZhikeLink` 仍仅限Page,`Link` 是受治理的外部HTTPS链接;H5支持Forms,仍不开放Settings入口。新包要求`remote-react.v3`,发布前必须先部署对应Host及Bundle准入合同。
|
|
208
|
+
|
|
209
|
+
新增组件的日常验证:修改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仅验证其允许的子集。
|
|
210
|
+
|
|
211
|
+
仓库内还提供无业务数据库的独立组件预览,命令和证据边界见`integration-app-runtime/verification/fixtures/ui-parity/README.md`。该预览不替代真实安装、权限与Server Function验证。
|
|
212
|
+
|
|
213
|
+
## 表单组件
|
|
214
|
+
|
|
215
|
+
Desktop 与 H5 均支持 17 个可视 Forms 节点,以及只读 `WithState`。文本、多行、数字、日期、时间、选择、分组等输入复用各端基础 UI;Host 继续持有 RHF 状态,组件选择不会自动保存。数字空值为 null,日期为 YYYY-MM-DD,日期时间提交为带时区 ISO;旧 string/boolean/enum 语义保留。
|
|
216
|
+
|
|
217
|
+
`RecordCombobox` 默认只需在 schema 声明单个 `objectSlug`(或 `Forms.record(objectSlug)`),由 Host 使用当前安装的 Records Read 权限读取公开记录,保存值是 `{ objectSlug, recordId }`。空词沿用 Query 分页,非空词使用最多 25 条、无分页的 Record Search,已选值独立 Get。自定义数据源才提供 `RecordOptionsProvider.search/getOption` 并可自行分页;业务有效性及显式保存仍由消费方负责。`WithState` 仅订阅指定的 values/errors/submitting,不提供状态修改或程序化提交。
|
|
218
|
+
|
|
219
|
+
可复制的非 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 应整体交付,不引入新的版本分叉。
|
|
220
|
+
|
|
221
|
+
## Dialog
|
|
222
|
+
|
|
223
|
+
通过 `showDialog({ title, Dialog })` 打开临时界面;内容组件接收 `hideDialog()`。Desktop 复用 Dialog,H5 复用 Sheet,继续组合已有 UI/Form。当前不提供 DialogList、嵌套 Dialog 或固定 Footer,按钮直接放在正文中。
|
|
224
|
+
|
|
225
|
+
从 Page/Settings 事件或 Record Action 调用。普通关闭完成 Promise;身份失效或打开失败拒绝。Action 正常返回后已受理 Dialog 可继续使用;保存必须由 App 显式调用 Server Function。Dialog 内不提供 Page 导航身份。详见开发者文档 `app-sdk/dialogs/show-dialog`。
|
|
226
|
+
|
|
227
|
+
## 当前成员的只读分析与 Chart
|
|
228
|
+
|
|
229
|
+
App Page 可从 `@77-zhike/app-sdk/client` 调用 `queryAnalysis({ operation, input })`。`operation` 仅允许 `list`、`get`、`execute`;输入沿用公开 MCP 分析目录、详情和执行合同。先分页读取获权目录,再从定义详情的 `capability` 与 `queryHints` 构造执行请求。禁止传入成员、租户、URL、任意 SQL 或内联定义。
|
|
230
|
+
|
|
231
|
+
此能力由 Host 注入当前成员和已安装 Page 的可信身份,不经 App 机器身份执行。Settings、Action 与 Dialog 不支持。读取前后均复验安装资格;页面关闭或切换工作空间时取消请求,迟到结果不交付。不可用历史、缺数据和权限错误须作为明确状态呈现。
|
|
232
|
+
|
|
233
|
+
```tsx
|
|
234
|
+
import { queryAnalysis } from '@77-zhike/app-sdk/client';
|
|
235
|
+
import { Chart } from '@77-zhike/app-sdk/ui';
|
|
236
|
+
|
|
237
|
+
const directory = await queryAnalysis({ operation: 'list', input: { limit: 20, offset: 0 } });
|
|
238
|
+
// 执行输入依据详情发布的维度和 Widget 能力构造。
|
|
239
|
+
const trend = <Chart label="经营趋势" variant="area" format="currency" height={280}
|
|
240
|
+
data={{ labels: ['一月', '二月'], series: [{ id: 'amount', label: '金额', values: [120000, null] }] }} />;
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Chart 仅接受纯数据 `labels` 与 `series`,不接受脚本、HTML 或任意样式。最多 200 个标签、4 条系列,32 KiB;系列 ID 唯一,各系列长度与标签相等。`null` 保留缺失断点;标签可重复,按位置区分。`variant` 支持 `line | bar | area`,`format` 支持 `number | currency | percent`,`height` 为 200、280 或 360。比例以 0..1 传入。`onSelect({ seriesId, index })` 支持提示和数据表选择;Web/H5 共用组件,图例与可展开数据表由 Host 提供。
|
|
244
|
+
|
|
245
|
+
### Dashboard 与高级分析呈现
|
|
246
|
+
|
|
247
|
+
`DashboardSurface` 提供固定 `graphite` 业务色板,深浅主题跟随主应用;`DashboardLayout` 提供 `mainAside`、`equal`、`three` 响应式布局,配合 `DashboardPanel` 和 `MetricReadout`。这些组件不接受任意 CSS、DOM 或主题变量。
|
|
248
|
+
|
|
249
|
+
`AnalysisView` 接收同一资产的 `queryAnalysis(get)` 定义和 `queryAnalysis(execute)` 结果。Host 复用已有分析渲染器,支持同期群、漏斗、基础/增强/堆叠/表格阶梯与历史构成瀑布;状态分析可切换度量,并查看全部度量原始结果。高级呈现须符合定义开放的能力,历史覆盖不足须由调用方明确显示。
|
|
250
|
+
|
|
251
|
+
定义与结果分别限制为 256 KiB / 20,000 项 / 24 层;状态分析最多 400 行、24 个唯一度量,结果单元格最多 2,400 个。拒绝跨资产结果、脚本、非有限值和无界数据。`onSelect` 仅返回受控数据选择,不开放内部记录路由或网络请求。使用这些组件需要同步升级本仓库的 SDK 和双端 Host。
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@77-zhike/app-sdk",
|
|
3
|
+
"version": "0.2.0-d7b1.10",
|
|
4
|
+
"description": "智客 Integration App 一期 SDK 与本地 Toolchain",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/tonyjiang88215/TruthLoop.git",
|
|
8
|
+
"directory": "developer-tools/packages/app-sdk"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=20"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"zhike": "src/bin.js"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./src/index.d.ts",
|
|
20
|
+
"default": "./src/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./client": {
|
|
23
|
+
"types": "./src/client/index.d.ts",
|
|
24
|
+
"default": "./src/client/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./server": {
|
|
27
|
+
"types": "./src/server/index.d.ts",
|
|
28
|
+
"default": "./src/server/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./cli": "./src/cli/index.js",
|
|
31
|
+
"./toolchain": "./src/toolchain/index.js",
|
|
32
|
+
"./ui": {
|
|
33
|
+
"types": "./src/ui/index.d.ts",
|
|
34
|
+
"default": "./src/ui/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./forms": {
|
|
37
|
+
"types": "./src/forms/index.d.ts",
|
|
38
|
+
"default": "./src/forms/index.js"
|
|
39
|
+
},
|
|
40
|
+
"./jsx-runtime": {
|
|
41
|
+
"types": "./src/jsx-runtime.d.ts",
|
|
42
|
+
"default": "./src/jsx-runtime.js"
|
|
43
|
+
},
|
|
44
|
+
"./jsx-dev-runtime": {
|
|
45
|
+
"types": "./src/jsx-dev-runtime.d.ts",
|
|
46
|
+
"default": "./src/jsx-dev-runtime.js"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"src"
|
|
51
|
+
],
|
|
52
|
+
"scripts": {
|
|
53
|
+
"generate:components": "node scripts/generate-components.mjs",
|
|
54
|
+
"check:components": "node scripts/generate-components.mjs --check",
|
|
55
|
+
"test": "node --test test/*.test.js"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@types/react": "19.1.0",
|
|
59
|
+
"esbuild": "0.25.12",
|
|
60
|
+
"typescript": "5.9.3"
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"react": "19.2.5"
|
|
64
|
+
},
|
|
65
|
+
"peerDependenciesMeta": {
|
|
66
|
+
"react": {
|
|
67
|
+
"optional": true
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"gitHead": "3d2268ba9ff4c7d0d13f5bfb9e3a285f1e0de716"
|
|
71
|
+
}
|
package/src/bin.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { runCli } from "./cli/index.js";
|
|
4
|
+
|
|
5
|
+
runCli(process.argv.slice(2)).catch((error) => {
|
|
6
|
+
if (process.argv.slice(2).includes("--json")) {
|
|
7
|
+
process.stdout.write(`${JSON.stringify({ schemaVersion: 1, status: "failed", target: null,
|
|
8
|
+
data: null, effects: { business: "none", authentication: "none" },
|
|
9
|
+
error: { code: error?.code || "zhike_error" }, nextAction: null })}\n`);
|
|
10
|
+
process.exitCode = error?.exitCode || 1;
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
process.stderr.write(`[${error?.code || "zhike_error"}] ${error?.message || "操作失败"}\n`);
|
|
14
|
+
process.exitCode = error?.exitCode || 1;
|
|
15
|
+
});
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { ZhikeCliError } from "../errors.js";
|
|
4
|
+
|
|
5
|
+
export class DeveloperApiClient {
|
|
6
|
+
constructor({ apiBaseUrl, fetchImpl = globalThis.fetch, accessToken = null }) {
|
|
7
|
+
this.apiBaseUrl = apiBaseUrl;
|
|
8
|
+
this.fetchImpl = fetchImpl;
|
|
9
|
+
this.accessToken = accessToken;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
withAccessToken(accessToken) {
|
|
13
|
+
return new DeveloperApiClient({
|
|
14
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
15
|
+
fetchImpl: this.fetchImpl,
|
|
16
|
+
accessToken,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async exchangeCliAuthorization(payload) {
|
|
21
|
+
return this.request("/api/developer/cli-auth/exchange", {
|
|
22
|
+
method: "POST",
|
|
23
|
+
body: payload,
|
|
24
|
+
authenticated: false,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async refreshCliSession(refreshCredential) {
|
|
29
|
+
return this.request("/api/developer/cli-auth/refresh", {
|
|
30
|
+
method: "POST",
|
|
31
|
+
body: { refresh_credential: refreshCredential },
|
|
32
|
+
authenticated: false,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async logoutCliSession(refreshCredential) {
|
|
37
|
+
return this.request("/api/developer/cli-auth/logout", {
|
|
38
|
+
method: "POST",
|
|
39
|
+
body: { refresh_credential: refreshCredential },
|
|
40
|
+
authenticated: false,
|
|
41
|
+
expectEmpty: true,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async listOrganizations() {
|
|
46
|
+
return this.request("/api/developer/organizations");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async queryAppLogs(appId, query) {
|
|
50
|
+
return this.request(`/api/developer/cli/apps/${encodeURIComponent(appId)}/logs?${new URLSearchParams(query)}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async getAppBySlug(organizationId, slug) {
|
|
54
|
+
const path = `/api/developer/organizations/${encodeURIComponent(
|
|
55
|
+
organizationId,
|
|
56
|
+
)}/apps/by-slug/${encodeURIComponent(slug)}`;
|
|
57
|
+
return this.request(path, { allowNotFound: true });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async createApp(organizationId, { name, slug, idempotencyKey }) {
|
|
61
|
+
return this.request(
|
|
62
|
+
`/api/developer/organizations/${encodeURIComponent(organizationId)}/apps`,
|
|
63
|
+
{
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: {
|
|
66
|
+
"Idempotency-Key": idempotencyKey,
|
|
67
|
+
"X-Request-ID": randomUUID(),
|
|
68
|
+
},
|
|
69
|
+
body: { name, slug },
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async uploadBundle(appId, artifact, idempotencyKey) {
|
|
75
|
+
return this.request(
|
|
76
|
+
`/api/developer/apps/${encodeURIComponent(appId)}/bundle-uploads`,
|
|
77
|
+
{
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: {
|
|
80
|
+
"Idempotency-Key": idempotencyKey,
|
|
81
|
+
"X-Zhike-Package-Format": artifact.packageFormat,
|
|
82
|
+
"X-Zhike-Tooling-Version": artifact.toolingVersion,
|
|
83
|
+
"X-Zhike-Artifact-Sha256": artifact.sha256,
|
|
84
|
+
"Content-Type":
|
|
85
|
+
artifact.contentType || "application/vnd.qiqizhike.mock-build+json",
|
|
86
|
+
},
|
|
87
|
+
rawBody: artifact.bytes,
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async getDevelopmentManifest(appId) {
|
|
93
|
+
return this.request(`/api/developer/apps/${encodeURIComponent(appId)}/development-manifest`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async updateDevelopmentManifest(appId, appBundleId, expectedScopeRevision) {
|
|
97
|
+
return this.request(
|
|
98
|
+
`/api/developer/apps/${encodeURIComponent(appId)}/development-manifest`,
|
|
99
|
+
{
|
|
100
|
+
method: "PUT",
|
|
101
|
+
headers: { "X-Request-ID": randomUUID() },
|
|
102
|
+
body: {
|
|
103
|
+
app_bundle_id: appBundleId,
|
|
104
|
+
...(expectedScopeRevision === undefined ? {} : {
|
|
105
|
+
expected_scope_configuration_revision: expectedScopeRevision,
|
|
106
|
+
}),
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async listManageableWorkspaces() {
|
|
113
|
+
return this.request("/api/developer/workspaces");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async ensureWorkspaceVisibility(appId, workspaceId) {
|
|
117
|
+
return this.request(
|
|
118
|
+
`/api/developer/apps/${encodeURIComponent(appId)}`
|
|
119
|
+
+ `/workspace-visibilities/${encodeURIComponent(workspaceId)}`,
|
|
120
|
+
{ method: "PUT" },
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async applyDevelopmentManifest(appId, workspaceId, snapshot) {
|
|
125
|
+
return this.request(
|
|
126
|
+
`/api/developer/apps/${encodeURIComponent(appId)}`
|
|
127
|
+
+ `/development-workspaces/${encodeURIComponent(workspaceId)}/apply`,
|
|
128
|
+
{ method: "POST", body: snapshot },
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async request(
|
|
133
|
+
path,
|
|
134
|
+
{
|
|
135
|
+
method = "GET",
|
|
136
|
+
headers = {},
|
|
137
|
+
body,
|
|
138
|
+
rawBody,
|
|
139
|
+
authenticated = true,
|
|
140
|
+
allowNotFound = false,
|
|
141
|
+
expectEmpty = false,
|
|
142
|
+
retryAuthentication = true,
|
|
143
|
+
} = {},
|
|
144
|
+
) {
|
|
145
|
+
const response = await this.openResponse(path, {
|
|
146
|
+
method, headers, body, rawBody, authenticated, retryAuthentication,
|
|
147
|
+
});
|
|
148
|
+
if (allowNotFound && response.status === 404) return null;
|
|
149
|
+
if (!response.ok) throw await responseError(response);
|
|
150
|
+
if (expectEmpty || response.status === 204) return null;
|
|
151
|
+
return response.json();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async openLogStream(appId, workspaceId, { cursor, signal } = {}) {
|
|
155
|
+
const response = await this.openResponse(
|
|
156
|
+
`/api/developer/apps/${encodeURIComponent(appId)}`
|
|
157
|
+
+ `/development-workspaces/${encodeURIComponent(workspaceId)}/logs/stream`,
|
|
158
|
+
{ headers: { Accept: "text/event-stream", ...(cursor ? { "Last-Event-ID": cursor } : {}) },
|
|
159
|
+
signal, timeoutMs: null },
|
|
160
|
+
);
|
|
161
|
+
if (!response.ok) throw await responseError(response);
|
|
162
|
+
return response;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// JSON 与 SSE 共用认证状态;连接取消和超时仍由每次请求独立管理。
|
|
166
|
+
async openResponse(path, {
|
|
167
|
+
method = "GET", headers = {}, body, rawBody, authenticated = true,
|
|
168
|
+
retryAuthentication = true, signal = this.signal, timeoutMs = 30_000,
|
|
169
|
+
} = {}) {
|
|
170
|
+
if (authenticated && !this.accessToken) {
|
|
171
|
+
throw new ZhikeCliError(
|
|
172
|
+
"zhike_cli_auth_required",
|
|
173
|
+
"需要先完成 Developer CLI 授权",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
let response;
|
|
177
|
+
const usedToken = this.accessToken;
|
|
178
|
+
const requestSignal = timeoutMs === null ? signal
|
|
179
|
+
: AbortSignal.any([...(signal ? [signal] : []), AbortSignal.timeout(timeoutMs)]);
|
|
180
|
+
try {
|
|
181
|
+
response = await this.fetchImpl(new URL(path, this.apiBaseUrl), {
|
|
182
|
+
method,
|
|
183
|
+
headers: {
|
|
184
|
+
Accept: "application/json",
|
|
185
|
+
...(body === undefined || rawBody !== undefined
|
|
186
|
+
? {}
|
|
187
|
+
: { "Content-Type": "application/json" }),
|
|
188
|
+
...(authenticated
|
|
189
|
+
? { Authorization: `Bearer ${usedToken}` }
|
|
190
|
+
: {}),
|
|
191
|
+
...headers,
|
|
192
|
+
},
|
|
193
|
+
body: rawBody ?? (body === undefined ? undefined : JSON.stringify(body)),
|
|
194
|
+
signal: requestSignal,
|
|
195
|
+
});
|
|
196
|
+
} catch (error) {
|
|
197
|
+
throw new ZhikeCliError(
|
|
198
|
+
"zhike_developer_server_unavailable",
|
|
199
|
+
"无法连接 Developer Server",
|
|
200
|
+
{ cause: error, retryable: true },
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (response.status === 401 && authenticated && retryAuthentication && this.refreshAccessToken) {
|
|
204
|
+
await response.body?.cancel();
|
|
205
|
+
// 迟到的旧 Token 401 不再触发第二次刷新。
|
|
206
|
+
if (this.accessToken === usedToken) {
|
|
207
|
+
this.refreshing ??= Promise.resolve().then(() => this.refreshAccessToken())
|
|
208
|
+
.then((token) => { this.accessToken = token; })
|
|
209
|
+
.finally(() => { this.refreshing = null; });
|
|
210
|
+
await waitForRefresh(this.refreshing, requestSignal);
|
|
211
|
+
}
|
|
212
|
+
requestSignal?.throwIfAborted();
|
|
213
|
+
return this.openResponse(path, {
|
|
214
|
+
method, headers, body, rawBody, authenticated, signal: requestSignal,
|
|
215
|
+
timeoutMs: null, retryAuthentication: false,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return response;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function waitForRefresh(refreshing, signal) {
|
|
223
|
+
if (!signal) return refreshing;
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
const aborted = () => reject(signal.reason);
|
|
226
|
+
if (signal.aborted) return aborted();
|
|
227
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
228
|
+
refreshing.then(resolve, reject).finally(() => signal.removeEventListener("abort", aborted));
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function responseError(response) {
|
|
233
|
+
const payload = await _safeJson(response);
|
|
234
|
+
const error = new ZhikeCliError(
|
|
235
|
+
payload?.code || payload?.detail?.code || "zhike_developer_api_failed",
|
|
236
|
+
payload?.detail?.message || (typeof payload?.detail === "string" ? payload.detail : null) || "Developer API 请求失败",
|
|
237
|
+
{ retryable: response.status >= 500 || response.status === 429 },
|
|
238
|
+
);
|
|
239
|
+
error.statusCode = response.status;
|
|
240
|
+
return error;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function _safeJson(response) {
|
|
244
|
+
try {
|
|
245
|
+
return await response.json();
|
|
246
|
+
} catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|