create-zleap-app 0.10.6 → 0.12.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 (36) hide show
  1. package/index.js +16 -14
  2. package/lib/scaffold.js +18 -5
  3. package/package.json +4 -4
  4. package/skills/zleap-cloud-app/SKILL.md +47 -11
  5. package/skills/zleap-cloud-app/references/cloud-functions.md +11 -6
  6. package/skills/zleap-cloud-app/references/limits.md +22 -19
  7. package/skills/zleap-cloud-app/references/sdk-database-files.md +4 -4
  8. package/skills/zleap-cloud-app/references/status-codes.md +24 -20
  9. package/skills/zleap-cloud-app/references/ui.md +304 -0
  10. package/skills/zleap-cloud-app-release/references/app-center-install.md +19 -11
  11. package/skills/zleap-cloud-app-release/references/app-center-publish.md +6 -6
  12. package/skills/zleap-cloud-app-release/references/console-release.md +1 -1
  13. package/skills/zleap-cloud-app-release/references/dev-to-production.md +67 -1
  14. package/skills/zleap-cloud-app-release/references/status-codes.md +24 -20
  15. package/templates/_shared/.cursor/mcp.json +14 -0
  16. package/templates/_shared/.mcp.json +14 -0
  17. package/templates/{react-ts → _shared}/scripts/zleap.mjs +138 -83
  18. package/templates/react-ts/AGENTS.md +9 -3
  19. package/templates/react-ts/package.json +4 -0
  20. package/templates/react-ts/src/App.tsx +227 -96
  21. package/templates/react-ts/src/cloud/client.ts +21 -6
  22. package/templates/react-ts/src/cloud/preview.ts +116 -0
  23. package/templates/react-ts/src/cloud/useCloudConnection.ts +2 -2
  24. package/templates/react-ts/src/index.css +5 -241
  25. package/templates/react-ts/src/main.tsx +3 -0
  26. package/templates/vanilla/AGENTS.md +37 -0
  27. package/templates/vanilla/CLAUDE.md +1 -0
  28. package/templates/vanilla/README.md +37 -0
  29. package/templates/vanilla/_gitignore +7 -0
  30. package/templates/vanilla/app/LICENSE +21 -0
  31. package/templates/vanilla/app/index.html +67 -0
  32. package/templates/vanilla/app/main.js +222 -0
  33. package/templates/vanilla/app/manifest.json +33 -0
  34. package/templates/vanilla/app/preview.js +114 -0
  35. package/templates/vanilla/app/style.css +5 -0
  36. package/templates/vanilla/package.json +32 -0
