关于 extension-core-infrastructure
Caffeine AI 项目的基础设施组件,为前端提供 Internet Identity 身份验证和后端 Actor 管理能力,面向使用该平台构建应用的开发者。它会随项目自动包含,并根据身份变化重建 Actor。
主要功能
- 01提供 Internet Identity 身份验证
- 02支持 Google、Microsoft 和工作区 SSO 登录
- 03管理登录状态与会话恢复
- 04按身份变化重建后端 Actor
- 05支持用于视觉 QA 的模拟后端
适用场景
- 01根据身份状态控制界面访问
- 02为应用添加多种 Internet Identity 登录入口
- 03在登录或退出后更新后端连接
- 04使用模拟后端进行视觉 QA
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ caffeinelabs/ skills --skill extension-core-infrastructure安装后在 Claude Code 中输入/extension-core-infrastructure确认已加载。
附带文件migration/v0.x.y-to-v1.x.y.mdmigration/v1.x.y-to-v2.x.y.md
详细介绍
核心基础设施
适用于 Caffeine AI 的核心基础设施扩展。
概述
此组件为所有项目提供基础设施:后端连接配置、Internet Identity 身份验证钩子和 actor 管理工具。
要求
"@caffeineai/core-infrastructure": "^2.0.0"
"@caffeineai/object-storage": "^1.1.0"
"@icp-sdk/auth": "^9.0.0"
"@icp-sdk/core": "^5.3.0"
@caffeineai/object-storage 是 core-infrastructure 的对等依赖项。每个项目都必须将其作为直接 npm 依赖项安装(构建模板已包含这两个包)。
集成
核心基础设施会自动包含在每个项目中。无需手动集成。
前端
核心基础设施前端包(@caffeineai/core-infrastructure)会自动包含在每个项目中。
应用入口点
使用 InternetIdentityProvider 和 QueryClientProvider 包裹应用:
import { InternetIdentityProvider } from "@caffeineai/core-infrastructure";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import ReactDOM from "react-dom/client";
import App from "./App";
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById("root")!).render(
<QueryClientProvider client={queryClient}>
<InternetIdentityProvider>
<App />
</InternetIdentityProvider>
</QueryClientProvider>,
);
useInternetIdentity() — 身份验证钩子
提供 Internet Identity 的身份状态、登录和登出功能。
返回值
| 字段 | 类型 | 描述 |
|---|---|---|
identity | Identity | undefined | 用户的身份(登录或恢复会话后可用) |
login | (options?: LoginOptions) => void | 打开 II 弹窗。触发后即返回——不要使用 await。请参阅登录方式。 |
clear | () => void | 登出并清除已存储的身份。触发后即返回。 |
isAuthenticated | boolean | 用户具有有效身份时为 true。用于控制 UI 的显示。 |
isInitializing | boolean | AuthClient 正从 IndexedDB 加载时为 true |
isLoggingIn | boolean | II 弹窗打开期间为 true |
isLoginSuccess | boolean | 仅在交互式登录后为 true(页面重新加载后恢复会话时不会为 true) |
isLoginError | boolean | 登录或初始化失败时为 true |
isSessionExpired | boolean | 会话结束时为 true(空闲/TTL/提供方撤销),主动登出不属于此情况 |
loginError | Error | undefined | isLoginError 为 true 时的错误对象 |
身份验证状态生命周期
| 场景 | loginStatus | isAuthenticated |
|---|---|---|
| 页面加载,没有已存储的会话 | "idle" | false |
| 正在恢复已存储的会话 | "initializing" | false → true |
| 页面重新加载后已恢复已存储的会话 | "idle" | true |
| 交互式登录进行中 | "logging-in" | false |
| 交互式登录刚完成 | "success" | true |
| 登录弹窗失败/已取消 | "loginError" | false |
| 会话已过期(空闲/TTL/撤销) | "expired" | false |
| 在另一个标签页中登出 | "idle" | false |
重要: isLoginSuccess 仅在通过弹窗完成交互式登录后为 true——页面重新加载时恢复已存储的身份不会使其变为 true。条件渲染时始终使用 isAuthenticated。
身份验证状态会被存储,而不是仅保存在客户端实例中:在一个标签页中登录或登出,其他标签页也会同步更新。会话过期(isSessionExpired)并非主动登出——应显示会话结束页面,而不是已登出后的落地页。
用法
使用 isAuthenticated 控制经过身份验证的 UI:
const { isAuthenticated } = useInternetIdentity();
{isAuthenticated ? <AuthenticatedApp /> : <LoginScreen />}
在初始化或登录期间禁用登录按钮:
const { login, isInitializing, isLoggingIn } = useInternetIdentity();
<button onClick={() => login()} disabled={isInitializing || isLoggingIn}>
Sign in
</button>
login() 和 clear() 触发后即返回——钩子的状态字段(isLoggingIn、isInitializing)会跟踪异步生命周期。不要使用本地 useState/isPending 逻辑来包装它们。
登录方式:普通 II、Google、Microsoft、工作区 SSO
login() 接受一个可选的 LoginOptions 对象,用于选择用户的登录方式。所有方式都会通过 Internet Identity 完成登录,并使用相同的身份、会话行为和注销方式——它们只改变用户首先看到的界面:
login(); // Plain Internet Identity sign-in
login({ provider: "google" }); // One-click Google sign-in (II opens Google OAuth directly)
login({ provider: "microsoft" }); // One-click Microsoft sign-in (II opens Microsoft OAuth directly)
login({ ssoDomain: "acme.com" }); // Company/workspace SSO via the domain's identity provider
- Google:无需 Google API 密钥或 OAuth 客户端设置——Internet Identity 会处理 OAuth 流程。
- Microsoft:无需注册 Azure/Entra 应用——Internet Identity 会管理 OAuth 客户端。支持个人 Microsoft 账户以及工作或学校账户。
- 工作区 SSO:用户输入公司域名(例如
acme.com);Internet Identity 会从https://<domain>/.well-known/ii-openid-configuration发现公司的 OpenID Connect 提供商,并通过该提供商完成登录(适用于 Okta、Entra ID 以及公司配置的其他 OIDC 提供商)。如果应用需要使用特定公司的自有租户,请使用此方式;如果需要无需按公司单独设置的一键式 Microsoft 按钮,请使用provider: "microsoft"。 - 不提供 Apple 登录:Internet Identity 不会为 Apple 返回电子邮件或姓名声明,因此属性回调将为空。
- 所有方式的会话存储方式相同:无论使用哪种方式,
isAuthenticated、重新加载时恢复会话以及clear()的行为都完全相同。 - 当后端使用
caffeineai-authorization时,Google、Microsoft 和 SSO 登录会自动将经过验证的姓名/电子邮件属性(以及 SSO 域名)传递给属性回调——请参阅extension-authorization技能。
只能在按钮的 onClick 处理程序中调用 login()。 只有在真实点击事件正在触发时,才能打开 Internet Identity 弹窗;其他情况下都会失败,并显示 Signer window should not be opened outside of click handler。具体来说:
- 切勿在表单的
onSubmit中调用login()——submit事件会在点击事件结束后触发,因此检查会失败。对于 SSO 域名字段,请使用普通的<div>(而非<form>)以及type="button"的提交按钮,在其onClick中验证域名并直接调用login({ ssoDomain })。 - 切勿在键盘处理程序中(例如在域名输入框中按 Enter 时)或
await之后调用login()——这两种情况都会在点击事件触发过程之外运行。
如果应用在调用 login 前验证 SSO 域名,请遵循 Internet Identity 自身的规则:接受至少包含两个标签的普通 DNS 名称(例如 acme.com),或环回主机——localhost 或 127.0.0.1,可选带有 :port(例如 localhost:3000)。II 接受环回域名用于本地测试,因此输入验证不得拒绝这些域名。
标准登录界面模式——醒目的 Google 和 Microsoft 按钮、普通 II 登录,以及提示用户输入域名的“公司 SSO”选项:
function SignInOptions() {
const { login, isInitializing, isLoggingIn } = useInternetIdentity();
const [ssoDomain, setSsoDomain] = useState("");
const disabled = isInitializing || isLoggingIn;
return (
<div>
<button onClick={() => login({ provider: "google" })} disabled={disabled}>
Continue with Google
</button>
<button onClick={() => login({ provider: "microsoft" })} disabled={disabled}>
Continue with Microsoft
</button>
<button onClick={() => login()} disabled={disabled}>
Sign in with Internet Identity
</button>
{/* Company SSO: deliberately not a <form> — login must run inside the
button's click event, and form onSubmit fires after the click ends */}
<input
value={ssoDomain}
onChange={(e) => setSsoDomain(e.target.value)}
placeholder="yourcompany.com"
/>
<button
onClick={() => login({ ssoDomain: ssoDomain.trim() })}
disabled={disabled || !ssoDomain.trim()}
>
Sign in with your company
</button>
</div>
);
}
仅提供应用实际需要的登录方式:除非需要 Google、Microsoft 或公司 SSO 登录,否则默认使用普通的 login()。当需要其中某种方式时,登录页面必须显示所需的直接登录选项(Google 按钮、Microsoft 按钮和/或 SSO 域名输入框),并且保留一个普通的“使用 Internet Identity 登录”按钮作为备用选项——没有 Google 或 Microsoft 账户,或没有已注册公司域名的用户仍必须能够登录。
useActor() — 后端 Actor Hook
创建并管理一个类型化的后端 Actor 实例。用户身份变更(登录/注销)时会自动重新创建 Actor。
import { useActor } from "@caffeineai/core-infrastructure";
import { createActor } from "@/backend";
function MyComponent() {
const { actor, isFetching } = useActor(createActor);
// actor is null while loading, then the typed backend actor
if (!actor || isFetching) return <Loading />;
// Call backend methods directly
const data = await actor.myBackendMethod();
}
返回值
| 字段 | 类型 | 描述 |
|---|---|---|
actor | T | null | 类型化的后端 Actor;加载期间为 null |
isFetching | boolean | 正在创建 Actor 时为 true |
身份变更(登录、注销或恢复会话)时,会自动使用新身份重新创建 Actor,并使所有依赖查询失效后重新获取。
视觉 QA 模拟后端(VITE_USE_MOCK=true)
useActor 可以使用应用自有的模拟后端,而不是连接 canister。将模拟后端放在 src/frontend/src/mocks/backend.ts,导出 mockBackend,并从应用源代码传入 glob:
import { useActor } from "@caffeineai/core-infrastructure";
import { createActor } from "@/backend";
const mockModules = import.meta.glob("../mocks/backend.{ts,tsx,js,jsx}");
export function useAppActor() {
return useActor(createActor, { mockModules });
}
glob 必须写在应用中,因为 Vite 会根据包含该调用的文件来解析 import.meta.glob;软件包无法访问应用的 mocks/ 目录。使用 glob 而非直接导入,可以在模拟文件不存在时仍确保构建成功。只有当 VITE_USE_MOCK=true 时才会使用模拟后端;否则,useActor 的行为与之前完全相同,并会加载真实后端配置。
在 React 外部,createActorWithConfig(createActor, { mockModules }) 接受相同的选项,而 loadMockBackendFromModules(mockModules) 会自行解析模拟后端。