引言:知识管理系统的架构困境与 AFFiNE 的技术突围
在数字化协作时代,我们面临着知识管理工具的核心矛盾:功能强大的商业产品如 Notion、Miro 虽然提供了丰富的协作能力,但要求用户将数据托管在第三方服务器上,这与企业级隐私合规要求和敏感数据保护需求存在根本冲突;而开源的隐私优先工具往往功能单一,无法满足现代团队的复杂协作需求。
AFFiNE 作为开源协作知识库的代表性项目,通过创新的技术架构巧妙地解决了这一矛盾。它将 CRDT(无冲突复制数据类型)、块协议和本地优先设计相结合,构建了一个既具备强大协作功能,又确保数据主权的技术体系。GitHub 上超过 50K 的星标和全球开发者社区的积极参与,证明了这个架构设计的成功。
本文将深入分析 AFFiNE 的技术底层,探讨其如何通过工程化的 CRDT 实现、模块化的 BlockSuite 架构和安全的本地优先机制,重新定义知识管理系统的技术标准。
核心技术架构:CRDT 驱动的分布式协作引擎
CRDT vs OT:AFFiNE 为何选择无冲突复制类型
AFFiNE 的技术架构核心在于其选择了 CRDT(Conflict-free Replicated Data Type)而非传统的 OT(Operational Transformation)作为协作算法的基础。这个选择反映了其对分布式系统设计理念的深刻理解。
传统的 OT 算法如 Google Docs 所采用的方法,依赖中心化服务器来协调并发操作。当多个客户端同时编辑文档时,服务器负责接收所有操作、通过 Transform 算法转换冲突操作,然后广播统一的结果。这种方法的优势在于实现相对简单,适合中心化的 SaaS 架构。
但 OT 架构存在根本性局限:
- 单点故障风险:服务器宕机导致协作中断
- 离线体验差:无网络时无法进行有意义的编辑
- 延迟敏感性:网络延迟直接影响协作体验
- 扩展性瓶颈:中心化服务器难以支撑大规模协作
相比之下,CRDT 通过在数据结构层面内建数学可交换性,彻底消除了这些限制。每个字符或块都分配全局唯一 ID(通常结合客户端 ID 和逻辑时钟),系统保证不同节点的操作最终自动收敛一致。
AFFiNE 基于 Yjs 实现了 CRDT 协作引擎。Yjs 提供了完善的 CRDT 实现,支持多种数据类型:
Y.Text:用于文本内容,支持字符级别的 CRDTY.Array:用于列表和块集合,支持动态增删Y.Map:用于键值数据,如块的属性信息
// AFFiNE中典型的CRDT文档结构
import * as Y from 'yjs'
class AFFiNEDocument {
private doc: Y.Doc
constructor() {
this.doc = new Y.Doc()
this.initializeBlocks()
}
private initializeBlocks() {
// 块容器,支持动态添加/删除
this.blocks = this.doc.getMap('blocks')
// 页面索引,支持嵌套结构
this.pages = this.doc.getArray('pages')
// 用户协作状态
this.collaboration = this.doc.getMap('collab')
}
// 创建新块,返回块ID
createBlock(type: string, content: any): string {
const blockId = generateBlockId()
const block = new Y.Map()
block.set('id', blockId)
block.set('type', type)
block.set('content', content)
block.set('createdAt', Date.now())
this.blocks.set(blockId, block)
return blockId
}
}
这种设计的核心优势在于:所有操作都是本地的、立即生效的,用户无需等待网络同步就能看到自己的更改。同时,由于 CRDT 的数学性质,多用户的并发操作不会产生冲突,系统自动合并所有更改。
BlockSuite:块协议的工程化实现
AFFiNE 的另一个技术创新在于其 BlockSuite 框架,这是一个专门为协作编辑设计的块协议实现。BlockSuite 采用了模块化的块架构,将所有内容抽象为可组合的块单元。
// OctoBase中块定义的Rust实现
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockSchema {
pub flavour: String, // 块类型标识
pub props: BlockProps, // 块属性
pub children: Vec<String>, // 子块ID列表
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockProps {
pub text: Option<String>,
pub type_hint: Option<String>,
pub attributes: HashMap<String, serde_json::Value>,
}
impl BlockSchema {
pub fn new(flavour: String) -> Self {
Self {
flavour,
props: BlockProps::default(),
children: Vec::new(),
}
}
// CRDT合并逻辑
pub fn merge(&self, other: &Self) -> Result<Self, MergeError> {
if self.flavour != other.flavour {
return Err(MergeError::FlavourMismatch);
}
Ok(Self {
flavour: self.flavour.clone(),
props: self.props.merge(&other.props)?,
children: merge_children(&self.children, &other.children),
})
}
}
BlockSuite 的架构设计体现了几个关键技术原则:
- 统一抽象:所有内容都遵循块的统一接口,支持嵌套、组合和动态重组
- 类型安全:强类型系统确保块属性的一致性和可预测性
- 协作原生:块级别的 CRDT 支持细粒度的并发控制
- 扩展友好:新的块类型可以无缝集成到现有系统
OctoBase:Rust 驱动的本地优先数据库
AFFiNE 的数据持久化层采用了自研的 OctoBase 数据库,这是一个用 Rust 编写的本地优先数据库引擎。Rust 的选择体现了对性能和安全的极致追求。
// OctoBase核心架构
use y_octo::{Doc, SyncMessage};
use tokio::sync::mpsc;
pub struct AFFiNEDatabase {
doc: Doc,
local_storage: LocalStorage,
sync_engine: SyncEngine,
encryption: EncryptionManager,
}
impl AFFiNEDatabase {
pub async fn new() -> Result<Self> {
let doc = Doc::new();
let local_storage = LocalStorage::new().await?;
let sync_engine = SyncEngine::new(doc.clone());
let encryption = EncryptionManager::new();
Ok(Self {
doc,
local_storage,
sync_engine,
encryption,
})
}
// 本地存储与加密
pub async fn store_block(&self, block: &BlockSchema) -> Result<()> {
let encrypted_data = self.encryption.encrypt_block(block)?;
self.local_storage.write(&block.id, &encrypted_data).await?;
Ok(())
}
// 云端同步(可选)
pub async fn sync_with_cloud(&self) -> Result<()> {
let changes = self.doc.get_changes();
let encrypted_changes = self.encryption.encrypt_changes(&changes)?;
// 通过WebSocket或WebRTC同步到云端
self.sync_engine.broadcast(encrypted_changes).await?;
Ok(())
}
}
OctoBase 的关键技术特性:
- 零信任架构:所有敏感数据在存储前进行本地加密
- 跨平台兼容:通过 FFI 提供多语言绑定
- 高性能索引:专为块结构优化的存储引擎
- 增量同步:只同步变更的块,减少网络开销
隐私优先设计:数据主权的技术实现
本地优先架构的数据流设计
AFFiNE 的 "本地优先"(Local-First)不是简单的离线功能,而是一种完整的数据主权架构。这种设计确保用户对自己的数据拥有完全控制权。
// AFFiNE数据流架构
class DataFlowManager {
private localStore: IndexedDBStore
private syncManager: SyncManager
private encryption: E2EEncryption
constructor() {
this.localStore = new IndexedDBStore('affine-data')
this.syncManager = new SyncManager()
this.encryption = new E2EEncryption()
}
// 写入数据流程
async writeBlock(block: Block): Promise<void> {
// 1. 本地立即写入(乐观更新)
const encryptedBlock = await this.encryption.encrypt(block)
await this.localStore.save(block.id, encryptedBlock)
// 2. 异步同步到云端(如果启用)
if (this.syncManager.isEnabled()) {
await this.syncManager.queueSync(block.id, encryptedBlock)
}
// 3. 触发UI更新
this.emit('blockUpdated', block)
}
// 离线优先读取
async readBlock(id: string): Promise<Block | null> {
// 优先从本地读取
const encryptedBlock = await this.localStore.read(id)
if (encryptedBlock) {
return await this.encryption.decrypt(encryptedBlock)
}
// 本地没有则从云端获取
if (this.syncManager.isEnabled()) {
const syncedBlock = await this.syncManager.fetchFromCloud(id)
if (syncedBlock) {
// 同步到本地
await this.localStore.save(id, syncedBlock)
return await this.encryption.decrypt(syncedBlock)
}
}
return null
}
}
端到端加密的实现细节
AFFiNE 的端到端加密采用多层加密策略,确保数据在传输和存储过程中的安全性:
- 传输层加密:使用 TLS 1.3 确保网络传输安全
- 应用层加密:使用 AES-256-GCM 对敏感数据进行加密
- 密钥管理:采用 PBKDF2 和随机盐值派生密钥
- 元数据保护:即使加密后也不泄露数据结构和内容类型
// AFFiNE加密实现
class AFFiNEEncryption {
private readonly ALGORITHM = 'AES-GCM'
private readonly KEY_LENGTH = 256
private readonly IV_LENGTH = 12
async encryptBlock(block: Block): Promise<EncryptedBlock> {
const key = await this.deriveKey(block.workspaceId)
const iv = crypto.getRandomValues(new Uint8Array(this.IV_LENGTH))
const encoder = new TextEncoder()
const data = encoder.encode(JSON.stringify(block))
const encrypted = await crypto.subtle.encrypt(
{
name: this.ALGORITHM,
iv: iv
},
key,
data
)
return {
encrypted: new Uint8Array(encrypted),
iv: iv,
version: '1.0'
}
}
private async deriveKey(salt: string): Promise<CryptoKey> {
const encoder = new TextEncoder()
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(this.getMasterKey()),
{ name: 'PBKDF2' },
false,
['deriveKey']
)
return crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: encoder.encode(salt),
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
{
name: this.ALGORITHM,
length: this.KEY_LENGTH
},
false,
['encrypt', 'decrypt']
)
}
}
这种加密策略确保即使 AFFiNE 的服务器被攻破,攻击者也无法获取用户的实际数据内容。所有的加密操作都在客户端进行,服务器只负责转发加密后的数据。
实时协作引擎:冲突解决与同步机制
协作状态管理
AFFiNE 的实时协作系统基于 WebRTC 和 WebSocket 的双通道设计,确保在不同网络条件下都能提供良好的协作体验。
// 协作管理器
class CollaborationManager {
private roomId: string
private doc: Y.Doc
private provider: WebRTCProvider | WebSocketProvider
private awareness: Awareness
constructor(roomId: string, doc: Y.Doc) {
this.roomId = roomId
this.doc = doc
this.setupProviders()
this.initializeAwareness()
}
private setupProviders() {
// 优先使用WebRTC(点对点,低延迟)
if (this.isWebRTCSupported()) {
this.provider = new WebRTCProvider(roomId, doc, {
signaling: ['wss://signaling.affine.pro'],
password: this.getRoomPassword()
})
} else {
// 降级到WebSocket
this.provider = new WebSocketProvider(
'wss://ws.affine.pro',
roomId,
doc,
{ password: this.getRoomPassword() }
)
}
}
// 用户状态感知
private initializeAwareness() {
this.awareness = new Awareness(this.doc)
// 设置本地用户状态
this.awareness.setLocalState({
user: {
name: this.getUserName(),
color: this.getUserColor(),
avatar: this.getUserAvatar()
},
cursor: null,
selection: null
})
// 监听其他用户状态变化
this.awareness.on('change', ({ added, updated, removed }) => {
this.handleAwarenessChange(added, updated, removed)
})
}
// 实时协作事件处理
private handleAwarenessChange(
added: number[],
updated: number[],
removed: number[]
) {
added.forEach(clientId => {
const state = this.awareness.getStates().get(clientId)
if (state) {
this.showUserJoined(state.user)
}
})
updated.forEach(clientId => {
const state = this.awareness.getStates().get(clientId)
if (state) {
this.updateRemoteCursor(state.user, state.cursor, state.selection)
}
})
}
}
冲突解决机制
在 CRDT 架构下,冲突解决不再是问题,但 AFFiNE 仍然需要处理用户体验相关的冲突:
- 选择冲突:多个用户同时选择同一内容
- 编辑冲突:快速连续的操作导致光标位置异常
- 权限冲突:无权限用户尝试编辑受保护内容
// 冲突解决策略
class ConflictResolver {
private document: Y.Doc
private locks: Map<string, UserLock>
constructor(document: Y.Doc) {
this.document = document
this.locks = new Map()
}
// 尝试获取编辑锁
async acquireLock(blockId: string, userId: string): Promise<boolean> {
const existing = this.locks.get(blockId)
if (existing && existing.userId !== userId) {
// 检查锁是否过期
if (Date.now() - existing.timestamp > 30000) { // 30秒超时
this.locks.delete(blockId)
return this.acquireLock(blockId, userId)
}
return false // 锁被其他用户持有
}
// 获取锁
this.locks.set(blockId, {
userId,
timestamp: Date.now()
})
return true
}
// 释放编辑锁
releaseLock(blockId: string, userId: string): void {
const lock = this.locks.get(blockId)
if (lock && lock.userId === userId) {
this.locks.delete(blockId)
}
}
// 处理光标冲突
resolveCursorConflict(
userId: string,
position: number,
selection: SelectionRange
): ResolvedCursor {
const currentState = this.getCurrentEditorState()
// 如果用户在编辑内容,隐藏其光标避免干扰
if (this.isUserTyping(userId)) {
return {
userId,
position: position,
visible: false
}
}
return {
userId,
position: position,
selection: selection,
visible: true
}
}
}
双模态编辑系统:Paper 与 Edgeless 的技术融合
统一渲染引擎
AFFiNE 最独特的技术特性是其双模态编辑系统:Paper(文档模式)和 Edgeless(白板模式)。这两种模式不是简单的界面切换,而是基于统一的渲染引擎实现的深度融合。
// 统一渲染管理器
class UnifiedRenderer {
private engine: RenderEngine
private modes: Map<string, RenderMode>
constructor() {
this.engine = new RenderEngine()
this.modes = new Map([
['paper', new PaperMode(this.engine)],
['edgeless', new EdgelessMode(this.engine)]
])
}
// 模式切换
switchMode(mode: 'paper' | 'edgeless', blockId: string) {
const currentBlock = this.engine.getBlock(blockId)
// 保存当前模式的状态
const currentMode = this.engine.getCurrentMode()
currentBlock.setState(currentMode.serialize())
// 切换到新模式
this.engine.setMode(this.modes.get(mode))
const newMode = this.modes.get(mode)
// 恢复块在新模式下的状态
const savedState = currentBlock.getState(mode)
if (savedState) {
newMode.deserialize(savedState)
}
// 触发重新渲染
this.engine.render()
}
// 跨模式内容同步
syncContentAcrossModes(blockId: string) {
const block = this.engine.getBlock(blockId)
const content = block.getContent()
// 确保两种模式下的内容保持同步
this.modes.forEach(mode => {
mode.updateContent(blockId, content)
})
}
}
块级画布的实现
AFFiNE 的 "块级画布" 技术允许将任何类型的块放置在无限画布上,这是通过特殊的布局算法实现的:
// 画布布局引擎
class CanvasLayoutEngine {
private blocks: Map<string, BlockWithPosition>
private viewport: Viewport
private grid: GridSystem
constructor() {
this.viewport = new Viewport()
this.grid = new GridSystem(20) // 20px网格对齐
}
// 放置块到画布
placeBlock(blockId: string, x: number, y: number): BlockPosition {
const snappedPosition = this.grid.snap(x, y)
// 检查碰撞
const collision = this.detectCollision(blockId, snappedPosition)
if (collision) {
return this.findFreePosition(snappedPosition)
}
const position = {
x: snappedPosition.x,
y: snappedPosition.y,
width: this.getBlockDefaultWidth(blockId),
height: this.getBlockDefaultHeight(blockId)
}
this.blocks.set(blockId, {
block: this.getBlock(blockId),
position: position,
zIndex: this.calculateZIndex(blockId)
})
return position
}
// 画布渲染
render(canvas: HTMLCanvasElement | HTMLElement) {
const ctx = canvas.getContext('2d')
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 绘制网格
this.grid.render(ctx)
// 按z-index排序渲染块
const sortedBlocks = Array.from(this.blocks.values())
.sort((a, b) => a.zIndex - b.zIndex)
sortedBlocks.forEach(blockWithPos => {
this.renderBlock(ctx, blockWithPos.block, blockWithPos.position)
})
}
// 连接线渲染
renderConnections(ctx: CanvasRenderingContext2D) {
const connections = this.findConnections()
connections.forEach(connection => {
const from = this.getBlockPosition(connection.from)
const to = this.getBlockPosition(connection.to)
// 贝塞尔曲线连接
this.drawBezierCurve(ctx, from.center, to.center, connection.style)
})
}
}
企业级部署:架构配置与最佳实践
私有化部署方案
AFFiNE 的企业级部署需要考虑性能、安全性和可维护性。以下是典型的部署架构:
# docker-compose.yml
version: '3.8'
services:
affine-app:
image: ghcr.io/toeverything/affine:stable
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://user:pass@postgres:5432/affine
- REDIS_URL=redis://redis:6379
- STORAGE_PATH=/app/data
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
volumes:
- affine-data:/app/data
- affine-backups:/app/backups
depends_on:
- postgres
- redis
restart: unless-stopped
postgres:
image: postgres:15
environment:
- POSTGRES_DB=affine
- POSTGRES_USER=user
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
restart: unless-stopped
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis-data:/data
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/ssl
depends_on:
- affine-app
restart: unless-stopped
volumes:
affine-data:
affine-backups:
postgres-data:
redis-data:
性能优化配置
// AFFiNE性能配置
interface AFFiNEConfig {
// 渲染优化
rendering: {
enableWebGL: boolean
renderDebounceMs: number
maxConcurrentRenders: number
}
// 协作优化
collaboration: {
maxConnectionsPerRoom: number
syncIntervalMs: number
conflictResolutionTimeout: number
}
// 存储优化
storage: {
compressionEnabled: boolean
cacheSizeMB: number
encryptionBatchSize: number
}
// 网络优化
network: {
maxFileSizeMB: number
uploadTimeoutMs: number
retryAttempts: number
}
}
const productionConfig: AFFiNEConfig = {
rendering: {
enableWebGL: true,
renderDebounceMs: 16, // 约60fps
maxConcurrentRenders: 4
},
collaboration: {
maxConnectionsPerRoom: 50,
syncIntervalMs: 100,
conflictResolutionTimeout: 5000
},
storage: {
compressionEnabled: true,
cacheSizeMB: 256,
encryptionBatchSize: 10
},
network: {
maxFileSizeMB: 100,
uploadTimeoutMs: 30000,
retryAttempts: 3
}
}
安全加固措施
# nginx.conf 安全配置
server {
listen 443 ssl http2;
server_name affine.example.com;
ssl_certificate /etc/ssl/certs/affine.crt;
ssl_certificate_key /etc/ssl/private/affine.key;
# 安全头部
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';" always;
# HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# 请求体限制
client_max_body_size 100M;
client_body_timeout 30s;
# 限流
limit_req_zone $binary_remote_addr zone=affine:10m rate=10r/s;
location / {
limit_req zone=affine burst=20 nodelay;
proxy_pass http://affine-app:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket支持
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
技术对比分析:开源架构 vs 商业产品
架构哲学差异
| 技术维度 | AFFiNE (开源) | Notion (商业) | Miro (商业) |
|---|---|---|---|
| 数据存储 | 本地优先 + 云同步 | 纯云端托管 | 纯云端托管 |
| 协作算法 | CRDT (Yjs) | OT (自研) | OT (自研) |
| 隐私保护 | 端到端加密 | 服务端加密 | 服务端加密 |
| 扩展性 | 插件系统 + 开源 | 内部 API | 内部 API |
| 部署方式 | 私有化部署可选 | 托管服务 | 托管服务 |
| 性能优化 | 客户端优先 | 服务端优先 | 服务端优先 |
技术创新对比
AFFiNE 相比商业产品的技术创新主要体现在:
- CRDT 架构的深度应用:不是简单的功能实现,而是将 CRDT 作为核心架构原则
- 块协议的标准化:BlockSuite 为协作编辑提供了可复用的基础组件
- 本地优先的用户体验:离线功能的完整性达到原生应用水平
- 开源生态的可持续性:技术透明度和社区驱动的创新模式
工程实践启示:构建下一代知识管理系统
架构设计原则
从 AFFiNE 的技术实践中,我们可以提取出构建现代知识管理系统的关键原则:
- 数据主权优先:用户必须对自己的数据拥有完整控制权
- 协作原生设计:协作能力不应是后期添加的功能,而是架构的基础
- 性能与隐私平衡:通过客户端计算和智能缓存兼顾性能和安全
- 开放标准:采用开放协议和可扩展的插件系统
技术选型建议
// 现代知识管理系统的技术栈建议
interface TechStackRecommendation {
// 前端架构
frontend: {
framework: 'React' | 'Vue' | 'SolidJS'
stateManagement: 'CRDT-based (Yjs/Automerge)'
rendering: 'Canvas + DOM hybrid'
buildTool: 'Vite' | 'Webpack 5'
}
// 协作算法
collaboration: {
algorithm: 'CRDT' // 优于OT for offline-first
library: 'Yjs' | 'Automerge' | 'Y.js'
transport: 'WebRTC + WebSocket fallback'
conflictResolution: 'Operational transforms for UX'
}
// 后端架构
backend: {
language: 'Rust' | 'Go' | 'Node.js'
database: 'PostgreSQL' | 'SQLite' // local-first
realTime: 'WebSocket' | 'WebRTC'
storage: 'IPFS' | 'S3-compatible' // for file storage
}
// 安全架构
security: {
encryption: 'AES-256-GCM'
keyManagement: 'PBKDF2 + salt'
transport: 'TLS 1.3'
authentication: 'JWT' | 'OAuth 2.0'
}
}
部署架构模式
# 现代知识管理系统部署架构
apiVersion: v1
kind: ConfigMap
metadata:
name: affine-config
data:
config.yaml: |
server:
port: 3000
host: 0.0.0.0
database:
type: postgresql
connectionString: ${DATABASE_URL}
poolSize: 20
redis:
url: ${REDIS_URL}
ttl: 3600
storage:
type: local
path: /app/data
encryption:
enabled: true
algorithm: AES-256-GCM
collaboration:
maxRoomSize: 100
syncInterval: 100ms
conflictTimeout: 5000ms
security:
enableCORS: true
enableCSP: true
sessionTimeout: 24h
结语:开源协作的工程化实践
AFFiNE 的成功不仅仅在于其功能的丰富性,更在于其对现代协作工具技术架构的重新思考。通过 CRDT、块协议和本地优先设计的巧妙结合,AFFiNE 证明了一个开源项目完全可以在技术上超越大型商业产品。
从工程实践角度看,AFFiNE 为构建下一代知识管理系统提供了宝贵的经验:用户数据主权必须得到技术层面的保障,协作能力需要内建于架构而非后期添加,性能优化不能以牺牲隐私为代价,开源生态的透明性和可扩展性是长期竞争力的基础。
随着远程工作和数据合规要求的不断发展,类似 AFFiNE 这样注重隐私、开放协作的技术架构将成为知识管理领域的主流。AFFiNE 用开源的方式证明了技术的可能性,也为整个行业指明了前进的方向。
参考资料: