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:优先读 @downcity/city
- 你只想知道账号登录怎么跑通:优先读 @downcity/services
- 你只想看某个 custom service 怎么写:优先读 Service 与 Action
最小可运行示例
下面这个例子说明 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",
});这个例子里真正重要的是三件事:
db决定 City 使用哪一个 Drizzle 数据库base.use(...)决定 City 暴露哪些能力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 routerbase.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 本身。
继续阅读
- 想知道 City 里到底挂的是什么,读 Service 与 Action
- 想知道服务和扩展点怎么进去,读 Hook 与服务挂载
- 想知道数据层怎么落地,读 Store 与 Table
- 想看更底层 API,再读 City API