@@ -0,0 +1,304 @@
1
+ # 界面:官方样式 zleap.css 与常用版式
2
+
3
+ `zleap.css` 随 `@zleap-ai/app-sdk` 分发(包内文件,符合沙箱 CSP)。颜色、圆角、字号与 zleapOS 桌面同源,
4
+ 桌面切换亮 / 暗时应用窗口自动跟随(宿主把主题设到窗口上,页面里 `prefers-color-scheme` 随之变化),不需要写代码。
5
+
6
+ - vanilla:`<link rel="stylesheet" href="./sdk/zleap.css" />`(`npm install` / `npm run deploy` 自动复制到 `app/sdk/`)
7
+ - React:`import '@zleap-ai/app-sdk/zleap.css'`(放在应用自己的 css 之前)
8
+ - 原生模板:`./sdk/zleap.css`
9
+
10
+ 组件类都以 `zl-` 开头;自己的样式只用 `--zl-*` 变量取色,覆盖时改变量,不改 zleap.css。
11
+
12
+ ## 变量
13
+
14
+ | 变量 | 用途 |
15
+ | --- | --- |
16
+ | `--zl-bg` / `--zl-surface` | 页面底色 / 卡片、列表、输入框底色 |
17
+ | `--zl-ink` / `--zl-ink-muted` / `--zl-ink-faint` | 正文 / 次要文字 / 最弱文字 |
18
+ | `--zl-line` / `--zl-line-strong` | 分隔线 / 控件边框 |
19
+ | `--zl-accent` / `--zl-accent-text` / `--zl-accent-subtle` | 主题色 / 主题色文字 / 主题色浅底 |
20
+ | `--zl-success` `--zl-warning` `--zl-danger`(及 `-subtle`) | 成功 / 提醒 / 危险 |
21
+ | `--zl-radius-sm/md/lg/xl`、`--zl-font-xs/sm/base/lg/xl`、`--zl-shadow` | 圆角、字号、阴影 |
22
+
23
+ ## 类
24
+
25
+ | 分类 | 类 |
26
+ | --- | --- |
27
+ | 版式 | `zl-page`(`--wide` / `--full`)、`zl-split`(左列表右详情)、`zl-header`、`zl-title`、`zl-subtitle`、`zl-section`、`zl-section-title`、`zl-stack`(`--sm`/`--lg`)、`zl-row`(`--between`/`--end`)、`zl-spacer`、`zl-grid`、`zl-divider` |
28
+ | 卡片 | `zl-card`(`--flat` / `--interactive`)、`zl-card-title` |
29
+ | 文字 | `zl-muted`、`zl-faint`、`zl-text-sm`、`zl-text-xs`、`zl-strong`、`zl-num`(等宽数字)、`zl-text-success`、`zl-text-danger`、`zl-ellipsis`、`zl-prose`(长文本 / AI 结果,保留换行) |
30
+ | 按钮 | `zl-btn` + `--primary` / `--danger` / `--ghost`,尺寸 `--sm` / `--lg` / `--block`;处理中加 `aria-busy="true"`(自动转圈) |
31
+ | 表单 | `zl-field`、`zl-label`、`zl-hint`、`zl-input`(行内编辑加 `--inline`;原生 `type="date"` 可用)、`zl-textarea`、`zl-select`、`zl-check`、`zl-composer`(一行输入 + 按钮)、`zl-segmented`(按钮带 `aria-pressed`);出错加 `aria-invalid="true"` |
32
+ | 列表 | `zl-list`、`zl-list-item`(完成态 `is-done`;可点选的行用 `<button>`,选中加 `aria-current="true"`)、`zl-item-main`、`zl-item-title`、`zl-item-meta`、`zl-item-actions`(`--hover`:悬停才显示) |
33
+ | 标签 / 状态 | `zl-tag`(`--accent`/`--success`/`--warning`/`--danger`)、`zl-status`(`data-state`:connecting / ready / error / preview) |
34
+ | 提示 | `zl-alert`(`--info`/`--success`/`--warning`/`--danger`)、`zl-toast` |
35
+ | 空 / 加载 | `zl-empty`、`zl-empty-icon`、`zl-empty-title`、`zl-empty-text`、`zl-skeleton`、`zl-spinner` |
36
+ | 数据 | `zl-stats`、`zl-stat`、`zl-stat-label`、`zl-stat-value`、`zl-progress`(内部 `<span style="width:40%">`) |
37
+ | 动效 | `zl-enter`(新出现的块淡入);系统开启「减少动态效果」时自动关闭 |
38
+
39
+ ## 版式
40
+
41
+ 下面的示例只示意**结构和类名**。中文都是占位,换成你应用自己的文案;凡是数据(列表、数量、时间、状态)都由代码渲染,
42
+ 不要把示例里的结构连同内容原样写死在 HTML 里。每块数据都要有加载中 / 出错 / 空 / 有数据四种状态。
43
+
44
+ vanilla 示例用下面这个小工具建元素:文字一律走 `textContent`,不拼 HTML 字符串(避免把用户输入当成 HTML)。
45
+ React 直接用 JSX,`className` 写同样的类。
46
+
47
+ ```js
48
+ // 建元素:h('li', { class: 'zl-list-item', onclick: fn }, 子元素或文字…)
49
+ export function h(tag, props = {}, ...children) {
50
+ const el = document.createElement(tag);
51
+ for (const [key, value] of Object.entries(props)) {
52
+ if (key.startsWith('on')) el.addEventListener(key.slice(2), value);
53
+ else if (key === 'class') el.className = value;
54
+ else if (value === true) el.setAttribute(key, '');
55
+ else if (value !== false && value != null) el.setAttribute(key, value);
56
+ }
57
+ el.append(...children.filter((child) => child != null && child !== false));
58
+ return el;
59
+ }
60
+ ```
61
+
62
+ ### 页面骨架(所有应用)
63
+
64
+ HTML 里只放固定的骨架,状态和数据由脚本填:
65
+
66
+ ```html
67
+ <main class="zl-page">
68
+ <header class="zl-header">
69
+ <div>
70
+ <h1 class="zl-title">应用名</h1>
71
+ <p class="zl-subtitle">一句话说明这个应用做什么</p>
72
+ </div>
73
+ <span id="status" class="zl-status" data-state="connecting">
74
+ 正在连接
75
+ </span>
76
+ </header>
77
+ <div id="error" class="zl-alert zl-alert--danger" role="alert" hidden>
78
+ <div><strong id="error-title"></strong><span id="error-text"></span></div>
79
+ <button id="retry" class="zl-btn zl-btn--sm" type="button">重试</button>
80
+ </div>
81
+ <section class="zl-section">…</section>
82
+ </main>
83
+ ```
84
+
85
+ ### 列表(筛选、空状态、行内操作)
86
+
87
+ 骨架:
88
+
89
+ ```html
90
+ <section class="zl-section">
91
+ <div class="zl-section-title">
92
+ <div id="filters" class="zl-segmented" role="group" aria-label="筛选">
93
+ <button type="button" data-filter="all">全部</button>
94
+ <button type="button" data-filter="open">未完成</button>
95
+ </div>
96
+ <span id="count" class="zl-num zl-text-sm"></span>
97
+ </div>
98
+ <ul id="loading" class="zl-list" aria-busy="true">
99
+ <li class="zl-list-item">
100
+ <span class="zl-skeleton" style="width: 60%"></span>
101
+ </li>
102
+ <li class="zl-list-item">
103
+ <span class="zl-skeleton" style="width: 40%"></span>
104
+ </li>
105
+ </ul>
106
+ <ul id="list" class="zl-list" hidden></ul>
107
+ <div id="empty" class="zl-empty" hidden>
108
+ <div class="zl-empty-icon" aria-hidden="true">+</div>
109
+ <p id="empty-title" class="zl-empty-title"></p>
110
+ <p id="empty-text" class="zl-empty-text"></p>
111
+ </div>
112
+ </section>
113
+ ```
114
+
115
+ 渲染(`items` 来自集合查询或 OS 接口;`remove` 是你的删除函数):
116
+
117
+ ```js
118
+ const $ = (id) => document.getElementById(id);
119
+
120
+ function row(item) {
121
+ const actions = h('div', { class: 'zl-item-actions zl-item-actions--hover' });
122
+ // 沙箱里不能用 confirm():删除用行内二次确认
123
+ const ask = () => actions.replaceChildren(
124
+ h('button', {
125
+ class: 'zl-btn zl-btn--danger zl-btn--sm', type: 'button',
126
+ onclick: () => remove(item),
127
+ }, '确认删除'),
128
+ h('button', {
129
+ class: 'zl-btn zl-btn--ghost zl-btn--sm', type: 'button',
130
+ onclick: () => render(),
131
+ }, '取消'),
132
+ );
133
+ actions.append(h('button', {
134
+ class: 'zl-btn zl-btn--ghost zl-btn--sm', type: 'button', onclick: ask,
135
+ }, '删除'));
136
+ return h('li', { class: 'zl-list-item' + (item.done ? ' is-done' : '') },
137
+ h('div', { class: 'zl-item-main' },
138
+ h('span', { class: 'zl-item-title' }, item.title),
139
+ h('span', { class: 'zl-item-meta' }, formatTime(item.updated_at)),
140
+ ),
141
+ item.tag && h('span', { class: 'zl-tag zl-tag--accent' }, item.tag),
142
+ actions,
143
+ );
144
+ }
145
+
146
+ function render() {
147
+ const visible = items.filter((it) => filter === 'all' || !it.done);
148
+ $('loading').hidden = true;
149
+ $('count').textContent = items.length ? `${visible.length} 条` : '';
150
+ $('list').replaceChildren(...visible.map(row));
151
+ $('list').hidden = visible.length === 0;
152
+ $('empty').hidden = visible.length > 0;
153
+ // 空状态分两种:真的没有数据 / 筛选后没有
154
+ $('empty-title').textContent = items.length ? '没有符合条件的' : '还没有内容';
155
+ $('empty-text').textContent = items.length
156
+ ? '换个筛选看看。'
157
+ : '告诉用户下一步做什么,比如「在上面新建第一条」。';
158
+ for (const btn of $('filters').querySelectorAll('button')) {
159
+ btn.setAttribute('aria-pressed', String(btn.dataset.filter === filter));
160
+ }
161
+ }
162
+ ```
163
+
164
+ React 写法相同,按状态返回不同的块:
165
+
166
+ ```tsx
167
+ if (loading) return <ListSkeleton />;
168
+ if (error) return <Alert tone="danger" onRetry={reload}>{error}</Alert>;
169
+ if (items.length === 0) return <Empty title="还没有内容" text="下一步做什么" />;
170
+ return (
171
+ <ul className="zl-list">
172
+ {items.map((item) => (
173
+ <li key={item._id} className="zl-list-item">
174
+ <div className="zl-item-main">
175
+ <span className="zl-item-title">{item.title}</span>
176
+ <span className="zl-item-meta">{formatTime(item.updated_at)}</span>
177
+ </div>
178
+ </li>
179
+ ))}
180
+ </ul>
181
+ );
182
+ ```
183
+
184
+ ### 表单 / 输入区(卡片包裹)
185
+
186
+ ```html
187
+ <form id="form" class="zl-card zl-stack">
188
+ <div class="zl-field">
189
+ <label class="zl-label" for="name">字段名</label>
190
+ <input id="name" class="zl-input" required />
191
+ </div>
192
+ <div class="zl-field">
193
+ <label class="zl-label" for="body">长文本字段</label>
194
+ <textarea id="body" class="zl-textarea" rows="6"></textarea>
195
+ <span class="zl-hint">告诉用户怎么填</span>
196
+ </div>
197
+ <div class="zl-row zl-row--end">
198
+ <button id="submit" class="zl-btn zl-btn--primary" type="submit">
199
+ 提交
200
+ </button>
201
+ </div>
202
+ </form>
203
+ ```
204
+
205
+ 提交时按钮加 `aria-busy="true"` 并禁用,结束后恢复;校验失败给对应输入框加 `aria-invalid="true"`,
206
+ 在 `zl-hint` 里写原因。
207
+
208
+ ### 左列表右详情(记录列表 + 当前项)
209
+
210
+ ```html
211
+ <main class="zl-page zl-page--wide">
212
+ <header class="zl-header">…</header>
213
+ <div class="zl-split">
214
+ <aside class="zl-stack zl-stack--sm">
215
+ <button id="new" class="zl-btn zl-btn--primary zl-btn--block"
216
+ type="button">新建</button>
217
+ <div id="records" class="zl-list"></div>
218
+ </aside>
219
+ <section id="detail" class="zl-card zl-stack"></section>
220
+ </div>
221
+ </main>
222
+ ```
223
+
224
+ ```js
225
+ function recordButton(record) {
226
+ return h('button', {
227
+ class: 'zl-list-item', type: 'button',
228
+ 'aria-current': record._id === selectedId ? 'true' : false,
229
+ onclick: () => select(record._id),
230
+ },
231
+ h('span', { class: 'zl-item-main' },
232
+ h('span', { class: 'zl-item-title zl-ellipsis' }, record.title),
233
+ h('span', { class: 'zl-item-meta' }, formatTime(record.updated_at)),
234
+ ),
235
+ );
236
+ }
237
+ ```
238
+
239
+ - 窗口窄于 720px 时自动上下排;宽窗口里左栏吸顶、单独滚动;
240
+ - 没有记录时左栏放 `zl-empty`,右栏直接显示新建表单,不要两边都空;
241
+ - 切换选中项时右栏先显示骨架,再填详情。
242
+
243
+ ### 行内可编辑字段(日期、负责人这类)
244
+
245
+ ```js
246
+ function dateField(item) {
247
+ const input = h('input', {
248
+ class: 'zl-input zl-input--inline', type: 'date',
249
+ value: item.due ?? '', 'aria-label': '日期',
250
+ });
251
+ input.addEventListener('change', async () => {
252
+ const before = item.due ?? '';
253
+ try {
254
+ await save(item, { due: input.value || null });
255
+ } catch (error) {
256
+ input.value = before; // 保存失败:值改回去并提示
257
+ toast('没有保存:' + describe(error));
258
+ }
259
+ });
260
+ return input;
261
+ }
262
+ ```
263
+
264
+ - `zl-input--inline` 平时像文字,悬停 / 聚焦才显出边框,适合放在列表行里;
265
+ - 原生 `type="date"`、`type="time"` 在沙箱里可用,弹出的日历也跟随亮 / 暗;原生 `<select>` 也可用,
266
+ 选项少(≤ 5)时 `zl-segmented` 更好点。
267
+
268
+ ### 统计 + 占比
269
+
270
+ 数值由数据计算,骨架里留空:
271
+
272
+ ```js
273
+ $('stats').replaceChildren(
274
+ ...summary.map(({ label, value, tone }) =>
275
+ h('div', { class: 'zl-stat' },
276
+ h('div', { class: 'zl-stat-label' }, label),
277
+ h('div', { class: 'zl-stat-value' + (tone ? ` zl-text-${tone}` : '') },
278
+ value),
279
+ ),
280
+ ),
281
+ );
282
+ const bar = (percent) =>
283
+ h('div', { class: 'zl-progress' },
284
+ h('span', { style: `width: ${Math.round(percent)}%` }));
285
+ ```
286
+
287
+ ### AI 结果(耗时操作)
288
+
289
+ - 点击后按钮 `aria-busy="true"`,旁边一句 `<span class="zl-muted zl-text-sm">` 说明预计要多久;
290
+ - 结果放进 `zl-card`,正文用 `zl-prose`,要点用列表,新出现的块加 `zl-enter`;
291
+ - 失败用 `zl-alert--danger` 显示原因和「重试」。
292
+
293
+ ### 卡片网格(多个对象并列)
294
+
295
+ `<div class="zl-grid">` 里放若干 `<article class="zl-card zl-card--interactive">`,每张卡片:标题 `zl-card-title`、
296
+ 一行 `zl-item-meta`、底部 `zl-row` 放标签或操作;同样由数据渲染。
297
+
298
+ ## 交付前自查
299
+
300
+ 1. `npm run preview` 打开,亮色、暗色各看一遍(浏览器或系统切换外观);
301
+ 2. 有数据、空、加载中、出错四种状态都能看到合理的界面;
302
+ 3. 窗口缩窄到 400px 左右不出现横向滚动、按钮不挤成两行;
303
+ 4. 只用 `--zl-*` 变量取色;每屏最多一个 `zl-btn--primary`;
304
+ 5. 部署后在 zleapOS 里打开再看一眼(真实数据量、真实错误提示)。
@@ -184,7 +184,7 @@
184
184
  **更新时授权怎么处理**(`appversion/apply.go`):
