Skip to content

Commit bfb3d29

Browse files
committed
添加普通用户日志审计知情权限及审计内容知情权限
1 parent b8852ad commit bfb3d29

14 files changed

Lines changed: 209 additions & 15 deletions

File tree

‎controller/misc.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ func GetStatus(c *gin.Context) {
9292
"password_login_enabled": common.PasswordLoginEnabled,
9393
"password_register_enabled": common.PasswordRegisterEnabled,
9494
"default_use_auto_group": setting.DefaultUseAutoGroup,
95+
"log_audit_enabled": common.LogRequestContentEnabled || common.LogResponseContentEnabled,
96+
"log_request_content_enabled": common.LogRequestContentEnabled,
97+
"log_response_content_enabled": common.LogResponseContentEnabled,
9598

9699
"usd_exchange_rate": operation_setting.USDExchangeRate,
97100
"price": operation_setting.Price,

‎model/log.go‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,56 @@ const (
6666
LogTypeRefund = 6
6767
)
6868

69+
func userVisibleAuditInfo(adminInfoValue interface{}) map[string]interface{} {
70+
if !common.LogRequestContentEnabled && !common.LogResponseContentEnabled {
71+
return nil
72+
}
73+
adminInfo, ok := adminInfoValue.(map[string]interface{})
74+
if !ok {
75+
return nil
76+
}
77+
78+
auditInfo := make(map[string]interface{})
79+
copyStringField := func(key string) {
80+
if value, ok := adminInfo[key].(string); ok && strings.TrimSpace(value) != "" {
81+
auditInfo[key] = value
82+
}
83+
}
84+
copyBoolField := func(key string) {
85+
if value, ok := adminInfo[key].(bool); ok && value {
86+
auditInfo[key] = value
87+
}
88+
}
89+
90+
if common.LogRequestContentEnabled {
91+
copyStringField("request_content")
92+
copyBoolField("request_content_truncated")
93+
}
94+
if common.LogResponseContentEnabled {
95+
copyStringField("response_content")
96+
copyBoolField("response_content_truncated")
97+
}
98+
if len(auditInfo) == 0 {
99+
return nil
100+
}
101+
return auditInfo
102+
}
103+
69104
func formatUserLogs(logs []*Log, startIdx int) {
70105
for i := range logs {
71106
logs[i].ChannelName = ""
72107
var otherMap map[string]interface{}
73108
otherMap, _ = common.StrToMap(logs[i].Other)
74109
if otherMap != nil {
110+
auditInfo := userVisibleAuditInfo(otherMap["admin_info"])
75111
// Remove admin-only debug fields.
76112
delete(otherMap, "admin_info")
113+
delete(otherMap, "audit_info")
77114
// delete(otherMap, "reject_reason")
78115
delete(otherMap, "stream_status")
116+
if auditInfo != nil {
117+
otherMap["audit_info"] = auditInfo
118+
}
79119
}
80120
logs[i].Other = common.MapToJsonStr(otherMap)
81121
logs[i].Id = startIdx + i + 1

‎model/log_test.go‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package model
2+
3+
import (
4+
"testing"
5+
6+
"github.com/MAX-API-Next/MAX-API/common"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func withLogAuditSettings(t *testing.T, requestEnabled bool, responseEnabled bool) {
11+
t.Helper()
12+
prevRequest := common.LogRequestContentEnabled
13+
prevResponse := common.LogResponseContentEnabled
14+
common.LogRequestContentEnabled = requestEnabled
15+
common.LogResponseContentEnabled = responseEnabled
16+
t.Cleanup(func() {
17+
common.LogRequestContentEnabled = prevRequest
18+
common.LogResponseContentEnabled = prevResponse
19+
})
20+
}
21+
22+
func TestFormatUserLogsExposesOnlyUserAuditContent(t *testing.T) {
23+
withLogAuditSettings(t, true, true)
24+
25+
logs := []*Log{
26+
{
27+
Other: common.MapToJsonStr(map[string]interface{}{
28+
"admin_info": map[string]interface{}{
29+
"request_content": "user prompt",
30+
"request_content_truncated": true,
31+
"response_content": "model answer",
32+
"response_content_truncated": true,
33+
"local_count_tokens": true,
34+
"use_channel": []int{1, 2},
35+
},
36+
"audit_info": map[string]interface{}{
37+
"request_content": "stale audit content",
38+
},
39+
"stream_status": map[string]interface{}{
40+
"status": "error",
41+
},
42+
}),
43+
},
44+
}
45+
46+
formatUserLogs(logs, 10)
47+
48+
other, err := common.StrToMap(logs[0].Other)
49+
require.NoError(t, err)
50+
require.NotContains(t, other, "admin_info")
51+
require.NotContains(t, other, "stream_status")
52+
require.Equal(t, 11, logs[0].Id)
53+
54+
auditInfo, ok := other["audit_info"].(map[string]interface{})
55+
require.True(t, ok)
56+
require.Equal(t, "user prompt", auditInfo["request_content"])
57+
require.Equal(t, true, auditInfo["request_content_truncated"])
58+
require.Equal(t, "model answer", auditInfo["response_content"])
59+
require.Equal(t, true, auditInfo["response_content_truncated"])
60+
require.NotContains(t, auditInfo, "local_count_tokens")
61+
require.NotContains(t, auditInfo, "use_channel")
62+
}
63+
64+
func TestFormatUserLogsHidesAuditContentWhenDisabled(t *testing.T) {
65+
withLogAuditSettings(t, false, false)
66+
67+
logs := []*Log{
68+
{
69+
Other: common.MapToJsonStr(map[string]interface{}{
70+
"admin_info": map[string]interface{}{
71+
"request_content": "user prompt",
72+
"response_content": "model answer",
73+
},
74+
}),
75+
},
76+
}
77+
78+
formatUserLogs(logs, 0)
79+
80+
other, err := common.StrToMap(logs[0].Other)
81+
require.NoError(t, err)
82+
require.NotContains(t, other, "admin_info")
83+
require.NotContains(t, other, "audit_info")
84+
}

‎web/default/src/features/auth/types.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ export interface SystemStatus {
128128
register_enabled?: boolean
129129
password_login_enabled?: boolean
130130
password_register_enabled?: boolean
131+
log_audit_enabled?: boolean
132+
log_request_content_enabled?: boolean
133+
log_response_content_enabled?: boolean
131134
custom_oauth_providers?: CustomOAuthProviderInfo[]
132135
[key: string]: unknown
133136
}
@@ -171,6 +174,9 @@ export interface SystemStatus {
171174
register_enabled?: boolean
172175
password_login_enabled?: boolean
173176
password_register_enabled?: boolean
177+
log_audit_enabled?: boolean
178+
log_request_content_enabled?: boolean
179+
log_response_content_enabled?: boolean
174180
custom_oauth_providers?: CustomOAuthProviderInfo[]
175181
[key: string]: unknown
176182
}

‎web/default/src/features/system-settings/security/log-audit-section.tsx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,15 @@ export function LogAuditSection({ defaultValues }: LogAuditSectionProps) {
9090
}
9191

9292
await queryClient.invalidateQueries({ queryKey: ['system-options'] })
93+
await queryClient.invalidateQueries({ queryKey: ['status'] })
9394
toast.success(t('Setting updated successfully'))
9495
} catch (error) {
9596
const message =
9697
error instanceof Error ? error.message : t('Failed to update setting')
9798
if (didApplyAny) {
9899
try {
99100
await queryClient.invalidateQueries({ queryKey: ['system-options'] })
101+
await queryClient.invalidateQueries({ queryKey: ['status'] })
100102
} catch {
101103
/* empty */
102104
}
@@ -130,7 +132,7 @@ export function LogAuditSection({ defaultValues }: LogAuditSectionProps) {
130132
<FormLabel>{t('Record request content')}</FormLabel>
131133
<FormDescription>
132134
{t(
133-
'Store request prompts and messages in admin-only usage log details.'
135+
'Store request prompts and messages in usage log details.'
134136
)}
135137
</FormDescription>
136138
</SettingsSwitchContent>
@@ -152,9 +154,7 @@ export function LogAuditSection({ defaultValues }: LogAuditSectionProps) {
152154
<SettingsSwitchContent>
153155
<FormLabel>{t('Record response content')}</FormLabel>
154156
<FormDescription>
155-
{t(
156-
'Store model output text in admin-only usage log details.'
157-
)}
157+
{t('Store model output text in usage log details.')}
158158
</FormDescription>
159159
</SettingsSwitchContent>
160160
<FormControl>

‎web/default/src/features/usage-logs/components/common-logs-filter-bar.tsx‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { type Table } from '@tanstack/react-table'
2323
import { Eye, EyeOff } from 'lucide-react'
2424
import { useTranslation } from 'react-i18next'
2525
import { useIsAdmin } from '@/hooks/use-admin'
26+
import { useStatus } from '@/hooks/use-status'
2627
import { Button } from '@/components/ui/button'
2728
import {
2829
Select,
@@ -37,6 +38,7 @@ import {
3738
TooltipContent,
3839
TooltipTrigger,
3940
} from '@/components/ui/tooltip'
41+
import { StatusBadge } from '@/components/status-badge'
4042
import { LOG_TYPE_ALL_VALUE, LOG_TYPE_FILTERS } from '../constants'
4143
import { buildSearchParams } from '../lib/filter'
4244
import { getDefaultTimeRange } from '../lib/utils'
@@ -71,8 +73,13 @@ export function CommonLogsFilterBar<TData>(
7173
const queryClient = useQueryClient()
7274
const searchParams = route.useSearch()
7375
const isAdmin = useIsAdmin()
76+
const { status } = useStatus()
7477
const { sensitiveVisible, setSensitiveVisible } = useUsageLogsContext()
7578
const fetchingLogs = useIsFetching({ queryKey: ['logs'] })
79+
const logAuditEnabled = Boolean(status?.log_audit_enabled)
80+
const logAuditDescription = logAuditEnabled
81+
? t('Request or response content may be recorded for usage log audit.')
82+
: t('Request and response content audit is disabled.')
7683

7784
const [filters, setFilters] = useState<CommonLogFilters>(() => {
7885
const { start, end } = getDefaultTimeRange()
@@ -82,6 +89,7 @@ export function CommonLogsFilterBar<TData>(
8289

8390
useEffect(() => {
8491
const { start, end } = getDefaultTimeRange()
92+
// eslint-disable-next-line react-hooks/set-state-in-effect
8593
setFilters({
8694
startTime: searchParams.startTime
8795
? new Date(searchParams.startTime)
@@ -199,6 +207,16 @@ export function CommonLogsFilterBar<TData>(
199207
const statsBar = (
200208
<div className='flex flex-wrap items-center gap-2'>
201209
<CommonLogsStats />
210+
<StatusBadge
211+
label={
212+
logAuditEnabled ? t('Log audit enabled') : t('Log audit disabled')
213+
}
214+
variant={logAuditEnabled ? 'warning' : 'neutral'}
215+
size='sm'
216+
copyable={false}
217+
title={logAuditDescription}
218+
aria-label={logAuditDescription}
219+
/>
202220
<Tooltip>
203221
<TooltipTrigger
204222
render={

‎web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,9 @@ export function DetailsDialog(props: DetailsDialogProps) {
464464
const showAdminIp =
465465
!!props.log.ip && (showTiming || (props.isAdmin && isTopup))
466466
const adminInfo = other?.admin_info
467+
const auditInfo = props.isAdmin
468+
? (adminInfo ?? other?.audit_info)
469+
: other?.audit_info
467470
const topupAuditFields =
468471
isTopup && props.isAdmin && adminInfo
469472
? ([
@@ -712,21 +715,21 @@ export function DetailsDialog(props: DetailsDialogProps) {
712715
</DetailSection>
713716
)}
714717

715-
{props.isAdmin && adminInfo?.request_content && (
718+
{auditInfo?.request_content && (
716719
<AuditContentSection
717720
label={t('Request Content')}
718-
content={adminInfo.request_content}
719-
truncated={adminInfo.request_content_truncated}
721+
content={auditInfo.request_content}
722+
truncated={auditInfo.request_content_truncated}
720723
copiedText={copiedText}
721724
onCopy={copyToClipboard}
722725
/>
723726
)}
724727

725-
{props.isAdmin && adminInfo?.response_content && (
728+
{auditInfo?.response_content && (
726729
<AuditContentSection
727730
label={t('Response Content')}
728-
content={adminInfo.response_content}
729-
truncated={adminInfo.response_content_truncated}
731+
content={auditInfo.response_content}
732+
truncated={auditInfo.response_content_truncated}
730733
copiedText={copiedText}
731734
onCopy={copyToClipboard}
732735
/>

‎web/default/src/features/usage-logs/types.ts‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,15 @@ export interface ChannelAffinityInfo {
119119
using_group?: string
120120
}
121121

122+
export interface LogAuditInfo {
123+
request_content?: string
124+
request_content_truncated?: boolean
125+
response_content?: string
126+
response_content_truncated?: boolean
127+
}
128+
122129
export interface LogOtherData {
130+
audit_info?: LogAuditInfo
123131
admin_info?: {
124132
is_multi_key?: boolean
125133
multi_key_index?: number
@@ -136,11 +144,7 @@ export interface LogOtherData {
136144
// Manage audit fields (type=3, admin only)
137145
admin_username?: string
138146
admin_id?: number | string
139-
request_content?: string
140-
request_content_truncated?: boolean
141-
response_content?: string
142-
response_content_truncated?: boolean
143-
}
147+
} & LogAuditInfo
144148
request_path?: string
145149
request_conversion?: string[]
146150
ws?: boolean

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2447,6 +2447,8 @@
24472447
"Locked": "Locked",
24482448
"log": "log",
24492449
"Log Audit": "Log Audit",
2450+
"Log audit disabled": "Log audit disabled",
2451+
"Log audit enabled": "Log audit enabled",
24502452
"Log Details": "Log Details",
24512453
"Log Directory": "Log Directory",
24522454
"Log File Count": "Log File Count",
@@ -3648,6 +3650,7 @@
36483650
"Report an issue": "Report an issue",
36493651
"request": "request",
36503652
"Request": "Request",
3653+
"Request and response content audit is disabled.": "Request and response content audit is disabled.",
36513654
"Request Body Disk Cache": "Request Body Disk Cache",
36523655
"Request Body Field": "Request Body Field",
36533656
"Request Body Memory Cache": "Request Body Memory Cache",
@@ -3665,6 +3668,7 @@
36653668
"Request Limits": "Request Limits",
36663669
"Request Model": "Request Model",
36673670
"Request Model:": "Request Model:",
3671+
"Request or response content may be recorded for usage log audit.": "Request or response content may be recorded for usage log audit.",
36683672
"Request overrides, routing behavior, and upstream model automation": "Request overrides, routing behavior, and upstream model automation",
36693673
"Request rule pricing": "Request rule pricing",
36703674
"Request success rate sampled over the last 24 hours": "Request success rate sampled over the last 24 hours",
@@ -4158,7 +4162,9 @@
41584162
"Store ID": "Store ID",
41594163
"Store ID is required": "Store ID is required",
41604164
"Store model output text in admin-only usage log details.": "Store model output text in admin-only usage log details.",
4165+
"Store model output text in usage log details.": "Store model output text in usage log details.",
41614166
"Store request prompts and messages in admin-only usage log details.": "Store request prompts and messages in admin-only usage log details.",
4167+
"Store request prompts and messages in usage log details.": "Store request prompts and messages in usage log details.",
41624168
"Stored value is not echoed back for security": "Stored value is not echoed back for security",
41634169
"stream": "stream",
41644170
"Stream": "Stream",

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2447,6 +2447,8 @@
24472447
"Locked": "Verrouillé",
24482448
"log": "journal",
24492449
"Log Audit": "Audit des journaux",
2450+
"Log audit disabled": "Audit des journaux désactivé",
2451+
"Log audit enabled": "Audit des journaux activé",
24502452
"Log Details": "Détails du journal",
24512453
"Log Directory": "Répertoire des journaux",
24522454
"Log File Count": "Nombre de fichiers journaux",
@@ -3648,6 +3650,7 @@
36483650
"Report an issue": "Signaler un problème",
36493651
"request": "requête",
36503652
"Request": "Requête",
3653+
"Request and response content audit is disabled.": "L'audit du contenu des requêtes et des réponses est désactivé.",
36513654
"Request Body Disk Cache": "Cache disque du corps de requête",
36523655
"Request Body Field": "Champ du corps de requête",
36533656
"Request Body Memory Cache": "Cache mémoire du corps de requête",
@@ -3665,6 +3668,7 @@
36653668
"Request Limits": "Limites de requêtes",
36663669
"Request Model": "Modèle demandé",
36673670
"Request Model:": "Modèle demandé :",
3671+
"Request or response content may be recorded for usage log audit.": "Le contenu des requêtes ou des réponses peut être enregistré pour l'audit des journaux d'utilisation.",
36683672
"Request overrides, routing behavior, and upstream model automation": "Surcharges de requête, comportement de routage et automatisation des modèles amont",
36693673
"Request rule pricing": "Règles de tarification de requête",
36703674
"Request success rate sampled over the last 24 hours": "Taux de réussite des requêtes échantillonné sur les dernières 24 heures",
@@ -4158,7 +4162,9 @@
41584162
"Store ID": "ID du magasin",
41594163
"Store ID is required": "L'ID de magasin est requis",
41604164
"Store model output text in admin-only usage log details.": "Stocker le texte de sortie du modèle dans les détails des journaux d'utilisation réservés aux administrateurs.",
4165+
"Store model output text in usage log details.": "Stocker le texte de sortie du modèle dans les détails des journaux d'utilisation.",
41614166
"Store request prompts and messages in admin-only usage log details.": "Stocker les prompts et messages de requête dans les détails des journaux d'utilisation réservés aux administrateurs.",
4167+
"Store request prompts and messages in usage log details.": "Stocker les prompts et messages de requête dans les détails des journaux d'utilisation.",
41624168
"Stored value is not echoed back for security": "Par sécurité, la valeur enregistrée n'est pas affichée",
41634169
"stream": "Flux",
41644170
"Stream": "Flux",

0 commit comments

Comments
 (0)