@@ -94,7 +119,7 @@ export function LogsWorkspace(): React.ReactElement {
日志
-
+
被阻止
+ setStartDate(event.target.value)}
+ />
+ setEndDate(event.target.value)}
+ />
{loading ? (
@@ -140,12 +177,22 @@ export function LogsWorkspace(): React.ReactElement {
return (
-
{purposeLabel(entry.purpose)}
-
发送给 {recipient}
+
+ {purposeLabel(entry.purpose)}
+
+
+ 发送给{' '}
+
+ {recipient}
+
+
{result.label}
@@ -154,17 +201,64 @@ export function LogsWorkspace(): React.ReactElement {
-
-
- {entry.recipientType === 'group' ? '群聊' : '联系人'} · {entry.recipientId}
-
-
+
+
{contentTypeLabel(entry.contentType)} · {entry.contentPreview || '无内容预览'}
-
+
+ {entry.reason ? (
+
+ {entry.reason}
+
+ ) : null}
+
+ {triggerLabel(entry.triggerType)} · {sourceLabel(entry.source)}
+
- {entry.reason ? (
- {entry.reason}
- ) : null}
+
+ 查看详情
+
+ - 接收对象 ID
+ -
+ {entry.recipientId}
+
+ - 操作 ID
+ -
+ {entry.id}
+
+ {entry.executionId ? (
+ <>
+ - 执行 ID
+ -
+ {entry.executionId}
+
+ >
+ ) : null}
+ {entry.idempotencyKey ? (
+ <>
+ - 幂等键
+ -
+ {entry.idempotencyKey}
+
+ >
+ ) : null}
+ - 来源
+ -
+ {entry.source}
+
+ - 用途
+ -
+ {entry.purpose}
+
+ - 触发方式
+ - {entry.triggerType || 'automation'}
+
+
)
})}
diff --git a/src/renderer/src/features/settings/SettingsWorkspace.tsx b/src/renderer/src/features/settings/SettingsWorkspace.tsx
index 769d866..9e84a96 100644
--- a/src/renderer/src/features/settings/SettingsWorkspace.tsx
+++ b/src/renderer/src/features/settings/SettingsWorkspace.tsx
@@ -14,6 +14,7 @@ import { AboutPage } from './pages/AboutPage'
import { VoiceRecognitionPage } from './pages/VoiceRecognitionPage'
import { TextToSpeechPage } from './pages/TextToSpeechPage'
import { PersonalWechatSendPage } from './pages/PersonalWechatSendPage'
+import { LogsWorkspace } from '../logs/LogsWorkspace'
import type { Contact } from '../../../../shared/types'
import type { AIRuntimeModelConfig } from '../../../../shared/ai-provider'
@@ -102,6 +103,8 @@ export function SettingsWorkspace({
onOpenTextToSpeechSettings={onOpenTextToSpeechSettings}
/>
)
+ case 'wechat-action-logs':
+ return
case 'text-to-speech':
return
case 'personal-wechat-send':
diff --git a/src/renderer/src/features/settings/model/settingsNavigation.ts b/src/renderer/src/features/settings/model/settingsNavigation.ts
index abd7a36..5218a05 100644
--- a/src/renderer/src/features/settings/model/settingsNavigation.ts
+++ b/src/renderer/src/features/settings/model/settingsNavigation.ts
@@ -18,6 +18,7 @@ export const SETTINGS_NAVIGATION: SettingsNavigationGroup[] = [
label: '智能能力',
items: [
{ id: 'wechat-send', label: '微信发送' },
+ { id: 'wechat-action-logs', label: '发送日志' },
{ id: 'voice-recognition', label: '语音转文字' },
{ id: 'text-to-speech', label: '文字转语音' },
{ id: 'ai-model', label: 'AI 模型' }
diff --git a/src/renderer/src/features/settings/model/types.ts b/src/renderer/src/features/settings/model/types.ts
index e57a0e1..1b4af93 100644
--- a/src/renderer/src/features/settings/model/types.ts
+++ b/src/renderer/src/features/settings/model/types.ts
@@ -4,6 +4,7 @@ export type SettingsCategoryId =
| 'image-key'
| 'voice-recognition'
| 'wechat-send'
+ | 'wechat-action-logs'
| 'text-to-speech'
| 'personal-wechat-send'
| 'ai-model'
diff --git a/src/shared/action-log.ts b/src/shared/action-log.ts
index 4c0e835..0e4af24 100644
--- a/src/shared/action-log.ts
+++ b/src/shared/action-log.ts
@@ -4,7 +4,8 @@ import type {
WechatActionOrigin,
WechatActionPurpose,
WechatActionRecipient,
- WechatActionStatus
+ WechatActionStatus,
+ WechatActionTriggerType
} from './wechat-action'
export interface ActionLogEntry {
@@ -12,6 +13,7 @@ export interface ActionLogEntry {
category: 'wechat_send'
source: WechatActionOrigin
purpose: WechatActionPurpose
+ triggerType?: WechatActionTriggerType
timestamp: string
recipientType: WechatActionRecipient['type']
recipientId: string
@@ -21,4 +23,6 @@ export interface ActionLogEntry {
status: WechatActionStatus
errorCode?: WechatActionErrorCode
reason?: string
+ executionId?: string
+ idempotencyKey?: string
}
diff --git a/tests/component/logs-workspace.test.tsx b/tests/component/logs-workspace.test.tsx
index 8f36302..fc9c698 100644
--- a/tests/component/logs-workspace.test.tsx
+++ b/tests/component/logs-workspace.test.tsx
@@ -1,4 +1,4 @@
-import { render, screen, waitFor } from '@testing-library/react'
+import { render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import { LogsWorkspace } from '../../src/renderer/src/features/logs/LogsWorkspace'
@@ -10,19 +10,23 @@ const entries: ActionLogEntry[] = [
category: 'wechat_send',
source: 'member_monitor',
purpose: 'member_left_notification',
+ triggerType: 'automation',
timestamp: '2026-09-03T08:30:21.000Z',
recipientType: 'group',
recipientId: '123456@chatroom',
recipientName: '测试 q1',
contentType: 'text',
contentPreview: 'Shinven 已退出群聊',
- status: 'sent'
+ status: 'sent',
+ executionId: 'execution-member-1',
+ idempotencyKey: 'member_left_notification:member-log'
},
{
id: 'report-log',
category: 'wechat_send',
source: 'scheduled_report',
purpose: 'scheduled_report',
+ triggerType: 'automation',
timestamp: '2026-09-03T07:00:00.000Z',
recipientType: 'group',
recipientId: 'report@chatroom',
@@ -31,7 +35,26 @@ const entries: ActionLogEntry[] = [
contentPreview: 'daily-report.png',
status: 'failed',
errorCode: 'SEND_CAPABILITY_UNAVAILABLE',
- reason: '个人微信发送能力不可用'
+ reason: '个人微信发送能力不可用',
+ executionId: 'execution-report-1',
+ idempotencyKey: 'scheduled_report:report-log'
+ },
+ {
+ id: 'blocked-log',
+ category: 'wechat_send',
+ source: 'user_tts',
+ purpose: 'tts_voice',
+ triggerType: 'user',
+ timestamp: '2026-09-03T06:00:00.000Z',
+ recipientType: 'contact',
+ recipientId: 'wxid_long_recipient_1234567890@chatroom.example',
+ recipientName: '文件传输助手',
+ contentType: 'voice',
+ contentPreview: 'voice_xxx.silk 中文正文可以自然换行',
+ status: 'blocked',
+ executionId: 'execution-blocked-1234567890',
+ idempotencyKey:
+ 'member_left_notification:49023470180@chatroom:2026-09-03T06:00:00.000Z:very-long-tail'
}
]
@@ -49,6 +72,18 @@ describe('LogsWorkspace', () => {
await waitFor(() => expect(screen.getByText(/Shinven 已退出群聊/)).toBeVisible())
expect(screen.getByText(/daily-report.png/)).toBeVisible()
expect(screen.getByText('发送能力不可用')).toBeVisible()
+ expect(screen.getByText(/自动化 · 退群监控/)).toBeVisible()
+ expect(screen.getByText('被阻止')).toBeVisible()
+ expect(screen.queryByText('执行 ID:execution-member-1')).not.toBeInTheDocument()
+
+ const details = screen.getAllByText('查看详情')[0]
+ await user.click(details)
+ expect(screen.getByText('execution-member-1')).toBeVisible()
+ expect(screen.getByText('member_left_notification:member-log')).toBeVisible()
+ expect(screen.getByText('member_left_notification:member-log')).toHaveClass(
+ 'whitespace-nowrap',
+ 'font-mono'
+ )
const search = screen.getByRole('searchbox', { name: '搜索日志' })
await user.type(search, '123456')
@@ -64,5 +99,52 @@ describe('LogsWorkspace', () => {
await user.click(screen.getByRole('combobox', { name: '筛选日志类型' }))
await user.click(await screen.findByRole('option', { name: '退群通知' }))
expect(screen.getByText('没有匹配的日志')).toBeVisible()
+
+ await user.click(screen.getByRole('combobox', { name: '筛选日志类型' }))
+ await user.click(await screen.findByRole('option', { name: '全部类型' }))
+ await user.click(screen.getByRole('combobox', { name: '筛选日志状态' }))
+ await user.click(await screen.findByRole('option', { name: '全部状态' }))
+ const startDate = document.querySelector('input[aria-label="开始日期"]')
+ const endDate = document.querySelector('input[aria-label="结束日期"]')
+ expect(startDate).not.toBeNull()
+ expect(endDate).not.toBeNull()
+ await user.clear(startDate!)
+ await user.type(startDate!, '2026-09-03')
+ await user.clear(endDate!)
+ await user.type(endDate!, '2026-09-03')
+ expect(screen.getByText(/Shinven 已退出群聊/)).toBeVisible()
+ expect(screen.getByText(/daily-report.png/)).toBeVisible()
+
+ await user.clear(startDate!)
+ await user.type(startDate!, '2026-09-04')
+ expect(screen.getByText('没有匹配的日志')).toBeVisible()
+ })
+
+ it('keeps long identifiers on one line and exposes complete values in details', async () => {
+ Object.defineProperty(window, 'api', {
+ configurable: true,
+ value: { listWechatActionLogs: vi.fn().mockResolvedValue(entries) } as unknown as typeof window.api
+ })
+ render()
+
+ const blockedCard = (await screen.findByText('被阻止')).closest('article')
+ expect(blockedCard).not.toBeNull()
+ const card = within(blockedCard!)
+ const recipient = card.getByTitle('wxid_long_recipient_1234567890@chatroom.example')
+ expect(recipient).toHaveClass('truncate', 'whitespace-nowrap')
+ const key = card.getByTitle(
+ 'member_left_notification:49023470180@chatroom:2026-09-03T06:00:00.000Z:very-long-tail'
+ )
+ expect(key).toHaveClass('truncate', 'whitespace-nowrap', 'font-mono')
+ expect(card.getByText('语音 · voice_xxx.silk 中文正文可以自然换行')).toHaveClass(
+ 'whitespace-normal',
+ 'break-words'
+ )
+
+ await userEvent.click(card.getByText('查看详情'))
+ expect(card.getByText('wxid_long_recipient_1234567890@chatroom.example')).toBeVisible()
+ expect(
+ card.getByText('member_left_notification:49023470180@chatroom:2026-09-03T06:00:00.000Z:very-long-tail')
+ ).toBeVisible()
})
})
diff --git a/tests/e2e/app.spec.ts b/tests/e2e/app.spec.ts
index 1bd02c4..c5ef78f 100644
--- a/tests/e2e/app.spec.ts
+++ b/tests/e2e/app.spec.ts
@@ -143,6 +143,26 @@ test('NAV-01 NAV-02 every top-level page is unique and switchable', async () =>
}
})
+test('LOGS-01 shows Action Gateway audit details and filters by date', async () => {
+ const fixture = await launchTestApp({ now: Date.parse('2026-09-12T10:00:00+08:00') })
+ try {
+ await fixture.page.getByRole('navigation', { name: '一级导航' }).getByRole('button', { name: '设置' }).click()
+ await fixture.page.getByRole('button', { name: '发送日志' }).click()
+ await expect(fixture.page.getByRole('heading', { name: '日志', exact: true })).toBeVisible()
+ await expect(
+ fixture.page.locator('article').filter({ hasText: '产品测试群' })
+ ).toBeVisible()
+ await fixture.page.getByText('查看详情').first().click()
+ await expect(fixture.page.getByText('fixture-execution', { exact: true })).toBeVisible()
+ await expect(fixture.page.getByText(/自动化 · 定时日报/)).toBeVisible()
+
+ await fixture.page.getByLabel('开始日期').fill('2026-09-13')
+ await expect(fixture.page.getByText('没有匹配的日志')).toBeVisible()
+ } finally {
+ await fixture.close()
+ }
+})
+
test('NAV-03 exit monitor page shows a member departure event and stays within the viewport', async () => {
const fixture = await launchTestApp()
const pageErrors: Error[] = []
diff --git a/tests/e2e/support/electron-main.cjs b/tests/e2e/support/electron-main.cjs
index 98264f5..ad4eac8 100644
--- a/tests/e2e/support/electron-main.cjs
+++ b/tests/e2e/support/electron-main.cjs
@@ -701,6 +701,24 @@ handle('db:getContactAvatars', (usernames) =>
.map((contact) => [contact.m_nsUsrName, contact.avatar])
)
)
+handle('wechat-action-log:list', () => [
+ {
+ id: 'fixture-action-log',
+ category: 'wechat_send',
+ source: 'scheduled_report',
+ purpose: 'scheduled_report',
+ triggerType: 'automation',
+ timestamp: new Date(fixtureNowMs - 60_000).toISOString(),
+ recipientType: 'group',
+ recipientId: 'group_regular@chatroom',
+ recipientName: '产品测试群',
+ contentType: 'image',
+ contentPreview: 'fixture-report.png',
+ status: 'sent',
+ executionId: 'fixture-execution',
+ idempotencyKey: 'scheduled_report:fixture-execution'
+ }
+])
handle('settings:getSelf', () => ({ ready: true, info: fixture.self }))
handle('db:getCachedMessages', (md5) => fixture.messages[md5] || [])
handle('db:getCachedMessagePage', (md5) => ({