create-zleap-app 0.10.0 → 0.10.2
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/index.js +24 -2
- package/lib/scaffold.js +10 -0
- package/package.json +4 -4
- package/skills/zleap-cloud-app/SKILL.md +6 -6
- package/skills/zleap-cloud-app/references/app-runtime.md +82 -75
- package/skills/zleap-cloud-app/references/cloud-functions.md +230 -43
- package/skills/zleap-cloud-app/references/console-database.md +8 -0
- package/skills/zleap-cloud-app/references/console-members.md +44 -30
- package/skills/zleap-cloud-app/references/limits.md +57 -56
- package/skills/zleap-cloud-app/references/manifest.md +36 -35
- package/skills/zleap-cloud-app/references/permissions.md +40 -47
- package/skills/zleap-cloud-app/references/sdk-database-files.md +116 -49
- package/skills/zleap-cloud-app/references/status-codes.md +126 -94
- package/skills/zleap-cloud-app/references/triggers.md +24 -24
- package/skills/zleap-cloud-app-release/SKILL.md +5 -2
- package/skills/zleap-cloud-app-release/references/app-center-install.md +64 -52
- package/skills/zleap-cloud-app-release/references/app-center-publish.md +118 -49
- package/skills/zleap-cloud-app-release/references/console-functions.md +12 -4
- package/skills/zleap-cloud-app-release/references/console-legacy-release.md +5 -2
- package/skills/zleap-cloud-app-release/references/console-members.md +44 -30
- package/skills/zleap-cloud-app-release/references/console-release.md +39 -28
- package/skills/zleap-cloud-app-release/references/console-workspace.md +32 -16
- package/skills/zleap-cloud-app-release/references/dev-to-production.md +330 -113
- package/skills/zleap-cloud-app-release/references/permissions.md +40 -47
- package/skills/zleap-cloud-app-release/references/status-codes.md +126 -94
- package/templates/react-ts/AGENTS.md +2 -2
- package/templates/react-ts/README.md +1 -1
- package/templates/react-ts/package.json +3 -0
- package/templates/react-ts/scripts/zleap.mjs +25 -3
- package/templates/react-ts/src/cloud/client.ts +2 -2
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
| 名词 | 是什么 | 写在哪 |
|
|
13
13
|
| ----------------------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
14
|
-
| **函数**(function) | 一段 Node
|
|
14
|
+
| **函数**(function) | 一段 Node 代码,入口导出一个函数(平台按函数名 / `main` / default 查找) | `runtime.backend.functions[]` + 包里的 `cloudfunctions/` 目录 |
|
|
15
15
|
| **Provider** | 「由谁来实现」:可以是函数(`managed_function`)、平台逻辑单元(`logic_unit`)或平台内置动作(`builtin`) | `runtime.backend.providers[]` |
|
|
16
16
|
| **后台能力**(headless capability) | 对外可调用的一个操作,指向某个 provider,带输入 / 输出 Schema | `runtime.capabilities.provides[]`,`execution: "headless"` |
|
|
17
17
|
|
|
@@ -142,35 +142,37 @@ my-app/
|
|
|
142
142
|
- 路径不存在时报「云函数制品入口缺失」/ `FUNCTION_ARTIFACT_MISSING`;
|
|
143
143
|
- 单个函数目录最多 4096 个文件、合计 20 MiB(`FUNCTION_ARTIFACT_TOO_LARGE`)。
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
入口的导出由 runner 按 **`module[函数名] ?? module.main ?? module.default`** 查找:导出与 `functions[].name` 同名的函数最稳妥,也支持导出 `main`(或 default)。
|
|
146
|
+
调用签名是 **`(event, context)`**,返回值就是作业的 `result`。`event` 是调用时传入的 `input` / `data`;
|
|
147
|
+
`context` 带 `invocationId`、`jobId`、`attemptId`、`deadlineAt` 和 `signal`——`signal` 是 `AbortSignal`,到截止时间触发,长任务应当响应取消。
|
|
146
148
|
|
|
147
149
|
```js
|
|
148
150
|
import { zl } from '@zleap/app-cloud-server';
|
|
149
151
|
|
|
150
|
-
export async function main(event) {
|
|
152
|
+
export async function main(event, context) {
|
|
151
153
|
zl.cloud.init({ env: zl.cloud.DYNAMIC_CURRENT_ENV });
|
|
152
154
|
const logger = zl.cloud.getLogger();
|
|
153
|
-
const context = zl.cloud.getContext();
|
|
154
155
|
|
|
155
156
|
const draftId = typeof event?.draft_id === 'string' ? event.draft_id : '';
|
|
156
157
|
if (!draftId) throw new Error('DRAFT_ID_REQUIRED');
|
|
157
158
|
|
|
158
|
-
logger.info('processing draft', { draftId });
|
|
159
|
+
logger.info('processing draft', { draftId, invocationId: context.invocationId });
|
|
160
|
+
if (context.signal.aborted) throw new Error('CANCELLED');
|
|
159
161
|
return { summary: `已处理 ${draftId}` };
|
|
160
162
|
}
|
|
161
163
|
```
|
|
162
164
|
|
|
163
165
|
### 运行环境 `node-lts/1`
|
|
164
166
|
|
|
165
|
-
| 项 | 值
|
|
166
|
-
| --------------------- |
|
|
167
|
-
| Node 版本 | 24.21.0(linux / arm64,非 root
|
|
168
|
-
| 网络 | **无**(`network_policy: none`)——函数里不能 `fetch` 外部地址
|
|
169
|
-
| `timeout_seconds` | 1–300,默认 30
|
|
170
|
-
| `memory_mb` | 64–4096,默认 512
|
|
171
|
-
| CPU / 进程数 / 临时盘 | 1000 millicore / 64 / 64 MB(固定,不可配置)
|
|
172
|
-
| 返回值大小 | ≤ 256 KiB
|
|
173
|
-
| 日志 | 每次调用 ≤ 1 MiB,单行 ≤ 16 KiB
|
|
167
|
+
| 项 | 值 |
|
|
168
|
+
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
169
|
+
| Node 版本 | **以实际部署镜像为准。** `node-lts/1` 画像在 `profile.json` 里登记为 Node 24.21.0(linux / arm64,非 root),但仓库中的沙箱镜像 Dockerfile 目前基于 `node:22-bookworm-slim`,两者尚未对齐。请勿依赖 Node 22 之后才有的特性,也不要写死补丁版本 |
|
|
170
|
+
| 网络 | **无**(`network_policy: none`)——函数里不能 `fetch` 外部地址 |
|
|
171
|
+
| `timeout_seconds` | 1–300,默认 30 |
|
|
172
|
+
| `memory_mb` | 64–4096,默认 512 |
|
|
173
|
+
| CPU / 进程数 / 临时盘 | 1000 millicore / 64 / 64 MB(固定,不可配置) |
|
|
174
|
+
| 返回值大小 | ≤ 256 KiB |
|
|
175
|
+
| 日志 | 每次调用 ≤ 1 MiB,单行 ≤ 16 KiB |
|
|
174
176
|
|
|
175
177
|
`@zleap/app-cloud-server` 由运行时提供,**不需要也不应该**打进包里。第三方 npm 依赖能否随包使用**未验证**;
|
|
176
178
|
由于没有网络,任何需要联网的库都用不了。
|
|
@@ -187,24 +189,22 @@ export async function main(event) {
|
|
|
187
189
|
| `zl.cloud.init({ env: zl.cloud.DYNAMIC_CURRENT_ENV })` | 建议第一行调用。只接受 `env: DYNAMIC_CURRENT_ENV` 或不传;写 `'dev'` 等会抛 `ENV_MISMATCH` |
|
|
188
190
|
| `zl.cloud.getContext()` | 本次调用的上下文(见下表) |
|
|
189
191
|
| `zl.cloud.getLogger()` | 结构化日志 |
|
|
190
|
-
| `zl.cloud.os.database.query(args)` |
|
|
192
|
+
| `zl.cloud.os.database.query(args)` | 读(只支持 `get` 单条 / `count` 计数) |
|
|
191
193
|
| `zl.cloud.os.database.mutate(args)` | 写(`add` / `set` / `update` / `remove`) |
|
|
192
|
-
| `zl.cloud.auth.getOpenId()` | 调用者在本应用里的 `openid`(不需要用户同意),用作应用里的用户键 |
|
|
193
|
-
| `zl.cloud.auth.getUserInfo()` | 调用者的 `openid` 与他授权给应用的信息,见 [授权登录](app-runtime.md#在云函数里读取) |
|
|
194
194
|
|
|
195
|
-
云函数里**没有** `zl.cloud.database()`、文件存储、`callFunction`
|
|
195
|
+
云函数里**没有** `zl.cloud.database()`、文件存储、`callFunction` 或能力调用,也**没有** `zl.cloud.auth`:`getOpenId()` / `getUserInfo()` 目前不可用。需要 `openid` 时,由页面用 `zl.auth.getIdentity()` 取到后作为输入传入,或使用文档自带的 `_openid`(见下文)。
|
|
196
196
|
|
|
197
197
|
### `getContext()`
|
|
198
198
|
|
|
199
|
-
| 字段 | 说明
|
|
200
|
-
| --------------------------------------- |
|
|
201
|
-
| `invocationId` | **调用编号**——日志筛选用它
|
|
202
|
-
| `jobId` | 作业编号
|
|
203
|
-
| `attemptId` | 本次尝试编号(重试时不同)
|
|
204
|
-
| `appId` / `environmentId` / `bindingId` | 应用、环境、数据绑定 ID
|
|
205
|
-
| `userId` | **已废弃**:平台内部用户 ID
|
|
206
|
-
| `traceId` | 链路追踪 ID
|
|
207
|
-
| `deadlineAt` | 截止时间(秒级时间戳);过了之后 SDK 调用抛 `FUNCTION_DEADLINE_EXCEEDED`
|
|
199
|
+
| 字段 | 说明 |
|
|
200
|
+
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
201
|
+
| `invocationId` | **调用编号**——日志筛选用它 |
|
|
202
|
+
| `jobId` | 作业编号 |
|
|
203
|
+
| `attemptId` | 本次尝试编号(重试时不同) |
|
|
204
|
+
| `appId` / `environmentId` / `bindingId` | 应用、环境、数据绑定 ID |
|
|
205
|
+
| `userId` | **已废弃**:平台内部用户 ID。当前服务端 SDK 没有 `auth` 命名空间,不要用它区分业务用户;用文档自带的 `_openid`,或由页面传入 `openid` |
|
|
206
|
+
| `traceId` | 链路追踪 ID |
|
|
207
|
+
| `deadlineAt` | 截止时间(秒级时间戳);过了之后 SDK 调用抛 `FUNCTION_DEADLINE_EXCEEDED` |
|
|
208
208
|
|
|
209
209
|
### `getLogger()`
|
|
210
210
|
|
|
@@ -339,15 +339,19 @@ await zl.cloud.os.database.mutate({
|
|
|
339
339
|
|
|
340
340
|
> **给云函数用的 provider 一定要写 `fields`**(注意)
|
|
341
341
|
>
|
|
342
|
-
> Manifest
|
|
342
|
+
> Manifest 校验技术上允许省略 `fields`(默认就是空数组),但云函数数据网关会把**空 `fields` 当成没有任何字段授权**而拒绝。
|
|
343
343
|
|
|
344
344
|
数据范围由平台按调用身份和数据绑定自动限定,函数里传 `user_id` 之类的字段绕不过去。
|
|
345
345
|
|
|
346
|
-
> **用 openid
|
|
346
|
+
> **用 openid 区分用户:从输入或 `_openid` 来**(提示)
|
|
347
|
+
>
|
|
348
|
+
> 当前服务端 SDK **没有** `zl.cloud.auth.getOpenId()` / `getUserInfo()`。需要「谁的数据」这类键时,有两个来源:
|
|
347
349
|
>
|
|
350
|
+
> - 页面用 `zl.auth.getIdentity()` 拿到 `openid`,作为能力的输入传给函数;
|
|
351
|
+
> - 私有集合里读到的文档自带 `_openid`(写入者标识),可以直接用(`examples/moments` 的 `stats` 就是这么做的)。
|
|
352
|
+
>
|
|
353
|
+
> 由页面传入的 `openid` 只是业务参数,函数无法再向平台核验;手机号 / 邮箱同理,函数拿不到授权登录的信息。
|
|
348
354
|
> `getContext().userId` 是平台内部用户 ID,只为兼容旧应用保留,新应用不要存它、也不要返回给前端。
|
|
349
|
-
> 需要「谁的数据」这类键时用 `await zl.cloud.auth.getOpenId()`:它和前端 `zl.auth.getIdentity()` 拿到的 `openid` 一致,
|
|
350
|
-
> 同一用户在不同应用里不同。
|
|
351
355
|
|
|
352
356
|
### 读写失败时拿到什么
|
|
353
357
|
|
|
@@ -390,28 +394,208 @@ function operationIdFor(context, purpose) {
|
|
|
390
394
|
> **跨 attempt 能否被识别为同一写入未验证**。带副作用的写操作建议 `retry.mode: "none"`,
|
|
391
395
|
> 或用 `expected_version` 乐观锁兜底,避免重复写入。
|
|
392
396
|
|
|
397
|
+
## 最小可运行示例:同事圈(moments)
|
|
398
|
+
|
|
399
|
+
仓库里的 `zleap-app-kit/examples/moments` 是一个完整可运行的示例(React + Vite):它用一个云函数 `stats` 统计「我的互动」,每 5 分钟由定时触发器跑一次。下面是它的关键声明和函数代码(节选,完整 Manifest 见该目录的 `manifest.json`)。
|
|
400
|
+
|
|
401
|
+
**1. 声明函数与 provider**(`runtime.backend` 下的两个数组)
|
|
402
|
+
|
|
403
|
+
```json
|
|
404
|
+
"functions": [
|
|
405
|
+
{
|
|
406
|
+
"name": "stats",
|
|
407
|
+
"entry": "cloudfunctions/stats",
|
|
408
|
+
"runtime_profile": "node-lts/1",
|
|
409
|
+
"timeout_seconds": 30,
|
|
410
|
+
"memory_mb": 256
|
|
411
|
+
}
|
|
412
|
+
],
|
|
413
|
+
"providers": [
|
|
414
|
+
{
|
|
415
|
+
"id": "moments.stats",
|
|
416
|
+
"type": "managed_function",
|
|
417
|
+
"function": "stats",
|
|
418
|
+
"permissions": {
|
|
419
|
+
"capabilities": ["cloud.database.read", "cloud.database.write"],
|
|
420
|
+
"data": [
|
|
421
|
+
{ "collection": "me", "scope": "current_user", "actions": ["get"], "fields": ["nickname", "created_at"] },
|
|
422
|
+
{ "collection": "posts", "scope": "current_user", "actions": ["query"], "fields": ["author"] },
|
|
423
|
+
{ "collection": "likes", "scope": "current_user", "actions": ["query"], "fields": ["post_author", "author"] },
|
|
424
|
+
{ "collection": "comments", "scope": "current_user", "actions": ["query"], "fields": ["post_author", "author"] },
|
|
425
|
+
{ "collection": "digest", "scope": "current_user", "actions": ["get", "set"], "fields": ["posts", "likes_received", "comments_received", "source", "updated_at"] }
|
|
426
|
+
]
|
|
427
|
+
},
|
|
428
|
+
"retry": { "mode": "none", "max_attempts": 1 }
|
|
429
|
+
}
|
|
430
|
+
]
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
**2. 声明对外能力**(`runtime.capabilities.provides` 里的一项)
|
|
434
|
+
|
|
435
|
+
```json
|
|
436
|
+
{
|
|
437
|
+
"id": "zl7b2843d539ca2df7.stats",
|
|
438
|
+
"contract_version": "1.0.0",
|
|
439
|
+
"title": "统计我的互动",
|
|
440
|
+
"execution": "headless",
|
|
441
|
+
"provider": "moments.stats",
|
|
442
|
+
"effect": "write",
|
|
443
|
+
"discoverable": false,
|
|
444
|
+
"input_schema": {
|
|
445
|
+
"type": "object",
|
|
446
|
+
"properties": { "source": { "type": "string", "enum": ["manual", "scheduled"] } },
|
|
447
|
+
"additionalProperties": false
|
|
448
|
+
},
|
|
449
|
+
"output_schema": {
|
|
450
|
+
"type": "object",
|
|
451
|
+
"properties": {
|
|
452
|
+
"posts": { "type": "integer", "minimum": 0 },
|
|
453
|
+
"likes_received": { "type": "integer", "minimum": 0 },
|
|
454
|
+
"comments_received": { "type": "integer", "minimum": 0 },
|
|
455
|
+
"error": { "type": "string", "maxLength": 300 }
|
|
456
|
+
},
|
|
457
|
+
"additionalProperties": false
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
**3. 写函数** — `cloudfunctions/stats/index.mjs`:
|
|
463
|
+
|
|
464
|
+
```js
|
|
465
|
+
// 统计「我的互动」:我发了几条动态、收到多少赞和评论(不算自己给自己的),写进私有集合 digest。
|
|
466
|
+
// 云函数里只能 get / count;本人在数据域里的标识从私有集合 me/profile 的 _openid 读出来。
|
|
467
|
+
import { createHash } from 'node:crypto';
|
|
468
|
+
import { zl } from '@zleap/app-cloud-server';
|
|
469
|
+
|
|
470
|
+
const opId = (ctx, purpose) =>
|
|
471
|
+
`op_${Math.floor(Date.now() / 1000)}_${createHash('sha256').update(`${ctx.invocationId}:${purpose}`).digest('hex').slice(0, 32)}`;
|
|
472
|
+
|
|
473
|
+
export async function main(event) {
|
|
474
|
+
zl.cloud.init({ env: zl.cloud.DYNAMIC_CURRENT_ENV });
|
|
475
|
+
const ctx = zl.cloud.getContext();
|
|
476
|
+
const logger = zl.cloud.getLogger();
|
|
477
|
+
const db = zl.cloud.os.database;
|
|
478
|
+
const rid = (purpose) => `${ctx.invocationId}:${purpose}`.slice(0, 128);
|
|
479
|
+
try {
|
|
480
|
+
const found = await db
|
|
481
|
+
.query({ collection: 'me', operation: 'get', document_key: 'profile', request_id: rid('me') })
|
|
482
|
+
.catch((e) => {
|
|
483
|
+
if (e?.code === 'DOCUMENT_NOT_FOUND') return null;
|
|
484
|
+
throw e;
|
|
485
|
+
});
|
|
486
|
+
const me = Array.isArray(found?.list) ? found.list[0] : null;
|
|
487
|
+
if (!me?._openid)
|
|
488
|
+
return {
|
|
489
|
+
posts: 0,
|
|
490
|
+
likes_received: 0,
|
|
491
|
+
comments_received: 0,
|
|
492
|
+
error: '还没打开过同事圈,没有资料',
|
|
493
|
+
};
|
|
494
|
+
const openid = me._openid;
|
|
495
|
+
const count = async (collection, filter, purpose) => {
|
|
496
|
+
const res = await db.query({
|
|
497
|
+
collection,
|
|
498
|
+
operation: 'count',
|
|
499
|
+
filter,
|
|
500
|
+
request_id: rid(purpose),
|
|
501
|
+
});
|
|
502
|
+
return Number(res?.total ?? res?.count ?? 0);
|
|
503
|
+
};
|
|
504
|
+
const others = (field) => ({
|
|
505
|
+
op: 'and',
|
|
506
|
+
filters: [
|
|
507
|
+
{ op: 'eq', field, value: openid },
|
|
508
|
+
{ op: 'neq', field: 'author', value: openid },
|
|
509
|
+
],
|
|
510
|
+
});
|
|
511
|
+
const posts = await count('posts', { op: 'eq', field: 'author', value: openid }, 'posts');
|
|
512
|
+
const likes = await count('likes', others('post_author'), 'likes');
|
|
513
|
+
const comments = await count('comments', others('post_author'), 'comments');
|
|
514
|
+
const source = event?.source === 'scheduled' ? 'scheduled' : 'manual';
|
|
515
|
+
await db.mutate({
|
|
516
|
+
collection: 'digest',
|
|
517
|
+
operation: 'set',
|
|
518
|
+
document_key: 'latest',
|
|
519
|
+
data: {
|
|
520
|
+
posts,
|
|
521
|
+
likes_received: likes,
|
|
522
|
+
comments_received: comments,
|
|
523
|
+
source,
|
|
524
|
+
updated_at: Date.now(),
|
|
525
|
+
},
|
|
526
|
+
value_types: { '/updated_at': 'date' },
|
|
527
|
+
operation_id: opId(ctx, 'digest'),
|
|
528
|
+
request_id: rid('digest'),
|
|
529
|
+
});
|
|
530
|
+
logger.info('stats refreshed', { posts, likes, comments, source });
|
|
531
|
+
return { posts, likes_received: likes, comments_received: comments };
|
|
532
|
+
} catch (error) {
|
|
533
|
+
logger.error('stats failed', { code: error?.code });
|
|
534
|
+
return {
|
|
535
|
+
posts: 0,
|
|
536
|
+
likes_received: 0,
|
|
537
|
+
comments_received: 0,
|
|
538
|
+
error: `${error?.code ?? 'ERROR'}: ${error?.message ?? error}`.slice(0, 300),
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
几个和声明对应的地方:
|
|
545
|
+
|
|
546
|
+
- 函数叫 `stats`,`entry` 指向目录 `cloudfunctions/stats`,runner 会在目录里找到 `index.mjs`;
|
|
547
|
+
- 函数只 `get` 了 `me`、`count` 了 `posts` / `likes` / `comments`,但 provider 里后三个集合声明的是 `query`——**`count` 按 `query` 动作核对**,只写 `count` 会被拒;
|
|
548
|
+
- `data.collections` 必须包含 `me`、`posts`、`likes`、`comments`、`digest`;示例另外还声明了 `photos` 和 `schemas` / `policies`;
|
|
549
|
+
- 写 `digest` 用 `set`,所以 `actions` 里要有 `set`(示例同时给了 `get`)。
|
|
550
|
+
|
|
551
|
+
**4. 构建与上传**
|
|
552
|
+
|
|
553
|
+
示例的 `vite.config.ts` 里有一个 `zleapPackage` 插件:构建时把项目根的 `manifest.json`、`LICENSE` 和整个 `cloudfunctions/` **原样复制进 `dist/`**(跳过 `node_modules`),并校验 `runtime.ui.entry` 是 `index.html`。所以构建产物 `dist/` 就是应用包根目录,可以直接交给 CLI:
|
|
554
|
+
|
|
555
|
+
```bash
|
|
556
|
+
npx zleap-app check dist
|
|
557
|
+
npx zleap-app pack dist --output out/app.zip
|
|
558
|
+
npx zleap-app publish dist
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
示例项目已经把这些串成脚本:`npm run deploy`(构建 → 检查 → 上传开发版本 → 装到自己桌面)、`npm run release -- --notes "…"`(发布为线上版本)。
|
|
562
|
+
|
|
563
|
+
包上传后还差平台侧两步:在应用窗口「权限」里勾选「统计我的互动」这项后台能力;再到控制台「版本管理」页下方「开发环境:云函数激活」把该版本激活到开发。定时触发器 `stats_refresh` 还要在控制台「触发器」里启用,否则只会手动调用时运行。
|
|
564
|
+
|
|
393
565
|
## 第三步:上传、签名、激活、授权
|
|
394
566
|
|
|
395
567
|
函数代码写好后,要走完下面这条链才会真正执行。缺任何一步,调用都不会进入函数沙箱:
|
|
396
568
|
|
|
397
569
|
```
|
|
398
|
-
① 上传:zleap-app publish
|
|
570
|
+
① 上传:zleap-app publish 或应用中心「上传应用包」
|
|
399
571
|
② 安装(安装即授予声明的 cloud.* 权限;后台能力为「停用」)
|
|
400
572
|
③ 开发环境与数据库:声明了集合的应用安装时自动创建并开通(集合按声明自动创建);否则在控制台「创建开发环境」
|
|
401
|
-
④
|
|
573
|
+
④ 激活开发:控制台「版本管理」页下方的「开发环境:云函数激活」(新建候选 → 签名验证 → 激活开发),
|
|
574
|
+
或在项目里运行 `npx zleap-app prepare <app_id> --version-id <id>`(只准备开发环境)
|
|
575
|
+
概览开通清单里对应「去签名」/「升级安装并激活」;这里只列出你本人上传的版本
|
|
402
576
|
⑤ 应用窗口「权限」→「后台能力(定时任务 / 云函数)」勾选该能力 →「保存并重新加载」
|
|
403
577
|
(或在控制台「后台能力」里点「允许后台运行」)
|
|
404
578
|
```
|
|
405
579
|
|
|
406
|
-
详细界面见
|
|
580
|
+
详细界面见 版本管理(文档站 /console/release)、云函数与后台能力(文档站 /console/functions)、安装与卸载(文档站 /app-center/install),
|
|
407
581
|
端到端顺序见 从开发到上线(文档站 /guide/dev-to-production)。
|
|
408
582
|
|
|
409
|
-
| 少了哪一步 | 现象
|
|
410
|
-
| ----------------------------------------------------- |
|
|
411
|
-
| 没有任何已启用的后台能力 | 会话里没有 `app-execution/1`,前端 `callFunction` / `capabilities.invoke` **在发请求前**就抛 `UNSUPPORTED_OPERATION
|
|
412
|
-
| 这项能力没被授权 | 403「此能力尚未由用户授权」
|
|
413
|
-
| 没有「激活开发」(`managed_function` 没绑定就绪发布) | `FUNCTION_RELEASE_REQUIRED`;「权限」对话框里该项下方提示「该版本尚未激活发布版本,暂时无法执行。请到「发布版本」页完成「激活开发」。」
|
|
414
|
-
| 部署没接入函数计算节点 / 调度服务 | `FUNCTION_RUNTIME_UNAVAILABLE`(平台未开放,不是你写错了)
|
|
583
|
+
| 少了哪一步 | 现象 |
|
|
584
|
+
| ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
585
|
+
| 没有任何已启用的后台能力 | 会话里没有 `app-execution/1`,前端 `callFunction` / `capabilities.invoke` **在发请求前**就抛 `BACKGROUND_CAPABILITY_UNAVAILABLE`(旧 SDK 为 `UNSUPPORTED_OPERATION`) |
|
|
586
|
+
| 这项能力没被授权 | 403「此能力尚未由用户授权」 |
|
|
587
|
+
| 没有「激活开发」(`managed_function` 没绑定就绪发布) | `FUNCTION_RELEASE_REQUIRED`;「权限」对话框里该项下方提示「该版本尚未激活发布版本,暂时无法执行。请到「发布版本」页完成「激活开发」。」 |
|
|
588
|
+
| 部署没接入函数计算节点 / 调度服务 | `FUNCTION_RUNTIME_UNAVAILABLE`(平台未开放,不是你写错了) |
|
|
589
|
+
|
|
590
|
+
### 开发环境激活 ≠ 生产发布
|
|
591
|
+
|
|
592
|
+
上面第 ④ 步只让云函数在**开发环境**运行。要让其他使用者在生产环境用上它:
|
|
593
|
+
|
|
594
|
+
- **appid 应用**:把版本「发布为线上版本」(控制台「版本管理」,或 `zleap-app release`)。带云资源的版本**第一次**发布时,平台以上传者身份自动准备生产(生产环境、生产授权、签名、激活、生产集合);上传者当前必须安装的就是这个版本,否则返回 40900(`UPLOADER_NOT_ON_VERSION`)。已经发布过的版本再次发布直接复用,不再要求。只用宿主能力的应用直接发布,不需要签名激活。
|
|
595
|
+
- **旧 `app_key` 应用**:继续在「版本与激活」里走发布候选 / 签名 / 激活。
|
|
596
|
+
|
|
597
|
+
`zleap-app prepare` 对两类应用都可用:**appid 应用**只准备开发环境(等价于上面的「开发环境:云函数激活」,不会碰生产),
|
|
598
|
+
准备完成后用 `zleap-app invoke <app_id> --capability <id> --environment dev` 测试;**旧 `app_key` 应用**只接受本人 private 包,一路准备到生产。
|
|
415
599
|
|
|
416
600
|
`logic_unit` 和 `builtin` provider 不进函数沙箱,**不需要**签名发布,授权后台能力即可执行。
|
|
417
601
|
|
|
@@ -634,16 +818,19 @@ const job = await zl.cloud.os.capabilities.invoke({
|
|
|
634
818
|
见示例应用 `zleap-web/examples/app-cloud/copilot-studio/`:`cloudfunctions/reviewContent` 读取草稿、计算摘要、带乐观锁写回;
|
|
635
819
|
`nightlyReview` 由定时触发器驱动;另有 `logic_unit` 与 `builtin`(`os.agent.complete`)两种 provider 的对照。
|
|
636
820
|
|
|
821
|
+
带云函数的可运行前端示例见 `zleap-app-kit/examples/moments`,声明与代码摘录见上文「最小可运行示例」。
|
|
822
|
+
|
|
637
823
|
## 常见问题
|
|
638
824
|
|
|
639
|
-
### 前端调用直接抛 `
|
|
825
|
+
### 前端调用直接抛 `BACKGROUND_CAPABILITY_UNAVAILABLE`
|
|
640
826
|
|
|
641
827
|
当前会话没有协商出 `app-execution/1`:这个安装在当前数据域里**没有任何已启用的后台能力**。
|
|
642
828
|
到应用窗口「权限」里勾选后台能力并保存,再重新加载应用。这不是函数写错了。
|
|
829
|
+
旧版 SDK 在同样情况下抛的是 `UNSUPPORTED_OPERATION`,看到旧错误码时处理方式相同。
|
|
643
830
|
|
|
644
831
|
### `FUNCTION_RELEASE_REQUIRED`
|
|
645
832
|
|
|
646
|
-
`managed_function`
|
|
833
|
+
`managed_function` 必须绑定**就绪且已激活**的发布。appid 应用到控制台「版本管理」页下方的「开发环境:云函数激活」完成激活,
|
|
647
834
|
然后重新打开应用窗口「权限」确认该能力下没有「尚未激活发布版本」的提示,再重新加载应用。
|
|
648
835
|
|
|
649
836
|
### `RUNTIME_GATEWAY_UNAVAILABLE` / 其他 `RUNTIME_GATEWAY_*`
|
|
@@ -675,6 +862,6 @@ const job = await zl.cloud.os.capabilities.invoke({
|
|
|
675
862
|
- [权限与授权](permissions.md) — 安装授权、Cloud 数据授权与后台能力授权
|
|
676
863
|
- [Manifest v2 配置](manifest.md) — 函数与 provider 的声明
|
|
677
864
|
- 云函数与后台能力(控制台)(文档站 /console/functions)
|
|
678
|
-
-
|
|
865
|
+
- 版本管理(文档站 /console/release)
|
|
679
866
|
- 日志(文档站 /console/logs)
|
|
680
867
|
- [状态与错误码](status-codes.md)
|
|
@@ -152,6 +152,10 @@
|
|
|
152
152
|
|
|
153
153
|
集合里还没有你的文档时显示「**这个集合还没有文档**」——「新建一条文档,或在应用里通过 Cloud SDK 写入后刷新。」
|
|
154
154
|
|
|
155
|
+
(截图:数据库文档视图,见文档站)
|
|
156
|
+
|
|
157
|
+
文档标签页:左侧集合列表,右侧文档表格、筛选、输入文档键直接打开、新建文档与备份入口。
|
|
158
|
+
|
|
155
159
|
### 筛选与排序
|
|
156
160
|
|
|
157
161
|
点「**筛选**」打开「**按条件筛选**」弹层:
|
|
@@ -281,6 +285,10 @@ JSON 视图提示「必须是 JSON 对象,字段需符合集合结构。」
|
|
|
281
285
|
表格列:「字段」「类型」「必填」(必填 / 可选)「约束」(选项值、「最多 {n} 字符」、日期字段「按毫秒时间戳存储」)。
|
|
282
286
|
底部「**查看 JSON Schema 原文**」展开服务端保存的完整定义。
|
|
283
287
|
|
|
288
|
+
(截图:集合结构标签页,见文档站)
|
|
289
|
+
|
|
290
|
+
结构标签页:明确写出「结构创建后不能修改」,下面按字段列出类型、必填与约束。
|
|
291
|
+
|
|
284
292
|
> **新版本改 Schema 不会更新已有集合**(注意)
|
|
285
293
|
>
|
|
286
294
|
> 集合一旦建好,后续版本 Manifest 里改了它的 Schema 也**不会**应用上去;私有 / 共享分区也不能改。
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
> **一句话流程**(提示)
|
|
9
9
|
>
|
|
10
10
|
> 控制台「创建应用」→ 复制 `appid` → 脚手架带上 `appid` 或写进 `manifest.json` → `npm run deploy` 上传 →
|
|
11
|
-
> (需要协作时)「团队」→「成员」里添加开发者
|
|
11
|
+
> (需要协作时)「团队」→「成员」里添加开发者 →(给全企业用时)「版本管理」里发布为线上版本(这一步只适用于 `appid` 应用)。
|
|
12
12
|
|
|
13
13
|
## 创建应用
|
|
14
14
|
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
|
|
21
21
|
打开的抽屉标题为「**创建应用**」,副标题「平台会生成 appid,你自动成为管理员」:
|
|
22
22
|
|
|
23
|
-
| 字段 | 说明
|
|
24
|
-
| ---------------- |
|
|
25
|
-
| 应用名称 | 必填,最多 64 个字
|
|
26
|
-
| 应用说明(可选) | 最多 2000 字
|
|
23
|
+
| 字段 | 说明 |
|
|
24
|
+
| ---------------- | ------------------ |
|
|
25
|
+
| 应用名称 | 必填,最多 64 个字 |
|
|
26
|
+
| 应用说明(可选) | 最多 2000 字 |
|
|
27
27
|
|
|
28
28
|
抽屉底部提示「创建后把 appid 写进 manifest 即可上传;名称和说明以后上传的 manifest 为准。」点「**创建**」完成。
|
|
29
29
|
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
刚创建、还没有上传过任何版本的应用,控制台不显示环境和开通清单,而是一张引导页:
|
|
39
39
|
「应用已创建,还没有上传过代码。按下面三步把第一个版本传上来。」
|
|
40
40
|
|
|
41
|
-
页面顶部是 `appid
|
|
41
|
+
页面顶部是 `appid` 芯片,旁边是「**复制**」按钮(无障碍名称是「复制 appid」,复制后短暂显示「已复制」)。三步是:
|
|
42
42
|
|
|
43
43
|
1. **新项目:用脚手架创建,自动带上 appid**
|
|
44
44
|
|
|
@@ -68,10 +68,16 @@
|
|
|
68
68
|
|
|
69
69
|
侧栏「**团队**」分组下的「**成员**」,页面说明「管理员和开发者都能上传、在控制台管理这个应用;只有管理员能增删成员。」
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
页面顶部是一条 appid 栏:`appid` 芯片、旁边的「**复制**」按钮,以及你在该应用里的**当前角色标签**(「管理员」或「开发者」)。
|
|
72
|
+
|
|
73
|
+
(截图:成员页,见文档站)
|
|
74
|
+
|
|
75
|
+
成员页:顶部是 appid + 复制 + 当前角色,下面是添加成员、成员行与角色、退出操作。
|
|
76
|
+
|
|
77
|
+
| 角色 | 能做什么 |
|
|
78
|
+
| ---------- | ----------------------------------------------------------------- |
|
|
79
|
+
| **管理员** | 管理成员与应用:添加、移除成员,修改角色;也能上传、调试与管理 |
|
|
80
|
+
| **开发者** | 上传、调试与管理:用同一个 `appid` 上传版本、在控制台管理这个应用 |
|
|
75
81
|
|
|
76
82
|
控制台的应用切换下拉列出**你是管理员或开发者的全部应用**。
|
|
77
83
|
|
|
@@ -80,17 +86,17 @@
|
|
|
80
86
|
在「**添加成员**」里输入对方的**登录名、手机号或邮箱**,选择角色(默认开发者),点「**添加**」。
|
|
81
87
|
输入框下方提示「管理员:管理成员与应用;开发者:上传、调试与管理。」
|
|
82
88
|
|
|
83
|
-
| 提示
|
|
84
|
-
|
|
|
85
|
-
| 没有找到这个用户
|
|
86
|
-
| 该账号匹配到多个用户,请改用登录名添加
|
|
87
|
-
| 该用户已经是成员
|
|
89
|
+
| 提示 | 含义 |
|
|
90
|
+
| -------------------------------------- | ------------------------------------------------------ |
|
|
91
|
+
| 没有找到这个用户 | 对方还没在本企业开通账号,或登录名 / 手机号 / 邮箱写错 |
|
|
92
|
+
| 该账号匹配到多个用户,请改用登录名添加 | 手机号或邮箱对应了多个账号 |
|
|
93
|
+
| 该用户已经是成员 | 需要调整角色请直接修改角色 |
|
|
88
94
|
|
|
89
95
|
### 修改角色、移除与退出
|
|
90
96
|
|
|
91
97
|
- **修改角色**(管理员):在成员行的角色下拉里切换「管理员 / 开发者」;
|
|
92
98
|
- **移除**(管理员):点成员行的「**移除**」,确认「移除 {名字}?——移除后对方不能再上传或管理这个应用,已上传的版本保留。」;
|
|
93
|
-
-
|
|
99
|
+
- **退出**(自己,且不是旧应用的上传者成员):在自己那一行点「**退出**」,确认「退出「{应用}」?——退出后你不能再上传或管理这个应用,需要管理员重新添加。」
|
|
94
100
|
|
|
95
101
|
应用**至少要保留一位管理员**。移除、退出或降级会让应用没有管理员时,操作被拒绝:
|
|
96
102
|
40900「应用至少要保留一位管理员」,处理方式是先把另一位成员设为管理员,再做这个操作。
|
|
@@ -99,21 +105,23 @@
|
|
|
99
105
|
|
|
100
106
|
### 旧应用(`app_key`)
|
|
101
107
|
|
|
102
|
-
用旧 `app_key`
|
|
108
|
+
用旧 `app_key` 命名空间上传的已有应用,**上传过应用包的人视为管理员**,在成员列表里显示为管理员,
|
|
109
|
+
但那一行只有角色标签:没有角色下拉,也没有「移除」/「退出」按钮,不能在这里修改角色或退出。
|
|
110
|
+
(其他通过邀请加入的成员仍按上面的规则管理。)
|
|
103
111
|
|
|
104
112
|
## 上传时的成员校验
|
|
105
113
|
|
|
106
114
|
用 `appid` 的应用,谁能上传由成员决定,Manifest 里的 `publisher` 不再起作用:
|
|
107
115
|
|
|
108
|
-
| 情况
|
|
109
|
-
|
|
|
110
|
-
| `appid` 还没创建
|
|
111
|
-
| 上传者不是应用成员
|
|
112
|
-
| 管理员或开发者上传
|
|
116
|
+
| 情况 | 结果 |
|
|
117
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
118
|
+
| `appid` 还没创建 | 40400「appid 不存在」(`detail.reason` 为 `APP_NOT_FOUND`):先在云开发控制台「创建应用」 |
|
|
119
|
+
| 上传者不是应用成员 | 40300「你不是这个应用的开发者」(`detail.reason` 为 `NOT_APP_MEMBER`):请联系应用管理员在云开发控制台「成员」里把你加为开发者 |
|
|
120
|
+
| 管理员或开发者上传 | 正常上传 |
|
|
113
121
|
|
|
114
122
|
## 成员上传的版本
|
|
115
123
|
|
|
116
|
-
|
|
124
|
+
用 `appid` 的应用里,每个成员上传的都是一个**开发版本**,全部列在侧栏「发布」组的「版本管理(文档站 /console/release#版本管理)」里,标出上传者和上传时间:
|
|
117
125
|
|
|
118
126
|
- 任何成员(管理员或开发者)都可以把其中一个「发布为线上版本」,或「回退到此版本」;
|
|
119
127
|
- 「我也用这个」把你本人的安装切到任一成员上传的版本,包括同事还没发布的版本(方便联调);
|
|
@@ -121,16 +129,22 @@
|
|
|
121
129
|
|
|
122
130
|
## 用户授权
|
|
123
131
|
|
|
124
|
-
|
|
132
|
+
侧栏「团队」组的「**用户授权**」页(appid 应用)统计使用者通过[授权登录](app-runtime.md#授权登录)给本应用的个人信息。
|
|
133
|
+
页面说明是「使用者通过授权登录(zl.auth.authorize)给本应用的个人信息:只统计人数,不展示具体用户和手机号。」
|
|
125
134
|
|
|
126
|
-
| 数字
|
|
127
|
-
|
|
|
128
|
-
| 已授权人数
|
|
129
|
-
| 头像和昵称 / 手机号 / 邮箱 | 各项同意人数
|
|
130
|
-
| 已撤销
|
|
135
|
+
| 数字 | 含义 |
|
|
136
|
+
| -------------------------- | ------------------------------------------ |
|
|
137
|
+
| 已授权人数 | 当前同意过至少一项的使用者人数 |
|
|
138
|
+
| 头像和昵称 / 手机号 / 邮箱 | 各项同意人数 |
|
|
139
|
+
| 已撤销 | 同意过、后来在「已授权信息」里撤销了的人数 |
|
|
131
140
|
|
|
132
141
|
只有人数,**看不到具体用户和手机号**——具体信息只在使用者本人同意后交给应用运行时。每次同意、拒绝、撤销和云函数读取都会记审计日志。
|
|
133
|
-
|
|
142
|
+
还没有人授权时显示「**还没有人授权**」,下方说明:应用调用 `zl.auth.authorize` 并且用户点了「允许」后,这里会显示人数。
|
|
143
|
+
接口:`GET /api/v1/app-cloud/apps/{app_id}/auth-stats`(成员)。
|
|
144
|
+
|
|
145
|
+
(截图:用户授权页,见文档站)
|
|
146
|
+
|
|
147
|
+
用户授权页:只展示人数统计,不展示具体用户(图为还没有人授权时的空状态)。
|
|
134
148
|
|
|
135
149
|
## 目前的限制
|
|
136
150
|
|