# @blinkdotnew/sdk 中文文档

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

版本：2\.11\.4
npm 元数据更新时间：2026-11-04T13:24:32.003Z
中文译文更新时间：2026-11-08T06:51:37.655Z
软件包许可：MIT

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

原始 npm 页面：https://www.npmjs.com/package/%40blinkdotnew/sdk
原文对照：https://npmcn.com/package/%40blinkdotnew/sdk/?lang=original
项目主页：https://blink.new/
源码仓库：https://github.com/ShadowWalker2014/blink-sdk

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

- 包体积（解压后）：882 kB，7 个文件
- 模块格式：ESM + CJS
- TypeScript 类型：包内自带
- 运行依赖：0 个
- 首次发布：2025-06-28T04:28:24.619Z
- 最新版本发布：2026-11-04T13:24:31.309Z
- 近 30 天下载量：13382（2026-10-09 至 2026-11-07，npm 官方下载统计）
- 已发布版本数：120（近 12 个月 47 个）

## 中文简介

用于客户端应用程序的 Blink TypeScript SDK - 0-boilerplate CRUD + auth + AI + 分析 + 现代 SaaS/AI 应用程序的通知

## 安装

```sh
npm install @blinkdotnew/sdk
```

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

## README

# @ blinkdotnew/ sdk (英语)

