npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zhitu/sdk

v0.5.0

Published

zhituSDK —— 平台沙箱工具官方前端 SDK:iframe 内经 postMessage 存取私有 KV、设备本地文件、受控导出,并可把条目提交到用户复习计划(零依赖、零凭据)

Readme

@zhitu/sdk

当前版本:0.5.0。

智图平台第三方工具的官方浏览器 SDK。工具运行在平台沙箱 iframe 中,通过宿主 Bridge 安全地访问:

  • 服务端私有 KV 数据;
  • 当前设备上的 Blob 文件;
  • 经顶层宿主确认的文件分享或下载;
  • 当前工具名下的复习计划;
  • 平台统一 AI 能力(可选)。

SDK 不持有用户 token,不直连平台 API,运行时零依赖,并自带 TypeScript 类型。

安装

pnpm add @zhitu/sdk
# 或 npm install @zhitu/sdk

包同时提供 ESM、CommonJS 和类型声明。

运行前提

@zhitu/sdk 供智图工具沙箱内的浏览器代码使用,不是服务端 SDK。调用能力前,工具的 manifest.json 必须声明相应 capability:

{
  "name": "示例工具",
  "version": "1.0.0",
  "entry": "index.html",
  "capabilities": [
    "ui",
    "tool-data-read",
    "tool-data-write",
    "tool-file-read",
    "tool-file-write",
    "tool-file-export"
  ],
  "outboundDomains": [],
  "dataSchema": {
    "collections": {
      "notes": {
        "fields": {
          "title": "string",
          "text": "string",
          "starred": "boolean"
        }
      }
    }
  }
}

只有服务端 KV 写入需要在 dataSchema 中声明 collection 和字段;设备本地文件由 tool-file-read/write 控制,导出由独立的 tool-file-export 控制。

快速开始

import { createToolClient } from '@zhitu/sdk'

const client = createToolClient()

await client.data.put('notes', 'note-1', {
  title: '一次函数',
  text: '斜率求错',
  starred: false,
})

const note = await client.data.get('notes', 'note-1')
console.log(note)

client.dispose()

data.get() 返回 unknown,工具应按自己的 manifest schema 做类型收窄。

服务端数据

await client.data.put('notes', 'a', { title: 'A', starred: true })
await client.data.put('notes', 'b', { title: 'B', starred: false })

const keys = await client.data.listKeys('notes')
const notes = await Promise.all(keys.map((key) => client.data.get('notes', key)))

await client.data.putMany([
  { collection: 'notes', key: 'c', value: { title: 'C', starred: false } },
  { collection: 'notes', key: 'd', value: { title: 'D', starred: true } },
])

await client.data.delete('notes', 'a')

数据值必须是 manifest 声明的扁平 string | number | boolean 对象。数组、Date、Map、函数和二进制不允许;二进制使用 client.files.*。

putMany() 最多写入 100 条,并保持原子性:批内重复或任一条校验失败时,整批都不会写入。

设备本地文件

文件保存在宿主站点的 IndexedDB 中,不上传服务端,也不会跨设备同步。

async function saveImage(file: File) {
  return client.files.put('images', 'question-1', file, {
    name: file.name,
  })
}

const stored = await client.files.get('images', 'question-1')
const bytes = await stored.blob.arrayBuffer()
console.log(stored.ref, bytes.byteLength)

const image = document.querySelector<HTMLImageElement>('#preview')
if (image) {
  const url = URL.createObjectURL(stored.blob)
  image.src = url
  try {
    await image.decode()
  } finally {
    URL.revokeObjectURL(url)
  }
}

await client.files.delete('images', 'question-1')

临时缓存可以显式设置 ISO 格式的过期时间:

async function cachePreview(file: File) {
  return client.files.put('cache', 'preview', file, {
    expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
  })
}

不传 expiresAt 时文件永久保留。到期后 files.get() 按不存在处理,宿主会在后续文件访问时机会式清理;页面关闭期间没有常驻定时器。

本地文件限额

| 范围 | 限额 | |---|---:| | 单文件 | 2 MiB | | 每账号 × 每工具 | 40 MiB / 20 个 | | 每账号全部工具 | 200 MiB / 5000 个 |

账号本地擦除流程会同步清除该账号的工具文件。浏览器清理站点数据时,本地文件也会被删除。

受控文件导出

files.export() 将 Blob 发给顶层宿主,但不会让 sandbox iframe 直接下载。宿主会显示文件名、MIME 和大小,用户点击确认后才执行:移动端支持 Web Share 时优先分享,其他情况由宿主下载。

const png = await new Promise<Blob>((resolve, reject) => {
  canvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error('PNG 生成失败')), 'image/png')
})

const { method } = await client.files.export(png, { name: '一次函数.png' })
console.log(method) // "share" | "download"

