diff --git a/app/protos/grpc.proto b/app/protos/grpc.proto index 89dfc9ec3e..e3836e3074 100644 --- a/app/protos/grpc.proto +++ b/app/protos/grpc.proto @@ -6842,6 +6842,7 @@ message HTTPFlow { string HiddenIndex = 49; string FromPlugin = 50; string Host = 52; + string PathSuffix = 53; } message FuzzableParam { @@ -6872,6 +6873,7 @@ message HTTPFlowsFieldGroupRequest { message HTTPFlowsFieldGroupResponse { repeated TagsCode Tags = 1; repeated TagsCode StatusCode = 2; + repeated TagsCode Suffixes = 3; } message HTTPFlowsShareRequest { diff --git a/app/renderer/src/main/public/locales/en/history.json b/app/renderer/src/main/public/locales/en/history.json index 0d96e51260..b401ede692 100644 --- a/app/renderer/src/main/public/locales/en/history.json +++ b/app/renderer/src/main/public/locales/en/history.json @@ -16,6 +16,7 @@ "bodyLength": "Body Length", "params": "Parameters", "contentType": "Content Type", + "pathSuffix": "Path Suffix", "durationMs": "Duration (ms)", "updatedAt": "Request Time", "requestSizeVerbose": "Request Size", diff --git a/app/renderer/src/main/public/locales/zh/history.json b/app/renderer/src/main/public/locales/zh/history.json index 3967906eaa..69625247ac 100644 --- a/app/renderer/src/main/public/locales/zh/history.json +++ b/app/renderer/src/main/public/locales/zh/history.json @@ -16,6 +16,7 @@ "bodyLength": "响应长度", "params": "参数", "contentType": "响应类型", + "pathSuffix": "扩展名", "durationMs": "延迟(ms)", "updatedAt": "请求时间", "requestSizeVerbose": "请求大小", diff --git a/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowExportFields.ts b/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowExportFields.ts index 2c20b8f9df..1c8165112f 100644 --- a/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowExportFields.ts +++ b/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowExportFields.ts @@ -1,4 +1,6 @@ -export const getHTTPFlowExportFields = (t: (key: string) => string) => { +import { TFunction } from '@/i18n/useI18nNamespaces' + +export const getHTTPFlowExportFields = (t: TFunction) => { return [ { title: t('YakitTable.order'), @@ -70,6 +72,11 @@ export const getHTTPFlowExportFields = (t: (key: string) => string) => { key: 'content_type', dataKey: 'ContentType', }, + { + title: t('HTTPFlowTable.pathSuffix'), + key: 'path_suffix', + dataKey: 'PathSuffix', + }, { title: t('HTTPFlowTable.durationMs'), key: 'duration', diff --git a/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowPathSuffix.ts b/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowPathSuffix.ts new file mode 100644 index 0000000000..32a37f050e --- /dev/null +++ b/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowPathSuffix.ts @@ -0,0 +1,59 @@ +import { FiltersItemProps } from '../TableVirtualResize/TableVirtualResizeType' + +type TagsCodeLike = { + Value?: string + Total?: number +} + +const PATH_SUFFIX_MODIFIER_SEPARATOR = /[!@]/ +const VALID_PATH_SUFFIX_REGEXP = /^[a-zA-Z0-9]+$/ + +export const normalizeHTTPFlowPathSuffix = (value?: string) => { + if (!value) return '' + + let normalized = value.trim() + if (!normalized) return '' + + if (normalized.startsWith('.')) { + normalized = normalized.slice(1) + } + normalized = normalized.split(PATH_SUFFIX_MODIFIER_SEPARATOR)[0] || '' + + if (!VALID_PATH_SUFFIX_REGEXP.test(normalized)) { + return '' + } + return normalized +} + +export const getHTTPFlowPathSuffixValue = (path: string, pathSuffix?: string) => { + const normalizedPathSuffix = normalizeHTTPFlowPathSuffix(pathSuffix) + if (normalizedPathSuffix) { + return normalizedPathSuffix + } + + const cleanPath = path.split('?')[0].replace(/\/+$/, '') + const match = cleanPath.match(/\.([a-zA-Z0-9]+)(?:[!@][^/]*)?$/) + return match?.[1] || '' +} + +export const formatHTTPFlowPathSuffix = (path: string, pathSuffix?: string) => { + return getHTTPFlowPathSuffixValue(path, pathSuffix) || '-' +} + +export const buildHTTPFlowSuffixOptions = (suffixes: TagsCodeLike[]): FiltersItemProps[] => { + const uniqueSuffixes = new Set() + + return suffixes.reduce((acc, item) => { + const normalized = normalizeHTTPFlowPathSuffix(item.Value) + if (!normalized || uniqueSuffixes.has(normalized)) { + return acc + } + + uniqueSuffixes.add(normalized) + acc.push({ + label: normalized, + value: normalized, + }) + return acc + }, []) +} diff --git a/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowTable.tsx b/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowTable.tsx index 735366ce81..872054155d 100644 --- a/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowTable.tsx +++ b/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowTable.tsx @@ -6,6 +6,7 @@ import { HTTPFlowDetail, HTTPFlowDetailProp } from '../HTTPFlowDetail' import { info, yakitNotify, yakitFailed } from '../../utils/notification' import style from './HTTPFlowTable.module.scss' import { formatTimestamp } from '../../utils/timeUtil' +import { buildHTTPFlowSuffixOptions, formatHTTPFlowPathSuffix } from './HTTPFlowPathSuffix' import { useControllableValue, useCreation, @@ -184,6 +185,7 @@ export interface HTTPFlow { HostPort?: string IPAddress?: string HtmlTitle?: string + PathSuffix?: string GetParams: FuzzableParams[] PostParams: FuzzableParams[] @@ -548,6 +550,7 @@ export interface HTTPFlowsToOnlineBatchResponse { export interface HTTPFlowsFieldGroupResponse { Tags: TagsCode[] StatusCode: TagsCode[] + Suffixes: TagsCode[] } export interface TagsCode { @@ -604,18 +607,6 @@ export const getClassNameData = (resData: HTTPFlow[]) => { return newData } -export const filterData = (filterArr: HTTPFlow[], key: keyof HTTPFlow) => { - const uniqueData: HTTPFlow[] = [] - const idSet = new Set() - filterArr.forEach((item) => { - if (!idSet.has(item[key])) { - idSet.add(item[key]) - uniqueData.push(item) - } - }) - return uniqueData -} - /** * @description 根据单位转为对应的值 * @returns {number} @@ -747,6 +738,8 @@ export const HTTPFlowTable = React.memo((props) => { const isOneceLoading = useRef(true) const [total, setTotal] = useState(0) + const [suffixList, setSuffixList] = useState([]) + const comSuffixList = useCampare(suffixList) const [loading, setLoading] = useState(false) const [selected, setSelected, getSelected] = useGetSetState() @@ -774,6 +767,7 @@ export const HTTPFlowTable = React.memo((props) => { const [afterBodyLength, setAfterBodyLength, getAfterBodyLength] = useGetSetState() const [beforeBodyLength, setBeforeBodyLength, getBeforeBodyLength] = useGetSetState() const [isReset, setIsReset] = useState(false) + const [watchRefresh, setWatchRefresh] = useState(false) const [checkBodyLength, setCheckBodyLength] = useState(false) // 查询BodyLength大于0 @@ -1082,6 +1076,20 @@ export const HTTPFlowTable = React.memo((props) => { isOneceLoading.current = false }) }) + useDebounceEffect( + () => { + if (!inViewport) return + ipcRenderer + .invoke('HTTPFlowsFieldGroup', { RefreshRequest: true, IsAll: true }) + .then((rsp: HTTPFlowsFieldGroupResponse) => { + setSuffixList(buildHTTPFlowSuffixOptions(rsp.Suffixes || [])) + }) + .catch(() => {}) + }, + [inViewport, refresh, watchRefresh], + { wait: 500 }, + ) + const onTableChange = useDebounceFn( (page: number, limit: number, sort: SortProps, filter: any) => { if (sort.order === 'none') { @@ -1596,6 +1604,7 @@ export const HTTPFlowTable = React.memo((props) => { } } } catch (error) {} + setWatchRefresh((prev) => !prev) setIsLoop(true) }) useEffect(() => { @@ -2160,10 +2169,24 @@ export const HTTPFlowTable = React.memo((props) => { filterSearchInputProps: { size: 'small', }, - filterIcon: , filters: contentType, }, }, + { + title: t('HTTPFlowTable.pathSuffix'), + dataKey: 'PathSuffix', + width: 100, + filterProps: { + filterKey: 'IncludeSuffix', + filtersType: 'select', + filterMultiple: true, + filterSearchInputProps: { size: 'small' }, + filters: suffixList, + }, + render: (_, rowData) => { + return
{formatHTTPFlowPathSuffix(rowData.Path || '', rowData.PathSuffix)}
+ }, + }, { title: t('HTTPFlowTable.durationMs'), dataKey: 'DurationMs', @@ -2322,6 +2345,7 @@ export const HTTPFlowTable = React.memo((props) => { excludeColumnsKey, idFixed, i18n.language, + comSuffixList, ]) // #endregion @@ -2582,6 +2606,9 @@ export const HTTPFlowTable = React.memo((props) => { if (j === 'UpdatedAt') { return formatTimestamp(v[j]) } + if (j === 'PathSuffix') { + return formatHTTPFlowPathSuffix(v['Path'], v['PathSuffix']) + } return v[j] }), ) @@ -3721,6 +3748,7 @@ export const HTTPFlowTable = React.memo((props) => { const resetAllFun = useMemoizedFn(() => { sortRef.current = defSort setIsReset(!isReset) + setWatchRefresh((prev) => !prev) setColor([]) setOnlyFavorite(false) setCheckBodyLength(false) @@ -4082,6 +4110,7 @@ export const HTTPFlowTable = React.memo((props) => { const onMitmNoResetRefresh = useMemoizedFn((version: string) => { if (version !== mitmVersion) return + setWatchRefresh((prev) => !prev) updateData() }) @@ -4507,6 +4536,7 @@ export const HTTPFlowTable = React.memo((props) => { onClick: ({ key }) => { switch (key) { case 'noResetRefresh': + setWatchRefresh((prev) => !prev) updateData() break case 'resetRefresh': diff --git a/app/renderer/src/main/src/components/HTTPFlowTable/__test__/HTTPFlowPathSuffix.test.ts b/app/renderer/src/main/src/components/HTTPFlowTable/__test__/HTTPFlowPathSuffix.test.ts new file mode 100644 index 0000000000..26ba099ac1 --- /dev/null +++ b/app/renderer/src/main/src/components/HTTPFlowTable/__test__/HTTPFlowPathSuffix.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest' +import { + buildHTTPFlowSuffixOptions, + getHTTPFlowPathSuffixValue, + normalizeHTTPFlowPathSuffix, +} from '../HTTPFlowPathSuffix' + +describe('HTTPFlowPathSuffix', () => { + it('normalizes a valid suffix and strips the leading dot', () => { + expect(normalizeHTTPFlowPathSuffix('.js')).toBe('js') + expect(normalizeHTTPFlowPathSuffix('json')).toBe('json') + }) + + it('rejects malformed suffix values from backend aggregation', () => { + expect(normalizeHTTPFlowPathSuffix('.com&app=2021&size=w240')).toBe('') + expect(normalizeHTTPFlowPathSuffix('.png!cc_216x216')).toBe('png') + }) + + it('prefers a valid PathSuffix field and falls back to parsing Path', () => { + expect(getHTTPFlowPathSuffixValue('/static/app.js?version=1', '.js')).toBe('js') + expect(getHTTPFlowPathSuffixValue('/item/assets/428.png!cc_216x216', '')).toBe('png') + }) + + it('does not treat embedded urls in path payloads as file suffixes', () => { + expect( + getHTTPFlowPathSuffixValue( + '/search/src=https://imgsrc.baidu.com/forum&app=2021&size=w240&n=0&g=0n&fmt=auto', + '.com&app=2021&size=w240&n=0&g=0n&fmt=auto', + ), + ).toBe('') + }) + + it('filters invalid suffix options from field group response', () => { + expect( + buildHTTPFlowSuffixOptions([ + { Value: '.js', Total: 2 }, + { Value: '.com&app=2021&size=w240', Total: 1 }, + { Value: '.png!cc_216x216', Total: 1 }, + ]), + ).toEqual([ + { label: 'js', value: 'js' }, + { label: 'png', value: 'png' }, + ]) + }) +}) diff --git a/app/renderer/src/main/src/pages/hTTPHistoryAnalysis/HTTPHistory/HTTPHistoryFilter.tsx b/app/renderer/src/main/src/pages/hTTPHistoryAnalysis/HTTPHistory/HTTPHistoryFilter.tsx index db4aaa5b71..34529ac031 100644 --- a/app/renderer/src/main/src/pages/hTTPHistoryAnalysis/HTTPHistory/HTTPHistoryFilter.tsx +++ b/app/renderer/src/main/src/pages/hTTPHistoryAnalysis/HTTPHistory/HTTPHistoryFilter.tsx @@ -11,6 +11,8 @@ import { } from 'ahooks' import { getRemoteValue, setRemoteValue } from '@/utils/kv' import { getHTTPFlowExportFields } from '@/components/HTTPFlowTable/HTTPFlowExportFields' +import { HTTPFlowsFieldGroupResponse } from '@/components/HTTPFlowTable/HTTPFlowTable' +import { buildHTTPFlowSuffixOptions, formatHTTPFlowPathSuffix } from '@/components/HTTPFlowTable/HTTPFlowPathSuffix' import { OutlineChevrondownIcon, OutlineCogIcon, @@ -390,6 +392,7 @@ export const defalutColumnsOrder = [ 'HtmlTitle', 'GetParamsTotal', 'ContentType', + 'PathSuffix', 'DurationMs', 'UpdatedAt', 'RequestSizeVerbose', @@ -451,6 +454,8 @@ const HTTPFlowFilterTable: React.FC = React.memo((props) => }) const [color, setColor] = useState([]) const [isShowColor, setIsShowColor] = useState(false) + const [suffixList, setSuffixList] = useState([]) + const comSuffixList = useCampare(suffixList) // 表格相关变量 const [isRefresh, setIsRefresh] = useState(false) @@ -708,6 +713,20 @@ const HTTPFlowFilterTable: React.FC = React.memo((props) => const [tagsFilter, setTagsFilter] = useState([]) /** ---- tags end ----*/ + useDebounceEffect( + () => { + if (!inViewport) return + ipcRenderer + .invoke('HTTPFlowsFieldGroup', { RefreshRequest: true, IsAll: true }) + .then((rsp: HTTPFlowsFieldGroupResponse) => { + setSuffixList(buildHTTPFlowSuffixOptions(rsp.Suffixes || [])) + }) + .catch(() => {}) + }, + [inViewport, refresh], + { wait: 500 }, + ) + /** ---- 响应长度 start ----*/ const [bodyLengthSort, setBodyLengthSort, getBodyLengthSort] = useGetSetState<'asc' | 'desc' | false>(false) const [checkBodyLength, setCheckBodyLength] = useState(false) @@ -1157,10 +1176,24 @@ const HTTPFlowFilterTable: React.FC = React.memo((props) => filterSearchInputProps: { size: 'small', }, - filterIcon: , filters: contentType, }, }, + { + title: t('HTTPFlowTable.pathSuffix'), + dataKey: 'PathSuffix', + width: 100, + filterProps: { + filterKey: 'IncludeSuffix', + filtersType: 'select', + filterMultiple: true, + filterSearchInputProps: { size: 'small' }, + filters: suffixList, + }, + render: (_, rowData) => { + return
{formatHTTPFlowPathSuffix(rowData.Path || '', rowData.PathSuffix)}
+ }, + }, { title: t('HTTPFlowTable.durationMs'), dataKey: 'DurationMs', @@ -1335,6 +1368,7 @@ const HTTPFlowFilterTable: React.FC = React.memo((props) => contentType, i18n.language, onlyFavorite, + comSuffixList, ]) // #endregion @@ -1994,6 +2028,9 @@ const HTTPFlowFilterTable: React.FC = React.memo((props) => if (j === 'UpdatedAt') { return formatTimestamp(v[j]) } + if (j === 'PathSuffix') { + return formatHTTPFlowPathSuffix(v['Path'], v['PathSuffix']) + } return v[j] }), ) diff --git a/app/renderer/src/main/src/pages/layout/publicMenu/utils.tsx b/app/renderer/src/main/src/pages/layout/publicMenu/utils.tsx index f74d76e22e..25757c562c 100644 --- a/app/renderer/src/main/src/pages/layout/publicMenu/utils.tsx +++ b/app/renderer/src/main/src/pages/layout/publicMenu/utils.tsx @@ -5,6 +5,7 @@ import { RouteToPageProps } from './PublicMenu' import { EnhancedPrivateRouteMenuProps } from '../HeardMenu/HeardMenuType' import { SendDatabaseFirstMenuProps } from '@/routes/newRouteType' import { YakitRoute } from '@/enums/yakitRoute' +import { TFunction } from '@/i18n/useI18nNamespaces' /** public版本前端增强型菜单项属性(用于前端数据对比和渲染逻辑使用) */ export interface EnhancedPublicRouteMenuProps extends PublicRouteMenuProps { @@ -283,7 +284,7 @@ export const separator = '|' /** 将菜单数据转换成 Menu组件数据 */ export const routeToMenu = ( routes: EnhancedPublicRouteMenuProps[] | EnhancedPrivateRouteMenuProps[], - t: (key: string) => string, + t: TFunction, parent?: string, ) => { const menus: YakitMenuItemProps[] = []