# @fytkit/sdk 中文文档

规范地址：https://npmcn.com/package/%40fytkit/sdk/

版本：5\.1\.2
npm 元数据更新时间：2026-11-12T11:56:17.864Z
中文译文更新时间：2026-11-15T21:59:32.588Z
软件包许可：UNLICENSED

本文由上游 README 翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%40fytkit/sdk
原文对照：https://npmcn.com/package/%40fytkit/sdk/?lang=original
项目主页：https://github.com/cbarlow1993/fytkit-web#readme
源码仓库：https://github.com/cbarlow1993/fytkit-web

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：846 kB，6 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- 运行依赖：2 个
- peer 依赖：@orpc/client ^1\.13\.5
- 首次发布：2026-05-18T17:02:41.217Z
- 最新版本发布：2026-11-12T11:56:17.634Z
- 近 30 天下载量：2180（2026-10-16 至 2026-11-14，npm 官方下载统计）
- 已发布版本数：27（近 12 个月 27 个）

## 中文简介

用于 fytkit 公共 API 的 Vanilla TypeScript SDK

## 安装

```sh
npm install @fytkit/sdk
```

也可以使用：`pnpm add @fytkit/sdk`、`yarn add @fytkit/sdk`、`bun add @fytkit/sdk`

## README

# @ fytkit/ sdk (英语)

Vanilla TypeScript SDK用于fytkit公共API. 在您的 fytkit 程序、 工作、 食谱和 Mux 保护的视频上, 建一个定制的见习软件 .