185
185
 
186
186
  - 新版本的 `requires` **没有变宽**:除后台能力外的授权(含 Cloud 数据读写、数据权限)原样延续到新版本;
187
- - `requires` **变宽**(新增能力或范围):这些授权全部撤销,**不会自动重新授予**——重新打开应用后要在应用窗口「权限」(含底部「管理其他权限」)里重新勾选保存;
187
+ - `requires` **变宽**(新增能力或范围):这些授权全部撤销,**不会自动重新授予**——重新打开应用后要在应用窗口「权限」(含底部「管理其他权限」)里重新勾选保存。打开窗口时宿主会主动弹出缺的授权:云数据授权缺项时弹「应用权限」,安装级权限缺项时弹「数据权限」(每个版本只主动弹一次,缺的项已预先勾好);
188
188
  - 后台能力逐项比较:权限没变宽的「延续」,变宽的「需要重新确认」,新版本删掉的「已移除」;
189
189
  - 运行会话、文件票据一律作废,应用会重新加载。
190
190
 
@@ -234,10 +234,10 @@ CLI 走的是开发会话接口(`appinstallation/version.go` 的 `SwitchVersio
234
234
 
235
235
  「权限」按钮打开哪个对话框,取决于应用声明了什么:
236
236
 
237
- | 应用声明 | 点「权限」 |
238
- | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
239
- | 用了 Cloud:有 `runtime.backend`,或 `requires` 里有 `cloud.*` / `os.agent.complete` | 打开「应用权限」对话框(见下文);若还声明了安装级权限,对话框底部有「**管理其他权限**」,进入「数据权限」对话框 |
240
- | 只声明了安装级权限(`storage.kv` / `space.read` / `space.write` / `notify.send`) | 直接打开「数据权限」对话框 |
237
+ | 应用声明 | 点「权限」 |
238
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
239
+ | 用了 Cloud:有 `runtime.backend`,或 `requires` 里有 `cloud.*` / `os.agent.complete` | 打开「应用权限」对话框(见下文);若还声明了安装级权限,对话框底部有「**管理其他权限**」,进入「数据权限」对话框 |
240
+ | 只声明了安装级权限(`storage.kv` / `space.read` / `space.write` / `notify.send`,或 `host_api.requires` 里的 `clipboard.write` / `workspace.files` / `workspace.images.read`) | 直接打开「数据权限」对话框 |
241
241
 
242
242
  两类授权对应服务端两套独立闸门,**不要混为一谈**。
243
243
 
@@ -304,6 +304,8 @@ CLI 走的是开发会话接口(`appinstallation/version.go` 的 `SwitchVersio
304
304
 
305
305
  入口:「应用权限」对话框底部的「**管理其他权限**」;只声明了安装级权限、没用 Cloud 的应用,点窗口顶部「**权限**」直接打开。
306
306
 
307
+ 当前版本声明了、但还没授权的安装级权限(例如新版本新加了 `storage`),打开应用窗口时会自动弹出这个对话框,并预先勾好缺的项;每个版本只主动弹一次,关掉后从入口自己改。没授权时应用调用会收到 40300,错误信息里写明缺哪项权限、去哪里开。
308
+
307
309
  **标题**:`{应用名}` 的数据权限
308
310
 
309
311
  **说明**:
@@ -316,12 +318,17 @@ CLI 走的是开发会话接口(`appinstallation/version.go` 的 `SwitchVersio
316
318
 
317
319
  **复选框**(只列出该版本声明的项):
318
320
 
319
- | 复选框文案 | 对应能力 |
320
- | ---------------------------- | ------------- |
321
- | **读取和保存此应用的设置** | `storage.kv` |
322
- | **读取此应用中的个人文件** | `space.read` |
323
- | **为此应用创建个人文件版本** | `space.write` |
324
- | **发送此应用的通知** | `notify.send` |
321
+ | 复选框文案 | 对应能力 |
322
+ | ---------------------------------------- | ----------------------- |
323
+ | **读取和保存此应用的设置** | `storage.kv` |
324
+ | **读取此应用中的个人文件** | `space.read` |
325
+ | **为此应用创建个人文件版本** | `space.write` |
326
+ | **发送此应用的通知** | `notify.send` |
327
+ | **写入剪贴板** | `clipboard.write` |
328
+ | **读取及整理你选定的空间文件夹内的文件** | `workspace.files` |
329
+ | **读取你选定的空间文件夹内的图片** | `workspace.images.read` |
330
+
331
+ 后三项是 `host_api.requires` 里的宿主能力:安装时一并授予;**新版本才声明**的(切换版本不会自动授予),打开应用时同样自动弹出这个对话框让你补授权。
325
332
 
326
333
  **按钮**:撤销全部 / 关闭 / 保存(未改动时不可点)
327
334
 
@@ -338,6 +345,7 @@ CLI 走的是开发会话接口(`appinstallation/version.go` 的 `SwitchVersio
338
345
 
339
346
  声明了 `host_api.requires: ["workspace.files"]` 的应用,在调用时会弹出「选择工作文件夹 · {应用}」,其中写明:
340
347
  「应用权限已在安装时授予。这里只选择工作范围;实际操作仍受你的空间权限限制。」——即这里**不是**再授权一次,只是选范围。
348
+ 新版本才声明 `workspace.files` 时,授权在「数据权限」对话框里补(打开应用会自动弹出),之后再选文件夹。
341
349
 
342
350
  ### 已授权信息
343
351
 
@@ -14,7 +14,7 @@
14
14
  >
15
15
  >
16
16
  > - **上传**:把 ZIP 变成一个固定版本。每次上传得到的都是**开发版本**,只有应用成员看得到、装得上。
17
- > - **发布为线上版本**(CLI 叫 `release`):在云开发控制台「版本管理」里把**某一个开发版本**设为线上版本,全企业可见。**没有审核步骤**。在「我的发布」里点成员版本的「**去版本管理发布**」也能直接打开对应入口。
17
+ > - **发布为线上版本**(CLI 叫 `release`):在云开发控制台「版本管理」里把**某一个开发版本**设为线上版本,全企业可见。**没有审核步骤**。在「我的发布」里点开发版本的「**去版本管理发布**」也能直接打开对应入口。
18
18
  > - **回退**:把旧版本再发布一次,控制台里的按钮是「**回退到此版本**」。
19
19
  > - **签名与绑定**(让云函数 / 后台任务能跑):带后台(headless)能力的版本上传时平台自动签名;安装、切换版本、允许后台运行时如果缺签,平台会自动补签并绑定,不需要手动操作。
20
20
  >
@@ -47,7 +47,7 @@
47
47
 
48
48
  1. 点「**上传应用包**」。
49
49
  2. 选择 `.zip` 文件。
50
- 3. 等待上传完成。列表刷新后,新版本出现在应用卡片里,初始是「**仅成员可见**」的开发版本。
50
+ 3. 等待上传完成。列表刷新后,新版本出现在应用卡片里,初始是「**仅本人和开发人员可见**」的开发版本。
51
51
 
52
52
  要让同事用上,点这个版本行上的「**去版本管理发布**」,或在云开发控制台「版本管理」里点「**发布为线上版本**」。
53
53
 
@@ -126,7 +126,7 @@
126
126
 
127
127
  ### 版本列表
128
128
 
129
- 展开后标题「**版本列表**」,说明:「这是 appid 应用:每次上传都是只有应用成员可见的开发版本,在云开发控制台「版本管理」里发布为线上版本后全企业可见。」
129
+ 展开后标题「**版本列表**」,说明:「这是 appid 应用:每次上传都是仅本人和开发人员可见的开发版本,在云开发控制台「版本管理」里发布为线上版本后全企业可见。」
130
130
 
131
131
  每一行:
132
132
 
@@ -134,14 +134,14 @@
134
134
  | ---- | ---------------------------------------------------------------------------------------------------- |
135
135
  | 左 | 版本号;最新上传的那一行带「**最近上传**」标记 |
136
136
  | 中 | 上传时间、包大小 |
137
- | 状态 | 「**仅成员可见**」或「**企业可见**」;已停用的版本另有「**已停用**」 |
138
- | 按钮 | 成员版本:「**去版本管理发布**」;已发布为线上版本的:「**云开发**」→ 打开控制台版本页并定位到该版本 |
137
+ | 状态 | 「**仅本人和开发人员可见**」或「**企业可见**」;已停用的版本另有「**已停用**」 |
138
+ | 按钮 | 开发版本:「**去版本管理发布**」;已发布为线上版本的:「**云开发**」→ 打开控制台版本页并定位到该版本 |
139
139
 
140
140
  已停用的版本没有「去版本管理发布」按钮,「云开发」按钮也不可点。
141
141
 
142
142
  > **版本行的两种状态**(提示)
143
143
  >
144
- > 开发版本显示「**仅成员可见**」和「**去版本管理发布**」;发布为线上版本后,这一行变为「**企业可见**」并保留「**云开发**」按钮。
144
+ > 开发版本显示「**仅本人和开发人员可见**」和「**去版本管理发布**」;发布为线上版本后,这一行变为「**企业可见**」并保留「**云开发**」按钮。
145
145
  > 卡片统计始终是「**企业 n / 开发版本 n**」。
146
146
 
147
147
  ## 发布为线上版本
@@ -7,7 +7,7 @@
7
7
 
8
8
  > **应用中心「我的发布」与这里的关系**(提示)
9
9
  >
10
- > `appid` 应用的版本在应用中心显示「**仅成员可见**」(卡片统计为「企业 n / 开发版本 n」),成员版本上的主按钮是「**去版本管理发布**」,
10
+ > `appid` 应用的版本在应用中心显示「**仅本人和开发人员可见**」(卡片统计为「企业 n / 开发版本 n」),开发版本上的主按钮是「**去版本管理发布**」,
11
11
  > 点它会打开本页;已发布的版本显示「**企业可见**」,并保留「**云开发**」按钮。上线、回退都在本页完成。
12
12
 
13
13
  ## 版本管理
@@ -125,6 +125,72 @@ npm run deploy
125
125
  - 命令是 `zleap-app test <plan.json> --runner-config <运行器配置>`,第一个参数是**计划文件**,不是应用目录;
126
126
  - 报告用 `zleap-app logs <report_id>` 查看。只用专用测试账号和测试数据。
127
127
 
128
+ #### 验收计划怎么写
129
+
130
+ 验收由运维或你本人在本机准备的「运行器」执行,分两份文件:运行器配置(含测试账号,只放本机)和验收计划(描述要点什么、核对什么)。
131
+ AI 工具通过 MCP 调用时用同一份运行器配置:启动 `zleap-app mcp serve` 时加 `--runner-config`,再用 `app_test` 提交计划。
132
+
133
+ **运行器配置**:JSON 文件,权限 0600,放在应用目录之外,绝不能打进应用包。
134
+
135
+ | 字段 | 说明 |
136
+ | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
137
+ | `node`、`puppeteer_module`、`browser` | Node.js(≥ 22.12)、Puppeteer Core(≥ 25)入口和 Chrome 可执行文件的绝对路径 |
138
+ | `auth_file` | 测试账号认证 JSON 的绝对路径(含 `access_token`,可含 `refresh_token`,0600)。由你用专用账号正常登录获得,只给运行器读取,不能放进 MCP 参数或交给应用 |
139
+ | `os_url`、`target` | 目标 OS 地址 |
140
+ | `app_key` | 固定为本应用(appid) |
141
+ | `app_id`、`app_name` | 可留空,由当前会话的发布记录按不可变 `version_id` 解析 |
142
+ | `allow_mutation` | 必须显式写 `true`,表示允许验收写入专用测试数据 |
143
+ | `probes` | 探针名到同一部署只读 API 路径的映射,例如 `{"todos": "/api/v1/…"}`;参数按 `app_api_get` 查到的契约填写 |
144
+
145
+ 测试账号必须和开发会话是同一个账号。可用的探针名可以用 `app_caps_get` 查看(`acceptance_runner.configured` 与 `probe_names`),
146
+ 计划里只能引用这里配置过的探针,不接受应用传入新的 URL、身份、脚本或浏览器跳转。
147
+
148
+ **验收计划**:`app_key`、`version_id` 和 4 到 128 个 `steps`。每一步都要写 `phase`,取值 `read`、`write` 或 `persistence`;
149
+ `target` 可选 `app`(应用窗口内)或 `host`(宿主页面)。可用的动作:
150
+
151
+ | 动作 | 必填字段 | 说明 |
152
+ | ------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
153
+ | `click`、`fill`、`select` | `selector` | 交互,`fill` / `select` 用 `value` 给出要填或要选的值;放在 `write` 阶段时算一次真实写操作 |
154
+ | `click_text` | `text` | 按文字点击 |
155
+ | `assert_text` | `text` | 页面出现指定文字 |
156
+ | `assert_count` | `selector`、`count`(0–10000) | 匹配元素的数量 |
157
+ | `assert_image` | `selector` | 图片必须真正解码成功、宽高非零,只检查有 `img` 标签不算通过 |
158
+ | `probe` | `probe`,以及 `expected` 或非空 `contains`(可配 `pointer`) | 用运行器配置里的探针独立读取数据核对 |
159
+ | `reload` | — | 刷新页面 |
160
+
161
+ 编排规则,运行器会逐条校验:
162
+
163
+ 1. 必须有 `read` 阶段的断言。
164
+ 2. 必须有 `write` 阶段的真实写操作,并在写之后用 `probe` 独立核对数据。
165
+ 3. `reload` 只能放在写操作和写后探针之后。
166
+ 4. `persistence` 阶段的断言必须在 `reload` 之后,证明刷新后数据还在。
167
+
168
+ 示例(待办应用:读取列表,新增一条,探针核对,刷新后确认还在):
169
+
170
+ ```json
171
+ {
172
+ "app_key": "<appid>",
173
+ "version_id": "<要验收的版本 ID>",
174
+ "steps": [
175
+ { "phase": "read", "action": "assert_text", "target": "app", "text": "待办" },
176
+ {
177
+ "phase": "write",
178
+ "action": "fill",
179
+ "target": "app",
180
+ "selector": "input[name=title]",
181
+ "value": "验收-001"
182
+ },
183
+ { "phase": "write", "action": "click_text", "target": "app", "text": "添加" },
184
+ { "phase": "write", "action": "probe", "probe": "todos", "contains": "验收-001" },
185
+ { "phase": "persistence", "action": "reload" },
186
+ { "phase": "persistence", "action": "assert_text", "target": "app", "text": "验收-001" }
187
+ ]
188
+ }
189
+ ```
190
+
191
+ 选择器和文字按你自己的页面填写,动作名以 `app_spec_get(section=acceptance)` 返回的 schema 为准。
192
+ 报告 `status` 为 `passed` 且 `browser_tested` 为 `true` 才算通过。验收期间如果版本发生变化,结果作废,需要重新验收。
193
+
128
194
  (截图:部署后的应用窗口:OS 已连接,能读写待办,见文档站)
129
195
 
130
196
  > **开发环境只对应用成员开放**(说明)
@@ -200,7 +266,7 @@ npm run release -- --notes "本次更新说明"
200
266
 
201
267
  > **应用中心「我的发布」里的版本**(提示)
202
268
  >
203
- > 版本行显示「**仅成员可见**」,卡片统计是「企业 n / 开发版本 n」。成员版本的主按钮是「**去版本管理发布**」,点它打开云开发控制台「版本管理」;
269
+ > 版本行显示「**仅本人和开发人员可见**」,卡片统计是「企业 n / 开发版本 n」。开发版本的主按钮是「**去版本管理发布**」,点它打开云开发控制台「版本管理」;
204
270
  > 已发布的版本显示「**企业可见**」并保留「**云开发**」按钮。上线统一在「版本管理」里点「发布为线上版本」。
205
271
 
206
272
  > **第一次发布没成功**(提示)
@@ -145,15 +145,15 @@
145
145
 
146
146
  ### 数据
147
147
 
148
- | 错误码 | 含义 | 处理 |
149
- | ---------------------- | ------------------------------------------ | ----------------------------------------------------------- |
150
- | `SCHEMA_INVALID` | 参数或文档不符合规则(含大小、嵌套等超限) | 见本页末尾「排查:`SCHEMA_INVALID`」 |
151
- | `VERSION_CONFLICT` | 乐观锁冲突,或键已存在等 409 类错误 | 重新读取最新版本再改 |
152
- | `DOCUMENT_NOT_FOUND` | 文档不存在(或 404 类错误) | 检查文档键;读取时可用 `throwOnNotFound: false` |
153
- | `COLLECTION_NOT_FOUND` | 集合不存在 | 在 Manifest `data.collections` 声明;生产环境要单独开通集合 |
148
+ | 错误码 | 含义 | 处理 |
149
+ | ---------------------- | -------------------------------------------- | -------------------------------------------------------------------------------- |
150
+ | `SCHEMA_INVALID` | 参数或文档不符合规则(含大小、嵌套等超限) | 见本页末尾「排查:`SCHEMA_INVALID`」 |
151
+ | `VERSION_CONFLICT` | 乐观锁冲突,或键已存在等 409 类错误 | 重新读取最新版本再改 |
152
+ | `DOCUMENT_NOT_FOUND` | 文档不存在(或 404 类错误) | 检查文档键;读取时可用 `throwOnNotFound: false` |
153
+ | `COLLECTION_NOT_FOUND` | 集合不存在 | 在 Manifest `data.collections` 声明;生产环境要单独开通集合 |
154
154
  | `SCOPE_FORBIDDEN` | 读写了 manifest **没声明**的集合(或没授权) | 先在 `data.collections` 声明再部署;已声明还报这个,看「权限」里是否勾了数据读写 |
155
- | `INDEX_REQUIRED` | 查询需要扫描的行数超过单次上限 | 给筛选字段建索引;已有索引仍超限时缩小筛选范围 |
156
- | `RESOURCE_NOT_READY` | 云数据库正在开通,或数据服务还没加载本应用 | 通常一分钟内自动就绪,稍后刷新重试 |
155
+ | `INDEX_REQUIRED` | 查询需要扫描的行数超过单次上限 | 给筛选字段建索引;已有索引仍超限时缩小筛选范围 |
156
+ | `RESOURCE_NOT_READY` | 云数据库正在开通,或数据服务还没加载本应用 | 通常一分钟内自动就绪,稍后刷新重试 |
157
157
 
158
158
  ### 文件存储
159
159
 
@@ -419,6 +419,10 @@
419
419
  `zleap-app publish` 等写操作网络结果不明时返回 `COMMIT_UNKNOWN`,**不会自动重传**:
420
420
  用 `zleap-app operation --request-id <原操作号>` 或 `zleap-app status` 查询原结果。
421
421
 
422
+ `zleap-app business get` / `call`(以及 MCP 的 `os_business_get` / `os_business_call`)请求超管、账号与组织、内部控制或未审核的接口时,
423
+ 返回 403 `OPERATION_NOT_EXPOSED`。应用页面直接调用 OS 接口时,未向应用开放的接口同样返回 `OPERATION_NOT_EXPOSED`(「该接口未向应用开放」)。
424
+ 这类接口不会开放,先用 `business search` 找可用的替代接口。
425
+
422
426
  ## 上传与 Manifest 校验
423
427
 
424
428
  上传、`zleap-app check` 或安装时,Manifest 相关失败通过 `detail.reason` 返回:
@@ -524,18 +528,18 @@
524
528
  平台在上传带后台能力的版本时自动签发开发发布,首次发布为线上版本时自动签发生产发布。
525
529
  签发或验签失败时按原因码归类返回:
526
530
 
527
- | 服务端原因码 | 含义 |
528
- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
529
- | `RELEASE_PROOF_TOO_LARGE` | 证明文件超过 2 MiB 限制 |
530
- | `RELEASE_PROOF_INVALID` | 证明 JSON 或签名信封无效 |
531
- | `RELEASE_IDENTITY_MISMATCH`、`RELEASE_DIGEST_MISMATCH`、`RELEASE_SOURCE_MISMATCH` | 证明与当前应用、发布、包、来源、摘要或策略不匹配 |
532
- | `RELEASE_SIGNATURE_INVALID` | 无法验证发布签名 |
533
- | `RELEASE_ARTIFACT_MISMATCH`、`RELEASE_MANIFEST_MISMATCH` | 签名 Manifest 或包制品清单不匹配 |
534
- | `RELEASE_POLICY_STALE` | 策略审核已过期或不是当前策略 |
535
- | `RELEASE_POLICY_UNAVAILABLE` | 尚未配置发布策略,无法验证 |
536
- | `RELEASE_TRUST_UNAVAILABLE` | 发布信任策略尚未配置,或发布者密钥不可用、已撤销或已过期;由管理员检查 |
537
- | `RELEASE_DISABLED` | 发布候选已撤回 |
538
- | 其他 | 签名验证请求被拒绝 |
531
+ | 服务端原因码 | 含义 |
532
+ | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
533
+ | `RELEASE_PROOF_TOO_LARGE` | 证明文件超过 2 MiB 限制 |
534
+ | `RELEASE_PROOF_INVALID` | 证明 JSON 或签名信封无效 |
535
+ | `RELEASE_IDENTITY_MISMATCH`、`RELEASE_DIGEST_MISMATCH`、`RELEASE_SOURCE_MISMATCH` | 证明与当前应用、发布、包、来源、摘要或策略不匹配 |
536
+ | `RELEASE_SIGNATURE_INVALID` | 无法验证发布签名 |
537
+ | `RELEASE_ARTIFACT_MISMATCH`、`RELEASE_MANIFEST_MISMATCH` | 签名 Manifest 或包制品清单不匹配 |
538
+ | `RELEASE_POLICY_STALE` | 策略审核已过期或不是当前策略 |
539
+ | `RELEASE_POLICY_UNAVAILABLE` | 尚未配置发布策略,无法验证 |
540
+ | `RELEASE_TRUST_UNAVAILABLE` | 发布信任策略尚未配置,或发布者密钥不可用、已撤销或已过期;由管理员检查 |
541
+ | `RELEASE_DISABLED` | 发布候选已撤回 |
542
+ | 其他 | 签名验证请求被拒绝 |
539
543
 
540
544
  签发时的 `RELEASE_POLICY_UNAVAILABLE`、`RELEASE_TRUST_UNAVAILABLE` 属于**部署未配置**;
541
545
  发布生产版本时的 `RELEASE_TRUST_UNAVAILABLE` 则表示这个发布的签名与当前信任策略对不上,重新发布即可(见 [发布与生产门禁](#发布与生产门禁))。
@@ -0,0 +1,14 @@
1
+ {
2
+ "mcpServers": {
3
+ "zleap-app": {
4
+ "command": "npx",
5
+ "args": [
6
+ "zleap-app",
7
+ "mcp",
8
+ "serve",
9
+ "--workspace",
10
+ "."
11
+ ]
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "mcpServers": {
3
+ "zleap-app": {
4
+ "command": "npx",
5
+ "args": [
6
+ "zleap-app",
7
+ "mcp",
8
+ "serve",
9
+ "--workspace",
10
+ "."
11
+ ]
12
+ }
13
+ }
14
+ }