Skip to content

Commit 89ffde6

Browse files
committed
feat(logs): 支持用量日志显示 IP 列
1 parent 0b7ae4e commit 89ffde6

14 files changed

Lines changed: 145 additions & 25 deletions

File tree

controller/user.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,6 +1207,7 @@ type UpdateUserSettingRequest struct {
12071207
UpstreamModelUpdateNotifyEnabled *bool `json:"upstream_model_update_notify_enabled,omitempty"`
12081208
AcceptUnsetModelRatioModel bool `json:"accept_unset_model_ratio_model"`
12091209
RecordIpLog bool `json:"record_ip_log"`
1210+
ShowIpInLogs *bool `json:"show_ip_in_logs,omitempty"`
12101211
}
12111212

12121213
func UpdateUserSetting(c *gin.Context) {
@@ -1301,6 +1302,10 @@ func UpdateUserSetting(c *gin.Context) {
13011302
if user.Role >= common.RoleAdminUser && req.UpstreamModelUpdateNotifyEnabled != nil {
13021303
upstreamModelUpdateNotifyEnabled = *req.UpstreamModelUpdateNotifyEnabled
13031304
}
1305+
showIpInLogs := existingSettings.ShowIpInLogs
1306+
if req.ShowIpInLogs != nil {
1307+
showIpInLogs = *req.ShowIpInLogs
1308+
}
13041309

13051310
// 构建设置
13061311
settings := dto.UserSetting{
@@ -1309,6 +1314,10 @@ func UpdateUserSetting(c *gin.Context) {
13091314
UpstreamModelUpdateNotifyEnabled: upstreamModelUpdateNotifyEnabled,
13101315
AcceptUnsetRatioModel: req.AcceptUnsetModelRatioModel,
13111316
RecordIpLog: req.RecordIpLog,
1317+
ShowIpInLogs: showIpInLogs,
1318+
SidebarModules: existingSettings.SidebarModules,
1319+
BillingPreference: existingSettings.BillingPreference,
1320+
Language: existingSettings.Language,
13121321
}
13131322

13141323
// 如果是webhook类型,添加webhook相关设置

dto/user_settings.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type UserSetting struct {
1313
UpstreamModelUpdateNotifyEnabled bool `json:"upstream_model_update_notify_enabled,omitempty"` // 是否接收上游模型更新定时检测通知(仅管理员)
1414
AcceptUnsetRatioModel bool `json:"accept_unset_model_ratio_model,omitempty"` // AcceptUnsetRatioModel 是否接受未设置价格的模型
1515
RecordIpLog bool `json:"record_ip_log,omitempty"` // 是否记录请求和错误日志IP
16+
ShowIpInLogs bool `json:"show_ip_in_logs,omitempty"` // 是否在使用日志列表显示IP
1617
SidebarModules string `json:"sidebar_modules,omitempty"` // SidebarModules 左侧边栏模块配置
1718
BillingPreference string `json:"billing_preference,omitempty"` // BillingPreference 扣费策略(订阅/钱包)
1819
Language string `json:"language,omitempty"` // Language 用户语言偏好 (zh, en)

web/default/src/features/profile/components/tabs/notification-tab.tsx

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { Bell, Loader2, Mail, Server, Webhook } from 'lucide-react'
2121
import { useTranslation } from 'react-i18next'
2222
import { toast } from 'sonner'
2323
import { ROLE } from '@/lib/roles'
24+
import { useAuthStore } from '@/stores/auth-store'
2425
import { Button } from '@/components/ui/button'
2526
import { Input } from '@/components/ui/input'
2627
import { Label } from '@/components/ui/label'
@@ -65,6 +66,8 @@ interface NotificationTabProps {
6566
export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
6667
const { t } = useTranslation()
6768
const isAdmin = (profile?.role ?? 0) >= ROLE.ADMIN
69+
const currentUser = useAuthStore((state) => state.auth.user)
70+
const setUser = useAuthStore((state) => state.auth.setUser)
6871
const [loading, setLoading] = useState(false)
6972
const [settings, setSettings] = useState<UserSettings>({
7073
notify_type: 'email',
@@ -78,6 +81,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
7881
gotify_priority: 5,
7982
accept_unset_model_ratio_model: false,
8083
record_ip_log: false,
84+
show_ip_in_logs: false,
8185
upstream_model_update_notify_enabled: false,
8286
})
8387

@@ -106,6 +110,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
106110
accept_unset_model_ratio_model:
107111
parsed.accept_unset_model_ratio_model || false,
108112
record_ip_log: parsed.record_ip_log || false,
113+
show_ip_in_logs: parsed.show_ip_in_logs || false,
109114
upstream_model_update_notify_enabled:
110115
parsed.upstream_model_update_notify_enabled || false,
111116
})
@@ -118,6 +123,17 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
118123
const response = await updateUserSettings(settings)
119124

120125
if (response.success) {
126+
if (currentUser) {
127+
const currentSetting =
128+
typeof currentUser.setting === 'string'
129+
? parseUserSettings(currentUser.setting)
130+
: currentUser.setting
131+
const nextSetting: Record<string, unknown> = {
132+
...(currentSetting || {}),
133+
...settings,
134+
}
135+
setUser({ ...currentUser, setting: nextSetting })
136+
}
121137
toast.success(t('Settings updated successfully'))
122138
onUpdate()
123139
} else {
@@ -375,19 +391,40 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
375391
</div>
376392

377393
{/* Record IP Log */}
378-
<div className='flex items-start justify-between gap-3 rounded-lg border p-3 sm:items-center sm:p-4'>
379-
<div className='space-y-0.5'>
380-
<Label htmlFor='recordIp'>{t('Record IP Address')}</Label>
381-
<p className='text-muted-foreground text-xs sm:text-sm'>
382-
{t('Log IP address for usage and error logs')}
383-
</p>
394+
<div className='grid gap-3 rounded-lg border p-3 sm:grid-cols-2 sm:p-4'>
395+
<div className='flex items-start justify-between gap-3 sm:items-center'>
396+
<div className='space-y-0.5'>
397+
<Label htmlFor='recordIp'>{t('Record IP Address')}</Label>
398+
<p className='text-muted-foreground text-xs sm:text-sm'>
399+
{t('Log IP address for usage and error logs')}
400+
</p>
401+
</div>
402+
<Switch
403+
id='recordIp'
404+
className='shrink-0'
405+
checked={settings.record_ip_log}
406+
onCheckedChange={(checked) =>
407+
updateField('record_ip_log', checked)
408+
}
409+
/>
410+
</div>
411+
412+
<div className='flex items-start justify-between gap-3 sm:items-center'>
413+
<div className='space-y-0.5'>
414+
<Label htmlFor='showIpInLogs'>{t('Show IP in Usage Logs')}</Label>
415+
<p className='text-muted-foreground text-xs sm:text-sm'>
416+
{t('Display the IP address column in usage logs')}
417+
</p>
418+
</div>
419+
<Switch
420+
id='showIpInLogs'
421+
className='shrink-0'
422+
checked={settings.show_ip_in_logs}
423+
onCheckedChange={(checked) =>
424+
updateField('show_ip_in_logs', checked)
425+
}
426+
/>
384427
</div>
385-
<Switch
386-
id='recordIp'
387-
className='shrink-0'
388-
checked={settings.record_ip_log}
389-
onCheckedChange={(checked) => updateField('record_ip_log', checked)}
390-
/>
391428
</div>
392429
</div>
393430

web/default/src/features/profile/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ export interface UserSettings {
114114
accept_unset_model_ratio_model?: boolean
115115
/** Record IP log */
116116
record_ip_log?: boolean
117+
/** Show IP address column in usage logs */
118+
show_ip_in_logs?: boolean
117119
/** Receive upstream model update notifications (admin only) */
118120
upstream_model_update_notify_enabled?: boolean
119121
/** Preferred interface/API response language */
@@ -144,6 +146,7 @@ export interface UpdateUserSettingsRequest {
144146
gotify_priority?: number
145147
accept_unset_model_ratio_model?: boolean
146148
record_ip_log?: boolean
149+
show_ip_in_logs?: boolean
147150
upstream_model_update_notify_enabled?: boolean
148151
}
149152

web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
1818
*/
1919
import { useState } from 'react'
2020
import { type ColumnDef } from '@tanstack/react-table'
21-
import { CircleAlert, GitBranch, Sparkles, KeyRound } from 'lucide-react'
21+
import { CircleAlert, GitBranch, Globe, Sparkles, KeyRound } from 'lucide-react'
2222
import { useTranslation } from 'react-i18next'
2323
import { getUserAvatarFallback, getUserAvatarStyle } from '@/lib/avatar'
2424
import { formatBillingCurrencyFromUSD } from '@/lib/currency'
@@ -272,7 +272,10 @@ function buildDetailSegments(
272272
return segments
273273
}
274274

275-
export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
275+
export function useCommonLogsColumns(
276+
isAdmin: boolean,
277+
showIpInLogs: boolean
278+
): ColumnDef<UsageLog>[] {
276279
const { t } = useTranslation()
277280
const columns: ColumnDef<UsageLog>[] = [
278281
{
@@ -577,6 +580,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
577580
},
578581
size: 160,
579582
})
583+
580584
columns.push(
581585
{
582586
accessorKey: 'model_name',
@@ -806,6 +810,37 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
806810
},
807811
},
808812

813+
...(showIpInLogs
814+
? [
815+
{
816+
accessorKey: 'ip',
817+
header: t('IP Address'),
818+
cell: function IpCell({ row }) {
819+
const { sensitiveVisible } = useUsageLogsContext()
820+
const log = row.original
821+
if (!log.ip) {
822+
return (
823+
<span className='text-muted-foreground/50 text-xs'>-</span>
824+
)
825+
}
826+
827+
const ipLabel = sensitiveVisible ? log.ip : '••••'
828+
return (
829+
<StatusBadge
830+
label={ipLabel}
831+
icon={Globe}
832+
copyText={sensitiveVisible ? log.ip : undefined}
833+
size='sm'
834+
showDot={false}
835+
className='border-border/60 bg-muted/30 text-foreground h-6 max-w-[150px] gap-1.5 overflow-hidden rounded-md border px-2 py-0.5 font-mono'
836+
/>
837+
)
838+
},
839+
size: 150,
840+
} satisfies ColumnDef<UsageLog>,
841+
]
842+
: []),
843+
809844
{
810845
accessorKey: 'content',
811846
header: t('Details'),

web/default/src/features/usage-logs/components/usage-logs-mobile-card.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ function CommonLogsCard<TData>({
217217
cell={cells.get('token_name')}
218218
valueClassName='[&_.flex-col]:max-w-none [&_.flex-col>*:not(:first-child)]:text-[11px] [&_.flex-col>*:not(:first-child)]:leading-none'
219219
/>
220+
<SummaryField label={t('IP Address')} cell={cells.get('ip')} />
220221
<SummaryField
221222
label={t('Timing')}
222223
cell={cells.get('use_time')}

web/default/src/features/usage-logs/components/usage-logs-table.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { toast } from 'sonner'
2525
import { cn } from '@/lib/utils'
2626
import { useIsAdmin } from '@/hooks/use-admin'
2727
import { useTableUrlState } from '@/hooks/use-table-url-state'
28+
import { useAuthStore } from '@/stores/auth-store'
2829
import {
2930
DataTablePage,
3031
DataTableRow,
@@ -61,6 +62,21 @@ function deserializeLogTypeFilter(value: unknown): unknown[] {
6162
return values.filter((item) => String(item) !== LOG_TYPE_ALL_VALUE)
6263
}
6364

65+
function getShowIpInLogsSetting(setting: unknown): boolean {
66+
if (!setting) return false
67+
if (typeof setting === 'object') {
68+
return Boolean((setting as Record<string, unknown>).show_ip_in_logs)
69+
}
70+
if (typeof setting !== 'string') return false
71+
72+
try {
73+
const parsed = JSON.parse(setting) as Record<string, unknown>
74+
return Boolean(parsed.show_ip_in_logs)
75+
} catch {
76+
return false
77+
}
78+
}
79+
6480
interface UsageLogsTableProps {
6581
logCategory: LogCategory
6682
}
@@ -70,6 +86,8 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) {
7086
const isAdmin = useIsAdmin()
7187
const isMobile = useMediaQuery('(max-width: 640px)')
7288
const searchParams = route.useSearch()
89+
const userSetting = useAuthStore((state) => state.auth.user?.setting)
90+
const showIpInLogs = getShowIpInLogsSetting(userSetting)
7391

7492
const {
7593
columnFilters,
@@ -146,7 +164,7 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) {
146164
})
147165

148166
const logs = data?.items || []
149-
const columns = useColumnsByCategory(logCategory, isAdmin)
167+
const columns = useColumnsByCategory(logCategory, isAdmin, showIpInLogs)
150168
const isLoadingData = isLoading || (isFetching && !data)
151169

152170
const { table } = useDataTable({

web/default/src/features/usage-logs/lib/columns.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,29 +23,33 @@ import type { ColumnDef } from '@tanstack/react-table'
2323
import { useCommonLogsColumns } from '../components/columns/common-logs-columns'
2424
import { useDrawingLogsColumns } from '../components/columns/drawing-logs-columns'
2525
import { useTaskLogsColumns } from '../components/columns/task-logs-columns'
26-
import type { LogCategory } from '../types'
26+
import type { LogCategory, MidjourneyLog, TaskLog } from '../types'
27+
import type { UsageLog } from '../data/schema'
28+
29+
type UsageLogsRow = UsageLog | MidjourneyLog | TaskLog
2730

2831
/**
2932
* Get column definitions based on log category
30-
* Returns any[] due to different log types (UsageLog, MjProxy log, TaskLog)
33+
* Returns a union row type because the table switches between common,
34+
* drawing, and task log shapes at runtime.
3135
*/
3236
export function useColumnsByCategory(
3337
logCategory: LogCategory,
34-
isAdmin: boolean
35-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
36-
): ColumnDef<any>[] {
37-
const commonColumns = useCommonLogsColumns(isAdmin)
38+
isAdmin: boolean,
39+
showIpInLogs = false
40+
): ColumnDef<UsageLogsRow>[] {
41+
const commonColumns = useCommonLogsColumns(isAdmin, showIpInLogs)
3842
const drawingColumns = useDrawingLogsColumns(isAdmin)
3943
const taskColumns = useTaskLogsColumns(isAdmin)
4044

4145
switch (logCategory) {
4246
case 'common':
43-
return commonColumns
47+
return commonColumns as ColumnDef<UsageLogsRow>[]
4448
case 'drawing':
45-
return drawingColumns
49+
return drawingColumns as ColumnDef<UsageLogsRow>[]
4650
case 'task':
47-
return taskColumns
51+
return taskColumns as ColumnDef<UsageLogsRow>[]
4852
default:
49-
return commonColumns
53+
return commonColumns as ColumnDef<UsageLogsRow>[]
5054
}
5155
}

web/default/src/i18n/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,6 +1332,7 @@
13321332
"Display name for this payment method.": "Display name for this payment method.",
13331333
"Display name shown to users.": "Display name shown to users.",
13341334
"Display Options": "Display Options",
1335+
"Display the IP address column in usage logs": "Display the IP address column in usage logs",
13351336
"Display Token Statistics": "Display Token Statistics",
13361337
"Displayed in": "Displayed in",
13371338
"Displays the mobile sidebar.": "Displays the mobile sidebar.",
@@ -3873,6 +3874,7 @@
38733874
"Show All": "Show All",
38743875
"Show sensitive data": "Show sensitive data",
38753876
"Show all providers including unbound": "Show all providers including unbound",
3877+
"Show IP in Usage Logs": "Show IP in Usage Logs",
38763878
"Show only bound providers": "Show only bound providers",
38773879
"Show or hide flow columns": "Show or hide flow columns",
38783880
"Show prices in currency instead of quota.": "Show prices in currency instead of quota.",

web/default/src/i18n/locales/fr.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,6 +1332,7 @@
13321332
"Display name for this payment method.": "Nom d'affichage pour ce mode de paiement.",
13331333
"Display name shown to users.": "Nom d'affichage affiché aux utilisateurs.",
13341334
"Display Options": "Options d'affichage",
1335+
"Display the IP address column in usage logs": "Afficher la colonne d'adresse IP dans les journaux d'utilisation",
13351336
"Display Token Statistics": "Afficher les statistiques des jetons",
13361337
"Displayed in": "Affiché en",
13371338
"Displays the mobile sidebar.": "Affiche la barre latérale mobile.",
@@ -3873,6 +3874,7 @@
38733874
"Show All": "Tout afficher",
38743875
"Show sensitive data": "Afficher les données sensibles",
38753876
"Show all providers including unbound": "Afficher tous les fournisseurs (y compris non liés)",
3877+
"Show IP in Usage Logs": "Afficher l'IP dans les journaux d'utilisation",
38763878
"Show only bound providers": "Afficher uniquement les fournisseurs liés",
38773879
"Show or hide flow columns": "Afficher ou masquer les colonnes du flux",
38783880
"Show prices in currency instead of quota.": "Afficher les prix en devise au lieu du quota.",

0 commit comments

Comments
 (0)