# 本地开发与测试企业

本文介绍怎样用命令行 `k3` 在自己的机器上开发应用，并接到平台上的测试企业里调试：安装 `k3`、建测试企业、拿测试凭证、让本机的进程以测试企业里的应用身份运行、收发事件、查日志，以及有自己数据的应用和目录里的应用该怎么办。

## 安装 k3

`k3` 由平台自己提供下载，地址在开发者门户的「工具与 SDK」页，也可以直接用下面的地址。

Linux 与 macOS 用一条命令安装。脚本按本机的系统和 CPU 下载对应的版本，核对平台公布的 SHA-256，不一致就不安装；装到 `~/.local/bin`，用环境变量 `K3_INSTALL_DIR` 可以改：

```bash
curl -fsSL "https://dev.${DOMAIN}/dl/k3/install.sh" | sh
```

Windows 没有安装脚本，直接下载 `https://dev.${DOMAIN}/dl/k3/k3-windows-amd64.exe`，在 PowerShell 里与 `https://dev.${DOMAIN}/dl/k3/SHA256SUMS` 里的值核对，改名为 `k3.exe` 放进 `PATH` 里的目录：

```text
(Get-FileHash k3-windows-amd64.exe -Algorithm SHA256).Hash.ToLower() -eq "<SHA256SUMS 里这个文件的值>"
```

平台提供的构建有 Linux（x86-64、ARM64）、macOS（x86-64、ARM64）和 Windows（x86-64）。`GET https://dev.${DOMAIN}/dl/k3/` 以 JSON 列出版本、文件、大小和 SHA-256。装好后 `k3 version` 打印命令行版本和它支持的清单协议。

## 用 API 凭证登录

`k3` 调开发者平台的命令（凭证、测试企业、日志、发布）用开发者组织的 API 凭证登录。组织管理员在门户的「API 凭证」页创建一个，密钥只显示一次。第一把凭证要管理员先登录一次门户；不用浏览器怎样建，见 [认证与凭证](/developers/guide/auth) 的「建第一把 API 凭证」：

```bash
k3 login --url "https://dev.${DOMAIN}" --client-id "${K3_CLIENT_ID}" --secret-stdin
k3 whoami
```

凭证保存在用户配置目录的 `k3/credentials.json`（只有你能读；`K3_CONFIG_DIR` 可以改目录）。在 CI 里不用登录，设置 `K3_URL`、`K3_CLIENT_ID`、`K3_CLIENT_SECRET` 三个环境变量即可，它们优先于 `k3 login` 保存的凭证。下面每个命令都可以加 `--json`，打印平台原样的回答。

## 建一个测试企业

测试企业是你的开发者组织自己的租户：数据独立，不计费，不出现在应用市场的统计里，可以带示例数据、测试员工和测试客户，草稿版本不上架也能试装进去。

```bash
k3 test-enterprise create "分销联调" --email sbx-admin@example.com --sample general
k3 test-enterprise list
```

- `--email` 是测试企业的管理员，必填。它必须是平台上还没有账号的邮箱：你登录开发者门户用的邮箱已经是平台的账号，用它会被拒绝，返回 HTTP 422 `adminEmail "…" is already in use`。换一个没在平台上用过的地址。
- 回答里有登录地址（`signInUrl`）和一次性的激活码（`initCode`），激活码只显示这一次。管理员在登录页填这个邮箱，再用激活码设置密码：平台自己的登录页上这一步是「输入激活码 / 验证码，设置密码」；部署用身份服务自己的登录页时，这一页的标题是「E-Mail Verification」，在验证码（Code）一栏填激活码，再填新密码。之后登录页可能请你设置双因素认证（「2-Factor Setup」），没有强制要求时可以跳过。
- `--sample general` 带 10 个测试客户和一张「线索」多维表；不写时是空的。
- 每个开发者组织最多同时有 3 个测试企业。每个存活 30 天，可以在门户里延期，从创建起最多 90 天。到期后先停用，7 天后连同数据删除。

门户的「测试企业」页做同样的事，还能延期和删除。

### 建一个草稿版本，不提交审核

`k3 publish` 建好草稿后总会提交审核。只要一个草稿（例如先试装进测试企业）时，直接调开发者平台的接口：

```bash
k3 api POST /api/dev/apps --data '{"id": "my-app", "listing": {"name": "我的应用"}}'
k3 api POST /api/dev/apps/my-app/versions --data @version.json
k3 status my-app
```

- 第一行建应用，应用已经有了就跳过。
- `version.json` 是 `{"manifest": {…}, "notes": "…"}`，`manifest` 是 `manifest.json` 的内容，版本号取清单的 `version`。带自己表结构的版本还可以写 `"migrations": [{"name": "0001_init.sql", "sql": "…"}]`。回答 HTTP 201，`k3 status` 里这个版本是 `draft`。
- 同一个版本号再 `POST` 返回 409。改草稿用 `PUT /api/dev/apps/my-app/versions/0.1.0`，正文同上。
- 要提交审核时调 `POST /api/dev/apps/my-app/versions/0.1.0/submit`，或者运行 `k3 publish`。

### 试装应用