[![npm version](https://badge.fury.io/js/%40blinkdotnew%2Fsdk.svg)](https://badge.fury.io/js/%40blinkdotnew%2Fsdk) [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)

**授权 Blink AI 生成的应用程序的全存储 TypeScript SDK**

Blink是一款AI App Builder,以秒为单位构建出功能完备的应用程序. 这个SDK( S)`@blinkdotnew/sdk`)是TypeScript的基础,它赋予每个 Blink app 本地权限,提供0-boilerplate认证,数据库操作,AI能力,以及文件存储. 在客户端(React,Vue等)和服务器端(Node.js,Deno,Edge功能)上无缝工作.

## QQ 快速启动

### **步骤1: 创建一个闪烁工程**

访问 [眨一眼,换一眼](https://blink.new/) 并创建一个新项目。 布莱克的AI代理 将在几秒钟内构建你的应用程序.

### **第2步:安装SDK**

使用 Blink 的 AI 代理在您的 Vite React TypeScript 客户端中自动安装此 SDK, 或者手动安装 :

```bash
npm install @blinkdotnew/sdk
```

### **第3步: 使用您的工程ID**

从您的闪烁仪表板上获取您的项目ID并开始构建:

```typescript
import { createClient } from '@blinkdotnew/sdk'

const blink = createClient({
  projectId: 'your-blink-project-id', // From blink.new dashboard
  authRequired: false // Don't force immediate auth - let users browse first
})

// Authentication - Choose your mode:

// 🎯 MANAGED MODE: Quick setup with hosted auth page
const blink = createClient({
  projectId: 'your-project',
  auth: { mode: 'managed' }
})
// Use: blink.auth.login() - redirects to blink.new auth

// 🎨 HEADLESS MODE: Custom UI with full control  
const blink = createClient({
  projectId: 'your-project', 
  auth: { mode: 'headless' }
})
// Use: blink.auth.signInWithEmail(), blink.auth.signInWithGoogle(), etc.

// Current user (works in both modes)
const user = await blink.auth.me()

// Database operations (zero config)
const todos = await blink.db.todos.list({
  where: { userId: user.id },
  orderBy: { createdAt: 'desc' },
  limit: 20
})

// AI operations (native)
const { text } = await blink.ai.generateText({
  prompt: "Write a summary of the user's todos"
})

// Data operations (extract text from documents)
const text = await blink.data.extractFromUrl("https://example.com/document.pdf")

// Website scraping and screenshots (crystal clear results!)
const { markdown, metadata, links } = await blink.data.scrape("https://competitor.com")
const screenshotUrl = await blink.data.screenshot("https://competitor.com")

// Web search (get real-time information)
const searchResults = await blink.data.search("chatgpt latest news", { type: 'news' })
const localResults = await blink.data.search("best restaurants", { location: "San Francisco,CA,United States" })

// Notifications (NEW!)
const { success } = await blink.notifications.email({
  to: 'customer@example.com',
  subject: 'Your order has shipped!',
  html: '<h1>Order Confirmation</h1><p>Your order #12345 is on its way.</p>'
})

// Secure API proxy (call external APIs with secret substitution)
const response = await blink.data.fetch({
  url: "https://api.sendgrid.com/v3/mail/send",
  method: "POST",
  headers: { "Authorization": "Bearer {{sendgrid_api_key}}" },
  body: { /* email data */ }
})

// Realtime operations (live messaging and presence)
const unsubscribe = await blink.realtime.subscribe('chat-room', (message) => {
  console.log('New message:', message.data)
})

await blink.realtime.publish('chat-room', 'message', { text: 'Hello world!' })

// Get presence - returns array of PresenceUser objects directly
const users = await blink.realtime.presence('chat-room')
console.log('Online users:', users.length)
// users is PresenceUser[] format:
// [
//   { 
//     userId: 'user123', 
//     metadata: { displayName: 'Alice', status: 'online' },
//     joinedAt: 1640995200000,
//     lastSeen: 1640995230000
//   }
// ]

// Analytics operations (automatic pageview tracking + custom events)
// Pageviews are tracked automatically on initialization and route changes
blink.analytics.log('button_clicked', {
  button_id: 'signup',
  page: '/pricing'
})

// Check if analytics is enabled
if (blink.analytics.isEnabled()) {
  console.log('Analytics is active')
}

// Turn SDK analytics off from the start: the SDK sends and stores nothing
// (sites hosted on Blink also get a separate page-view tag, not controlled here)
// const blink = createClient({ projectId, analytics: false })

// Opt a visitor out (remembered on this device) or back in
blink.analytics.disable()
blink.analytics.enable()

// Storage operations (instant - returns public URL directly)
const { publicUrl } = await blink.storage.upload(
  file,
  `avatars/${user.id}-${Date.now()}.${file.name.split('.').pop()}`, // ✅ Extract original extension
  { upsert: true }
)
```

## 什么是闪烁?

**闪烁是 AI App 构建器** 以秒数创建全功能应用程序。 简而言之,你想要构建的东西,而Blink的AI代理将:

-   🏗️ **生成完整的应用程序** 使用反应 + 类型脚本 + Vite
-   🔧 **自动安装此 SDK** 配置为零
-   🎨 **创建美丽的 UIs** 有尾风 CSS
-   🚀 **立即部署** 具有认证、数据库、AI和存储内置

## SDK 特性

这个 SDK 赋予每个 Blink 生成的应用程序 :

-   **认证**: 具有管理(定向)和无头(海关UI)模式的灵活认证系统,电子邮件/密码,社交供应商(Google,GitHub,苹果,微软),魔法链接,RBAC,以及自定义电子邮件品牌
-   **QQ 数据库**: PostgREST 兼容 CRUD 操作,高级过滤
-   **大赦国际**:多模式图像生成和编辑(10个模式),文本生成,同时进行网络搜索,对象生成,语音合成,并抄录
-   **数据**: 从文档中取出文本内容, 以秘密替代安全 API 代理, 网络刮取, 截图和网络搜索
-   **📁 存储**: 文件上传、下载和管理
-   **\* 通知**:带有附件的电子邮件发送、定制品牌和交货跟踪
-   **实时**: 基于 WebSocket 的 pub/ sub 消息、 存在跟踪和实时更新
-   **分析**: 自动页面视图跟踪、自定义事件记录、会话管理以及隐私第一设计
-   **通用**: 客户端和服务器端的工作
-   **框架不可知论**:反应,Vue,斯维尔特,香草JS,Node.js,德诺, **原生反应**
-   **\* 反应原生**:具有AsyncStorage集成和平台意识功能的一等移动支持
-   **实时**: 内置认证状态管理并刷新标志
-   **零波列尔普拉特**: 一切都在盒子里运作

## QQ 手动安装和设置

> **QQ 小费**:如果你使用Blink的AI代理,这都是自动为你完成的!

### 客户端( 反应, Vue等)

```typescript
import { createClient } from '@blinkdotnew/sdk'

const blink = createClient({
  projectId: 'your-blink-project-id', // From blink.new dashboard
  authRequired: false, // Let users browse first - require auth only for protected areas
  auth: {
    mode: 'managed' // Use new explicit configuration
  }
})
```

> **版本要求**: 灵活的认证系统( 管理 vs 无头模式) 需要 SDK 版本 **0.1.8.0或以上(含)**. 若使用0.17.x或以下版本,则只能访问遗留认证系统. 请升级以访问新的认证功能 .

### 服务器侧 (Node.js, Deno, Edge 函数)

```typescript
import { createClient } from '@blinkdotnew/sdk'

const blink = createClient({
  projectId: 'your-blink-project-id', // From blink.new dashboard
  auth: { mode: 'managed' } // Manual token management
})

// Token injection is only needed when calling blink.auth.* methods on the server
```

### QQ 本地反应( iOS & Android)

SDK已经 **一等反应原生支持** 拥有自动适应移动环境的平台意识功能.

#### 步骤1:安装附属物

```bash
npm install @blinkdotnew/sdk @react-native-async-storage/async-storage expo-web-browser
```

**需要的软件包 :**

-   `@blinkdotnew/sdk` - 闪闪发光的SDK
-   `@react-native-async-storage/async-storage` - 为象征的坚持
-   `expo-web-browser` - 用于OAuth认证(Google, GitHub, Apple等)

#### 步骤2: 创建客户端( E)`lib/blink.ts`(中文(简体) )

**你一定要通过 `webBrowser: WebBrowser` 启用 OAuth 移动!**

```typescript
// lib/blink.ts
import { createClient, AsyncStorageAdapter } from '@blinkdotnew/sdk'
import AsyncStorage from '@react-native-async-storage/async-storage'
import * as WebBrowser from 'expo-web-browser'  // ← Import this
import * as Linking from 'expo-linking'

export const blink = createClient({
  projectId: 'your-project-id',
  authRequired: false,
  auth: {
    mode: 'headless',
    webBrowser: WebBrowser,  // ← Pass it here! Required for OAuth
    linking: Linking         // ← Binds the sign-in to your app (needs a reverse-domain app.json scheme)
  },
  storage: new AsyncStorageAdapter(AsyncStorage)
})
```

与 `linking` 和一个倒置域 `scheme` 内 `app.json` (例如:. `com.example.myapp`), OAuth 返回到 您的应用程序有一次性代码, 只有您的应用程序可以收集登录。 没有他们(或在博览会) sDK又回到了旧的流量.

当应用程序启动时, SDK 会恢复保存的会话 `AsyncStorage` 独自一人。 在此之前 结束, `onAuthStateChanged` 报告 `isLoading: true`,所以等待它 之前显示登录.

#### 第3步: 使用认证

**电子邮件/密码(立即工作):**

```typescript
// Sign up
const user = await blink.auth.signUp({ 
  email: 'user@example.com', 
  password: 'SecurePass123' 
})

// Sign in
const user = await blink.auth.signInWithEmail('user@example.com', 'SecurePass123')
```

**OAuth (谷歌, GitHub, 苹果, 微软):**

```typescript
// ✅ Same code works on web, iOS, AND Android!
const user = await blink.auth.signInWithGoogle()
const user = await blink.auth.signInWithGitHub()
const user = await blink.auth.signInWithApple()
const user = await blink.auth.signInWithMicrosoft()
```

#### 第4步: 创建 Auth Hook(`hooks/useAuth.ts`(中文(简体) )

```typescript
// hooks/useAuth.ts
import { useEffect, useState } from 'react'
import { blink } from '@/lib/blink'
import type { BlinkUser } from '@blinkdotnew/sdk'

export function useAuth() {
  const [user, setUser] = useState<BlinkUser | null>(null)
  const [isLoading, setIsLoading] = useState(true)

  useEffect(() => {
    const unsubscribe = blink.auth.onAuthStateChanged((state) => {
      setUser(state.user)
      setIsLoading(state.isLoading)
    })
    return unsubscribe
  }, [])

  return {
    user,
    isLoading,
    isAuthenticated: !!user,
    signInWithGoogle: () => blink.auth.signInWithGoogle(),
    signInWithGitHub: () => blink.auth.signInWithGitHub(),
    signInWithApple: () => blink.auth.signInWithApple(),
    signOut: () => blink.auth.signOut(),
  }
}
```

#### 第5步:组件使用

```typescript
import { useAuth } from '@/hooks/useAuth'
import { View, Text, Button } from 'react-native'

function App() {
  const { user, isLoading, signInWithGoogle, signOut } = useAuth()

  if (isLoading) return <Text>Loading...</Text>

  if (!user) {
    return <Button onPress={signInWithGoogle} title="Sign in with Google" />
  }

  return (
    <View>
      <Text>Welcome, {user.email}!</Text>
      <Button onPress={signOut} title="Sign Out" />
    </View>
  )
}
```

#### 常见错误

```typescript
// ❌ WRONG: Missing webBrowser - OAuth won't work on mobile!
const blink = createClient({
  projectId: 'your-project-id',
  auth: { mode: 'headless' },  // Missing webBrowser!
  storage: new AsyncStorageAdapter(AsyncStorage)
})

// ✅ CORRECT: Include webBrowser for OAuth support
import * as WebBrowser from 'expo-web-browser'

const blink = createClient({
  projectId: 'your-project-id',
  auth: { 
    mode: 'headless',
    webBrowser: WebBrowser  // ← Required for OAuth!
  },
  storage: new AsyncStorageAdapter(AsyncStorage)
})
```

#### 低级 OAuth (用于自定义控制)

如果您需要自定义的投票超时或人工浏览器处理 :

```typescript
// Get auth URL and authenticate function separately
const { authUrl, authenticate } = await blink.auth.signInWithProviderMobile('google')

// Open browser manually
await WebBrowser.openAuthSessionAsync(authUrl)

// Poll with custom options
const user = await authenticate({
  maxAttempts: 120,  // 60 seconds (default: 60 = 30 seconds)
  intervalMs: 500    // Check every 500ms
})
```

#### 平台特征

-   ✅ **同步组合** - 坚守安全
-   ✅ **通用操作** - 同一代码在网络+移动上有效
-   ✅ **前网络浏览器** - 本地浏览器UI
-   ✅ **无深层链接** - 按会期投票
-   ✅ **博览会** - 不需要自定义dev客户端
-   ✅ **自动签名刷新** - 无缝会议

## API 参考

### 认证

> **版本要求**: 灵活的认证系统需要SDK版本 **0.1.8.0或以上(含)**. 0.17.x和以下版本只支持遗留的认证系统.

闪烁提供 **两种认证模式**数字 :

## QQ 管理模式( 直接模式)

**完美为:** 快速设置, 最小代码 **最佳服务:** 网站,简易应用程序,MVP开发

```typescript
const blink = createClient({
  projectId: 'your-project',
  auth: { mode: 'managed' }
})

// ONE METHOD: Redirect to hosted auth page
blink.auth.login() // → Redirects to blink.new/auth
blink.auth.logout() // Clear tokens and redirect

// User state (automatic after redirect)
const user = await blink.auth.me()
```

## QQ 无头模式( Custom UI)

**完美为:** 自定义品牌、高级UX、移动应用程序 **最佳服务:** 生产应用、品牌经验

```typescript
const blink = createClient({
  projectId: 'your-project',
  auth: { mode: 'headless' }
})

// MULTIPLE METHODS: Build your own UI
const user = await blink.auth.signUp({ email, password })
const user = await blink.auth.signInWithEmail(email, password)
const user = await blink.auth.signInWithGoogle()
const user = await blink.auth.signInWithGitHub()
const user = await blink.auth.signInWithApple()
const user = await blink.auth.signInWithMicrosoft()

// ✅ Store custom signup fields inside metadata
await blink.auth.signUp({
  email: 'founder@example.com',
  password: 'SuperSecret123',
  displayName: 'Alex Founder',
  role: 'operations',
  metadata: {
    company: 'Acme Freight',
    marketingConsent: true
  }
})
// `displayName`, `avatar`, and `role` map to dedicated auth columns.
// Everything else goes into auth.users.metadata automatically.
// Keep custom fields in metadata or your own profile table—avoid adding NOT NULL
// columns directly to auth tables.

// Magic links (passwordless)
await blink.auth.sendMagicLink(email)

// Password management
await blink.auth.sendPasswordResetEmail(email)
await blink.auth.sendPasswordResetEmail(email, { 
  redirectUrl: 'https://myapp.com/reset-password' 
})
await blink.auth.changePassword(oldPass, newPass)

// Email verification
await blink.auth.sendEmailVerification()
```

### QQ 快速模式比较

| 特性 | **管理模式** | **无头模式** |
| --- | --- | --- |
| **设置** | 1行码 | 需要自定义用户界面 |
| **方法** | `login()` 仅限 | `signInWith*()` 方法 |
| **用户** | 主机认证页面 | 您的自定义表格 |
| **标记** | 闪闪发光 | 完全自定义 |
| **移动** | 网络重定向 | 土著融合 |

### 🚨 **常见错误**

```typescript
// ❌ WRONG: Using managed method in headless mode
const blink = createClient({
  auth: { mode: 'headless' }
})
blink.auth.login() // Still redirects! Wrong method for headless

// ✅ CORRECT: Use headless methods
await blink.auth.signInWithEmail(email, password)
await blink.auth.signInWithGoogle()
```

#### 提供方配置

**步骤1: 在您的工程中启用提供者**

1.  转到 [眨一眼,换一眼](https://blink.new/) 打开您的项目
2.  导航到 **项目-工作空间-认证**
3.  切换打开/关闭提供者 :
    -   **电子邮件** QQ(默认启用) - 包括电子邮件/密码、魔法链接和校验
    -   **谷歌** (默认启用)
    -   **GitHub 图像** qq( 默认残疾)
    -   **苹果公司** qq( 默认残疾)
    -   **微软** qq( 默认残疾)
4.  配置电子邮件设置 :
    -   **需要电子邮件验证**: 默认关闭(容易执行)
    -   **允许用户注册**: 在默认情况下

**步骤2:发现现有供应商**

```typescript
// Get providers enabled for your project
const availableProviders = await blink.auth.getAvailableProviders()
// Returns: ['email', 'google'] (based on your project settings)

// Use in your UI to show only enabled providers
const showGoogleButton = availableProviders.includes('google')
const showGitHubButton = availableProviders.includes('github')
```

**第3步: 客户端- 系统过滤( 无头模式)**

```typescript
const blink = createClient({
  projectId: 'your-project',
  auth: {
    mode: 'headless'
    // All providers controlled via project settings
  }
})
```

**管理模式: 自动提供者显示**

```typescript
const blink = createClient({
  projectId: 'your-project',
  auth: { mode: 'managed' }
})

// The hosted auth page automatically shows only enabled providers
blink.auth.login() // Shows Email + Google by default
```

#### 电子邮件验证流程

**默认情况下, 不需要电子邮件验证** 以方便执行。 仅在需要时启用 :

**步骤1:配置验证(可选)**

1.  转到 [眨一眼,换一眼](https://blink.new/) 项目 工作空间 认证
2.  切换 **"需要电子邮件验证"** ON( 默认损坏)

**第2步:处理整流**

```typescript
// User signup - always send verification email for security
try {
  const user = await blink.auth.signUp({ email, password })
  await blink.auth.sendEmailVerification()
  setMessage('Account created! Check your email to verify.')
} catch (error) {
  setError(error.message)
}

// User signin - handle verification requirement
try {
  await blink.auth.signInWithEmail(email, password)
  // Success - user is signed in
} catch (error) {
  if (error.code === 'EMAIL_NOT_VERIFIED') {
    setError('Please verify your email first')
    await blink.auth.sendEmailVerification() // Resend verification
  } else {
    setError(error.message)
  }
}

// Manual verification resend
await blink.auth.sendEmailVerification()
```

**发生什么了**

1.  **注册**: 创建用户账户, `email_verified = false`
2.  **校验电子邮件**: 用户点击链接 `email_verified = true`
3.  **签名检查**: 如果需要核查而未经核查 □ `EMAIL_NOT_VERIFIED` 错误
4.  **成绩**: 用户一旦核实(或不需要核实)即可签名

#### 灵活电子邮件系统

**最大灵活性** - 控制电子邮件品牌,而Brink处理安全标志:

> **`generatePasswordResetToken()` 财务报告和审定财务报表 `generateMagicLinkToken()` 只有服务器.** 他们 返回可赎回的证书,所以他们需要客户端与 `secretKey` 并且现在 拒绝(403)否则。 从浏览器代码使用中 `sendPasswordResetEmail()` 编号 : `sendMagicLink()` 换句话说.

```typescript
// Option A: Custom email delivery (full branding control) — SERVER-ONLY
const serverBlink = createClient({
  projectId: process.env.BLINK_PROJECT_ID,
  secretKey: process.env.BLINK_SECRET_KEY,
})
const resetData = await serverBlink.auth.generatePasswordResetToken('user@example.com')
// Returns: { token, expiresAt, resetUrl }

// Send with your email service and branding
await yourEmailService.send({
  to: 'user@example.com',
  subject: 'Reset your YourApp password',
  html: `
    <div style="font-family: Arial, sans-serif;">
      <img src="https://yourapp.com/logo.png" alt="YourApp" />
      <h1>Reset Your Password</h1>
      <a href="${resetData.resetUrl}" 
         style="background: #0070f3; color: white; padding: 16px 32px; 
                text-decoration: none; border-radius: 8px;">
        Reset Password
      </a>
    </div>
  `
})

// Option B: Blink default email (zero setup)
await blink.auth.sendPasswordResetEmail('user@example.com')

// Email verification is scoped to the signed-in user, so it stays client-safe
const verifyData = await blink.auth.generateEmailVerificationToken()

// Magic links mint a sign-in credential — server-only, like Option A above
const magicData = await serverBlink.auth.generateMagicLinkToken('user@example.com')
```

#### 角色接入控制

```typescript
// Configure roles and permissions
const blink = createClient({
  projectId: 'your-project',
  auth: {
    mode: 'headless',
    roles: {
      admin: { permissions: ['*'] },
      editor: { permissions: ['posts.create', 'posts.update'] },
      viewer: { permissions: ['posts.read'] }
    }
  }
})

// Check permissions
const canEdit = blink.auth.can('posts.update')
const isAdmin = blink.auth.hasRole('admin')
const isStaff = blink.auth.hasRole(['admin', 'editor'])

// Use in components
function EditButton() {
  if (!blink.auth.can('posts.update')) return null
  return <button onClick={editPost}>Edit Post</button>
}
```

#### 核心方法

> QQ Tokens 为 Blink API 自动管理. 使用 `getValidToken()` 只有您必须手动将一个符号传递给您的后端或第三方服务.

```typescript
// User management
const user = await blink.auth.me()
await blink.auth.updateMe({ displayName: 'New Name' })

// Token management
blink.auth.setToken(jwt, persist?)
const isAuth = blink.auth.isAuthenticated()
const token = await blink.auth.getValidToken() // Get valid token (auto-refreshes)

// Password management
await blink.auth.sendPasswordResetEmail('user@example.com')
await blink.auth.sendPasswordResetEmail('user@example.com', { 
  redirectUrl: 'https://myapp.com/reset-password' // Custom reset page
})
await blink.auth.changePassword('oldPass', 'newPass')
await blink.auth.confirmPasswordReset(token, newPassword)

// Email verification
await blink.auth.verifyEmail(token)

// Provider discovery
const providers = await blink.auth.getAvailableProviders()

// Auth state listener (REQUIRED for React apps!)
const unsubscribe = blink.auth.onAuthStateChanged((state) => {
  console.log('Auth state:', state)
  // state.user - current user or null
  // state.isLoading - true while auth is initializing
  // state.isAuthenticated - true if user is logged in
  // state.tokens - current auth tokens
})
```

> **会话在网络blips中幸存.** SDK仅在认证服务器表示会话无效(过期,被撤销,被封禁,或被删除的账户)时才会将用户签名出局. 如果一个令牌刷新无法到达服务器——离线,当一个电话应用程序从背景恢复时,一个请求被放弃,或者一个临时的服务器错误——存储会话和用户被保留. 在下一个请求中重试了已丢弃的连接; 在服务器出错后, SDK 在重试前等待了几秒钟 . 虽然认证服务器无法访问, `getValidToken()` 返回当前访问符号,即使它已经过期(而不是 `null`),所以您的请求作为一个网络错误失败或获得一个 `401` 您可以重试—— 它返回 `null` 只有在没有会议的时候.

#### 登录方向行为

何时 `login()` 名称, SDK 自动确定认证后方向:

```typescript
// Automatic redirect (uses current page URL)
blink.auth.login()
// → Redirects to: blink.new/auth?redirect_url=https://yourapp.com/current-page

// Custom redirect URL
blink.auth.login('https://yourapp.com/dashboard')
// → Redirects to: blink.new/auth?redirect_url=https://yourapp.com/dashboard

// Manual login button example
const handleLogin = () => {
  // The SDK will automatically use the current page URL
  blink.auth.login()
  
  // Or specify a custom redirect
  // blink.auth.login('https://yourapp.com/welcome')
}
```

**固定在v1.x中**: SDK 现在确保重定向 URL 总是绝对的, 防止在 `window.location.href` 返回相对路径.

### 数据库操作

**QQ新:自动大小写转换!**  
SDK现在在JavaScript骆驼Case和SQL蛇 case之间自动转换:

-   **表格名称**数字 : `blink.db.emailDrafts` → `email_drafts` 图表
-   **字段名称**数字 : `userId`, (中文(简体) ), `createdAt`, (中文(简体) ), `isCompleted` → `user_id`, (中文(简体) ), `created_at`, (中文(简体) ), `is_completed`
-   **不需要手动转换!**

**QQ 重要: 总是在您的代码中使用骆驼Case**

-   ✅ **对**数字 : `blink.db.emailDrafts.create({ userId: user.id, createdAt: new Date() })`
-   ❌ **错误**数字 : `blink.db.email_drafts.create({ user_id: user.id, created_at: new Date() })`

```typescript
// Create (ID auto-generated if not provided)
const todo = await blink.db.todos.create({
  id: 'todo_12345', // Optional - auto-generated if not provided
  title: 'Learn Blink SDK',
  userId: user.id,        // camelCase in code
  createdAt: new Date(),  // camelCase in code
  isCompleted: false      // camelCase in code
})

// Read with filtering - returns camelCase fields
const todos = await blink.db.todos.list({
  where: {
    AND: [
      { userId: user.id },                    // camelCase in filters
      { OR: [{ status: 'open' }, { priority: 'high' }] }
    ]
  },
  orderBy: { createdAt: 'desc' },           // camelCase in orderBy
  limit: 20
})
// `todos` is a direct array: Todo[]

// Note: Boolean fields are returned as "0"/"1" strings from SQLite
// Check boolean values using Number(value) > 0
const completedTodos = todos.filter(todo => Number(todo.isCompleted) > 0)
const incompleteTodos = todos.filter(todo => Number(todo.isCompleted) === 0)

// Update
await blink.db.todos.update(todo.id, { isCompleted: true })

// Delete
await blink.db.todos.delete(todo.id)

// Bulk operations (IDs auto-generated if not provided)
await blink.db.todos.createMany([
  { title: 'Task 1', userId: user.id }, // ID will be auto-generated
  { id: 'custom_id', title: 'Task 2', userId: user.id } // Custom ID provided
])
await blink.db.todos.upsertMany([...])


```

### AI 操作

```typescript
// Text generation (simple prompt)
const { text } = await blink.ai.generateText({
  prompt: 'Write a poem about coding',
  maxTokens: 150
})

// Web search (OpenAI only) - get real-time information
const { text, sources } = await blink.ai.generateText({
  prompt: 'Who is the current US president?',
  search: true // Returns current info + source URLs
})

// Multi-step reasoning - for complex analysis
const { text } = await blink.ai.generateText({
  prompt: 'Research and analyze tech trends',
  search: true,
  maxSteps: 10, // Override default (25 when tools used)
  experimental_continueSteps: true // Override default (true when tools used)
})

// Text generation with image content
// ⚠️ IMPORTANT: Images must be HTTPS URLs with file extensions (.jpg, .jpeg, .png, .gif, .webp)
// For file uploads, use blink.storage.upload() first to get public HTTPS URLs
// 🚫 CRITICAL: When uploading files, NEVER hardcode extensions - use file.name or auto-detection
const { text } = await blink.ai.generateText({
  messages: [
    { 
      role: "user", 
      content: [
        { type: "text", text: "What do you see in this image?" },
        { type: "image", image: "https://storage.googleapis.com/.../.../photo.jpg" }
      ]
    }
  ]
})

// Mixed content with multiple images
const { text } = await blink.ai.generateText({
  messages: [
    { 
      role: "user", 
      content: [
        { type: "text", text: "Compare these two images:" },
        { type: "image", image: "https://storage.googleapis.com/.../.../image1.jpg" },
        { type: "image", image: "https://cdn.example.com/image2.png" }
      ]
    }
  ]
})

// Structured object generation
const { object } = await blink.ai.generateObject({
  prompt: 'Generate a user profile',
  schema: {
    type: 'object',
    properties: {
      name: { type: 'string' },
      age: { type: 'number' }
    }
  }
})

// ⚠️ IMPORTANT: Schema Rule for generateObject()
// The top-level schema MUST use type: "object" - you cannot use type: "array" at the top level
// This ensures clear, robust, and extensible API calls with named parameters

// ✅ Correct: Array inside object
const { object: todoList } = await blink.ai.generateObject({
  prompt: 'Generate a list of 5 daily tasks',
  schema: {
    type: 'object',
    properties: {
      tasks: {
        type: 'array',
        items: { 
          type: 'object',
          properties: {
            title: { type: 'string' },
            priority: { type: 'string', enum: ['low', 'medium', 'high'] }
          }
        }
      }
    },
    required: ['tasks']
  }
})
// Result: { tasks: [{ title: "Exercise", priority: "high" }, ...] }

// ❌ Wrong: Top-level array (will fail)
// const { object } = await blink.ai.generateObject({
//   prompt: 'Generate tasks',
//   schema: {
//     type: 'array',  // ❌ This will throw an error
//     items: { type: 'string' }
//   }
// })
// Error: "schema must be a JSON Schema of 'type: \"object\"', got 'type: \"array\"'"

// Generate and modify images with AI - Multi-Model Support (10 models available)
// 🔥 Choose between fast generation or high-quality results
// 🎨 For style transfer: provide ALL images in the images array, don't reference URLs in prompts

// Basic image generation (uses default fast model: fal-ai/nano-banana)
const { data } = await blink.ai.generateImage({
  prompt: 'A serene landscape with mountains and a lake at sunset'
})
console.log('Image URL:', data[0].url)

// High-quality generation with Pro model
const { data: proImage } = await blink.ai.generateImage({
  prompt: 'A detailed infographic about AI with charts and diagrams',
  model: 'fal-ai/nano-banana-pro', // High quality model
  n: 1,
  size: '1792x1024' // Custom size
})

// Generate multiple variations
const { data } = await blink.ai.generateImage({
  prompt: 'A futuristic robot in different poses',
  model: 'fal-ai/nano-banana', // Fast model
  n: 3
})
data.forEach((img, i) => console.log(`Image ${i+1}:`, img.url))

**Available Models for Text-to-Image:**

| Model | Speed | Quality | Best For |
|-------|-------|---------|----------|
| `fal-ai/nano-banana` (default) | ⚡ Fast | Good | Prototypes, high-volume generation |
| `fal-ai/nano-banana-pro` | Standard | ⭐ Excellent | Marketing materials, high-fidelity visuals |
| `fal-ai/gemini-25-flash-image` | ⚡ Fast | Good | Alias for `nano-banana` |
| `fal-ai/gemini-3-pro-image-preview` | Standard | ⭐ Excellent | Alias for `nano-banana-pro` |
| `gemini-2.5-flash-image-preview` | ⚡ Fast | Good | Legacy - Direct Gemini API |
| `gemini-3-pro-image-preview` | Standard | ⭐ Excellent | Legacy - Direct Gemini API |

// Image editing - transform existing images with prompts (uses default fast model)
const { data: headshots } = await blink.ai.modifyImage({
  images: ['https://storage.example.com/user-photo.jpg'], // Up to 16 images supported!
  prompt: 'Transform into professional business headshot with studio lighting'
})

// High-quality editing with Pro model
const { data: proEdited } = await blink.ai.modifyImage({
  images: ['https://storage.example.com/portrait.jpg'],
  prompt: 'Add a majestic ancient tree in the background with glowing leaves',
  model: 'fal-ai/nano-banana-pro/edit' // High quality editing
})

// Advanced image editing with multiple input images
const { data } = await blink.ai.modifyImage({
  images: [
    'https://storage.example.com/photo1.jpg',
    'https://storage.example.com/photo2.jpg',
    'https://storage.example.com/photo3.jpg'
  ],
  prompt: 'Combine these architectural styles into a futuristic building design',
  model: 'fal-ai/nano-banana/edit', // Fast editing
  n: 2
})

**Available Models for Image Editing:**

| Model | Speed | Quality | Best For |
|-------|-------|---------|----------|
| `fal-ai/nano-banana/edit` (default) | ⚡ Fast | Good | Quick adjustments, style transfers |
| `fal-ai/nano-banana-pro/edit` | Standard | ⭐ Excellent | Detailed retouching, complex edits |
| `fal-ai/gemini-25-flash-image/edit` | ⚡ Fast | Good | Alias for `nano-banana/edit` |
| `fal-ai/gemini-3-pro-image-preview/edit` | Standard | ⭐ Excellent | Alias for `nano-banana-pro/edit` |

// 🎨 Style Transfer & Feature Application
// ⚠️ IMPORTANT: When applying styles/features from one image to another,
// provide ALL images in the array - don't reference images in the prompt text

// ❌ WRONG - Don't reference images in prompt
const wrongWay = await blink.ai.modifyImage({
  images: [userPhotoUrl],
  prompt: `Apply this hairstyle from the reference image to the person's head. Reference hairstyle: ${hairstyleUrl}`
})

// ✅ CORRECT - Provide all images in the array
const { data } = await blink.ai.modifyImage({
  images: [userPhotoUrl, hairstyleUrl], // Both images provided
  prompt: 'Apply the hairstyle from the second image to the person in the first image. Blend naturally with face shape.'
})

// More style transfer examples
const { data } = await blink.ai.modifyImage({
  images: [portraitUrl, artworkUrl],
  prompt: 'Apply the artistic style and color palette from the second image to the portrait in the first image'
})

const { data } = await blink.ai.modifyImage({
  images: [roomPhotoUrl, designReferenceUrl],
  prompt: 'Redesign the room in the first image using the interior design style shown in the second image'
})

// Multiple reference images for complex transformations
const { data } = await blink.ai.modifyImage({
  images: [
    originalPhotoUrl,
    lightingReferenceUrl,
    colorPaletteReferenceUrl,
    compositionReferenceUrl
  ],
  prompt: 'Transform the first image using the lighting style from image 2, color palette from image 3, and composition from image 4'
})

// 📱 File Upload + Style Transfer
// ⚠️ Extract file extension properly - never hardcode .jpg/.png

// ❌ WRONG - Hardcoded extension
const userUpload = await blink.storage.upload(file, `photos/${Date.now()}.jpg`) // Breaks HEIC/PNG files
// ✅ CORRECT - Extract original extension
const userUpload = await blink.storage.upload(
  userPhoto.file, 
  `photos/${Date.now()}.${userPhoto.file.name.split('.').pop()}`
)
const hairstyleUpload = await blink.storage.upload(
  hairstylePhoto.file,
  `haircuts/${Date.now()}.${hairstylePhoto.file.name.split('.').pop()}`
)

const { data } = await blink.ai.modifyImage({
  images: [userUpload.publicUrl, hairstyleUpload.publicUrl],
  prompt: 'Apply hairstyle from second image to person in first image'
})


// Speech synthesis
const { url } = await blink.ai.generateSpeech({
  text: 'Hello, world!',
  voice: 'nova'
})

// Audio transcription - Multiple input formats supported
// 🔥 Most common: Browser audio recording → Base64 → Transcription
let mediaRecorder: MediaRecorder;
let audioChunks: Blob[] = [];

// Step 1: Start recording
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);

mediaRecorder.ondataavailable = (event) => {
  audioChunks.push(event.data);
};

mediaRecorder.start();

// Step 2: Stop recording and transcribe
mediaRecorder.onstop = async () => {
  const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
  
  // SAFE method for large files - use FileReader (recommended)
  const base64 = await new Promise<string>((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      const dataUrl = reader.result as string;
      const base64Data = dataUrl.split(',')[1]; // Extract base64 part
      resolve(base64Data);
    };
    reader.onerror = reject;
    reader.readAsDataURL(audioBlob);
  });
  
  // Transcribe using base64 (preferred method)
  const { text } = await blink.ai.transcribeAudio({
    audio: base64,  // Raw base64 string
    language: 'en'
  });
  
  console.log('Transcription:', text);
  audioChunks = []; // Reset for next recording
};

