Skip to content

去中心化社交模块

本文档是 系统设计主文档 的子文档,详细描述去中心化社交模块的设计。


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 数据模型

本地社交数据库表结构:

sql
-- 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.10WebRTC传输层
@libp2p/noise 1.0.1Noise协议加密
@libp2p/kad-dht 16.1.2Kademlia DHT
@libp2p/circuit-relay-v2 4.1.2中继节点支持
WebRTCwerift 0.22.2WebRTC实现
WebRTC兼容wrtc-compat.js ⭐新增兼容层(152行,v0.20.0)
端到端加密Signal协议@privacyresearch/libsignal-protocol-typescript 0.0.16
分布式存储IPFS公开内容的永久存储
DHTKademlia节点发现和路由
签名算法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原生实现
E2EESignal 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管理
E2EESignal Protocol✅ 100% - 完整实现 (~8,195行) ⭐
密钥交换X3DH✅ 密钥协商 + 预密钥包
双棘轮Double Ratchet✅ 前向安全 + 密钥轮转
会话管理PersistentSessionManager✅ 持久化 + 密钥导出/导入
身份验证SafetyNumbers✅ 60位安全码 + 会话指纹
高级E2EEReadReceiptManager✅ 已读回执加密 + 批量确认
MessageRecallManager✅ 消息撤回 + 灵活策略
MessageQueueManager✅ 离线消息队列 + 优先级
GroupEncryptionManager✅ 群组加密接口 + AES-256
P2P网络core-p2p模块✅ 100% - WebRTC连接
WebRTCWebRTC 120.0.0✅ STUN/TURN配置
P2P UIfeature-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代码


跨平台对比:

特性桌面端iOSAndroid
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 而非 GossipProtocolLRU 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(公网回到中心化引导)

测试金字塔

  • 单元 18p2p-manager-dispatch.test.js 覆盖 encode/decode/dispatch helper
  • 集成 4gossip-channel-receiver.integration.test.js 真 GossipProtocol + 真 ChannelManager + mock libp2p
  • e2e 3p2p-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)

  • 单元 17mtc/__tests__/mtc-federation-manager.test.js 用 mock transport 覆盖全部 lifecycle / publish / subscribe / 失败语义
  • 集成 12social/__tests__/community-ipc-dual-track.integration.test.js 真 community-ipc + mock gossipProtocol + mock mtcFederationManager,验证双发 / 双订 / 任一失败不阻塞 / 单管道 fallback
  • e2e 4mtc/__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 收到带签名消息时按顺序:

  1. DID 一致性computeDID(pubkey) === sender_did(防 pubkey/DID 错配——攻击者声明 alice DID 但塞自己 pubkey 这种)
  2. Ed25519 签名nacl.sign.detached.verify(canonical(subset), sig, pubkey)
  3. 形状合法: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:

js
{
  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 formatcore-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 或外部 verifierdesktop 本机也能用 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

冒号路径转义:treeHeadIdsha256:<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):

