跳转到内容

输入关键词开始搜索文档。

路由

最后更新 查看 Markdown 源文件

Flue 永远不会自动挂载智能体。注册智能体('use agent' 指令)使其在你的应用程序内_可寻址_;通过 HTTP 提供它是你在应用程序路由图中做出的单独、显式的决定。本指南涵盖该路由图——app.ts——使用 createAgentRouter(...) 挂载智能体,每个对话获得的 URL 表面,以及如何保护它。

app.ts 是路由图

每个 Flue 应用程序都有一个 HTTP 入口点:src/app.ts。它的默认导出是服务器——你的应用程序提供的每个智能体、通道和自定义路由都在那里显式挂载。Flue 不会从文件名或目录约定生成路由:如果路由存在,app.ts 就把它放在那里。脚手架版本是一个完整的应用程序:

src/app.ts
import { createAgentRouter } from '@flue/runtime/routing';
import { Hono } from 'hono';
import { Hello } from './agents/hello.ts';

const app = new Hono();

app.route('/agents/hello', createAgentRouter(Hello));

export default app;

相同的 app.ts 在两个目标上都有效。在 Node.js 上,构建的服务器提供它导出的任何内容;在 Cloudflare 上,相同的导出成为 Worker 的 fetch 处理程序。请参阅部署。

Flue 使用 Hono 作为约定,但这里没有任何 Hono 特定的内容:默认导出只需要一个 fetch 兼容的形状,而 Flue 给你的路由器自己暴露 .fetch,因此它们可以挂载在任何基于 fetch 的框架中。当你需要为自定义应用程序入口输入类型时,Fetchable 接口是可用的:

import type { Fetchable } from '@flue/runtime/routing';

const app: Fetchable = {
  fetch(request, env, ctx) {
    return new Response('Not found', { status: 404 });
  },
};

export default app;

Hono 应用程序已经满足此接口。在 Cloudflare 上,env 包含绑定,ctx 是执行上下文。在 Node.js 上,env 包含 Hono Node 适配器绑定,ctx 是 undefined。

因为它是一个普通路由器,app.ts 也是你的应用程序其余 HTTP 存在的地方:健康检查、dispatch(...) 到智能体的 webhook 接收器、聊天 UI 的静态资源,以及通道挂载都与你的智能体路由并列组合。

挂载智能体

来自 @flue/runtime/routing 的 createAgentRouter(agent) 为一个智能体构建 HTTP 表面:一个小型子路由器,你可以挂载在你希望它存在的任何地方。

src/app.ts
import { createAgentRouter } from '@flue/runtime/routing';
import { Hono } from 'hono';
import { Support } from './agents/support.ts';
import { Triage } from './agents/triage.ts';

const app = new Hono();

app.route('/agents/support', createAgentRouter(Support));
app.route('/api/assistants/triage', createAgentRouter(Triage));

export default app;

一些值得了解的属性:

  • URL 是你的。 /agents/<name> 是约定,不是要求——可以挂载在 /api 下,带版本前缀,任何地方。挂载路径是纯路由,客户端寻址你选择的任何 URL。
  • 挂载路径不是智能体的身份。 对话由智能体的持久化身份键控——其函数名称,或 agentName 静态属性覆盖——绝不通过 URL。你可以在不进行数据迁移的情况下移动挂载,并且将同一个智能体挂载在两个路径上从两个路径提供相同的对话。
  • 它是纯工厂。 createAgentRouter(...) 没有副作用也没有选项;调用任意次数,或完全不调用。关于智能体的其他所有内容——模型、持久性、初始数据 schema——都在智能体模块本身上声明,而不是在挂载处。
  • 挂载是暴露决策,不是注册。 'use agent' 扫描是使智能体存在的原因;路由器只是在已注册的智能体上构建 HTTP 表面。已注册但从未挂载的智能体只是通过 HTTP 无法访问——dispatch(...) 和调度仍然可以驱动它(参见下面的仅分派智能体)。

对话 URL

每个对话存在于挂载路径加上你选择的对话 ID:/agents/support/ticket-8472。ID 与智能体指南中描述的调用者选择的标识符相同——用户 ID、工单号、任何字符串——对话在接收到第一条消息时创建。相对于挂载,路由器提供:

路由用途
POST /:id传递一条消息(202 接收)。
GET /:id读取对话(快照、更新或实时流)。
HEAD /:id读取对话流元数据。
POST /:id/abort中止正在进行和排队的工作。
GET /:id/attachments/:attachmentId下载一个附件的字节。

