应用
通过 SDK 和 React 创建、部署和控制 Beeuni 应用。
Apps 是项目范围内的部署,每个都有一个固定 URL,支持不可变版本、
按进程组划分的机器,以及持久化卷。根据 app.capabilities 进行分支处理。本页介绍 SDK 接口:
项目句柄上的 apps 外观接口,制品、版本、机器和卷的
调用,登录令牌、导出类型以及 React hooks。
有关模型、版本配置、来源类型、CLI、固定 URL 和冷启动唤醒行为,请阅读 Apps。
const apps = beeuni.project(projectId).apps;
apps 外观接口
| 方法 | 封装 | 功能 |
|---|---|---|
apps.list() |
GET /projects/:pid/apps |
列出调用者可打开的 Apps(项目管理员可看到全部) |
apps.create(input) |
POST …/apps |
创建 App 并为其分配固定 URL |
apps.get(appId) |
GET …/apps/:id |
读取一个 App |
apps.update(appId, input) |
PATCH …/apps/:id |
重命名,或更改机器、空闲超时、运行模式(always_on)、uses 或 labels。运行模式在 5 分钟内生效;机器变更在下一个版本生效 |
apps.remove(appId) |
DELETE …/apps/:id |
删除 App、其机器和所有版本镜像;返回 images: {released, pending}。其卷保留 final 快照 7 天 |
apps.start(appId) |
POST …/apps/:id/start |
将 desired_state 设为 running 并预热运行时。静态 App 返回 409 static_app_no_runtime |
apps.stop(appId) |
POST …/apps/:id/stop |
立即暂停计算;下一个请求会恢复计算。静态 App 返回 409 static_app_no_runtime:它没有运行时,只要有活跃部署就会提供服务 |
apps.rollback(appId, deploymentId) |
POST …/apps/:id/rollback |
从一个就绪版本的制品和配置创建新版本,并像部署一样推出(202)。卷保留其数据 |
制品是部署的不可变输入:
| 方法 | 封装 | 功能 |
|---|---|---|
apps.artifacts.register(input) |
POST …/apps/artifacts |
注册 archive 或 oci_image;对于归档文件,返回上传 URL |
apps.artifacts.uploadArchive(bytes, options?) |
— | 在一次调用中注册、上传、计算哈希并完成一个 .tar.gz |
apps.artifacts.finalize(artifactId, input) |
POST …/apps/artifacts/:id/finalize |
确认手动上传的 sha256 和 size_bytes |
版本(部署)不可变且带有编号:
| 方法 | 封装 | 功能 |
|---|---|---|
apps.deployments.create(appId, input) |
POST …/apps/:id/deployments |
根据制品、来源和可选的版本 config 启动一个版本 |
apps.deployments.list(appId) |
GET …/apps/:id/deployments |
列出部署历史 |
apps.deployments.get(appId, deploymentId) |
GET …/deployments/:did |
读取一个部署及其事件 |
apps.deployments.logs(appId, deploymentId, options?) |
GET …/deployments/:did/logs |
使用游标读取运行时日志 |
apps.deployments.remove(appId, deploymentId) |
DELETE …/deployments/:did |
删除一个部署及其镜像;对于正在提供服务的部署或进行中的构建,返回 409。返回 image: 'released' | 'pending' | 'none' |
机器、扩缩容和 exec(服务端 App;没有机器的 App 返回
409 app_no_machines):
| 方法 | 封装 | 功能 |
|---|---|---|
apps.machines.list(appId) |
GET …/apps/:id/machines |
每台机器:machine_id、process_group、state、release、vm、volume |
apps.machines.start/stop/restart(appId, machineId) |
POST …/machines/:mid/{start,stop,restart} |
对一台机器执行操作;完成后返回机器列表 |
apps.machines.destroy(appId, machineId) |
DELETE …/machines/:mid |
销毁一台机器。其卷保留;其组运行的机器减少一台 |
apps.scale(appId, count) |
PUT …/apps/:id/scale |
每个进程组的机器数({ worker: 3 },0–20),在后台应用(202)。完成后返回 scale 和 machines |
apps.exec(appId, { command, machine_id?, timeout_s? }) |
POST …/apps/:id/exec |
在运行中的机器上执行一条命令(project.app.admin;经过审计)。timeout_s 为 1–300,默认 60。完成后返回 exit_code、stdout、stderr(各 1 MiB)、truncated |
卷(持久化磁盘):
| 方法 | 封装 | 功能 |
|---|---|---|
apps.volumes.list(appId) |
GET …/apps/:id/volumes |
每个卷:name、process_group、machine_id、size_gb、status、attached、snapshot_retention_days |
apps.volumes.create(appId, { name, size_gb, process_group?, snapshot_retention_days? }) |
POST …/volumes |
预先创建卷;之后其组中具有同名挂载的机器会使用该卷 |
apps.volumes.update(appId, volumeId, { size_gb?, snapshot_retention_days? }) |
PATCH …/volumes/:vid |
扩大卷(不可缩小;最大 100 GB)或更改其保留期(1–60 天) |
apps.volumes.destroy(appId, volumeId, confirmName) |
DELETE …/volumes/:vid?confirm=<name> |
project.app.admin。机器挂载该卷时,会以 409 volume_attached 拒绝。保留 final 快照 7 天 |
apps.volumes.snapshots(appId, volumeId) |
GET …/volumes/:vid/snapshots |
daily、manual 和 final 快照 |
apps.volumes.snapshot(appId, volumeId) |
POST …/volumes/:vid/snapshots |
立即创建 manual 快照 |
apps.volumes.restore(appId, volumeId, snapshotId) |
POST …/volumes/:vid/restore |
project.app.admin。机器停止,卷恢复到快照,机器在该卷上启动(202) |
apps.volumes.fork(appId, volumeId, { name, snapshot_id? }) |
POST …/volumes/:vid/fork |
从快照或当前内容,在同一个 App 中创建一个未挂接的新卷 |
登录令牌(能力 member_tokens;App 缺少该能力时
返回 409 app_capability_unsupported):
| 方法 | 封装 | 功能 |
|---|---|---|
apps.token(appId) |
POST …/apps/:id/token |
为 App 提供有效期 15 分钟的登录令牌,其中标明调用者 |
访问权限由 App 自身的授权策略决定:
| 方法 | 封装 | 功能 |
|---|---|---|
apps.access.get(appId) |
GET …/apps/:id/access |
读取策略。需要 project.app.read |
apps.access.update(appId, input) |
PATCH …/apps/:id/access |
替换策略并递增其修订号 |
apps.access.session(appId) |
POST …/apps/:id/access-session |
生成有效期五分钟的 URL,可用来换取仅限主机的 cookie |
部署静态站点
uploadArchive 完成整个制品握手流程:注册制品,
依据 max_bytes 检查制品,通过 PUT 上传字节,计算 SHA-256,然后
完成制品确认。
const apps = beeuni.project(projectId).apps;
const app = await apps.create({ slug: 'docs', name: 'Docs' });
const artifact = await apps.artifacts.uploadArchive(tarGzBytes, {
onProgress: (uploaded, total) => console.log(`${uploaded}/${total}`),
});
const deployment = await apps.deployments.create(app.app_id, {
artifact_id: artifact.artifact_id,
source: { kind: 'static', spa: true },
});
console.log(app.url, deployment.status); // https://…apps.beeuni.ai queued
create 也接受机器字段:cpu、memory_gb、
disk_gb、idle_timeout_seconds 和 always_on。省略这些字段即可使用默认值。
Apps 没有月度预算:计算费用由账户额度支付。常驻运行的
服务端 App 全天候计费;按需运行的 App 仅在唤醒期间计费。
always_on: true 让服务端 App 全天候运行
(websockets、后台循环);false 在 idle_timeout_seconds 后停止它,
并在下一个请求到来时唤醒。静态 App 没有运行时:always_on 为
false。
如果旧客户端发送 monthly_budget_usd,API 会忽略它(200,不
报错)。任何 App 响应都不包含它。
uses 按 slug 列出此 App
可以获取登录令牌并通过其绑定挂载访问的 Apps;
默认为空,update(appId, { uses }) 会替换该列表(参见
App 使用的 Apps)。
create 和 update 返回 warnings,其值始终为 []。
const app = await beeuni.project(projectId).apps.create({ slug: 'worker', name: 'Worker', always_on: true });
console.log(app.always_on, app.hosting_type); // true null (until the first deploy)
通过轮询状态等待部署完成:
async function waitForReady(appId: string, deploymentId: string) {
for (;;) {
const { deployment } = await apps.deployments.get(appId, deploymentId);
if (deployment.status === 'ready') return deployment;
if (deployment.status === 'failed' || deployment.status === 'cancelled') {
throw new Error(deployment.error ?? deployment.error_code ?? deployment.status);
}
await new Promise((resolve) => setTimeout(resolve, 2000));
}
}
状态值为 queued、validating、building、provisioning、
checking、ready、failed 和 cancelled。
部署 OCI 镜像
注册不可变镜像引用,然后声明进程命令和 公开的目标端口:
const registered = await apps.artifacts.register({
kind: 'oci_image',
image: 'ghcr.io/hekmat/service:2026-08-07',
});
await apps.deployments.create(app.app_id, {
artifact_id: registered.artifact.artifact_id,
source: {
kind: 'oci_image',
image: 'ghcr.io/hekmat/service:2026-08-07',
command: ['node', 'server.js'],
port: 3000,
readiness_path: '/health',
},
});
对于 oci_image,register 返回 upload: null。只有 archive 会获得
上传 URL。
CreateAppDeploymentInput 还接受 environment(非机密的运行时
值)、secrets(运行时键到项目机密名称的映射)和 config(AppReleaseConfigInput,
版本配置)。无效的 config
会返回 400 app_config_invalid,并带有 issues: [{ path, message }];在不支持卷的运行环境中
挂载会返回 400 app_volumes_unsupported。
await apps.deployments.create(app.app_id, {
artifact_id: registered.artifact.artifact_id,
source: { kind: 'oci_image', image, command: ['node', 'server.js'] },
config: {
processes: { app: '', worker: 'node worker.js' },
http_service: { internal_port: 3000, access: 'app', processes: ['app'] },
mounts: [{ source: 'data', destination: '/data', initial_size_gb: 10, processes: ['app'] }],
deploy: { release_command: 'node migrate.js' },
},
});
读取运行时日志
let cursor = 0;
for (;;) {
const page = await apps.deployments.logs(app.app_id, deployment.deployment_id, {
after: cursor,
limit: 200,
});
for (const entry of page.entries) console.log(entry.source, entry.line);
cursor = page.next_cursor;
if (page.entries.length === 0) break;
}
每个条目都包含 cursor、time、source(app、appd、caddy)以及
line。caddy 行是访问日志:方法、URI、状态、耗时和
响应头。它从不包含请求头,因为请求头携带
网关的凭据(访问者令牌和身份、提供方入口令牌)。
机器、扩缩容和卷
const apps = beeuni.project(projectId).apps;
const machines = await apps.machines.list(app.app_id);
await apps.scale(app.app_id, { worker: 3 }); // 202: applied in the background
const { exit_code, stdout } = await apps.exec(app.app_id, { command: ['ls', '/data'] });
const [data] = await apps.volumes.list(app.app_id);
const snapshot = await apps.volumes.snapshot(app.app_id, data.volume_id);
await apps.volumes.restore(app.app_id, data.volume_id, snapshot.snapshot_id); // project.app.admin
const copy = await apps.volumes.fork(app.app_id, data.volume_id, { name: 'data_copy' });
机器在各次发布中保留其 machine_id;每次发布都会替换其
box(instance_id)。同一时间只有一台机器挂载一个卷。恢复会生成
卷的新版本:被替换的内容会保留在历史记录中。
已挂载卷的快照可能遗漏最后约 1 秒的写入。
Convex 后端
Convex 后端是一个带有卷的普通服务器 App,使用
convex 模板创建(beeuni apps launch --template convex --name db;参见
模板 convex)。
SDK 没有 Convex 专用调用。使用
exec 从机器读取管理员密钥,并通过 uses 让 UI App 访问后端:
const db = (await apps.list()).find((a) => a.slug === 'db')!;
const { stdout } = await apps.exec(db.app_id, { command: ['sh', '-c', 'cd /convex && ./generate_admin_key.sh'] });
// The admin key controls all code and data: never log it or ship it to a browser.
await apps.update(site.app_id, { uses: ['db'] }); // the site may mint db tokens and bind to it
db.services 保存后端的 URL:http(Convex 客户端 URL)和
site(HTTP actions)。
你的 App 已经知道谁在访问
打开 Beeuni 托管 App 的人,Beeuni 已经让其登录。Apps 网关会在提供第一个字节之前验证其身份,因此你的 App 无需自己的登录流程——没有第二个密码、没有授权同意页面,也没有重定向。
在浏览器中:
import { createBeeuni, beeuniAppViewerToken } from '@beeuni/sdk';
import { useBeeuniAppViewer } from '@beeuni/sdk/react';
const beeuni = createBeeuni({
backendUrl: '/_beeuni/api/v1', // the gate, on your App's own origin
getToken: beeuniAppViewerToken(), // the viewer's own App-scoped token
});
function Header() {
const { status, viewer } = useBeeuniAppViewer();
return <span>{status === 'viewer' ? viewer.email : 'Signed out'}</span>;
}
浏览器在你的 App 自身的源上调用 /_beeuni/api/v1/*,绝不
直接调用 https://api.beeuni.ai/v1:API 不响应来自
App 源的 CORS 预检,因此直接调用会在浏览器中失败。网关使用访问者的 App 范围
令牌,以已登录访问者的身份,将每次调用
转发到 Beeuni API。规则如下:
- 必须有
viewer_token_scope: 'api'。否则返回403 viewer_api_disabled。 - 仅代表网关已让其登录的访问者执行操作(通过 Beeuni 登录或访问
链接)。没有此类访问者时,返回
401 no_viewer_identity。 - 仅响应来自 App 自身源的请求。跨站请求
返回
403 cross_site_request。 - 转发
/v1/*,但/v1/oauth/*和 MCP 端点除外,它们返回404。你的 App 的 Cookie 和自身的Authorization请求头永远不会到达 API,API 也无法在你的 App 上设置 Cookie。
在你的 App 服务器上,网关会将签名后的身份写入每个请求:
import { readAppViewer, createAppViewerBeeuni } from '@beeuni/sdk/server';
const viewer = await readAppViewer(request);
// { userId, email, name, picture, groupIds, groups, role, projectId, accountId, appId, accessMode, token }
if (!viewer) return new Response('Not found', { status: 404 });
// and, for an `api`-scoped App, act as them:
const beeuni = await createAppViewerBeeuni(request, { backendUrl });
await beeuni.projects.list(); // their projects, their role
readAppViewer 使用 BEEUNI_APP_VIEWER_SECRET 验证请求头的 HMAC,
Beeuni 会在部署时将后者注入你的 App。伪造的请求头永远无法通过验证:
网关会在转发前删除客户端提供的任何副本,并使用
为每个 App 派生的密钥生成签名。
你的 App 能获知多少信息
由 App 访问策略中的一个设置决定——Beeuni 中的 Settings → Access、
beeuni apps access <app> --viewer off|identity|api,或 PATCH /projects/:id/apps/:appId/access
上的 viewer_token_scope:
| 范围 | 你的 App 收到的信息 |
|---|---|
identity(默认) |
访问者的 id、电子邮件、姓名、头像、账户角色和 Beeuni 群组(id 和名称,仅限此 App 所属账户中的群组),以及一个 profile email 令牌。足以向每个人展示其自己的数据,并按群组控制访问。 |
api |
以上信息,以及一个在 Beeuni API 上以该用户身份操作的令牌——权限受其自身角色限制。 |
off |
无。 |
令牌绝不是用户的 Beeuni 会话:它有效期为一小时,仅包含 这些范围;当 App 被删除或其 访问策略更改时,该 App 签发的每个令牌都会失效。
使用访问者令牌
- 在服务器上,每次请求都读取令牌。 网关会在
每个代理请求的
x-beeuni-app-viewer-token中放入有效令牌。将其作为Authorization: Bearer发送给 Beeuni API——createAppViewerBeeuni(request)正是这样做的。每个请求创建一个客户端;不要存储令牌。 - 访问策略更改后,旧令牌不再有效。 通过网关的下一次请求
会携带新令牌。在浏览器中,
beeuniAppViewerToken()会自行恢复:当 API 拒绝其令牌时,SDK 会重新读取/_beeuni/viewer,并重试该调用一次。 - 访问者的角色是权限上限。 令牌只能执行
访问者在 Beeuni 中能执行的操作,绝不会更多。在具有智能体权限的项目中,
member需要获得你的 App 所启动智能体的授权:没有授权时,POST /projects/:id/sessions返回403 no_agent_access。向 访问者授予该智能体的权限,而不是更宽泛的角色。 - 谁具有访问者身份。 使用 Beeuni 登录打开 App 的人——或
使用访问链接打开 App 的人(链接会让其以该用户身份登录)——具有访问者身份。
passwordApp 没有访问者身份:密码证明的是共享密钥,而不是个人身份。publicApp 仅对通过访问链接进入的访客具有访问者身份; 直接访问 URL 仍为匿名。 - 它是凭据。 Beeuni 不会将请求头写入 App 的访问
日志(
beeuni apps logs)。不要在自己的代码中记录该请求头。
通过自己的域名(而非 *.apps.beeuni.ai)提供服务的 App 前面没有网关
——请在那里改用 使用 Beeuni 登录。
在每种运行时中使用同一成员身份
Beeuni 在三个地方为同一个人的身份担保:浏览器向网关请求
(/_beeuni/viewer),App 服务器读取签名请求头,而验证
登录令牌的 App(Convex 后端或你自己的服务器)会收到
Beeuni 签名的令牌。readBeeuniMember 将这些不同形式都转换为同一种
BeeuniMember,而 requireBeeuniMember 决定谁可以继续。无需
框架,也无依赖:同一套代码可在浏览器、Node、Bun、
Worker 和 Convex 函数中运行。
import {
readBeeuniMember, requireBeeuniMember, verifyBeeuniToken,
beeuniToken, beeuniBinding, fetchBeeuniAppViewer, BeeuniMemberError,
} from '@beeuni/sdk';
// Browser: who is looking (null when nobody).
const me = readBeeuniMember(await fetchBeeuniAppViewer());
// Browser → an App this App uses, on the same origin: its URL and a token fetcher.
const db = beeuniBinding('db');
const convex = new ConvexReactClient(db.url);
convex.setAuth(db.token);
// React: read the member with useQuery(api.members.me), never useConvexAuth() (see below).
// Browser: a token for this App itself, or for an App it uses.
const token = await beeuniToken()();
const dbToken = beeuniToken({ audience: 'db' });
// A Convex function: the runtime verified the token; enforce the rules.
const member = requireBeeuniMember(await ctx.auth.getUserIdentity(), { groups: ['Finance'] });
// A server App: verify the bearer. No options = the BEEUNI_AUTH_* env Beeuni injects.
// Any other server: pass the App's `auth` values.
const caller = await verifyBeeuniToken(bearer, { issuer: app.auth.issuer, audience: app.auth.audience, jwks: app.auth.jwks_uri });
在 React 中,不要调用 useConvexAuth():它在普通的
ConvexProvider 下会抛出异常(它需要 ConvexProviderWithAuth,且该提供方不会
验证 beeuniBinding)。通过查询读取已登录成员:
// convex/members.ts
export const me = query({ args: {}, handler: async (ctx) => readBeeuniMember(await ctx.auth.getUserIdentity()) });
const me = useQuery(api.members.me); // undefined: loading. null: signed out. Otherwise the member.
对于匿名调用者,readBeeuniMember 返回 null。仅在
函数必须抛出异常的地方使用 requireBeeuniMember。
| 字段 | 值 |
|---|---|
userId |
Beeuni 用户 id。稳定不变;用它作为数据行的键。 |
email、name、picture |
来自成员的个人资料。未知时为 null。 |
groups、groupIds |
成员在此账户中的 Beeuni 群组,分别按名称和 id 表示。 |
role |
账户角色:owner、admin 或 member。 |
accountId、projectId |
App 所在的位置。 |
requireBeeuniMember(claims, { groups, roles }) 返回成员,或者抛出
BeeuniMemberError,其 code 为 unauthenticated(没有用户,或令牌
未通过检查)或 forbidden(不属于指定群组或角色)。每个列表都表示
“其中任意一个”;群组按名称或 id 匹配;空列表不允许任何人访问。
登录令牌
每个项目有一个签发方,为每个 App 的令牌签名:iss =
<API origin>/v1/projects/<projectId>,aud = 令牌所属 App 的 id,
有效期为 15 分钟。其密钥集公开于 <issuer>/jwks.json,由
<issuer>/.well-known/openid-configuration 标识。每个 App 都携带用于
验证其令牌的值:app.auth.issuer、app.auth.audience、app.auth.jwks_uri。
令牌包含 sub、email、name、picture、groups、group_ids、
role、account_id 和 project_id;智能体的令牌包含
kind: 'agent',不包含群组或角色。
| 函数 | 功能 |
|---|---|
beeuniToken({ audience? }) |
用于 App 自身(默认)或其使用的 App 的令牌获取函数(实时客户端用作认证回调的形式)。在 App 自身的源上读取 /_beeuni/token。缓存至即将过期时;{ forceRefreshToken: true } 会重新请求。没有已登录的访问者时返回 null;对于其未使用的 App,返回 null 并在控制台输出一次警告(403 app_not_linked)。从不抛出异常。 |
beeuniBinding(slug, { origin? }) |
此 App 使用的 App 的 { url, token }:url = <origin>/_beeuni/apps/<slug>(HTTP 和 WebSocket,同源,无 CORS),token = beeuniToken({ audience: slug })。 |
verifyBeeuniToken(bearer, { issuer?, audience?, jwks? }) |
检查 ES256 签名、有效期(允许 60 秒时钟偏差)、签发者和受众。默认值:BEEUNI_AUTH_ISSUER、BEEUNI_AUTH_AUDIENCE、BEEUNI_AUTH_JWKS。Beeuni 将这三项注入每个服务端 App 的运行时(签发者:项目;受众:App 自身的 id;密钥集:一个 data: URI),因此 verifyBeeuniToken(bearer) 在其中无需任何选项即可使用。切勿在服务端 App 的 env 中设置 BEEUNI_AUTH_*:BEEUNI_ 前缀为保留前缀,beeuni validate 会拒绝它。密钥集可以是对象、JSON 文本、data: URI 或 https URL(auth.jwks_uri,每个进程获取一次)。任何失败都会抛出 BeeuniMemberError,包括缺少密钥集或受众:一个项目密钥为所有 App 的令牌签名,因此 aud 才是阻止其他 App 令牌进入的机制。audience: false 接受项目中任意 App 的令牌。 |
apps.token(appId) |
来自 API、标明调用者的令牌(POST …/apps/:id/token)。智能体会话获得的令牌会标明其智能体。 |
从项目或群组中移除的成员会在一个令牌有效期内失去访问权限;App 访问控制会在其下次加载页面时拒绝访问。Beeuni 群组需要 Enterprise 套餐;角色适用于所有套餐。
管理访问权限
await apps.access.update(app.app_id, {
mode: 'restricted',
member_ids: [memberId],
group_ids: [groupId],
});
const preview = await apps.access.session(app.app_id);
window.open(preview.url); // valid for five minutes
AppAccessConfig 返回 password_configured,绝不返回密码或其哈希。使用 { mode: 'password', password } 设置密码。每次更新都会递增 revision,从而撤销现有的 App Cookie。
类型
以下所有类型均从 @beeuni/sdk 导出。
| 类型 | 包含的内容 |
|---|---|
App |
身份、capabilities、labels、scale、services(AppServiceSummary[])、machines(total、running、stopped)、volumes(count、size_gb)、config(线上发布版本的 AppReleaseConfig,或 null)、uses、used_by、auth、url、access_mode、access_revision、desired_state、active_deployment_id、hosting_type(当前发布版本的:static、sandbox,首次部署前为 null)、machine(线上发布版本中提供服务的组的规模)、idle_timeout_seconds、always_on、retained_deployments、warnings(仅限创建和更新;始终为 [])、last_request_at、viewer_can_access |
AppCapability |
'deployments' | 'rollback' | 'preview' | 'sleep' | 'static' | 'member_tokens' | 'machines' | 'scale' | 'exec' | 'volumes' | 'snapshots' |
AppReleaseConfig · AppReleaseConfigInput |
解析后的发布配置(processes、services、mounts、vm、deploy)· 编写形式(processes、http_service、services、mounts、vm、deploy,以及简写 port、command、readiness_path、always_on、resources) |
AppServiceConfig · AppServiceAccess · AppAutoStop · AppCheck |
name、internal_port、protocol、access、auto_stop_machines、auto_start_machines、min_machines_running、processes、checks · 'public' | 'app' | 'admins' · 'off' | 'stop' | 'suspend' · type、port、path、interval_s、timeout_s、grace_period_s |
AppMountConfig · AppVmConfig · AppDeployStrategy |
source、destination、initial_size_gb、snapshot_retention_days、processes · cpu、memory_gb、disk_gb、processes · 'rolling' | 'immediate' | 'bluegreen' | 'canary' |
AppServiceSummary |
name、protocol、access、internal_port、processes、url(主机标签过长时为 null) |
AppMachine · AppMachineState |
machine_id、instance_id、process_group、state、release(deployment_id、version)、vm、volume、created_at、started_at、stopped_at · 'started' | 'starting' | 'stopped' | 'stopping' | 'provisioning' |
AppVolume · AppVolumeSnapshot · CreateAppVolumeInput |
volume_id、name、process_group、machine_id、size_gb、status、attached、snapshot_retention_days、created_at · snapshot_id、kind(daily、manual、final)、commit_id、created_at · name、size_gb、process_group、snapshot_retention_days |
AppExecInput · AppExecResult |
command、machine_id、timeout_s · machine_id、exit_code、stdout、stderr、truncated |
AppAuth |
issuer、audience(App id)、jwks_uri:用于验证 App 登录令牌的公开值 |
AppToken |
token、expires_at |
BeeuniTokenOptions · BeeuniTokenFetcher · BeeuniBinding · VerifyBeeuniTokenOptions |
上述登录令牌辅助函数 |
AppDeployment |
version、status、source_kind、hosting_type(static:从存储提供服务,无运行时;sandbox:机器)、hosting_provider、runtime_spec、build_spec、error_code、attempt_count、created_by、actor_type、source_session_id、config(发布版本解析后的配置)、strategy、release_command_status(running、succeeded、failed 或 null) |
AppRollback |
apps.rollback 成功完成时返回的值:App 加上 deployment 中排队的发布版本 |
AppDeploymentDetail |
一个 deployment 及其 events:生命周期事件和构建日志(build_log 行,以及构建输出超过 5,000 行时的一个 log_truncated 事件) |
AppAccessConfig |
mode、revision、member_ids、group_ids、password_configured |
AppAccessMode |
'private' | 'project' | 'restricted' | 'public' | 'password' |
AppSource |
StaticAppSource | BundleAppSource | DockerfileAppSource | OciImageAppSource |
AppArtifact |
kind、status、sha256、size_bytes、image_reference |
AppLogEntry · AppLogsResponse |
一行日志,以及一页数据加上 next_cursor |
apps.list() 和 useProjectApps 仅返回调用者根据各 App 的访问策略可以打开的 App(私有:创建者;受限:列出的成员和群组;项目、密码和公开:项目团队)。项目管理员可以看到所有 App,因此即使创建者离开,私有 App 仍可管理。因此,每个列出的 App 的 viewer_can_access 都为 true。对调用者无法打开的 App 执行 get 时,返回 404,而非 403。应将 undefined 视为未知,而非拒绝访问。
AppAccessMode 是角色模型之上的按资源可见性设置,而不是角色。restricted 指定用户和群组——与角色模型使用的主体类型相同。参见
账户与访问权限。
DockerfileAppSource 和 OciImageAppSource 需要 command。port 是主服务的端口:仅在只有 worker 的发布版本(带有 processes 且没有服务的 config)中可以省略。StaticAppSource 和 BundleAppSource 均不接受这两项。
React hooks
@beeuni/sdk/react 导出七个用于 App 的 hook。
useProjectApps(projectId)
调用者可以打开的 App,以及其生命周期变更操作。每个变更操作成功后都会使清单缓存失效。
import { useProjectApps } from '@beeuni/sdk/react';
function AppList({ projectId }: { projectId: string }) {
const apps = useProjectApps(projectId);
if (!apps.data) return null;
return (
<ul>
{apps.data.map((app) => (
<li key={app.app_id}>
<a href={app.url}>{app.slug}</a>
<button onClick={() => apps.stop.mutate(app.app_id)}>Stop</button>
</li>
))}
</ul>
);
}
它返回查询字段,以及 create、update、start、stop 和 remove。
useAppDeployments(projectId, appId)
不可变的部署历史,每 5 秒重新获取一次,使正在运行的构建自动更新进度。
const deployments = useAppDeployments(projectId, appId);
await deployments.deploy.mutateAsync({
artifact_id: artifact.artifact_id,
source: { kind: 'static', spa: true },
});
await deployments.rollback.mutateAsync(previousDeploymentId);
两个变更操作都会使部署列表和 App 清单缓存失效。
useAppDeployment(projectId, appId, deploymentId)
单个部署及其事件,包括构建日志(AppDeploymentDetail)。每 2 秒轮询一次,直到部署为 ready、failed 或 cancelled。将 null 作为 deploymentId 传入,可使其保持空闲。
const detail = useAppDeployment(projectId, appId, deploymentId);
const log = (detail.data?.events ?? [])
.map((event) => (event.type === 'build_log' ? event.message : `[${event.type}] ${event.message}`))
.join('\n');
一次构建会保留最前面的 2,500 行和最后面的 2,500 行。日志行以批次写入, 最迟在输出后 1 秒内写入。失败或取消的部署 会将构建日志保留 14 天。
useAppAccess(projectId, appId, options?)
访问策略和短期访问会话。两部分是独立的 查询,且均为可选。
const access = useAppAccess(projectId, appId, {
policy: canEditAccess,
session: app.viewer_can_access,
});
access.policy.data; // AppAccessConfig
access.session.data; // { url, expires_at }
await access.update.mutateAsync({ mode: 'project' });
| 选项 | 默认值 | 何时使用 false |
|---|---|---|
policy |
true |
界面仅预览 App。GET …/access 是管理性读取,对没有项目管理权限的调用者返回 403。 |
session |
true |
调用者可能无权打开 App。传入 app.viewer_can_access(对于已列出的 App,始终为 true)。 |
如果 App 网格将两个选项都保留为 true,则会为每个 App 发起一次策略读取和一次
会话创建;对于无权打开该 App 的成员,两者都会返回
403。
useAppMachines(projectId, appId)
App 的机器,每 5 秒轮询一次,另有 start、stop、restart、
destroy(各接受一个 machineId)和 scale(接受
{ [group]: count })。每次变更操作都会刷新机器和 App
清单。appMachinesKey(projectId, appId) 是查询键。
const machines = useAppMachines(projectId, app.app_id);
await machines.scale.mutateAsync({ worker: 2 });
await machines.restart.mutateAsync(machineId);
useAppVolumes(projectId, appId)
App 的卷,另有 snapshot(volumeId)、restore
({ volumeId, snapshotId })、fork({ volumeId, name, snapshotId? })、
extend({ volumeId, sizeGb })和 destroy({ volumeId, name })。每次
变更操作都会刷新卷。appVolumesKey(projectId, appId) 是查询
键。
const volumes = useAppVolumes(projectId, app.app_id);
await volumes.snapshot.mutateAsync(volumeId);
await volumes.restore.mutateAsync({ volumeId, snapshotId });
useAppVolumeSnapshots(projectId, appId, volumeId)
一个卷的快照。将 null 作为 volumeId 传入,可使其保持空闲。
错误
import { featureDisabledKey, isFeatureDisabledError } from '@beeuni/sdk';
try {
await beeuni.project(projectId).apps.list();
} catch (error) {
if (isFeatureDisabledError(error)) {
console.log(`${featureDisabledKey(error)} is off for this project`);
}
}
你还应处理以下响应:
- slug 重复时返回
409;账户达到 App 数量上限时返回402,并附带app_quota_exceeded;规格超出范围时返回400,并附带app_machine_out_of_range。 uses中存在未知 slug 时返回400 app_not_found(slugs)。- 调用 App 不提供的功能时返回
409 app_capability_unsupported(capability)。 - 部署:
400 app_config_invalid(issues)、400 app_volumes_unsupported。 回滚:409 release_not_ready。 - 机器和 exec:
409 app_no_machines、404 machine_not_found、409 machine_not_running、400 process_group_unknown、400 scale_out_of_range、400 command_required。 - 卷:
400 volume_name_invalid、volume_size_invalid、volume_shrink_refused、volume_retention_invalid、snapshot_invalid、confirmation_required;404 volume_not_found;409 volume_attached。
token() 返回 Cache-Control: no-store。