// Alternative: Data URL format (also supported)
const reader = new FileReader();
reader.onload = async () => {
  const dataUrl = reader.result as string;
  const { text } = await blink.ai.transcribeAudio({
    audio: dataUrl,  // Data URL format
    language: 'en'
  });
};
reader.readAsDataURL(audioBlob);

// File upload transcription
const fileInput = document.getElementById('audioFile') as HTMLInputElement;
const file = fileInput.files[0];

// Option 1: Convert to base64 using FileReader (recommended for large files)
const base64Audio = await new Promise<string>((resolve, reject) => {
  const reader = new FileReader();
  reader.onload = () => {
    const dataUrl = reader.result as string;
    const base64Data = dataUrl.split(',')[1]; // Extract base64 part
    resolve(base64Data);
  };
  reader.onerror = reject;
  reader.readAsDataURL(file);
});

const { text } = await blink.ai.transcribeAudio({
  audio: base64Audio,
  language: 'en'
});

// Option 2: Use ArrayBuffer directly (works for any file size)
const arrayBuffer = await file.arrayBuffer();
const { text } = await blink.ai.transcribeAudio({
  audio: arrayBuffer,
  language: 'en'
});

// Option 3: Use Uint8Array directly (works for any file size)
const arrayBuffer = await file.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
const { text } = await blink.ai.transcribeAudio({
  audio: uint8Array,
  language: 'en'
});

