关于 eve-fullstack-app-design
eve-fullstack-app-design 是面向在 Eve Horizon 上设计或迁移应用的团队的架构技能。它以 Eve manifest 规划服务、基础设施、数据库和部署,并提供常见全栈布局的模式。
主要功能
- 01以 manifest 为核心的应用架构
- 02服务拓扑与入口规划
- 03托管及外部服务指导
- 04数据库与迁移模式
- 05SPA、API 和数据库参考布局
适用场景
- 01规划新的 Eve 应用
- 02将现有应用迁移到 Eve
- 03选择服务与数据库架构
- 04设计部署流水线和环境
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ incept5/ eve-skillpacks --skill eve-fullstack-app-design安装后在 Claude Code 中输入/eve-fullstack-app-design确认已加载。
详细介绍
Eve Horizon 全栈应用设计
设计应用时,让清单成为蓝图,让平台处理基础设施,并确保每项设计决策都有明确意图。
适用场景
在以下情况下加载此技能:
- 在 Eve 上从头设计新应用
- 将现有应用迁移到该平台
- 评估当前架构是否充分利用 Eve 的功能
- 规划服务拓扑、数据库策略或部署流水线
- 在托管服务和外部服务之间做出选择
此技能介绍 Eve PaaS 层的设计思维。如需 CLI 用法和运维详情,请加载相应的 eve-se 技能(eve-manifest-authoring、eve-deploy-debugging、eve-auth-and-secrets、eve-pipelines-workflows)。
将清单作为蓝图
清单(.eve/manifest.yaml)是应用结构的唯一可信来源。应将其视为架构文档,而不只是配置文件。
清单声明的内容
| 关注点 | 清单部分 | 设计决策 |
|---|---|---|
| 服务拓扑 | services | 运行哪些进程,以及它们如何连接 |
| 基础设施 | services[].x-eve | 托管数据库、入口、角色 |
| 构建策略 | services[].build + registry | 构建哪些内容,以及镜像存放在哪里 |
| 发布流水线 | pipelines | 代码如何从提交流转到生产环境 |
| 环境结构 | environments | 存在哪些环境,以及它们使用哪些流水线 |
| Agent 配置 | x-eve.agents、x-eve.chat | Agent 配置文件、团队分派、聊天路由 |
| 运行时默认值 | x-eve.defaults | Harness、工作区、Git 策略 |
设计原则:如果 Agent 或运维人员无法通过阅读清单了解应用结构,那么清单就不完整。
服务拓扑
选择服务
大多数 Eve 应用采用以下模式之一:
API + 数据库(最简单):
services:
api: # HTTP service with ingress
db: # managed Postgres
API + Worker + 数据库:
services:
api: # HTTP service (user-facing)
worker: # Background processor (jobs, queues)
db: # managed Postgres
多服务:
services:
web: # Frontend/SSR
api: # Backend API
worker: # Background jobs
db: # managed Postgres
redis: # external cache (x-eve.external: true)
服务设计规则
- 每个服务只负责一项职责。 将 HTTP 服务与后台处理分开。API 服务也不应运行定时任务。
- Postgres 使用托管数据库。 声明
x-eve.role: managed_db,由平台负责配置、连接并注入凭据。无需手动设置连接字符串。 - 明确标记外部服务。 对托管在 Eve 之外的服务(Redis、第三方 API),使用
x-eve.external: true和x-eve.connection_url。 - 一次性任务使用
x-eve.role: job。 迁移、种子数据和数据回填都属于作业服务,而非持久进程。 - 有意配置入口。 只有需要外部 HTTP 访问的服务才设置
x-eve.ingress.public: true。内部服务通过集群网络通信。 - 尽早选择主机名策略。 默认情况下,每个公共服务都会获得一个平台生成的 URL。可使用
x-eve.ingress.alias配置更易记的平台子域名(ingest.eve.example.com),或者通过x-eve.ingress.domains: [limelee.com]使用自己的域名。自定义域名按环境限定,且首次绑定优先——在声明域名前,应先规划由哪个环境拥有顶级域名(通常是production)。有关声明和 DNS 验证流程,请参阅eve-manifest-authoring和eve-deploy-debugging。
稳定的出站 IP(Egress)
大多数应用无需关注其出站源 IP,但如果你要集成会将源 IP 列入允许列表的供应商(摄像头、支付处理商、对源 IP 有严格要求的合作伙伴 API),请在服务级别声明选择启用稳定的出站流量。平台会将 Pod 调度到启用了 hostNetwork: true 的公共出站节点组,为服务提供稳定、可预测的出站路径,而不是共享 NAT 映射。应将此视为一项有意的架构选择,仅用于一两个确有需要的服务,而非默认配置。
应用对象存储
需要存储文件(上传文件、头像、导出文件)的应用可以在清单中声明对象存储桶:
services:
api:
x-eve:
object_store:
buckets:
- name: uploads
visibility: private
- name: avatars
visibility: public
**注意:**应用对象存储的数据库架构已存在,但尚未接通从清单自动配置的功能。当前状态请参阅
references/object-store-filesystem.md。
接通后,平台会向服务容器注入 STORAGE_ENDPOINT、STORAGE_ACCESS_KEY、STORAGE_SECRET_KEY、STORAGE_BUCKET 和 STORAGE_FORCE_PATH_STYLE。
凭证隔离:应用 Pod 接收的是应用专属存储凭证,而非平台内部凭证。遭到入侵的应用服务无法访问平台存储桶或其他租户的数据。请根据这一边界进行设计——不要尝试在应用之间共享凭证;请为每个应用声明其存储桶,并由平台颁发范围受限的密钥。
Cloud FS / Google Drive 存储
对于面向文档的存储,请使用 Cloud FS 挂载。每个组织通过 BYOA OAuth 凭证连接自己的 Google Drive,然后将文件夹挂载到组织文件系统:
eve integrations configure google-drive --client-id "..." --client-secret "..."
eve integrations connect google-drive
eve cloud-fs mount --org org_xxx --provider google-drive --folder-id <id> --label "Shared Drive"
应用可以通过 Eve 的 Cloud FS 接口(eve cloud-fs ls、eve cloud-fs search 和每个挂载点对应的 Cloud FS API 路由)浏览和搜索已挂载的 Drive 内容。它与对象存储桶互为补充——共享文档和协作使用 Cloud FS,应用管理的二进制资源使用对象存储。
平台注入的变量
每个已部署的服务都会收到 EVE_API_URL、EVE_PUBLIC_API_URL、EVE_PROJECT_ID、EVE_ORG_ID 和 EVE_ENV_NAME。服务端到服务端调用请使用 EVE_API_URL。面向浏览器的代码请使用 EVE_PUBLIC_API_URL。请让应用读取这些变量,而不是将 URL 硬编码。
参考架构:SPA + API + 托管数据库
这是最常见的 Eve 全栈模式。由 nginx 提供服务的 SPA 会将 API 调用代理到内部后端,并使用托管 Postgres 和 eve-migrate 管理架构。
服务布局
services:
web: # nginx SPA (public ingress, proxies /api/ → api service)
api: # NestJS/Express backend (internal, no public ingress)
db: # managed Postgres 16
migrate: # eve-migrate job (runs SQL migrations)
为什么使用 nginx 代理? Web 服务的 nginx 会将 /api/ 反向代理到内部 API 服务。这样可消除 CORS,避免硬编码 API 主机名,并让 SPA 能以同源方式访问后端。API 服务没有公共入口——只能在集群内部访问。
清单结构
services:
api:
build:
context: ./apps/api
dockerfile: ./apps/api/Dockerfile
ports: [3000]
environment:
NODE_ENV: production
DATABASE_URL: ${managed.db.url}
CORS_ORIGIN: "https://myapp.eve.example.com"
# No x-eve.ingress — API is internal only
web:
build:
context: ./apps/web
dockerfile: ./apps/web/Dockerfile
ports: [80]
environment:
API_SERVICE_HOST: ${ENV_NAME}-api # k8s service DNS for nginx proxy
depends_on:
api:
condition: service_healthy
x-eve:
ingress:
public: true
port: 80
alias: myapp # https://myapp.{org}-{project}-{env}.eve.example.com
migrate:
image: public.ecr.aws/w7c4v0w3/eve-horizon/migrate:latest
environment:
DATABASE_URL: ${managed.db.url}
MIGRATIONS_DIR: /migrations
x-eve:
role: job
files:
- source: db/migrations
target: /migrations
db:
x-eve:
role: managed_db
managed:
class: db.p1
engine: postgres
engine_version: "16"
nginx 代理
Web 服务的 Dockerfile 使用 Vite 构建 SPA,然后通过 nginx 提供服务。nginx 配置使用 envsubst 在容器启动时解析 ${API_SERVICE_HOST}:
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location /api/ {
proxy_pass http://${API_SERVICE_HOST}:3000/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off;
}
location / {
try_files $uri $uri/ /index.html;
}
location /health {
return 200 "ok";
add_header Content-Type text/plain;
}
}
在清单中,API_SERVICE_HOST: ${ENV_NAME}-api 会解析为 k8s 服务名称(例如 sandbox-api),为 nginx 提供稳定的内部 DNS 目标。
使用 Eve-Migrate 管理架构
Eve 提供了专用的迁移运行器:public.ecr.aws/w7c4v0w3/eve-horizon/migrate:latest。它使用带时间戳前缀的纯 SQL 文件,并在 schema_migrations 表中进行跟踪(支持幂等、校验和及事务)。
db/
migrations/
20260312000000_initial_schema.sql
20260312100000_seed_data.sql
20260315000000_add_status_column.sql
通过 x-eve.files 将迁移文件挂载到容器中。流水线中的迁移步骤会在部署后运行(必须先配置好托管数据库)。
不要使用 TypeORM、Knex 或 Flyway 迁移——它们会增加复杂性,并偏离 Eve 平台的迁移跟踪机制。eve-migrate 运行器可确保本地开发和预发布环境保持一致。
多阶段 Dockerfile
API Dockerfile(NestJS/Node):
FROM node:22-slim AS base
WORKDIR /app
ENV PNPM_HOME="/pnpm" PATH="$PNPM_HOME:$PATH"
RUN corepack enable && corepack prepare pnpm@latest --activate
FROM base AS deps
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile 2>/dev/null || pnpm install
FROM deps AS build
COPY tsconfig.json ./
COPY src ./src
RUN pnpm build
FROM node:22-slim AS production
WORKDIR /app
RUN groupadd --gid 1000 node || true && useradd --uid 1000 --gid node --shell /bin/bash --create-home node || true
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
USER node
ENV NODE_ENV=production PORT=3000
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD node -e "fetch('http://localhost:3000/health').then(r => r.ok ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))"
CMD ["node", "dist/main.js"]
Web Dockerfile(Vite SPA + nginx):
FROM node:22-slim AS build
WORKDIR /app
ENV PNPM_HOME="/pnpm" PATH="$PNPM_HOME:$PATH"
RUN corepack enable && corepack prepare pnpm@latest --activate
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile 2>/dev/null || pnpm install
COPY tsconfig.json vite.config.ts index.html ./
COPY src ./src
RUN pnpm build
FROM nginx:alpine AS production
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/templates/default.conf.template
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost/health || exit 1
CMD ["nginx", "-g", "daemon off;"]
惯例:以 node:22-slim 为基础镜像,通过 corepack 使用 pnpm,锁定锁文件,API 使用非 root 用户,两个服务均配置健康检查。
数据库设计
配置
在清单中声明托管数据库:
services:
db:
x-eve:
role: managed_db
managed:
class: db.p1
engine: postgres
engine_version: "16"
在其他服务中引用连接 URL:${managed.db.url}。
Schema 策略
- 迁移使用纯 SQL。 在
db/migrations/中创建以时间戳为前缀的 SQL 文件(例如,20260312000000_initial.sql)。使用 eve-migrate 运行(见上文“参考架构”)。切勿手动修改生产环境的架构。 - 从一开始就为 RLS 做好设计。 每个包含多租户数据的表都要添加
org_id TEXT NOT NULL、RLS 策略,以及一个用于设置会话上下文的DatabaseService(见下文)。事后再添加行级安全会很麻烦。 - 修改前先检查。 使用
eve db schema查看当前架构。开发期间使用eve db sql --env <env>执行临时查询。 - 将应用数据与 Agent 数据分开。 使用不同的架构或命名约定。应用表服务于产品;Agent 表用于记忆和协调(存储模式见
eve-agent-memory)。
RLS + DatabaseService 模式(NestJS)
在 NestJS 中实现多租户 RLS 的可靠模式是使用原生 pg.Pool(而非 ORM),并通过请求级事务封装器处理:
db.ts — 配置连接池并在启动时进行健康检查:
import { Pool } from 'pg';
const databaseUrl = process.env.DATABASE_URL || 'postgresql://app:app@localhost:5432/myapp';
const parsed = new URL(databaseUrl);
const isLocal = ['localhost', '127.0.0.1'].includes(parsed.hostname);
export const pool = new Pool({
connectionString: databaseUrl,
ssl: !isLocal ? { rejectUnauthorized: false } : undefined,
});
database.service.ts — 带有 RLS 上下文的事务封装器:
import { Injectable } from '@nestjs/common';
import type { PoolClient, QueryResult, QueryResultRow } from 'pg';
import { pool } from '../db';
export interface DbContext {
org_id: string;
user_id?: string;
}
@Injectable()
export class DatabaseService {
async withClient<T>(context: DbContext | null, fn: (client: PoolClient) => Promise<T>): Promise<T> {
const client = await pool.connect();
try {
await client.query('BEGIN');
if (context?.org_id) {
await client.query("SELECT set_config('app.org_id', $1, true)", [context.org_id]);
}
if (context?.user_id) {
await client.query("SELECT set_config('app.user_id', $1, true)", [context.user_id]);
}
const result = await fn(client);
await client.query('COMMIT');
return result;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
async query<T extends QueryResultRow>(ctx: DbContext | null, sql: string, params?: unknown[]): Promise<QueryResult<T>> {
return this.withClient(ctx, (client) => client.query<T>(sql, params));
}
async queryOne<T extends QueryResultRow>(ctx: DbContext | null, sql: string, params?: unknown[]): Promise<T | null> {
const result = await this.query<T>(ctx, sql, params);
return result.rows[0] ?? null;
}
}
为什么采用此模式?
set_config('app.org_id', $1, true)的作用域仅限于事务——连接归还到连接池时会自动清除。- 所有数据库访问都通过
withClient进行,确保在执行任何查询前设置好 RLS 上下文。 - 没有 ORM 开销——原生 SQL 可全面控制查询计划和联接。
DbContext对象由req.user派生(由 Eve 身份验证中间件设置)。
RLS 策略模板(在迁移 SQL 中应用于每个表):
ALTER TABLE my_table ENABLE ROW LEVEL SECURITY;
CREATE POLICY my_table_select ON my_table FOR SELECT
USING (current_setting('app.org_id', true) IS NOT NULL
AND org_id = current_setting('app.org_id', true));
CREATE POLICY my_table_insert ON my_table FOR INSERT
WITH CHECK (current_setting('app.org_id', true) IS NOT NULL
AND org_id = current_setting('app.org_id', true));
CREATE POLICY my_table_update ON my_table FOR UPDATE
USING (current_setting('app.org_id', true) IS NOT NULL
AND org_id = current_setting('app.org_id', true))
WITH CHECK (current_setting('app.org_id', true) IS NOT NULL
AND org_id = current_setting('app.org_id', true));
表约定:每个表都应包含 id UUID PRIMARY KEY DEFAULT gen_random_uuid()、org_id TEXT NOT NULL、created_at TIMESTAMPTZ NOT NULL DEFAULT NOW();可变表还应包含 updated_at TIMESTAMPTZ(并配有触发器)。在第一个迁移中启用 pgcrypto 扩展。
访问模式
| 谁发起查询 | 方式 | 身份验证 |
|---|---|---|
| 应用服务 | 服务环境中的 ${managed.db.url} | 部署时注入连接字符串 |
| Agent 通过 CLI | eve db sql --env <env> | 作业令牌限制访问范围 |
| Agent 通过 RLS | 使用 app.current_user_id() 的 SQL | 运行时设置会话上下文 |
构建和发布流水线
标准流程
每个生产应用都应遵循 build → release → deploy → migrate → smoke-test:
pipelines:
deploy:
steps:
- name: build
action:
type: build # Creates BuildSpec + BuildRun, produces image digests
- name: release
depends_on: [build]
action:
type: release # Creates immutable release from build artifacts
- name: deploy
depends_on: [release]
action:
type: deploy # Deploys release to target environment
- name: migrate
depends_on: [deploy]
action:
type: job
service: migrate # Runs eve-migrate against the managed DB
- name: smoke-test
depends_on: [migrate]
script:
run: ./scripts/smoke-test.sh
timeout: 300
此顺序很重要的原因:
build会生成 SHA256 镜像摘要。release会固定这些确切的摘要。deploy使用固定的发布版本。部署的内容与构建的内容完全一致——不会出现标签漂移或“latest”带来的意外。migrate在部署之后运行,因为必须先配置好托管数据库。eve-migrate 作业会应用所有待处理的 SQL 迁移。smoke-test会在流水线报告成功前端到端验证已部署的服务。
镜像仓库选择
| 选项 | 使用场景 |
|---|---|
registry: "eve" | 默认选项。带有 JWT 身份验证的内部镜像仓库。配置最简单。 |
| BYO 镜像仓库(GHCR、ECR) | 需要在 Eve 之外访问镜像,或已有 CI 时使用。 |
registry: "none" | 仅使用公开基础镜像。不构建自定义镜像。 |
对于 GHCR,在 Dockerfile 中添加 OCI 标签,以自动关联仓库:
LABEL org.opencontainers.image.source="https://github.com/YOUR_ORG/YOUR_REPO"
构建配置
每个使用自定义镜像的服务都需要 build 部分:
services:
api:
build:
context: ./apps/api
dockerfile: Dockerfile
image: ghcr.io/org/my-api
使用多阶段 Dockerfile。BuildKit 可原生处理这类文件。将 OCI 标签放在最后一个阶段。
部署和环境
环境策略
| 环境 | 类型 | 用途 | 流水线 |
|---|---|---|---|
staging | 持久 | 集成测试、演示 | deploy |
production | 持久 | 线上流量 | deploy(通过晋级发布) |
preview-* | 临时 | PR 预览、功能分支 | deploy(自动清理) |
在清单中将每个环境关联到一个流水线:
environments:
staging:
pipeline: deploy
production:
pipeline: deploy
部署模式
标准部署:eve env deploy staging --ref main --repo-dir . 会触发已关联的流水线。
直接部署(绕过流水线):遇到紧急情况或配置简单时,使用 eve env deploy staging --ref <sha> --direct。
晋级:在 staging 中构建一次,然后将相同的发布制品晋级到 production。构建步骤的摘要会沿用,从而确保镜像完全一致。
恢复
部署失败时:
- 诊断:
eve env diagnose <project> <env>— 显示运行状况、近期部署和服务状态。 - 日志:
eve env logs <project> <env>— 容器输出。 - 回滚:重新部署上一个已知可用的版本。
- 重置:
eve env reset <project> <env>— 最彻底的选项,从头重新配置。
设计应用时要确保其支持安全回滚:迁移应向前兼容,服务在滚动部署期间应能妥善处理架构版本不匹配的情况。
系统应用模式
平台层和管理类应用通过与客户应用相同的 Eve 流水线部署,不需要特殊流程。Eve Dashboard 是典型示例:它通过标准的构建 → 发布 → 部署流程交付,与其他应用一同纳入 CI 发布矩阵,并使用与任何客户应用相同的 SSO + 分析 API。如果你正在构建内部管理控制台、自有平台的控制平面或系统层界面,请采用此模式。其优势在于保持一致性:所有应用共用一套部署流程、一套身份验证流程和一套可观测性流程。避免另设定制的“仅限平台”部署流程——这类流程容易腐化并逐渐偏离。
应用集成的组织级 OAuth
与 Google Drive、Slack 或其他 OAuth 提供方集成的应用使用组织级凭据(BYOA——自带应用)。每个组织都注册自己的 OAuth 应用,从而可以控制品牌、作用域、速率限制和凭据轮换。
eve integrations configure google-drive --client-id "..." --client-secret "..."
eve integrations connect google-drive
设计影响:使用 Google Drive 数据或 Slack 消息的应用应通过 Eve API 引用集成令牌,而不是自行存储 OAuth 凭据。平台会使用组织注册的 OAuth 应用凭据处理令牌刷新。
工作流的事件触发器
工作流可以由平台事件触发,从而实现响应式自动化:
workflows:
on-deploy:
trigger:
system.event: environment.deployed
steps:
- name: smoke-test
script:
run: ./scripts/smoke-test.sh
on-ingest:
trigger:
system.event: doc.ingest.completed
steps:
- name: process
agent: doc-processor
事件来源包括:GitHub Webhook、Slack 事件、系统事件(部署、构建、摄取)、Cron 计划和手动触发器。触发器语法请参阅 eve-pipelines-workflows,完整事件目录请参阅 references/events.md。
应用 CLI 框架——Eve 之道
每个带 API 的应用都应提供 CLI。 这是 Eve 之道——Agent 通过 CLI 命令而非原始 REST 调用与应用数据交互。CLI 可为 Agent 提供易于发现、认证透明且类型安全的应用领域访问方式。它将每次操作的大模型调用次数从 3-5 次(curl)降至 1 次(CLI 命令),无需构造 URL 或转义 JSON,并能显示特定于领域的错误消息,而非 HTTP 状态码。
CLI 优先的重要性
当编码 Agent 需要读取或写入应用数据时,它面临两种选择:构造包含正确 URL、认证标头和 JSON 请求体的 curl 命令,或者运行 eden projects list --json。CLI 方案在各个方面都更胜一筹:
| 维度 | CLI | 原始 REST |
|---|---|---|
| 认证 | 无需显式处理(自动读取 EVE_JOB_TOKEN) | 手动构造标头 |
| URL | 无需处理(CLI 知道服务 URL) | 根据 EVE_APP_API_URL_* 构造 |
| 可发现性 | myapp --help | 阅读 OpenAPI 规范或文档 |
| 错误 | 特定于领域的消息 | HTTP 状态码 |
| 大模型成本 | 每次操作 1 次调用 | 每次操作 3-5 次调用 |
在清单中声明 CLI
services:
api:
x-eve:
api_spec:
type: openapi
cli:
name: myapp # Binary name on $PATH
bin: cli/bin/myapp # Pre-bundled executable (repo-bundled mode)
平台会自动从清单中发现带有 x-eve.cli 的服务,并将其添加到项目中所有 Agent 作业的 $PATH 中,无需显式指定 with_apis。只需在清单中声明 CLI,所有 Agent 都能使用它。Agent 可以运行 myapp --help 来了解其功能。声明详情请参阅 eve-manifest-authoring,完整的实现模式(打包、环境变量约定、测试)请参阅 references/app-cli.md。
设计指导
- 尽早构建 CLI。 不要等到 API“完成”后再开始。应在首批 API 端点推出时同步构建 CLI。Agent 会立即使用它。
- 与 API 的功能保持一致。 每个 REST 端点都应有对应的 CLI 子命令。
GET /items→myapp items list,POST /items→myapp items create --file data.json。 - 在所有命令中支持
--json。 默认输出为便于人类阅读的表格;--json则输出适用于 Agent 工作流的机器可读内容。 - 打包为单个文件。 使用 esbuild 生成独立的 Node.js 脚本并提交到代码库,实现零启动延迟。
- 让 Agent 技能使用 CLI。 技能说明中应写“使用
myapp items list”,而不是“用 curl 调用 API……”。
嵌入式对话
如果你的应用有聊天界面——例如侧边栏窗格、内联助手或“询问有关此对象的问题”按钮——请使用 Eve 的嵌入式对话 API 和聊天 SDK。不要自行实现身份验证、分发、SSE、重连或乐观发送。平台提供基于应用指定标识符(例如 myapp:{project_id}:{conversation_id})的查找或创建线程路由、经 JWKS 验证的浏览器令牌,以及可重放的 SSE 流;这些功能已封装在 @eve-horizon/chat 和 @eve-horizon/chat-react 中,约 50 行 UI 代码即可完成集成。
设计注意事项:
- 通过稳定键将产品对象(文档、项目、工单)映射到 Eve 线程。线程状态由平台负责;你的应用负责管理引用
thread_id的产品投影。 - 使用与外部网关相同的基础机制,通过
chat.yaml、Agent、团队或工作流进行路由。聊天只是另一个分发目标。 - 第一阶段的推送使用带快照和轮询补齐的 SSE;设计 UI 时应允许短暂重连,而不要假设套接字会一直保持连接。
SDK 结构和路由模式请参阅 eve-read-eve-docs 技能中的 references/eve-sdk.md。
应用取消部署/删除生命周期
管理环境和项目的完整生命周期:
# Undeploy services (stops pods, keeps env record and history)
eve env undeploy <project> <env>
# Delete environment entirely (cascades to managed DB, secrets)
eve env delete <project> <env>
# Delete project (cascades to all environments, artifacts, history)
eve project delete <project-id>
设计应用时应确保可以干净地拆除:迁移应具备幂等性;托管数据库删除后不可恢复;即使环境已删除,流水线历史仍会保留在审计日志中。
密钥与配置
作用域模型
密钥按层级优先级解析:项目 > 用户 > 组织 > 系统。项目级的 API_KEY 会覆盖组织级的 API_KEY。
设计规则
- 按项目设置密钥。 使用
eve secrets set KEY "value" --project proj_xxx。确保项目密钥自成体系。 - 在清单中使用插值。 在服务环境变量配置块中引用
${secret.KEY}。平台会在部署时进行解析。 - 部署前进行验证。 运行
eve manifest validate --validate-secrets,在缺失的密钥引用导致部署失败之前将其找出。 - 本地开发时使用
.eve/dev-secrets.yaml。 使用本地值映射生产环境中的密钥。此文件已加入 gitignore。 - 切勿直接将密钥存储在环境变量中。 始终使用
${secret.KEY}插值,确保密钥通过平台的解析和审计链路流转。
服务令牌(在清单中声明,默认只读)
代表自身调用 Eve API 的服务会自动注入 EVE_SERVICE_TOKEN。每项服务的权限都在清单中显式声明,且默认为只读——需要写入的服务必须主动选择启用。请将此视为最小权限约定:服务不应仅因部署就悄然获得写入权限。声明每项服务所需的最少能力,并通过代码审查发现权限变更。有关声明语法,请参阅 eve-read-eve-docs 中的 references/secrets-auth.md 和 references/manifest.md。
范围限定的作业令牌(平台层级最小权限)
对于通过多租户资源编排作业的平台层级应用,仅有权限名称并不足够——orgfs:read 并未说明 具体是哪个前缀。在工作流步骤上声明资源范围,以便在令牌层级强制执行最小权限:
workflows:
scoped-review:
scope:
orgfs: { allow_prefixes: [/groups/projects/proj-a/**] }
steps:
- name: review
agent: { name: reviewer }
scope:
cloud_fs: { allow_mount_ids: [mount_a] }
范围轴与访问绑定相匹配:orgfs、orgdocs、envdb、cloud_fs。工作流、步骤和调用范围会求交集(交集为空时默认拒绝),并持久化为 jobs.token_scope。编排器会使用同一范围来构建工作区 .org 挂载并签发作业令牌——磁盘上的视图与 API 授权保持一致。设计平台层级工作流时,应确保每个步骤只能访问其实际需要的资源。
Git 凭据
Agent 需要访问代码仓库。将 github_token(HTTPS)或 ssh_key(SSH)设为项目机密。工作器会在 Git 操作期间自动注入这些凭据。
SSO 身份验证
为应用添加 SSO
Eve 提供共享身份验证包,可省去样板代码。只需约 25 行代码即可为 Eve 添加 SSO 登录。
后端(@eve-horizon/auth):
import { eveUserAuth, eveAuthGuard, eveAuthConfig } from '@eve-horizon/auth';
app.use(eveUserAuth()); // Parse tokens (non-blocking)
app.get('/auth/config', eveAuthConfig()); // Serve SSO discovery
app.get('/auth/me', eveAuthGuard(), (req, res) => {
res.json(req.eveUser); // { id, email, orgId, role }
});
app.use('/api', eveAuthGuard()); // Protect all API routes
前端(@eve-horizon/auth-react):
import { EveAuthProvider, EveLoginGate } from '@eve-horizon/auth-react';
function App() {
return (
<EveAuthProvider apiUrl="/api">
<EveLoginGate>
<ProtectedApp />
</EveLoginGate>
</EveAuthProvider>
);
}
如需从组件发起经过身份验证的 API 调用,请使用 createEveClient:
import { createEveClient } from '@eve-horizon/auth-react';
const client = createEveClient('/api');
const res = await client.fetch('/data');
自定义身份验证门——如果需要控制加载和登录状态(自定义登录页面、更丰富的加载界面),请直接使用 useEveAuth(),而不是 EveLoginGate:
import { EveAuthProvider, useEveAuth } from '@eve-horizon/auth-react';
function AuthGate() {
const { user, loading, loginWithToken, loginWithSso, logout } = useEveAuth();
if (loading) return <Spinner />;
if (!user) return <LoginPage onSso={loginWithSso} onToken={loginWithToken} />;
return <AppShell user={user} onLogout={logout}><Routes /></AppShell>;
}
export default function App() {
return (
<EveAuthProvider apiUrl={API_BASE}>
<AuthGate />
</EveAuthProvider>
);
}
工作原理
EveAuthProvider检查sessionStorage中是否有缓存的令牌- 如果没有令牌,则探测 SSO broker
/session(根域 Cookie) - 如果存在 SSO 会话,则获取新的 Eve RS256 令牌
- 如果没有会话,则显示登录表单(SSO 重定向或粘贴令牌)
- 所有 API 请求都会包含
Authorization: Bearer <token>
NestJS 后端
在 main.ts 中将 eveUserAuth() 设为全局中间件。如果现有控制器需要的是 req.user 而不是 req.eveUser,请添加一个轻量桥接层,在一处将 Eve 角色映射为应用专属角色:
import { eveUserAuth } from '@eve-horizon/auth';
app.use(eveUserAuth());
app.use((req, _res, next) => {
if (req.eveUser) {
req.user = { ...req.eveUser, role: req.eveUser.role === 'member' ? 'viewer' : 'admin' };
}
next();
});
自动注入的变量
平台会将 EVE_SSO_URL、EVE_API_URL、EVE_ORG_ID 和 EVE_PROJECT_ID 注入已部署的容器中。无需手动配置。在清单的环境变量区块中使用 ${SSO_URL},以配置前端可访问的 SSO URL。EVE_PROJECT_ID 可让平台将品牌设置、重定向允许列表和应用范围的身份验证策略限定到你的项目。
无密码 / 魔法链接应用
如果你的应用入驻流程是“管理员邀请用户,用户点击邮件,随后进入应用”,就应将其建模为无密码应用。不要构建不需要的密码重置流程。在清单中声明此配置,让平台呈现带有品牌标识的魔法链接登录页面:
x-eve:
auth:
login_method: magic_link # or password_or_magic_link, password
self_signup: false # unknown emails return generic success, no email
invite_requires_password: false # invite acceptance skips /set-password
设计影响:
- 用户通过邀请创建,而非自行注册。请据此规划管理员邀请体验。
- 魔法链接邮件与邀请邮件共用同一个
x-eve.branding区块——一个品牌配置来源,两种邮件副本。 - Eve API 会在发送邮件之前处理资格检查(现有用户、待处理邀请、域名注册),因此无需应用端额外处理,也能防止账户枚举。
应用品牌(邀请邮件和魔法链接邮件)
每个应用的品牌设置属于清单配置,而非代码配置。只需声明一次,平台便会将其应用于邀请邮件、魔法链接邮件和 SSO 登录页面:
x-eve:
branding:
app_name: "ACME Portal"
app_logo_url: "https://app.example.com/assets/logo.svg"
primary_color: "#1f6feb"
email_from_name: "ACME Portal"
reply_to_email: "[email protected]"
support_email: "[email protected]"
support_url: "https://example.com/help"
From: 显示名称和邮件正文体现应用身份;发件人地址仍使用平台默认值(按租户验证 From: 将在后续阶段推出)。品牌应视为项目属性,而非组织级设置。
应用组织访问和应用内管理员邀请
默认情况下,应用仅限其项目所属组织访问。对于服务多个客户组织的多租户应用,请声明允许列表,并可选择启用应用内管理员邀请:
x-eve:
auth:
org_access:
mode: allowlist
allowed_orgs: [org_customer123, customer-slug]
invite:
enabled: true
admin_roles: [admin, owner]
invited_role: member # fixed; app invites cannot create admins
设计影响:
- 后端应使用
eveAppUserAuth()(而非eveUserAuth()),这样授权依据的是应用策略,而非项目所属组织。中间件会查询GET /auth/app-access,并从X-Eve-Org-Id、?eve_org_id=或第一个允许访问的组织中选择组织。 - 管理员页面应围绕
POST /auth/app-invites构建(发送带品牌标识的魔法链接,引导用户完成入门流程),而非使用通用的组织邀请 API。平台会在发送前强制执行组织角色和应用允许列表策略。 - React SDK 提供
useEveAppAccess(),用于列出组织和管理员组织,以及邀请成员。
基于域名的注册(路径 C 自动关联)
如果应用需要服务整个客户邮件域名——“任何拥有 @yourcompany.com 地址的用户都应以成员身份加入 org_yourcompany”——请使用域名注册。Eve 会在首次请求魔法链接时创建一次性邀请,并在用户接受后自动关联其成员身份。一个项目可以将不同域名路由到不同组织:
x-eve:
auth:
org_access:
mode: allowlist
allowed_orgs: [org_acme, org_globex]
domain_signup:
enabled: true
domains:
- { domain: acme.com, target_org: org_acme, role: member }
- { domain: globex.com, target_org: org_globex }
设计影响:
- 规则按声明顺序依次匹配,首个匹配项生效。请先声明范围更具体的模式。
- 信任模型为“运营方声明,平台信任”——v1 不进行 DNS 验证。声明
free-mail.example会产生一致性警告,但仍会被允许。 - 明确待处理的邀请(路径 B)始终优先于域名注册(路径 C);已属于允许访问组织的现有用户(路径 A)则会收到标准的品牌化邮件。
- 删除规则会阻止新用户注册,但不会追溯移除现有成员身份——请通过明确执行
eve org members remove来规划用户离开流程。 - 可通过
auth.domain_signup.invite_created和auth.domain_signup.member_attached事件进行审计。
项目级重定向允许列表(自定义域名)
如果应用运行在自己的自定义域名上(而非集群域名下),请声明 SSO 可重定向到哪些来源。否则,SSO 会将重定向改写为平台默认主机名,导致品牌化的应用 URL 丢失:
x-eve:
auth:
allowed_redirect_origins:
- https://app.example.com
- https://www.example.com
最终允许列表由清单条目、项目自身符合条件的自定义域名,以及(对于 mode: allowlist)由 allowed_orgs 中项目拥有的跨组织自定义域名组成。这取代了旧版硬编码的 EVE_DEFAULT_DOMAIN 允许列表。平台还会确保 SSO 会话 Cookie 使用 SameSite=None; Secure,这样来自自定义域名前端的跨站 /session 探测请求便会携带凭据——你不再需要自行配置 Cookie 属性。
设计规则
- 使用 SDK,而非自定义身份验证。 SDK 可用约 50 行代码取代约 750 行手写身份验证代码。
- 优先使用非阻塞中间件。 全局使用
eveUserAuth(),再对受保护路由使用eveAuthGuard()。这样即可混合使用公开和私有路由。 /auth/config端点是握手入口。 前端通过调用后端的eveAuthConfig()端点来获取 SSO URL。这样前端无需依赖平台环境变量,并且在本地开发和部署环境中的工作方式完全一致。- 考虑令牌过期问题。
orgsJWT 声明反映令牌签发时的成员关系(TTL 为 1 天)。如需立即撤销,请使用strategy: 'remote'。
完整 SDK 参考请参阅 eve-read-eve-docs 技能中的 references/auth-sdk.md。
可观测性和调试
调试阶梯
按以下阶段逐步排查:
1. Status → eve env show <project> <env>
2. Diagnose → eve env diagnose <project> <env>
3. Logs → eve env logs <project> <env>
4. Pipeline → eve pipeline logs <pipeline> <run-id> --follow
5. Recover → eve env deploy (rollback) or eve env reset
从最上层开始。每个阶段提供更多细节,成本也更高。大多数问题在第 1-2 阶段即可解决。
流水线可观测性
实时监控流水线执行情况:
eve pipeline logs <pipeline> <run-id> --follow # stream all steps
eve pipeline logs <pipeline> <run-id> --follow --step build # stream one step
失败的步骤会包含失败提示,并在适用时链接到构建诊断信息。
构建调试
构建失败时:
eve build list --project <project_id>
eve build diagnose <build_id>
eve build logs <build_id>
常见原因:缺少注册表凭据、Dockerfile 路径不匹配、构建上下文过大。
健康检查
为服务设计健康检查端点。Eve 会轮询健康状态,以确定部署是否就绪。当 ready === true 且 active_pipeline_run === null 时,部署即告完成。
设计检查清单
服务拓扑:
- 每个服务只负责一项职责
- 对 Postgres 的需求声明托管数据库
- 使用
x-eve.external: true标记外部服务 - 仅为面向公众的服务启用入口
- 使用平台注入的环境变量(而非硬编码 URL)
- 选择主机名策略(生成的 URL、别名,或通过
x-eve.ingress.domains设置自定义域名) - 仅为需要与按源 IP 设置允许列表的供应商集成的服务启用稳定出站连接
数据库:
- 迁移使用
db/migrations/中带时间戳前缀的纯 SQL 文件 - 在清单中声明
eve-migrate作业服务,并挂载x-eve.files -
DatabaseService使用 RLS 上下文(set_config)封装所有数据库访问 - 为每个包含
org_id的表设置 RLS 策略 - 启用
pgcrypto扩展,使用 UUID 主键,并设置updated_at触发器 - 通过架构或约定将应用数据与 Agent 数据分开
流水线:
- 定义规范的
build → release → deploy → migrate → smoke-test流水线 - 部署后运行迁移步骤(必须先有托管数据库)
- 冒烟测试脚本端到端验证已部署的服务
- 选择注册表,并将凭据设为机密
- Dockerfile 中包含 OCI 标签(用于 GHCR)
- 发布流程传递镜像摘要(不按标签部署)
环境:
- 定义暂存和生产环境
- 将每个环境关联到一个流水线
- 定义晋级工作流(构建一次,多次部署)
- 了解恢复流程(诊断 -> 回滚 -> 重置)
机密:
- 使用
eve secrets set为每个项目设置所有机密 - 清单使用
${secret.KEY}插值 -
eve manifest validate --validate-secrets验证通过 - 本地开发所需的
.eve/dev-secrets.yaml已存在 - 已配置 Git 凭据(
github_token或ssh_key) - 按服务声明服务令牌权限(默认只读;明确选择启用写入权限)
身份验证:
- 在后端添加
@eve-horizon/auth中间件(eveUserAuth+eveAuthGuard,或多组织应用使用eveAppUserAuth) - 身份验证配置端点提供 SSO 发现信息(
eveAuthConfig) - 前端使用
@eve-horizon/auth-react封装(EveAuthProvider+EveLoginGate,或自定义useEveAuth门禁) - 前端使用
createEveClient发起经过身份验证的 API 调用 - 使用平台注入的身份验证环境变量(
EVE_SSO_URL、EVE_ORG_ID、EVE_PROJECT_ID) - 在一处将 Eve 角色映射为应用角色(桥接中间件),而非分散在各个控制器中
- 如果使用无密码登录:设置
x-eve.auth.login_method: magic_link+invite_requires_password: false - 如果是多租户:设置
x-eve.auth.org_access.mode: allowlist并声明allowed_orgs;应用内管理员邀请使用POST /auth/app-invites - 如果基于域名进行引导注册:
domain_signup.domains为每条规则声明target_org;更具体的规则排在前面 - 如果使用自定义域名:声明
x-eve.auth.allowed_redirect_origins;运行eve project auth-context确认解析后的允许列表 - 如果使用品牌定制:声明
x-eve.branding(邀请邮件和魔法链接邮件共用);设置From:显示名称和徽标
应用 CLI(Eve 方式):
- 使用领域 CLI 封装应用 API(例如
eden projects list) - 在清单中通过
x-eve.cli声明 CLI,并设置name和bin - 将 CLI 打包为单文件可执行程序(Node.js 使用 esbuild)
- CLI 自动读取
EVE_APP_API_URL_{SERVICE}和EVE_JOB_TOKEN - 所有 CLI 命令均支持
--json,以输出机器可读内容 - Agent 技能引用 CLI 命令,而非原始 curl/REST 调用
可观测性:
- 服务提供健康检查端点
- 了解调试流程(status -> diagnose -> logs -> recover)
- 可通过
eve pipeline logs --follow访问流水线日志
交叉引用
- 清单语法和选项:
eve-manifest-authoring - 部署命令和错误排查:
eve-deploy-debugging - 密钥管理和访问组:
eve-auth-and-secrets - 流水线和工作流定义:
eve-pipelines-workflows - 本地开发工作流:
eve-local-dev-loop - 在此基础上叠加 Agent 能力:
eve-agentic-app-design - Auth SDK 和 SSO 集成:
eve-read-eve-docs→references/auth-sdk.md - 对象存储和文件系统:
eve-read-eve-docs→references/object-store-filesystem.md - 外部集成(Slack、GitHub):
eve-read-eve-docs→references/integrations.md