把应用的一个版本装进测试企业：在门户应用详情的「测试企业」页签点「试装」，或者用接口：

```bash
k3 api POST /api/dev/sandboxes/${SANDBOX_ID}/install --data '{"app": "my-app", "version": "0.1.0"}'
```

装进去的是这个版本的一个副本（版本号带 `-sbx`），不能再提交上架；草稿也可以装。商店应用的后端在你自己的测试企业里，试装就等于同意它申请的范围。带托管进程（清单写了 `runtime`）的版本，只有已经发布的那个版本能装进测试企业，原样安装；它的草稿不能试装。

回答形如 `{"app": "my-app", "version": "0.1.0-sbx1", "from": "0.1.0", "scopes": []}`：`version` 是装进去的副本，`from` 是你给的版本。`scopes` 只列商店应用（纯后端）的版本文档里申请、试装时一并同意的开放接口范围；带清单的应用这里总是 `[]`。清单 `uses` 里的范围不在这个列表里：应用的令牌在装了它的企业里自带这些范围。要核对，用测试凭证换一个令牌，看 `GET /open/v1/whoami` 回答里的 `scopes`。

### 以测试员工身份进入

每个测试企业一开始有三个测试员工（测试店长、测试前台、测试员工），可以在门户里添加，测试员工最多 20 个。用其中一个人的身份打开测试企业的工作台，就能像企业里的员工一样使用装好的应用：

```bash
k3 test-enterprise users ${SANDBOX_ID}
k3 test-enterprise signin ${SANDBOX_ID} ${USER_ID}
```

`users` 列出测试员工和（带示例数据时的）测试客户。`signin` 生成一个一次性的登录地址并打开浏览器，10 分钟内有效，用过一次就失效；`--no-open` 只打印地址。门户的「测试企业」页上每个测试员工旁边也有「以此身份打开」。

这个地址先到平台登录服务的 `/api/login/test-start`：它种下一个只发给登录接口、10 分钟有效的一次性 cookie，再转到测试企业的工作台。工作台去登录时，平台的登录页认出这个 cookie，直接以这名测试员工登录，不用密码。

自下一个平台版本起，部署用身份服务自己的登录页时也是这样。在这之前，这样的部署（登录页是英文的「Select Account」「Login」）不认这个 cookie，测试员工的地址进不去；请改用测试企业的管理员（激活码设置的密码）登录工作台。

#### 测试员工与应用的权限点

测试员工是测试企业的普通成员，不是管理员。应用用权限点（`app.<应用 ID>.…`）决定谁能做什么时，测试员工不一定有这些点：

- 测试企业的管理员（建测试企业时 `--email` 的那个人）在每个权限点上都放行，数据范围是全部。用 `appkit` 写的应用也是这样。
- 测试企业没有打开「精细权限」时（默认），普通成员有没有一个点，由应用自己的默认规则决定（`appkit.Authz` 的 `Defaults` 里写给 `tenant.member` 的点），平台不另外给。
- 打开「精细权限」以后，普通成员只有分给他的业务角色授予的点。

要让测试员工有应用的权限点：以测试企业的管理员登录工作台，在「我的组织 → 业务角色」打开「精细权限」，建一个授予这些点的角色（装着的应用声明的点在可选的点里），再把角色分给测试员工。

### 不用浏览器以测试员工身份登录

没有浏览器时，自己走一遍工作台的登录（OpenID Connect 授权码流程，PKCE），只是登录那一步换成测试员工的 cookie。授权码流程的每一步见 [认证与凭证](/developers/guide/auth) 的「不用浏览器登录门户」，不同的是：

1. 用同一个 cookie 文件打开 `signin` 给的地址，拿到 `k3_test_signin` cookie：

```bash
URL=$(k3 test-enterprise signin ${SANDBOX_ID} ${USER_ID} --no-open)
curl -sS -c jar.txt -b jar.txt -o /dev/null "$URL"
```

2. 授权码流程从测试企业的工作台地址开始：`GET https://${SANDBOX_ID}.${DOMAIN}/api/portal-config` 给出工作台的 `clientId` 与 `scope`，`redirect_uri` 是 `https://${SANDBOX_ID}.${DOMAIN}/auth/callback`。
3. 跳到登录页、拿到 `authRequest` 以后，不填密码，而是带着同一个 cookie 文件调 `POST /api/login/test-signin`，正文 `{"authRequest": "<ID>"}`（`Content-Type: application/json`）。回答和密码登录的一样：`{"step": "done", "redirect": "…"}` 时，跟着 `redirect` 走到回调地址，用回调里的 `code` 换令牌；`{"step": "passkey_setup", "session": …}` 时，先用 `POST /api/login/passkey/skip` 跳过设置通行密钥，正文和别的 `step` 见 [认证与凭证](/developers/guide/auth) 的「不用浏览器登录门户」第 3 步。跳过这一步不再要 cookie。

cookie 只能用一次，10 分钟内有效；不对或过期时返回 HTTP 403 `test_signin_invalid`，`authRequest` 过期时返回 HTTP 410 `expired`。再运行一次 `signin` 拿新的地址。这条路依赖上面说的「自下一个平台版本起」。

