Packages 包@downcity/city

City 运行时

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

City 是 Downcity 的长期运行容器。它不是一个零散函数集合,而是把 Drizzle db、service、鉴权和统一 /v1/* 路由收在同一个服务端入口里。

这个概念在说什么

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

它长期持有这些能力:

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

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

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

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

什么时候这页不是重点

最小可运行示例

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

import { AIService, City } 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 CityBase({ db, dialect: "sqlite", raw: sqlite });

const ai = new AIService();

ai.use({
  id: "local-echo",
  name: "Local Echo",
  default: ["text"],
  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: base.router().fetch,
  port: 43127,
  hostname: "127.0.0.1",
});

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

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

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

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

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

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

这种方式适合:

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

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

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

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

这种方式适合:

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

为什么只传 db

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

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

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

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

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

常用 API / 入口

这一页最相关的 City API 是:

  • new CityBase({ db }):创建 CityBase 实例
  • base.use(...):挂 service、AIService 和官方服务
  • base.router():拿到底层 HTTP router
  • base.handleRequest(request):处理单次请求
  • base.health():触发初始化并返回健康信息
  • base.table(name):拿某张表的受控数据接口

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

常见误解

City 是 runtime,不是整个 SDK

City 是服务端长期运行容器。产品侧真正调用它用的是 @downcity/city

City 不是纯 HTTP 代理

它背后还持有:

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

City 不等于某一个 AI provider

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

继续阅读