参考

Admin City

可信环境如何管理 city、签发 user_token,并维护 Runtime env。

Admin City 只应该运行在可信环境里。

典型场景:

  • 你自己的产品后端
  • 本地管理脚本
  • 内网管理工具
  • CI 或运维脚本

不要把 admin_secret_key 放到浏览器、公开前端或不受控客户端。

它在整条链路里负责什么

Admin City 看成“可信侧和 City 的管理桥”最准确。

它最常负责三件事:

  • city
  • user_token
  • 维护 runtime env
  • 执行可信侧 balance / redeem_code 管理动作

如果 User City 负责“用户态怎么调用 City”,那 Admin City 负责“可信侧怎么把这个调用环境准备好”。

最小示例

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

const admin = new City({
  role: "admin",
  federation_url: "https://base.example.com",
  city_id: "city_demo",
  admin_secret_key: process.env.DOWNCITY_FEDERATION_ADMIN_SECRET_KEY,
});

如果没有显式传 admin_secret_key,SDK 会尝试从 process.env.DOWNCITY_FEDERATION_ADMIN_SECRET_KEY 读取。

典型调用链

可信后端完成登录、鉴权和业务判断。
Admin City调用 /v1/cities/*/v1/cities/tokens/apply/v1/env/*/v1/ai/models/v1/base/instruction
Downcity校验 admin_secret_key 后,再执行 city、token 和 env 管理动作。

tokens.apply()

这是最常用的管理端能力:为某个用户签发 user_token

city_idCity 构造时传入,会自动注入到请求中。调用方只需传 user_id

const issued = await admin.tokens.apply({
  user_id: "user_123",
  metadata: {
    plan: "pro",
    org_id: "org_001",
  },
  ttl: "7d",
});

返回值包含:

  • user_token
  • city_id
  • user_id
  • expires_at

ttl 支持:

  • 30m
  • 1h
  • 7d
  • 秒数

推荐登录链路

router.post("/login", async (c) => {
  const user_id = await login(c);

  const issued = await admin.tokens.apply({
    user_id,
    ttl: "7d",
  });

  return c.json({
    city_id: issued.city_id,
    user_token: issued.user_token,
  });
});

也就是:

  1. 你的业务后端先完成用户登录
  2. 再由可信后端向 City 申请 user_token
  3. city_id + user_token 返回给前端
  4. 前端再用 User City 调 City

它和 accounts 服务是什么关系

Admin City 不会替代 accounts 服务,反过来也一样。

两种典型模式是:

模式 A:你自己的后端做登录

  • 业务后端自己校验用户名密码或 session
  • 业务后端再用 Admin City 申请 user_token
  • 前端拿到 city_id + user_token

模式 B:accounts 服务做登录

  • 前端先用 guest User Cityaccounts.login/start -> login/continue -> login/result 或注册流程
  • accounts service 通过 login/result 返回 user_token
  • 前端切换到正常 User City

所以:

  • Admin City 负责可信侧管理动作
  • accounts 服务负责最小账号能力
  • 两者不是替代关系,而是两种不同的登录接法

env

admin.env 负责 Runtime env 管理。写入的 provider key 会保存到 Federation 数据库,Runtime 读取时优先使用数据库值。

如果你想确认某个代码注册模型依赖哪些 provider env,可以直接通过 admin.listModels() 读取同一份模型目录,再和 admin.env.list() 对照:

const models = await admin.listModels();

list()

const envs = await admin.env.list();

upsert()

await admin.env.upsert({
  key: "OPENAI_API_KEY",
  value: "sk-xxx",
});

remove()

await admin.env.remove("OPENAI_API_KEY");

import()

await admin.env.import(`
OPENAI_API_KEY=sk-xxx
OPENAI_BASE_URL=https://api.openai.com/v1
`);

refresh()

await admin.env.refresh();

通过 admin.env.upsert()remove()import() 或 fed admin 工作区修改 env 时,当前 Runtime cache 会自动更新。

如果你绕过 Admin API 直接修改数据库里的 env 表,需要调用 admin.env.refresh(),或在 fed admin 工作区的 Env 菜单执行 Refresh runtime cache

这些修改会写入 Federation 数据库的 env 表。业务 env 和系统级 secret 都以 Federation 这张表为准,不需要再去手动改 Worker / Node 宿主 env。

cities

admin.cities 用来管理同一个 Federation 里的产品边界。

const cities = await admin.cities.list();

const city = await admin.cities.create({
  name: "My App",
  city_id: "city_my_app",
});

await admin.cities.pause(city.city_id);
await admin.cities.activate(city.city_id);
await admin.cities.remove(city.city_id);

如果你想在一次调用里给任意 city 签发 token,可以使用 admin.cities 下更底层的 token 管理器:

const issued = await admin.cities.tokens.apply({
  city_id: "city_my_app",
  user_id: "user_123",
  ttl: "7d",
});

如果一个 Admin City 实例只管理一个目标 city,优先使用 admin.tokens.apply()。它会自动注入构造时传入的 city_id

listServices() / listModels() / instruction()

除了 tokensenv,管理端还可以读取 City 当前暴露的能力目录。

listServices()

const services = await admin.listServices();

返回当前 City 已注册的 service 列表,以及每个模块声明的 env 需求。

listModels()

const models = await admin.listModels();

这会返回 admin 视角的完整模型目录。相比用户态模型目录,它还会包含:

  • env_requirements

instruction()

const text = await admin.instruction();
console.log(text);

它对应 GET /v1/base/instruction,返回 City 聚合后的纯文本说明文档,适合:

  • 远程管理端查看当前 City 挂了哪些模块
  • 确认每个模块暴露了哪些路由
  • 确认每个模块声明了哪些 env 依赖
  • 给 CLI 或 agent 读取运行时说明

Admin City 也可以调 service 的管理侧 service

除了 tokensenvAdmin City 也可以调用 service 暴露出来的管理侧 service。

例如:

const users = await admin.service("accounts").get("users");
const sessions = await admin.service("accounts").get("sessions");

const payments = await admin.service("payment").get("payments");

现在 balance 还提供了一层 typed invoker,适合管理余额和 redeem_code

const issued = await admin.balance.redeemCodes.create({
  credits: 300_000_000,
  note: "campaign gift",
});

await admin.balance.redeemCodes.disable({
  redeem_code_id: issued.redeem_code_id,
});

也就是说,Admin City 不只是在调内建 token/env 接口,它同样是可信侧调用统一 service 路由的入口。

错误处理

Admin City 收到非 2xx HTTP 响应时会抛出 Error。这个错误会带两个额外字段:

  • status:HTTP 状态码。
  • body:City 返回的原始响应文本,通常是 {"error":"..."}
try {
  await admin.tokens.apply({
    user_id: "user_123",
  });
} catch (error) {
  const status = error instanceof Error && "status" in error ? error.status : undefined;
  const body = error instanceof Error && "body" in error ? error.body : undefined;

  console.log(status, body);
}

常见状态码:

  • 401admin_secret_key 缺失或错误。
  • 403:目标 city 已暂停,不能继续签发 token。
  • 404:目标 city 不存在。
  • 500:City 缺少必要配置,或管理动作内部执行失败。

当前不归 Admin City 管的事情

这几件事当前不走 Admin City

  • 模型配置
  • 注册 service handler
  • 用户前端直接登录交互
  • 浏览器里的用户态调用

它们分别属于:

  • City 运行时层

也就是说,Admin City 负责维护 city、token 和 env;模型定义和 service 挂载方式仍然属于 City 运行时层。