json
{
  "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):

json
{
  "type": "mtc:envelope-request",
  "requestId": "req-<ts>-<seq>",
  "communityId": "comm-X",
  "messageId": "msg-Y"
}

envelope 响应(typed message):

json
{
  "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 过滤。理由:

  1. Noise 防 MITM:transport 层加密保证 wire 内容没被篡改
  2. 最终验证关在 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*、findEnvelope origin 字段)
  • 单元 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.jsmtc: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 格式

js
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 _assembleBatchLocaldid-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 onlyextractIssuerDID 只看 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

ts
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 通用接口yescommunity store 已经全用这套,preload 不用改;新 IPC channel 自动可达
Composable 不 watchyescaller 自管 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 组件本身deliberateAnt Design Modal/Teleport 在 jsdom 下脆弱 + 没业务分支;composable + IPC 已测试覆盖;e2e Playwright 那批一起加

Wire format(renderer ↔ main)

renderer 调用:

ts
electronAPI.invoke('channel:get-message-envelope', communityId, messageId)

main 返回(community-ipc.js handler):

js
{
  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 接口契约

ts
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-mtc assembleBatchFederated 加 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 ↔ pubkeyyes防止 "我说我是 alice 但塞自己 pubkey" 攻击;与 B4a / B4-cross-trust 相同精神
同 DID 重复 add = no-opyes (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 自家实现yescore-mtc index 因 @noble/curves 加载失败,沿用 channel-event-batch / channel-envelope-archiver 已建立的"绕开 index 用 subpath primitives" 模式
不接 cc governance v1 路径yesPhase 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 默认排除过期yestrust 不该用过期数据;caller 想看历史用 listTrusted (不过滤)
union 在 social-initializer adapter 而不在 distribution 里yesdistribution 只关心 "give me trusted DIDs"——不关心来源是 local or cross-fed;扩展性好(未来加 OAUTH provider 等也只动 adapter)
revokeTrust idempotent return false 而不抛yesrenderer 调用时不需要先 list;try-revoke 是常见模式
不验证 remoteMembers 真的属于那个联邦v1 yesv2 应该 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.getchannel:get-message-envelope
mtc.archive.push / .restore / .listchannel-archive:push / restore / list
mtc.governance-mofn.create / .sign / .status / .finalize / .listgovernance-mofn:create / sign / status / finalize / list
mtc.cross-fed-trust.establish / .revoke / .list / .get-trusted-didscross-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-envelopedotted 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().isEmbedded pattern)。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主要 reactiveactions
useMtcEnvelopestate (5-phase: idle/loading/found/not-found/error)fetch(communityId, messageId) / reset()
useMtcArchivearchives / lastResult / loading / errorMessagelistArchives / pushArchive / restoreArchive
useGovernanceMofnproposals / currentStatus / loading / errorMessagelistProposals / createProposal / addSignature (序列化 base64) / getStatus / finalize
useCrossFedTrustrecords / trustedDids / loading / errorMessagelistTrust / 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 兜底 + reset
  • useMtcArchive.test.js (11) — list/push/restore × 成功+错误+缺参 × isEmbedded reflect
  • useGovernanceMofn.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 commitdesktop 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 的产物

主进程内部:

js
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.jsdidManager 加进 createCommunityMtcHandlers({...}) opts → community-mtc-handlers.js 在 v2 handler 工厂里读 opts.didManager。任何一层缺失都返回明确 error envelope,UI 可显示。

22.E composable 改造

js
// 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

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();结果挂到 hasStoredWebdavCredentials ref
  • 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 同步改名。单元测试加字段名锁定:

js
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 / IPChasCredentials 响应只含 2 字段Object.keys(r).sort() 严格相等
Composablerequest 和 response 都不带凭据字段expect(args).not.toHaveProperty('password' / 'url' / 'username')
UItoggle 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 决策

  1. 不另起 credential-vault——直接复用 §Phase 3c 已落的 sync-credentials(secure-config.enc + safeStorage / AES-256-GCM),单 source of truth;
  2. 不暴露 credentials.encrypt/decrypt IPC——所有解密只在主进程 archiveProviderFactory 内部进行;
  3. 工厂抽出——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 toggle

P2P 社交全栈完成回顾 (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.autoArchive namespace,重启后 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-now

config 形状:

json
{
  "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):

js
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 让单测不需要真等:

js
const timers = { setInterval: vi.fn(), clearInterval: vi.fn() }

24.C 关键不变量

不变量锁定方式
intervalMs ≥ 5minsetConfig 校验 < MIN_INTERVAL_MS
enabled=true 必须 providerSpecsetConfig 校验
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 自动 stopif (this._timer) this.stop()

24.D Boot-time 自动恢复

DI 注册:

js
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 决策

  1. 不引第三方 cron 库——setInterval 已足够,不需要 cron 表达式 / 季节性触发;
  2. 复用 app-config.json——跟 ui.useV6ShellByDefault 等设置一个层级,不开新 store;
  3. scheduler 纯 Node 无 Electron API——单元可测 timers 注入;
  4. runOnce 自动持久化 lastRun* 字段——UI 不需要单独 status WS topic,getConfig 同时拿到运行历史;
  5. 不实现真 cron 表达式 (e.g. node-cron 0 2 * * *)——v1 范围内 setInterval 足够覆盖"每 N 小时"语义;用户真要"每天凌晨 2 点"以后再加;
  6. 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

ChainlessChain 系统设计文档 — 面向开发者