// Public URL transcription (for hosted audio files)
const { text } = await blink.ai.transcribeAudio({
  audio: 'https://example.com/audio/meeting.mp3',
  language: 'en'
});

// Advanced options
const { text } = await blink.ai.transcribeAudio({
  audio: base64Audio,
  language: 'en',
  model: 'whisper-1',
  response_format: 'verbose_json'  // Get timestamps and confidence scores
});

// Supported audio formats: MP3, WAV, M4A, FLAC, OGG, WebM
// Supported input types:
// - string: Base64 data, Data URL (data:audio/...;base64,...), or public URL (https://...)
// - ArrayBuffer: Raw audio buffer (works for any file size)
// - Uint8Array: Audio data as byte array (works for any file size)
// - number[]: Audio data as number array

// ⚠️ IMPORTANT: For large audio files, use FileReader or ArrayBuffer/Uint8Array
// Avoid btoa(String.fromCharCode(...array)) as it crashes with large files

// Streaming support
await blink.ai.streamText(
  { prompt: 'Write a story...' },
  (chunk) => console.log(chunk)
)

// Streaming with web search
await blink.ai.streamText(
  { prompt: 'Latest AI news', search: true },
  (chunk) => console.log(chunk)
)

// React streaming example - parse chunks for immediate UI display
const [streamingText, setStreamingText] = useState('')

await blink.ai.streamText(
  { prompt: 'Write a story about AI...' },
  (chunk) => {
    setStreamingText(prev => prev + chunk) // chunk is a string
  }
)
```

### 数据操作

```typescript
// Simple text extraction (default - returns single string)
const text = await blink.data.extractFromUrl('https://example.com/document.pdf');
console.log(typeof text); // 'string'

// Extract with chunking enabled
const chunks = await blink.data.extractFromUrl('https://example.com/document.pdf', {
  chunking: true,
  chunkSize: 2000
});
console.log(Array.isArray(chunks)); // true

// Extract from different file types
const csvText = await blink.data.extractFromUrl('https://example.com/data.csv');
const htmlText = await blink.data.extractFromUrl('https://example.com/page.html');
const jsonText = await blink.data.extractFromUrl('https://example.com/config.json');

// Extract from uploaded file blob (simple)
const fileInput = document.getElementById('fileInput') as HTMLInputElement;
const file = fileInput.files[0];
const extractedText = await blink.data.extractFromBlob(file);

// Extract from uploaded file blob (with chunking)
const chunks = await blink.data.extractFromBlob(file, {
  chunking: true,
  chunkSize: 3000
});

// Website scraping (NEW!) - Crystal clear destructuring
const { markdown, metadata, links, extract } = await blink.data.scrape('https://example.com');
console.log(markdown); // Clean markdown content
console.log(metadata.title); // Page title
console.log(links.length); // Number of links found

// Even cleaner - destructure only what you need
const { metadata, extract } = await blink.data.scrape('https://blog.example.com/article');
console.log(metadata.title); // Always available
console.log(extract.headings); // Always an array

// Website screenshots (NEW!)
const screenshotUrl = await blink.data.screenshot('https://example.com');
console.log(screenshotUrl); // Direct URL to screenshot image

