去中心化社交模块
本文档是 系统设计主文档 的子文档,详细描述去中心化社交模块的设计。
2.2 去中心化社交模块
2.2.1 功能描述
构建基于身份自主权(DID)的去中心化社交网络,用户完全掌控自己的社交图谱和内容,无需依赖中心化平台。
2.2.2 架构设计
去中心化社交
├── 身份层 (DID - Decentralized Identity)
│ ├── 身份生成 (基于U盾/SIMKey的公私钥对)
│ ├── DID文档
│ │ ├── DID标识符: did:chainlesschain:<public_key_hash>
│ │ ├── 公钥列表 (加密、签名、认证)
│ │ ├── 服务端点 (个人节点地址)
│ │ └── 验证方法
│ └── 可验证凭证 (Verifiable Credentials)
│ ├── 自我声明 (昵称、头像、简介)
│ ├── 信任背书 (他人签名的凭证)
│ └── 技能证书 (链上存证)
│
├── 通信层 (P2P Network)
│ ├── 节点发现
│ │ ├── DHT (分布式哈希表) - Kademlia
│ │ ├── 引导节点 (Bootstrap Nodes)
│ │ └── mDNS (本地网络发现)
│ │
│ ├── 消息传输
│ │ ├── WebRTC (直接P2P通信)
│ │ ├── QUIC协议 (低延迟)
│ │ ├── 中继节点 (NAT穿透失败时)
│ │ └── 离线消息存储 (临时中继)
│ │
│ └── 消息加密
│ ├── Signal协议 (端到端加密)
│ ├── 双棘轮算法 (前向安全)
│ └── 会话密钥管理
│
├── 内容层
│ ├── 内容类型
│ │ ├── 文本动态 (类似微博)
│ │ ├── 长文章 (类似博客)
│ │ ├── 私密消息 (加密聊天)
│ │ └── 群组讨论
│ │
│ ├── 内容存储
│ │ ├── 本地存储 (自己的内容)
│ │ ├── IPFS (公开内容的分布式存储)
│ │ ├── 缓存 (关注者的内容)
│ │ └── Git仓库 (版本历史)
│ │
│ └── 内容分发
│ ├── 关注者推送 (主动推送给在线关注者)
│ ├── 拉取同步 (用户主动拉取更新)
│ └── 中继广播 (通过友好节点传播)
│
├── 社交图谱层
│ ├── 关系管理
│ │ ├── 关注/粉丝 (单向关注)
│ │ ├── 好友 (双向关注)
│ │ ├── 分组 (自定义标签)
│ │ └── 黑名单
│ │
│ ├── 信任网络
│ │ ├── 信任评分 (基于互动历史)
│ │ ├── Web of Trust (信任传递)
│ │ └── 信誉证明 (区块链锚定)
│ │
│ └── 隐私控制
│ ├── 内容可见性 (公开/仅好友/私密)
│ ├── 选择性同步 (只缓存感兴趣的内容)
│ └── 匿名模式 (临时身份)
│
└── 发现层
├── 内容发现
│ ├── 时间线 (关注者内容流)
│ ├── 话题标签 (#hashtag)
│ ├── 全文搜索 (本地索引)
│ └── 推荐算法 (本地AI推荐)
│
└── 用户发现
├── 好友推荐 (共同好友)
├── 兴趣匹配 (基于内容分析)
└── 附近的人 (蓝牙/GPS可选)2.2.3 核心流程
用户注册流程:
1. 用户选择创建新身份
2. U盾/SIMKey生成密钥对 (Ed25519签名 + X25519加密)
3. 生成DID标识符: did:chainlesschain:<pubkey_hash>
4. 创建DID文档并自签名
5. 配置个人节点 (可选自托管或使用免费中继)
6. 发布DID文档到DHT网络
7. 生成DID二维码/链接供他人添加添加好友流程:
1. 用户A扫描用户B的DID二维码
2. 从DHT网络获取B的DID文档
3. 验证DID文档签名
4. 发送好友请求 (使用B的加密公钥加密)
- 消息内容: A的DID + 公钥 + 自我介绍 + 签名
5. B收到请求后验证签名
6. B同意后建立Signal加密会话
7. 双方交换公钥,存储到本地联系人数据库
8. 开始P2P通信发布动态流程:
1. 用户撰写动态内容
2. 选择可见性级别 (公开/好友/私密)
3. 内容签名 (私钥签名证明authorship)
4. 如果是公开内容:
- 上传到IPFS获取CID
- 将CID + 元数据发布到DHT
5. 如果是好友可见:
- 加密内容 (每个好友的公钥单独加密)
- 通过P2P推送给在线好友
- 离线好友消息存储在中继节点
6. 记录到本地Git仓库 (版本历史)
7. 更新本地内容索引查看时间线流程:
1. 用户打开时间线
2. 从本地数据库读取缓存内容
3. 并行向所有关注者的节点拉取更新
- 发送最后同步时间戳
- 获取增量更新
4. 验证每条内容的签名
5. 解密私密内容
6. 合并排序显示 (按时间/算法推荐)
7. 异步下载多媒体资源 (图片、视频)
8. 更新本地缓存2.2.4 数据模型
本地社交数据库表结构:
-- DID身份表 (本人的多个身份)
CREATE TABLE identities (
did TEXT PRIMARY KEY,
nickname TEXT,
avatar_path TEXT,
bio TEXT,
public_key_sign TEXT NOT NULL, -- 签名公钥
public_key_encrypt TEXT NOT NULL, -- 加密公钥
private_key_ref TEXT NOT NULL, -- U盾/SIMKey中的私钥引用
did_document TEXT NOT NULL, -- JSON格式的DID文档
created_at INTEGER NOT NULL,
is_default INTEGER DEFAULT 0
);
-- 联系人表
CREATE TABLE contacts (
did TEXT PRIMARY KEY,
nickname TEXT,
avatar_url TEXT,
bio TEXT,
public_key_sign TEXT NOT NULL,
public_key_encrypt TEXT NOT NULL,
did_document TEXT,
relationship TEXT, -- 'following', 'follower', 'friend', 'blocked'
trust_score REAL DEFAULT 0.0, -- 0-1之间的信任评分
tags TEXT, -- JSON数组: 分组标签
node_address TEXT, -- 对方的节点地址
last_seen INTEGER,
added_at INTEGER NOT NULL
);
-- 动态内容表
CREATE TABLE posts (
id TEXT PRIMARY KEY,
author_did TEXT NOT NULL,
content TEXT NOT NULL,
content_type TEXT DEFAULT 'text', -- 'text', 'article', 'image', 'video'
media_urls TEXT, -- JSON数组: 多媒体附件
visibility TEXT DEFAULT 'public', -- 'public', 'friends', 'private'
ipfs_cid TEXT, -- 公开内容的IPFS CID
signature TEXT NOT NULL, -- 作者签名
created_at INTEGER NOT NULL,
synced_at INTEGER,
is_local INTEGER DEFAULT 0, -- 是否本人发布
FOREIGN KEY (author_did) REFERENCES contacts(did)
);
-- 私密消息表
CREATE TABLE messages (
id TEXT PRIMARY KEY,
conversation_id TEXT NOT NULL,
sender_did TEXT NOT NULL,
receiver_did TEXT NOT NULL,
content_encrypted BLOB NOT NULL, -- Signal协议加密
media_encrypted BLOB,
ratchet_state TEXT, -- Signal双棘轮状态
signature TEXT NOT NULL,
created_at INTEGER NOT NULL,
delivered_at INTEGER,
read_at INTEGER,
FOREIGN KEY (sender_did) REFERENCES contacts(did),
FOREIGN KEY (receiver_did) REFERENCES contacts(did)
);
-- 群组表
CREATE TABLE groups (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
avatar_path TEXT,
creator_did TEXT NOT NULL,
group_key_encrypted BLOB NOT NULL, -- 群组对称密钥(用成员公钥加密)
created_at INTEGER NOT NULL,
FOREIGN KEY (creator_did) REFERENCES contacts(did)
);
-- 群组成员表
CREATE TABLE group_members (
group_id TEXT,
member_did TEXT,
role TEXT DEFAULT 'member', -- 'admin', 'member'
joined_at INTEGER NOT NULL,
PRIMARY KEY (group_id, member_did),
FOREIGN KEY (group_id) REFERENCES groups(id),
FOREIGN KEY (member_did) REFERENCES contacts(did)
);
-- 信任背书表 (Web of Trust)
CREATE TABLE endorsements (
id TEXT PRIMARY KEY,
endorser_did TEXT NOT NULL, -- 背书人
endorsee_did TEXT NOT NULL, -- 被背书人
skill_or_trait TEXT NOT NULL, -- 背书的技能或特质
comment TEXT,
signature TEXT NOT NULL, -- 背书人签名
created_at INTEGER NOT NULL,
FOREIGN KEY (endorser_did) REFERENCES contacts(did),
FOREIGN KEY (endorsee_did) REFERENCES contacts(did)
);2.2.5 技术选型 (实际实现 v0.26.0)
桌面端 (100%完成):
| 组件 | 技术选择 | 版本/说明 |
|---|---|---|
| DID标准 | W3C DID Core | 符合国际标准 |
| P2P网络 | libp2p 3.1.2 | 成熟的P2P通信库 |
| P2P子模块 | @libp2p/webrtc 6.0.10 | WebRTC传输层 |
| @libp2p/noise 1.0.1 | Noise协议加密 | |
| @libp2p/kad-dht 16.1.2 | Kademlia DHT | |
| @libp2p/circuit-relay-v2 4.1.2 | 中继节点支持 | |
| WebRTC | werift 0.22.2 | WebRTC实现 |
| WebRTC兼容 | wrtc-compat.js ⭐新增 | 兼容层(152行,v0.20.0) |
| 端到端加密 | Signal协议 | @privacyresearch/libsignal-protocol-typescript 0.0.16 |
| 分布式存储 | IPFS | 公开内容的永久存储 |
| DHT | Kademlia | 节点发现和路由 |
| 签名算法 | Ed25519 | 高效的椭圆曲线签名 |
| 加密算法 | X25519 + ChaCha20-Poly1305 | 现代加密组合 |
实现文件 (29个文件):
p2p-manager.js(58KB) - 核心P2P编排signal-session-manager.js(23KB) - Signal协议加密voice-video-manager.js(23KB) - WebRTC音视频wrtc-compat.js(152行) - WebRTC兼容层 ⭐v0.20.0新增file-transfer-manager.js- 大文件传输(64KB分块)message-manager.js- P2P消息传递device-sync-manager.js- 跨设备同步
iOS (20%完成, v0.2.0) ⭐最新:
| 组件 | 技术选择 | 实现状态 |
|---|---|---|
| DID实现 | CoreDID模块 | ✅ 100% - did:key, Ed25519 |
| DID方法 | did:key | ✅ Multibase + Multicodec |
| 签名算法 | Ed25519 | ✅ CryptoKit原生实现 |
| E2EE | Signal Protocol | ✅ 100% - CryptoKit实现 |
| 密钥交换 | X3DH | ✅ P256曲线 |
| 消息加密 | Double Ratchet | ✅ CryptoKit实现 |
| P2P网络 | WebRTC | ✅ 框架完成 - Google WebRTC iOS SDK |
| 信令服务 | WebSocket | ✅ Starscream库 |
| 设备发现 | Bonjour (NSD) | ✅ iOS原生支持 |
| P2P管理 | P2PManager | ✅ 596行 - 连接管理/状态同步 |
| WebRTC管理 | WebRTCManager | ✅ 466行 - 音视频/数据通道 |
| Signal管理 | SignalProtocolManager | ✅ 431行 - 会话管理/密钥轮转 |
| 消息管理 | MessageManager | ✅ 294行 - 发送/接收/存储 |
| 图像处理 | ImageProcessor | ✅ 499行 - 压缩/格式转换/滤镜 |
| 图像缓存 | ImageCacheManager | ✅ 394行 - 三级缓存系统 |
| 图像存储 | ImageStorageManager | ✅ 417行 - 文件管理/元数据 |
| UI集成 | SwiftUI视图 | 🚧 进行中 - P2PChatView等 |
核心模块 (Swift Package Manager) ⭐最新:
- CoreDID: DID生成,文档创建,数字签名验证 (100%)
DIDManager.swift(236行): did:key生成, Ed25519签名
- CoreE2EE: Signal Protocol完整实现
E2EESessionManager.swift(203行): X3DH + Double Ratchet- P256曲线密钥协议
- 预密钥包系统 (100个一次性预密钥)
- 会话持久化 (UserDefaults)
- 安全码验证 (Safety Numbers)
- CoreP2P: P2P网络框架
WebRTCManager.swift(466行): DataChannel/音视频通道P2PManager.swift(596行): 连接管理/状态同步SignalProtocolManager.swift(431行): 会话管理/密钥轮转MessageManager.swift(294行): 消息发送/接收/存储
图像处理系统 ⭐新增 (v0.2.0):
ImageProcessor.swift(499行): 压缩, 格式转换, 滤镜ImageCacheManager.swift(394行): 三级缓存 (内存/磁盘/远程)ImageStorageManager.swift(417行): 文件管理, 元数据
UI模块 (SwiftUI):
- ✅ P2PChatView (452行): P2P聊天界面
- ✅ ConversationListView (137行): 会话列表
- ✅ 设置界面 (DID显示, PIN修改)
代码规模: ~8,500行 P2P/E2EE代码
Android (60%完成, v0.4.1) ⭐最新:
| 组件 | 技术选择 | 实现状态 |
|---|---|---|
| DID实现 | core-did模块 | ✅ 100% - DID管理 |
| E2EE | Signal Protocol | ✅ 100% - 完整实现 (~8,195行) ⭐ |
| 密钥交换 | X3DH | ✅ 密钥协商 + 预密钥包 |
| 双棘轮 | Double Ratchet | ✅ 前向安全 + 密钥轮转 |
| 会话管理 | PersistentSessionManager | ✅ 持久化 + 密钥导出/导入 |
| 身份验证 | SafetyNumbers | ✅ 60位安全码 + 会话指纹 |
| 高级E2EE | ReadReceiptManager | ✅ 已读回执加密 + 批量确认 |
| MessageRecallManager | ✅ 消息撤回 + 灵活策略 | |
| MessageQueueManager | ✅ 离线消息队列 + 优先级 | |
| GroupEncryptionManager | ✅ 群组加密接口 + AES-256 | |
| P2P网络 | core-p2p模块 | ✅ 100% - WebRTC连接 |
| WebRTC | WebRTC 120.0.0 | ✅ STUN/TURN配置 |
| P2P UI | feature-p2p模块 | ✅ 100% - 8个完整界面 ⭐ |
| 设备发现 | DeviceListScreen | ✅ NSD实时扫描 + 配对状态 |
| 设备配对 | DevicePairingScreen | ✅ 5阶段流程 + QR扫描 |
| 安全验证 | SafetyNumbersScreen | ✅ 60位数字 + QR扫描 |
| 会话指纹 | SessionFingerprintDisplay | ✅ 色块可视化 |
| DID管理 | DIDManagementScreen | ✅ 导出 + 分享 |
| P2P聊天 | P2PChatScreen | ✅ 端到端加密聊天 |
| QR扫描 | QRCodeScannerScreen | ✅ CameraX集成 |
| 消息队列 | MessageQueueScreen | ✅ 离线消息管理 |
核心模块 (Kotlin + Hilt DI):
- core-e2ee:
- SignalProtocolManager (X3DH + Double Ratchet)
- EncryptedReceiptManager (加密回执)
- MessageRecallManager (消息撤回)
- SessionSyncManager (会话同步)
- GroupEncryptionHelper (群组加密)
- KeyRotationManager (密钥轮换)
- BackupManager (密钥备份)
- core-p2p:
- WebRTCManager (点对点连接)
- SignalingClient (WebSocket信令)
- STUNServerConfig (NAT穿透)
- core-did:
- DIDManager (身份管理)
- DIDRepository (持久化)
UI导航集成 (Jetpack Compose):
- ✅ 主导航图集成 (commit edc563d0)
- ✅ 嵌套P2P导航图
- ✅ Material 3 设计规范
- ✅ 完整的用户交互流程
性能优化 (commit 34359aa6):
- ✅ buildSrc统一依赖管理 (6,228行)
- ✅ Gradle并行构建 + G1GC
- ✅ 构建速度提升 30-50%
- ✅ 启动速度提升 20-40%
- ✅ APK大小减小 15-25%
- ✅ 数据库WAL模式 (+60%性能)
测试覆盖 (64+ test cases):
- ✅ Unit tests (ViewModels, repositories, crypto)
- ✅ Integration tests (Database, E2EE protocols)
- ✅ UI tests (P2P flows, authentication, chat)
- ✅ Performance tests (Database benchmarks)
代码规模: ~15,000行 P2P/E2EE代码
跨平台对比:
| 特性 | 桌面端 | iOS | Android |
|---|---|---|---|
| DID身份 | ✅ 100% | ✅ 100% | ✅ 100% |
| Signal E2EE | ✅ 100% | ✅ 100% | ✅ 100% |
| WebRTC P2P | ✅ 100% | 🚧 30% | ✅ 100% |
| P2P UI | ✅ 100% | 🚧 10% | ✅ 100% |
| 音视频通话 | ✅ 100% | ❌ 0% | 🚧 50% |
| 文件传输 | ✅ 100% | ❌ 0% | 🚧 30% |
| 群组加密 | ✅ 100% | ❌ 0% | ✅ 100% |
| 离线消息 | ✅ 100% | 🚧 50% | ✅ 100% |
架构一致性:
- ✅ 统一的DID did:key格式 (Ed25519)
- ✅ 相同的Signal Protocol实现
- ✅ 兼容的消息加密格式
- 🚧 P2P协议对齐 (iOS WebRTC集成中)
2.2.10 社区/频道跨机同步 — Phase A 实战架构 (2026-05-07)
这一节描述真实落地的端到端数据流。在 Phase A 之前文档承诺了去中心化社交但实测两台机器互不通气;Phase A 把 send / receive / dispatch 三段都接上之后,频道消息能真正跨机送达。
数据流(A 在频道发消息 → B 看到)
[A 进程]
渲染层 ChatPanel.send → ipcRenderer.invoke('channel:send-message', {channelId, content, ...})
↓
community-ipc.js: ipcMain.handle('channel:send-message')
├─ channelManager.sendMessage(...) # 写 A 的本地 channel_messages
└─ gossipProtocol.broadcast(communityId, # 走 gossip 广播
{type:'channel_message', channelId, message})
↓
gossipProtocol.forward(peerId, gossipMessage)
↓
p2pManager.sendMessage(peerId, # 把 typed envelope 包好
{type:'gossip:message', protocol, data:gossipMessage})
↓
encodeWireMessage() # JS 对象 → JSON-line UTF-8 Uint8Array
↓
stream.send(payload) # libp2p 3.x stream API
⇊ wire (TCP/WebSocket/WebRTC, Noise 加密)
[B 进程] /chainlesschain/message/1.0.0 protocol handler
↓ (P2PManager.initialize 期间通过 registerMessageHandler() 注册)
for await of stream → Buffer.concat
↓
decodeWireMessage() # bytes → 优先 JSON-line, 失败回 raw
↓
dispatchTypedMessage(this, parsed, fromPeerId)
# 按 type 分发: gossip:message / gossip:subscribe / gossip:unsubscribe / message:call-* / message:typed
↓
emit('gossip:message', {…, fromPeerId})
↓
gossipProtocol.handleIncomingMessage(data)
├─ LRU 去重 + TTL 检查 + 订阅过滤
└─ emit('message:received', {communityId, payload, sender, …})
↓
social-initializer:gossipReceiver # bootstrap 时注册
↓
if payload.type === 'channel_message':
channelManager.handleMessageReceived(channelId, message)
└─ INSERT OR IGNORE channel_messages WHERE id = ? (idempotent)
└─ emit('channel:message-received') → renderer 增量刷新关键设计决定
| 决定 | 原因 |
|---|---|
| JSON-line 而非 Protobuf | 调试友好;社区消息频次低(人类输入级),不在带宽热点;encodeWireMessage 仍兼容 raw bytes 通道(加密消息走单独的 /encrypted-message/1.0.0) |
dispatch 按 type 字符串硬编码 | 4 类已知 type(gossip:* / call-* / 通用 typed / raw)覆盖了现有所有 in-process 调用方;新加协议时显式在 dispatchTypedMessage 里加分支,避免 generic registry 带来的隐式耦合 |
保留 message:received 兜底 | 老监听者(friend / device-broadcast 等专属 protocol 走自己的 handler,但兜底事件不破坏向后兼容)持续工作 |
gossipReceiver 写在 social-initializer | 而不是写进 ChannelManager 自己——避免 ChannelManager 反向依赖 gossipProtocol;保持 manager 层"被动接收"语义;DI 容器统一管理依赖图 |
| 去重在 ChannelManager 而非 GossipProtocol | LRU dedup 解决"同条消息被多次 forward";DB-level INSERT OR IGNORE 解决"同一进程重启后再收一遍"和"多 peer 同时转发同一条" |
已知边界
- 只 wire 了
channel_message:governance 提案 / moderation 上报 / 频道创建/删除 这些都不走 gossip,只在创建方本地起作用;如果 B 要看到 A 创建的新频道还得手动刷新或等 community sync 拉一遍 - 不验证签名:当前 wire 上的
message.sender_did是任何人都能伪造的字段;MTC 联邦(Phase B 规划)会接管签名 + Merkle 包装 - fanout = 3:默认每条消息 forward 到 3 个 peer;社区规模 > ~20 后需要重新评估(gossip mesh 形态会显著影响延迟与冗余)
- mDNS 仅本地段:跨子网发现仍依赖 DHT + bootstrap nodes(公网回到中心化引导)
测试金字塔
- 单元 18 —
p2p-manager-dispatch.test.js覆盖 encode/decode/dispatch helper - 集成 4 —
gossip-channel-receiver.integration.test.js真 GossipProtocol + 真 ChannelManager + mock libp2p - e2e 3 —
p2p-gossip-roundtrip.test.js双真 libp2p 节点,TCP + noise + yamux + identify - 详见根
README.md同日条目"P2P 去中心化社交跨机同步真正打通"
2.2.11 MTC 联邦双轨同步 — Phase B v1 (2026-05-07)
在 §2.2.10 Phase A 直连 gossip 之上加第二条同步通道:MTC 联邦 gossipsub。两条路并存,本地 INSERT OR IGNORE 去重。Phase B v1 只到 transport 层;DID 签名 / Merkle 批 / M-of-N 留 B4 sub-phase。
双轨数据流
[A 进程] channel:send-message IPC
├─ channelManager.sendMessage # 写 A 的本地表
└─ 双发:
├─ gossipProtocol.broadcast # Phase A 直连 gossip
│ → /chainlesschain/message/1.0.0 protocol stream
│ → encodeWireMessage JSON-line bytes
│
└─ mtcFederationManager.publishCommunityEvent
→ topic = "cc.community.<communityId>.events"
→ core-mtc Libp2pTransport.publishRaw
→ @chainsafe/libp2p-gossipsub mesh forward
[B 进程] 双轨接收
Phase A 路径:
/chainlesschain/message/1.0.0 handler
→ decodeWireMessage + dispatchTypedMessage
→ emit('gossip:message') → gossipProtocol.handleIncomingMessage
→ emit('message:received') → social-initializer:gossipReceiver
→ channelManager.handleMessageReceived(channelId, message)
Phase B 路径:
gossipsub message event
→ Libp2pTransport._dispatchRawTopic
→ MtcFederationManager 的 wrapped handler (JSON parse)
→ community-ipc inline closure (subscribeCommunity 时注册)
→ channelManager.handleMessageReceived(channelId, message)
最后一公里 (两路汇合):
INSERT OR IGNORE INTO channel_messages WHERE id = ?
→ 同 message.id 只插一行 (idempotent)
→ emit('channel:message-received') → renderer 增量刷新关键设计决定 (Phase B v1)
| 决定 | 选 | 理由 |
|---|---|---|
| libp2p node 独立 vs 复用 P2PManager | 独立(自带 libp2p 节点) | 复用要侵入 P2PManager.createLibp2p 配置加 pubsub: gossipsub(),影响范围大;独立节点是干净 transport,未来再优化为复用 |
| Topic 命名 | cc.community.<communityId>.events 平铺 | v1 一个社区 ≈ 一个隐式联邦(1-of-N 任何 member 可发,所有 member 接收);B4 加 governance 后改为按联邦 + per-event-type |
| Wire 格式 | JSON UTF-8(与 Phase A 同) | 调试友好;B4 加 typed envelope {kind, issuer, issuedAt, sig, payload} 时可以渐进切 |
| 失败语义 | MTC fed 初始化失败 → required:false 容错降级,社区功能 fallback Phase A 直连 | 双轨的核心好处之一就是 graceful degradation;任何一条路单点故障不致命 |
| 自动 peer 发现 | 不做 | v1 走 connectPeer(multiaddrStr) 手动桥接;生产 ground-truth 是 Phase A 已经能跑了,Phase B 是审计增强 |
| 接收去重 | 复用 channelManager.handleMessageReceived 的 INSERT OR IGNORE | 同条消息双轨到达 → 第二次 INSERT 被 IGNORE,DB 单行 |
已知边界 (v1)
- 没签名:
message.sender_did仍是任意可伪造字段,Noise transport 加密只防 MITM 不防发送者声明造假;B4 加 DID 签名后由 channelManager 在 INSERT 前验证 - 没 Merkle finality:v1 单条 publish,没批 envelope;B4 加
assembleBatch持久化 envelopes 到磁盘 (类似 audit-mtc 的~/.chainlesschain/audit-mtc/batches/<batch-id>/) - 没 M-of-N:v1 单签发布;B4 加
assembleBatchFederated用于 governance-critical events(提案/投票/罢免) - 没自动 peer bridging:v1 用户必须手动喂 multiaddr 给 mtcFedMgr;B4 用 P2PManager.on('peer:connected') 自动 dial 对应 MTC port
- 没跨联邦信任锚:v1 一个社区 = 一个独立 federation;MTC v0.11 cross-fed-trust-create 已实现 CLI 端,desktop 端没接入
测试金字塔 (Phase B v1)
- 单元 17 —
mtc/__tests__/mtc-federation-manager.test.js用 mock transport 覆盖全部 lifecycle / publish / subscribe / 失败语义 - 集成 12 —
social/__tests__/community-ipc-dual-track.integration.test.js真 community-ipc + mock gossipProtocol + mock mtcFederationManager,验证双发 / 双订 / 任一失败不阻塞 / 单管道 fallback - e2e 4 —
mtc/__tests__/mtc-federation-roundtrip.test.js双真 libp2p gossipsub 节点(call path + conditional delivery + dual-track 幂等) - 全 22 文件 891/891 ✅
2.2.12 B4 — DID 签名 channel 消息 + auto peer bridging (2026-05-07)
在 §2.2.11 双轨同步基础上加两件事:(a) channel 消息走 Ed25519 签名 + receiver 三重验证,关掉之前
sender_did任何 peer 都能伪造的 spoofing window;(b) Phase B v1 留的 follow-up——MtcFederationManager 跑独立 libp2p 节点,需要 P2PManager 帮忙告知对端 MTC 多址才能形成 mesh,这里自动桥接。
B4a — 签名格式
Wire envelope 新增字段:
sender_pubkey:base64 of 32-byte Ed25519 public key(直接嵌入消息,无 DID resolver 依赖)signature:base64 of 64-byte detached signature
签名输入:immutable subset 的 minimal-deterministic JSON:
{
"id": "<uuid>",
"channel_id": "<chan-id>",
"sender_did": "did:chainlesschain:<40-hex>",
"content": "<text>",
"message_type": "text" | "image" | "file" | "system",
"reply_to": "<msg-id>" | null,
"created_at": <unix-millis>
}故意不签的字段:is_pinned / reactions / updated_at——这些是后续可变状态,签了以后改不了。
Canonicalize 实现选择:自家 15 LoC 的 minimal deterministic JSON(key sort + JSON.stringify 转义),不引入 canonicalize npm 包(它是 core-mtc 的 transitive dep,desktop-app-vue 已不是 workspace 看不到)。模块约束输入只能是 flat 对象,nested 直接 throw 防误用。
B4a — 三重验证
receiver channel-manager.handleMessageReceived 收到带签名消息时按顺序:
- DID 一致性:
computeDID(pubkey) === sender_did(防 pubkey/DID 错配——攻击者声明 alice DID 但塞自己 pubkey 这种) - Ed25519 签名:
nacl.sign.detached.verify(canonical(subset), sig, pubkey) - 形状合法:sig 长度=64 / pubkey 长度=32 / base64 解码不抛
任一失败 → 丢消息 + emit channel:message-rejected 事件 + log warn。
三态向后兼容:
| envelope 形态 | 行为 |
|---|---|
sender_pubkey + signature 都有 | strict verify,失败丢 |
| 都缺 | log warn + 接受(migration window:老客户端兼容) |
| 只一个有 | reject as malformed |
未来可以 flip 到 strict-only(拒所有 unsigned);当前 v1 保 backward-compat。
B4a — Schema migration
避开项目历史 fragmented numbered-SQL migration runner(005/006/009 有重复,无清晰 runner)。新策略:
initializeTables()的 CREATE 加新列(新装机)- 在 CREATE 后跑 PRAGMA
table_info('channel_messages'),遍历返回 rows,缺哪列就 ALTER ADD(升级既存 DB) - 失败的 ALTER swallow 不致命(log warn),最坏情况向后退化为 unsigned legacy
auto peer bridging — 桥接机制
问题:MtcFederationManager 跑独立 libp2p 节点(与 P2PManager 不同 port),双方互不知道对方的 MTC 多址。手动 connectPeer(maddr) 在生产里没人填,导致 Phase B 通道空跑。
方案:复用 Phase A 的 /chainlesschain/message/1.0.0 信道发一个 typed message:
{
type: "mtc:advertise",
peerId: "<MTC libp2p peer id>",
multiaddrs: ["/ip4/.../tcp/9100/p2p/...", ...]
}p2p-manager.dispatchTypedMessage 拿到 mtc:advertise → emit 'mtc:peer-advertise' 事件。
social-initializer.wireMtcAutoBridge(独立 export 便于测试):
- 出站:on
p2pManager.on('peer:connected', {peerId})→ 调mtcFedMgr.peerIdString() + multiaddrs()拼 envelope →p2pManager.sendMessage(peerId, envelope) - 入站:on
p2pManager.on('mtc:peer-advertise', {peerId, multiaddrs})→ 顺序mtcFedMgr.connectPeer(maddr)直到首个成功
双向:A 连 B 时两端都 emit peer:connected → 两端都 advertise → libp2p 自带 dial dedup。
容错矩阵:
| 状况 | 行为 |
|---|---|
mtcFedMgr.isInitialized() === false | 跳过(Phase A 直连仍工作) |
| MTC peerId 缺失 | 跳过 advertise |
| MTC multiaddrs 空 | 跳过 advertise |
sendMessage 失败 | log warn,不抛 |
| 所有 multiaddrs dial 失败 | log warn,下次 peer:connected 再试 |
| 收到 advertise 但 multiaddrs 缺 | 跳过 dial |
B4 已知边界
- 没自动 multiaddr 通告升级:A 的 MTC 节点重启后 multiaddr 改了,老 advertise 不会重发——下次 peer 重连时才重广播
- 没签名 mesh 限速:恶意 peer 可以猛发 unsigned legacy 消息把 receiver log 噪音化;当前 v1 不限速,未来可以 per-peer rate-limit
- 没 mtc:advertise 签名:自己也是 typed message,理论上可以伪造 advertise 让别人 dial 错地址。低风险(受害者就是浪费一个 dial timeout),但严格来讲 B4 之后的 governance/critical messages 都该签
测试金字塔 (B4 新增 47 / 全 938 / 938)
- 单元 22 —
did/__tests__/did-signer.test.js - 单元 +2 —
p2p/__tests__/p2p-manager-dispatch.test.js(+mtc:advertise派发) - 集成 8 —
social/__tests__/channel-manager-signing.integration.test.js - 集成 15 —
bootstrap/__tests__/mtc-auto-bridge.integration.test.js - 25 文件全绿,详见根
README.md同日条目"B4 — DID 签名 + auto peer bridging"
2.2.13 B4-merkle v1 — channel 事件 Merkle 批 envelope finality (2026-05-07)
在 §2.2.12 B4a 单条签名 + auto peer bridging 之上加最后一层:本机发出的每条 channel 消息进离线可验的 Merkle 批 envelope。从"消息可信"升级到"任何第三方都能拿密码学证据复核"。
数据流
[A 进程] channel:send-message IPC
├─ channelManager.sendMessage # 写本地 + B4a 签名 (sender_pubkey + signature)
├─ gossipProtocol.broadcast # Phase A 直连
├─ mtcFederationManager.publishCommunityEvent # Phase B v1 双轨
└─ channelEventBatcher.enqueueEvent(community_id, signedMessage) # B4-merkle v1 ← NEW
└─ 写 staging/<message-id>.json
└─ 若 stagedCount >= threshold: 触发 closeBatch
└─ _assembleBatchLocal(rawLeaves, keys, meta)
└─ leafHash + MerkleTree (核心 mtc primitives)
└─ tweetnacl Ed25519 sign tree-head (我方 DID 私钥)
└─ 写 batches/<batch-id>/{manifest,landmark,envelope-*}.json (atomic rename)
└─ rm staging/<message-id>.json (only after rename)
[A or 任何第三方有 batch dir 的进程] channel:get-message-envelope IPC
└─ channelEventBatcher.loadEnvelopeAndLandmark(community_id, message_id)
└─ 找 batch_id (扫 manifest)
└─ 读 envelope-<message-id>.json + landmark.json
└─ 返回 {found, envelope, landmark, treeHeadId, batchId, leafIndex}
↓ 给 renderer → 用户
↓ 或给外部 verifier (cc mtc verify)
↓ 验 inclusion proof + tree-head signature → "alice 在 X 时间确实发了 Y"文件系统布局
每个用户 + 每个社区独立子树:
<userData>/channel-mtc/
<communityId>/
staging/
<message-id>.json # B4a 签了的 channel_message 全量
batches/
000001/
manifest.json # schema=channel-batch-manifest/v1
landmark.json # MTC landmark (tree_head + signature + trust_anchors)
envelope-<msg-id>.json # 每条消息一份 inclusion_proof
000002/
...参考 audit-mtc 的 layout(CLI cc audit mtc 那套),共享 atomic-rename 模式 + 失败 rollback 设计。
关键设计决定
| 决定 | 选 | 理由 |
|---|---|---|
| Batcher 在哪里跑 | desktop main 进程,不是 CLI | 用户体验:发完消息就该有 envelope,不要靠 cron;renderer 通过 IPC 查询 |
| 谁签 tree-head | 本人 DID Ed25519 sign key | 一条 envelope = "我(owner of DID X)证明这批消息是我承认发出的";不需要联邦多签 |
| Wire format | core-mtc landmark/envelope schema 不动 | 跟 cc mtc verify / federation tooling 完全互通 |
| 加密库 | tweetnacl (绕开 @noble/curves 版本陷阱) | 详见下面 known gotcha |
| Trigger 模型 | threshold (100) OR timer (1h) OR explicit force | 适合 chat 场景:忙时按量切,闲时按时切,shutdown 时强制 close 不丢 |
| 跨机分发 | 不做 (v1 only-local) | follow-up sub-phase——v1 先把本地证据链跑通 |
| Verify 谁来做 | renderer 或外部 verifier | desktop 本机也能用 core-mtc primitives 验 |
已知 gotcha — @noble/curves 跨版本 subpath 删除
core-mtc 的 package.json 声明 @noble/curves: ^1.9.7,那一支有 "./ed25519" subpath export。但 desktop-app-vue 的 standalone node_modules(已不是 workspace)解析到的是 @noble/curves@2.2.0,v2 把 /ed25519 这个 subpath 删了。任何 require("@chainlesschain/core-mtc") 都会触发 index 文件 top-level 的 require("./signers/ed25519") → require("@noble/curves/ed25519") → 抛 Package subpath './ed25519' is not defined。整个 core-mtc index 加载失败。
修复策略:
- 不 require
@chainlesschain/core-mtc(avoid index 文件) - 直接 require subpath primitives:
/hash、/jcs、/merkle、/constants——这四个都不依赖 @noble/curves - 用
tweetnacl(已是 desktop-app-vue 直接 dep,跨版本稳定)写一个 minimal MTC signer 实现{ALG, signTreeHead, trustAnchorEntry}接口 - 自己实现一个
_assembleBatchLocal调用上述 primitives + tweetnacl signer——逻辑和 core-mtc/lib/batch.js 完全一致,输出 wire-compatible
为什么不直接 fix package.json 装 @noble/curves@^1:会跟 desktop-app-vue 现有 deps 起版本冲突;且 core-mtc 自家 ed25519 signer 也不是 hot path,desktop 这边走自己的更可控。配 memory desktop_release_npm_workspace_hoisting.md 收录又一种 manifestation。
已知边界 (v1)
- 本机 only:A 发出去的 envelope 只在 A 的盘上;B 收到 A 的 message 后没有 A 的 envelope。Cross-machine envelope 分发是 follow-up sub-phase(federation channel publish landmark + on-demand pull envelopes)
- 不批远端消息:远端 message 已经由对方 DID 签了,我们 batch 它没有 added value(除非要做 multi-witness——不在 v1 范围)
- threshold 100 + timer 1h 是默认:超大社区或低频小群可能要调;写死配置项时再说
- 不归档 OSS/IPFS:v1 落本地盘,磁盘满或卸载就丢;归档是 follow-up
测试金字塔 (B4-merkle 新增 31 / 全 969 / 969)
- 单元 23 —
mtc/__tests__/channel-event-batch.test.js(真 fs + 真 core-mtc primitives × 全 lifecycle + 文件系统 path-traversal 防护) - 集成 8 —
social/__tests__/community-ipc-merkle-enqueue.integration.test.js(IPC enqueue + envelope 查询 + 各种容错) - 27 文件全绿,详见根
README.md同日条目"B4-merkle v1 — channel 事件 Merkle batch envelope finality"
2.2.14 B4-cross v1 — envelope 跨机分发 (2026-05-07)
§2.2.13 B4-merkle v1 落地了 envelope 但只对发件人有用——对端没你的 batch dir,verify 跑不起来。本节补完最关键缺口:landmark 走 federation gossipsub 自动广播,envelope 按需 peer-pull,让"任何第三方都能验"成为运营事实。
双轨架构
[Alice] closeBatch()
├─ 写本地 batches/000001/{manifest,landmark,envelope-*}.json
└─ batcher.onBatchClosed callback 触发
↓
ChannelEnvelopeDistribution._publishLandmark
↓
mtcFederationManager.publishCommunityEvent
(synthetic communityId = `<id>.envelopes-track`)
↓ gossipsub topic `cc.community.<id>.envelopes-track`
[Bob] community:join(communityId)
├─ gossipProtocol.subscribe (Phase A)
├─ mtcFederationManager.subscribeCommunity(<id>) (Phase B v1, 消息流)
└─ channelEnvelopeDistribution.subscribeCommunity(<id>) ← B4-cross v1
↓ subscribes `<id>.envelopes-track` topic
↓ on inbound landmark:
↓ batcher.storeRemoteLandmark(communityId, treeHeadId, landmark)
↓ → 落 remote-landmarks/sha256_xxx.json
[Bob 想验 Alice 发的 messageId X]
channel:get-message-envelope(communityId, messageId) IPC
├─ batcher.loadEnvelopeAndLandmark → not found locally
└─ for peer in p2pManager.getConnectedPeers():
channelEnvelopeDistribution.requestEnvelope(peer, communityId, X)
↓ p2pManager.sendMessage(peer, {type:'mtc:envelope-request', ...})
↓ wire (Phase A `/chainlesschain/message/1.0.0`)
[Alice 收到 envelope-request]
↓ p2pManager dispatchTypedMessage → 'mtc:envelope-request' event
↓ ChannelEnvelopeDistribution._onEnvelopeRequest
↓ batcher.findEnvelope → 在自己 batches/ 里找到
↓ p2pManager.sendMessage(Bob, {type:'mtc:envelope-response', envelope, ...})
[Bob 收回包]
↓ dispatchTypedMessage → 'mtc:envelope-response' event
↓ resolve in-flight Promise + batcher.storeRemoteEnvelope
↓ → 落 remote-envelopes/messageId.json
↓ rerun batcher.loadEnvelopeAndLandmark
↓ 这次 found origin:'remote' + envelope + 之前缓存的 landmark
↓ 返回完整证据给 renderer
Renderer / cc mtc verify
↓ 验 Merkle inclusion_proof 对照 landmark.snapshots[0].tree_head.root_hash
↓ 验 landmark.snapshots[0].signature 对照 trust_anchors[0].pubkey_jwk
↓ ✅ "Alice 在 X 时间确实在 Y 频道发了 Z 内容"文件系统布局(接收端新增)
<userData>/channel-mtc/<communityId>/
staging/ # B4-merkle v1
batches/ # B4-merkle v1(本机 closed batches)
remote-landmarks/ # B4-cross v1 NEW(按 treeHeadId 索引)
sha256_<base64url>.json
remote-envelopes/ # B4-cross v1 NEW(按 messageId 索引)
<messageId>.json冒号路径转义:treeHeadId 是 sha256:<base64url> 形式,: 在 Windows 路径不安全 → 全局换 _。
Topic 命名
为了不污染 Phase B v1 的消息流 topic,envelope 自走一个 dedicated topic:
- 消息:
cc.community.<communityId>.events(Phase B v1) - envelope landmark:
cc.community.<communityId>.envelopes-track(B4-cross v1)
实现细节:MtcFederationManager v1 的 API 只暴露 subscribeCommunity(communityId, handler),不直接给 raw topic 接口。绕过的方法是 channelEnvelopeDistribution 内部把 communityId append .envelopes-track 当合成 id 喂给 mtcFedMgr,等价于走另一条 gossipsub topic。slight abuse 但避免泄露 transport 细节,未来 mtcFedMgr 加 subscribeRawTopic 可以无缝迁移。
Wire format
landmark 广播(gossipsub payload):
{
"type": "channel.landmark",
"communityId": "comm-X",
"batchId": "000001",
"treeHeadId": "sha256:base64url...",
"landmark": { /* core-mtc landmark */ },
"manifest": { /* core-mtc manifest */ },
"publishedAt": "ISO-timestamp"
}envelope 请求(typed message via Phase A protocol):
{
"type": "mtc:envelope-request",
"requestId": "req-<ts>-<seq>",
"communityId": "comm-X",
"messageId": "msg-Y"
}envelope 响应(typed message):
{
"type": "mtc:envelope-response",
"requestId": "req-<ts>-<seq>",
"communityId": "comm-X",
"messageId": "msg-Y",
"found": true,
"envelope": { /* core-mtc envelope */ },
"batchId": "000001"
}信任模型 (v1)
没有 inbound 校验——缓存所有 landmark/envelope,不按 trust_anchors 过滤。理由:
- Noise 防 MITM:transport 层加密保证 wire 内容没被篡改
- 最终验证关在 envelope 验证:用户最终调用
cc mtc verify或等价物时,必然用 landmark 的 tree-head signature 验签,所以 fake landmark 通不过
v2 加固方向:
- inbound landmark 的
trust_anchors[].issuer必须匹配 join 时声明的 federation membership(防 noise pollution) - 缓存 envelope 时 inline 验 inclusion proof(捕获损坏 envelope 不进 cache)
- per-peer rate-limit
mtc:envelope-request(防 DOS / amplification)
已知边界 (v1)
- 无 trust_anchors filter(见上)
- lazy peer-pull 单线程:当前
channel:get-message-envelope顺序询问 peer 直到首发命中。万人社区典型场景应该 1-2 跳就够,超大社区或对端集体失联时 worst-case =peers.length * timeoutMs(默认 8s × N)。可以加并行限速优化但 v1 没做 - envelope-request 没签名:恶意 peer 可以伪造 requestId 让本机 sendMessage 浪费 bandwidth。低风险(受害者最多浪费一个响应),未来加 signing
- 不归档:远端缓存的 envelope/landmark 跟本地 batches 一样落本地盘。设备 wipe / 重装 = 全丢。归档到 OSS/WebDAV/IPFS 是 follow-up
- 无 UI envelope viewer:renderer 端目前只有 IPC 暴露,没有"显示这条消息的密码学证据"按钮。是 follow-up
测试金字塔 (B4-cross 新增 34 / 全 1003 / 1003)
- 单元 +11 —
mtc/__tests__/channel-event-batch.test.js(cross-machine API 扩展:onBatchClosed、storeRemote*、findEnvelopeorigin字段) - 单元 21 —
mtc/__tests__/channel-envelope-distribution.test.js(distribution 完整覆盖:lifecycle、自动 publish、入站 landmark 缓存、requestEnvelope 全流程含 timeout / 错 requestId / close reject、入站 envelope-request 服务) - 单元 +2 —
p2p/__tests__/p2p-manager-dispatch.test.js(mtc:envelope-request/mtc:envelope-response字段透传) - 28 文件全绿。详见根
README.md同日条目"B4-cross v1 — envelope 跨机分发"
2.2.15 B4-cross-trust v1 — landmark inbound trust filter (2026-05-07)
§2.2.14 B4-cross v1 列在 deferred 第一项的事:v1 trust 模型 NONE,缓存所有入站 landmark。本次按 community 成员校验,不在 member list 的 issuer 一律 reject。是 v1 → v2 加固方向的第一步。
校验流程
[mtcFedMgr 收到 inbound landmark via gossipsub]
→ ChannelEnvelopeDistribution._handleIncomingLandmark(communityId, payload)
│
│ 1. payload.type === 'channel.landmark' && treeHeadId/landmark 存在?
│ no → silently drop
│ yes ↓
│
│ 2. trust filter installed? (getCommunityMembers != null)
│ no → 跳到 step 5(v1 trust-none,向后兼容)
│ yes ↓
│
│ 3. issuerDID = extractIssuerDID(landmark)
│ null → emit landmark:rejected{reason:"issuer DID not extractable"} + drop
│ ↓
│
│ 4. members = await getCommunityMembers(communityId)
│ throw → emit landmark:rejected{reason:"membership lookup failed"} + drop
│ issuerDID ∉ members → emit landmark:rejected{reason:"issuer not a community member"} + drop
│ issuerDID ∈ members ↓
│
│ 5. batcher.storeRemoteLandmark(communityId, treeHeadId, landmark)
└─ emit landmark:received{communityId, treeHeadId, batchId}extractIssuerDID — 容忍两种 issuer 格式
landmark.snapshots[0].signature.issuer === "did-bound:did:chainlesschain:abc"
↓ strip "did-bound:" 前缀
"did:chainlesschain:abc"
landmark.snapshots[0].signature.issuer === "did:chainlesschain:bare" // 兼容裸 DID
↓ pass-through
"did:chainlesschain:bare"为啥要兼容两种:
- channel-event-batch.js
_assembleBatchLocal用did-bound:<did>前缀(参考 audit-mtc 的命名) - CLI 工具 / 联邦发布者 / 其它实现可能直接用裸 DID
- 静态方法纯函数,单测直接断言
默认行为:trust-off 兼容
| getCommunityMembers 配置 | 行为 |
|---|---|
| undefined / null(默认) | 缓存所有入站 landmark;初始化时 logger.warn("trust filter OFF") 一次 |
| function | 入站 landmark 走完整校验链;初始化时 logger.info("trust filter ON") |
兼容性目的:测试 fixture / dev 启动 / 老调用方不会因为本 patch 突然 break。生产路径 social-initializer 强制注入 communityManager.getMembers 适配器。
已知边界 (v1) — 后续加固方向
- 单签 landmark only:
extractIssuerDID只看snapshots[0],B4-merkle v1 单签足够;M-of-N 联邦 multi-sig 落地后需要校验所有 signatures,每个 signer 都得是 member - 不验签:v1 只比 issuer DID 字符串。如果攻击者拿到一个真实 member 的 DID 字符串但没私钥,他塞个 fake landmark 也能过。完整防御还得验
landmark.snapshots[0].signature.sig对照trust_anchors[0].pubkey_jwk—— 这是后续 sub-phase - member list snapshot:每次 inbound 都查 DB,对高频 landmark 可能 hot path;可以加 LRU cache(per community + TTL)
- 没 federation governance 接入:当前 member = community member,未来联邦自治后应该是 federation member(可以是 community member 子集 or 超集)
测试金字塔 (B4-cross-trust 新增 8 / 全 1011 / 1011 across 28 文件)
- 单元 +8 → 29 —
mtc/__tests__/channel-envelope-distribution.test.js- trust filter ON:member 通过 / 非 member 拒 + reject event payload 验证 / 不可提取 issuer 拒 / DB throw 拒
- trust filter OFF:保留 v1 cache-all 行为
extractIssuerDID静态方法:with prefix / no prefix / malformed 三种 case
- 28 文件全绿 (B4 全套 + Phase A + Phase B v1 + B4-merkle 完整回归)
2.2.16 B4-ui v1 — Merkle envelope viewer (renderer 端) (2026-05-07)
§2.2.13 / §2.2.14 落了完整后端,但用户在 V6 chat panel 里看不到任何"这条消息有密码学证据"的可视入口。本次接通:每条带签名的消息加 "🔐 验证" 按钮 → 弹窗显示完整证据链 + 一键复制 raw JSON。
UI 流
[V6 CommunityDetailsDrawer 频道消息]
v-for="m in store.channelMessages"
↓ 渲染消息行
↓ if (m.signature && m.sender_pubkey)
↓ → 显示 "🔐 验证" 按钮(带 Tooltip)
↓ → click → openEnvelope(m.id, m.content) → set envelopeViewerOpen=true
[MessageEnvelopeViewer Modal 弹出]
watch [open, communityId, messageId] → useMessageEnvelope.fetch(...)
↓
useMessageEnvelope: state.value = {phase:'loading'}
↓ electronAPI.invoke('channel:get-message-envelope', communityId, messageId)
↓ ↓ community-ipc.js handler
↓ ↓ 1. local lookup (channelEventBatcher.loadEnvelopeAndLandmark)
↓ ↓ 2. lazy peer-pull (channelEnvelopeDistribution.requestEnvelope per peer)
↓ ↓ → {found, origin, envelope, landmark, treeHeadId, batchId, leafIndex}
↓ state.value = {phase:'found', result}
↓
Modal 显示 5 部分:
1. 消息预览 (m.content 截断)
2. 来源 Tag (local 绿 vs remote 蓝) + staging 橙 (尚未关闭批次)
3. Descriptions: treeHeadId / batchId / namespace / leafIndex (copy-able)
4. 签名状态 ✅ + Tooltip (引导用户去 cc mtc verify)
5. "展开 raw JSON" 折叠区:envelope + landmark 完整 JSON + 复制按钮5-phase reactive state design
type EnvelopeState =
| { phase: 'idle' } // 初始
| { phase: 'loading' } // IPC 在飞
| { phase: 'found'; result: { /* normalized envelope */ } } // 命中
| { phase: 'not-found'; reason?: string } // 后端确认无证据
| { phase: 'error'; message: string }; // IPC throw / preload 缺失每种 phase 对应一种 UI 渲染(Spin / Alert/warning / Alert/error / Descriptions)。test 直接断言 state.value.phase + 守卫读 if-narrowing 防 TS。
设计决定
| 决定 | 选 | 理由 |
|---|---|---|
复用 electronAPI.invoke 通用接口 | yes | community store 已经全用这套,preload 不用改;新 IPC channel 自动可达 |
| Composable 不 watch | yes | caller 自管 fetch() 时机,避免 prop 变化的隐式 re-fetch;测试简单 |
| Viewer 不放 ChatPanel.vue 而放 community/ | 结构对齐 | V6 chat 在 CommunityDetailsDrawer,ChatPanel.vue 是 V5 老路径 |
按钮仅在 signature && sender_pubkey 都有时显示 | yes | 老消息(B4a 之前)没签,按钮显示也没结果;UX 干净 |
| Raw JSON 折叠 + 复制按钮 | yes | 一键导出给 cc mtc verify 离线复核——这是整个证据链的"逃生口" |
| 没单测 viewer 组件本身 | deliberate | Ant Design Modal/Teleport 在 jsdom 下脆弱 + 没业务分支;composable + IPC 已测试覆盖;e2e Playwright 那批一起加 |
Wire format(renderer ↔ main)
renderer 调用:
electronAPI.invoke('channel:get-message-envelope', communityId, messageId)main 返回(community-ipc.js handler):
{
found: boolean,
origin?: 'local' | 'remote',
envelope?: object, // core-mtc envelope schema
landmark?: object | null, // core-mtc landmark; null when remote envelope arrived without matching landmark cached
treeHeadId?: string, // "sha256:base64url"
namespace?: string, // "mtc/v1/channel/<community>/<batch>"
batchId?: string, // "000001"
leafIndex?: number,
staging?: boolean, // true if message is in staging/ but batch not yet closed
reason?: string // present when found=false (e.g. "not found locally or among peers")
}已知边界 (v1)
- 没 viewer 单测:见 coverage 缺口段;与"先 ship 再补 e2e"权衡
- 不 polling staging → closed:用户在 staging 期间打开 viewer 看到
staging: true,不会自动等 batch close 后刷新;用户手动重开 - 未连 peer 时返回 not-found:lazy peer-pull 没 peer 可问就直接报 not-found;UI 没区分 "真没证据" vs "没 peer 在线"
- 没多语言:当前文案中文写死,B4-ui 后续 sub-phase 加 i18n
- viewer 没 dark mode 单独样式:用 antd CSS 变量自动跟随主题(
--ant-color-bg-container/--ant-color-fill-alter),实测 dark theme 下不丑但没专门优化
测试 (B4-ui 新增 10 / 全 1021 / 1021 across 29 文件)
- 单元 10 —
composables/__tests__/useMessageEnvelope.test.ts:5 phase × electronAPI 各形态全覆盖 - 29 文件全绿(前 28 + 新 composables 1)
2.2.17 B4-archive v1 — envelope 外部归档 (2026-05-07)
§2.2.13 / §2.2.14 / §2.2.15 / §2.2.16 都在维护本机盘上的 envelope 链;设备 wipe / 卸载 / 磁盘损坏一发生 = 全丢。本节加入 zip 打包 + provider 推送,让 audit 链可以存活在本机生命周期之外。
数据流
[本机 channel-mtc 维护中]
~/.chainlesschain/channel-mtc/<communityId>/
batches/000001/{manifest,landmark,envelope-*}.json # 本人发出 (B4-merkle)
remote-landmarks/<treeHeadId>.json # 别人发出 (B4-cross 缓存)
remote-envelopes/<messageId>.json # 拉来验过的
[手动 / 定时触发 archive]
channel-archive:push(communityId, {kind:'webdav', baseUrl, ...})
↓
archiver.pack(communityId, {sinceBatchId})
↓ AdmZip 把上面三类内容 + MANIFEST.json 拼成 zip Buffer
↓
archiveProviderFactory({kind:'webdav', ...}) → webdavProvider
↓
provider.putFile(`<communityId>/<archiveName>.zip`, buffer)
↓ ↓ webdavClient.putFile(含 retry / etag)
↓ → 远端:https://nas.example/dav/cc-archives/<communityId>/<archiveName>.zip
[设备 reinstall / 用户在新机器上恢复]
channel-archive:list(communityId, {kind:'webdav', ...})
↓ → ['archive-1.zip', 'archive-2.zip', ...]
channel-archive:restore(communityId, 'archive-2.zip', {kind:'webdav', ...})
↓
archiver.restore(provider, communityId, archiveName)
↓ provider.getFile → Buffer
↓ AdmZip 解压
↓ for each entry: 写到 root/<archivePath>,已存在跳过 (idempotent)
↓ → 本地 channel-mtc 重建
↓ → 之后 channel:get-message-envelope 能命中 origin:'local' 历史证据Archive 命名
channel-mtc-<communityId>-<isoTimestamp>-<sinceBatchId>-to-<latestBatchId>.zip例:channel-mtc-comm-engineering-2026-05-07T08-30-00.000Z-000005-to-000012.zip
存到 <remoteRoot>/<communityId>/<archiveName>.zip,多社区不互相污染。
Provider 接口契约
interface ArchiveProvider {
putFile(remotePath: string, buffer: Buffer): Promise<{ok: boolean, ...}>;
getFile(remotePath: string): Promise<Buffer>;
listFiles(remoteDir: string): Promise<string[]>;
}任何实现这三个方法的对象都能当 provider。v1 内置:
| 实现 | 适用场景 |
|---|---|
filesystemProvider({rootDir}) | Syncthing / Resilio Sync 同步文件夹、USB stick 备份、本机另一块盘、CI artifact |
webdavProvider(webdavClient) | NAS(Synology / 群晖 / TrueNAS)、Nextcloud、ownCloud、阿里云盘 (经过 webdav 网关) |
未来 follow-up(不在 v1):OSS(aliyun/aws s3)、IPFS pin services、Git LFS。
增量归档
pack({sinceBatchId}) 过滤 batches/<id>/ 只包含 id > sinceBatchId 的,配合调用方维护"最后归档到哪个 batch"高水位。万级别消息的社区一晚跑一次增量 push,稳。
已知 v1 限制
- 手动触发 only:v1 不在 main 进程里做 cron,每次通过 IPC 调。原因:默认开 cron 容易在用户没配 provider 凭证时反复失败刷 log,反而是噪音。Follow-up 加 SettingsPanel 提供"开启自动归档 + 间隔"
- OSS / IPFS / Git 没接:provider 接口干净,加这些只要写 ~50 LoC adapter,不影响 archiver 本体
- 凭证不持久化:webdav password 每次 IPC 调用从 renderer 传来。Follow-up 应该接 keytar / Electron safeStorage 让用户存一次后免每次输
- 没差量 (delta) 内容:当前一个 batch 全部 ship,没做"只 ship batches/
<id>/manifest.json + 已变化 envelope" 这种 chunk-level 增量。审计批次本身不可变,所以全 batch ship 不浪费 - adm-zip 路径净化导致 traversal 测试受限:adm-zip 的
addFile()会 strip..、绝对路径、反斜杠等,所以"恶意 zip 写出"的 path-traversal 攻击向量在 adm-zip API 内做不出来。restore() 里的archivePath.includes("..") || isAbsolute()防御代码仍然有用——防御的是用其它工具(zip(1) / archiver / 手写 zip parser)造出来的真实恶意包。这层用例只能 e2e 真包测,单测覆盖不到
测试金字塔 (B4-archive 新增 26 / 全 1047 / 1047 across 30 文件)
- 单元 26 —
mtc/__tests__/channel-envelope-archiver.test.js- constructor / pack(3 组)/ filesystemProvider(5 组)/ push+restore round-trip(5 组)/ webdavProvider 适配器(6 组)/ provider 失败传播
- 钉
// @vitest-environment node:adm-zip Buffer round-trip 在 jsdom 下 0 entries(Buffer realm 错位,与 libp2p e2e 同根)
- 30 文件全绿
2.2.18 B4-mofn v1 — governance M-of-N 多签 (2026-05-07)
Phase 54
cc governance一直是单 DID 投票,到 finalize 没有"M 个共同签名作证"的概念。本节接 core-mtcassembleBatchFederated加 M-of-N,关键 governance 事件(提案 / 投票 / role-change)能拿到 N 个 trust_anchors + ≥M signatures 的 multi-sig landmark 作为 audit 物证。
数据流 (3-of-5 happy path)
[admin / proposer] governance-mofn:create IPC
↓
GovernanceMultiSig.createProposal({communityId:'comm-X', proposalId:'rule-1',
payload:{kind:'rule_change', body:'...'}, members:[did-A, did-B, did-C, did-D, did-E],
threshold:3})
↓
写 <userData>/governance-mofn/comm-X/rule-1/proposal.json (含 members + threshold)
+ mkdir signatures/
[member A signs] (renderer 走 governance-mofn:sign IPC)
↓ 传 {did:A, secretKey, publicKey} 序列化 base64
↓ GovernanceMultiSig.addSignature
↓ 1. 校验 DID 在 proposal.members
↓ 2. 校验 sha256(pubkey).slice(0,20).toString('hex') === did 后缀(防伪)
↓ 3. 写 signatures/did_chainlesschain_<hex>.json
↓ → status: {collected:1, complete:false, ...}
[member B signs / C signs] (重复,每签一次 collected++)
↓ collected==3 → complete:true
[anyone calls finalize]
↓ GovernanceMultiSig.finalize
↓ 1. 收齐 sigs (取前 M=3 个 deterministic)
↓ 2. _assembleBatchFederatedLocal(rawLeaves, signers, {threshold:3, namespace, issuer})
↓ ↓ core-mtc primitives (hash/jcs/merkle/constants) + tweetnacl signer
↓ ↓ 每个 signer 在 tree-head 上签
↓ ↓ landmark.snapshots[0] = {tree_head_id, signatures:[3 sigs], threshold:3}
↓ ↓ landmark.trust_anchors = [3 trust anchors with pubkey_jwk]
↓ 3. 写 landmark.json + 标记 proposal._finalized=true
↓ → result: {ok, treeHeadId, threshold:3, signers:[did], landmarkPath}
[anyone verifies later]
读 landmark.json → 用 core-mtc verifier 验 ≥ threshold 个 signatures 对照 trust_anchors[].pubkey_jwk → ✅文件系统布局
<userData>/governance-mofn/<communityId>/<proposalId>/
proposal.json # {schema, communityId, proposalId, payload, members[], threshold, createdAt, _finalized?, _treeHeadId?, _signers?, _finalizedAt?}
signatures/
did_chainlesschain_<hex>.json # 一个 member 一份 (DID `:` → `_` 文件名安全)
landmark.json # 只 finalize 后写关键设计决定
| 决定 | 选 | 理由 |
|---|---|---|
| addSignature 校验 DID ↔ pubkey | yes | 防止 "我说我是 alice 但塞自己 pubkey" 攻击;与 B4a / B4-cross-trust 相同精神 |
| 同 DID 重复 add = no-op | yes (idempotent) | renderer 抖动重复调不会污染 |
| finalize 用前 M 个 sigs (deterministic by file order) | yes | 多签人都齐时可重现;不等齐才 finalize 避免 "谁先到谁的优先级高" 不确定性 |
| key material 落本地 | v1 yes (单机 demo) | v2 改 wire — 只持久 (proposal, signerDID, treeHeadSig),secret key 永远不离 owner 机器 |
| federated assembler 自家实现 | yes | core-mtc index 因 @noble/curves 加载失败,沿用 channel-event-batch / channel-envelope-archiver 已建立的"绕开 index 用 subpath primitives" 模式 |
| 不接 cc governance v1 路径 | yes | Phase 54 是单 DID 投票 + status 转换;M-of-N 是叠加层。需要时 governance-engine.js 可以增加"调 governanceMultiSig.createProposal" 的 hook,但本 patch 不动 cc governance 的现有行为 |
已知边界 (v1)
- 单机 sig collection:v1 假设所有签名来自同机;真分布式 M-of-N 需要 wire (federation gossipsub 收集 partial sigs)。架构上接 ChannelEnvelopeDistribution 的 typed message channel 应该不难
- 不集成 cc governance UI:renderer 还没 UI 入口;governance-mofn:* 是 raw IPC,要等 next sub-phase 加按钮 / proposal builder
- 没 expiry / 撤销:finalize 不可逆;中途 member 想退出签名要靠手动删文件
- secret key 落盘是 v1 妥协:filesystem 加密由 SQLCipher / OS keystore 在更上层,本 v1 不集成(因为 GovernanceMultiSig 不依赖 SQLite)
测试金字塔 (B4-mofn 新增 24 / 全 1071 / 1071 across 31 文件)
- 单元 24 —
mtc/__tests__/governance-multisig.test.js- constructor (1) / createProposal (7:threshold 边界、空、dup、bad DID、unsafe id、existing) / addSignature (6:member-only、idempotent、non-member、DID-pubkey 不一致、key shape 错、threshold 触发 complete) / getStatus (2) / finalize (5:不足 throw、3-of-5 完整 + landmark 内容验证、超 threshold deterministic、idempotent 二次同 treeHeadId、post-finalize 拒签) / listProposals (2)
- 31 文件全绿,含 B4 全套 + Phase A/B/B4-merkle/B4-cross/B4-cross-trust/B4-ui/B4-archive 完整回归
2.2.19 B4-crossfed v1 — 跨联邦信任锚 (2026-05-07)
§2.2.15 B4-cross-trust v1 把 inbound landmark 校验锁在"必须是本社区 member"。本节扩展:用户可以 record "我也信任另一个 federation 的 trust anchors"——他们发的 landmark 也能过校验,不必加入对方 community。这是 Phase B v1 留下的最后一个 "MTC 联邦"-味的 follow-up。
数据流
[用户 admin 在本机配置]
cross-fed-trust:establish IPC
localCommunityId='comm-engineering',
{remoteCommunityId='comm-research', remoteMembers:[did-a, did-b], expiresAt?, note?}
↓
CrossFedTrust.establishTrust → 写 <userData>/cross-fed-trust/comm-engineering/comm-research.json
[Inbound landmark 到来]
ChannelEnvelopeDistribution._handleIncomingLandmark
↓ getCommunityMembers('comm-engineering')
↓ ↓ social-initializer 提供的 adapter:
↓ ↓ 1. communityManager.getMembers('comm-engineering') → [本社区 members]
↓ ↓ 2. crossFedTrust.getTrustedDIDs('comm-engineering') → [来自 'comm-research' 的 DIDs]
↓ ↓ 3. union → 完整可接受 DID 集合
↓ extractIssuerDID(landmark) ∈ 上述集合?
↓ yes → batcher.storeRemoteLandmark + emit landmark:received
↓ no → emit landmark:rejected文件系统布局
<userData>/cross-fed-trust/<localCommunityId>/<remoteCommunityId>.json
{
schema: 'cross-fed-trust-record/v1',
localCommunityId, remoteCommunityId, remoteMembers: [did],
issuedAt, expiresAt: ISO|null, note: string|null
}关键设计决定
| 决定 | 选 | 理由 |
|---|---|---|
| 跟 community membership 分开存 | yes | 不污染 community-members 表 / 也不修改 cc community 现有 schema |
| getTrustedDIDs 默认排除过期 | yes | trust 不该用过期数据;caller 想看历史用 listTrusted (不过滤) |
| union 在 social-initializer adapter 而不在 distribution 里 | yes | distribution 只关心 "give me trusted DIDs"——不关心来源是 local or cross-fed;扩展性好(未来加 OAUTH provider 等也只动 adapter) |
| revokeTrust idempotent return false 而不抛 | yes | renderer 调用时不需要先 list;try-revoke 是常见模式 |
| 不验证 remoteMembers 真的属于那个联邦 | v1 yes | v2 应该 validate via 远端 federation 的 governance landmark; v1 本质是 "用户信任声明" |
已知边界 (v1)
- 没自动同步 remote member 变化:用户填了
remoteMembers: [did-a, did-b],远端 community 后来加了 did-c,本机不知道——下次需要手动 establishTrust 更新(idempotent,覆盖原记录) - 不验证 cross-trust 双向性:comm-A 信任 comm-B 不要求 comm-B 也信任 comm-A——单向 trust。如果想 mutual,两边各自 establishTrust 一次
- 没 trust depth:A 信 B、B 信 C 不会让 A 默认信 C(chain of trust)。是否要 transitive trust 是个 governance 问题,v1 故意不做
- 没 UI:v1 raw IPC,UX 留 follow-up(与 web-shell parity 一起)
测试金字塔 (B4-crossfed 新增 16 / 全 1087 / 1087 across 32 文件)
- 单元 16 —
mtc/__tests__/cross-fed-trust.test.js- constructor (1)
- establishTrust (6):写 + 返回 / unsafe ids / 空 members / 错 DID / dup / 二次更新覆盖
- revokeTrust (2):删除 + 不存在 idempotent
- listTrusted (2):空 / 多记录
- getTrustedDIDs (5):union 跨记录 / 排除过期 / 包含未来 expiresAt / clock 注入 (2030 视角让 2025 过期) / 空 community
- 32 文件全绿(B4 全套 deferred 完成 + 前面 Phase A/B/B4 全回归)
B4 全 deferred 完成回顾 (2026-05-07)
§2.2.15 → §2.2.19 这五节涵盖 deferred 列表全部 5 项,配合 §2.2.10 → §2.2.14 的 B4 主线,完成了 P2P 社交从"消息可信 + 自动联网" → "跨机分发 envelope" → "trust 校验" → "UI 可见" → "外部归档" → "M-of-N 多签" → "跨联邦信任" 的完整 audit-grade 闭环。
剩下唯一外部依赖项:用户反馈"需要在 web-shell 版本可以看到这些功能"——B4 全套 IPC handlers 都通过 ipcMain.handle 注册(V5/V6 桌面壳直接可用),但 Phase 1.6 默认壳是 web-shell(/v6-preview → web-panel via WS),需要为这些 IPC 写对等的 WS topic handlers + web-panel UI。这是下一个 sub-phase(B4-webshell v1)。
2.2.20 B4-webshell v1 — 全 B4 套件 web-shell 可见 (2026-05-07)
跟 §2.2.10 → §2.2.19 收口的 follow-up。Phase 1.6 hard-flip 后默认壳是 web-shell(
caaddf530),但 B4 全套(envelope viewer / archive / governance-mofn / cross-fed-trust)只走ipcMain.handle——web-panel 用户看不见。本节补 13 个 WS topic + 依赖透传,让默认壳用户也能用全套。
Topic 命名 (mirror IPC)
| WS topic | 对应 ipcMain channel |
|---|---|
mtc.envelope.get | channel:get-message-envelope |
mtc.archive.push / .restore / .list | channel-archive:push / restore / list |
mtc.governance-mofn.create / .sign / .status / .finalize / .list | governance-mofn:create / sign / status / finalize / list |
mtc.cross-fed-trust.establish / .revoke / .list / .get-trusted-dids | cross-fed-trust:establish / revoke / list / get-trusted-dids |
跟 §2.2.10 的 mtc.audit-status / Phase 3b 的 sync.* / Phase 3c 的 notification.* 同 dotted naming convention。
数据流(web-shell 用户验某条消息)
[web-panel UI in default web-shell]
↓ ws.sendRaw({type:'mtc.envelope.get', communityId, messageId})
↓ wire (WebSocket)
↓
[main process ws-cli-loader → wsHandlers map]
↓ createEnvelopeGetHandler(opts) returns wrapped handler
↓
[handler 内部]
1. channelEventBatcher.loadEnvelopeAndLandmark(communityId, messageId) → 本地命中?
yes → return {success:true, ...local}
2. 否:channelEnvelopeDistribution.requestEnvelope(peer, communityId, messageId) for each connected peer
首发命中 → batcher.loadEnvelopeAndLandmark 二次拿(包含远端 cache 后的 origin:remote 完整 shape)
3. 全 peer 失败 → return {success:false, error:'envelope not found locally or among peers'}
[wrap]
ws-cli-loader 包成 {ok:true, result:{success:true, origin:'remote', envelope:{}, landmark:{}, treeHeadId:'sha256:...', batchId:'000007', leafIndex:3}}
↓ wire back
[web-panel UI]
反序列化 → 显示 verify modal(与 §2.2.16 V6 viewer 对等)依赖透传链
DI container instances → MainProcess.<this>.* → startWebShell({…six managers…})
→ web-shell-bootstrap → wsHandlers via createCommunityMtcHandlers({…opts…})6 个 manager(channelEventBatcher, channelEnvelopeDistribution, channelEnvelopeArchiver, archiveProviderFactory, governanceMultiSig, crossFedTrust)+ p2pManager 一并打包。任一 null → 对应 topic 返回 {success:false, error:"... not initialized"},不让 dispatcher 崩。
跟 IPC 路径的差异点
| 维度 | IPC (ipcMain.handle) | WS (createCommunityMtcHandlers) |
|---|---|---|
| 错误返回 | {ok:false, reason:...} (community-ipc 自家约定) | {success:false, error:...} (web-shell 通用) |
| sign 入参 | base64 字符串 → main revive | 一致(base64) |
| providerSpec | 对象 {kind, ...} | 一致 |
| 命名 | colon-separated channel:get-message-envelope | dotted mtc.envelope.get |
错误 envelope 形状差异是有意为之:IPC 沿用既有 community-ipc 风格,WS 沿用既有 mtc.audit-status / sync.* / notification.* 风格。Renderer/web-panel 两端各自映射处理。
已知 v1 限制
- 没 web-panel UI:13 个 WS topic 都暴露了,但 web-panel
packages/web-panel/src/views/还没实现 envelope viewer / archive 配置 / governance-mofn 提案 builder / cross-fed-trust 管理器界面。是 follow-up - renderer 不会自动选择 IPC vs WS:V5/V6 走 IPC,web-shell 走 WS——上层 store / composable 需要
useShellMode()双路径分叉(参考 Phase 3b/3c sync-settings 的useShellMode().isEmbeddedpattern)。Follow-up
测试金字塔 (B4-webshell 新增 19 / 全 1106 / 1106 across 33 文件)
- 单元 19 —
web-shell/__tests__/community-mtc-handlers.test.js- topic 注册 check (1):精确 13 个,名字一致
- envelope.get (4):local 命中、远端 fallback peer-pull 顺序、缺 batcher、缺 args
- archive.* (5):push / restore / list / 缺 factory / 缺 spec
- governance-mofn.* (5):create / sign 带 base64→Buffer revive + key 长度断言 / sign 缺 signerKeys / status finalize list 三件齐 / 缺 manager
- cross-fed-trust.* (4):establish 把 localCommunityId 单独 split / revoke + list + get-trusted-dids 三件齐 / 缺 manager
- 通用:sync throw 包成
{success:false, error}
- 33 文件全绿(B4 全套 + Phase A/B 完整回归)
B4 + web-shell parity 全完成回顾 (2026-05-07)
§2.2.10 → §2.2.20 这 11 节涵盖整个 P2P 社交跨机同步从"破壳查 bug"到"audit-grade 闭环 + 默认壳可见"的完整路径:
Phase A: cross-machine sync 真正打通 (7 bug 修)
→ Phase B v1: MTC federation 双轨同步
→ B4: DID 签名 + auto peer bridging
→ B4-merkle v1: Merkle 批 envelope finality
→ B4-cross v1: envelope 跨机分发 (gossipsub broadcast + on-demand pull)
→ B4-cross-trust v1: inbound landmark 按 community 成员校验
→ B4-ui v1: renderer 端 verify 按钮 + viewer 弹窗 (V6 桌面壳)
→ B4-archive v1: envelope 外部归档 (filesystem + WebDAV)
→ B4-mofn v1: M-of-N 多签 governance proposal
→ B4-crossfed v1: 跨联邦信任锚扩展 trust filter
→ B4-webshell v1: 全套 13 WS topic 暴露给 web-shell 默认壳10 commits, +273 测试 (149 → 1106 全绿 across 33 文件), 完整 audit-grade P2P 社交栈。
2.2.21 B4-webpanel v1 — web-panel UI 接全套 WS topic (2026-05-07)
§2.2.20 把 IPC → WS 桥接层铺好,但 web-panel 没 UI 入口——用户照样点不到。本节加 4 个 composable + 1 个 4-tab 页面
MtcAudit,把 envelope / archive / governance-mofn / cross-fed-trust 全部接到默认壳的 sidebar 上。
架构
┌─────────────── packages/web-panel ───────────────┐
│ views/MtcAudit.vue (4 tabs) │
│ ├─ Tab 1 Envelope 查询 → useMtcEnvelope │
│ ├─ Tab 2 Archive → useMtcArchive │
│ ├─ Tab 3 Governance → useGovernanceMofn │
│ └─ Tab 4 Cross-Fed → useCrossFedTrust │
│ │
│ composables/use*.js ─┐ │
│ ws.sendRaw({type}) │ 双路径分叉 │
│ useShellMode() │ (isEmbedded) │
│ ↓ │
└────────────────── ws (WebSocket) ────────────────┘
↓
┌──────── desktop-app-vue web-shell ───────────────┐
│ web-shell-bootstrap │
│ ↓ wsHandlers map │
│ community-mtc-handlers (B4-webshell §2.2.20) │
│ 13 dotted topics → main process B4 managers │
└──────────────────────────────────────────────────┘Composable shape 对齐
| Composable | 主要 reactive | actions |
|---|---|---|
useMtcEnvelope | state (5-phase: idle/loading/found/not-found/error) | fetch(communityId, messageId) / reset() |
useMtcArchive | archives / lastResult / loading / errorMessage | listArchives / pushArchive / restoreArchive |
useGovernanceMofn | proposals / currentStatus / loading / errorMessage | listProposals / createProposal / addSignature (序列化 base64) / getStatus / finalize |
useCrossFedTrust | records / trustedDids / loading / errorMessage | listTrust / establishTrust / revokeTrust / getTrustedDids |
每个都 expose isEmbedded(pure-browser 模式提示用户切桌面壳)+ errorMessage(最近一次失败原因)。
MtcAudit.vue 页面布局
- 顶部固定 banner:非 embedded 模式时显示 "请用桌面壳" 警告
- 4 个 tab 用 antd
<Tabs>切换 - 每个 tab 自带表单 + 操作按钮 + 状态展示(envelope 用 Descriptions,archive/governance/trust 用 List + Tag)
- 公共按钮
:disabled="!composable.isEmbedded"防 pure-browser 误调 - envelope tab 有 raw JSON 折叠 + 复制按钮(用
navigator.clipboard.writeText),跟 V6 桌面 viewer §2.2.16 等价
已知 v1 限制
- 签名收集 v1 没在 web-panel 里做:
addSignature需要传 secretKey 而 web-panel 不持有用户私钥(安全考虑)。Tab 3 用 alert 提示用户改用 cc CLI / 桌面壳完成签名。v2 应该接 ukey-sign 或 desktop-only flow - 没归档凭证持久化:WebDAV password 每次操作都要在表单里填一遍(复用 sync-credentials.js 是 follow-up)
- 没 batch 触发 / 自动归档配置:用户需要手动按按钮;自动化/cron 跟桌面 SettingsPanel 的 sync 自动化模式对齐是 follow-up
测试 (B4-webpanel 新增 33 单元 / 全 web-panel 1829 / 1829 across 62 文件)
useMtcEnvelope.test.js(8) — 5-phase 全覆盖 + ws.ok=false 兜底 + resetuseMtcArchive.test.js(11) — list/push/restore × 成功+错误+缺参 × isEmbedded reflectuseGovernanceMofn.test.js(9) — list/create/sign/finalize/status × base64 序列化关键 (Uint8Array → string;已 base64 不二次 encode)useCrossFedTrust.test.js(6) — list/establish/revoke/getTrustedDids × 全 transport 透传 + 拒空 + 错误捕获- 全 web-panel 62 文件回归
P2P 社交全栈完成回顾 (2026-05-07,11+1 commits)
§2.2.10 → §2.2.21 这 12 节涵盖完整路径:
跨机同步 (Phase A 7 bug 修)
→ MTC federation 双轨 (B v1)
→ DID 签名 + auto peer bridging (B4)
→ Merkle 批 envelope finality (B4-merkle)
→ envelope 跨机分发 gossipsub broadcast + on-demand pull (B4-cross)
→ trust filter 按 community 成员校验 (B4-cross-trust)
→ V6 桌面 viewer 按钮 + Modal (B4-ui)
→ 外部归档 filesystem + WebDAV (B4-archive)
→ M-of-N 多签 governance (B4-mofn)
→ 跨联邦信任锚扩展 trust filter (B4-crossfed)
→ 13 WS topic 给默认 web-shell (B4-webshell)
→ 4 composable + MtcAudit.vue UI (B4-webpanel)桌面端(V5/V6)IPC + web-shell 默认壳 WS topic + Vue UI 完整对等。11 个 social commit + 1 个 web-panel UI commit,desktop 1106 + web-panel 1829 = 2935 测试全绿。
2.2.22 B4-mofn-sign v2 — sign-as-self(main 持私钥代签)+ 潜伏 IPC 包 bug 修 (2026-05-07)
§2.2.21 把 web-panel UI 接好之后,governance-mofn Tab 想点"代我签名"时撞墙——v1
mtc.governance-mofn.sign协议要求渲染端把 64 字节 secretKey base64 后塞进 wire。Web-panel 不持有用户私钥(这是显式安全选择),所以这个按钮在 v1 协议下根本无法实现。本节把签名搬进主进程:渲染端只发(communityId, proposalId),main 通过 DIDManager 取当前身份私钥代签,密钥永不离开主进程。
顺手修了一个潜伏 bug——
registerAllIPC的依赖包从 §2.2.10 Phase A 起就漏传communityManager / channelManager / gossipProtocol / governanceEngine / contentModerator + B4 全套(channelEventBatcher、channelEnvelopeDistribution、channelEnvelopeArchiver、archiveProviderFactory、governanceMultiSig、crossFedTrust、mtcFederationManager),phase-3-4-social 拿到的全是null,桌面 V5/V6 的社区 IPC 全程返回空数组或 "not initialized"。Phase 1.6 hard-flip (caaddf530) 默认壳走 web-shell,没人发现;这次顺道结构性补齐。
22.A 安全模型对比
| 维度 | v1(§2.2.16) | v2(本节) |
|---|---|---|
| 渲染端持私钥? | 是(base64 over WS) | 否 |
| 私钥过线? | 是 | 否 |
| 主进程拿钥路径 | 不需要 | DIDManager.getCurrentIdentity → private_key_ref.sign |
| 适用场景 | CLI / 测试 / 高级用户 | 默认壳所有 web-panel UI |
v1 的 mtc.governance-mofn.sign IPC + WS topic 保留作为 CLI 提案流程的向后兼容面,但 UI 全面切到 v2。
22.B 协议形状
WS topic: mtc.governance-mofn.sign-as-self
请求: {communityId, proposalId} // 仅两个 ID
响应: {success, status, signerDID} // status 是 governanceMultiSig.addSignature 的产物主进程内部:
const identity = didManager.getCurrentIdentity() // 当前登录 DID
const ref = JSON.parse(identity.private_key_ref) // {sign: "<base64-64B>", encrypt: "<base64-32B>"}
const signerKeys = {
did: identity.did,
secretKey: Buffer.from(ref.sign, 'base64'), // 64B Ed25519 secretKey
publicKey: Buffer.from(identity.public_key_sign, 'base64'), // 32B Ed25519 publicKey
}
return { success: true, status: governanceMultiSig.addSignature(communityId, proposalId, signerKeys), signerDID: identity.did }每一步都有独立的描述性 error:no didManager / not-logged-in / missing keys / bad JSON / missing sign field。
22.C 潜伏 IPC 包 bug 全貌
src/main/index.js (改前) | src/main/index.js (改后)
───────────────────────────────────────────────────── | ──────────────────────────────────────────────────
const { communityManager, ... } = instances | this.communityManager = instances.communityManager
| // ... 12 个管理器全部 hoist 到 this.*
this.registerAllIPC({ | this.registerAllIPC({
// ... 缺 communityManager / channelManager / | communityManager: this.communityManager,
// gossipProtocol / governanceEngine / | channelManager: this.channelManager,
// contentModerator + B4 全套 | gossipProtocol: this.gossipProtocol,
}) | governanceEngine: this.governanceEngine,
| contentModerator: this.contentModerator,
| mtcFederationManager: this.mtcFederationManager,
| channelEventBatcher: this.channelEventBatcher,
| channelEnvelopeDistribution: this.channelEnvelopeDistribution,
| channelEnvelopeArchiver: this.channelEnvelopeArchiver,
| archiveProviderFactory: this.archiveProviderFactory,
| governanceMultiSig: this.governanceMultiSig,
| crossFedTrust: this.crossFedTrust,
| didManager: this.didManager ?? null,
| })phase-3-4-social.js 通过 dependencies.X 解构这些字段,改前全是 undefined → community-ipc.js 每个 handler 第一行 if (!communityManager) throw new Error("Community manager not initialized") 都立刻命中。改后桌面 V5/V6 路径恢复全功能。
22.D web-shell 透传
startWebShell({...didManager: this.didManager ?? null}) → web-shell-bootstrap.js 把 didManager 加进 createCommunityMtcHandlers({...}) opts → community-mtc-handlers.js 在 v2 handler 工厂里读 opts.didManager。任何一层缺失都返回明确 error envelope,UI 可显示。
22.E composable 改造
// useGovernanceMofn.js — 新方法
async function signAsSelf(communityId, proposalId) {
if (!communityId || !proposalId) {
errorMessage.value = 'communityId + proposalId 必填'
return null
}
const reply = await ws.sendRaw({
type: 'mtc.governance-mofn.sign-as-self',
communityId,
proposalId,
// 关键:不带任何 signerKeys / secretKey / publicKey
})
// ... 同 v1 解 envelope
}测试断言 expect(args).not.toHaveProperty('signerKeys') 防止未来回归把私钥误塞回 wire。
22.F UI
MtcAudit.vue Tab 3:
- alert copy 从 v1 的 advisory("需 cc CLI 签")改成 v2 安全说明("渲染端只发 ID,主进程持钥")
- 每条 proposal 行追加
代我签名按钮,!finalized && isEmbedded时启用 - 按下走
mofn.signAsSelf(...),成功后刷新currentStatus
22.G 测试
- desktop
community-mtc-handlers.test.js+6 → 25:sign-as-self happy + 5 错误支线(key shape Buffer 64/32 / no didManager / not-logged-in / missing signing keys / malformed JSON / missing sign field) - web-panel
useGovernanceMofn.test.js+4 → 13:wire payload 不带 signerKeys / 拒空 args / handler error envelope / currentStatus 更新 - 全量回归 desktop 1112 + web-panel 1833 = 2945 全绿
22.H 命中节奏
触发:§2.2.21 后用户问"代我签名"按钮怎么实现
诊断:v1 协议要求 secretKey 过 wire → web-panel 显式不持私钥 → 不可实现
设计:v2 = main 持钥 + 渲染端只发 ID
实现:1 IPC + 1 WS topic + 1 composable 方法 + 1 UI 按钮
顺手修:发现 registerAllIPC 包从未传社区/B4 管理器(潜伏 ~1 个月)P2P 社交全栈完成回顾 (2026-05-07,12+1 commits)
§2.2.10 → §2.2.22 这 13 节涵盖完整路径:
跨机同步 (Phase A 7 bug 修)
→ MTC federation 双轨 (B v1)
→ DID 签名 + auto peer bridging (B4)
→ Merkle 批 envelope finality (B4-merkle)
→ envelope 跨机分发 gossipsub broadcast + on-demand pull (B4-cross)
→ trust filter 按 community 成员校验 (B4-cross-trust)
→ V6 桌面 viewer 按钮 + Modal (B4-ui)
→ 外部归档 filesystem + WebDAV (B4-archive)
→ M-of-N 多签 governance v1 (B4-mofn,secretKey 过 wire)
→ 跨联邦信任锚扩展 trust filter (B4-crossfed)
→ 13 WS topic 给默认 web-shell (B4-webshell)
→ 4 composable + MtcAudit.vue UI (B4-webpanel)
→ 代我签名 v2 + IPC 包潜伏 bug 修 (B4-mofn-sign v2)desktop 1112 + web-panel 1833 = 2945 测试全绿。私钥不过线,UI 默认壳全套可见,桌面 V5/V6 IPC 路径同步恢复。
2.2.23 B4-cred-persist v1 — WebDAV 凭据复用 secure-config + 修一个潜伏 ~1 个月字段名 bug (2026-05-07)
§2.2.21 落 MtcAudit Archive Tab 时让用户每次推 archive 都手输 baseUrl/username/password 走 wire——这违反了"凭据不应在 wire 上反复出现"原则。更糟:实际去用 WebDAV archive 立刻撞 "url 必填"——WebDAVClient 构造器读
url/remotePath,archive 工厂从 §2.2.16 起一直传baseUrl/remoteRoot,字段名根本对不上,B4-archive WebDAV 路径从落地起就完全跑不起来。XIV 之前没人真点过那按钮(CLI 用 filesystem 居多)所以一直没爆。本节同时解决两个问题。
23.A 双问题对比
| 维度 | v1 - XIV(broken) | v2 - XVII(本节) |
|---|---|---|
| WebDAV 字段名 | spec.baseUrl / remoteRoot(错的) | spec.url / remotePath(对的) |
| 凭据来源 | 每次 push 手输 + 走 wire | 复用 Phase 3c sync-credentials(safeStorage / AES-256-GCM) |
| Renderer 持密码? | 是(input v-model) | 否(toggle 默认 ON 时不显示输入框) |
| Wire 携密码? | 是 | 否(main 内部从 vault 解密) |
23.B 工厂抽离
老路:social-initializer.js 内联 ~58 行的 archiveProviderFactoryImpl,没法独立单测,扩展 OSS/S3 要继续往里塞。
新路:抽出 src/main/mtc/archive-provider-factory.js:
function createArchiveProviderFactory(deps = {}) {
const syncCredentials = deps.syncCredentials || require('../sync/sync-credentials')
const WebDAVClient = deps.WebDAVClient || require('../sync/webdav-client').WebDAVClient
return function archiveProviderFactoryImpl(spec) {
if (spec.kind === 'filesystem') return filesystemProvider({ rootDir: spec.rootDir })
if (spec.kind === 'webdav') {
let url, username, password, remotePath
if (spec.useStoredCredentials) {
if (!syncCredentials.hasCredentials('webdav')) {
throw new Error('useStoredCredentials=true but no WebDAV credentials saved yet ...')
}
const stored = syncCredentials.getCredentials('webdav')
url = stored.url; username = stored.username
password = stored.password; remotePath = stored.remotePath || '/'
} else {
// 显式 spec — CLI / 测试 / advanced 用
url = spec.url; username = spec.username
password = spec.password; remotePath = spec.remotePath
}
if (!url) throw new Error('webdav provider needs url ...')
return webdavProvider(new WebDAVClient({ url, username, password, remotePath }))
}
throw new Error('unsupported provider kind: ' + spec.kind)
}
}social-initializer 现在 init 只剩一行 return createArchiveProviderFactory()。
23.C 凭据探测协议
UI 不能直接拿凭据(vault 内容永不出主进程),但需要知道"vault 是否已配过 WebDAV"才能正确渲染 toggle。新增轻量协议:
WS topic: mtc.archive.has-stored-webdav-credentials → {success, hasCredentials}
IPC channel: channel-archive:has-stored-webdav-credentials → {ok, hasCredentials}响应 schema 单元测试锁死:expect(Object.keys(r).sort()).toEqual(['hasCredentials','success'])——任何回归想偷渡 url/password/username 进 response 立刻断。
23.D UI 改造
MtcAudit.vue Archive Tab:
- onMounted 调
archive.checkStoredWebdavCredentials();结果挂到hasStoredWebdavCredentialsref - WebDAV 选项下加
<a-switch>:- 默认 ON
- vault 空时 disable + 显 a-alert 引导去 Settings → 同步 → WebDAV 配置一次
- vault 有时显 a-alert "已找到,主进程从 secure-config.enc 解密后构造 WebDAVClient"
- toggle ON 时:input 字段全部隐藏,wire payload 只发
{kind:'webdav', useStoredCredentials:true} - toggle OFF 时:恢复 v1 的 4 个 input 字段(url/username/password/remotePath),CLI / 测试可走
23.E 字段名修
archive-provider-factory 重写时同步把字段名从 baseUrl/remoteRoot 改成 url/remotePath 匹配 WebDAVClient 构造器。MtcAudit.vue archForm + providerSpec computed 同步改名。单元测试加字段名锁定:
expect(captured).toEqual({ url, username, password, remotePath })
expect(captured).not.toHaveProperty('baseUrl')
expect(captured).not.toHaveProperty('remoteRoot')23.F 安全断言
| 层 | 不变量 | 锁定 |
|---|---|---|
| 工厂 | useStoredCredentials=true 时 inline url/username/password 完全忽略 | 单元测试"vault wins over inline" |
| WS / IPC | hasCredentials 响应只含 2 字段 | Object.keys(r).sort() 严格相等 |
| Composable | request 和 response 都不带凭据字段 | expect(args).not.toHaveProperty('password' / 'url' / 'username') |
| UI | toggle ON 时 input 字段不渲染(v-if=!useStoredCredentials) | Vue template 静态可见 |
23.G 测试 (8 新)
- desktop
archive-provider-factory.test.js新文件 12 测试 — filesystem / webdav 显式 / useStoredCredentials 4 子用例(happy / spec.* 被忽略 / vault 空 / remotePath 默认 /)/ 字段名锁定 / 拒空 spec / 拒未知 kind - desktop
community-mtc-handlers.test.js+4 — has-stored true/false / 缺 syncCredentials 注入 / 响应 schema 锁死 - web-panel
useMtcArchive.test.js+4 — flag true/false / handler error → soft false / transport error → null
23.H 决策
- 不另起 credential-vault——直接复用 §Phase 3c 已落的 sync-credentials(
secure-config.enc+ safeStorage / AES-256-GCM),单 source of truth; - 不暴露 credentials.encrypt/decrypt IPC——所有解密只在主进程 archiveProviderFactory 内部进行;
- 工厂抽出——DI 装配 -58/+5,可独立单测,未来加 OSS/S3 也好扩。
23.I 命中节奏
触发:§2.2.21 后想点 archive Tab 的 push 按钮
诊断 1:每次手输密码走 wire = 反凭据持久化原则
诊断 2:实际跑发现 url='' 立刻抛——字段名 baseUrl/remoteRoot vs url/remotePath 错配 ~1 个月
方案:抽工厂 + 加 useStoredCredentials 模式 + 修字段名 + UI toggleP2P 社交全栈完成回顾 (2026-05-07,13+1 commits)
§2.2.10 → §2.2.23 这 14 节涵盖完整路径:
跨机同步 (Phase A 7 bug 修)
→ MTC federation 双轨 (B v1)
→ DID 签名 + auto peer bridging (B4)
→ Merkle 批 envelope finality (B4-merkle)
→ envelope 跨机分发 gossipsub broadcast + on-demand pull (B4-cross)
→ trust filter 按 community 成员校验 (B4-cross-trust)
→ V6 桌面 viewer 按钮 + Modal (B4-ui)
→ 外部归档 filesystem + WebDAV (B4-archive,字段名 broken)
→ M-of-N 多签 governance v1 (B4-mofn,secretKey 过 wire)
→ 跨联邦信任锚扩展 trust filter (B4-crossfed)
→ 13 WS topic 给默认 web-shell (B4-webshell)
→ 4 composable + MtcAudit.vue UI (B4-webpanel)
→ 代我签名 v2 + IPC 包潜伏 bug 修 (B4-mofn-sign v2)
→ WebDAV 凭据走 secure-config + 字段名修 (B4-cred-persist v1)desktop 1244 + web-panel 1976 (=1978-2 pre-existing flake) ≈ 3220 测试全绿。私钥 / 密码均不过线,secure-config.enc 在主进程内部解密,UI 默认壳全套可见,WebDAV archive 从"理论上能跑"恢复到"实际可用"。
2.2.24 B4-auto-archive v1 — 主进程定时归档 cron + MtcAudit 第 5 个 Tab (2026-05-07)
§2.2.21 (XIV) 接好 Archive Tab 后归档仍是手动——用户必须主动按"推送"。§2.2.23 修了凭据持久化让推送不再需要每次输密码;本节让 cron 自己跑——主进程 setInterval 周期触发
ChannelEnvelopeArchiver.push,配置写到 app-config.json 的mtc.autoArchivenamespace,重启后 enabled=true 的旧配置自动恢复。
24.A 协议形状
WS: mtc.auto-archive.config-get → {success, config}
mtc.auto-archive.config-set → {success, config} payload: {patch}
mtc.auto-archive.run-now → {success, result} result = {status, error?, summary}
IPC: auto-archive:config-get / config-set / run-nowconfig 形状:
{
"enabled": false,
"intervalMs": 86400000,
"providerSpec": { "kind": "webdav", "useStoredCredentials": true },
"communityIds": [],
"lastRunAt": 1741355400000,
"lastRunStatus": "ok",
"lastRunError": null,
"lastRunSummary": {
"startedAt": 1741355399000,
"finishedAt": 1741355400000,
"perCommunity": { "comm-A": { "ok": true, "name": "channel-mtc-...zip", "bytes": 12345 } },
"totalArchives": 1,
"totalBytes": 12345
}
}24.B 调度器结构
src/main/mtc/auto-archive-scheduler.js 是纯 Node(无 Electron API):
class AutoArchiveScheduler {
constructor({ archiver, archiveProviderFactory, communityManager, appConfig, logger, timers })
getConfig() // 合并默认值,clamp sub-min intervalMs
async setConfig(patch) // 校验 + 持久化 + 重启 timer
start() / stop() // setInterval/clearInterval;start() 内 enabled=false 直接 return
async runOnce() // 一次完整 sweep,per-community try/catch,自动持久化 lastRun*
}DI 注入 timers 让单测不需要真等:
const timers = { setInterval: vi.fn(), clearInterval: vi.fn() }24.C 关键不变量
| 不变量 | 锁定方式 |
|---|---|
| intervalMs ≥ 5min | setConfig 校验 < MIN_INTERVAL_MS 抛 |
| enabled=true 必须 providerSpec | setConfig 校验 |
| runOnce 不重入 | _running flag + 早返回 {skipped:true} |
| 单 community 失败不阻断 | per-community try/catch;status='partial' 记 perCommunity[id].ok=false |
| 无 communityManager + 空白名单 → 不报错 | 返回 summary.note='no-target-communities' |
| start() 幂等 | if (this._timer) return |
| setConfig disabled 自动 stop | if (this._timer) this.stop() |
24.D Boot-time 自动恢复
DI 注册:
factory.register({
name: 'autoArchiveScheduler',
dependsOn: ['channelEnvelopeArchiver', 'archiveProviderFactory', 'communityManager'],
required: false,
async init(context) {
const sched = new AutoArchiveScheduler({...})
sched.start() // enabled=false 时 no-op;enabled=true 时立刻接上
return sched
}
})主进程 boot 后已 enabled=true 的配置自动接续;用户手动 disable 后 setConfig({enabled:false}) 走 stop 路径不会留 dangling timer。
24.E UI
MtcAudit.vue 第 5 个 Tab("Auto Archive 定时归档"):
┌ Switch enabled
├ InputNumber intervalHours (min 0.083h = 5min)
├ provider 切换 button[Filesystem | WebDAV]
│ ├ filesystem → rootDir input
│ └ webdav → useStoredCredentials switch (默认 ON) + 否则手输 4 字段
├ Textarea communityIdsRaw (每行一个;留空 = 全部已加入)
└ Buttons:
├ 保存配置 → setConfig
├ 立即归档一次 → runNow
└ 刷新 → getConfig
+
└ Card 当前持久化配置 (Descriptions: enabled/interval/provider/targets/lastRunAt/lastRunStatus + 可选 lastRunError Alert)
└ Card 本次手动 run-now 摘要 (JSON pretty-print perCommunity)onMounted 自动调一次 getConfig 把 form pre-fill。重要:password 字段从主进程读回来时永远是 ''(vault 不外泄),所以 useStoredCredentials=false 路径下 form 显示空 password 框,用户手动改才 push。
24.F 测试 (27 新)
- desktop
auto-archive-scheduler.test.js新文件 19 测试 — 构造校验 3 / getConfig 2 / setConfig 4 / start/stop 2 / runOnce 7 (happy / 白名单 / partial / providerSpec 缺 / 重入守卫 / lastRun 持久化 / 无 communityManager) - desktop
community-mtc-handlers.test.js+5 — config-get/set/run-now 各 happy + setConfig 校验透传 + 缺 dep - web-panel
useAutoArchive.test.js新文件 9 测试 — getConfig 2 / setConfig 3 / runNow 3 / isEmbedded
24.G 决策
- 不引第三方 cron 库——
setInterval已足够,不需要 cron 表达式 / 季节性触发; - 复用 app-config.json——跟
ui.useV6ShellByDefault等设置一个层级,不开新 store; - scheduler 纯 Node 无 Electron API——单元可测 timers 注入;
- runOnce 自动持久化 lastRun* 字段——UI 不需要单独 status WS topic,getConfig 同时拿到运行历史;
- 不实现真 cron 表达式 (e.g. node-cron
0 2 * * *)——v1 范围内 setInterval 足够覆盖"每 N 小时"语义;用户真要"每天凌晨 2 点"以后再加; - runOnce 失败按 community 隔离——某个 community 的 zip 包失败不应阻断别的 community 归档(部分价值优于 0 价值)。
24.H 命中节奏
触发:§2.2.23 解决凭据问题后用户问"那能不能自动跑"
设计:纯 Node 调度器 + DI timers 单测可控 + 配置走 app-config.json
实现:scheduler + 3 IPC + 3 WS topic + composable + UI 5th tab
不变量:min interval / providerSpec required / 重入守卫 / 部分成功 / 自动恢复P2P 社交全栈完成回顾 (2026-05-07,14+1 commits)
§2.2.10 → §2.2.24 这 15 节涵盖完整路径:
跨机同步 (Phase A 7 bug 修)
→ MTC federation 双轨 (B v1)
→ DID 签名 + auto peer bridging (B4)
→ Merkle 批 envelope finality (B4-merkle)
→ envelope 跨机分发 gossipsub broadcast + on-demand pull (B4-cross)
→ trust filter 按 community 成员校验 (B4-cross-trust)
→ V6 桌面 viewer 按钮 + Modal (B4-ui)
→ 外部归档 filesystem + WebDAV (B4-archive,字段名 broken)
→ M-of-N 多签 governance v1 (B4-mofn,secretKey 过 wire)
→ 跨联邦信任锚扩展 trust filter (B4-crossfed)
→ 13 WS topic 给默认 web-shell (B4-webshell)
→ 4 composable + MtcAudit.vue UI (B4-webpanel)
→ 代我签名 v2 + IPC 包潜伏 bug 修 (B4-mofn-sign v2)
→ WebDAV 凭据走 secure-config + 字段名修 (B4-cred-persist v1)
→ 主进程定时归档 cron + 5th MtcAudit Tab (B4-auto-archive v1)P2P 社交从"消息可信 + 自动联网" → "envelope 跨机" → "trust 校验" → "UI 可见" → "外部归档(活的)" → "M-of-N 多签" → "跨联邦信任" → "默认壳全套" → "凭据加密复用" → "周期自动归档"。全闭环 audit-grade。