## 管理应用的测试凭证

应用调 `/open/v1` 与 `/app/v1` 用应用密钥。测试环境的密钥（`kwo_test_` 开头）只能用在你自己组织的测试企业：

```bash
k3 credentials create my-app --env test --name laptop
k3 credentials list my-app --env test
k3 credentials rotate my-app ${CREDENTIAL_ID} --overlap-days 7
k3 credentials revoke my-app ${CREDENTIAL_ID}
```

`create` 把密钥打印一次，形如 `export RAISE_CLIENT_ID=… RAISE_CLIENT_SECRET=…`，平台不能再显示它。可选 `--ttl-days`（几天后失效）、`--rps`（每秒请求数）、`--ip`（只接受这些来源地址，可重复）。正式环境（`--env live`）的密钥要组织管理员创建。`revoke` 先确认，`--yes` 跳过确认。

## 让本机的进程接到测试企业

`k3 dev --test-enterprise` 让你在本机运行的进程以「装在测试企业里的这个应用」的身份调用平台：

```bash
k3 dev --test-enterprise ${SANDBOX_ID} --app my-app -- go run ./api
```

- 应用要先装在这个测试企业里（见「试装应用」），否则命令会提示先装。
- `k3` 为这次运行建一把测试凭证，命令结束时吊销；`--keep-credential` 保留它。已经设置了 `RAISE_CLIENT_ID` 与 `RAISE_CLIENT_SECRET` 时用你的那把，不另建。
- 子进程拿到四个环境变量：`RAISE_BASE_URL`（`/open/v1` 的地址）、`RAISE_CLIENT_ID`、`RAISE_CLIENT_SECRET`、`RAISE_TENANT`（测试企业 ID）。在代码里读它们交给 SDK，例如 `raise.New(os.Getenv("RAISE_CLIENT_ID"), os.Getenv("RAISE_CLIENT_SECRET"), raise.WithBaseURL(os.Getenv("RAISE_BASE_URL")), raise.WithTenant(os.Getenv("RAISE_TENANT")))`。应用读别的变量名时，在命令里自己转一下，见下文「平台根地址：RAISE_BASE_URL 与 K3_PLATFORM_URL」。
- 运行期间，`k3` 每 2 秒把这个测试企业里应用的新调用（方法、路径、状态、错误码、请求 ID）和新出现或变多的错误打印出来。子进程的退出码就是 `k3` 的退出码。

不加 `--endpoint` 时，只有本机进程调平台这一个方向。要让平台调你的进程（草稿服务的操作、服务运行、提供方、审批），加上平台够得着的地址：

```bash
k3 dev --test-enterprise ${SANDBOX_ID} --app my-app --endpoint https://my-tunnel.example.com -- go run ./api
```

运行期间平台把这个测试企业里对这个应用的这些调用发到 `--endpoint`，不再发到清单声明的地址或平台托管的进程；别的企业不受影响。平台接上的应用的站点动作和数据事件也在内，见下文「站点动作在测试企业里能试到哪一步」与「平台的数据事件」。这个租约 90 秒有效，`k3` 每 30 秒续一次，命令结束时交还，交还失败也会在 90 秒内自己结束。地址必须是 `https`、不能是内网地址；只有部署允许时（例如本机的开发栈）才可以用 `http` 与内网地址，例如 `http://host.docker.internal:8080`。平台有多个副本时，租约只在接到它的那个副本上生效，部分调用可能仍发到原来的地址。

租约期间，这个测试企业的工作台也给应用的页面发页面会话，和平台托管的应用一样，虽然平台不运行你的进程。页面就是试装版本清单里的 `web.url`，由你自己提供，可以是本机的开发服务器（`web.url` 能写什么，见下一节）。前端经 Bridge 拿到会话令牌，请求你自己的后端时带上它；后端用平台的 JWKS 验签，再原样作为 `Authorization: Bearer` 转给 `/app/v1`（见 [应用接口](/developers/guide/app-api)「使用哪种令牌」）。没有租约时，不由平台托管的应用拿不到页面会话（`POST /api/apps/{id}/token` 返回 404）。

### 开发版本的 opsApi 与 web.url

开发版本是你自己运行、只试装进测试企业的草稿。它不写 `runtime`，也不写 `data.migrations`：

- 写了 `runtime` 的草稿不能试装进测试企业（试装返回 HTTP 422，说明是 `a version that runs a hosted process (runtime) cannot be tried in a sandbox yet …`），镜像写什么都一样。只有应用已经发布的那个版本可以带着 `runtime` 装进去。
- `runtime` 一旦写了，`runtime.image` 必须按摘要固定（`名字@sha256:<64 位十六进制>`），否则建草稿就返回 HTTP 422。
- 带 `data.migrations` 而没有 `runtime` 的草稿可以建、可以试装，但平台不给它建模式，提交审核时自动检查 `migrations.runtime` 不通过。

不写 `runtime` 的版本由你自己运行，平台按清单里的两个地址找它：

