Skip to content

协议增强:实现 Airtable 风格的 Comments & Activity Timeline(Feed/Chatter 协议标准) #731

Description

@hotlong

背景

Airtable 等现代 SaaS 平台广泛采用了右侧 Activity Feed 面板,将字段变更、评论(支持@提及)、系统任务活动等融合为统一时间线,这为协同、审核、历史追踪带来极大便利。

当前 @objectstack/spec 协议已经有

  • feeds: true / activities: true / trackHistory: true 等能力开关(Data Protocol)
  • 字段级的审计开关和变更跟踪能力
  • UI 层有 record:activity 组件,但 record:chatter 未定义(空壳),也缺乏 Feed/Comment 数据结构

目标

  1. 对标 Airtable/Notion/Salesforce,补全和标准化 Comment/Feed 协议,支持 audit + comment + mention + reaction + 通知订阅(一致时间线)
  2. 让 UI 配置模式下能声明式插入"所有活动"侧边栏,兼容过滤、操作、@提及等能力

主要差距和需求

  • 缺少 FeedItem/Comment/Mention 数据协议(应统一 comments���field_change、task、note、approval 等为 FeedItemType 并支持 @Mentions、Reactions)
  • record:chatter 组件 props 必须补全,支持评论输入/过滤/订阅等配置
  • RecordActivity/Chatter 组件应支持过滤(all/changes/comments/... 过滤)、unifiedTimeline 等属性
  • Feed 评论支持 @提及(mention)、回复(线程)、reaction、编辑删除、附件
  • 补全 API 协议(GET/POST/PUT/DELETE /feed、/reactions、/subscribe 等端点约定)

推荐实现步骤

  1. 创建 src/data/feed.zod.ts,定义 FeedItemType、FeedItemSchema、MentionSchema、ReactionSchema
  2. 增强 component.zod.ts,扩展 RecordActivityProps/RecordChatterProps 属性,并修正 ComponentPropsMap
  3. 定义 src/data/subscription.zod.ts(通知订阅能力,可选)
  4. 明确 API 端点模型约定

相关参考

任务参考实现(可分多个 PR 完善)

  • 定义 feed 协议基础 zod schema
  • 增强 UI 层协议与 props
  • API 约定与安全
  • 完善测试
    test

对标 Airtable 截图中的 右侧 Activity 面板(评论 + 字段变更历史 + @ Mentions)


🔍 一、截图分析:Airtable 的 Comments & Activity 功能拆解

从你提供的 Airtable 截图 image1 可以看到:

功能区Airtable 实现关键能力
Activity 侧边栏记录详情页右侧固定面板独立区域、可折叠
Activity Feed"All activity" 下拉过滤 + 时间线字段变更 + 评论混合排列
字段变更历史显示字段名、旧值→新值、时间戳REGION → Asia-Pacific, STATUS → Active
评论 (Comments)底部 "Leave a comment" 输入框富文本、@mentions
通知铃铛面板顶部通知开关订阅/取消该记录的变更通知
操作者信息"You edited using Omni 1d ago"用户头像、来源、时间
扩展历史"Upgrade to extend your history!"付费门控的历史深度

🟢 二、当前协议已有的基础设施

A. Data Protocol — 能力开关(✅ 良好)