// Full-page screenshot with custom dimensions
const fullPageUrl = await blink.data.screenshot('https://example.com', {
  fullPage: true,
  width: 1920,
  height: 1080
});

// 🔥 Web Search (NEW!) - Google search results with clean structure
// Perfect for getting real-time information and current data

// Basic web search - just provide a query
const searchResults = await blink.data.search('chatgpt');
console.log(searchResults.organic_results); // Main search results
console.log(searchResults.related_searches); // Related search suggestions
console.log(searchResults.people_also_ask); // People also ask questions

// Search with location for local results
const localResults = await blink.data.search('best restaurants', {
  location: 'San Francisco,CA,United States'
});
console.log(localResults.local_results); // Local business results
console.log(localResults.organic_results); // Regular web results

// News search - get latest news articles
const newsResults = await blink.data.search('artificial intelligence', {
  type: 'news'
});
console.log(newsResults.news_results); // News articles with dates and sources

// Image search - find images
const imageResults = await blink.data.search('elon musk', {
  type: 'images',
  limit: 20
});
console.log(imageResults.image_results); // Image results with thumbnails

// Search in different languages
const spanishResults = await blink.data.search('noticias tecnología', {
  language: 'es',
  type: 'news'
});

// Shopping search - find products
const shoppingResults = await blink.data.search('macbook pro', {
  type: 'shopping'
});
console.log(shoppingResults.shopping_results); // Product results with prices

// All search types return consistent, structured data:
// - organic_results: Main search results (always included)
// - related_searches: Related search suggestions
// - people_also_ask: FAQ-style questions and answers
// - local_results: Local businesses (when location provided)
// - news_results: News articles (when type='news')
// - image_results: Images (when type='images')
// - shopping_results: Products (when type='shopping')
// - ads: Sponsored results (when present)

// 🔥 Secure API Proxy (NEW!) - Make API calls with secret substitution

// Basic API call with secret substitution
const response = await blink.data.fetch({
  url: 'https://api.sendgrid.com/v3/mail/send',
  method: 'POST',
  headers: {
    'Authorization': 'Bearer {{sendgrid_api_key}}', // Secret replaced server-side
    'Content-Type': 'application/json'
  },
  body: {
    from: { email: 'me@example.com' },
    personalizations: [{ to: [{ email: 'user@example.com' }] }],
    subject: 'Hello from Blink',
    content: [{ type: 'text/plain', value: 'Sent securely through Blink!' }]
  }
});

console.log('Email sent:', response.status === 200);
console.log('Response:', response.body);
console.log('Took:', response.durationMs, 'ms');

// GET request with secret in URL and query params
const weatherData = await blink.data.fetch({
  url: 'https://api.openweathermap.org/data/2.5/weather',
  method: 'GET',
  query: {
    q: 'London',
    appid: '{{openweather_api_key}}', // Secret replaced in query params
    units: 'metric'
  }
});

console.log('Weather:', weatherData.body.main.temp, '°C');

// Async/background requests (fire-and-forget)
const asyncResponse = await blink.data.fetchAsync({
  url: 'https://api.stripe.com/v1/customers',
  method: 'POST',
  headers: {
    'Authorization': 'Bearer {{stripe_secret_key}}',
    'Content-Type': 'application/x-www-form-urlencoded'
  },
  body: 'email=customer@example.com&name=John Doe'
});

console.log(asyncResponse.status); // 'triggered'
console.log(asyncResponse.message); // 'Request triggered in background'

// Multiple secrets in different places
const complexRequest = await blink.data.fetch({
  url: 'https://api.github.com/repos/{{github_username}}/{{repo_name}}/issues',
  method: 'POST',
  headers: {
    'Authorization': 'token {{github_token}}',
    'Accept': 'application/vnd.github.v3+json',
    'User-Agent': '{{app_name}}'
  },
  body: {
    title: 'Bug Report',
    body: 'Found via {{app_name}} monitoring'
  }
});

// Secret substitution works everywhere:
// - URL path: /api/{{version}}/users
// - Query params: ?key={{api_key}}&user={{user_id}}
// - Headers: Authorization: Bearer {{token}}
// - Body: { "apiKey": "{{secret}}", "data": "{{value}}" }

// Error handling for data extraction
try {
  const result = await blink.data.extractFromUrl('https://example.com/huge-file.pdf');
} catch (error) {
  if (error instanceof BlinkDataError) {
    console.error('Data processing error:', error.message);
  }
}
```

### 存储操作

```typescript
// Upload files (returns public URL directly)
const { publicUrl } = await blink.storage.upload(
  file,
  `uploads/${Date.now()}.${file.name.split('.').pop()}`, // ✅ Extract original extension
  {
    upsert: true,
    onProgress: (percent) => console.log(`${percent}%`)
  }
)

// ❌ WRONG - Hardcoded extensions break HEIC/PNG/WebP files
const wrong = await blink.storage.upload(file, `uploads/${Date.now()}.jpg`) // Corrupts non-JPG files
// ✅ CORRECT - Extract file extension
const correct = await blink.storage.upload(file, `uploads/${Date.now()}.${file.name.split('.').pop()}`)

// Remove files
await blink.storage.remove('file1.jpg', 'file2.jpg')
```

### 通知业务

```typescript
// 🔥 Email Notifications (NEW!) - Send emails with attachments, custom branding, and delivery tracking

// Send a simple email - returns success status and message ID
const result = await blink.notifications.email({
  to: 'customer@example.com',
  subject: 'Your order has shipped!',
  html: '<h1>Order Confirmation</h1><p>Your order #12345 is on its way.</p>'
})

console.log(result.success)    // true/false - whether email was sent
console.log(result.messageId)  // "msg_abc123..." - unique message identifier

// Send with plain text fallback (recommended for better deliverability)
const { success, messageId } = await blink.notifications.email({
  to: 'customer@example.com',
  subject: 'Welcome to our platform!',
  html: '<h1>Welcome!</h1><p>Thanks for joining us.</p>',
  text: 'Welcome!\n\nThanks for joining us.' // Plain text version
})

// Send an email with attachments and custom branding
const result = await blink.notifications.email({
  to: ['team@example.com', 'manager@example.com'],
  from: 'invoices@mycompany.com', // Must be valid email address
  replyTo: 'support@mycompany.com',
  subject: 'New Invoice #12345',
  html: `
    <div style="font-family: Arial, sans-serif;">
      <h2>Invoice Ready</h2>
      <p>Please find the invoice attached.</p>
    </div>
  `,
  text: 'Invoice Ready\n\nPlease find the invoice attached.',
  cc: 'accounting@mycompany.com',
  bcc: 'archive@mycompany.com',
  attachments: [
    { 
      url: 'https://mycompany.com/invoices/12345.pdf',
      filename: 'Invoice-12345.pdf', // Custom filename
      type: 'application/pdf'        // MIME type (optional)
    },
    {
      url: 'https://mycompany.com/terms.pdf',
      filename: 'Terms-of-Service.pdf'
    }
  ]
})

console.log(`Email ${result.success ? 'sent' : 'failed'}`)
console.log(`Message ID: ${result.messageId}`)

// Send to multiple recipients with different recipient types
const { success, messageId } = await blink.notifications.email({
  to: ['customer1@example.com', 'customer2@example.com'],
  cc: ['manager@example.com'],
  bcc: ['audit@example.com', 'backup@example.com'],
  from: 'notifications@mycompany.com',
  subject: 'Monthly Newsletter',
  html: '<h2>This Month\'s Updates</h2><p>Here are the highlights...</p>'
})

// Dynamic email content with user data
const user = await blink.auth.me()
const welcomeEmail = await blink.notifications.email({
  to: user.email,
  from: 'welcome@mycompany.com',
  subject: `Welcome ${user.displayName}!`,
  html: `
    <h1>Hi ${user.displayName}!</h1>
    <p>Welcome to our platform. Your account is now active.</p>
    <p>Account ID: ${user.id}</p>
    <a href="https://myapp.com/dashboard">Get Started</a>
  `,
  text: `Hi ${user.displayName}!\n\nWelcome to our platform. Your account is now active.\nAccount ID: ${user.id}\n\nGet Started: https://myapp.com/dashboard`
})