- `opsApi`：平台调应用的基础地址，写完整的地址，路径是你的服务提供 op 的前缀，例如 `https://my-app.example.com/k3`。平台在它上面调 `POST {opsApi}/ops/{op}/dry-run` 与 `POST {opsApi}/ops/{op}/execute`（草稿服务的预演与执行），`POST {opsApi}/approvals/prepare` 与 `POST {opsApi}/approvals/decided`（审批）。服务运行发到服务的 `endpoint`（一个路径），拼在 `opsApi` 的源站上。
- `web.url`：工作台在 iframe 里打开的页面地址，导航的 `route` 相对于它。清单校验只收 `https://…`、`http://localhost…` 与 `http://127.0.0.1…`；别的 `http` 地址（例如 `http://host.docker.internal:5173`）建草稿时就返回 HTTP 422 `web.url must be https (http only for localhost)`。`http://localhost` 与本机地址只适合试装进测试企业的草稿：提交审核时，自动检查 `web.url`（发布的应用必须是 `https`）与 `web.address`（不能指向本机或内网）不通过。

用 `--endpoint` 时，平台把这些调用的源站（协议、主机、端口）换成 `--endpoint` 的，路径和查询不变。例如 `opsApi` 是 `https://my-app.example.com/k3`、`--endpoint` 是 `http://host.docker.internal:8080` 时，预演 op `add_lead` 发到 `POST http://host.docker.internal:8080/k3/ops/add_lead/dry-run`。所以开发版本的 `opsApi` 不必真的连得上：主机写你以后正式运行的地址，路径要和本机进程提供的一致。`--endpoint` 不改 `web.url`，见下文「应用的页面在测试企业的工作台里」。

每次调用都带平台签的断言，应用用平台的公钥校验它，见下一节。

### 平台根地址：RAISE_BASE_URL 与 K3_PLATFORM_URL

`RAISE_BASE_URL` 是开放接口的地址，以 `/open/v1` 结尾，例如 `https://api.${DOMAIN}/open/v1`，交给 `raise.New`（`raise.WithBaseURL`）用。

用 `appkit` 写的应用（`k3 app init` 生成的项目、目录应用）要的是平台的根地址：`appkit.Tokens` 的 `Base` 加上 `/open/v1/oauth/token` 换令牌，`/app/v1` 和平台的公钥 `/.well-known/k3-jwks.json` 也从这个根地址调。平台托管的进程从 `K3_PLATFORM_URL` 读到它（见 [应用接口](/developers/guide/app-api) 的「托管进程的环境」）。在本机，根地址就是 `RAISE_BASE_URL` 去掉末尾的 `/open/v1`，例如 `https://api.${DOMAIN}`。

应用读托管时的变量名时，在命令里转一下。单引号让这些变量在 `k3` 启动的子进程里展开，而不是在你自己的 shell 里：

```bash
k3 dev --test-enterprise ${SANDBOX_ID} --app my-app --endpoint http://host.docker.internal:8080 -- \
  sh -c 'K3_PLATFORM_URL="${RAISE_BASE_URL%/open/v1}" K3_APP_CLIENT_ID="$RAISE_CLIENT_ID" K3_APP_CLIENT_SECRET="$RAISE_CLIENT_SECRET" exec go run ./api'
```

有的目录应用还读 `K3_APP_ADDR`（监听地址）和 `KWORK_BASE_DOMAIN`（部署的基础域名）。这两个名字来自它们还在平台里时的运行方式（平台自己的 `kwork app-run`），不属于托管进程的环境：平台托管运行的进程拿到的是 `PORT`（端口）和 `K3_BASE_DOMAIN`（基础域名），`k3 dev` 也不设置它们。应用读它们时，在命令里自己设置，例如 `K3_APP_ADDR=:8080 KWORK_BASE_DOMAIN=${DOMAIN}`；要由平台托管运行，让应用改读 `PORT` 与 `K3_BASE_DOMAIN`。

### 应用的页面在测试企业的工作台里

以测试员工身份进入测试企业后，工作台的导航里有装着的应用。打开它时，工作台在 iframe 里加载装着的那个版本清单的 `web.url`（加上导航的 `route`）。iframe 由你自己的浏览器加载，所以开发版本的 `web.url` 可以是本机的开发服务器，例如 `http://localhost:5173` 或 `https://localhost:5173`。工作台不限制 iframe 地址的协议；工作台本身是 `https`，Chrome 与 Firefox 在 `https` 的页面里也加载 `http://localhost` 与 `http://127.0.0.1`。`k3 dev --endpoint` 不改页面的地址；要换 `web.url`，改草稿（`PUT /api/dev/apps/<应用 ID>/versions/<版本>`）后再试装一次。

页面里的人：工作台和 App Bridge 替页面调 `POST /api/apps/{id}/token`，拿到正在用页面的这个人的页面会话（10 分钟有效）。页面发给你后端的请求带着它（请求头 `K3-App-Token`），后端原样转给平台，见 [应用接口](/developers/guide/app-api) 的「使用哪种令牌」。

自下一个平台版本起，不由平台托管的应用（清单没有 `runtime`）在测试企业里也能拿到页面会话。在这之前，这个接口只给平台托管的应用发令牌，别的应用得到 HTTP 404 `no hosted App with this id`，页面拿不到会话。

