VisibleBase
参考

Base

Base 的初始化、service 注册、query fallback、hooks 和 HTTP 接入方式。

Base 是 VisibleBase 的服务端运行时入口。

你用它来做 4 件事:

  1. 初始化默认数据库和 Runtime env
  2. 注册 text()stream() 等 service
  3. default()match() 描述调用逻辑
  4. 通过 serve()handleRequest()router() 暴露成 HTTP 服务

最小可用示例

import { Base } from "@visiblebase/base";

const base = new Base();

base.text()
  .default({
    model: "gpt-5.4",
    temperature: 0.2,
  })
  .handle(async (ctx) => {
    return {
      id: crypto.randomUUID(),
      role: "assistant",
      parts: [
        {
          type: "text",
          text: String(ctx.query.prompt ?? ""),
          state: "done",
        },
      ],
    };
  });

await base.serve({
  host: "127.0.0.1",
  port: 3001,
});

这段代码里有两个关键语义:

  • default() 补的是 query fallback,不是“注册默认模型”
  • handler 统一从 ctx.query 读取 client 传来的最终参数

Base 的调用心智

请求进入 BaseHTTP 场景下会先校验 user_token、读取 product_id,再确认当前 service。
拼出最终 ctx.query先读 client query,再把 default() 返回的对象当作 fallback 合并进去。
解析模型并分流ctx.query.model 解析模型,再按 match() 顺序命中 handler。
执行 hooks 与 handlerbefore → handler → after,失败时再进入 onError

初始化

默认自动初始化

const base = new Base();

默认行为:

  • 默认数据库路径是 ./.base/visiblebase.sqlite
  • 默认 Runtime 表结构是 productsmodels
  • 首次启动时会补齐 VISIBLEBASE_ADMIN_SECRET_KEY
  • 首次启动时会补齐 VISIBLEBASE_TOKEN_SIGNING_KEY
  • 首次 serve()handleRequest()models()invoke() 时会自动完成初始化

显式指定数据库或 .env

const base = new Base({
  database_url: process.env.VISIBLEBASE_DATABASE_URL,
  env_file_path: ".env.local",
});

业务 schema 与 table()

const base = new Base({
  schema: {
    notes,
  },
});

await base.table("notes").insert({
  id: "note_1",
  title: "First note",
});

const notes = await base.table("notes").select();

注意几点:

  • new Base({ schema }) 用来注册你的业务表,例如任务、usage、订单或业务数据表
  • base.table(name) 支持 select()insert()update()delete()
  • 第一次 table() 操作会自动初始化 Base 默认表和业务表
  • 只有你要替换 Base Runtime 自己使用的 products/models 表对象时,才需要在第一次 init() 时传入 Runtime schema

读取模型与 service

base.models()

const models = await base.models();

这会返回 Base 当前公开暴露给 client 的模型目录。

base.services()

const services = base.services();

它返回当前进程里已经注册过的 service 名称,不会自动扫描数据库。

注册 service

内置快捷入口

VisibleBase 内置了这几个快捷入口:

  • base.text()
  • base.stream()
  • base.image()
  • base.video()
  • base.tts()
  • base.asr()

它们本质上都等价于 base.service(name)

类型上,内置快捷入口会约束 handler 返回值:

  • base.text() 返回 UIMessage
  • base.stream() 返回 Response
  • base.image() 返回 UIMessage
  • base.video() 返回 UIMessage

如果你需要完全自定义返回结构,使用 base.service(name) 注册自定义 service,再用 client.invoke<T>() 调用。

自定义 service

base.service("rewrite")
  .default({
    model: "gpt-5.4",
    tone: "formal",
  })
  .handle(async (ctx) => {
    return {
      ok: true,
      query: ctx.query,
      model: ctx.model.model_id,
    };
  });

default()

default() 定义的是 query fallback。

base.text().default({
  model: "gpt-5.4",
  temperature: 0.2,
});

也可以用函数,根据当前用户或产品动态补齐 query:

