Packages 包@downcity/city

Service 与 Action

Service、Action、统一路由和 AIService 在 @downcity/city 里的关系。

ServiceAction@downcity/city 里最重要的业务组织单位。

先记一句话就够了:

Service 是一组长期存在的业务能力,Action 是真正被调用的具体入口。

这个概念在说什么

你最终给产品暴露的,不是一个“随便写的 handler”,而是 City 里统一管理的一组 action。

每个 Action 同时承担四个角色:

  • 业务入口
  • HTTP 路由节点
  • hook 作用点
  • 权限边界

所以理解 Service / Action,本质上就是理解 Downcity City 如何组织业务能力。

什么时候应该写 custom service

  • 你要表达的是产品业务动作,而不是单纯模型推理
  • 你要让前端、App、服务都走统一的业务入口
  • 你希望一个能力能被 hook、鉴权、usage 和服务治理

典型例子:

  • 改写内容
  • 生成报告
  • 执行工作流
  • 处理上传后的业务逻辑

什么时候更适合先用 AIService

如果你只是想提供这些 AI 能力:

  • text
  • stream
  • image
  • video

优先从 AIService 开始会更自然,因为它已经替你处理了模型目录和 AI 输出约定。

最小 custom service 示例

import { CityBase, Service } from "@downcity/city";

const base = new CityBase({ db });

const translate = new Service({
  id: "translate",
  name: "翻译",
});

translate.action("zh2en", async (ctx) => {
  return {
    text: await runTranslate(String(ctx.input.text ?? ""), "en"),
  };
});

translate.action("list", async () => {
  return {
    items: ["zh2en"],
  };
}, {
  method: "GET",
});

base.use(translate);

这里你做了三件事:

  1. 创建一个 service
  2. 往里面注册多个 action
  3. 把整个 service 挂到 City

路由会怎么暴露

上面的 Action 会自动进入统一路由空间:

POST /v1/translate/zh2en
GET  /v1/translate/list

这也是为什么产品端最后统一都能通过 @downcity/city 调:

const result = await client
  .service("translate")
  .action("zh2en")
  .invoke<{ text: string }>({
    text: "你好 Downcity",
  });

const list = await client.service("translate").get("list");

常见场景

场景一:产品业务 Action

const writer = new Service({ id: "writer", name: "内容写作" });

writer.action("draft", async (ctx) => {
  return {
    title: "Draft",
    topic: ctx.input.topic,
  };
});

适合:

  • 业务工作流
  • 自定义生成结果
  • 你自己的 SaaS 能力

场景二:管理侧 Action

writer.action("remove", async (ctx) => {
  await removeDraft(String(ctx.input.id));
  return { ok: true };
}, {
  auth: ["admin"],
});

适合:

  • 只能由可信后端执行的动作
  • 运维、删除、配置变更

场景三:guest 入口

writer.action("webhook", async (ctx) => {
  return { accepted: true };
}, {
  auth: [],
});

适合:

  • 第三方 webhook
  • 登录前入口
  • 回调型 Action

AIService 和普通 Service 的关系

AIService 不是另一套协议,而是 City 里一类更偏 AI 的 service。

它额外提供:

  • 模型目录
  • 多 modality 调用
  • OpenAI 兼容入口
  • 更适合 UIMessage / stream 的返回约定

所以你可以这样区分:

  • 直接模型推理:优先 AIService
  • 业务动作或服务动作:优先 Service

常用 API / 入口

这一页最相关的调用入口有:

  • new Service({ id, name })
  • service.action(name, handler, options?)
  • options.method: "GET" | "POST"
  • options.auth: ["user"] | ["admin"] | []
  • base.use(service)
  • client.service(id).action(name).invoke(input)
  • client.service(id).get(path, query?)

常见误解

Action 不只是一个函数

它最终会成为:

  • HTTP 路由
  • 鉴权边界
  • hook 作用点
  • usage 记录点

service 和 custom service 不需要两套调用方式

对产品端来说,它们最终都统一走:

client.service("service_id").action("action_id").invoke(...)

GET / POST 的差别不是语法装饰

读操作更适合 GET,例如:

  • me
  • list
  • summary

写操作、执行动作更适合 POST,例如:

  • login
  • draft
  • checkout/create

继续阅读