### 站点动作在测试企业里能试到哪一步

站点动作是企业的站点（例如推广页）上的访客调用的应用接口，在清单的 `siteActions` 与 `siteActionSpecs` 里声明。访客的浏览器请求站点上的 `/_k/v1/<动作名>`，平台检查方法、来源、频率和访客以后，以 `POST {应用的地址}/k3/site-actions/<动作名>` 转给应用，带平台签的断言。

- **只对平台接上的应用。** 平台从应用已经发布的那个版本的清单里取站点动作，而且只接清单写了 `runtime` 的应用和部署自己运行的目录应用。你自己运行的应用（清单没有 `runtime`）的站点动作平台不接，访客得到 HTTP 404；`--endpoint` 不改变这一点。试装的草稿里新加的动作也不算，要等它所在的版本发布。
- **租约也管站点动作。** 对平台接上的应用，`k3 dev --test-enterprise … --endpoint …` 运行期间，这个测试企业的站点上的动作发到 `--endpoint`（路径 `/k3/site-actions/<动作名>`）。别的企业不受影响。
- **站点。** 测试企业建好时没有装站点应用（`site`）。测试企业的管理员在工作台里装上它（和别的企业一样，`PUT /api/admin/apps/site/installation`），再建站点、发布；站点发布的版本要在它的清单里列出这个动作。站点的预览里不执行应用的站点动作（HTTP 409「预览里不会真的提交或下单，发布后在线上试」），要在发布后的站点上试。
- **访客。** `visitor` 不写或是 `make` 的动作，任何浏览器都能调。`visitor` 是 `identified` 的动作要访客先在站点上识别身份（微信、短信验证码或小程序登录），应用读 `GET /app/v1/site-visitors/{site}/{visitor}` 得到客户和是否已验证。平台没有给测试企业的测试验证码或测试访客，例如短信验证码要测试企业自己的短信账号和站点设置里的短信模板。所以要已识别访客的站点动作，现在在测试企业里还试不了。

### 有自己数据的应用

清单写了 `data.migrations` 的版本，平台给应用建自己的模式 `app_<id>` 与同名角色。这个数据库的连接只交给平台运行的进程（环境变量 `K3_APP_DATABASE_URL`），开发者拿不到，平台之外的进程也拿不到。所以：

- 带 `data.migrations` 的版本必须同时声明 `runtime.image`，只能托管运行；没写时，提交版本的自动检查 `migrations.runtime` 不通过。
- 带托管进程的草稿不能试装进测试企业，只有发布了的版本能装。

在测试企业里调试这样的应用，有两条路：

1. **托管运行**。用 `k3 publish` 提交版本（首个版本放在一个套件里提交，见 `k3 suite`），审核通过并发布后，把发布的版本试装进测试企业。平台运行进程并给它数据库连接。调试看门户的「日志」与「错误」，或 `k3 logs`；平台不显示托管进程自己的输出（里面可能有租户数据），需要的记录请应用自己写。
2. **本机进程加自己的数据库**。测试企业里装着一个版本以后，用上面的 `k3 dev --test-enterprise … --endpoint …` 把这个测试企业里平台对应用的调用改发给本机的新代码。平台不给本机进程数据库连接：在本机起一个你自己的数据库，照下一节建好平台会准备的部分，再执行 `data.migrations`，把连接交给应用自己读的配置（例如应用读 `K3_APP_DATABASE_URL` 时，在命令里设置它）。装进测试企业的版本可以是已发布的托管版本；还没有发布过的应用，可以先建一个不写 `data.migrations` 与 `runtime` 的开发版本草稿（见「建一个草稿版本，不提交审核」）试装，`opsApi` 与 `web.url` 照「开发版本的 opsApi 与 web.url」填。

### 在本机准备应用的数据库

平台不只是给应用一个空模式。它以角色 `app_<id>`（ID 里的 `-` 换成 `_`）执行迁移，在这之前已经建好：

- 角色 `app_<id>`：不能登录，不绕过行级安全，`search_path` 是自己的模式。
- 模式 `app_<id>`，归这个角色所有，别人没有权限。
- 模式 `platform` 里的函数 `platform.tenant_visible(text)`，行级安全策略用它：值等于会话设置 `kwork.tenant` 时可见，`kwork.scope` 是 `system` 时都可见。`appkit` 在每个事务开始时设置这两个值。

本机建这些有两种办法，按应用选一种，不要两种都做：

- **应用仓库带着建库的测试夹具时**（目录应用就是这样，例如 `raise-distribution` 的 `internal/distributionapp/testdata/platform.sql`），只用夹具：以超级用户执行它，再照应用仓库的说明往下做。下面的语句会和夹具建过的对象重复，PostgreSQL 答 `already exists`。
- **没有夹具时**，以超级用户执行下面的语句，再执行 `data.migrations`。

库名平台不管：平台上应用的模式在平台自己的数据库里，本机用哪个库都行，连接串里写同一个名字即可（下面的例子是 `dev`）。应用的测试或夹具要求某个库名时，照它们的。

没有夹具时，以超级用户建好这些（下面以应用 `my-app` 为例）：

