ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

CopilotKit 前端鉴权实战:基于 V2 运行时 `onRequest` 钩子的 Bearer Token 认证方案与 QA 验证清单

2026/9/13 18:46:59 拓冰建站 浏览量
CopilotKit 前端鉴权实战:基于 V2 运行时 `onRequest` 钩子的 Bearer Token 认证方案与 QA 验证清单 CopilotKit 前端鉴权实战基于 V2 运行时onRequest钩子的 Bearer Token 认证方案与 QA 验证清单【免费下载链接】CopilotKitThe Frontend Stack for Agents Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit导读本文以 CopilotKit 仓库中 CrewAI 集成示例showcase/integrations/crewai-crews的认证演示auth demo为蓝本系统讲解如何为 CopilotKit V2 运行时接入框架原生的请求鉴权通过createCopilotRuntimeHandler的onRequest钩子在路由处理前校验Authorization: Bearer token头配合前端headers注入与双层onError错误监听实现完整的登录、登出、401 拒绝与重新登录闭环。读完本文你将掌握前端鉴权状态机首屏登录卡 → 挂载聊天 → 登出后保持挂载以展示 401 → 重新登录清除错误的实现思路以及用 Playwright 与稳定data-testid契约搭建认证回归测试清单的完整方法。一、演示在讲什么运行时层鉴权而非 Agent 内部鉴权在 qa/auth.md 这份 QA 文档中验证对象是 CopilotKit 框架原生支持的请求级认证鉴权是运行时Runtime层的关注点与 Agent 内部业务逻辑完全解耦。也就是说CrewAI 智能体本身并不知道认证的存在认证在请求到达 Agent 之前就被运行时拦截完成。其运行位置在演示路由/demos/authNext.js App Router 下对应 src/app/demos/auth/page.tsx后端复用共享 CrewAI crew通过HttpAgent代理到AGENT_URL。认证门卫是运行时层的onRequest钩子属于纯框架能力。二、后端接线createCopilotRuntimeHandler与onRequest钩子2.1 为什么不用 V1 适配器演示路由 src/app/api/copilotkit-auth/[[...slug]]/route.ts 的注释明确说明了一个关键坑V1 的 Next.js 适配器copilotRuntimeNextJSAppRouterEndpoint不会把hooks选项透传给 V2 fetch handler。因此要使用onRequest钩子必须绕过 V1 适配器直接使用copilotkit/runtime/v2导出的框架无关 handlercreateCopilotRuntimeHandler——它返回一个纯(Request) PromiseResponse函数可直接塞进 Next.js App Router 的POST/GET导出中。2.2 完整路由实现import type { NextRequest } from next/server; import { CopilotRuntime, createCopilotRuntimeHandler, } from copilotkit/runtime/v2; import { HttpAgent } from ag-ui/client; import { DEMO_AUTH_HEADER } from /app/demos/auth/demo-token; const AGENT_URL process.env.AGENT_URL || http://localhost:8000; function createAgent() { return new HttpAgent({ url: ${AGENT_URL}/chat }); } const runtime new CopilotRuntime({ agents: { auth-demo: createAgent(), // Fallback: useAgent() 不带参数时解析为 default与同一 Agent 做别名 // 保证演示页面中的 hooks 能正确解析。 default: createAgent(), }, }); const BASE_PATH /api/copilotkit-auth; const handler createCopilotRuntimeHandler({ runtime, basePath: BASE_PATH, hooks: { onRequest: ({ request }) { const authHeader request.headers.get(authorization); if (authHeader ! DEMO_AUTH_HEADER) { // 抛出一个 Response 即可短路整个处理管线。 // 运行时会把抛出的 Response 原样映射为 HTTP 响应状态码 响应体。 throw new Response( JSON.stringify({ error: unauthorized, message: Missing or invalid Authorization header. Click Authenticate above to send messages., }), { status: 401, headers: { content-type: application/json }, }, ); } }, }, }); export const POST (req: NextRequest) handler(req); export const GET (req: NextRequest) handler(req);2.3onRequest钩子的语义与可用钩子全景onRequest是 CopilotKit V2 运行时提供的生命周期钩子之一。其类型定义与完整钩子集合位于 packages/runtime/src/v2/runtime/core/hooks.ts钩子按管线阶段划分为钩子执行阶段典型用途onRequest路由处理之前前置认证、注入 correlation ID、注入/改写请求头onBeforeHandler路由之后、处理函数之前路由级授权可按route.method、agentId精细化控制onResponse处理函数之后追加响应头、记录日志、设置 CookieonError出错时自定义错误响应体onRequest的HookContext携带request当前 Fetch Request可被前置钩子修改、path解析后的 URL pathname与runtime实例而onBeforeHandler额外拿到解析后的RouteInfo如agent/run、info、agent/suggest等 20 余种路由方法这是实现某些 Agent 或某些操作才需要授权的关键。onRequest做认证的原理它在路由处理前运行通过throw new Response(...)短路请求——匹配请求authorization头等于Bearer demo-token-123时放行不匹配时直接抛出一个401 Response请求永远到不了 Agent。演示使用HttpAgent把请求转发到AGENT_URL/chat默认http://localhost:8000可用环境变量AGENT_URL覆盖因此认证门卫与 Agent 内部行为完全无关任何后端 Agent 都能复用这套机制。2.4 共享的 Demo Token 常量令牌常量定义在 src/app/demos/auth/demo-token.ts客户端与服务端路由共同 import避免两处漂移export const DEMO_TOKEN demo-token-123; export const DEMO_AUTH_HEADER Bearer ${DEMO_TOKEN};注意代码注释中的反复强调这是 Demo 令牌。真实生产环境应通过身份提供商IdP为每个用户签发独立令牌绝不能硬编码共享密钥也不要把真实令牌写进前端localStorage。三、前端鉴权状态机首屏登录卡 登出后保持挂载3.1 为什么首屏默认未登录演示页面有一个精妙设计首次渲染默认未认证渲染登录卡SignInCard直到用户至少登录过一次才挂载CopilotKit。原因是 V2 运行时在挂载CopilotChat时会执行初始/info握手如果一上来就带着未认证状态挂载传输层 401 会直接让CopilotChat崩溃。先不挂载CopilotKit就绕开了这个初始握手崩溃问题。而用户登录过一次之后CopilotKit在登出 → 重新登录的整个周期内保持挂载——这正是演示的核心目的登出后聊天界面仍然可见只是变成琥珀色警告横幅从而能真实展示运行时对未认证请求的 401 拒绝而不是把用户弹回登录卡让拒绝永远不会发生。只有整页刷新才会重置回登录卡首屏状态。3.2 登录态管理 Hook状态管理由 src/app/demos/auth/use-demo-auth.ts 提供核心 APIisAuthenticated是否持有令牌token/authorizationHeader令牌字符串与完整的Bearer tokenhasEverSignedIn本页会话内是否登录过至少一次决定渲染登录卡还是聊天界面signIn(token)/signOut()登录 / 登出。实现要点localStorage 持久化令牌写入localStorage键copilotkit:auth-demo:token因此刷新页面不会把用户踢回登录卡但读取放在useEffect中延迟水合避免 SSR 下window未定义造成的水合警告首屏始终保持未认证。登出不清hasEverSignedInsignOut()只清空令牌故意保留hasEverSignedIn true让页面继续渲染聊天界面琥珀色横幅这正是展示 401 拒绝的前提。UI 只负责发送什么头校验是运行时的职责因此signIn虽允许任意字符串但运行时只认DEMO_TOKEN。3.3 把授权头注入请求页面用useMemo根据当前认证状态构建请求头并通过CopilotKit headers属性注入const headers useMemo( (): Recordstring, string authorizationHeader ? { Authorization: authorizationHeader } : {}, [authorizationHeader], ); CopilotKit runtimeUrl/api/copilotkit-auth agentauth-demo headers{headers} useSingleEndpoint{false} onError{handleProviderError} 其中useSingleEndpoint{false}明确选用 V2 多端点协议独立的/info、/agents/id/run等路由这也是演示运行时路由所对接的协议形态。登出后authorizationHeader变为nullheaders变为空对象后续请求不再携带Authorization头从而触发运行时 401。3.4 认证状态横幅 AuthBannersrc/app/demos/auth/auth-banner.tsx 是纯展示组件不持有任何状态通过data-testidauth-banner和data-authenticated属性对外暴露稳定的测试契约已认证绿色变体文案✓ Signed in as demo user显示Sign out按钮data-testidauth-sign-out-button未认证琥珀色变体文案⚠ Signed out — the agent will reject your messages until you sign in.显示Sign in按钮data-testidauth-authenticate-button。登出后不弹回登录卡而是保留横幅目的正如组件注释所说让演示名副其实——展示运行时拒绝未认证请求而不是把用户弹回门卫页让拒绝永不发生。3.5 登录卡 SignInCardsrc/app/demos/auth/sign-in-card.tsx 是未认证首屏落地卡data-testidauth-sign-in-card直接以明文展示 demo 令牌data-testidauth-demo-token方便访问者看到请求到底发送了什么头没有真实表单因为令牌值由运行时门卫固定。点击Sign in with demo tokendata-testidauth-sign-in-button即触发useDemoAuth().signIn(DEMO_TOKEN)从而让父组件挂载CopilotKit。四、错误展示的关键陷阱必须同时监听 Provider 级与 Chat 级onError演示页面代码头注释揭示了最容易踩的坑登出后的 401 拒绝只能通过 Agent 作用域chat-scoped的CopilotChat onError捕获而 Provider 级的CopilotKit onError不会为这类agent_run_failed错误触发。原因Agent 运行错误agent_run_failed可靠地投递到聊天作用域的订阅通道而如果演示只依赖CopilotKit onError拒绝横幅永远不会渲染。因此页面把同一个错误处理器同时注册到两个通道CopilotKit onError覆盖 Provider 级错误例如初始/info握手失败CopilotChat onError覆盖 Agent 运行拒绝登出路径产生的正是这种错误。错误消息在 page.tsx 中以琥珀色横幅渲染data-testidauth-demo-error展示错误 message 与错误码codedata-testidauth-demo-error-message。4.1 错误清空的状态竞态陷阱还有一个反直觉的细节错误横幅的渲染只以authError状态为准绝不叠加!isAuthenticated条件。页面用useEffect在isAuthenticated变回true时清空authErroruseEffect(() { if (isAuthenticated) setAuthError(null); }, [isAuthenticated]);如果采用显而易见但错误的守卫authError !isAuthenticated会产生登出后的竞态401 拒绝的onError触发并setAuthError但若该次提交落在一个认证状态尚未翻转为false的渲染帧中authError !isAuthenticated会求值为假横幅就永远不出现。以authError单独驱动渲染、并在重新认证时清空就移除了跨状态片的顺序依赖拒绝一定会渲染重新登录一定清空它。五、QA 检查清单逐条拆解附自动化验证5.1 原文档检查清单原 qa/auth.md 的 6 条 QA 检查项构成完整生命周期闭环导航到/demos/auth页面默认已认证挂载初始/info握手不崩溃路由注按 5.2 节源码当前实现首屏为未认证登录卡登录后才挂载聊天——文档描述的是挂载后的初始握手成功这一目标状态认证横幅data-testidauth-banner显示data-authenticatedtrueSign out 按钮可见发送消息Agent 正常回复无错误横幅点击 Sign out横幅翻转为data-authenticatedfalseSign in 按钮可见再发消息出现data-testidauth-demo-error错误横幅并带 401 信息ChatErrorBoundary保证页面其余部分正常渲染不白屏边界 fallbackdata-testidauth-demo-chat-boundary可能出现在聊天容器内点击 Sign in横幅翻回已认证错误横幅清除无需整页刷新聊天即恢复可用。5.2 用 Playwright 把清单固化为回归测试这套检查清单已完整落为 tests/e2e/auth.spec.ts 中的 6 个用例测试配置见 playwright.config.tstestDir: ./tests/e2e默认baseURL: http://localhost:3000本地通过pnpm dev起服务CI 中重试 2 次检查项对应测试关键断言首屏未认证page loads unauthenticated with SignInCard visibleauth-sign-in-card可见、auth-sign-in-button可用、auth-demo-token可见此时auth-banner与输入框计数为 0聊天面与横幅只在首次登录后渲染登录挂载聊天signing in mounts the chat surface with AuthBanner横幅data-authenticatedtrue、状态文案含 Signed in、auth-sign-in-card计数归 0已认证发送正常authenticated send produces an assistant response发送 Say hello in one short sentence 后 30s 内出现copilot-assistant-message登出翻琥珀、聊天保持挂载signing out flips the banner amber and keeps the chat surface mounted横幅data-authenticatedfalse、文案含 Signed out、auth-authenticate-button可见、auth-sign-in-card计数为 0、输入框仍可见未认证发送出现 401 不白屏unauthenticated send surfaces a 401 error without crashing the page发送后 15s 内auth-demo-error可见auth-banner仍可见页面不白屏copilot-assistant-message计数为 0重新登录清错误恢复聊天re-signing in from the amber banner clears the error and resumes chat5s 内横幅翻回data-authenticatedtrue、auth-demo-error计数为 0随后发送 Give me a fun fact 能收到助手回复其中页面不白屏的验证手法值得复用在 401 拒绝后断言auth-banner页面 chrome仍可见同时断言没有产生任何助手消息——既证明错误被正确表面化又证明ChatErrorBoundary兜住了渲染边界页面其余部分继续存活。5.3 测试选择器的契约化设计从页面、横幅到测试所有data-testid都是稳定契约auth-banner、auth-sign-in-card、auth-sign-in-button、auth-sign-out-button、auth-authenticate-button、auth-demo-token、auth-demo-error、auth-demo-error-message、copilot-assistant-message。QA 文档、实现组件与 Playwright 规格三方引用同一组 ID重构组件时若改动这些 ID测试会第一时间失败。六、关键结论与生产落地建议认证属于运行时层CopilotKit V2 的onRequest钩子在路由前校验请求头throw new Response(status: 401)即可短路管线与后端 AgentCrewAI、LangGraph 等完全解耦。实现参考 route.ts、钩子语义参考 hooks.ts。用createCopilotRuntimeHandler而非 V1 适配器V1 的copilotRuntimeNextJSAppRouterEndpoint不透传hooks选项V2 的框架无关 handler 可直接导出为 Next.js App Router 的POST/GET。首屏挂载策略未登录时不渲染CopilotKit避免初始/info握手 401 崩溃登录过至少一次后保持挂载让登出后的 401 在聊天界面内真实可见。双层onError缺一不可agent_run_failed只投递到CopilotChat onErrorProvider 级CopilotKit onError不会触发两个通道都要注册同一处理器。错误横幅只由authError驱动在重新认证时清空避免跨状态片竞态导致横幅永不显示。安全底线DEMO_TOKEN demo-token-123仅为演示生产环境必须由 IdP 签发每用户令牌令牌只应保存在服务端会话或安全的 HttpOnly Cookie 中绝不能硬编码共享密钥或写入localStorage。QA 自动化以稳定data-testid为契约用 Playwright 把认证生命周期固化为回归测试401 场景下同时断言错误横幅出现与页面 chrome 存活是验证拒绝而非崩溃的标准手法。【免费下载链接】CopilotKitThe Frontend Stack for Agents Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考