参考

Admin City

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

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

典型场景:

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

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

它在整条链路里负责什么

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

它最常负责三件事:

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

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

最小示例

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

const admin = new City({
  role: "admin",
  city_url: "https://base.example.com",
  admin_secret_key: process.env.DOWNCITY_CITY_ADMIN_SECRET_KEY,
});

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

典型调用链

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

towns

list()

const items = await admin.towns.list();

create()

const town = await admin.towns.create({
  name: "Chrome Extension",
});

返回值包含:

  • town_id
  • name
  • status
  • created_at
  • updated_at

pause() / activate()

await admin.towns.pause(town.town_id);

await admin.towns.activate(town.town_id);

适合:

  • 暂停某个产品的所有用户调用
  • 临时下线某个产品
  • 重新开放一个 town

remove()

await admin.towns.remove(town.town_id);

当前语义就是删除 town 记录。调用前要先确认这是否符合你的业务预期。

tokens.apply()

这是最常用的管理端能力:为某个用户在某个 town 下申请 user_token

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

返回值包含:

  • user_token
  • town_id
  • user_id
  • expires_at

ttl 支持:

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

推荐登录链路

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

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

  return c.json({
    town_id: "town_xxx",
    user_token: issued.user_token,
  });
});

也就是:

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

它和 accounts 服务是什么关系

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

两种典型模式是:

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

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

模式 B:accounts 服务做登录

  • 前端先用 guest User Cityaccounts.login/register
  • accounts service 直接返回 user_token
  • 前端切换到正常 User City

所以:

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

env

admin.env 负责 Runtime env 管理。写入的 provider key 会保存到 City 数据库,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() 或 city CLI 修改 env 时,当前 Runtime cache 会自动更新。

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

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

listServices() / listModels() / instruction()

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

listServices()

const services = await admin.listServices();

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

listModels()

const models = await admin.listModels();

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

  • env_requirements
  • default_modes

instruction()

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

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

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

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

除了 towns.*env.*Admin 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.stripe").get("payments");

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

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

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

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

错误处理

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

  • status:HTTP 状态码。
  • body:City 返回的原始响应文本,通常是 {"error":"..."}
try {
  await admin.towns.tokens.apply({
    town_id: "town_xxx",
    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:目标 town 已暂停,不能继续签发 token。
  • 404:目标 town 不存在。
  • 500:City 缺少必要配置,或管理动作内部执行失败。

当前不归 Admin City 管的事情

这几件事当前不走 Admin City

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

它们分别属于:

  • City 运行时层

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