```sql
CREATE ROLE app_my_app LOGIN PASSWORD 'dev-only' NOSUPERUSER NOBYPASSRLS;
ALTER ROLE app_my_app SET search_path = app_my_app;
CREATE SCHEMA app_my_app AUTHORIZATION app_my_app;
CREATE SCHEMA IF NOT EXISTS platform;
CREATE OR REPLACE FUNCTION platform.tenant_visible(t text) RETURNS boolean LANGUAGE sql STABLE AS $$
  SELECT t = current_setting('kwork.tenant', true) OR current_setting('kwork.scope', true) = 'system'
$$;
GRANT USAGE ON SCHEMA platform TO app_my_app;
GRANT EXECUTE ON FUNCTION platform.tenant_visible(text) TO app_my_app;
```

再以这个角色依次执行 `data.migrations` 里的文件，例如 `psql "postgres://app_my_app:dev-only@localhost:5432/dev" -f migrations/0001_init.sql`。

- 为了在本机直接连，这里给了角色 `LOGIN` 和密码；平台上它不能登录，平台运行的进程拿到的是平台发的短期账号。
- 迁移给平台的运行时角色 `kwork_app` 授权时，本机也先建这个角色：`CREATE ROLE kwork_app NOLOGIN;`。
- 目录应用（原来的一方应用，例如分销）的表最早是平台自己的迁移建的（分销的模式 `app_distribution` 也是），只执行 `data.migrations` 建不全，所以要用上面说的夹具。
- `k3` 没有建库的命令。

## 接收和触发事件

`k3 listen` 把平台在你的测试企业里发给应用的事件实时传到本机，并按原样（正文和签名头不变）转发给本机运行的应用：

```bash
k3 listen --app my-app --forward-to http://localhost:8080/k3/events
```

- 不写 `--app` 时用当前目录 `manifest.json` 里的 ID；不写 `--forward-to` 时只打印事件。
- `--events` 只要这些事件类型（逗号分隔），`--tenant` 只要这个测试企业的事件。
- 开始时打印这次会话的签名密钥（`whsec_…`），转发的副本用它签名；你的某个订阅本来就要的事件，用那个订阅的密钥签名。用 `--secret` 或环境变量 `K3_LISTEN_SECRET` 固定这个密钥。断线后自动重连。
- 正式企业的事件从不会传过来。

`k3 trigger` 让平台在测试企业里发一个示例事件，走真实事件的同一条路，所以应用的订阅和正在运行的 `k3 listen` 都会收到：

```bash
k3 trigger customer.created --app my-app --tenant ${SANDBOX_ID}
k3 trigger customer.created --data '{"customer": "cus_123"}'
```

- 类型必须是平台会发给这个应用的事件：业务事件（见文档站的「事件」页）、生命周期事件（`app.installed`、`app.upgraded`、`app.uninstalled`、`app.scopes_changed`），以及应用版本在 `events.receives` 里声明了的数据事件（`tenant.deleted`、`site.deleted`、`customer.merged`、`app.uninstalled`）。看的是装在这个测试企业里的版本，没装时看最新的版本。写错或没声明时，错误里列出这个版本可用的类型；`k3 trigger --help` 也写了这条规则。
- 数据事件只给应用发一次测试投递：`POST /k3/events/{kind}`，发到平台运行的应用进程或 `k3 dev --endpoint` 的地址，正文是 `{"tenant", "subject", "test": true}`。平台什么都不删、不合并、不卸载。`tenant.deleted` 的 subject 是这个测试企业，`app.uninstalled` 的是应用自己；`site.deleted` 和 `customer.merged` 可以用 `--data '{"subject": "…"}'` 指定（后者写成 `<被合并的客户>><保留的客户>`）。声明了 `app.uninstalled` 的应用，生命周期事件和数据事件都会收到。
- 业务事件 `customer.created`、`customer.updated`：应用要装在这个测试企业里，并且有 `customer:read` 范围，否则事件照样发出，但不发给这个应用，回答里的 `warning` 说明原因。
- 不写 `--data` 时按事件的结构生成一份示例数据；`--data-file` 从文件读。业务事件的数据必须有 `customer`。
- 组织只有一个测试企业时可以不写 `--tenant`。

签名的校验见 [Webhooks 与事件订阅](/developers/guide/webhooks)。

### 平台的数据事件

清单 `events.receives` 里的平台数据事件（`tenant.deleted`、`site.deleted`、`customer.merged`、`app.uninstalled`，见 [应用接口](/developers/guide/app-api) 的「平台的数据事件」）不经过 `k3 listen`；`k3 trigger` 能给声明了的种类发一次测试投递（见上一节）。真实的数据事件，平台以 `POST {应用的地址}/k3/events/{种类}` 送给应用：

- 只送给平台接上的应用，和站点动作一样：清单写了 `runtime` 的应用和部署自己运行的目录应用，要收的种类取自已经发布的那个版本的清单。你自己运行的应用（清单没有 `runtime`）收不到。
- 对接上的应用，`k3 dev --test-enterprise … --endpoint …` 运行期间，这个测试企业的数据事件发到 `--endpoint`。
- 要在测试企业里收到一个真实的，就在测试企业里真的做这件事：删掉一个站点（`site.deleted`），合并两位客户（`customer.merged`），卸载应用（`app.uninstalled`），删除测试企业（`tenant.deleted`）。
- 应用处理程序要能重复执行：失败后平台会按退避重试，同一个事件可能来不止一次。

