mcpField
E
eve-fullstack-app-design

eve-fullstack-app-design

github.com/incept5云与 DevOps0273 次安装17 周前更新
代码仓库

关于 eve-fullstack-app-design

eve-fullstack-app-design 是面向在 Eve Horizon 上设计或迁移应用的团队的架构技能。它以 Eve manifest 规划服务、基础设施、数据库和部署,并提供常见全栈布局的模式。

主要功能

  1. 01以 manifest 为核心的应用架构
  2. 02服务拓扑与入口规划
  3. 03托管及外部服务指导
  4. 04数据库与迁移模式
  5. 05SPA、API 和数据库参考布局

适用场景

  1. 01规划新的 Eve 应用
  2. 02将现有应用迁移到 Eve
  3. 03选择服务与数据库架构
  4. 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.chatAgent 配置文件、团队分派、聊天路由
运行时默认值x-eve.defaultsHarness、工作区、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)
服务设计规则
  1. 每个服务只负责一项职责。 将 HTTP 服务与后台处理分开。API 服务也不应运行定时任务。
  2. Postgres 使用托管数据库。 声明 x-eve.role: managed_db,由平台负责配置、连接并注入凭据。无需手动设置连接字符串。
  3. 明确标记外部服务。 对托管在 Eve 之外的服务(Redis、第三方 API),使用 x-eve.external: true 和 x-eve.connection_url。
  4. 一次性任务使用 x-eve.role: job。 迁移、种子数据和数据回填都属于作业服务,而非持久进程。
  5. 有意配置入口。 只有需要外部 HTTP 访问的服务才设置 x-eve.ingress.public: true。内部服务通过集群网络通信。
  6. 尽早选择主机名策略。 默认情况下,每个公共服务都会获得一个平台生成的 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 策略
  1. 迁移使用纯 SQL。 在 db/migrations/ 中创建以时间戳为前缀的 SQL 文件(例如,20260312000000_initial.sql)。使用 eve-migrate 运行(见上文“参考架构”)。切勿手动修改生产环境的架构。
  2. 从一开始就为 RLS 做好设计。 每个包含多租户数据的表都要添加 org_id TEXT NOT NULL、RLS 策略,以及一个用于设置会话上下文的 DatabaseService(见下文)。事后再添加行级安全会很麻烦。
  3. 修改前先检查。 使用 eve db schema 查看当前架构。开发期间使用 eve db sql --env <env> 执行临时查询。
  4. 将应用数据与 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 通过 CLIeve 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。构建步骤的摘要会沿用,从而确保镜像完全一致。

恢复

部署失败时:

  1. 诊断:eve env diagnose <project> <env> — 显示运行状况、近期部署和服务状态。
  2. 日志:eve env logs <project> <env> — 容器输出。
  3. 回滚:重新部署上一个已知可用的版本。
  4. 重置: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。

设计指导
  1. 尽早构建 CLI。 不要等到 API“完成”后再开始。应在首批 API 端点推出时同步构建 CLI。Agent 会立即使用它。
  2. 与 API 的功能保持一致。 每个 REST 端点都应有对应的 CLI 子命令。GET /items → myapp items list,POST /items → myapp items create --file data.json。
  3. 在所有命令中支持 --json。 默认输出为便于人类阅读的表格;--json 则输出适用于 Agent 工作流的机器可读内容。
  4. 打包为单个文件。 使用 esbuild 生成独立的 Node.js 脚本并提交到代码库,实现零启动延迟。
  5. 让 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。

设计规则
  1. 按项目设置密钥。 使用 eve secrets set KEY "value" --project proj_xxx。确保项目密钥自成体系。
  2. 在清单中使用插值。 在服务环境变量配置块中引用 ${secret.KEY}。平台会在部署时进行解析。
  3. 部署前进行验证。 运行 eve manifest validate --validate-secrets,在缺失的密钥引用导致部署失败之前将其找出。
  4. 本地开发时使用 .eve/dev-secrets.yaml。 使用本地值映射生产环境中的密钥。此文件已加入 gitignore。
  5. 切勿直接将密钥存储在环境变量中。 始终使用 ${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>
  );
}
工作原理
  1. EveAuthProvider 检查 sessionStorage 中是否有缓存的令牌
  2. 如果没有令牌,则探测 SSO broker /session(根域 Cookie)
  3. 如果存在 SSO 会话,则获取新的 Eve RS256 令牌
  4. 如果没有会话,则显示登录表单(SSO 重定向或粘贴令牌)
  5. 所有 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 属性。

设计规则
  1. 使用 SDK,而非自定义身份验证。 SDK 可用约 50 行代码取代约 750 行手写身份验证代码。
  2. 优先使用非阻塞中间件。 全局使用 eveUserAuth(),再对受保护路由使用 eveAuthGuard()。这样即可混合使用公开和私有路由。
  3. /auth/config 端点是握手入口。 前端通过调用后端的 eveAuthConfig() 端点来获取 SSO URL。这样前端无需依赖平台环境变量,并且在本地开发和部署环境中的工作方式完全一致。
  4. 考虑令牌过期问题。 orgs JWT 声明反映令牌签发时的成员关系(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

评论

支持 Markdown