exportconstObjectCapabilities=z.object({trackHistory: z.boolean().default(false).describe('Enable field history tracking for audit compliance'),feeds: z.boolean().default(false).describe('Enable social feed, comments, and mentions (Chatter-like)'),activities: z.boolean().default(false).describe('Enable standard tasks and events tracking'),// ...});

评估:feeds: trueactivities: truetrackHistory: true 这三个开关已经存在,定义了启用的意图,但 没有对应��� Feed/Comment 数据模型协议

B. Field Protocol — 字段级变更追踪(✅ 存在)

trackFeedHistory: z.boolean().optional().describe('Track field changes in Chatter/activity feed (Salesforce pattern)'),auditTrail: z.boolean().default(false).describe('Enable detailed audit trail for this field'),

评估: 字段级别的 trackFeedHistory 可控制哪些字段的变更出现在 Feed 中(类似 Airtable 截图中只显示了 REGION, OPPORTUNITIES, STATUS 等关键字段变更)。

C. UI Protocol — Activity 组件(🟡 骨架存在)

exportconstRecordActivityProps=z.object({types: z.array(z.enum(['task','event','email','call','note'])).optional(),limit: z.number().int().positive().default(10),showCompleted: z.boolean().default(false),aria: AriaPropsSchema.optional(),});

以及 ComponentPropsMap 中的注册:

// Record'record:details': RecordDetailsProps,'record:related_list': RecordRelatedListProps,'record:highlights': RecordHighlightsProps,'record:activity': RecordActivityProps,'record:chatter': EmptyProps,// ← 空壳!'record:path': RecordPathProps,

D. Page Protocol — Lead Detail 示例用法(✅ 演示存在)

{type: 'record:activity',id: 'lead_activity',label: 'Activity Timeline',properties: {types: ['task','event','email','call','note'],limit: 20,showCompleted: false,},},// History Tab{type: 'record:related_list',id: 'field_history',label: 'Field History',properties: {objectName: 'field_history',relationshipField: 'record_id',columns: ['field','old_value','new_value','changed_by','changed_date'],},},

E. System Protocol — Audit(✅ 丰富)

src/system/audit.zod.ts 有完整的 AuditEventSchemaAuditEventActorAuditEventChange 等,适合后端存储。


🔴 三、关键差距分析(对标 Airtable)

#差距严重度说明
G1record:chatter 是 EmptyProps🔴 CriticalComments/Mentions 组件没有任何 Props 定义,无法声明式配置
G2缺少 FeedItem / Comment 数据模型🔴 Criticalfeeds: true 开关存在,但没有 src/data/feed.zod.ts 定义 Feed Item 的数据结构
G3Activity 缺少 comment 类型🟠 MajorRecordActivityProps.types 只有 ['task', 'event', 'email', 'call', 'note'],没有 'comment''field_change'
G4缺少 Activity Filter / Stream 控制🟠 MajorAirtable 有 "All activity" 下拉菜单可筛选(只看评论 / 只看变更),当前没有 filterMode
G5缺少通知订阅协议🟠 MajorAirtable 的铃铛图标控制记录级订阅,当前无 RecordSubscription 协议
G6缺少 @mention 协议🟡 MediumFeeds 描述提到 "mentions" 但无 Mention 的目标类型定义(user / team / record)
G7History 与 Comment 未统一 Timeline🟡 MediumCRM 示例中 Activity 和 History 放在不同 Tab,Airtable 是混合 Timeline
G8缺少 Reaction / Emoji 协议🟢 Nice-to-haveAirtable/Notion 风格的 reaction(👍❤️🎉)

🛠️ 四、推荐实现方案

Phase 1: Data Protocol — Feed & Comment 数据模型

需要创建 src/data/feed.zod.ts

// src/data/feed.zod.ts — Feed & Comment Data Protocolimport{z}from'zod';/** * Feed Item Type * Unified activity types for the record timeline */exportconstFeedItemType=z.enum(['comment',// User comment (rich text + mentions)'field_change',// Field value change (audit)'task',// Task created/completed'event',// Calendar event'email',// Email sent/received'call',// Phone call logged'note',// Internal note'file',// File uploaded/deleted'record_create',// Record was created'record_delete',// Record was deleted'approval',// Approval action (submit/approve/reject)'sharing',// Sharing change'system',// System-generated event]);exporttypeFeedItemType=z.infer<typeofFeedItemType>;/** * @mention Target */exportconstMentionSchema=z.object({type: z.enum(['user','team','record']).describe('Mention target type'),id: z.string().describe('Target ID'),name: z.string().describe('Display name for rendering'),offset: z.number().int().min(0).describe('Character offset in body text'),length: z.number().int().min(1).describe('Length of mention token in body text'),});exporttypeMention=z.infer<typeofMentionSchema>;/** * Field Change Entry (for field_change feed items) */exportconstFieldChangeEntrySchema=z.object({field: z.string().describe('Field machine name'),fieldLabel: z.string().optional().describe('Field display label'),oldValue: z.unknown().optional().describe('Previous value'),newValue: z.unknown().optional().describe('New value'),oldDisplayValue: z.string().optional().describe('Human-readable old value'),newDisplayValue: z.string().optional().describe('Human-readable new value'),});exporttypeFieldChangeEntry=z.infer<typeofFieldChangeEntrySchema>;/** * Reaction (Emoji Reaction) */exportconstReactionSchema=z.object({emoji: z.string().describe('Emoji character or shortcode (e.g., "👍", ":thumbsup:")'),userIds: z.array(z.string()).describe('Users who reacted'),count: z.number().int().min(1).describe('Total reaction count'),});exporttypeReaction=z.infer<typeofReactionSchema>;/** * Feed Item Schema * A single entry in the unified activity timeline. *  * @example Comment * { * type: 'comment', * body: 'Great progress on this deal! @jane.doe can you follow up?', * mentions: [{ type: 'user', id: 'user_123', name: 'Jane Doe', offset: 36, length: 9 }], * actor: { type: 'user', id: 'user_456', name: 'John Smith' } * } *  * @example Field Change * { * type: 'field_change', * changes: [ * { field: 'status', oldDisplayValue: 'New', newDisplayValue: 'Active' }, * { field: 'region', oldDisplayValue: '', newDisplayValue: 'Asia-Pacific' } * ], * actor: { type: 'user', id: 'user_456', name: 'John Smith' } * } */exportconstFeedItemSchema=z.object({/** Unique identifier */id: z.string().describe('Feed item ID'),/** Feed item type */type: FeedItemType.describe('Activity type'),/** Target record reference */object: z.string().describe('Object name (e.g., "account")'),recordId: z.string().describe('Record ID this feed item belongs to'),/** Actor (who performed the action) */actor: z.object({type: z.enum(['user','system','service','automation']).describe('Actor type'),id: z.string().describe('Actor ID'),name: z.string().optional().describe('Actor display name'),avatarUrl: z.string().url().optional().describe('Actor avatar URL'),source: z.string().optional().describe('Source application (e.g., "Omni", "API", "Studio")'),}).describe('Who performed this action'),/** Content (for comments/notes) */body: z.string().optional().describe('Rich text body (Markdown supported)'),/** @Mentions */mentions: z.array(MentionSchema).optional().describe('Mentioned users/teams/records'),/** Field changes (for field_change type) */changes: z.array(FieldChangeEntrySchema).optional().describe('Field-level changes'),/** Reactions */reactions: z.array(ReactionSchema).optional().describe('Emoji reactions on this item'),/** Reply threading */parentId: z.string().optional().describe('Parent feed item ID for threaded replies'),replyCount: z.number().int().min(0).default(0).describe('Number of replies'),/** Visibility */visibility: z.enum(['public','internal','private']).default('public').describe('Visibility: public (all users), internal (team only), private (author + mentioned)'),/** Timestamps */createdAt: z.string().datetime().describe('Creation timestamp'),updatedAt: z.string().datetime().optional().describe('Last edit timestamp'),editedAt: z.string().datetime().optional().describe('When comment was last edited'),isEdited: z.boolean().default(false).describe('Whether comment has been edited'),});exporttypeFeedItem=z.infer<typeofFeedItemSchema>;

Phase 2: UI Protocol — 增强 record:chatterrecord:activity

// Enhanced RecordActivityProps — Unified TimelineexportconstRecordActivityProps=z.object({/** Activity types to display (unified enum) */types: z.array(FeedItemType).optional().describe('Feed item types to show (default: all)'),/** Default filter mode (Airtable-style dropdown) */filterMode: z.enum(['all','comments_only','changes_only','tasks_only']).default('all').describe('Default activity filter'),/** Allow user to switch filter modes */showFilterToggle: z.boolean().default(true).describe('Show filter dropdown in panel header'),/** Pagination */limit: z.number().int().positive().default(20).describe('Number of items to load per page'),/** Show completed activities */showCompleted: z.boolean().default(false),/** Merge field_change + comment in a unified timeline */unifiedTimeline: z.boolean().default(true).describe('Mix field changes and comments in one timeline (Airtable style)'),/** Show the comment input box at the bottom */showCommentInput: z.boolean().default(true).describe('Show "Leave a comment" input at the bottom'),/** Enable @mentions in comments */enableMentions: z.boolean().default(true),/** Enable emoji reactions */enableReactions: z.boolean().default(false),/** Enable threaded replies */enableThreading: z.boolean().default(false),/** Show notification subscription toggle (bell icon) */showSubscriptionToggle: z.boolean().default(true).describe('Show bell icon for record-level notification subscription'),/** ARIA accessibility */aria: AriaPropsSchema.optional(),});// New: RecordChatterProps (replaces EmptyProps)exportconstRecordChatterProps=z.object({/** Panel position */position: z.enum(['sidebar','inline','drawer']).default('sidebar').describe('Where to render the chatter panel'),/** Panel width (for sidebar/drawer) */width: z.union([z.string(),z.number()]).optional().describe('Panel width (e.g., "350px", "30%")'),/** Collapsible */collapsible: z.boolean().default(true),defaultCollapsed: z.boolean().default(false),/** Feed configuration (delegates to RecordActivityProps) */feed: RecordActivityProps.optional(),/** ARIA accessibility */aria: AriaPropsSchema.optional(),});

Phase 3: API Protocol — Feed 端点

// API Contract for Feed operations// Would go in src/api/ or src/contracts/// GET /api/data/{object}/{recordId}/feed?type=all&limit=20&cursor=xxx// POST /api/data/{object}/{recordId}/feed { type: 'comment', body: '...' }// PUT /api/data/{object}/{recordId}/feed/{feedId} { body: '...' }// DELETE /api/data/{object}/{recordId}/feed/{feedId}// POST /api/data/{object}/{recordId}/feed/{feedId}/reactions { emoji: '👍' }// POST /api/data/{object}/{recordId}/subscribe { events: ['comment', 'field_change'] }// DELETE /api/data/{object}/{recordId}/subscribe

Phase 4: Record Subscription (通知)

// src/data/subscription.zod.ts — Record-Level Notification Subscriptionimport{z}from'zod';exportconstRecordSubscriptionSchema=z.object({/** Target */object: z.string().describe('Object name'),recordId: z.string().describe('Record ID'),/** Subscriber */userId: z.string().describe('Subscribing user ID'),/** Events to subscribe to */events: z.array(z.enum(['comment','mention','field_change','task','approval','all',])).default(['all']).describe('Event types to receive notifications for'),/** Notification channels */channels: z.array(z.enum(['in_app','email','push','slack'])).default(['in_app']).describe('Notification delivery channels'),/** Active */active: z.boolean().default(true),createdAt: z.string().datetime(),});exporttypeRecordSubscription=z.infer<typeofRecordSubscriptionSchema>;

📊 五、总结评分

维度当前评分实施后目标说明
Data Model (Feed/Comment)🔴 D⭐ A缺少 FeedItemSchema,开关有但无数据结构
UI Component (Chatter)🟡 C⭐ Arecord:chatter 空壳,record:activity 缺少 unified timeline
API (Feed CRUD)🔴 D⭐ AapiMethodshistory 但无 feed 端点协议
Field History Tracking🟢 B+⭐ AtrackFeedHistory + auditTrail 字段级已定义
Notification/Subscription🔴 D⭐ A无记录级订阅协议
@Mention🔴 D⭐ Afeeds 描述提及 mentions 但无数据结构

📋 实施优先级

  1. P0src/data/feed.zod.ts (FeedItem + Comment + FieldChange + Mention 数据模型)
  2. P0 — 增强 RecordActivityProps (unifiedTimeline, filterMode, commentInput)
  3. P0 — 实现 RecordChatterProps 替换 EmptyProps
  4. P1src/data/subscription.zod.ts (记录级通知订阅)
  5. P1 — API Contract for Feed CRUD
  6. P2 — Reactions、Threading、Rich Media Attachments

需要我开 Issue 或直接创建 PR 来实现这些协议变更吗?

Metadata

Metadata

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions