Base
Base 的初始化、service 注册、query fallback、hooks 和 HTTP 接入方式。
Base 是 VisibleBase 的服务端运行时入口。
你用它来做 4 件事:
- 初始化默认数据库和 Runtime env
- 注册
text()、stream()等 service - 用
default()和match()描述调用逻辑 - 通过
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 的调用心智
user_token、读取 product_id,再确认当前 service。ctx.query先读 client query,再把 default() 返回的对象当作 fallback 合并进去。ctx.query.model 解析模型,再按 match() 顺序命中 handler。before → handler → after,失败时再进入 onError。初始化
默认自动初始化
const base = new Base();默认行为:
- 默认数据库路径是
./.base/visiblebase.sqlite - 默认 Runtime 表结构是
products、models - 首次启动时会补齐
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()返回UIMessagebase.stream()返回Responsebase.image()返回UIMessagebase.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,
};
});合并顺序是:
- 先读取 client 传入的 query
- 再把
default()返回的对象当 fallback 合进去 - 最终从
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 能力的场景。