发送消息

POST 消息到对话 URL。正文与服务器端 dispatch(...) 接收的 DeliveredMessage 形状相同——user 聊天回合或结构化的 signal——可选地伴随用于实例创建的 initialData:

提示支持对话
POST /agents/support/ticket-8472 HTTP/1.1
Content-Type: application/json

{
  "kind": "user",
  "body": "你能总结一下我案例中的开放问题吗?"
}

发送是即发即忘的:服务器在消息持久化接收后立即响应 202,在智能体运行之前。响应携带跟踪结果的坐标——对话的流 URL、不透明的恢复偏移量和提交 ID:

{
  "streamUrl": "https://example.com/agents/support/ticket-8472",
  "offset": "-1",
  "submissionId": "sub_01HZX..."
}

此路由上没有”等待回复”模式。智能体的回复落在对话中,你从那里读取它。

读取对话

GET 同一个 URL 来读取对话。普通的 GET 返回一个物化的快照——每条消息都简化为完整的、可渲染的部分。查询参数选择实时模式:?view=updates&offset=... 读取偏移量之后的更改,带有长轮询或服务器发送事件以进行持续流式传输。线路协议在流协议参考中有文档记录,但你很少手动使用它,因为 Flue 智能体 SDK 封装了它。

SDK 封装此表面

createFlueClient(...) 客户端正好寻址一个对话 URL,并将整个表面——send()、wait()、observe()、history()、abort() 和 attachmentUrl()——封装在上述路由上:

import { createFlueClient } from '@flue/sdk';

const conversation = createFlueClient({
  url: 'https://example.com/agents/support/ticket-8472',
  token: userToken,
});

const admission = await conversation.send({
  message: { kind: 'user', body: '你能总结一下我案例中的开放问题吗?' },
});
await conversation.wait(admission);
const { messages } = await conversation.history();

客户端不需要智能体名称或部署地址——URL 就是整个契约,因此挂载布局永远不会泄露到客户端配置中。对于聊天 UI,@flue/react 中的 useFlueAgent({ url }) 用维护的对话状态封装相同的客户端。每个路由上的错误共享一个机器可读的信封;请参阅错误参考。

保护你的智能体

挂载的智能体没有内置的身份验证:任何能够访问对话 URL 的人都可以与该对话交谈——向它发送消息、读取其完整历史、中止其工作。也没有每个智能体的中间件导出。将挂载视为任何其他敏感端点,并用你应用程序的正常中间件在 app.ts 中在挂载之前分层保护它。

有两个检查,生产应用程序需要两者:

  1. 身份验证 — 调用者是谁?
  2. 授权 — 此调用者是否被允许访问_此对话 ID_?

对话 ID 是调用者选择的路径段:没有所有权检查,任何经过身份验证的用户都可以通过猜测其 ID 来读取另一个用户的对话。

src/app.ts
import { createAgentRouter } from '@flue/runtime/routing';
import { Hono } from 'hono';
import { Support } from './agents/support.ts';
import { canAccessTicket, verifySession } from './shared/auth.ts';

const app = new Hono();

app.use('/agents/support/*', async (c, next) => {
  const user = await verifySession(c.req.raw); // 你的应用程序的认证
  if (!user) return c.json({ error: 'unauthorized' }, 401);

  // 对话 ID 是挂载之后的第一个路径段。
  const [conversationId] = c.req.path.slice('/agents/support/'.length).split('/');
  if (!(await canAccessTicket(user, conversationId))) {
    return c.json({ error: 'forbidden' }, 403);
  }
  return next();
});
app.route('/agents/support', createAgentRouter(Support));

export default app;

