关于 descope-byos-builder
descope-byos-builder 帮助基于 Descope 导出的 flow JSON 构建 React BYOS 界面,同时保留 Descope 的流程引擎。它指导 Agent 解析屏幕与子流程、生成界面并核验认证流程,重点说明常见错误及其排查方法。
主要功能
- 01解析导出的 flow JSON
- 02映射屏幕与交互
- 03生成 React BYOS 界面
- 04追踪子流程及屏幕冲突
- 05核验认证流程与常见故障
适用场景
- 01为 Descope flow 构建自定义界面
- 02流程变更后更新现有 BYOS
- 03排查认证状态或屏幕处理异常
- 04检查自定义界面与托管屏幕的一致性
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ descope/ skills --skill descope-byos-builder安装后在 Claude Code 中输入/descope-byos-builder确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
parse-flow.mjsreferences/byos-component-patterns.mdgotchas.md详细介绍
Descope BYOS 构建器
将 Descope 流程 JSON 导出文件转换为可运行的 React BYOS 界面,并调用 state.next(interactionId, form)。以下故障记录自真实 BYOS 会话——每次首次遇到都要花 15–60 分钟才能解决。
适用场景
- 在保留 Descope 流程引擎的同时,为 Descope 流程构建自定义 UI(不解析客户端 JWT,完整保留流程逻辑)
- Descope 控制台中的底层流程发生变化后,修改现有 BYOS 实现
- 调试“流程已完成但会话状态不对”/“按钮不起作用”/“会话未认证”/“通行密钥仪式中止”等问题
- 审查现有 BYOS 是否与托管屏幕保持一致
- 添加认证后的升级子流程(例如
add-passkeys),在logged-in后运行
**不要用于:**完全可以通过托管的 <Descope flowId=... /> 组件运行的流程(BYOS 是一种取舍——流程修改不会在不改代码的情况下自动生效)。
铁律
**BYOS 的每个组件都必须以导出的流程 JSON 为依据。**不要猜测交互 ID、输出键名或屏幕名称。故障目录中的每个问题,起因都是有人编造了一个看似合理的值。
所需输入(先询问用户)
此技能无法从 Descope 控制台获取流程——Agent 无法访问控制台。开始任何工作前,先请用户提供:
- 流程 JSON 文件——从 Descope 控制台导出主流程以及它调用的所有子流程(
LoadSubflow的arguments.flowId),包括认证后的升级子流程(例如add-passkeys)。可提供文件路径或粘贴 JSON。 - 项目 ID + 基础 URL(如果尚未配置)——
AuthProvider/ SDK 初始化所需。 - React 应用中的挂载点——应渲染 BYOS 入口的文件/组件。
- 现有 BYOS 代码(如需修改)——当前屏幕组件和屏幕映射的路径。
如果用户只提供了主流程 JSON,请先停止并索要子流程,再生成组件——缺少子流程 JSON 是导致 [byos] no handler for screen "..." 运行时错误的首要原因。
工作流
-
从用户处收集流程 JSON。 Agent 无法访问 Descope 控制台,用户必须导出并粘贴 JSON,或提供其路径。需要提供主流程以及它调用的每个子流程(
arguments.flowId中指定的LoadSubflow操作)。这包括在logged-in操作之后运行的登录后升权子流程(例如add-passkeys)——用户此时已经完成身份验证,很容易漏掉。如果用户只提供了主流程,请扫描其中的LoadSubflow操作,并在继续之前要求用户导出每个子流程。缺少子流程 JSON → 未发现相应界面 → 运行时出现[byos] no handler for screen …。 -
使用
parse-flow.mjs解析(位于此目录中)。运行node parse-flow.mjs <path/to/flow.json>,它会打印每个界面任务的screenName、allInputKeys、contextKeys、下一步规则(interactionId → taskId)、UI 节点摘要(输入框的name属性、按钮标签)以及子流程调用。 -
构建界面名称映射。 在所有流程中,每个唯一界面名称对应一个 React 组件。注意名称冲突——多个任务经常共用同一个名称(例如,"Welcome Screen" 同时用于邮箱输入和密码输入)。如果存在冲突,请编写一个路由器组件,根据
state.context.form.*启发式判断并分发(参见“注意事项”→“界面名称冲突”)。 -
编写每个组件。 每个 BYOS 界面都执行以下四项操作:
- 读取上下文以显示内容:
state.context.sentTo.maskedEmail、state.context.form.email等 - 将输入收集到
form中(通过setForm({ ...form, key: value })) - 点击按钮时触发
state.next(interactionId, payload) - Descope 报告错误时渲染
state.error?.text
- 读取上下文以显示内容:
-
接入流程。 Descope React SDK 通过
@descope/react-sdk提供<Descope flowId="..." onScreenUpdate={handler} />,作为 BYOS 入口点;onScreenUpdate接收(screenName, state, next),你可以据此分发到匹配的界面组件。一种常见做法是构建轻量级的FlowOrByos包装组件,接收byosScreens映射并在内部进行分发;但FlowOrByos不是 SDK 导出项,需要自行构建。在入口点挂载一个实例。在onSuccess时,清除所有身份验证状态缓存(参见“注意事项”→“会话粘滞”)。 -
端到端验证。 走完流程支持的每种用户操作路径。任何在控制台中出现
[byos] no handler for screen "..."的界面,都说明映射中缺少对应处理程序。
关键规则
注意: 以下若干规则(嵌套
<form>标签的行为、WebAuthn 仪式的归属、ctxKey预填、componentsConditions字段名、E.164 格式号码被静默拒绝)是根据真实 BYOS 会话的经验得出的,并未在 Descope 的官方文档中明确说明,但已在生产环境中验证,并与流程 JSON 结构核对过。
- 表单键与输入节点的
name属性匹配,而不是allInputKeys或inputsMetadata.key。Task 40 的 Set Password 输入项中,name="newPassword",但allInputKeys: ["newPassword_noPolicyOverrides"]。提交时使用{ newPassword: "..." }。 - 在
state.next载荷中只发送当前屏幕的输出。将累积的完整表单数据展开到子流程边界之外,可能会污染上下文并导致静默失败。 - 切勿嵌套
<form>标签。 Descope 的 Web 组件会将子元素包裹在<form>中。请使用<div>、onClick和onKeyDown={makeEnterHandler(submit)}。 - OAuth 按钮必须在载荷中设置
provider({ provider: 'google' }),不能只设置交互 ID。 - 电话号码必须采用 E.164 格式。 非 E.164 格式的号码会被 SMS 连接器静默拒绝——流程会完成,但不会创建会话。
- 在
onSuccess时使身份验证缓存失效。 会话升级(匿名 → 已验证)可能会复用同一个sub,因此以 userId 为键的缓存不会自动刷新。 - 从屏幕 JSON 中读取
componentsConditions,以复现显示/隐藏规则(例如,当unauthUser.verifiedPhone为 false 时,隐藏“使用代码登录”)。 - WebAuthn 流程:由 SDK 执行仪式。 当某个交互路由到 WebAuthn 操作任务(
webauthn-update-user-start/finish、Sign Up or In / Passkeys)时,只需调用state.next(interactionId)。切勿在 BYOS 中调用navigator.credentials.create/get。切勿导入@simplewebauthn/browser、startAuthentication、startRegistration或任何 WebAuthn 辅助库——Descope SDK 已经会执行仪式的两个阶段。SDK 会在onScreenUpdate时观察该操作并自行执行仪式;取消或错误会通过state.error.text呈现。 - 读取输入项的
ctxKey以进行预填。 当输入节点具有props.ctxKey="someKey"时,首次渲染时通过以上下文值为依赖项的useEffect,从state.context[someKey]初始化form[name](仅在本地字段为空时设置)。 - 复现
Device Not Supported分支。 WebAuthn 流程通常会根据deviceInfo.webAuthnSupport进行分支。不支持的分支是一个真实的屏幕任务——请为其提供 BYOS 组件,否则托管小部件会在推广过程中间渲染。
共享屏幕名称的启发式判断
当两个任务共用一个屏幕名称时,不能凭猜测来选择区分信号。必须追踪每条路径,并构建一张“表单字段累积”表。
流程(必须遵循——跳过会导致不易察觉的错误):
- 对于每个使用冲突屏幕名称的任务,沿着
next.rules反向追踪,直到父级流程的入口点。 - 对于每条路径,列出沿途的每个屏幕任务及其
allInputKeys——这些就是用户在该路径上填写的表单字段。 - 找出一个仅在一条路径上填充的表单字段。
- 使用
Boolean(state?.context?.form?.<that field>)作为启发式判断条件。
常见错误:选择了在两条路径上都会填充的字段,因为没有完整追踪两条路径。
以下示例来自一组特定的流程——你的任务 ID、屏幕名称和子流程名称会有所不同。这些示例展示的是方法;请始终在自己的流程上运行
parse-flow.mjs。示例:对于
sign-in-sms-otp和progressive-profile-sms共用的“Verify OTP”,form.phone看起来很合适,但这两个流程在到达 Verify OTP 之前,都会先通过 Phone-input 屏幕收集电话号码。应改用form.password:sign-in-sms-otp路径从父级流程的密码屏幕进入,该屏幕会写入form.password;而progressive-profile-sms路径是在 magic link 之后进入的,用户从未输入过密码。
参考启发式判断(示例来自本技能所依据的流程集,并非通用规则):
| 冲突 | 路径 A 在此屏幕前收集的字段 | 路径 B 在此屏幕前收集的字段 | 信号 |
|---|---|---|---|
| “Welcome Screen”(email-entry 与 password-entry) | — | email | state.context.form.email |
| “Verify OTP”(sign-in-sms-otp 与 progressive-profile-sms) | email、password、phone | email、phone | state.context.form.password(不要用 phone——两条路径都会收集该字段!) |
三路冲突示例(主流程和两个子流程变体中的 Magic Link Sent):
| 路径 | 此屏幕前收集的字段 | |
|---|---|---|
| 主流程 | email | 无密码 |
| 子流程 new-user | email、password、fullName | 有密码和 fullName |
| 子流程 existing-user | email、password | 有密码,没有 fullName |
判断条件:supportsChooseOther = !hasPassword || hasFullName。
在路由器组件顶部记录所选的启发式判断,并附上完整追踪过程。如果能在 Descope 控制台中重命名其中一个屏幕,请提出申请——唯一的名称永远胜过启发式判断。
**成功案例:**两个 User Information 任务(verify-email-magic-link 的 existing-user 与 new-user 分支)在控制台中被重命名为 User Information - Unverified - Email Only 和 User Information - Unverified - Email and Name。虽然可以使用启发式判断(检查是否存在 fullName 输出),但重命名更省事、更清晰,也能经受流程编辑的变更。默认优先考虑重命名。
常见问题参考
完整故障目录(问题 → 症状 → 修复方法):参见 references/gotchas.md。在将静默失败归咎于 BYOS 代码之前,先查阅该文件——大多数“无法运行”的症状都对应已知问题。
验证
对流程支持的每条用户旅程进行端到端测试:
- 流程 JSON 是否显示主流程从
task N开始,并以action=logged-in的task M结束? - 流程中的每个屏幕任务是否都有 BYOS 组件,或已明确决定“回退到托管流程”?
onSuccess是否触发,且应用是否到达预期的已认证状态(没有卡在匿名状态)?onError是否触发,并显示 Descope 错误消息(没有静默失败)?- 是否已端到端走完认证后提升子流程(passkey 提升等),包括
Device Not Supported分支? - WebAuthn 屏幕是否只调用
state.next(interaction)——BYOS 代码中没有调用navigator.credentials.*?
完整走完一条用户旅程期间,控制台应当没有 [byos] no handler for screen "..." 警告。
解析器
parse-flow.mjs(本目录)——Node 脚本。输入:一个或多个 flow JSON 路径。输出:每个 flow 一张摘要表,显示屏幕任务、输入、退出交互、UI 节点名称和子流程加载器。运行脚本,将输出粘贴到屏幕映射注释头部,即可获得 BYOS 组件所需所有常量的真实来源。
警示
如果你发现自己在:
- 硬编码一个未在 flow JSON 中看到的交互 ID——停下,先解析
- 在子流程边界处将
{ ...form }展开到state.next中——只发送输出 - 未检查每个 flow 的规则,就给每个“Verify OTP”变体添加相同按钮——根据上下文设置门控
- 在 BYOS 组件中编写
<form>标签——使用<div> - 在
onSuccess中调用navigate(),却未使身份验证缓存失效——先调用 invalidate - 在 BYOS 点击处理程序中调用
navigator.credentials.create/get——SDK 会处理;只需触发交互 - 导入
@simplewebauthn/browser或任何 WebAuthn 辅助工具——同一个陷阱,只是换成了第三方库;SDK 会处理 - 因为“用户到那时已经登录”而跳过子流程导出——登录后的升级子流程会渲染真实屏幕
这些情况都意味着:暂停,重新阅读 references/gotchas.md,并根据 flow JSON 进行核实。
参考资料
references/byos-component-patterns.md——正向代码模式:核心连接方式(onScreenUpdate→ByosState)、屏幕路由器、屏幕骨架,以及电子邮件、OTP、电话、OAuth、冲突路由器和 ctxKey 预填的完整示例。开始搭建时从这里入手。references/gotchas.md——故障目录:19 种真实的 BYOS 故障模式,每种都包含症状 → 根本原因 → 修复方法。还包含发版前检查清单。parse-flow.mjs——Node 解析器:node parse-flow.mjs <flow.json> [more.json ...]。输出屏幕任务、真实表单键的name属性、交互 ID、子流程加载器和冲突警告。编写任何组件前都要运行。