跳到内容
BeeuniBeeuni
中文
Esc
↑↓导航↵打开⌘J预览
本页内容

应用

通过 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 的人(链接会让其以该用户身份登录)——具有访问者身份。 password App 没有访问者身份:密码证明的是共享密钥,而不是个人身份。 public App 仅对通过访问链接进入的访客具有访问者身份; 直接访问 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。

这个页面有帮助吗?