Packages 包@downcity/city

Federation 运行时

Federation 实例、runtime、serve 和统一入口在 @downcity/city 里的角色。

Federation 是 Downcity 的服务端长期运行容器(文档里也叫 Federation)。它不是一个零散函数集合,而是把 Drizzle db、service、鉴权和统一 /v1/* 路由收在同一个服务端入口里。产品侧访问它用的是 City 客户端,区别见 Federation 与 City

这个概念在说什么

可以先把 Federation 理解成“多个产品共用的 AI infrastructure runtime 进程”。

它长期持有这些能力:

  • Drizzle 数据库连接
  • service 注册表
  • 服务挂载点
  • HTTP 路由入口
  • token、city、env 等基础设施

所以你通常不是“调用一下 Federation”,而是“启动一套 Federation runtime,然后让多个产品 client 长期连接它”。

什么时候你需要关心这一页

  • 你准备第一次真正把 Federation 跑起来
  • 你想理解为什么 Federation 只需要一个 Drizzle db
  • 你已经会写 service,但还不清楚 fetch()serve() 各自用在什么地方
  • 你要把 Federation 接到现有 Node 服务、Cloudflare Worker 或别的 HTTP 入口上

什么时候这页不是重点

最小可运行示例

下面这个例子说明 Federation 运行时最小长什么样:

import { AIService, Federation } from "@downcity/city";
import { serve } from "@hono/node-server";
import Database from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";

const sqlite = new Database("./.base/downcity.sqlite");
sqlite.pragma("journal_mode = WAL");

const db = Object.assign(drizzle(sqlite), {
  $client: { exec: (sql: string) => sqlite.exec(sql) },
});

const base = new Federation({ db });

const ai = new AIService();

ai.use({
  id: "local-echo",
  name: "Local Echo",
  actions: {
    text: async (ctx) => ({
      id: crypto.randomUUID(),
      role: "assistant",
      parts: [
        {
          type: "text",
          text: String(ctx.input.prompt ?? ""),
          state: "done",
        },
      ],
    }),
  },
});

base.use(ai);

await base.health();

serve({
  fetch: (request) => base.fetch(request),
  port: 43127,
  hostname: "127.0.0.1",
});

这个例子里真正重要的是三件事:

  1. db 决定 Federation 使用哪一个 Drizzle 数据库
  2. base.use(...) 决定 Federation 暴露哪些能力
  3. base.fetch() 决定你怎么把它接进 HTTP 层

场景一:Federation 自己就是主服务

如果你想让 Federation 自己作为主 HTTP 入口,最直接的理解方式就是上面的 serve()

import { serve } from "@hono/node-server";

await base.health();
serve({ fetch: (request) => base.fetch(request), port: 3001, hostname: "127.0.0.1" });

这种方式适合:

  • 先快速跑起一个独立 Federation
  • 单独部署一个可复用的 AI backend
  • 小团队先把 Federation 和业务后端拆成两个清晰边界

场景二:接进你自己的服务框架

如果你已经有现成的 HTTP 框架,更常见的是按请求转给 Federation:

export default {
  async fetch(request: Request) {
    return base.fetch(request);
  },
};

这种方式适合:

  • Cloudflare Workers
  • 你已经有现成网关或 API server
  • 想把 Federation 当成内部运行时,而不是单独开一个端口

场景三:本机 HTTP

如果 Federation 只需要被同一台机器上的可信进程访问,可以直接把标准 fetch handler 挂到你的本机 HTTP server:

import { Federation } from "@downcity/city";
import { serve } from "@hono/node-server";

const base = new Federation({ db });

serve({
  fetch: (request) => base.fetch(request),
  hostname: "127.0.0.1",
  port: 15315,
});

const federation_url = "http://127.0.0.1:15315";

然后 City 可以直接连接这个 HTTP 地址:

import { City } from "@downcity/city";

const admin = new City({
  role: "admin",
  federation_url,
  city_id: "city_demo",
  admin_secret_key,
});

await admin.listServices();

本机 HTTP 和远程 HTTP 使用同一套产品侧模型:Admin City 使用 admin_secret_key,公开 action 可以作为 guest 调用,需要用户身份的 action 仍然必须携带正常的 user_token

如果 AccountsService 配置了 local_login: true/v1/accounts/providers 会只返回本地登录方式。调用 accounts.login/start 并传入 provider: "local"city_id 后会得到 login_id,再用 accounts.login/result 读取正常 user_token,之后用 city_id + user_token 创建或更新 User City。

为什么只传 db

@downcity/city 负责的是跨运行时都应该稳定的逻辑:

  • service 生命周期
  • action 路由
  • token 鉴权
  • city / env / store 基础设施

而具体宿主环境只负责把数据库对象创建好:

  • Node.js 可以用 drizzle-orm/better-sqlite3 或 Drizzle pg
  • Cloudflare Workers 可以用 drizzle-orm/d1
  • Federation 启动时会把需要的 env 写入内置 env

这就是为什么同一套 runtime 模型可以复用到不同宿主环境,而不是每换一个平台就重写一套后端。

默认 Storage

如果你的 Federation 需要把运行期产生的外部文件转存到自有存储,可以注册默认 storage:

import { Federation, R2Storage } from "@downcity/city";

const base = new Federation({ db });

base.storage(R2Storage({
  bucket: env.DOWNCITY_STORAGE,
  public_url_prefix: env.DOWNCITY_STORAGE_PUBLIC_URL_PREFIX,
}));

注册后,service 可以通过 ctx.storage 访问这套能力。内置 AI 图片任务会在 image_fetch 成功后自动把远程 file part URL 转存到默认 storage;如果转存失败,会保留上游原始 URL,不会让图片任务失败。

常用 API / 入口

这一页最相关的 Federation API 是:

  • new Federation({ db }):创建 Federation 实例
  • base.use(...):挂 service、AIService 和官方服务
  • base.storage(...):注册默认文件存储后端
  • base.fetch(request):处理单次请求
  • base.health():触发初始化并返回健康信息
  • base.table(name):拿某张表的受控数据接口

如果你是从“怎么写能力”进入,下一页更关键的是 Service 与 Action

常见误解

Federation 是 runtime,不是整个 SDK

Federation 是服务端长期运行容器。产品侧真正调用它用的是 City 客户端,见 Client SDK

Federation 不是纯 HTTP 代理

它背后还持有:

  • 内置表
  • token / city / env 基础设施
  • service 数据层
  • hook 生命周期

Federation 不等于某一个 AI provider

provider、model、service、custom service 都只是 Federation 上挂载的能力,不是 Federation 本身。

继续阅读