// Comprehensive error handling with detailed error information
try {
  const result = await blink.notifications.email({
    to: 'customer@example.com',
    subject: 'Important Update',
    html: '<p>This is an important update about your account.</p>'
  })
  
  if (result.success) {
    console.log('✅ Email sent successfully!')
    console.log('📧 Message ID:', result.messageId)
  } else {
    console.error('❌ Email failed to send')
    // Handle failed send (retry logic, fallback notification, etc.)
  }
  
} catch (error) {
  if (error instanceof BlinkNotificationsError) {
    console.error('❌ Email error:', error.message)
    
    // Common error scenarios:
    // - "The 'to', 'subject', and either 'html' or 'text' fields are required."
    // - "Invalid email address format"
    // - "Attachment URL must be accessible"
    // - "Failed to send email: Rate limit exceeded"
    
    // Handle specific error types
    if (error.message.includes('Rate limit')) {
      // Implement retry with backoff
      console.log('⏳ Rate limited, will retry later')
    } else if (error.message.includes('Invalid email')) {
      // Log invalid email for cleanup
      console.log('📧 Invalid email address, removing from list')
    }
  } else {
    console.error('❌ Unexpected error:', error)
  }
}

// Email validation and best practices
const validateAndSendEmail = async (recipient: string, subject: string, content: string) => {
  // Basic validation
  if (!recipient.includes('@') || !subject.trim() || !content.trim()) {
    throw new Error('Invalid email parameters')
  }
  
  try {
    const result = await blink.notifications.email({
      to: recipient,
      from: 'noreply@mycompany.com',
      subject: subject,
      html: `
        <div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">
          <div style="background: #f8f9fa; padding: 20px; text-align: center;">
            <h1 style="color: #333; margin: 0;">My Company</h1>
          </div>
          <div style="padding: 20px;">
            ${content}
          </div>
          <div style="background: #f8f9fa; padding: 15px; text-align: center; font-size: 12px; color: #666;">
            <p>© 2024 My Company. All rights reserved.</p>
            <p><a href="https://mycompany.com/unsubscribe">Unsubscribe</a></p>
          </div>
        </div>
      `,
      text: content.replace(/<[^>]*>/g, '') // Strip HTML for text version
    })
    
    return result
  } catch (error) {
    console.error(`Failed to send email to ${recipient}:`, error)
    throw error
  }
}

// Usage with validation
try {
  const result = await validateAndSendEmail(
    'customer@example.com',
    'Account Verification Required',
    '<p>Please verify your account by clicking the link below.</p><a href="https://myapp.com/verify">Verify Account</a>'
  )
  console.log('Email sent with ID:', result.messageId)
} catch (error) {
  console.error('Email validation or sending failed:', error.message)
}

// Bulk email sending with error handling
const sendBulkEmails = async (recipients: string[], subject: string, htmlContent: string) => {
  const results = []
  
  for (const recipient of recipients) {
    try {
      const result = await blink.notifications.email({
        to: recipient,
        from: 'newsletter@mycompany.com',
        subject,
        html: htmlContent,
        text: htmlContent.replace(/<[^>]*>/g, '')
      })
      
      results.push({ 
        recipient, 
        success: result.success, 
        messageId: result.messageId 
      })
      
      // Rate limiting: wait between sends
      await new Promise(resolve => setTimeout(resolve, 100))
      
    } catch (error) {
      results.push({ 
        recipient, 
        success: false, 
        error: error.message 
      })
    }
  }
  
  return results
}

// Response format details:
// ✅ Success response: { success: true, messageId: "msg_abc123...", from: "noreply@project.blink-email.com", to: ["recipient@example.com"], subject: "Email Subject", timestamp: "2024-01-20T10:30:00.000Z" }
// ❌ The method throws BlinkNotificationsError on failure
// 🔍 Error types: validation errors, rate limits, network issues, invalid attachments

// API Response Format:
// The notifications API returns data directly (not wrapped in {data: ..., error: ...})
// This is consistent with other Blink APIs like database and storage
// All Blink APIs follow this pattern for clean, predictable responses

// Best practices:
// 1. Always include both HTML and text versions for better deliverability
// 2. Use valid email addresses for 'from' field (not display names)
// 3. Keep HTML simple with inline CSS for email client compatibility
// 4. Handle rate limits with retry logic
// 5. Validate email addresses before sending
// 6. Use message IDs for tracking and debugging
// 7. Include unsubscribe links for compliance
```

### 分析操作

```typescript
// 🔥 Analytics (NEW!) - Automatic pageview tracking + custom events
// Pageviews are tracked automatically on initialization and route changes

// Log custom events - context data is added automatically
blink.analytics.log('button_clicked', {
  button_id: 'signup',
  campaign: 'summer_sale'
})

// All events automatically include:
// - timestamp, project_id, session_id (no user IDs or emails)
// - pathname (current page), referrer, screen_width
// - device/browser/OS info (parsed server-side)
// - channel detection (Organic Search, Social, Direct, etc.)
// - UTM parameters (source, medium, campaign, content, term)
// - UTM persistence for attribution tracking across sessions

// Control analytics
// createClient({ projectId, analytics: false }) → SDK analytics off from the start: nothing sent or stored
// (sites hosted on Blink also get a separate page-view tag, not controlled here)
blink.analytics.disable() // stops sending, clears queued events and stored IDs, remembers the opt-out
blink.analytics.enable()  // opts back in
const isEnabled = blink.analytics.isEnabled()
// Off automatically when the browser sends Do Not Track or Global Privacy Control

// Clear attribution data (e.g., when user logs out)
blink.analytics.clearAttribution()

// Features: Privacy-first, offline support, event batching, session management
// Attribution: UTM params persist across sessions for conversion tracking

// How UTM persistence works:
// 1. User visits with ?utm_source=google&utm_campaign=summer_sale
// 2. These params are saved to localStorage for attribution
// 3. Future events (even days later) include these UTM params
// 4. Perfect for tracking which campaigns drive conversions
// 5. New UTM params override old ones (last-touch model available)
```

### 实时操作

**零波纹连接管理!**  
所有连接状态,排队,再连接都是自动处理的. 别再犯"状态"错误了!

**QQ 反应用户**: 见 [反应 + 实时连接](https://npmcn.com/package/%40blinkdotnew/sdk/#readme-react--realtime-connections) 下面的章节,用于适当的ASync清理模式,以避免“订阅已取消”错误.

```typescript
// 🔥 Real-time Messaging & Presence (NEW!)
// Perfect for chat apps, live collaboration, multiplayer games, and live updates

// Simple subscribe and publish (most common pattern)
const unsubscribe = await blink.realtime.subscribe('chat-room', (message) => {
  console.log('New message:', message.data)
  console.log('From user:', message.userId)
  console.log('Message type:', message.type)
})
// message callback receives RealtimeMessage format:
// {
//   id: '1640995200000-0',
//   type: 'chat',
//   data: { text: 'Hello!', timestamp: 1640995200000 },
//   timestamp: 1640995200000,
//   userId: 'user123',
//   metadata: { displayName: 'John' }
// }

// Publish a message to all subscribers - returns message ID
const messageId = await blink.realtime.publish('chat-room', 'message', {
  text: 'Hello everyone!',
  timestamp: Date.now()
})
// messageId is string format: '1640995200000-0'

// Advanced channel usage with presence tracking
const channel = blink.realtime.channel('game-lobby')

// Subscribe with user metadata
await channel.subscribe({
  userId: user.id,
  metadata: { 
    displayName: user.name,
    avatar: user.avatar,
    status: 'online'
  }
})

// Listen for messages
const unsubMessage = channel.onMessage((message) => {
  if (message.type === 'chat') {
    addChatMessage(message.data)
  } else if (message.type === 'game-move') {
    updateGameState(message.data)
  }
})
// message parameter format:
// {
//   id: '1640995200000-0',
//   type: 'chat',
//   data: { text: 'Hello!', timestamp: 1640995200000 },
//   timestamp: 1640995200000,
//   userId: 'user123',
//   metadata: { displayName: 'John' }
// }

// Listen for presence changes (who's online)
// Callback receives array of PresenceUser objects
const unsubPresence = channel.onPresence((users) => {
  console.log(`${users.length} users online:`)
  users.forEach(user => {
    console.log(`- ${user.metadata?.displayName} (${user.userId})`)
  })
  updateOnlineUsersList(users)
})
// users parameter format:
// [
//   { 
//     userId: 'user123', 
//     metadata: { displayName: 'John', status: 'online' },
//     joinedAt: 1640995200000,
//     lastSeen: 1640995230000
//   }
// ]

// Publish different types of messages
await channel.publish('chat', { text: 'Hello!' }, { userId: user.id })
await channel.publish('game-move', { x: 5, y: 3, piece: 'king' })
await channel.publish('typing', { isTyping: true })

// Get current presence (one-time check)
// Returns array of PresenceUser objects directly
const currentUsers = await channel.getPresence()
console.log('Currently online:', currentUsers.length)
// currentUsers is PresenceUser[] format:
// [
//   { 
//     userId: 'user123', 
//     metadata: { displayName: 'John', status: 'online' },
//     joinedAt: 1640995200000,
//     lastSeen: 1640995230000
//   }
// ]

// Get message history - returns array of RealtimeMessage objects
const recentMessages = await channel.getMessages({ 
  limit: 50,
  before: lastMessageId // Pagination support
})
// recentMessages is RealtimeMessage[] format:
// [
//   {
//     id: '1640995200000-0',
//     type: 'chat',
//     data: { text: 'Hello!', timestamp: 1640995200000 },
//     timestamp: 1640995200000,
//     userId: 'user123',
//     metadata: { displayName: 'John' }
//   }
// ]

// Cleanup when done
unsubMessage()
unsubPresence()
await channel.unsubscribe()

// Or use the simple unsubscribe from subscribe()
unsubscribe()

// Multiple channels for different features
const chatChannel = blink.realtime.channel('chat')
const notificationChannel = blink.realtime.channel('notifications')
const gameChannel = blink.realtime.channel('game-state')

// Each channel is independent with its own subscribers and presence
await chatChannel.subscribe({ userId: user.id })
await notificationChannel.subscribe({ userId: user.id })
await gameChannel.subscribe({ userId: user.id, metadata: { team: 'red' } })

// Real-time collaboration example
const docChannel = blink.realtime.channel(`document-${docId}`)

await docChannel.subscribe({
  userId: user.id,
  metadata: { 
    name: user.name,
    cursor: { line: 1, column: 0 }
  }
})

// Broadcast cursor movements
docChannel.onMessage((message) => {
  if (message.type === 'cursor-move') {
    updateUserCursor(message.userId, message.data.position)
  } else if (message.type === 'text-change') {
    applyTextChange(message.data.delta)
  }
})

// Send cursor updates
await docChannel.publish('cursor-move', {
  position: { line: 5, column: 10 }
}, { userId: user.id })

// Send text changes
await docChannel.publish('text-change', {
  delta: { insert: 'Hello', retain: 5 },
  timestamp: Date.now()
})

// Presence with live cursor positions
docChannel.onPresence((users) => {
  users.forEach(user => {
    if (user.metadata?.cursor) {
      showUserCursor(user.userId, user.metadata.cursor)
    }
  })
})

// Auto-cleanup on page unload
window.addEventListener('beforeunload', () => {
  docChannel.unsubscribe()
})

// Error handling
try {
  await blink.realtime.publish('restricted-channel', 'message', { data: 'test' })
} catch (error) {
  if (error instanceof BlinkRealtimeError) {
    console.error('Realtime error:', error.message)
  }
}
```

## 高级用途

### 处理错误

```typescript
import { 
  BlinkAuthError, 
  BlinkAuthErrorCode,
  BlinkAIError, 
  BlinkStorageError, 
  BlinkDataError, 
  BlinkRealtimeError, 
  BlinkNotificationsError 
} from '@blinkdotnew/sdk'

// Authentication error handling
try {
  const user = await blink.auth.signInWithEmail(email, password)
} catch (error) {
  if (error instanceof BlinkAuthError) {
    switch (error.code) {
      case BlinkAuthErrorCode.EMAIL_NOT_VERIFIED:
        console.log('Email verification required')
        await blink.auth.sendEmailVerification()
        break
      case BlinkAuthErrorCode.INVALID_CREDENTIALS:
        console.error('Invalid email or password')
        break
      case BlinkAuthErrorCode.RATE_LIMITED:
        console.error('Too many attempts, try again later')
        break
      default:
        console.error('Auth error:', error.message)
    }
  }
}

// Other error types
try {
  const { text } = await blink.ai.generateText({ prompt: 'Hello' })
} catch (error) {
  if (error instanceof BlinkAIError) {
    console.error('AI error:', error.message)
  }
}
```

### 自定义配置

```typescript
const blink = createClient({
  projectId: 'your-project',
  baseUrl: 'https://custom-api.example.com',
  auth: {
    mode: 'headless', // 'managed' | 'headless'
    authUrl: 'https://your-auth-service.com', // Custom auth domain (for all auth endpoints)
    coreUrl: 'https://custom-core.example.com', // Custom API domain (for db, ai, storage)
    // Providers controlled via project settings
    redirectUrl: 'https://myapp.com/dashboard',
    roles: {
      admin: { permissions: ['*'] },
      editor: { permissions: ['posts.create', 'posts.update'], inherit: ['viewer'] },
      viewer: { permissions: ['posts.read'] }
    }
  },
  httpClient: {
    timeout: 30000,
    retries: 3
  }
})
```

### 类型脚本支持

SDK是用TypeScript写成的,并提供完全类型的安全性:

```typescript
interface Todo {
  id: string
  title: string
  isCompleted: boolean  // Will be returned as "0" or "1" string from SQLite
  userId: string        // Automatically converted from snake_case user_id
  createdAt: string     // Automatically converted from snake_case created_at
}

// Note: Boolean fields are returned as "0"/"1" strings from SQLite
// Use Number(value) > 0 to check boolean values
const todos = await blink.db.todos.list<Todo>()

// Check boolean values properly
const completedTodos = todos.filter(todo => Number(todo.isCompleted) > 0)
const incompleteTodos = todos.filter(todo => Number(todo.isCompleted) === 0)

// When filtering by boolean values in queries, use "0"/"1" strings
const onlyCompleted = await blink.db.todos.list<Todo>({
  where: { isCompleted: "1" }  // Use string "1" for true, "0" for false
})
// todos is fully typed as Todo[]
```

### API 代理服务器的秘密管理

这个 `blink.data.fetch()` 方法允许您用自动秘密替代方式安全地拨打API。 下面是如何设置:

**步骤一: 将您的秘密存储在您的 Blink 项目中** 访问您的项目仪表板 [眨一眼,换一眼](https://blink.new/) 并添加您的 API 密钥到“ 秘密” 部分 :

-   `sendgrid_api_key` → `SG.abc123...`
-   `openweather_api_key` → `d4f5g6h7...`
-   `stripe_secret_key` → `sk_live_abc123...`

**第2步:在您的 API 呼叫中使用秘密**

```typescript
// Secrets are automatically substituted server-side - never exposed to frontend
const result = await blink.data.fetch({
  url: 'https://api.example.com/endpoint',
  headers: {
    'Authorization': 'Bearer {{your_secret_key}}' // Replaced with actual value
  }
})
```

**第3步:秘密替代在任何地方都起作用**

```typescript
await blink.data.fetch({
  url: 'https://api.{{service_domain}}/v{{api_version}}/users/{{user_id}}',
  query: { 
    key: '{{api_key}}',
    format: 'json' 
  },
  headers: { 
    'X-API-Key': '{{secondary_key}}' 
  },
  body: {
    token: '{{auth_token}}',
    data: 'regular string data'
  }
})
```

全体 `{{secret_name}}` 占位符被您项目秘密存储的加密值所取代。 秘密从不离开服务器 也永远看不到前端代码.

## 框架实例

### 反应 + 实时连接

**QQ 关键: 避免多个 WebSocket 连接**

最常见的错误是在使用效果中使用失去清理功能的同源函数:

```typescript
import type { RealtimeChannel } from '@blinkdotnew/sdk'

// ❌ WRONG - Async function loses cleanup (causes "Subscription cancelled" errors)
useEffect(() => {
  const initApp = async () => {
    const channel = blink.realtime.channel('room')
    await cha
```

## 常见问题

### @blinkdotnew/sdk 是什么？

用于客户端应用程序的 Blink TypeScript SDK - 0-boilerplate CRUD \+ auth \+ AI \+ 分析 \+ 现代 SaaS/AI 应用程序的通知

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

在项目目录运行 npm install @blinkdotnew/sdk，或使用 pnpm add / yarn add / bun add。

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

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

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

同时提供 ES Module 与 CommonJS 入口，import 和 require\(\) 都可以使用，打包工具会自动选择合适的版本。

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

@blinkdotnew/sdk 使用 MIT 许可。宽松许可：允许商业使用、修改和再分发，通常只需在分发时保留原版权与许可声明。以上为概括，不构成法律意见，请以许可原文为准。

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

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

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

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