base.text().default((ctx) => {
  if (ctx.user.metadata?.plan === "pro") {
    return {
      model: "gpt-5.4",
      temperature: 0.4,
    };
  }
  return {
    model: "gpt-4.1-mini",
    temperature: 0.2,
  };
});

合并顺序是:

  1. 先读取 client 传入的 query
  2. 再把 default() 返回的对象当 fallback 合进去
  3. 最终从 ctx.query.model 解析模型

如果最终没有 ctx.query.model,Base 会直接返回 422

match()handle()

match()

match() 按上下文决定当前 service 该走哪种调用方式。

base.text()
  .default({ model: "gpt-5.4" })
  .match((ctx) => ctx.model.provider === "openai", async (ctx) => {
    return openai.responses.create({
      model: ctx.model.upstream_model,
      input: ctx.query.prompt,
    });
  })
  .match((ctx) => ctx.model.provider === "anthropic", async (ctx) => {
    return anthropic.messages.create({
      model: ctx.model.upstream_model,
      messages: [{ role: "user", content: String(ctx.query.prompt ?? "") }],
    });
  });

规则:

  • match() 按注册顺序执行
  • 命中第一条后停止继续匹配
  • 如果没有任何 handler 命中,Base 返回 422

handle()

handle() 是最简单的兜底写法,等价于“永远命中”的 match()

base.text()
  .default({ model: "gpt-5.4" })
  .handle(async (ctx) => {
    return {
      id: crypto.randomUUID(),
      role: "assistant",
      parts: [
        {
          type: "text",
          text: String(ctx.query.prompt ?? ""),
          state: "done",
        },
      ],
    };
  });

hooks

全局 hooks:base.all()

base.all().before(async (ctx) => {
  await quotaService.check({
    product_id: ctx.product.product_id,
    user_id: ctx.user.user_id,
    model: ctx.model.model_id,
  });
});

适合挂:

  • 限额
  • usage 记录
  • 统一日志
  • 风控检查

service 级 hooks

base.text()
  .before(async (ctx) => {
    ctx.meta.startedBy = "text-service";
    return ctx;
  })
  .after(async (ctx) => {
    return ctx;
  })
  .onError(async (ctx) => {
    console.error(ctx.error);
    return ctx;
  });

handler 错误如何返回

handler 抛错时,Base 会先执行 service 级和全局 onError hook,再把错误转成 HTTP 响应。默认返回 500;如果错误对象带 statusCode,则返回这个状态码。

import type { ErrorWithStatus } from "@visiblebase/base";

base.text().handle(async () => {
  const error = new Error("quota exceeded") as ErrorWithStatus;
  error.statusCode = 429;
  throw error;
});

onError 适合记录日志、清理资源和补充监控,不会吞掉原始错误。

暴露成 HTTP 服务

serve()

最省事的方式:

await base.serve({
  host: "127.0.0.1",
  port: 3001,
});

handleRequest()

如果你已经有标准 Fetch Request -> Response 入口,可以直接接:

const response = await base.handleRequest(request);

router()

如果你在用 Hono 风格挂载,可以用:

app.route("/viba", base.router());

router() 返回的是一个兼容 Hono 风格的 runtime router。挂到子路径后,它会把 /viba/v1/... 这类路径重写回内部的 /v1/...

这里也不需要手动 init()

  • router() 本身只是返回一个可挂载对象
  • 真正第一次请求进入这个 router 时,会通过内部的 handleRequest() 自动完成初始化

进程内调用:invoke()

如果你不想走 HTTP,也可以直接在进程内调用:

const result = await base.invoke("text", {
  model: "gpt-5.4",
  prompt: "你好",
  user: {
    user_id: "user_123",
    metadata: {},
  },
  product: {
    product_id: "prod_xxx",
    name: "Demo Product",
    status: "active",
    created_at: new Date().toISOString(),
    updated_at: new Date().toISOString(),
  },
});

这适合测试、脚本或需要直接在服务端复用 Base 能力的场景。

目录