## 查看调用日志与错误

平台记录应用最近 7 天的调用。在开发者门户里，打开应用详情的「日志」页签：

- 按环境（测试、正式）和状态类别（全部、2xx、4xx、5xx）筛选，最新的在上面，「加载更多」往前翻。
- 每行是时间、请求（方法与路径）、状态、错误码、耗时、请求 ID；测试环境还写出是哪个测试企业，正式环境不写企业。点一行看它的元数据。
- 4xx 是调用方的错误，不进「错误」页签；5xx 可以跳到「错误」页签里同一类错误。「错误」页签列出应用的错误分组（已脱敏）。
- 平台调应用（方向 `out`：草稿的预演与执行、服务运行）被应用拒绝或失败时，错误码一栏是失败类别（如 `http_4xx`），点开那一行还能看到应用自己给出的原因：应用回答的 JSON 错误里的 code 和 message（`{"error": {"code", "message"}}`、`{"error": "…", "code": "…"}` 或操作的 `issues` 第一条），和错误分组一样脱敏（值换成 `<str>`、`<n>`、`<email>` 这类占位），最多 300 字节。回答里别的内容平台不读。

命令行看的是同一份记录，还能按方向和测试企业筛：

```bash
k3 logs my-app --env test --status 4xx
k3 logs my-app --env test --tenant ${SANDBOX_ID} --follow
k3 logs my-app --errors --days 7
k3 logs my-app --errors --fingerprint ${FINGERPRINT}
```

- `--env test|live`、`--status 2xx|4xx|5xx`、`--direction in|out`（`in` 是应用调平台，`out` 是平台调应用）、`--tenant`（只能和 `--env test` 一起用）、`--limit`（先显示多少条，默认 50，最多 100）。
- `--follow` 每 2 秒追加新的调用，直到 Ctrl-C。
- `--errors` 列出错误分组，`--days` 往回看 1 到 14 天；`--fingerprint` 列出一组错误的每一次。
- 应用给出的原因显示在那一行下面，以 `↳` 开头；`--json` 里是 `errorMessage`。

按请求 ID 查一次调用见 [错误码与请求 ID](/developers/guide/errors)。

### 4xx 的原因在哪里看

一行 4xx 的错误码有两种来源。应用调平台被拒绝时（方向 `in`），它是平台错误信封里的 `code`，例如 `not_installed`，含义见 [错误码与请求 ID](/developers/guide/errors)。平台调应用、应用回了 4xx 时（方向 `out`），它是 `http_4xx`，原因在应用的回答里，例如 SDK 拒绝断言时的 `assertion audience mismatch`。

- 自下一个平台版本起，平台把应用回答里的原因写进这一行：门户里点开这一行看它的元数据，`k3 logs <应用 ID> --json` 里也有。
- 在这之前，日志里只有 `http_4xx`。原因要看调用方得到的回答：草稿的预演结果（`dryRun.issues` 里的 `message`）、审批申请人看到的说明，或者你自己进程的输出。用 `k3 dev` 时，进程的输出就在 `k3` 打印的调用旁边。

## 在终端部署与回退

托管应用可以用 `k3` 查看部署、分阶段放量，在终端或你们自己的 CI 里都行。调用的是更新日志里"比例灰度"那几个接口；命令加上了逐阶段跟随，以及流水线可以信任的退出码。

```
k3 deployments --app my-app                 # 最新的在前：状态、版本、谁、何时、范围
k3 deployment ${DEPLOYMENT_ID} --app my-app # 步骤、没有升级的租户按原因计数、监测所看的窗口
k3 rollout 1.2.0 --percent 10 --wait        # 把已批准的版本给 10% 的租户，并跟随它
k3 rollout advance 50 --wait                # 提高正在进行的灰度
k3 rollout abort                            # 租户回到原来的版本
k3 rollback                                 # 从现在在用的版本退回；先询问
```

- `--app` 默认取 `./manifest.json` 里的 id。`--json` 原样输出平台的回答。任何输出都不含租户 id，平台只给计数。
- `k3 rollout` 有 `--max-5xx`（比例，默认 0.02）和 `--min-requests`（默认 100）：在至少有后者那么多请求的窗口里，新版本的 5xx 占比超过前者，灰度就自动中止。
- `--wait` 跟随到平台的监测对这个阶段有了结论（新版本已回答足够多的请求且在限度之内，或整个窗口过去也没出问题），或版本已发布为止。从这条命令开始灰度或提高比例起，不满一个完整的 10 分钟监测窗口，绝不会报告阶段已稳定，所以主要发生在变更之前的窗口不会让它看起来已被验证；中止或发布则立即结束。灰度被中止（无论是监测还是任何人中止的），或先到了 `--timeout`（默认 30 分钟），退出码为 1。进度写到标准错误，所以 `--json` 的输出仍是一份完整的 JSON。
- `k3 rollback` 找到现在在用的部署，显示离开的版本、回到的版本，以及留在原地的迁移（迁移不会撤销）。不加 `--yes` 就询问；标准输入不是终端时从不询问，以退出码 2 要求加 `--yes`。留下的迁移里有旧版本可能无法运行的改动时，只加 `--yes` 会被拒绝：读过警告后再加 `--accept-contract`。
- 退出码：0 完成，1 失败、被中止、被拒绝或超时，2 用法错误。