因为中间件模式以 /* 结尾,它覆盖了智能体路由器提供的每个路由——提示、读取、中止和附件下载。这是普通的 Hono 组合,因此你的框架支持的任何东西都可以在这里工作:更广泛前缀上的共享中间件(app.use('/agents/*', requireUser))、不记名令牌、会话 cookie、签名验证、每个路由的速率限制。

两种相关模式:

  • 服务器颁发的 ID。 不是信任调用者选择的 ID,而是从经过身份验证的主体派生它们(user-${user.id})或从你自己的数据库颁发它们。然后所有权检查变成简单的相等性测试。
  • 私有智能体。 只有你自己的后端应该访问的智能体根本不需要公共挂载——保持它仅分派(如下),或者在 Cloudflare 上,通过服务绑定访问私有 Worker。

CORS

跨源访问是应用程序关注的问题:智能体路由器自己不设置 Access-Control-* 头。在本地开发中,vite dev 和 vite preview 应用宽松的 localhost CORS 默认值,以便单独提供的 SPA 开箱即用——这意味着在本地工作的跨源设置在部署后可能会失败,除非你自己配置 CORS。

如果另一个源上的浏览器在生产中调用你的智能体,请在 app.ts 中添加你的框架的 CORS 中间件。暴露流协调头,以便 SDK 可以在重新连接时恢复对话流:

src/app.ts
import { cors } from 'hono/cors';

app.use(
  '/agents/*',
  cors({
    origin: 'https://app.example.com',
    credentials: true,
    exposeHeaders: ['Stream-Next-Offset', 'Stream-Up-To-Date', 'Location'],
  }),
);

同源部署——常见的设置,其中 app.ts 同时提供 UI 和智能体挂载——根本不需要 CORS 配置。

挂载通道

通道对象暴露自己的 .route() 工厂——与智能体路由器不同的 API,但相同类型的纯、可挂载子路由器。它相对于挂载点提供提供商声明的路由:

src/app.ts
import { channel as slack } from './channels/slack.ts';

app.route('/channels/slack', slack.route());
// Slack 的 Events API 端点现在是 POST /channels/slack/events

通道包声明其路由后缀(/events、/webhook、/interactions 等);挂载点是你的。请参阅通道。

挂载智能体目录

按路由挂载保持路由图显式,但没有什么能阻止你生成它。Vite 的 import.meta.glob 在用户空间中恢复目录式挂载——枚举智能体模块并挂载每个导出的智能体:

src/app.ts
import type { Agent } from '@flue/runtime';
import { createAgentRouter } from '@flue/runtime/routing';
import { Hono } from 'hono';

const app = new Hono();

const modules = import.meta.glob<Record<string, Agent>>('./agents/*.ts', { eager: true });
for (const mod of Object.values(modules)) {
  for (const [exportName, agent] of Object.entries(mod)) {
    if (typeof agent !== 'function' || !/^[A-Z]/.test(exportName)) continue;
    app.route(`/agents/${agent.agentName ?? exportName}`, createAgentRouter(agent));
  }
}

export default app;

glob 只枚举模块——每个挂载都像手写的一样显式。跳过或过滤 glob 以获取应保持仅分派的智能体。

仅分派智能体

注册来自 'use agent' 扫描,因此任何注册的智能体都可以通过服务器端 dispatch(...) 接收消息——从 app.ts 中的 webhook 路由、通道或调度——而无需被挂载。只对应用程序事件做出反应的内部智能体没有理由从外部可达:

src/app.ts
import { dispatch } from '@flue/runtime';
import { Hono } from 'hono';
import { InvoiceAuditor } from './agents/invoice-auditor.ts';
import { verifyBillingWebhook } from './shared/billing.ts';

const app = new Hono();

// 没有 createAgentRouter(InvoiceAuditor) 挂载——智能体是
// 注册的,但只有这个经过验证的 webhook 可以访问它。
app.post('/webhooks/billing', async (c) => {
  const event = await verifyBillingWebhook(c.req.raw);
  const receipt = await dispatch(InvoiceAuditor, {
    id: event.invoiceId,
    message: {
      kind: 'signal',
      type: 'billing.invoice.flagged',
      body: event.summary,
    },
  });
  return c.json(receipt, 202);
});

export default app;

webhook 路由属于你的应用程序:它决定哪些请求有效,哪个智能体对话接收接受的消息。挂载和分派也可以组合:支持智能体可能为其聊天 UI 挂载_并且_通过 dispatch(...) 接收 webhook 信号——两者都馈送相同的每对话队列。要通过 HTTP 观察仅分派智能体的对话(例如,内部仪表板),在仅管理员中间件后面添加挂载。

后续步骤

  • 智能体 — 与智能体交互的每种方式:CLI、HTTP、dispatch() 和独立脚本。
  • 智能体 SDK — 封装对话 URL 的客户端。
  • React — 在挂载的智能体上构建聊天 UI。
  • 通道 — 经过验证的提供商 webhook,将事件传递到智能体中。
  • 流协议 — 对话读取背后的线路协议。
  • 部署 — 将 app.ts 和你的智能体构建为可部署的服务器。