diff --git a/mateclaw-server/src/main/java/vip/mate/activity/ActivityFeedController.java b/mateclaw-server/src/main/java/vip/mate/activity/ActivityFeedController.java
new file mode 100644
index 00000000..132b1183
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/activity/ActivityFeedController.java
@@ -0,0 +1,166 @@
+package vip.mate.activity;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+import vip.mate.approval.model.ToolApprovalEntity;
+import vip.mate.approval.repository.ToolApprovalMapper;
+import vip.mate.audit.model.AuditEventEntity;
+import vip.mate.audit.service.AuditEventService;
+import vip.mate.common.result.R;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * RFC-090 §4.5 / §7 — unified Activity feed.
+ *
+ *
Merges three sources into one chronologically-ordered stream:
+ *
+ * {@code audit_event} — CRUD-style events on agents / channels /
+ * skills / wiki / workspace (the existing audit log)
+ * {@code tool_approval} — approval requests + their resolution
+ * (granted / denied / expired). Ties tool gating decisions
+ * directly to the audit timeline.
+ * Successful tool calls — RFC §4.5 mentions these, but the
+ * runtime doesn't yet persist a row per successful call.
+ * Returning an empty bucket keeps the API contract stable so
+ * the UI can light up automatically once a future commit adds
+ * persistence.
+ *
+ *
+ * Pagination is best-effort: each source is paged from index 0
+ * up to {@code size * 2}, then the merged list is trimmed and offset
+ * in-memory. For workspaces with >>1k events / day a follow-up should
+ * push merging into SQL; this is good enough for v1.
+ */
+@Tag(name = "Activity Feed (RFC-090)")
+@RestController
+@RequestMapping("/api/v1/activity")
+@RequiredArgsConstructor
+public class ActivityFeedController {
+
+ private final AuditEventService auditEventService;
+ private final ToolApprovalMapper toolApprovalMapper;
+
+ @Operation(summary = "Unified activity feed (audit + approval + tool calls)")
+ @GetMapping("/feed")
+ public R> feed(
+ @RequestParam(required = false) Long workspaceId,
+ @RequestParam(required = false) String source,
+ @RequestParam(defaultValue = "1") int page,
+ @RequestParam(defaultValue = "20") int size) {
+ if (size <= 0 || size > 200) size = 20;
+ if (page <= 0) page = 1;
+
+ // Pull a chunk from each source large enough to cover the
+ // requested page after merge. We read at most page*size rows
+ // from each side; for stable cursors, future iterations should
+ // push the merge into SQL with a UNION ALL view.
+ int chunk = Math.max(size * page, 50);
+
+ List rows = new ArrayList<>();
+
+ boolean wantAudit = source == null || source.isBlank() || "audit".equalsIgnoreCase(source);
+ boolean wantApproval = source == null || source.isBlank() || "approval".equalsIgnoreCase(source);
+
+ if (wantAudit) {
+ try {
+ IPage audit = auditEventService.listEvents(
+ workspaceId, null, null, null, null, 1, chunk);
+ for (AuditEventEntity ev : audit.getRecords()) {
+ rows.add(fromAuditEvent(ev));
+ }
+ } catch (Exception ignored) { /* surface silence > one-source crash */ }
+ }
+ if (wantApproval) {
+ try {
+ Page p = new Page<>(1, chunk);
+ LambdaQueryWrapper qw = new LambdaQueryWrapper()
+ .orderByDesc(ToolApprovalEntity::getCreatedAt);
+ IPage approvals = toolApprovalMapper.selectPage(p, qw);
+ for (ToolApprovalEntity ap : approvals.getRecords()) {
+ rows.add(fromApproval(ap));
+ }
+ } catch (Exception ignored) { /* see above */ }
+ }
+
+ rows.sort(Comparator.comparing(ActivityRow::time, Comparator.nullsLast(Comparator.reverseOrder())));
+
+ long total = rows.size();
+ int from = Math.min((page - 1) * size, rows.size());
+ int to = Math.min(from + size, rows.size());
+ List sliced = rows.subList(from, to);
+
+ Map resp = new LinkedHashMap<>();
+ resp.put("page", page);
+ resp.put("size", size);
+ resp.put("total", total);
+ resp.put("records", sliced);
+ return R.ok(resp);
+ }
+
+ private ActivityRow fromAuditEvent(AuditEventEntity ev) {
+ Map detail = new LinkedHashMap<>();
+ detail.put("detailJson", ev.getDetailJson());
+ detail.put("userAgent", ev.getUserAgent());
+ detail.put("workspaceId", ev.getWorkspaceId());
+ return new ActivityRow(
+ "audit-" + ev.getId(),
+ "audit",
+ ev.getCreateTime(),
+ ev.getUsername(),
+ ev.getAction(),
+ ev.getResourceType(),
+ ev.getResourceName() != null ? ev.getResourceName() : ev.getResourceId(),
+ ev.getIpAddress(),
+ detail);
+ }
+
+ private ActivityRow fromApproval(ToolApprovalEntity ap) {
+ Map detail = new LinkedHashMap<>();
+ detail.put("toolArguments", ap.getToolArguments());
+ detail.put("summary", ap.getSummary());
+ detail.put("maxSeverity", ap.getMaxSeverity());
+ detail.put("status", ap.getStatus());
+ detail.put("resolvedAt", ap.getResolvedAt());
+ // Map approval status onto an audit-style action so the UI's
+ // existing action coloring (CREATE / DELETE / etc.) keeps
+ // working without a special case.
+ String action = "APPROVAL_" + (ap.getStatus() == null ? "PENDING" : ap.getStatus().toUpperCase());
+ return new ActivityRow(
+ "approval-" + ap.getId(),
+ "approval",
+ ap.getCreatedAt(),
+ ap.getResolvedBy() != null ? ap.getResolvedBy() : ap.getRequesterName(),
+ action,
+ "TOOL_APPROVAL",
+ ap.getToolName(),
+ null,
+ detail);
+ }
+
+ /**
+ * Wire-format row. Public record so Jackson serializes it directly
+ * without needing a separate DTO.
+ */
+ public record ActivityRow(
+ String id,
+ String source,
+ LocalDateTime time,
+ String username,
+ String action,
+ String resourceType,
+ String resourceName,
+ String ipAddress,
+ Map detail
+ ) {}
+}
diff --git a/mateclaw-ui/src/api/index.ts b/mateclaw-ui/src/api/index.ts
index 15e0df61..3cfbe384 100644
--- a/mateclaw-ui/src/api/index.ts
+++ b/mateclaw-ui/src/api/index.ts
@@ -177,6 +177,12 @@ export const skillApi = {
employees: (id: string | number) => http.get(`/skills/${id}/employees`),
}
+// ==================== Activity Feed (RFC-090 §4.5) ====================
+export const activityApi = {
+ feed: (params: { source?: string; page?: number; size?: number; workspaceId?: number } = {}) =>
+ http.get('/activity/feed', { params }),
+}
+
// ==================== ACP Endpoints (RFC-090 Phase 7) ====================
export const acpApi = {
list: () => http.get('/acp/endpoints'),
diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts
index 749bae1d..582a338e 100644
--- a/mateclaw-ui/src/i18n/locales/en-US.ts
+++ b/mateclaw-ui/src/i18n/locales/en-US.ts
@@ -800,13 +800,19 @@ export default {
},
activity: {
title: 'Activity Log',
- desc: 'View all create, update, and delete operations on workspace resources.',
+ desc: 'Unified feed: audit events + tool approvals (and successful tool calls when persistence lands).',
allActions: 'All Actions',
allResources: 'All Resources',
+ allSources: 'All Sources',
+ sourceAudit: 'Audit',
+ sourceApproval: 'Approval',
+ detailTitle: 'Event detail',
+ detailDetails: 'Detail',
loading: 'Loading...',
noEvents: 'No activity yet',
columns: {
time: 'Time',
+ source: 'Source',
user: 'User',
action: 'Action',
resource: 'Resource',
diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts
index 69bc1413..a13eb55d 100644
--- a/mateclaw-ui/src/i18n/locales/zh-CN.ts
+++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts
@@ -800,13 +800,19 @@ export default {
},
activity: {
title: '操作日志',
- desc: '查看工作区内所有资源的创建、修改、删除操作记录。',
+ desc: '聚合视图:审计事件 + 工具审批(成功 tool 调用待落库后自动接入)。',
allActions: '全部操作',
allResources: '全部资源',
+ allSources: '全部来源',
+ sourceAudit: '审计',
+ sourceApproval: '审批',
+ detailTitle: '事件详情',
+ detailDetails: '详情',
loading: '加载中...',
noEvents: '暂无操作记录',
columns: {
time: '时间',
+ source: '来源',
user: '用户',
action: '操作',
resource: '资源类型',
diff --git a/mateclaw-ui/src/views/Security/Activity/index.vue b/mateclaw-ui/src/views/Security/Activity/index.vue
index b9851be3..3c81b767 100644
--- a/mateclaw-ui/src/views/Security/Activity/index.vue
+++ b/mateclaw-ui/src/views/Security/Activity/index.vue
@@ -15,15 +15,23 @@
-
+
+
+ {{ t('security.activity.allSources') }}
+ {{ t('security.activity.sourceAudit') }}
+ {{ t('security.activity.sourceApproval') }}
+
+
{{ t('security.activity.allActions') }}
CREATE
UPDATE
DELETE
ENABLE
DISABLE
+ APPROVAL_GRANTED
+ APPROVAL_DENIED
-
+
{{ t('security.activity.allResources') }}
Agent
Channel
@@ -31,6 +39,7 @@
Wiki
Member
Workspace
+ Tool Approval
@@ -46,6 +55,7 @@
{{ t('security.activity.columns.time') }}
+ {{ t('security.activity.columns.source') }}
{{ t('security.activity.columns.user') }}
{{ t('security.activity.columns.action') }}
{{ t('security.activity.columns.resource') }}
@@ -54,10 +64,13 @@
-
- {{ formatTime(event.createTime) }}
+
+ {{ formatTime(event.time || event.createTime) }}
- {{ event.username }}
+ {{ event.source || 'audit' }}
+
+
+ {{ event.username || '—' }}
@@ -76,6 +89,28 @@
{{ t('security.activity.noEvents') }}
+
+
+
+
{{ t('security.activity.columns.time') }} {{ formatTime(detailEvent.time || detailEvent.createTime) }}
+
{{ t('security.activity.columns.source') }} {{ detailEvent.source }}
+
{{ t('security.activity.columns.user') }} {{ detailEvent.username || '—' }}
+
{{ t('security.activity.columns.action') }} {{ detailEvent.action }}
+
{{ t('security.activity.columns.resource') }} {{ detailEvent.resourceType }}: {{ detailEvent.resourceName || detailEvent.resourceId || '—' }}
+
{{ t('security.activity.columns.ip') }} {{ detailEvent.ipAddress }}
+
+
{{ t('security.activity.detailDetails') }}
+
{{ JSON.stringify(detailEvent.detail, null, 2) }}
+
+
+
+