## 接手目录里的应用

应用 ID 在整个平台唯一。它由 3 到 48 个小写字母、数字和 `-` 组成，以字母开头、以字母或数字结尾，不能是平台保留的词。保留的 ID（包括下面的目录应用 ID）创建时返回 HTTP 409 `app_id_reserved`，说明里写明这个 ID 是保留的；已被别的组织占用时返回 HTTP 409 `conflict`。之前的平台版本对保留的 ID 返回 HTTP 422 `validation_failed`，说明是 `app id must be 3-48 characters … (not a reserved word)`。

`distribution`（分销）、`campaign`、`journey`、`membership`、`order`、`wiki`、`doc` 是原来的一方应用，现在是平台托管的目录应用。这些 ID 由平台保留，别的开发者组织不能用它们新建应用。要接手维护其中一个，由平台运营把应用转交给你的组织，不能自助完成：联系平台运营，给出接收的开发者组织 ID 和原因。

转交只换所有者：

- 应用 ID、版本、各企业的安装和企业的数据都不变；下一个版本由你的组织提交。还没提交的草稿用你的组织的密钥重新封存。
- 应用的价格和订阅改由你的组织结算。价格由平台运营设定：开发者平台没有给应用定价或向企业收款的接口，你的组织在那里能看的是结算单和用量。原组织的应用密钥在运营设定的宽限期（0 到 168 小时）后停止，在这之前把运行中的进程换成你的组织的密钥。
- 装了这个应用的企业和双方组织都会收到通知，平台的审计链有记录。
- 应用有版本在审核中时不能转交，先等审核结论或撤回。属于某个企业的开发者组织不能接收。

不接手时，用你自己的应用 ID 发布一个新应用。

### 接手以后的第一个版本

转交来的应用，已经发布的版本可能是平台运营构建的，而不是用 `k3 publish` 提交的：清单里没有 `runtime` 和 `data`，`opsApi` 是平台内网里的地址。平台按部署自己的设置找到这样的进程，不按清单。`k3 status <应用 ID>` 列出已有的版本，`GET /api/dev/apps/<应用 ID>/versions/<版本>` 给出一个版本的清单。

- 新版本的版本号不能比已有的小（HTTP 422，说明以 `versions only go up` 结尾），也不能和已有的相同（HTTP 409）。
- 在测试企业里调试，有两种起点。一是直接试装已经发布的版本：它没有 `runtime`，照常复制成 `-sbx` 版本装进去；再用 `k3 dev --test-enterprise … --endpoint …` 把调用改发到本机。平台只换源站，路径照清单的 `opsApi`，本机进程的路径要和它一致。二是建一个自己的开发版本草稿，不写 `runtime` 和 `data.migrations`，`opsApi` 与 `web.url` 照「开发版本的 opsApi 与 web.url」写。
- 要由平台托管运行、提交审核的版本，写 `runtime.image`（按摘要固定的镜像）；表结构写在 `data.migrations` 里，见「有自己数据的应用」。
- 应用用自己的命令生成清单，而这个命令总要一个镜像参数（例如 `-image`）时：发布版本给它你构建好的镜像；开发版本草稿在生成以后删掉清单里的 `runtime`（`data.migrations` 也删掉），再建草稿。写了 `runtime` 的草稿不能试装进测试企业。

### 转交之前运行目录应用的代码

目录应用的代码（例如分销的 `raise-distribution`）把应用 ID 写在代码里：清单的 `id`、op 的路由、平台断言的受众（audience）和权限点的前缀都从它来。平台调应用时（草稿服务的预演与执行、服务运行、审批），断言的受众是测试企业里装着的那个应用的 ID；SDK 只接受受众里有自己清单 `id` 的断言，否则答 HTTP 403 `assertion audience mismatch`。

所以在你自己的测试企业里跑这份代码，只有两条路：

1. **先转交。** 以 `distribution` 这样的 ID 运行，要先由平台运营把应用转交给你的组织（见上文）。转交之前，你的组织建不了这个 ID（HTTP 409 `app_id_reserved`），也不能把它试装进你的测试企业：试装只收你自己组织的应用。转交之后，照「试装应用」「让本机的进程接到测试企业」做。
2. **用你自己的 ID 的分支。** fork 代码，让应用 ID 从配置读，清单的 `id`、路由和断言检查用同一个值；再以你的 ID 建应用、建开发版本。只把清单的 `id` 换掉、代码里的 ID 不变是不行的：平台的每次调用都会被进程以 `assertion audience mismatch` 拒绝，`k3 dev` 打印 `POST …/dry-run 403`，日志里是 `http_4xx`。

目录应用的本机数据库还要平台的基础表，见「在本机准备应用的数据库」。
