web-dc-api是一个革命性的去中心化 Web 开发 SDK,让您无需任何服务器即可构建完整的互联网应用。
web-dc-api 是面向浏览器的去中心化应用 SDK。它把钱包身份、用户私有数据库、共享 KeyValue、文件、评论、消息、AI 代理、实时通信、白板和支付能力统一在一个 DC 实例中。
本文档对应仓库当前 SDK 0.2.72,示例以当前 TypeScript 类型和实现为准。
| 需求 | 模块/导出 | 适合存储或处理 | 关键入口 | 额外前置条件 |
|---|---|---|---|---|
| 钱包登录与身份 | dc.auth |
登录、签名、解密、用户空间 | accountLoginWithWallet |
dc.init() |
| 用户私有结构化数据 | dc.db |
设置、笔记、草稿、个人记录 | dc.initUserDB、create、find、save |
登录后初始化数据库 |
| 多用户共享数据 | dc.keyValue |
配置、商品、排行榜、全局状态 | createStore、createSharedStore、set、getWithIndex |
登录;创建者公钥和权限配置 |
| 类型化 KV 仓储 | EntityRepository |
基于 KeyValue 的实体、索引查询 | save、findById、findByIndex |
已获得 KeyValueDB |
| 文件与目录 | dc.file |
加密上传、CID 下载、目录遍历、流式读取 | addFile、addFolder、getFileFromDir、getSeekableFileStream |
登录;保管好加密密钥 |
| 时间线与留言 | dc.comment |
动态、评论、回复、点赞/踩/转发 | addThemeObj、publishCommentToTheme |
登录;明确主题作者 |
| 离线消息箱 | dc.message |
通知、私信、稍后拉取的消息 | sendMsgToUserBox、getMsgFromUserBox |
登录 |
| 在线实时消息 | dc.rtm |
在线单聊、在线状态、信令接收 | login、sendMessageToPeer |
RTM Token,或可自动取 Token 的 AI 代理配置 |
| AI 与 MCP | dc.aiproxy |
流式模型调用、MCP、异步资源、实时语音 | DoAIProxyCall、GenerateAndPollAIResource |
服务配置和用户授权 |
| 实时音视频 | dc.rtc |
通话、会议、屏幕共享、实时房间消息 | init、callPeer、joinRoom |
RTC Token 服务;呼叫信令另需 RTM 登录 |
| 协作白板 | dc.whiteboard |
绘制、文档、翻页、视角同步 | init、joinRoom、getWhiteboard |
白板 Token 服务;邀请功能另需 RTM 登录 |
| 支付 | dc.pay |
套餐、订单、扫码支付、托管收银台 | config、listRenewPackages、createPayOrder |
支付服务地址 |
| 短期分布式缓存 | dc.cache |
有过期时间的临时值 | setCacheKey、getCacheValue |
登录 |
| 网络与密钥工具 | dc.client / dc.util |
节点信息、对称密钥、应用信息 | getHostID、createSymmetricKey |
dc.init() |
| 能力 | 数据边界 | 查询方式 | 典型场景 |
|---|---|---|---|
ThreadDB (dc.db) |
当前用户专属,跨设备同步 | JSON 条件、排序、游标 | 用户设置、笔记、私有业务数据 |
普通 KeyValue (createStore) |
同一个 key 可按写入用户分别保存 | key、写入者、索引、时间 | 用户提交、排行榜、多人共享记录 |
共享 KeyValue (createSharedStore) |
同一个 key 全局只保留最新值 | key、索引、时间 | 全局配置、公告、状态标志 |
评论系统适合按时间线组织互动内容;消息箱适合发给指定用户;它们不应替代通用数据库。
ReadableStream;wrapWorker / exposeDC 等 Worker 封装需要 Web Worker。只使用核心模块时不必把 Web Worker 视为强制前提。localhost。window 等浏览器全局对象的 RTC/白板能力。SSR 框架中不要在服务端模块顶层导入 web-dc-api;应从客户端组件加载,或使用关闭 SSR 的动态导入,然后只在客户端创建 DC。appId 是数据命名空间的一部分。上线后不要随意修改,否则应用会访问到不同的数据空间。npm install web-dc-apipnpm add web-dc-apiyarn add web-dc-api包同时提供 ESM、CommonJS 和 TypeScript 类型。应用代码应从包根入口导入:
import { DC, LogLevel, ThemePermission } from "web-dc-api";不要从 web-dc-api/lib/... 或未声明的 web-dc-api/dist/... 路径深层导入;包的公开 exports 只保证根入口、web-dc-api/sw.js 和 web-dc-api/package.json。
CDN 版本还需要先加载 grpc-libp2p-client。建议在生产环境固定版本:
下面的示例完成 SDK 初始化、钱包登录、用户数据库初始化、写入和查询。链节点地址及应用信息应由部署配置提供。
…登录成功后,account 的常用字段是:
| 字段 | 类型 | 用途 |
|---|---|---|
nftAccount |
string |
面向用户显示的账号 |
appAccount |
Uint8Array |
当前应用专用账号原始字节 |
account |
string |
应用账号字符串,可用于存储或业务标识 |
ethAccount |
string |
EVM 兼容账号 |
dc.publicKey?.string() |
string |
当前 DC 身份公钥;钱包登录成功后才是当前用户的应用公钥 |
dc.init() 的第 4 步会先创建临时身份,因此不能只用 dc.publicKey 是否存在来判断钱包登录状态。业务模块调用前应以 accountLoginWithWallet() 成功返回 Account 为准;需要在其他代码路径校验时,同时检查 dc.userInfo 和 dc.publicKey。正常退出请使用 await dc.exit(),不要只调用 auth.exitLogin() 后继续复用旧身份字段。
new DC({
wssUrl,
backWssUrl,
appInfo,
swUrl,
logLevel,
});| 字段 | 必填 | 说明 |
|---|---|---|
wssUrl |
是 | 主链 WebSocket 地址 |
backWssUrl |
是 | 主链连接失败时使用的备用地址;可以和主地址相同 |
appInfo.appId |
业务上必填 | 稳定且唯一的应用 ID,也是数据命名空间的一部分 |
appInfo.appName |
业务上必填 | 钱包及界面显示的应用名称 |
appInfo.appVersion |
否 | 应用展示版本;它不是 ThreadDB 的 schema 版本 |
appInfo.appIcon |
否 | 应用图标 URL |
appInfo.appUrl |
否 | 应用 URL |
appInfo.themeColor |
否 | 钱包页面使用的主题色 |
appInfo.rtcAppId |
否 | RTC/RTM 服务需要的应用 ID |
swUrl |
否 | 文件流 Service Worker 脚本地址;未设置时文件模块会尝试使用同源 /sw.js |
logLevel |
否 | SDK 日志级别,例如 LogLevel.WARN |
dc.init() 可重复调用;实例已经初始化时会直接返回 true。应用中应复用一个实例,避免建立重复的 P2P 和数据库连接。
文件模块初始化时会自动尝试注册 Service Worker。浏览器不支持、页面不是 HTTPS/localhost 或脚本不存在时,注册失败只会记录日志,不会导致整个 dc.init() 失败;不需要媒体 Range 请求或 seek 时可忽略该能力。SDK 构建产物包含 dist/sw.js,但浏览器要求 Service Worker 与应用同源,因此接入方仍需把它部署到应用根目录。完整步骤见 Service Worker 媒体访问。
数据、文件、认证、评论等模块通常返回元组:
const [value, error] = await operation();
if (error) throw error;部分更新/删除方法直接返回 Error | null:
const error = await dc.db?.save(threadId, "notes", JSON.stringify(note));
if (error) throw error;RTM、RTC、白板和支付的大多数方法失败时会抛异常,应使用 try/catch:
try {
await dc.rtm?.sendMessageToPeer(receiverPubkey, "hello");
} catch (error) {
console.error(error);
}不要只判断返回值是否为空而忽略 error;空值有时表示“记录不存在”,有时表示操作失败。
创建单例 DC
-> dc.init()
-> 用户手势触发 accountLoginWithWallet()
-> 按需初始化 ThreadDB / RTM / RTC / 白板
-> 使用业务模块
-> dc.exit() 清理当前身份字段和钱包登录态
-> dc.shutdown() 关闭模块、数据库、gRPC 和 P2P 连接页面只是切换路由时通常不需要 shutdown()。只有整个应用退出或确定不再使用 SDK 时才关闭实例。
推荐把实例和初始化 Promise 一起做成单例,避免 React Strict Mode 或并发渲染创建多个连接。钱包登录仍应放在按钮点击事件中。
…import { getDC } from "./lib/dc-client";
export function LoginButton() {
const login = async () => {
const dc = await getDC();
const auth = dc.auth;
if (!auth) throw new Error("Auth 模块不可用");
const [account, error] = await auth.accountLoginWithWallet();
if (error || !account) throw error ?? new Error("登录失败");
console.log("logged in:", account.nftAccount);
};
return 使用 DC 钱包登录;
}在 Next.js、Nuxt 等 SSR 项目中,将上述文件只从客户端组件加载,或使用关闭 SSR 的动态导入。当前包入口不是 Node.js/SSR 安全入口:即使服务端不调用 new DC(),顶层导入也可能访问 window,因此不能先在服务端静态导入再延迟初始化。
应用开发者最常用的是以下方法:
const auth = dc.auth;
if (!auth) throw new Error("Auth 模块不可用");
const [account, loginError] = await auth.accountLoginWithWallet();
const [currentAccount, infoError] = await auth.getLoginInfo();
const payload = new TextEncoder().encode("content to sign");
const [signature, signError] = await auth.sign(payload);
// 推荐退出方式:同时退出钱包,并清理公钥、dbThreadId、EVM 地址和账号备份连接。
await dc.exit();auth.exitLogin() 只调用钱包侧退出,不会清理 DC 上的身份字段。dc.exit() 的清理范围更完整,但当前实现不会把 dc.userInfo / dc.accountInfo 置空,也不会关闭模块和网络连接;因此登录判断应同时检查 dc.userInfo 和 dc.publicKey,彻底停止 SDK 则另行调用 dc.shutdown()。
其他认证能力包括钱包消息签名、EIP-712 签名、NFT 账号绑定、应用账号生成、用户空间检查和存储套餐续订。普通 DApp 不应自行实现账号密码托管流程,优先使用钱包登录。
ThreadDB 用于当前用户的私有结构化数据。dc.initUserDB() 会创建或恢复用户数据库,并设置 dc.dbThreadId;同步和维护任务会在后台继续运行。
name 和 JSON Schema。_id,它是记录主键。创建数据时可省略,由 SDK 在校验前生成;保存时必须带回已有 _id。_mod 字段,但不要把它放进 required。SDK 会移除调用方传入的 _mod,再写入自己的修改时间标记和索引,业务代码不要覆盖它。schemaVersion 是正整数,不是 "1.0.0"。集合字段或索引变化时递增。reset=true 会重建数据库并导致原数据丢失,只能用于明确的开发调试场景。…常用补充接口:has、deleteMany、modifiedSince、getDBRecordsCount、refreshDBFromDC、syncDBToDC、exportDBToFile。
KeyValue 主题由 (appId, theme, themeAuthor) 唯一定位。themeAuthor 是创建该主题的用户公钥,必须作为应用配置长期保存;它不一定等于当前登录用户。
keyvalue_ 前缀。KeyValueStoreType.Public 主题名必须以 _pub 结尾;具备可用 SDK 身份的用户无需额外 READ 授权即可读取,写入仍需授权。KeyValueStoreType.Auth 的读取和写入都需要相应授权。createStore 为每个写入用户保留同 key 的独立值。createSharedStore 自动规范化为 keyvalue_shared_ 前缀,同 key 全局只保留时间戳最新值。权限使用 ThemePermission 枚举;主题创建者始终具有 ADMIN 权限。
…获取共享主题时,必须把 toSharedTheme(...) 的结果传给 getStore。如果用普通主题名获取,再用 createSharedStore 创建,两次实际访问的主题名不同。
ThemePermission 是互斥状态枚举,不是位掩码。不要用 READ | WRITE 组合,也不要用数值大小比较权限:
| 权限 | KeyValue 中的实际能力 | 建议用途 |
|---|---|---|
NONE |
无读写权限 | 撤销授权或用户主动退出 |
APPLY |
只有申请状态,不授予数据访问 | 用户申请加入,等待管理员审批 |
READ |
可读,不可写 | 观察者、审核员 |
WRITE |
可读写 | 编辑者、普通成员 |
ADMIN |
可读写,并可管理其他用户权限 | 主题管理员;按最小人数授予 |
ONLY_WRITE |
可写,但不获得主题读取权限,也不能修改授权信息 | 匿名投稿、设备上报、只收集不公开的数据 |
DEVICE |
物联网管理扩展权限 | 仅用于配套的 IoT 流程 |
QUERY |
查询服务专用权限 | AI/服务查询流程,不可替代 READ |
NOT_EXIST |
服务端表示“没有授权记录”的哨兵值 | 不要主动配置给用户 |
存储类型决定读取边界,用户权限决定写入能力:
| 存储类型 | 读取 | 写入 |
|---|---|---|
KeyValueStoreType.Auth |
READ、WRITE、ADMIN |
WRITE、ADMIN、ONLY_WRITE |
KeyValueStoreType.Public |
已登录用户无需额外 READ 授权 |
WRITE、ADMIN、ONLY_WRITE |
主题作者或已有 ADMIN 权限的用户可以配置其他用户。状态码 0 才表示成功:
…authPubkey 传 "all" 可设置未单独授权用户的默认权限;某个用户的单独记录优先于 "all"。这个能力应谨慎使用:
"all" + WRITE,否则任何登录用户都能覆盖同一个 key。KeyValueStoreType.Auth 的普通存储,并授予 "all" + ONLY_WRITE;用户可提交数据,但不能枚举或读取其他用户数据。remark 适合角色、分组等业务元数据,不要存放密钥或其他敏感信息。store.getDbAuthList(limit, seekKey),或 keyValue.getDbAuthList(store, limit, seekKey) 分页读取;把返回的 nextSeekKey 传入下一页,直到它为空。KeyValue 的原始读取结果包含业务值和 DC 元数据:
$$$dckv_extra$$$直接 JSON.parse(raw) 会失败,应先拆分:
…常用补充接口:getValues、getBatch、getWithTimeOrder、setWithCount、getDBCount、getDbAuthList、GetUserOwnAuth、GetUserAuth。删除一个 key 时使用 set(store, key, "", "")。
SDK 导出 BaseEntity、EntityRepository、Entity、Column、Index 和 composeCompositeIndexValue。它们建立在 KeyValue 之上,提供实体校验、保存、局部更新、主键查询、单字段/复合索引查询及分页读取。
import { BaseEntity, EntityRepository } from "web-dc-api";
class Product extends BaseEntity {
id = "";
name = "";
category = "";
price = 0;
validate() {
if (!this.id || !this.name) throw new Error("invalid product");
}
}
const products = new EntityRepository(Product, keyValue, store);
await products.save(
Product.from({ id: "p-1", name: "Keyboard", category: "hardware", price: 299 }),
);
const product = await products.findById("p-1");仓储使用 KeyValue 的索引规则;需要 findByIndex 时,应通过装饰器或底层 set 预先建立对应索引。
文件模块支持普通文件、目录、完整下载、流式读取、可 seek 流和本地缓存。
…可 seek 流会按“CID/目录路径 + 解密密钥”在内存中缓存约 100 秒;重复获取时可能返回同一个可变流实例。直接调用 seek() / read() 的多个消费者不要并发共享同一实例,需要重新开始时先 seek(0),需要强制新建时可调用 fileModule.clearFileCache(cid)。createReadableStream() 默认从当前 getPosition() 开始,但之后使用独立的流位置,不会推进实例位置;必须从头读取时传 { start: 0 }。
浏览器选择文件夹时必须同时设置 webkitdirectory 和 multiple;SDK 使用每个文件的 webkitRelativePath 还原多级目录:
…进度回调中的 totalBlocks / processedBlocks 是 DC 接收的数据块数量,不等同于用户选择的源文件数量。上传时 SDK 会在根目录加入内部所有者标记 dc_ownuser;目录列表会自动隐藏它,业务代码不要自行创建同名文件。
…目录项字段含义:
| 字段 | 含义 |
|---|---|
Name |
当前文件或目录名 |
Type |
0 为文件,1 为目录 |
Hash |
当前项的 CID |
Path |
相对根目录的路径,可直接传给 getFileFromDir / getSeekableFileStreamFromDir |
Size |
文件在 UnixFS 中的存储字节数;目录为 0,加密文件包含分块加密开销 |
Content |
仅 getFolderFileListWithContent 的文件项包含完整字节内容 |
getFileFromDir 的路径指向文件时返回 Uint8Array,指向目录时返回该目录的一级列表,因此必须用 instanceof Uint8Array 收窄类型。getFolderFileListWithContent(cid, key, recursive, peerAddr?) 会逐个下载并把所有文件内容放进内存,只适合小目录;大目录应先获取列表,再按路径读取单个文件或使用可 seek 流。已通过其他目录操作连接到同一根 CID 时才使用 cidNeedConnect.NOT_NEED,普通业务的首次读取应使用 NEED。
没有原生文件夹选择框时,可把路径和内容转换成 FileList,再复用 addFolder:
…addFolderInLocal 只把目录构建到当前浏览器节点的本地块存储,不会向 DC 注册或持久化上传,适合本地预处理和调试,不能代替正式的 addFolder。拿到未知 CID 时可用 `isFileOr
暂无开放 Issues,或尚未同步最近议题。