DAO MESSAGE — Vibecoding 建造指南
使用说明: 将本文档全文粘贴给 AI,说"请按照这份指南从零构建工程"即可。
本文由三部分组成:架构原则 + SDK API 参考(机器可读) + 接线示例
Part A: 架构原则(10 条铁律)
DAO MESSAGE 不是一个 App,而是一套 AI 时代的隐私通讯协议 + 中继网络即服务。 你正在帮用户构建的,是一个基于 DAO MESSAGE 协议的专属聊天客户端。
SDK 安装
bash
npm install @daomessage_sdk/sdkSDK 已发布到 npm 公共仓库,直接
npm install即可。无需本地 link、submodule 或 monorepo 配置。 源码仓库:https://github.com/daomessage/securechat-sdk-typescript
| # | 铁律 | 约束 |
|---|---|---|
| 1 | SDK 是黑盒 | @daomessage_sdk/sdk 处理所有加密、密钥、WebSocket、IndexedDB。App 只做 UI 壳。不要重复实现 SDK 已有的任何功能。 |
| 2 | 协议至上 | 所有客户端共享同一套 WS 帧格式和加密协议,你不能自创私有协议 |
| 3 | 绝对零信任 | 服务端只转发密文,App 层不得发送任何明文到服务端 |
| 4 | API 已硬编码 | SecureChatClient 构造函数无参数,API 地址 https://relay.daomessage.com 硬编码在 SDK 内,不可传参、不可覆盖 |
| 5 | 命名二元规则 | HTTP 接口返回 snake_case(如 alias_id, friendship_id);IndexedDB 存储 camelCase(如 conversationId, theirAliasId)。不得混用 |
| 6 | 只用 SDK 导出的 API | 不要猜测或发明 SDK 不存在的方法。所有可用方法见下方 Part B 的 TypeScript 声明 |
| 7 | 事件驱动 | 用 client.on('message' | 'status_change' | 'network_state' | 'typing' | 'goaway', handler) 订阅事件,返回 unsubscribe 函数 |
| 8 | 异步安全 | 所有 SDK 异步调用必须 try/catch,UI 层禁止 Silent Failure。按钮必须有 loading/disabled 状态防止重复点击 |
| 9 | 全面出海 | 面向海外用户,UI 默认英文 |
| 10 | Vibe Coding 生态 | 你生成的 App 必须通过 SDK 接入官方中继,与其他开发者的 App 互通 |
技术栈(强制)
| 层 | 技术 | 版本 |
|---|---|---|
| 构建工具 | Vite | 6+ |
| 前端框架 | React | 19+ |
| 样式方案 | TailwindCSS + @tailwindcss/vite 插件 | v4(不是 v3) |
| 状态管理 | Zustand | 5+ |
| 路由 | React Router | 7+ |
| 语言 | TypeScript(strict mode) | 5.5+ |
构建优先级
按以下顺序实现,每个阶段完成可运行后再进入下一阶段:
- 工程骨架 — Vite 初始化 + TailwindCSS v4 + 路由 + SDK 单例
- 注册/登录 — 助记词生成 + 备份确认 + 注册 + 恢复会话
- 通讯录 — 好友列表 + 添加好友 + 好友请求处理
- 文字聊天 — 消息收发 + 历史记录 + 已读回执 + 正在输入
- 多媒体 — 图片/文件/语音消息
- 频道 — 频道列表 + 订阅 + 发帖
- 音视频通话 — WebRTC 信令 + E2EE 帧加密(最后实现)
Part B: SDK TypeScript 类型声明(机器可读合约)
以下是
@daomessage_sdk/sdk的完整.d.ts类型声明文件。 AI 必须严格按照这些类型签名调用 SDK,不存在的方法不能调用。 这是单一真相源——如果本文任何其他描述与此.d.ts冲突,以.d.ts为准。
typescript
// ═══════════════════════════════════════════════
// @daomessage_sdk/sdk — 完整类型声明
// 自动生成自 sdk-typescript/dist/index.d.ts
// ═══════════════════════════════════════════════
/**
* keys/index.ts - SDK 密钥体系
*
* 架构设计 §1.3.1 HD 派生路径规范:
* - m/44'/0'/0'/0/0 → Ed25519(身份认证/签名)
* - m/44'/1'/0'/0/0 → X25519(ECDH 消息加密)
*
* 依赖:
* - @scure/bip39:助记词生成/验证
* - @noble/curves/ed25519:Ed25519 签名
* - @noble/curves/x25519:X25519 ECDH
* - @noble/hashes/sha512:PBKDF KDF
*/
interface KeyPair {
privateKey: Uint8Array;
publicKey: Uint8Array;
}
interface Identity {
mnemonic: string;
/** Ed25519 身份密钥,用于 Challenge-Response 认证 */
signingKey: KeyPair;
/** X25519 ECDH 密钥,用于消息会话密钥协商 */
ecdhKey: KeyPair;
}
/** 生成 12 词英文助记词 */
declare function newMnemonic(): string;
/** 验证助记词是否合法(12 词,BIP-39 词库)*/
declare function validateMnemonicWords(mnemonic: string): boolean;
/**
* 从助记词完整派生 Identity(包含两对密钥)
*/
declare function deriveIdentity(mnemonic: string): Identity;
/**
* 计算 60 字符安全码
* 算法:SHA-256(min(pubA, pubB) ‖ max(pubA, pubB))[0..30] → hex
* 双方使用相同的确定性拼接顺序,MITM 无法伪造一致结果
*/
declare function computeSecurityCode(myEcdhPublicKey: Uint8Array, theirEcdhPublicKey: Uint8Array): string;
declare function toBase64(bytes: Uint8Array): string;
declare function fromBase64(b64: string): Uint8Array;
declare function toHex(bytes: Uint8Array): string;
declare function fromHex(hex: string): Uint8Array;
/**
* keys/store.ts - T-012 IndexedDB 三存储持久化
* 关键数据结构:identity / sessions / offlineInbox
*/
interface StoredIdentity {
uuid: string;
aliasId: string;
nickname: string;
mnemonic: string;
signingPublicKey: string;
ecdhPublicKey: string;
}
interface SessionRecord {
conversationId: string;
theirAliasId: string;
theirEcdhPublicKey: string;
theirEd25519PublicKey?: string;
sessionKeyBase64: string;
trustState: 'unverified' | 'verified';
createdAt: number;
}
interface OfflineMessage {
conversationId: string;
seq: number;
payloadEncrypted: string;
createdAt: number;
}
declare function loadIdentity(): Promise<StoredIdentity | undefined>;
declare function clearIdentity(): Promise<void>;
declare function loadSession(conversationId: string): Promise<SessionRecord | undefined>;
declare function listSessions(): Promise<SessionRecord[]>;
declare function deleteSession(conversationId: string): Promise<void>;
declare function markSessionVerified(conversationId: string): Promise<void>;
interface StoredMessage {
id: string;
conversationId: string;
text: string;
isMe: boolean;
time: number;
status: 'sending' | 'sent' | 'delivered' | 'read' | 'failed';
msgType?: string;
mediaUrl?: string;
caption?: string;
seq?: number;
fromAliasId?: string;
replyToId?: string;
}
interface OutboxIntent {
internalId: string;
conversationId: string;
toAliasId: string;
text: string;
addedAt: number;
replyToId?: string;
}
type NetworkState = 'connected' | 'connecting' | 'disconnected';
type NetworkListener = (state: NetworkState) => void;
declare class RobustWSTransport implements WSTransport {
private ws;
isConnected: boolean;
private messageHandlers;
private openHandlers;
private closeHandlers;
private networkListeners;
private goawayListeners;
private reconnectAttempts;
private reconnectTimer;
private heartbeatTimer;
private intentionalClose;
lastUrl: string;
constructor();
onNetworkStateChange(fn: NetworkListener): () => void;
/** 监听 GOAWAY 帧(被其他设备踢下线) */
onGoaway(fn: (reason: string) => void): () => void;
private emitNetworkState;
connect(url: string): void;
private _doConnect;
private _scheduleReconnect;
private _startHeartbeat;
private _stopHeartbeat;
send(data: string): void;
onMessage(handler: (data: string) => void): void;
onOpen(handler: () => void): void;
onClose(handler: () => void): void;
disconnect(): void;
}
/**
* sdk-typescript/src/messaging/index.ts — T-100+T-101
* MessageModule:完整的消息收发封装 + 离线同步引擎 + 强制本地持久化 (Vibe Coding Refactor)
*/
interface OutgoingMessage {
conversationId: string;
toAliasId: string;
text: string;
replyToId?: string;
}
interface MessageStatus {
id: string;
status: 'sending' | 'sent' | 'delivered' | 'read' | 'failed';
}
interface WSTransport {
send(data: string): void;
onMessage(handler: (data: string) => void): void;
onOpen(handler: () => void): void;
onClose(handler: () => void): void;
isConnected: boolean;
}
declare class MessageModule {
private transport;
onMessage?: (msg: StoredMessage) => void;
onStatusChange?: (status: MessageStatus) => void;
onChannelPost?: (data: any) => void;
/** 对方正在输入通知 */
onTyping?: (data: {
fromAliasId: string;
conversationId: string;
}) => void;
/** 链上支付确认通知(pay-worker 确认后 WS 推送,由 VanityModule 订阅)*/
onPaymentConfirmed?: (data: {
type: string;
order_id: string;
ref_id: string;
}) => void;
constructor(transport: WSTransport);
send(msg: OutgoingMessage): Promise<string>;
private _trySendIntent;
sendDelivered(convId: string, seq: number, toAliasId: string): void;
sendRead(convId: string, seq: number, toAliasId: string): void;
sendTyping(toAliasId: string, convId: string): void;
/** 发送消息撤回帧(架构 §4.2 V1.1 新增) */
sendRetract(messageId: string, toAliasId: string, convId: string): void;
private handleFrame;
private handleIncomingMsg;
private handleStatusChange;
/**
* 处理 delivered/read 回执帧(基于 conv_id 批量更新)
* 回执帧格式:{type:'delivered'|'read', conv_id, seq, to}
* 不含消息 id,所以需要按 conv_id 查找自己发出的消息并更新
*/
private handleReceiptByConvId;
private onConnected;
private handleRetract;
}
declare class HttpClient {
private apiBase;
private token;
constructor(apiBase?: string);
setApiBase(apiBase: string): void;
getApiBase(): string;
setToken(token: string | null): void;
getToken(): string | null;
getHeaders(customHeaders?: Record<string, string>): Record<string, string>;
get<T = any>(path: string): Promise<T>;
post<T = any>(path: string, body: any): Promise<T>;
put<T = any>(path: string, body?: any): Promise<T>;
delete<T = any>(path: string): Promise<T>;
/**
* For direct fetch calls (like Media Presigned URL PUT / GET)
*/
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
}
declare class AuthModule {
private http;
private _uuid;
constructor(http: HttpClient);
/**
* 供 SDK 内部建立 WebSocket 时调用,防误用
*/
get internalUUID(): string;
/**
* 恢复会话:从本地 IndexedDB 加载身份 -> 防伪签名挑战 -> 写入 Token
*/
restoreSession(): Promise<{
aliasId: string;
nickname: string;
} | null>;
/**
* 注册:执行 PoW 防刷验证 -> 计算公钥 -> /register -> /auth/challenge -> /auth/verify
* V1.4.1 方案 A:靓号在注册完成后通过 vanity.bind() 接口绑定,注册时不再传入靓号订单
*/
registerAccount(mnemonic: string, nickname: string): Promise<{
aliasId: string;
}>;
/**
* 针对给定的 UUID 和私钥执行防伪鉴权,成功后将 token 注册到 http 内部并返回
*/
performAuthChallenge(userUUID: string, signingPrivateKey: Uint8Array): Promise<string>;
}
interface FriendProfile {
friendship_id: number;
alias_id: string;
nickname: string;
status: 'pending' | 'accepted' | 'rejected';
direction: 'sent' | 'received';
conversation_id: string;
x25519_public_key: string;
ed25519_public_key: string;
created_at: string;
}
declare class ContactsModule {
private http;
constructor(http: HttpClient);
/**
* 同步通讯录:获取所有好友,并为已经接受的好友自动创建本地安全会话(按需)
*/
syncFriends(): Promise<FriendProfile[]>;
/**
* 发起好友请求
*/
sendFriendRequest(toAliasId: string): Promise<void>;
/**
* 接受好友请求
*/
acceptFriendRequest(friendshipId: number): Promise<void>;
/**
* 按 Alias ID 查找用户
*/
lookupUser(aliasId: string): Promise<{
alias_id: string;
nickname: string;
x25519_public_key: string;
ed25519_public_key: string;
}>;
}
/**
* sdk-typescript/src/media/manager.ts — 多媒体上传/下载(零知识盲中转)
* 支持图片(压缩)、文件(原始)、语音(原始)三类
*/
declare class MediaModule {
private http;
constructor(http: HttpClient);
/**
* 极简外壳:自动压缩、调用端侧加密、并获得安全返回
* 成功即返回拼接好的快捷消息内容: "[img]media_key"
*/
uploadImage(conversationId: string, file: File, maxDim?: number, quality?: number): Promise<string>;
/**
* 上传通用文件(不压缩,直接加密分片上传)
* 返回 "[file]media_key|original_name|size_bytes"
*/
uploadFile(file: File, conversationId: string): Promise<string>;
/**
* 上传语音消息(不压缩,直接加密分片上传)
* @param durationMs 录音时长(毫秒)
* 返回 "[voice]media_key|duration_ms"
*/
uploadVoice(blob: Blob, conversationId: string, durationMs: number): Promise<string>;
/**
* 分片上传加密大文件 (由于原生 AES-GCM 限制,采用基于 Chunk 的流式加密)
* 返回 "[img]media_key" (此处重用业务层格式)
*/
uploadEncryptedFile(file: File, conversationId: string, maxDim?: number, quality?: number): Promise<string>;
/**
* 通用加密分片上传(共享逻辑)
* @returns media_key
*/
private encryptAndUpload;
/**
* 下载加密的媒体文件 (裸流,仅供内部解密使用)
*/
private downloadMedia;
/**
* 下载并流式解密媒体文件
*/
downloadDecryptedMedia(mediaKey: string, conversationId: string): Promise<ArrayBuffer>;
/**
* 压缩图片到指定最大尺寸(宽/高)
*/
private compressImage;
}
declare class PushModule {
private http;
constructor(http: HttpClient);
/**
* 浏览器申请推送凭证并向服务端注册
* 此方法需要依赖浏览器的 ServiceWorker API,仅在 Web 端有效
*/
enablePushNotifications(swRegistration: ServiceWorkerRegistration, vapidPublicKey?: string): Promise<void>;
private urlBase64ToUint8Array;
}
interface ChannelInfo {
id: string;
name: string;
description: string;
role?: string;
is_subscribed?: boolean;
/** 频道是否处于出售状态 */
for_sale?: boolean;
/** 出售价格(USDT),仅当 for_sale=true 时有值 */
sale_price?: number;
}
interface ChannelPost {
id: string;
type: string;
content: string;
created_at: string;
author_alias_id: string;
}
/** 频道交易订单(来自 POST /api/v1/channels/{id}/buy)*/
interface ChannelTradeOrder {
order_id: string;
/** 单位 USDT */
price_usdt: number;
/** TRON 收款地址 */
pay_to: string;
/** 订单有效期(ISO 8601) */
expired_at: string;
}
declare class ChannelsModule {
private http;
constructor(http: HttpClient);
/**
* Search for public channels
*/
search(query: string): Promise<ChannelInfo[]>;
/**
* Get channels subscribed by current user
*/
getMine(): Promise<ChannelInfo[]>;
/**
* Get channel details
*/
getDetail(channelId: string): Promise<ChannelInfo>;
/**
* Create a new channel
*/
create(name: string, description: string, isPublic?: boolean): Promise<{
channel_id: string;
}>;
/**
* Subscribe to a channel
*/
subscribe(channelId: string): Promise<void>;
/**
* Unsubscribe from a channel
*/
unsubscribe(channelId: string): Promise<void>;
/**
* Post a message to a channel
*/
postMessage(channelId: string, content: string, type?: string): Promise<{
post_id: string;
}>;
/**
* Get channel post history
*/
getPosts(channelId: string): Promise<ChannelPost[]>;
/**
* Check if current user can post in the channel
*/
canPost(channelInfo: ChannelInfo | null): boolean;
/**
* 将自有频道挂牌出售(T-096,需要 JWT,必须是频道 Owner)
*
* 使用乐观锁 CAS 设置售价,挂牌后其他用户可通过 `buyChannel` 购买。
*
* @param channelId 要出售的频道 ID
* @param priceUsdt 出售价格(USDT,整数)
*
* @example
* await client.channels.listForSale('ch_abc123', 200)
*/
listForSale(channelId: string, priceUsdt: number): Promise<void>;
/**
* 购买频道 — 创建支付订单(T-096,需要 JWT)
*
* 使用乐观锁 CAS 防止超卖。返回后向用户展示 TRON 收款地址,
* 链上确认后 pay-worker 自动完成频道所有权转移,并推送 `payment_confirmed` WS 事件。
*
* @throws 409 — 频道刚被其他人抢购,请刷新后重试
* @throws 404 — 频道不存在
* @throws 400 — 试图购买自己的频道
*
* @example
* const order = await client.channels.buyChannel('ch_abc123')
* showQRCode(order.pay_to, order.price_usdt)
*/
buyChannel(channelId: string): Promise<ChannelTradeOrder>;
/**
* 购买额外频道创建配额(每次购买增加 1 个席位,固定 5 USDT)
*
* @throws 401 — 需要登录
* @example
* const order = await client.channels.buyQuota()
* showQRCode(order.pay_to, order.price_usdt)
*/
buyQuota(): Promise<ChannelTradeOrder>;
}
/**
* sdk-typescript/src/vanity/manager.ts — T-095 VanityModule
*
* 负责靓号的搜索、购买(创建支付订单)和链上确认回调订阅。
* 支付感知通过 WS `payment_confirmed` 帧 → MessageModule.onPaymentConfirmed → 本模块路由实现。
*
* V1.4.1(方案 A):靓号商店已移至注册完成后,主流程:
* 1. purchase(aliasId) — 注册后购买(需要 JWT),创建 PENDING 订单
* 2. 监听 onPaymentConfirmed() WS 推送 → 链上确认
* 3. bind(orderId) — 支付确认后绑定靓号到账户 alias_id
*
* @deprecated reserve() / orderStatus() 为旧版注册前流程遗留接口,不再使用
*/
/** 靓号列表项(来自 GET /api/v1/vanity/search)
* V1.3 规则引擎版:价格由后端 rules.go 实时评估,不再依赖预填表 */
interface VanityItem {
alias_id: string;
price_usdt: number;
/** 靓号等级:'top' | 'premium' | 'standard' */
tier: string;
is_featured: boolean;
}
/** 购买靓号返回的支付订单(来自 POST /api/v1/vanity/purchase,需 JWT)*/
interface PurchaseOrder {
order_id: string;
alias_id: string;
/** 单位 USDT */
price_usdt: number;
/** NOWPayments 托管支付页 URL(V1.5.0 新增,接入 NOWPayments 后返回)*/
payment_url: string;
/** @deprecated 原始 TRON 地址,NOWPayments 接入后不再使用 */
pay_to?: string;
/** 订单有效期(ISO 8601) */
expired_at: string;
}
/**
* 注册前预订靓号返回的订单(来自 POST /api/v1/vanity/reserve,**无需 JWT**)
* 与 PurchaseOrder 结构相同,但不绑定用户 UUID(注册前无身份)
*/
interface ReserveOrder {
order_id: string;
alias_id: string;
price_usdt: number;
/** TRON 收款地址 */
pay_to: string;
/** 订单有效期(ISO 8601) */
expired_at: string;
}
/**
* 订单状态查询结果(来自 GET /api/v1/vanity/order/{id}/status,**无需 JWT**)
* 用于注册前用户轮询支付结果
*/
interface OrderStatus {
status: 'pending' | 'confirmed' | 'expired';
alias_id: string;
}
/** WS payment_confirmed 事件(pay-worker 链上确认后推送)*/
interface PaymentConfirmedEvent {
type: 'payment_confirmed';
order_id: string;
/** 靓号购买 → alias_id;频道交易 → channel_id */
ref_id: string;
}
declare class VanityModule {
private http;
private paymentListeners;
constructor(http: HttpClient);
/**
* 搜索靓号 / 获取精选列表(T-091 公开接口,无需 JWT)
*
* - `q` 为空 → 返回精选 (is_featured=1),按价格升序
* - `q` 非空 → 按 alias_id 前缀匹配(LIKE 'q%')
*
* @example
* const featured = await client.vanity.search()
* const results = await client.vanity.search('888')
*/
search(q?: string): Promise<VanityItem[]>;
/**
* @deprecated V1.4.1 方案 A 后不再使用。旧版注册前公开预订靓号(无需 JWT)。
* 请改用 purchase()(注册后,需 JWT)+ bind() 流程。
*/
reserve(aliasId: string): Promise<ReserveOrder>;
/**
* @deprecated V1.4.1 方案 A 后不再使用。旧版注册前轮询订单状态(无需 JWT)。
* 注册后请改用 onPaymentConfirmed() WS 推送。
*/
orderStatus(orderId: string): Promise<OrderStatus>;
/**
* 购买靓号 — 创建支付订单(T-090,**需要 JWT**)
*
* V1.4.1 方案 A:注册完成后的首次引导页调用此方法。
* 使用乐观锁 CAS 占位 15 分钟。返回后向用户展示 TRON 收款地址,
* 用户链上转账后 pay-worker 自动完成确认,并通过 WS 推送 `payment_confirmed`。
* 收到推送后,调用 bind(orderId) 将靓号正式绑定到账户。
*
* @throws 409 — 靓号已被其他人抢占,请提示用户更换
* @throws 404 — 靓号不存在
*
* @example
* const order = await client.vanity.purchase('88888888')
* // 展示支付弹窗
* client.vanity.onPaymentConfirmed(async e => {
* const { alias_id } = await client.vanity.bind(e.order_id)
* store.setAliasId(alias_id)
* })
*/
purchase(aliasId: string): Promise<PurchaseOrder>;
/**
* 绑定靓号到当前账户(**V1.4.1 新增,需要 JWT**)
*
* 在 pay-worker 确认链上支付后,调用此方法将 `alias_id` 正式写入 identity 表。
* 通常在 onPaymentConfirmed() 回调内调用。
*
* @param orderId — 已确认的 `payment_order.id`
* @returns `{ alias_id }` — 绑定成功的靓号
*
* @throws 404 — 订单不存在或不属于当前用户
* @throws 409 — 订单未确认 / 靓号绑定冲突
*
* @example
* const { alias_id } = await client.vanity.bind(orderId)
* store.setAliasId(alias_id)
*/
bind(orderId: string): Promise<{
alias_id: string;
}>;
/**
* 订阅支付完成回调(链上确认后 pay-worker → WS 推送)
*
* 返回 unsubscribe 函数,可直接在 React `useEffect` 清理函数中调用。
*
* @example
* useEffect(() => {
* return client.vanity.onPaymentConfirmed(e => {
* toast(`🎉 靓号 ${e.ref_id} 已绑定到你的账号!`)
* router.push('/profile')
* })
* }, [])
*/
onPaymentConfirmed(cb: (e: PaymentConfirmedEvent) => void): () => void;
/**
* @internal SDK 内部路由入口,由 MessageModule handleFrame 在收到
* `payment_confirmed` WS 帧时调用,App 层不应直接调用此方法。
*/
_handlePaymentConfirmed(event: PaymentConfirmedEvent): void;
}
/**
* sdk-typescript/src/calls/index.ts — T-072+T-073
* WebRTC 信令状态机 + Insertable Streams E2EE(视频帧加密)
*
* 架构 §4:通话建立流程
* Caller → call_offer(含 SDP + Ed25519 签名)
* → Relay(透明转发)
* → Callee → call_answer / call_reject
* → ICE Candidate 交换
* ← TURN 中继 ← RTP
*/
type CallState = 'idle' | 'calling' | 'ringing' | 'connecting' | 'connected' | 'hangup' | 'rejected' | 'ended';
interface CallOptions {
audio?: boolean;
video?: boolean;
}
interface SignalTransport {
send(env: unknown): void;
onMessage(handler: (env: unknown) => void): void;
}
declare class CallModule {
private transport;
private iceConfigProvider;
private pc;
private callId;
private state;
private localStream;
private remoteStream;
private pendingCandidates;
private flushIceCandidates;
private signingPrivKey;
private signingPubKey;
private myAliasId;
onStateChange?: (state: CallState) => void;
onRemoteStream?: (stream: MediaStream) => void;
onLocalStream?: (stream: MediaStream) => void;
onIncomingCall?: (fromAlias: string) => void;
onError?: (err: Error) => void;
getLocalStream(): MediaStream | null;
getRemoteStream(): MediaStream | null;
constructor(transport: SignalTransport, iceConfigProvider: () => Promise<RTCConfiguration>, opts: {
signingPrivKey: Uint8Array;
signingPubKey: Uint8Array;
myAliasId: string;
});
call(toAliasId: string, opts?: CallOptions): Promise<void>;
answer(): Promise<void>;
reject(): void;
hangup(): void;
private _callerAlias;
private _remoteAlias;
private handleSignal;
private handleOffer;
private handleAnswer;
private handleICE;
private createPeerConnection;
private sendSignal;
private setState;
private cleanup;
private _pubKeyCache;
private fetchPubKey;
private _sessionKeyCache;
private getSessionKey;
}
/**
* setupE2EETransform:为 RTCRtpSender/Receiver 安装帧级加解密 Transform
* @param kind 'sender' | 'receiver'
* @param rtpObject RTCRtpSender 或 RTCRtpReceiver
* @param keyMaterial AES-256-GCM 密钥 + BaseIV(44字节:32+12,由 HKDF 从会话密钥派生)
*/
declare function setupE2EETransform(kind: 'sender' | 'receiver', rtpObject: RTCRtpSender | RTCRtpReceiver, keyMaterial: Uint8Array): Promise<void>;
type ClientEvent = 'message' | 'status_change' | 'network_state' | 'channel_post' | 'typing' | 'goaway';
interface TypingEvent {
fromAliasId: string;
conversationId: string;
}
declare class SecureChatClient {
readonly transport: RobustWSTransport;
readonly messaging: MessageModule;
readonly auth: AuthModule;
readonly contacts: ContactsModule;
readonly media: MediaModule;
readonly push: PushModule;
readonly channels: ChannelsModule;
readonly vanity: VanityModule;
calls: CallModule | null;
http: HttpClient;
private eventListeners;
static readonly CORE_API_BASE = "https://relay.daomessage.com";
constructor();
/**
* 恢复历史会话:从本地加载身份鉴权,成功后即可调用 connect
* 返回值新增 nickname,App 不再需要从 localStorage 读取昵称
*/
restoreSession(): Promise<{
aliasId: string;
nickname: string;
} | null>;
/**
* 建立连接:自动从 AuthModule 与 HTTP 获取底层验证标识,封装并连接 WSS
*/
connect(): void;
/**
* 断开连接
*/
disconnect(): void;
/**
* 获取当前网络状态
*/
get isConnected(): boolean;
/**
* 初始化通话模块(需要提供从 DB 提取的用户身份密钥)
*/
initCalls(opts: {
signingPrivKey: Uint8Array;
signingPubKey: Uint8Array;
myAliasId: string;
alwaysRelay?: boolean;
}): void;
/**
* 事件订阅 — 返回 unsubscribe 函数,可直接在 useEffect return 中调用
* @example
* useEffect(() => {
* return client.on('message', handleMsg) // 自动解绑
* }, [])
*/
on(event: 'message', listener: (msg: StoredMessage) => void): () => void;
on(event: 'status_change', listener: (status: MessageStatus) => void): () => void;
on(event: 'network_state', listener: (state: NetworkState) => void): () => void;
on(event: 'channel_post', listener: (data: any) => void): () => void;
on(event: 'typing', listener: (data: TypingEvent) => void): () => void;
on(event: 'goaway', listener: (reason: string) => void): () => void;
/**
* 移除事件订阅(on() 已返回 unsubscribe,推荐用 on() 的返回值代替此方法)
*/
off(event: ClientEvent, listener: any): void;
/**
* 发送端到端加密消息(自动入队或发送)
* @param replyToId 可选,引用回复的原消息 ID(架构 §SendOptions.replyTo)
*/
sendMessage(conversationId: string, toAliasId: string, text: string, replyToId?: string): Promise<string>;
/**
* 发送图片消息:压缩、盲加密上传、拼接协议发回
* @param thumbnail 可选,Base64 低分辨率骨架缩略图,供接收方在高清图加载前展示
*/
sendImage(conversationId: string, toAliasId: string, file: File, thumbnail?: string): Promise<string>;
/**
* 发送文件消息:直接加密上传(不压缩)
* 消息协议: [file]media_key|filename|size
*/
sendFile(conversationId: string, toAliasId: string, file: File): Promise<string>;
/**
* 发送语音消息:录音 Blob 直接加密上传
* @param durationMs 录音时长(毫秒),前端 MediaRecorder 计时
* 消息协议: [voice]media_key|duration_ms
*/
sendVoice(conversationId: string, toAliasId: string, blob: Blob, durationMs: number): Promise<string>;
/**
* 发送正在输入状态(含节流,建议调用方在 300ms 防抖后触发)
*/
sendTyping(conversationId: string, toAliasId: string): void;
/**
* 标记收到的消息为已读
* @param toAliasId 消息发送方的 alias_id,后端据此路由已读回执
*/
markAsRead(conversationId: string, maxSeq: number, toAliasId: string): void;
/**
* 撤回消息(架构 §4.2 V1.1 新增)
* 仅可撤回自己发的消息,不限时间
* @param messageId 要撤回的消息 UUID
* @param toAliasId 对方 alias_id
* @param conversationId 会话 ID
*/
retractMessage(messageId: string, toAliasId: string, conversationId: string): Promise<void>;
/**
* 获取会话的历史消息(来自 SDK 内部持久化数据库 IndexedDB)
* @param opts.limit 最多返回条数(默认全量)
* @param opts.before 只返回时间戳小于此值的消息(用于分页加载更早消息)
*/
getHistory(conversationId: string, opts?: {
limit?: number;
before?: number;
}): Promise<StoredMessage[]>;
/**
* 获取单条消息细节
*/
getMessageData(messageId: string): Promise<StoredMessage | undefined>;
/**
* 清除某个会话历史
*/
clearHistory(conversationId: string): Promise<void>;
/**
* 清除所有会话历史
*/
clearAllHistory(): Promise<void>;
/**
* 导出会话存档(NDJSON 格式)
* @param conversationId 指定会话 ID,可传 'all' 导出全部
* @returns string 生成下载用途的 Blob Object URL
*/
exportConversation(conversationId: string): Promise<string>;
}
/**
* security/index.ts — SecurityModule(P3-004 修复)
*
* 实现文档 §2.2.1 SecurityModule 接口设计:
* - getSecurityCode(contactId) → 60位安全码(MITM 防御)
* - verifyInputCode(contactId, code) → 输入验证(主路径)
* - markAsVerified(contactId) → 手动标记已验证(辅助路径)
* - getTrustState(contactId) → 信任状态
* - resetTrustState(contactId) → 重置信任
*
* 所有状态存储于 IndexedDB(服务器完全不知情)
* 防劫持守护:每条消息触发前调用 guardMessage() 检测公钥突变
*/
interface SecurityCode {
contactId: string;
/** 60 位十六进制字符串,每 4 字符一组,如 "AB12 · F39C · ..." */
displayCode: string;
/** 原始 hex(用于 verifyInputCode 内部比对)*/
fingerprintHex: string;
}
type TrustState = {
status: 'unverified';
} | {
status: 'verified';
verifiedAt: number;
fingerprintSnapshot: string;
};
interface SecurityViolationEvent {
type: 'security_violation';
contactId: string;
previousFingerprint: string;
currentFingerprint: string;
detectedAt: number;
message: null;
}
declare class SecurityModule {
/**
* 获取与指定联系人的安全码(60 位 hex 字符串)
* 每次加好友后全自动生成,UI 打开"加密详情"页时调用
*/
getSecurityCode(contactId: string, myEcdhPublicKey: Uint8Array, theirEcdhPublicKey: Uint8Array): Promise<SecurityCode>;
/**
* 输入验证(主路径):
* 用户粘贴对方通过微信/TG 发来的 60 位安全码,SDK 自动与本地计算值比对
* 返回 true → 一致(无 MITM)→ 自动写入 verified
* 返回 false → 不一致(公钥被篡改)
*/
verifyInputCode(contactId: string, inputCode: string, myEcdhPublicKey: Uint8Array, theirEcdhPublicKey: Uint8Array): Promise<boolean>;
/**
* 手动标记为"已验证"(辅助路径):
* 用户通过截图肉眼比对后,手动点击按钮调用此方法
* 服务器完全不知情(存储于 IndexedDB)
*/
markAsVerified(contactId: string, myEcdhPublicKey: Uint8Array, theirEcdhPublicKey: Uint8Array): Promise<void>;
/**
* 获取指定联系人当前的信任状态
*/
getTrustState(contactId: string): Promise<TrustState>;
/**
* 重置验证状态:将 trust_state 还原为 'unverified'
* 场景:用户主动换设备/助记词后需重新核查
*/
resetTrustState(contactId: string): Promise<void>;
/**
* 防劫持守护(文档 §2.2 流程图三):
* 每条消息到达时自动调用,若检测到公钥突变,返回 SecurityViolationEvent
*
* @returns null → 验证通过,可正常解密
* @returns SecurityViolationEvent → 公钥突变,拒绝解密并上报 UI
*/
guardMessage(contactId: string, currentMyEcdh: Uint8Array, currentTheirEcdh: Uint8Array): Promise<SecurityViolationEvent | null>;
}
declare const securityModule: SecurityModule;
interface EstablishedSession {
conversationId: string;
securityCode: string;
trustedAt?: number;
}
interface MessageEnvelope {
type: 'msg';
id: string;
to: string;
conv_id: string;
crypto_v: number;
payload: string;
}
export { CallModule, type CallOptions, type CallState, type ChannelTradeOrder, type FriendProfile as ContactProfile, ContactsModule, type EstablishedSession, type Identity, type KeyPair, type MessageEnvelope, MessageModule, type MessageStatus, type NetworkState, type OfflineMessage, type OrderStatus, type OutboxIntent, type OutgoingMessage, type PaymentConfirmedEvent, type PurchaseOrder, type ReserveOrder, RobustWSTransport, SecureChatClient, type SecurityCode, SecurityModule, type SecurityViolationEvent, type SessionRecord, type SignalTransport, type StoredIdentity, type StoredMessage, type TrustState, type TypingEvent, type VanityItem, VanityModule, type WSTransport, clearIdentity, computeSecurityCode, deleteSession, deriveIdentity, fromBase64, fromHex, listSessions, loadIdentity, loadSession, markSessionVerified, newMnemonic, securityModule, setupE2EETransform, toBase64, toHex, validateMnemonicWords };Part C: 最小可运行接线示例
以下是将 SDK 接入 App 的关键接线代码。 AI 应以此为模式参考,扩展出完整的聊天应用。
C.1 SDK 单例(全局一个实例)
typescript
import { SecureChatClient } from '@daomessage_sdk/sdk';
// 无参实例化——API 地址已硬编码在 SDK 内
export const client = new SecureChatClient();C.2 冷启动流程
typescript
// App 启动时 → 尝试恢复会话
const session = await client.restoreSession();
if (!session) {
// 首次使用 → 进入注册流程
navigateTo('welcome');
} else {
// 已注册 → 连接 WebSocket + 同步好友
const { aliasId, nickname } = session;
client.connect();
await client.contacts.syncFriends();
navigateTo('main');
}C.3 注册流程
typescript
import { newMnemonic } from '@daomessage_sdk/sdk';
// Step 1: 生成 12 词助记词(同步函数!)
const mnemonic = newMnemonic();
// → 展示给用户备份
// Step 2: 用户确认备份后注册
const { aliasId } = await client.auth.registerAccount(mnemonic, nickname);
// → SDK 自动完成:PoW → 密钥派生 → HTTP 注册 → JWT 获取
// Step 3: 连接
client.connect();C.4 事件订阅(React 模式)
typescript
useEffect(() => {
// 返回值是 unsubscribe 函数,可直接用于 cleanup
const unsub1 = client.on('message', (msg) => {
// msg: StoredMessage — 已解密的消息
setMessages(prev => [...prev, msg]);
});
const unsub2 = client.on('network_state', (state) => {
// state: 'connected' | 'connecting' | 'disconnected'
setNetworkState(state);
});
const unsub3 = client.on('goaway', (reason) => {
// 被其他设备踢下线 → 清除身份 → 跳转登录
client.disconnect();
clearIdentity();
navigateTo('welcome');
});
return () => { unsub1(); unsub2(); unsub3(); };
}, []);C.5 好友列表 + 分类
typescript
// syncFriends() 自动建立 ECDH 会话(不需要 App 层干预)
const friends = await client.contacts.syncFriends();
// HTTP 返回 snake_case 字段!
const pendingReceived = friends.filter(f => f.status === 'pending' && f.direction === 'received');
const pendingSent = friends.filter(f => f.status === 'pending' && f.direction === 'sent');
const accepted = friends.filter(f => f.status === 'accepted');
// 点击好友进入聊天 → 用 conversation_id(snake_case!)
setActiveChatId(friend.conversation_id);C.6 发消息 + 收消息
typescript
// 发文本
const msgId = await client.sendMessage(conversationId, toAliasId, 'Hello!');
// 发图片(SDK 自动:压缩 → 加密 → 上传 → 拼协议)
const imgId = await client.sendImage(conversationId, toAliasId, file);
// 撤回
await client.retractMessage(msgId, toAliasId, conversationId);
// 标记已读
client.markAsRead(conversationId, maxSeq, toAliasId);
// 加载历史(分页)
const history = await client.getHistory(conversationId, { limit: 50, before: oldestTimestamp });C.7 退出登录(顺序很重要)
typescript
import { clearIdentity } from '@daomessage_sdk/sdk';
client.disconnect(); // 1. 断开 WebSocket
await clearIdentity(); // 2. 清除 IndexedDB 身份
await client.clearAllHistory(); // 3. 清除消息历史
localStorage.clear(); // 4. 清除 App 层缓存
navigateTo('welcome');C.8 GOAWAY 多端踢出处理
typescript
// 必须监听!不处理会导致多设备冲突
useEffect(() => client.on('goaway', async (reason) => {
client.disconnect();
await clearIdentity();
await client.clearAllHistory();
localStorage.clear();
navigateTo('welcome');
}), []);Part D: Android/Kotlin 实现指南
§十七 Android SDK 接入指南
本节面向 AI:基于
sdk-android/生成 Android 版 DAO MESSAGE 客户端时,必须遵守本节所有约束。
17.1 SDK 位置与依赖
monorepo/
├── sdk-android/ ← Android Library(Kotlin)
│ ├── sdk/ ← 主模块,对标 @daomessage_sdk/sdk
│ ├── sample-app/ ← 最小示例(Compose)
│ └── README.md ← AI Vibecoding Prompt 入口接入方式(monorepo 本地依赖):
kotlin
// settings.gradle.kts
includeBuild("../sdk-android")
// app/build.gradle.kts
dependencies {
implementation("space.securechat:sdk")
}17.2 初始化(Application.onCreate,一次)
kotlin
// 🔒 SDK 自动创建 Room DB、HttpClient、BouncyCastle 安全提供者
SecureChatClient.init(applicationContext)
val client = SecureChatClient.getInstance()17.3 注册流程(对标 Web 端 Welcome → GenerateMnemonic → SetNickname)
kotlin
// Step 1: 生成助记词(展示给用户备份)
val mnemonic = KeyDerivation.newMnemonic()
// Step 2: 用户确认备份后注册(PoW + 密钥派生 + JWT 全自动)
val aliasId = client.auth.registerAccount(mnemonic, nickname = "Alice")
// Step 3: 连接 WebSocket
client.connect()
// Step 4: 同步好友(建立 ECDH 会话)
val friends = client.contacts.syncFriends()17.4 恢复会话(每次 App 启动)
kotlin
val session = client.restoreSession()
if (session == null) {
navigateToWelcome() // 首次:进入注册流程
} else {
val (aliasId, nickname) = session
client.connect()
client.contacts.syncFriends()
navigateToMain()
}17.5 消息收发
kotlin
// 接收(在 Activity/Fragment 生命周期内注册)
val unsub = client.on(SecureChatClient.EVENT_MESSAGE) { msg: StoredMessage ->
// ✅ 主线程回调,直接更新 UI
adapter.addMessage(msg)
}
// onStop/onPause 中: unsub()
// 发送 E2EE 文本
val msgId = client.sendMessage(conversationId, toAliasId, "Hello!")
// 撤回
client.retractMessage(msgId, toAliasId, conversationId)
// Typing
client.sendTyping(conversationId, toAliasId)
// 已读
client.markAsRead(conversationId, maxSeq, toAliasId)17.6 退出登录
kotlin
// 🔒 SDK 自动:disconnect + 清 Room DB + 清 JWT
client.logout()
navigateToWelcome()17.7 FCM 推送接入
kotlin
// FirebaseMessagingService.onNewToken() 中
client.push.register(fcmToken)
// 推送 data payload(relay-server 格式):
// { "type": "new_msg", "conv_id": "c-xxx" }
// ⚠️ E2EE 原则:服务端不推送明文内容,App 展示"新消息"通知即可17.9 SDK 责任边界(Android 对应版)
| 职责 | 🔒 SDK 自动完成 | 👤 App 实现 |
|---|---|---|
| 密钥派生 | ✅ BIP-39 + SLIP-0010 | ❌ |
| PoW 计算 | ✅ SHA-256 nonce | ❌ |
| JWT 获取/注入 | ✅ Challenge-Response | ❌ |
| ECDH 会话建立 | ✅ syncFriends() 自动完成 | ❌ |
| AES-GCM 加解密 | ✅ 收发透明加解密 | ❌ |
| 消息持久化 | ✅ Room DB | ❌ |
| WebSocket 重连 | ✅ 指数退避 8 级 | ❌ |
| FCM Token 上传 | ✅ push.register() | ❌ |
| UI 路由 | ❌ | ✅ Welcome/Main/Chat |
| 助记词备份 UI | ❌ | ✅ 12 宫格展示 |
| 消息列表渲染 | ❌ | ✅ RecyclerView/LazyColumn |
| 本地通知展示 | ❌ | ✅ NotificationManager |
17.10 Android App UI 组装与实施清单 (Phase 1-7)
⚠️ 强烈注意:不要止步于 SDK 初始化和无聊的骨壳框架!Android App 必须和 React 网页端一样惊艳! 如果你在接下来的任务中负责开发
sample-app,请参考 TSXPhase 1-7的概念,在 Compose 或 XML 中实现以下界面:
| 阶段 | 对标 TS 目标 | Android 最佳实践映射 (Jetpack Compose) |
|---|---|---|
| ✅ Phase 1 基础入驻 | AppStore/AccountRecovery | 欢迎页/恢复页: 12 宫格助记词生成/录入 (MnemonicGrid),必须包含复制/粘贴校验机制。 |
| ✅ Phase 2 会话骨架 | MessagesTab/ChatWindow | 消息 Tab: 渲染 db.messageDao().getPaged() 历史。必须实现:发送骨架、接收回显对齐。 |
| ✅ Phase 3 好友系统 | ContactsTab/lookupUser | 通讯录 Tab: 必须实现三分区渲染 (新请求、待回应、我的好友)。提供添加按钮,触发 lookupUser + sendFriendRequest。 |
| ✅ Phase 4 极致交互 | ChatWindow 细节特效 | 消息撤回 (长按弹窗调用 sendRetract)、双勾已读回执UI、引用回复逻辑。 |
| ✅ Phase 5 零级信任 | SecurityModule UI | 信赖验证盾牌: 接入新加入的 SecurityModule,在对方信息页显示黄盾(未验证),双方出示扫码/比对 60 位提取安全码(verifyInputCode 行动)。 |
| ✅ Phase 6 沉浸推送 | FCM / 本地通知 | 进程保活与唤醒: google-services.json FCM 后台静默唤醒触发 Notification 弹窗,点击跳转特定 conversation_id。 |
| ✅ Phase 7 富媒体 | Channels / 媒体扩展 | 图片放大器,文件下载中心、sendVoice 音频录音拖动波形 UI 交互。 |
| ✅ Phase 8 端到端音视频 | WebRTC Calls | 隐私通讯: 接入 CallModule,实现 P2P 加密音视频流,处理来电响铃/接听/挂断 UI 状态,并遵守防挂起规则。 |