> v0.1.x是一个前1.0放行. 每种导出类型的形状在 [`etc/sdk.api.md`](https://npmcn.com/package/%40fytkit/sdk/etc/sdk.api.md); 断开更改记录在 [`CHANGELOG.md`](https://npmcn.com/package/%40fytkit/sdk/CHANGELOG.md)。 。 。 .

## 安装

```bash
pnpm add @fytkit/sdk @orpc/client
# or
npm install @fytkit/sdk @orpc/client
```

在SDK为前 1. 0 时按精确的版本 :

```bash
pnpm add @fytkit/sdk@0.1.0 @orpc/client
```

完全安装 + 版本连接 : [`docs/developer/installation.md`](https://npmcn.com/package/docs/developer/installation.md). semver政策: [`docs/developer/concepts/versioning.md`](https://npmcn.com/package/docs/developer/concepts/versioning.md)。 。 。 .

## 30秒快速启动

```ts
import { createFytkitClient } from "@fytkit/sdk";

const fytkit = createFytkitClient({
  clientId: "fk_app_xxxxxxxxxxxxxxxxx", // from /dashboard/developer
});

// Sign a trainee in (your own auth UI calls this)
const session = await fytkit.auth.signIn({
  email: "trainee@example.com",
  password: "their-password",
});

// Fetch their workouts
const { items } = await fytkit.workouts.list({ limit: 20 });

// Subscribe to auth state (e.g. to redirect on sign-out)
const off = fytkit.auth.onAuthStateChange((state) => {
  if (state.type === "signed_out") window.location.assign("/sign-in");
});

// Sign out — revokes the server-side session and clears local storage
await fytkit.auth.signOut();
off();
```

## 加入一个工作室并启动

一个Fytkit教练风格的应用从工作室的联想代码(它的QR代码或 并阅读工作室的发射设置:

```ts
import { createFytkitClient, resolveJoinCode, resolvePaymentMode } from "@fytkit/sdk";

// Once, when the member scans or types the code: keep the client id.
const studio = await resolveJoinCode(scannedCode);
const fytkit = createFytkitClient({ clientId: studio.clientId });

// Every launch: revalidate the stored settings with their ETag.
const stored = await loadStoredBootstrap(); // { data, etag } | null, your storage
const result = await fytkit.app.bootstrap({ etag: stored?.etag });
if (result.status === "ok") await saveStoredBootstrap({ data: result.data, etag: result.etag });
const settings = result.status === "ok" ? result.data : stored?.data;
// settings.studio (name, logoUrl), settings.theme (palette, variants),
// settings.featureOverrides, settings.minAppVersion, settings.links,
// settings.realtime, settings.paymentPolicy

// "Subscribe" (a link out to the studio's checkout) only where the store allows it.
const { mode } = resolvePaymentMode(settings.paymentPolicy, {
  platform: "ios",
  storefront: storefrontCountryCode, // StoreKit's Storefront.current?.countryCode
});
```

`app.bootstrap` 决定: `{ status: "unchanged" }` (a 304,无尸体) 页:1 `etag` 您发送是当前。 换了 `appConfig.get` 在4.0.0中选择: [`app.bootstrap`](https://npmcn.com/package/docs/developer/reference/app-bootstrap.md),与 逐地迁移. [`resolveJoinCode`](https://npmcn.com/package/docs/developer/reference/resolveJoinCode.md) 不需要客户端. `resolvePaymentMode` 是计划信息 ("你的计划包括什么") 保险单上没有列出每个商店: [订阅或您的计划包括](https://npmcn.com/package/docs/developer/reference/app-bootstrap.md#subscribe-or-what-your-plan-includes)。 。 。 .

## 训练员预览

教练不能签入会员应用程序:员工账户在那里被拒 (单位:千美元)`first_party_account`)取而代之的是App →主题上的"app中的审查" 仪表板显示链接的QR代码,例如: `https://www.trainerlane.com/app-link/preview?client=<clientId>&token=<token>`. 培训员Lane 教练赎回来的 **没有成员的工作室只读预览** (未释放;下一个未成年人的船舶):

```ts
import { createFytkitClient, createMemoryStorageAdapter, refusalData } from "@fytkit/sdk";

// Start from the link's `client` and `token`. Use a dedicated client: the
// phone may be bound to no studio, or to another one, and a preview never
// shares a client with a signed-in member.
const url = new URL(scannedLink);
const preview = createFytkitClient({
  clientId: url.searchParams.get("client") ?? "",
  storage: createMemoryStorageAdapter(),
});
const session = await preview.auth.startPreview({ token: url.searchParams.get("token") ?? "" });
// session: { kind: "preview", accessToken, expiresAt }: show a Preview banner.

// Studio content works as for a member on any plan: app.bootstrap, programs,
// workouts, exercises, articles, recipes, nutrition, media, plans.
const programs = await preview.programs.list({});

// Anything a member owns (profile, progress, logs, check-ins, habits,
// messages, health) and every write is refused with `preview_mode`:
// show the screen's empty state.
try {
  await preview.progress.getStreak();
} catch (err) {
  if (!refusalData(err, "preview_mode")) throw err;
  showEmptyState();
}

// End it (signOut does the same while previewing).
await preview.auth.endPreview();
```

-   **一次,15分钟内.** QR令牌在15分钟之内赎回一次 显示。 一个未知的,过期的,使用过的 或其他工作室的代号拒绝 `preview_token_invalid` (单位:千美元)`not_found`:向教练索取新代码. A 类 工作室自己的应用程序(不是Fytkit教练) `preview_unavailable`。 。 。 .
-   **60分钟,只有记忆.** 预览会持续60分钟,永远不会 清新而永无止境 `storage`,所以重装结束。 结束于 `expiresAt`打开 `endPreview` 或者说 `signOut`,任何401(工作室转向) 当一个成员签到同一个客户时 每端 但最后的喷出 `signed_out`· ; `onPreviewChange` 报告每个开始和 结束.
-   **不与一员并.** `startPreview` 本地拒绝 `member_session_active` (单位:千美元)`conflict`),而一名成员则在其中签字 客户端.

反应 : `usePreviewSession`, (中文(简体) ), `useStartPreview` 财务报告和审定财务报表 `useEndPreview` 内 `@fytkit/react` (释出. 每一种方法: [`auth.startPreview`](https://npmcn.com/package/docs/developer/reference/auth.md#authstartpreview)。 。 。 .

## 追踪健身

登录一个解决临时选择( 不需要程序注册), 记录集, 完成 然后读回进展:

```ts
// Start an ad-hoc log against any workout — or pass { sessionId, enrollmentId }
// instead to log a scheduled program session.
const { data: log } = await fytkit.sessionLogs.create({ workoutId });

await fytkit.sessionLogs.addSet({
  id: log.id,
  exerciseId,
  setNumber: 1,
  reps: 8,
  weightKg: "80.00",
});

await fytkit.sessionLogs.update({ id: log.id, completedAt: new Date().toISOString() });

// Workout-by-workout history for that workout
const { data: history } = await fytkit.sessionLogs.list({ workoutId });

// Stored progress: streak, PR history, aggregate summary
const { data: streak } = await fytkit.progress.getStreak({});
const { data: prs } = await fytkit.progression.listPrHistory({ exerciseId });
const { data: summary } = await fytkit.progress.getSummary({ period: "12w" });
```

## 二因子登录

`auth.signIn` 返回歧视性结果。 当学员有2FA时 启用时,以他们选择的方法完成挑战:

```ts
const result = await fytkit.auth.signIn({ email, password });

if (result.kind === "two_factor_required") {
  const { challenge } = result; // challenge.availableMethods: "totp" | "backup_code" | "email_otp"

  // TOTP from their authenticator app…
  const session = await fytkit.auth.verifyTotp({ challenge, code: "123456" });

  // …or a one-time backup code:
  // await fytkit.auth.verifyBackupCode({ challenge, code });

  // …or an emailed code:
  // await fytkit.auth.requestEmailOtp({ challenge });
  // await fytkit.auth.verifyEmailOtp({ challenge, code });
}
```

每个验证方法缓存已发行的会话完全像普通的登录 (单位:千美元)`signed_in` 开火 `onAuthStateChange`) (中文(简体) ).

## 2FA 注册和账户安全

所有这一切都需要签署会议:

```ts
// Enroll: render totpUri as a QR code; show backupCodes exactly once.
const { totpUri, backupCodes } = await fytkit.auth.enableTotp({ password });
await fytkit.auth.verifyTotpSetup({ code: "123456" }); // SDK swaps the rotated token for you

const status = await fytkit.auth.getSecurityStatus(); // { twoFactorEnabled, totpEnabled, hasBackupCodes }
await fytkit.auth.regenerateBackupCodes({ password });
await fytkit.auth.disableTwoFactor({ password });

// Password change (verifies the current password server-side). With
// revokeOtherSessions every other session of the account ends, in any app,
// and the SDK adopts the rotated token transparently.
await fytkit.auth.changePassword({ currentPassword, newPassword, revokeOtherSessions: true });

// Email change — a confirmation link is emailed to the new address;
// nothing changes until the trainee opens it.
await fytkit.auth.changeEmail({ newEmail });

// Resend the sign-up verification email to the trainee's own address (there
// is no email input). Resolves to { status: "sent" } or, when the address is
// already verified and nothing is sent, { status: "already_verified" }.
// Throttled per account (one a minute, five an hour): over the limit it
// rejects with { kind: "rate_limited", retryAfter } (milliseconds).
const { status } = await fytkit.auth.resendVerification();

// Devices signed in through this app (plus the calling one); the trainee's
// sessions elsewhere are never listed. revokeId is an opaque id (never the
// bearer token).
const sessions = await fytkit.auth.listSessions();
await fytkit.auth.revokeSession({ revokeId: sessions.find((s) => !s.isCurrent)!.revokeId });
await fytkit.auth.revokeOtherSessions(); // "sign out of your other devices" on this app

// Permanently close the account (re-verifies email + password). On success the
// SDK clears the session and emits signed_out.
await fytkit.auth.closeAccount({ confirmEmail, password });
```

密码错误或一次性代码为 `{ kind: "server", status: 400 }` 错误 与 `code: "invalid_password"` 编号 : `"invalid_code"`; 需要改变 新签注是 `{ kind: "forbidden", code: "reauth_required" }`无 签下受训者——只有401个经认证的电话意味着 课会结束。 密码错误 `signIn` (例如再次签署) 之后 `reauth_required`是一个平地 `auth_required` 离开电流 单独开会。 在Fytkit的训练员上运行或工作的账户不能 通过应用程序和现有应用程序签名、注册或完成两个要素 会话无法更改密码或电子邮件, 无法重新发送其校验邮件或关闭它 : `{ kind: "forbidden", code: "first_party_account" }`指给Fytkit自己看 注册。 两次在程序中注册是 `{ kind: "conflict" }` 与 现有 `enrollmentId`。 。 。 .

## 拒绝

当服务器因某种原因拒绝时, 您的应用程序可以采取行动( 教练功能) 计划缺乏,一个已经发送的报到, 一个消息限制,一个工作室是 , 错误带有 `refusal: { reason, data }` 除了平常的田园. 分会 `refusal.reason`,从未在消息文本上; `refusalData` 读取 已知原因的数据已输入 :

```ts
import { refusalData } from "@fytkit/sdk";

try {
  await fytkit.messages.send({ body, clientRequestId });
} catch (err) {
  const plan = refusalData(err, "entitlement_required");
  if (plan) return showUpgrade(plan.entitlement);
  const daily = refusalData(err, "daily_limit");
  if (daily) return keepDraftUntil(daily.retryAt);
  throw err;
}
```

所有原因及其数据: [`docs/developer/reference/errors.md`](https://npmcn.com/package/docs/developer/reference/errors.md#refusals)。 。 。 .

## 实时聊天

`subscribeToMessages` 在Supabase 上保持对讲屏 实时: 它获取一个符号( Q)`messages.realtimeToken`)和fytkit的公众 连接设置( e)`app.bootstrap`),加入受训者自己的私人 频道(并在实时关闭后重新加入), 刷新符号 并报告何时投票 没有消息内容的事件 : `onChange` 意思是读取 谈话和未读数".

SDK并不依赖Supabase -js. 添加 `@supabase/supabase-js` 改为 您的应用程序( y)`npm install @supabase/supabase-js@^2.97.0`并通过一个工厂 仅为订阅创建一个客户端。 必须是2.87.1或 后:助教用手将学员的道具放入插座上,并 更早的2.x客户端在下个心跳时将它换为肛门键 将订阅保留在 `fallback`保留 `auth` 以下选项: 客户端只携带学员的标志,不得保存或刷新 语句,或从页面的 URL 读取一个将替换它的签名 .

```ts
import { createClient } from "@supabase/supabase-js";
import { MESSAGES_FALLBACK_POLL_MS, subscribeToMessages } from "@fytkit/sdk";

const unsubscribe = subscribeToMessages(fytkit, {
  realtime: ({ url, anonKey }) =>
    createClient(url, anonKey, {
      auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false },
    }),
  onChange: () => refreshConversation(), // messages.list + messages.unreadCount
  onStatus: (status) => {
    // "live": stop polling. "connecting" / "fallback": poll the conversation
    // every MESSAGES_FALLBACK_POLL_MS.conversation (10 s) and the count every
    // MESSAGES_FALLBACK_POLL_MS.unreadCount (60 s). "refused": no trainee to
    // listen for (signed out); the subscription has stopped.
    setPolling(status === "connecting" || status === "fallback");
  },
});

// On leaving the screen or signing out:
unsubscribe();
```

给大家看"Sam在写作" 而训练队的教练 写回信、通行证 `onCoachTyping`报告 `{ kind: "typing", names }` (播放姓名,正如谈话显示在他们的信息中)和 `{ kind: "idle" }`。它仅是活的:一个名字本身就消失了8秒 在其最后的标志之后(`COACH_TYPING_EXPIRY_MS`),所以一个迷失的"停止"从来没有 离开它显示,它去 `idle` 当频道丢失,而你 民意调查从不显示.

```ts
subscribeToMessages(fytkit, {
  realtime,
  onChange: () => refreshConversation(),
  onCoachTyping: (typing) =>
    setWritingLine(typing.kind === "typing" ? `${firstName(typing.names[0])} is writing…` : null),
});
```

Polling 花费您的应用程序的每分钟请求限制, 所有用户都会使用此限制 共享,所以当状态是 `live`。完整指南: [`messages.realtimeToken`](https://npmcn.com/package/docs/developer/reference/data-namespaces.md#messagesrealtimetoken) 和食谱 [与实时更新聊天屏幕](https://npmcn.com/package/docs/developer/recipes/chat-screen-with-live-updates.md)。 。 。 .

## 通知偏好

成员切换被存储的服务器侧( 默认适用到第一次写入) :

```ts
const { data: prefs } = await fytkit.preferences.getNotifications();
await fytkit.preferences.updateNotifications({ streakAlerts: false }); // partial patch
```

`coachMessages`, (中文(简体) ), `checkInReminders` 财务报告和审定财务报表 `coachFeedback` 是推动的切换 (直到成员关机为止);“指定程序”按下 `newPrograms`。 。 。 .

## 推动通知

Fytkit教练为签约成员注册了设备的"博览会"推克. 代币绑在它注册的会话上 所以签了, 取消该会话或关闭账户会将其删除到服务器上; 呼叫 `unregister` 首先,所以电话停止 被推一次.

```ts
await fytkit.push.register({ token: expoPushToken, platform: "ios" }); // or "android"
await fytkit.push.unregister({ token: expoPushToken }); // idempotent
```

一个工作室自己的开发者应用被拒绝 `{ kind: "forbidden" }`: 仅 Fytkit教练收到推力. 每一次推车 `data` 是一个 `PushNotificationData` (单位:千美元)`{ v: 1, clientId, kind, href }`,没有消息文本); 忽略 `clientId` 工作室不是固定的 合同: [文件/开发者/指南/push-notections.md](https://npmcn.com/package/docs/developer/guides/push-notifications.md)。 。 。 .

## 此预览中包含的内容

-   `resolveJoinCode` (一个工作室与其客户端id,名称,标志和app主题的联想代码,没有客户端)并 `app.bootstrap` (发射读取,用其ETag重新验证) `resolvePaymentMode` ("订阅"链接出或"你的计划包括什么",每个商店前端)
-   `auth.signUp` 编号 : `signIn` 编号 : `signOut` 编号 : `getSession` 编号 : `onAuthStateChange`
-   `auth.signInWithIdToken` (与Apple或Google签约,Fytkit教练仅:通过生非ce;Apple获得其SHA-256六,Google获得生非ce)
-   训练员预览 : `auth.startPreview` 编号 : `getPreview` 编号 : `onPreviewChange` 编号 : `endPreview` (只读,无成员预览从"app中的预览"QR代码;参见: [训练员预览](https://npmcn.com/package/%40fytkit/sdk/#readme-trainer-preview)(中文(简体) )
-   `auth.requestPasswordReset` 编号 : `confirmPasswordReset` 编号 : `confirmEmail` 编号 : `resendVerification`
-   双因素: `auth.verifyTotp` 编号 : `verifyBackupCode` 编号 : `requestEmailOtp` 编号 : `verifyEmailOtp` (签名), `enableTotp` 编号 : `verifyTotpSetup` 编号 : `disableTwoFactor` 编号 : `regenerateBackupCodes` 编号 : `getSecurityStatus` (管理层)
-   账户安全 : `auth.changePassword` 编号 : `changeEmail` 编号 : `confirmEmailChange` 编号 : `listSessions` 编号 : `revokeSession` 编号 : `revokeOtherSessions` 编号 : `closeAccount`
-   数据命名空间 : `workouts`, (中文(简体) ), `programs`, (中文(简体) ), `recipes`, (中文(简体) ), `nutrition`, (中文(简体) ), `nutritionLog`, (中文(简体) ), `profile`, (中文(简体) ), `preferences`, (中文(简体) ), `push`, (中文(简体) ), `articles`, (中文(简体) ), `enrollments`, (中文(简体) ), `sessionLogs`, (中文(简体) ), `progression`, (中文(简体) ), `progress`, (中文(简体) ), `bodyMetrics`, (中文(简体) ), `bodyMeasurements`, (中文(简体) ), `progressPhotos`, (中文(简体) ), `checkIns`, (中文(简体) ), `messages`, (中文(简体) ), `timeline`, (中文(简体) ), `health`, (中文(简体) ), `habits`, (中文(简体) ), `media`, (中文(简体) ), `subscription`
-   工作跟踪:程序会话日志和临时工作日志(`sessionLogs.create` 与 `workoutId`),设置记录,每个工作历史(`sessionLogs.list({ workoutId })`(中文(简体) )
-   进展: `progress.getSummary` 编号 : `getAdherence` 编号 : `getStreak` (存储记录) / `getVolume` (上周硬套每肌肉组), `progression.getPrSummary` 编号 : `listPrHistory` (存储PR历史) / `getE1rmTrend` (每届估计1-雷普最大,每次升起)/ `getHistory`, (中文(简体) ), `bodyMetrics.list` 编号 : `record` 编号 : `update` 编号 : `delete`
-   教练对记录的会议的反馈: `sessionLogs.getFeedback` 编号 : `markFeedbackSeen` 编号 : `unseenFeedbackCount`
-   登录 : `checkIns.current` 编号 : `submit` 编号 : `update` 编号 : `list` 编号 : `get` 编号 : `markReplySeen` 编号 : `requestPhotoUpload` (每周或每两周向教练报到;需要与 `coaching_checkins`(中文(简体) )
-   信件数 : `messages.list` 编号 : `send` 编号 : `requestAttachmentUpload` 编号 : `markRead` 编号 : `delete` 编号 : `unreadCount` 编号 : `realtimeToken` (受训者与导师的对话; 开始一个计划需要与 `coaching_messages`回答教练不; `requestAttachmentUpload` 保留随信件一起发送的照片)
-   实时聊天 : `subscribeToMessages` (加入学员的私人实时频道 `messages.realtimeToken`并说何时投票 [实时聊天](https://npmcn.com/package/%40fytkit/sdk/#readme-live-chat)(中文(简体) )
-   自动信头注射( E)`X-TrainerLane-Client-Id`, (中文(简体) ), `Authorization: Bearer …`(中文(简体) )
-   主动刷新~60 s 在令牌过期前( 下线时用后退) + 被动 401 重试; 只有服务器拒绝刷新标志退出
-   可插值存储( E)`localStorage` 默认, SSR 的内存+ cookie)

## 储存权衡

访问令牌在 `localStorage` 默认时,名称空格 `clientId`. 运行在您来源上的任何脚本都可以读取这些脚本——发送严格的CSP,并将SDK像任何其他具有认证的依赖性一样对待. 符號是服务器会话,持续时间可长达7天;签署,关闭账户或撤销开发者app结束其服务器侧.

使用 `createMemoryStorageAdapter()` 用于SSR+测试.

## 下一个

-   `@fytkit/react` ——钩子+ `<FytkitVideo>`
-   `examples/sdk-react` ——可运行的样本应用程序
-   `docs/sdk/quickstart.md` ——全向导

## 常见问题

### @fytkit/sdk 是什么？

用于 fytkit 公共 API 的 Vanilla TypeScript SDK

### 如何安装 @fytkit/sdk？

在项目目录运行 npm install @fytkit/sdk，或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖：@orpc/client（^1\.13\.5）。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖，Yarn 需要手动安装。

### @fytkit/sdk 支持 TypeScript 吗？

支持。包内自带 TypeScript 类型声明（package\.json 声明了 types 入口），安装后即可获得类型提示，无需再安装 @types 包。

### @fytkit/sdk 支持 ESM 还是 CommonJS？

以 ES Module 形式发布，推荐使用 import 引入。在 CommonJS 项目中，Node\.js 20\.19\+ / 22\.12\+ 可以直接 require\(\) 不含顶层 await 的 ES Module，更早的版本需要改用动态 import\(\)。

### @fytkit/sdk 可以商用吗？

@fytkit/sdk 使用 UNLICENSED 许可。作者声明为 UNLICENSED，即不授予公开使用许可（与公共领域的 Unlicense 不同）。使用前需获得作者授权。以上为概括，不构成法律意见，请以许可原文为准。

### @fytkit/sdk 的下载量有多少？

根据 npm 官方下载统计，@fytkit/sdk 在 2026/10/16 – 2026/11/14 这 30 天内被下载 2,180 次，日均约 73 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入，适合横向比较热度，不能直接等同于使用人数。

### @fytkit/sdk 还在维护吗？

最新版本 5\.1\.2 发布于 2026/11/12（3 天前）；至今共发布 27 个版本，其中过去 12 个月发布了 27 个；首次发布于 2026/05/19。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

