-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Labels
Description
背景
Airtable 等现代 SaaS 平台广泛采用了右侧 Activity Feed 面板,将字段变更、评论(支持@提及)、系统任务活动等融合为统一时间线,这为协同、审核、历史追踪带来极大便利。
当前 @objectstack/spec 协议已经有
feeds: true/activities: true/trackHistory: true等能力开关(Data Protocol)- 字段级的审计开关和变更跟踪能力
- UI 层有
record:activity组件,但record:chatter未定义(空壳),也缺乏 Feed/Comment 数据结构
目标
- 对标 Airtable/Notion/Salesforce,补全和标准化 Comment/Feed 协议,支持 audit + comment + mention + reaction + 通知订阅(一致时间线)
- 让 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 等端点约定)
推荐实现步骤
- 创建
src/data/feed.zod.ts,定义 FeedItemType、FeedItemSchema、MentionSchema、ReactionSchema - 增强
component.zod.ts,扩展RecordActivityProps/RecordChatterProps属性,并修正 ComponentPropsMap - 定义
src/data/subscription.zod.ts(通知订阅能力,可选) - 明确 API 端点模型约定
相关参考
-
- Airtable Chatter/Activity
- Salesforce Chatter, Notion 评论
- examples/app-crm/src/pages/lead_detail.page.ts
任务参考实现(可分多个 PR 完善)
- 定义 feed 协议基础 zod schema
- 增强 UI 层协议与 props
- API 约定与安全
- 完善测试
test
对标 Airtable 截图中的 右侧 Activity 面板(评论 + 字段变更历史 + @ Mentions)
🔍 一、截图分析:Airtable 的 Comments & Activity 功能拆解
| 功能区 | 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 — 能力开关(✅ 良好)
export const ObjectCapabilities = 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: true 和 activities: true 和 trackHistory: 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 组件(🟡 骨架存在)
export const RecordActivityProps = 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 有完整的 AuditEventSchema、AuditEventActor、AuditEventChange 等,适合后端存储。
🔴 三、关键差距分析(对标 Airtable)
| # | 差距 | 严重度 | 说明 |
|---|---|---|---|
| G1 | record:chatter 是 EmptyProps |
🔴 Critical | Comments/Mentions 组件没有任何 Props 定义,无法声明式配置 |
| G2 | 缺少 FeedItem / Comment 数据模型 |
🔴 Critical | feeds: true 开关存在,但没有 src/data/feed.zod.ts 定义 Feed Item 的数据结构 |
| G3 | Activity 缺少 comment 类型 |
🟠 Major | RecordActivityProps.types 只有 ['task', 'event', 'email', 'call', 'note'],没有 'comment' 和 'field_change' |
| G4 | 缺少 Activity Filter / Stream 控制 | 🟠 Major | Airtable 有 "All activity" 下拉菜单可筛选(只看评论 / 只看变更),当前没有 filterMode |
| G5 | 缺少通知订阅协议 | 🟠 Major | Airtable 的铃铛图标控制记录级订阅,当前无 RecordSubscription 协议 |
| G6 | 缺少 @mention 协议 |
🟡 Medium | Feeds 描述提到 "mentions" 但无 Mention 的目标类型定义(user / team / record) |
| G7 | History 与 Comment 未统一 Timeline | 🟡 Medium | CRM 示例中 Activity 和 History 放在不同 Tab,Airtable 是混合 Timeline |
| G8 | 缺少 Reaction / Emoji 协议 | 🟢 Nice-to-have | Airtable/Notion 风格的 reaction(👍❤️🎉) |
🛠️ 四、推荐实现方案
Phase 1: Data Protocol — Feed & Comment 数据模型
需要创建 src/data/feed.zod.ts:
// src/data/feed.zod.ts — Feed & Comment Data Protocol
import { z } from 'zod';
/**
* Feed Item Type
* Unified activity types for the record timeline
*/
export const FeedItemType = 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
]);
export type FeedItemType = z.infer<typeof FeedItemType>;
/**
* @mention Target
*/
export const MentionSchema = 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'),
});
export type Mention = z.infer<typeof MentionSchema>;
/**
* Field Change Entry (for field_change feed items)
*/
export const FieldChangeEntrySchema = 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'),
});
export type FieldChangeEntry = z.infer<typeof FieldChangeEntrySchema>;
/**
* Reaction (Emoji Reaction)
*/
export const ReactionSchema = 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'),
});
export type Reaction = z.infer<typeof ReactionSchema>;
/**
* 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' }
* }
*/
export const FeedItemSchema = 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'),
});
export type FeedItem = z.infer<typeof FeedItemSchema>;Phase 2: UI Protocol — 增强 record:chatter 和 record:activity
// Enhanced RecordActivityProps — Unified Timeline
export const RecordActivityProps = 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)
export const RecordChatterProps = 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}/subscribePhase 4: Record Subscription (通知)
// src/data/subscription.zod.ts — Record-Level Notification Subscription
import { z } from 'zod';
export const RecordSubscriptionSchema = 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(),
});
export type RecordSubscription = z.infer<typeof RecordSubscriptionSchema>;📊 五、总结评分
| 维度 | 当前评分 | 实施后目标 | 说明 |
|---|---|---|---|
| Data Model (Feed/Comment) | 🔴 D | ⭐ A | 缺少 FeedItemSchema,开关有但无数据结构 |
| UI Component (Chatter) | 🟡 C | ⭐ A | record:chatter 空壳,record:activity 缺少 unified timeline |
| API (Feed CRUD) | 🔴 D | ⭐ A | apiMethods 有 history 但无 feed 端点协议 |
| Field History Tracking | 🟢 B+ | ⭐ A | trackFeedHistory + auditTrail 字段级已定义 |
| Notification/Subscription | 🔴 D | ⭐ A | 无记录级订阅协议 |
| @Mention | 🔴 D | ⭐ A | feeds 描述提及 mentions 但无数据结构 |
📋 实施优先级
- P0 —
src/data/feed.zod.ts(FeedItem + Comment + FieldChange + Mention 数据模型) - P0 — 增强
RecordActivityProps(unifiedTimeline, filterMode, commentInput) - P0 — 实现
RecordChatterProps替换 EmptyProps - P1 —
src/data/subscription.zod.ts(记录级通知订阅) - P1 — API Contract for Feed CRUD
- P2 — Reactions、Threading、Rich Media Attachments
需要我开 Issue 或直接创建 PR 来实现这些协议变更吗?
Reactions are currently unavailable