导出文件不会自动写入 IndexedDB。当前限制为单文件 10 MiB,文件名 1~128 字符,不得含路径或系统保留字符,扩展名必须与 MIME 匹配。允许的组合为:

| MIME | 扩展名 | |---|---| | image/png | .png | | image/jpeg | .jpg / .jpeg | | image/webp | .webp | | image/svg+xml | .svg | | application/pdf | .pdf | | application/json | .json | | text/csv | .csv | | text/plain | .txt |

宿主同一时间只处理一个导出确认,防止工具静默或批量触发。稳定错误码:

| code | 含义 | |---|---| | CAPABILITY_NOT_DECLARED | manifest 未声明 tool-file-export | | FILE_EXPORT_INVALID_ARGUMENT | Blob、文件名或 MIME 不合法 | | FILE_EXPORT_TOO_LARGE | Blob 超过 10 MiB | | FILE_EXPORT_BUSY | 已有导出正在等待用户处理 | | USER_CANCELLED | 用户在宿主确认层取消 | | FILE_EXPORT_FAILED | 分享和下载均无法完成 |

能力探测

ready() 是可选探测,不会阻塞其他 API:

const bridge = await client.ready()

if (bridge.capabilities.includes('tool-file-write')) {
  console.log('单文件上限:', bridge.limits.maxFileBytes)
}

**0.4.0 BREAKING:**宿主成功响应 ready() 时,必须返回非数组对象 launch: { reviewKey: string | null },其中字符串必须是合法 review key;同时 limits.maxReviewItemsPerTool、limits.maxReviewKeyLength、limits.maxReviewTitleLength 均为必填的非负有限数值。任一字段缺失或非法都会抛出 BRIDGE_INVALID_RESPONSE。 既有 file limits 仍为可选字段。

宿主不响应导致短超时,或明确返回 UNSUPPORTED_MESSAGE 时,ready() 仍返回协议 v1 fallback: capabilities 为空、launch.reviewKey 为 null,三个 review limits 分别为 200、128、100。 此降级不会阻塞其他 API。

帮我记(review-add)

工具需在 manifest.json 中声明 review-add capability:

{
  "name": "示例工具",
  "version": "1.0.0",
  "entry": "index.html",
  "capabilities": ["ui", "review-add"],
  "outboundDomains": []
}

调用时只提交工具内稳定 key 和展示标题;宿主会向用户确认:

import { createToolClient } from '@zhitu/sdk'

const client = createToolClient()

await client.ready()
const key = client.launch.reviewKey ?? 'algebra.linear-1'
const { added } = await client.review.add({
  key,
  title: ' 一次函数:斜率 ',
})

console.log(added) // 1:新加入;0:已存在
client.dispose()

key 仅允许 A-Z、a-z、0-9、.、_、-,长度为 1~128;title 会先 trim(),之后长度必须为 1~100。输入不合法时 SDK 会在本地拒绝,不发送 Bridge 消息。

帮我记限额

| ready().limits 字段 | 当前限额 | 说明 | |---|---:|---| | maxReviewItemsPerTool | 200 | 每账号、每工具最多保存的复习项 | | maxReviewKeyLength | 128 | key 最大长度 | | maxReviewTitleLength | 100 | title trim() 后最大长度 |

这三个字段在 0.4.0 的成功 ready() 响应和 fallback 中均为必填字段。

帮我记错误码

| code | 含义 | |---|---| | REVIEW_ITEM_LIMIT_EXCEEDED | 该工具的复习项已达上限 | | CAPABILITY_NOT_DECLARED | manifest 未声明 review-add | | REVIEW_REQUEST_BUSY | 已有加入复习请求正在等待处理 | | USER_CANCELLED | 用户取消加入 | | BRIDGE_REQUEST_FAILED | 宿主返回通用失败且未提供可公开错误码 |

client.launch.reviewKey 遵循 read-after-ready:ready() 完成前始终为 null;成功响应中的 launch.reviewKey 必须是合法 key 或 null,fallback 时为 null。

reviewKey 只是定位提示,绝不是授权凭据。 是否允许加入复习由宿主依据当前工具会话、manifest capability 和用户确认独立决定。

查询复习计划(review-read)

review-read 允许工具读取当前用户、当前工具名下的复习数据;它与 review-add 是分离的权限和职责, 互不蕴含。只需要读取时,manifest 仅声明 review-read:

{
  "name": "示例工具",
  "version": "1.0.0",
  "entry": "index.html",
  "capabilities": ["ui", "review-read"],
  "outboundDomains": []
}
import { createToolClient } from '@zhitu/sdk'

const client = createToolClient()
const { items, total } = await client.review.list()

for (const item of items) {
  console.log(item.key, item.title, item.dueAt)
}
console.log('总数:', total)
client.dispose()

review.list() 返回全部条目,包括尚未到期的未来排期;SDK 不按 dueAt 做本地时间过滤。 返回值严格为 { items: { key, title, dueAt }[], total },最多 200 项,且 total === items.length。

可能抛出的错误码:

| code | 含义 | |---|---| | CAPABILITY_NOT_DECLARED | manifest 未声明 review-read | | TOOL_NOT_AVAILABLE | 当前工具不可用 | | UNSUPPORTED_MESSAGE | 宿主不支持 tool-review-list | | BRIDGE_INVALID_RESPONSE | 宿主返回的 list 数据不符合协议 | | BRIDGE_REQUEST_FAILED | 宿主返回通用失败且未提供可公开错误码 | | TIMEOUT | 请求超时 | | BRIDGE_SEND_FAILED | Bridge 消息发送失败 | | CLIENT_DISPOSED | client 已释放 |

错误处理

import { ToolBridgeError } from '@zhitu/sdk'

try {
  await client.files.get('images', 'missing')
} catch (error) {
  if (error instanceof ToolBridgeError) {
    switch (error.code) {
      case 'LOCAL_FILE_NOT_FOUND':
        // 展示空态
        break
      case 'LOCAL_FILE_QUOTA_EXCEEDED':
        // 提示用户删除不再需要的文件
        break
      case 'TIMEOUT':
        // 可重试或提示宿主未就绪
        break
      default:
        console.error(error.code, error.message)
    }
  }
}

SDK 自身使用的稳定错误码包括:

| code | 含义 | |---|---| | TIMEOUT | 请求超过配置的等待时间 | | CLIENT_DISPOSED | 客户端已释放 | | BRIDGE_SEND_FAILED | postMessage 发送失败 | | BRIDGE_INVALID_RESPONSE | 宿主响应格式不符合协议 | | BRIDGE_REQUEST_FAILED | 宿主返回失败但未提供错误码 | | INVALID_INPUT | SDK 本地输入校验失败 |

宿主还会返回经过白名单处理的权限、配额、数据校验和 AI 错误码。

生命周期

页面或组件卸载时必须调用 dispose()。它会移除消息监听,并以 CLIENT_DISPOSED 拒绝未完成及后续请求。

import { createToolClient } from '@zhitu/sdk'
import { useEffect, useMemo } from 'react'

function ToolApp() {
  const client = useMemo(() => createToolClient(), [])

  useEffect(() => () => client.dispose(), [client])

  return null
}

API

| API | 说明 | |---|---| | createToolClient(config?) | 创建客户端 | | ready() | 探测协议、能力与限制;timeout/UNSUPPORTED_MESSAGE 时返回 fallback | | data.get(collection, key) | 读取一条服务端私有数据 | | data.put(collection, key, value) | 写入或覆盖一条数据 | | data.delete(collection, key) | 幂等删除一条数据 | | data.listKeys(collection) | 列出 collection 的全部 key,最近更新在前 | | data.putMany(entries) | 最多 100 条原子写入 | | files.put(collection, key, blob, options?) | 保存 Blob,支持 name、expiresAt | | files.get(collection, key) | 返回 { ref, blob } | | files.delete(collection, key) | 幂等删除本地文件 | | files.export(blob, { name }) | 经宿主确认后分享或下载,返回 { method } | | review.add({ key, title }) | 经用户确认加入复习,返回 { added: 0 \| 1 } | | review.list() | 读取当前工具的全部复习条目(含未来排期) | | launch.reviewKey | ready() 后可读的复习定位 key;无合法提示时为 null | | invokeAi({ input, instruction? }) | 调用平台统一 AI 入口;需 ai-invoke | | dispose() | 释放客户端 |

配置

| 字段 | 默认值 | 说明 | |---|---:|---| | timeoutMs | 10000 | 普通 Bridge 请求超时 | | aiTimeoutMs | 190000 | AI 请求超时 | | hostOrigin | "*" | postMessage 的 targetOrigin | | win | 当前 window | 测试或特殊运行环境注入 |

沙箱 iframe 没有 allow-same-origin,因此是浏览器的不透明 origin。宿主通过 event.source === iframe.contentWindow 钉定工具窗口,SDK 只接受 event.source === window.parent 的回复;安全性不依赖可伪造的消息字段。

兼容性

旧版平铺方法仍然可用:

  • getData() → data.get()
  • putData() → data.put()
  • deleteData() → data.delete()
  • listKeys() → data.listKeys()
  • putMany() → data.putMany()

新代码建议使用命名空间 API。SDK 模块顶层不访问 window,可以被 SSR 构建安全导入,但客户端只能在智图宿主 iframe 中真正发起请求。

维护者发布

只能使用 pnpm publish。开发态 exports 指向 TypeScript 源码,pnpm 发布时会应用 publishConfig,将入口切换到 dist/;prepublishOnly 会阻止裸 npm publish 并自动执行构建和测试。

License

MIT