@neoaira/scoutaro-sdk
v0.1.3
Published
Scoutaro browser API client and embedded registration widget
Readme
Scoutaro 浏览器 SDK 接入手册
Scoutaro SDK 为落地页提供完整的 Google 登录与内测报名区域:加载登录按钮、按需收集建议和隐私确认、按服务端配置执行 Turnstile、提交报名,以及恢复已预约状态。纯 TypeScript / 原生 DOM 实现,不依赖 React 或 Vue;可用于静态 HTML、React、Vue、Astro 等浏览器页面。
这份文档是 SDK 对外接入的独立维护入口。npm 包名为 @neoaira/scoutaro-sdk,采用 MIT 许可,提供 npm 安装、本地包安装和自托管 ES Module 接入方式。没有官方公共 CDN;不要把示例 API 地址当成公共托管服务。MIT 许可仅覆盖 SDK 及其发布产物,不授予本仓库 API 和管理后台的使用许可。
目录
- 1. 快速接入
- 2. 接入前准备
- 3. 初始化参数与完整流程
- 4. 可运行的 HTML 示例
- 5. React 与 Astro 示例
- 6. Vue 3 示例
- 7. 生命周期与回调
- 8. 配色与页面布局
- 9. 多语言与条款
- 10. 纯 API 客户端
- 11. 会话、错误与排查
- 12. 打包、升级与验证
1. 快速接入
准备一个用于容纳 SDK 的 DIV。将 SDK 的 dist/scoutaro.js 放到网站的 /assets/scoutaro.js 后,在页面添加:
<div id="registration"></div>
<script type="module">
import { mountRegistration } from '/assets/scoutaro.js';
const widget = mountRegistration({
container: document.querySelector('#registration'),
apiBase: 'https://your-public-api.example.com',
slug: 'your-product',
});
widget.ready.catch((error) => {
// 可用于接入诊断。保留组件,SDK 的入口支持用户点击重试。
console.warn('Scoutaro initialization:', error.code);
});
</script>页面初始化只显示一个“使用 Google 预约内测”按钮,配置加载完成后也不会自动展开面板。用户点击入口后,SDK 展示 Google 官方登录按钮,用户点击该按钮完成登录。此流程不通过模拟点击或隐藏覆盖层触发 Google 登录。
默认不显示隐私确认和功能建议。用户完成登录后,SDK 按需执行 Turnstile,然后直接报名。成功后显示白底细边框的“已预约”状态按钮和已验证邮箱;按钮不可重复提交,不再展示资格卡片。初始化不会自动登录或报名。
不需要接入方自己加载 Google / Turnstile 脚本、获取第三方 token 或编写报名请求。公开 Key 由 Scoutaro 的 config 接口提供,Secret Key 不进入页面。
2. 接入前准备
| 配置 | 由谁设置 | 用途 |
| ----------------------- | ----------------------- | ------------------------------------------------------------ |
| 公共 API 地址 apiBase | Scoutaro 服务维护者 | 提供 config、stats、applications、application 四个接口 |
| 产品标识 slug | Scoutaro 后台创建产品 | 唯一标识,如 replybridge,不是产品 UUID |
| 产品允许来源 | Scoutaro 后台产品编辑页 | 登记落地页准确的协议、主机和端口 |
| Google Client ID | Scoutaro 后台产品编辑页 | 当前产品 Client ID;SDK 自动读取公开值 |
| Google JavaScript 来源 | Google OAuth Web 客户端 | 必须与实际打开落地页的 Origin 对应 |
| Turnstile | Scoutaro 后台产品编辑页 | 逐产品控制启用和配对 Key,组件登记该产品 hostname |
| 隐私条款 | Scoutaro 后台产品条款页 | 后台开启“要求隐私确认”时检查条款;未自定义的语言使用系统默认 |
本地示例:API 为 http://127.0.0.1:8787,SDK 示例页面为 http://127.0.0.1:8792,产品标识为 scoutaro-local-test。在产品允许来源和 Google 控制台登记的是 8792 页面来源,不是 8787 API 来源。localhost 与 127.0.0.1 是不同来源;端口也必须匹配。
生产页面通过 HTTPS 提供 SDK 文件并连接 HTTPS API。浏览器自动附带 Origin;不要在页面代码中伪造该请求头。API 只接受产品已经登记的来源。
服务维护者可参考仓库的运维说明配置 Google、Turnstile、JWT 和后台身份服务;SDK 使用者只需取得 API 地址、产品标识以及页面来源授权。
3. 初始化参数与完整流程
mountRegistration({
container, // 必填:已挂载的 HTMLElement
apiBase: 'https://api.example.com',
slug: 'my-product',
featureRequest: false, // 可选:默认 false
locale: 'zh-CN', // 可选:省略时优先页面语言,再浏览器语言
storage: null, // 可选:null 表示仅内存;省略由 SDK 自动持久化并跨标签页恢复
nonce: 'server-generated-nonce', // 可选:有 CSP nonce 要求时传入
onSuccess({ google_email }) {
/* 服务端确认报名后调用 */
},
});| 参数 | 类型 | 默认值/说明 |
| --------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------ |
| container | HTMLElement | 必填。SDK 仅管理自己追加的区域 |
| apiBase | string | 必填。公共 API 的基础地址,不带 /api/products/... 路径 |
| slug | string | 必填。后台创建的产品唯一标识 |
| privacyConfirmation | boolean | 已弃用,传入值不生效;由后台产品的 privacy_enabled 控制 |
| featureRequest | boolean | false。是否展示选填建议,最长 2000 字符 |
| locale | string | 默认先读页面语言,再读浏览器首选语言;显式传入不支持的语言会报错 |
| storage | Storage 子集或 null | 默认 localStorage,受限时降级;自定义存储须提供同步的 getItem/setItem/removeItem |
| nonce | string | 无。用于 SDK 样式和动态加载的第三方脚本 |
| onSuccess | (result: { google_email: string }) => void | 无。报名 POST 成功或恢复已有报名的 POST 成功后调用 |
featureRequest 必须使用布尔值 true / false,默认关闭。隐私确认请在后台“产品管理 → 创建产品/编辑产品 → 隐私确认 → 要求隐私确认”设置,默认开启,保存后刷新落地页生效。初始化参数不可原地修改;改变建议选项时销毁并重新挂载,语言切换使用 setLocale()。
| 隐私确认 | 功能建议 | 登录前展示与操作 | | -------- | -------- | ---------------------------------------- | | 关闭 | 关闭 | 只展示 Google 登录按钮 | | 开启 | 关闭 | 按钮下展示条款,勾选同意后才可登录 | | 关闭 | 开启 | 按钮下展示选填建议,可留空后登录 | | 开启 | 开启 | 按钮下展示建议和条款,勾选同意后才可登录 |
所有组合均在 Google 登录成功后,按需执行 Turnstile 并直接提交,不再要求点击“确认预约”。未勾选时 Google 授权被阻止;点击登录区域或用键盘激活会显示当前语言的同意提示,并聚焦勾选框。勾选后提示消失,才能登录;回调也会再次检查同意状态。提交失败才显示“重试”;条款变化会重新加载、清空勾选并要求再次登录。
**隐私开关的实际含义:**关闭时不展示条款、不要求确认;即使后台保存了条款,也不会阻止该次报名或保存条款快照。开启时,SDK 从后台获取条款并要求明确勾选,条款缺失则不开放报名;服务端核对摘要并保存首次快照。
隐私确认由后台产品的 privacy_enabled 唯一控制。SDK 从 config 读取开关,API 也读取数据库并强制校验。旧 privacyConfirmation 参数和请求中的 privacy_enabled 字段仅保留兼容,不能覆盖后台配置。管理员在提交过程中切换开关时,SDK 会重新加载配置,不自动再次提交;开启后必须重新勾选并登录。关闭不删除条款自定义内容或历史报名快照。Google 身份、来源权限和 Turnstile 校验仍执行。
**功能建议的实际含义:**关闭时 SDK 不发送 feature_request,新记录保存为 null;开启后仍然选填。重复报名不会覆盖第一次提交的建议。
4. 可运行的 HTML 示例
示例文件使用 ../dist/scoutaro.js,可连同 dist/ 一起通过 HTTP 静态服务器提供。不要直接双击 HTML 使用 file:// 打开。
从 Scoutaro 仓库根目录执行:
pnpm dev该命令同时启动 SDK 调试页面(8792)、API(8787)和后台(8788),并显示地址与 API 状态;首次运行先按根目录 README 迁移本地数据库。如果示例服务已经运行,新增示例路由后需要重启一次。仅开发 SDK 或运行模拟回归时,也可以单独执行 pnpm --filter @neoaira/scoutaro-sdk dev,但不要与根目录 pnpm dev 重复启动。示例是真实服务接入示例,点击登录和提交会在所选产品下产生报名;与 /__tests 的模拟回归页面不同。
示例 A:最简预约
地址:http://127.0.0.1:8792/examples/basic.html
const widget = mountRegistration({
container: document.querySelector('#registration'),
apiBase: 'http://127.0.0.1:8787',
slug: 'scoutaro-local-test',
});
widget.ready.catch((error) => console.warn(error.code));默认优先页面 html.lang,再使用浏览器语言。隐私确认遵循后台开关;开启时先同意条款,Google 登录后直接报名,成功按钮包含邮箱。没有自定义条款时使用系统默认正文。
示例 B:隐私确认+功能建议
地址:http://127.0.0.1:8792/examples/form.html
const widget = mountRegistration({
container: document.querySelector('#registration'),
apiBase: 'http://127.0.0.1:8787',
slug: 'scoutaro-local-test',
featureRequest: true,
});
widget.ready.catch((error) => console.warn(error.code));先在后台开启产品的“要求隐私确认”,检查默认或自定义条款。Google 按钮下方展示建议和条款:填写建议或留空,阅读条款并勾选,再点击 Google 登录。登录成功后直接报名。条款在填写期间变化时,SDK 重新加载条款并清空勾选,用户重新确认并登录后才能提交。
示例 C:只确认隐私
地址:http://127.0.0.1:8792/examples/privacy.html
后台开启“要求隐私确认”,SDK 不启用功能建议。用户阅读条款并勾选同意后,通过 Google 登录自动报名。
示例 D:品牌主题+建议+统计
地址:http://127.0.0.1:8792/examples/themed.html
此示例使用深绿色背景和独立配色变量,启用建议;隐私确认仍遵循后台开关。页面首次进入与报名成功后主动调用 getStats() 更新人数。统计失败显示暂不可用,不把失败误报为零人,也不会改变已经成功的报名状态。
四个示例默认连接同一个测试产品,因而会共享该产品的已预约会话,包括同一浏览器、同一来源的新标签页。要比较未报名流程,可清除该产品对应的本地会话(localStorage 与旧 sessionStorage)后刷新。清除会话不会删除服务端报名;同一 Google 账号再次报名仍会去重。
5. React 与 Astro 示例
先按打包说明安装本地 SDK 包。React 组件使用客户端 effect 挂载,卸载时释放:
import { useEffect, useRef } from 'react';
import { mountRegistration } from '@neoaira/scoutaro-sdk';
export function Registration({ apiBase, slug }: { apiBase: string; slug: string }) {
const container = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!container.current) return;
const widget = mountRegistration({
container: container.current,
apiBase,
slug,
featureRequest: true,
});
void widget.ready.catch((error) => console.warn('Scoutaro:', error.code));
return () => widget.destroy();
}, [apiBase, slug]);
return <div ref={container} />;
}React Strict Mode 在开发时会执行 effect 的清理与重建;必须保留 destroy()。不要把组件实例保存到模块级变量,也不要在 render 或服务端渲染阶段调用 mountRegistration()。
Astro 页面可以复用上面的 React 组件:
---
import { Registration } from '../components/Registration';
---
<Registration
client:load
apiBase="http://127.0.0.1:8787"
slug="scoutaro-local-test"
/>Astro 页面通常使用不同端口,例如 4321。应在 Scoutaro 产品与 Google 控制台登记实际 Astro 页面来源,而不是照抄 SDK 示例的 8792。
6. Vue 3 示例
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue';
import { mountRegistration, type RegistrationWidget } from '@neoaira/scoutaro-sdk';
const container = ref<HTMLElement | null>(null);
let widget: RegistrationWidget | undefined;
onMounted(() => {
if (!container.value) return;
widget = mountRegistration({
container: container.value,
apiBase: 'http://127.0.0.1:8787',
slug: 'scoutaro-local-test',
featureRequest: true,
});
void widget.ready.catch((error) => console.warn('Scoutaro:', error.code));
});
onBeforeUnmount(() => widget?.destroy());
</script>
<template><div ref="container" /></template>该示例的产品与功能开关固定。如果把它们改为可变 props,应在变化时先 destroy(),再重新挂载;不要在同一个容器里叠加多个实例。
7. 生命周期与回调
mountRegistration() 返回:
| 成员 | 返回值 | 行为 |
| ------------------- | --------------- | --------------------------------------------------------------------- |
| ready | Promise<void> | 首次配置加载、已有会话检查及登录入口准备完成;初始化失败会 reject |
| setLocale(locale) | Promise<void> | 切换语言并重新读取配置;保留尚未提交的建议,清除隐私勾选,不自动提交 |
| destroy() | void | 取消请求与挑战、释放 Google 回调归属、移除 SDK 添加的 DOM;可重复调用 |
ready 不是“报名已完成”的通知,也不跟踪用户之后的所有重试。初始化失败时入口仍保留,接入页面不要在 catch 中移除或隐藏整个组件。
const widget = mountRegistration({
container: document.querySelector('#registration'),
apiBase,
slug,
onSuccess({ google_email }) {
// 这里只表示服务端已经确认报名,可触发页面自己的反馈。
document.querySelector('#feedback').textContent = '报名已确认';
void refreshStats().catch(() => {
// 统计故障不撤销报名,也不重新提交。
});
},
});
void widget.ready.catch((error) => console.warn(error.code));
// 页面语言控件触发:
// await widget.setLocale('ja');
// 路由离开时:
// widget.destroy();onSuccess 不包含 Google ID Token。刷新页面后通过查询 JWT 只读恢复已预约状态时,不触发 onSuccess。回调中如有异步工作,应自行捕获 Promise 错误。销毁无法撤销服务端已经保存的报名。
成功区域只显示“已预约”和邮箱,不显示内测资格,也不提供“切换账号”按钮。需要获取资格时可独立使用 getApplication()。Google 登录时由用户选择账号。
8. 配色与页面布局
SDK 外层透明,无卡片背景、边框、阴影和内边距。继承容器的字体家族与文字颜色;SDK 保持表单字号层级。区域最大宽度为 460px,Google 按钮及成功状态按钮最大宽度为 400px。外围背景、位置、居中方式和留白由页面控制。
#registration {
max-width: 460px;
margin-inline: auto;
color: #e2e8f0;
font-family: inherit;
--scoutaro-control-bg: #1e293b;
--scoutaro-control-text: #e2e8f0;
--scoutaro-border: #64748b;
--scoutaro-primary: #a7f3d0;
--scoutaro-on-primary: #064e3b;
--scoutaro-focus: #67e8f9;
--scoutaro-error: #fca5a5;
}| CSS 变量 | 作用 |
| ------------------------- | ------------------------------------ |
| --scoutaro-control-bg | 输入框、普通操作按钮和已预约按钮背景 |
| --scoutaro-control-text | 上述控件的文字颜色 |
| --scoutaro-border | 控件边框和表单分隔线 |
| --scoutaro-primary | 确认按钮背景及勾选框强调色 |
| --scoutaro-on-primary | 确认按钮文字颜色 |
| --scoutaro-focus | 键盘焦点轮廓 |
| --scoutaro-error | 提交错误文案颜色 |
不传变量时使用默认浅色控件。变量不修改 Google 官方 iframe 或 Turnstile 内部界面。成功状态使用包内 Google 图标,按“已预约:邮箱”同行展示,长邮箱自动换行;邮箱来自服务端验证结果。此状态不是可点击的 Google 登录按钮。
SDK 不使用 Shadow DOM,页面过强的全局选择器仍可能覆盖内部样式。建议把宿主的 button、form、summary 等规则限制在自身组件范围内;无需复制或覆盖 SDK 整份 CSS。
9. 多语言与条款
SDK 和公共接口支持以下 20 个语言选项。选择口径为全球网站用户覆盖,不代表严格的语言人口排名。
| 代码 | 语言 | 代码 | 语言 | | ----- | -------- | ----- | ------------ | | zh-CN | 简体中文 | zh-TW | 繁体中文 | | en | 英语 | es | 西班牙语 | | ja | 日语 | de | 德语 | | fr | 法语 | pt | 葡萄牙语 | | ru | 俄语 | it | 意大利语 | | ko | 韩语 | ar | 阿拉伯语 | | hi | 印地语 | id | 印度尼西亚语 | | vi | 越南语 | tr | 土耳其语 | | th | 泰语 | nl | 荷兰语 | | pl | 波兰语 | uk | 乌克兰语 |
优先级:显式 locale → 页面 html.lang → navigator.languages 中第一个支持的语言 → 简体中文。兼容 navigator.language。对应语言未自定义时由 API 返回该语言的系统默认条款,不要求接入方手动复制模板。阿拉伯语使用 RTL。
SDK 文案、成功状态和错误文案内置翻译;通过 Accept-Language 与服务端协商错误及条款语言。Google 按钮传入官方 locale;SDK 自行加载的 Google 脚本会随语言切换更新 hl,切换期间保留加载入口。若页面自行预加载 GIS,脚本语言由页面负责,应避免重复加载;账号选择器自身语言仍由 Google / 浏览器控制。
隐私条款由管理员逐语言填写纯文本,保存为 JSON。SDK 不翻译条款正文或用户建议。缺少当前页面语言时返回 null,开启隐私确认时阻止首次报名;正文只按文本渲染。
启用确认时,SDK 提交实际条款的 locale 和 digest,服务端在首次报名中保存正文、语言和摘要。摘要用于防止阅读后内容变化,不是后台版本管理编号。关闭确认时不展示、不提交这些信息,快照保持空。重复报名不会补写或改写第一次快照。
10. 纯 API 客户端
需要完全自定义 UI 时使用 ScoutaroClient。它只封装 API、语言和查询 JWT,不会渲染 Google / Turnstile 或自动收集第三方 token。希望 SDK 全部处理时使用 mountRegistration()。
import { ScoutaroClient } from '@neoaira/scoutaro-sdk';
const client = new ScoutaroClient({ apiBase, slug, locale: 'zh-CN' });
const stats = await client.getStats();
console.log(stats.reservation_count);| 方法 | HTTP 接口 | 返回值 |
| ----------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------- |
| getConfig(signal?) | GET /api/products/{slug}/config | ProductConfig:公开 Key、产品人机验证开关、语言与条款 |
| getStats(signal?) | GET /api/products/{slug}/stats | Statistics:报名总数、内测人数、配额、候补及剩余名额 |
| submitApplication(input, signal?) | POST /api/products/{slug}/applications | ApplicationResult:token、expires_in、google_email;自动保存查询 JWT |
| getApplication(signal?) | GET /api/products/{slug}/application | ApplicationStatus:registered、eligible、google_email |
| setLocale(locale) | 无请求 | 修改后续请求使用的语言 |
| clearSession() | 无请求 | 清除本地查询 JWT;不退出 Google、不删除报名 |
四个业务方法均返回 Promise;可选 signal 为 AbortSignal。setLocale() 和 clearSession() 是同步生命周期操作。
config 与条款类型
interface PrivacyPolicy {
locale: Locale; // 实际返回的条款语言
content: string; // 纯文本正文
digest: string; // locale + 正文的服务端摘要,提交时原样带回
}
interface ProductConfig {
product_id: string;
google_client_id: string; // 公开值
privacy_enabled: boolean; // 后台产品设置,默认开启
turnstile_enabled: boolean;
turnstile_site_key: string; // 公开值;没有 Secret Key
locale: Locale; // 本次协商语言
default_locale?: Locale; // 已弃用的兼容字段,不是产品设置
supported_locales: Locale[];
privacy_policy: PrivacyPolicy | null;
}API 对未自定义的语言返回系统默认条款。若异常配置返回 privacy_policy: null,后台开启隐私确认时 SDK 会阻止报名。privacy_enabled 和 turnstile_enabled 都由后台产品设置控制,接入页面不能通过组件参数关闭。
请求字段
interface ApplicationInput {
google_credential: string;
privacy_enabled?: boolean; // 已弃用,服务端忽略;新代码不发送
privacy_confirmed?: boolean;
privacy_locale?: Locale;
privacy_digest?: string;
feature_request?: string | null;
turnstile_token?: string;
}google_credential:Google 返回的 ID Token,不是 Client ID、Google access token 或 Scoutaro 查询 JWT;每次提交都由后端验签。privacy_enabled请求字段:已弃用,服务端忽略,新 SDK 不发送。请读取config.privacy_enabled,它由后台产品设置控制。privacy_confirmed:开启隐私确认时首次报名必须为 true。privacy_locale/privacy_digest:开启确认且产品有条款时,从config.privacy_policy原样复制;正文变化返回PRIVACY_CHANGED。feature_request:选填,最多 2000 字符;去掉首尾空白,空内容保存为 null。SDK 关闭建议时不发送它。turnstile_token:服务端配置启用 Turnstile 时首次报名必填;token 一次性使用,失败后重新获取。
后台产品已关闭确认时的纯客户端示例(先读取 config 确认;Google token 和挑战需由调用方获取):
const result = await client.submitApplication({
google_credential: googleIdToken,
...(turnstileToken ? { turnstile_token: turnstileToken } : {}),
});
// result.google_email 为后端验证的邮箱。常用返回值
interface Statistics {
reservation_count: number; // 有效报名总人数
beta_count: number; // min(报名人数, 配额)
beta_quota: number; // 内测配额
waitlist_count: number; // max(报名人数 - 配额, 0)
remaining_count: number; // max(配额 - 报名人数, 0)
}
interface ApplicationStatus {
registered: boolean;
eligible: boolean;
google_email: string | null;
}
interface ApplicationResult {
token: string;
expires_in: number; // 秒
google_email: string;
}类型声明随包提供,还导出 Locale、ClientOptions、WidgetOptions、RegistrationWidget、ProductConfig、PrivacyPolicy、ScoutaroError。SDK 不自动获取统计;按页面需要自行调用。
11. 会话、错误与排查
查询 JWT 由 SDK 默认保存在当前站点的 localStorage,键按 API 地址与产品隔离。相同浏览器、相同来源的新标签页或重新打开页面,会在有效期内携带凭据调用 API 恢复已预约状态与邮箱;宿主页面无需实现存储。SDK 不单独持久保存邮箱或已预约标记,也不自动重复提交报名。
升级后首次读取会将当前标签页旧 sessionStorage 查询会话迁移到 localStorage,保留原过期时间。已关闭标签页的旧会话无法恢复;再次用相同 Google 账号登录时,服务端按既有规则去重。localStorage 不可用时降级到 sessionStorage,两者均不可用时退回内存。过期、服务端 401 或 clearSession() 会清除默认存储中的当前产品会话;临时网络故障保留凭据。
storage: null 关闭持久化;自定义 storage 时完全由指定存储负责,SDK 不读取或迁移默认浏览器存储。Google ID Token 和 Turnstile Token 始终只在内存中保留。此改动不延长服务端 JWT 有效期(默认 24 小时),也不能跨浏览器、隐私窗口、根域与 www 等不同来源共享身份。
刷新时,有有效查询 JWT 就请求本人状态并恢复“已预约+邮箱”,不会重新 POST。JWT 默认有效期为 24 小时,实际以服务端 expires_in 为准;没有自动续期,过期后用户重新 Google 登录。服务端按产品与 Google 身份去重,重复提交不会重复计数或通知。
遇到 TURNSTILE_REQUIRED、TURNSTILE_FAILED 或 TURNSTILE_UNAVAILABLE 时,组件每次失败最多重新获取一次产品配置,然后等待用户主动重试;不会自动再次提交报名。重试按最新开关和 Site Key 创建新挑战。建议内容保留;Google Client ID 变化时要求重新登录,条款摘要或语言变化时清空同意勾选。配置刷新失败时,重试先重新加载配置,不能继续使用旧配置提交。配置未变化时仍显示原错误,持续失败应检查服务配置。
SDK 每个 API 请求超时为 15 秒。网络失败、503 或 401 不应被接入页面解读为未报名。提交超时可能意味着响应丢失;SDK 不自动重试写入,用户主动重试时由服务端去重。
错误类型 ScoutaroError 提供 code、message、status、requestId。按稳定 code 判断,不按翻译后的 message 判断。传给 onSuccess 的只有邮箱,业务 API 的完整结果可通过纯客户端获取。
| 现象/错误 | 排查方式 |
| ----------------------------------------- | ------------------------------------------------------------------------------- |
| 初始入口有显示,但点了无法登录 | 检查 API、产品 slug、Google Client ID、浏览器网络请求;ready 可读取初始化错误 |
| ORIGIN_NOT_ALLOWED | 产品后台登记实际页面 Origin;注意 localhost、127.0.0.1 和端口区别 |
| Google 来源错误 | 在 Google Web OAuth 客户端登记页面 JavaScript Origin;当前使用弹窗回调流程 |
| PRIVACY_CHANGED | 阅读期间条款更新;重新加载并再次勾选,禁止自动同意 |
| TURNSTILE_REQUIRED / TURNSTILE_FAILED | 检查产品开关、对应的 Key、允许 hostname;重试时重新获取挑战 |
| INVALID_GOOGLE_TOKEN | 重新登录;检查 Google Client ID 与服务端验签受众一致 |
| GOOGLE_BUSY | 同一页面已挂载另一个完整组件;先销毁旧组件 |
| 刷新仍显示已预约 | 正常会话恢复;清除会话不会删除服务端记录 |
| 修改演示后台后 API 没变化 | 本仓库 8790 演示库与 8787/8788 开发库独立 |
| 新 SDK 报 INVALID_INPUT | 确认 config 已返回布尔型 privacy_enabled,先升级服务端 |
CSP 限制页面需要传入对应 nonce,并允许 API 地址的 connect-src,以及 Google / Turnstile 官方要求的 script、frame、connect 来源。初始化入口使用包内 PNG data URL 图标,img-src 需允许 data:。SDK 无法绕过宿主 CSP、浏览器弹窗限制或第三方登录限制。
同页同时只允许一个完整报名组件;同页 Google Client ID 固定,不应与另一套 GIS 初始化混用。可以创建多个纯 API 客户端。SDK 使用 Google 弹窗登录,不实现整页 OAuth 重定向;真实手机、内置浏览器和第三方 Cookie 设置需在目标环境验收。
12. 打包、升级与验证
本地包安装
在 Scoutaro 仓库根目录:
pnpm --filter @neoaira/scoutaro-sdk build
pnpm --filter @neoaira/scoutaro-sdk pack --pack-destination /tmp/scoutaro-sdk-package把生成的包复制到落地页仓库的 vendor/ 中,然后在落地页项目中执行(版本号以产物为准):
npm install ./vendor/neoaira-scoutaro-sdk-0.1.0.tgz落地页应保存 vendor 包和依赖锁文件。包内包含翻译和独立类型声明,不需安装 @scoutaro/i18n 或引用相邻 Scoutaro 源码。pack 会自动重新构建;发布白名单只包含 JS、类型声明、示例、README、LICENSE 和包清单,不包含 source map 或原始 TypeScript。
npm 公共包
在接入项目执行:
pnpm add @neoaira/scoutaro-sdkimport { mountRegistration } from '@neoaira/scoutaro-sdk';apiBase 填写自己的 Scoutaro API 地址。公开 SDK 不会将报名数据发送给 npm 或 Neoaira。
维护者在仓库根目录先执行 pnpm test:sdk-package,验证实际 tarball 可以在仓库外离线安装、导入和通过类型检查。发布前完成 pnpm check、pnpm format:check,提交发布准备改动,再按下节通过 GitHub Release 发布。无需在本机登录 npm 或上传包。
只有 SDK 包公开,主仓库和其他 workspace 包保持私有。每次发布使用新的版本号;发布完成后应验证 npm view @neoaira/scoutaro-sdk version --registry=https://registry.npmjs.org/,实际可用版本以 npm registry 为准。
通过 GitHub Release 发布
正式发布入口统一为 GitHub Release,工作流为 .github/workflows/publish-sdk.yml,Actions 页面显示为 Publish SDK。合并到 main 只运行 CI,不再自动发布 npm。工作流使用既有 npm Trusted Publishing(OIDC),不需要保存 npm Token;组织 neoaira、仓库 scoutaro、工作流文件 publish-sdk.yml 的绑定保持不变。
下一版以 0.1.2 为例:
- 修改
apps/sdk/package.json的version为0.1.2,将 SDK 改动、测试和说明合并到main,确认 CI 通过。 - 打开 GitHub →
neoaira组织 →scoutaro仓库 → Code → 右侧 Releases → Draft a new release(第一次可能显示 Create a new release)。 - 点击 Choose a tag → 输入
v0.1.2→ Create new tag,Target 选择已通过 CI 的main提交。标签必须是v加包版本号,且指向已合入main的提交。 - 填写标题
Scoutaro SDK v0.1.2和更新说明,保持 Set as a pre-release 未勾选,点击 Publish release。保存草稿、只推送标签、编辑既有 Release 均不触发正式发布。 - 进入仓库 Actions → Publish SDK → 对应 Release 的运行记录。流程检出标签指向的代码,核对标签与包版本、格式、类型、测试、构建、独立安装和持久化,再发布到 npm。新版本必须高于 npm
latest;已存在的版本安全跳过,不覆盖。 - 等待 npm 完成处理后执行
npm view @neoaira/scoutaro-sdk version --registry=https://registry.npmjs.org/,确认新版本可安装。GitHub Release 已发布不等于 npm 流程已经成功;应检查 Actions 结果。
**仅演练:**仓库 Actions → Publish SDK → Run workflow → 选择 main → 填写当前包版本 → Run workflow。此入口始终只测试和打包,不能正式发布;已有版本跳过 npm 发布预检。正式发布失败后,在对应 Release 的 Actions 运行记录选择 Re-run failed jobs 或 Re-run all jobs,不要移动已经发布的标签或覆盖 npm 版本。不要重新运行历史旧版发布工作流以绕过当前入口。
旧变量 SDK_AUTO_PUBLISH 已不再使用。只支持 vX.Y.Z 正式 Release,预发布不会发布 npm。当前仓库私有,GitHub Release 也仅仓库有权限的成员可见;npm 包仍为公开包。工作流关闭 provenance 生成,不影响既有 OIDC 认证。Release 事件与检出规则见 GitHub 官方说明。
v0.1.1 是为已发布 npm 包补建的历史 Release,标签固定指向实际发布提交 7af268ee69eff532f4904db154a773a0d05ac4b0,不重新上传 npm;新流程从后续版本开始使用。
静态文件接入
将 dist/scoutaro.js 放到自己站点静态目录,通过 <script type="module"> 导入。不要把文件当普通脚本期待出现 window.Scoutaro;当前产物是 ES Module,没有 UMD 全局入口。自建 CDN 时需正确设置 JavaScript MIME、CORS 和版本化缓存。
从旧版升级
- 先应用
0008_product_privacy_confirmation.sql(现有产品默认开启),再更新 API 和后台,最后升级 SDK 与重建落地页。已发布迁移保持不变。 - 隐私确认在后台产品设置管理;功能建议仍通过
featureRequest控制。旧 SDK 的界面仍受旧参数影响,必须升级才能跟随后台开关;旧请求参数不能绕过新版 API 校验。 - 成功状态改为“已预约+邮箱”;若页面需要资格或人数,使用纯客户端独立查询。
- 重新构建落地页;如果使用复制的压缩包,仅重新构建 Scoutaro 不会更新落地页中的旧副本。
- 检查四种开关组合、有效会话恢复、语言切换、失败重试以及手机排版。
开发与回归
pnpm --filter @neoaira/scoutaro-sdk typecheck
pnpm --filter @neoaira/scoutaro-sdk build
pnpm --filter @neoaira/scoutaro-sdk dev
pnpm checkhttp://127.0.0.1:8792/__tests 提供模拟 Google、Turnstile 和 API 的浏览器回归,不写真实报名。它覆盖 20 种语言、RTL、选项组合、确认门槛、重复回调、会话恢复、错误重试和销毁。
维护接口或默认值时,必须同步更新本手册、HTML 示例、TypeScript 声明和公共 OpenAPI;外部发布前需使用真实 Google / Turnstile 配置完成联调。包名、npm 渠道、初始版本 0.1.0 和 MIT 许可已确定;实际发布状态以上方说明及 npm registry 为准。
