diff --git a/app/renderer/src/main/public/locales/en/history.json b/app/renderer/src/main/public/locales/en/history.json index ca26f777b1..0d96e51260 100644 --- a/app/renderer/src/main/public/locales/en/history.json +++ b/app/renderer/src/main/public/locales/en/history.json @@ -42,6 +42,10 @@ "configured": "Configured", "protocolType": "Protocol Type", "all": "All", + "favorites": "Favorites", + "favoriteSuccess": "Favorite successful", + "cancelFavoriteSuccess": "Favorite removed", + "onlyFavorites": "Only Favorites", "resetRequestID": "Reset Request ID", "doNotResetRequestID": "Do Not Reset Request ID", "saveFile": "Save File", @@ -63,6 +67,8 @@ "batchTestPoCTemplate": "Batch Test PoC Template", "tagColor": "Tag Color", "removeColor": "Remove Color", + "favorite": "Favorite", + "cancelFavorite": "Cancel Favorite", "sendToComparer": "Send to Comparer", "sendToComparerLeft": "Send to Left Comparer", "sendToComparerRight": "Send to Right Comparer and Navigate", diff --git a/app/renderer/src/main/public/locales/en/layout.json b/app/renderer/src/main/public/locales/en/layout.json index 33f72a0022..f8dca93b49 100644 --- a/app/renderer/src/main/public/locales/en/layout.json +++ b/app/renderer/src/main/public/locales/en/layout.json @@ -216,6 +216,8 @@ "chromePath": "Chrome Startup Path", "chromePathDesc": "If Chrome cannot be started, please configure the Chrome startup path", "configured": "Configured", + "mcp": "Yak Mcp", + "mcpDesc": "May affect the use of some features", "systemProxy": "System Proxy", "statusRefreshInterval": "Status Refresh Interval", "zoomScale": "Zoom Scale Adjustment", @@ -346,6 +348,7 @@ "started": "MCP service started: {{serverUrl}}", "error": "MCP service error: {{message}}", "stopped": "MCP service stopped: {{message}}", + "MCPStopped": "Yak Mcp service stopped", "serviceStopped": "Service stopped", "urlRequired": "Startup address cannot be empty", "urlFormatError": "Startup address format error, e.g. 127.0.0.1:11432", diff --git a/app/renderer/src/main/public/locales/zh/history.json b/app/renderer/src/main/public/locales/zh/history.json index 9316d30976..3967906eaa 100644 --- a/app/renderer/src/main/public/locales/zh/history.json +++ b/app/renderer/src/main/public/locales/zh/history.json @@ -42,6 +42,10 @@ "configured": "已配置", "protocolType": "协议类型", "all": "全部", + "favorites": "收藏夹", + "favoriteSuccess": "收藏成功", + "cancelFavoriteSuccess": "取消收藏成功", + "onlyFavorites": "仅收藏夹", "resetRequestID": "重置请求 ID", "doNotResetRequestID": "不重置请求 ID", "saveFile": "保存文件", @@ -63,6 +67,8 @@ "batchTestPoCTemplate": "批量检测 PoC 模版", "tagColor": "标注颜色", "removeColor": "移除颜色", + "favorite": "收藏", + "cancelFavorite": "取消收藏", "sendToComparer": "发送到对比器", "sendToComparerLeft": "发送到对比器左侧", "sendToComparerRight": "发送到对比器右侧并跳转", diff --git a/app/renderer/src/main/public/locales/zh/layout.json b/app/renderer/src/main/public/locales/zh/layout.json index 0ff4956c02..6ee4f25b1f 100644 --- a/app/renderer/src/main/public/locales/zh/layout.json +++ b/app/renderer/src/main/public/locales/zh/layout.json @@ -216,6 +216,8 @@ "chromePath": "Chrome 启动路径", "chromePathDesc": "如无法启动 Chrome,请配置 Chrome 启动路径", "configured": "已配置", + "mcp": "Yak Mcp", + "mcpDesc": "可能会影响部分功能的使用", "systemProxy": "系统代理", "statusRefreshInterval": "状态刷新间隔时间", "zoomScale": "缩放比例调整", @@ -346,6 +348,7 @@ "started": "MCP 服务已启动:{{serverUrl}}", "error": "MCP 服务错误:{{message}}", "stopped": "MCP 服务已停止:{{message}}", + "MCPStopped": "Yak Mcp服务已停用", "serviceStopped": "服务已停止", "urlRequired": "启动地址不能为空", "urlFormatError": "启动地址格式错误,例如:127.0.0.1:11432", diff --git a/app/renderer/src/main/src/assets/icon/colors.tsx b/app/renderer/src/main/src/assets/icon/colors.tsx index e46ff2899b..52bffdbbc7 100644 --- a/app/renderer/src/main/src/assets/icon/colors.tsx +++ b/app/renderer/src/main/src/assets/icon/colors.tsx @@ -785,30 +785,26 @@ export const IconSolidAIIcon = (props: Partial) => { return } -const IconSolidAI = () => ( - - - - - - - - - - -) +const IconSolidAI = () => { + const id = uuidv4() + return ( + + + + + + + + + + + ) +} /** * @description Icon/Solid/ai-white diff --git a/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowTable.module.scss b/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowTable.module.scss index 7027412db9..ac5be005f7 100644 --- a/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowTable.module.scss +++ b/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowTable.module.scss @@ -612,6 +612,23 @@ .icon-hover:hover { color: var(--Colors-Use-Main-Primary); } + + .favorite-icon svg { + height: 16px; + width: 16px; + } + + .favorite-icon-active { + margin-top: -3px; + margin-left: -2px; + width: 19px; + color: var(--Colors-Use-Yellow-Primary) !important; + + svg { + height: 19px; + width: 19px; + } + } } .hover-grey-row { diff --git a/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowTable.tsx b/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowTable.tsx index f7fe5d62af..735366ce81 100644 --- a/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowTable.tsx +++ b/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowTable.tsx @@ -80,8 +80,10 @@ import { OutlineRefreshIcon, OutlineSearchIcon, OutlineSelectorIcon, + OutlineStarIcon, OutlineXIcon, } from '@/assets/icon/outline' +import { SolidStarIcon } from '@/assets/icon/solid' import { serverPushStatus } from '@/utils/duplex/duplex' import { useCampare } from '@/hook/useCompare/useCompare' import { queryYakScriptList } from '@/pages/yakitStore/network' @@ -395,6 +397,39 @@ const TableRowColor = (key: string) => { } } +export const HTTP_FLOW_FAVORITE_TAG = 'YAKIT_FAVORITE' + +const getHTTPFlowTags = (tags?: string) => { + return tags ? tags.split('|').filter(Boolean) : [] +} + +export const isHTTPFlowFavorite = (flow?: HTTPFlow) => { + return getHTTPFlowTags(flow?.Tags).includes(HTTP_FLOW_FAVORITE_TAG) +} + +export const buildFavoriteTags = (tags: string | undefined, favorite: boolean) => { + const nextTags = getHTTPFlowTags(tags).filter((tag) => tag !== HTTP_FLOW_FAVORITE_TAG) + if (favorite) nextTags.push(HTTP_FLOW_FAVORITE_TAG) + return nextTags +} + +export const buildHTTPFlowQueryTags = (tags: string[], onlyFavorite: boolean) => { + return onlyFavorite ? [...tags, HTTP_FLOW_FAVORITE_TAG] : [...tags] +} + +const matchHTTPFlowTagsFilter = (flow: HTTPFlow, tagsFilter: string[]) => { + if (!tagsFilter.length) return true + const flowTags = getHTTPFlowTags(flow.Tags) + return tagsFilter.some((tag) => flowTags.includes(tag)) +} + +export const filterHTTPFlowsByFavoriteAndTags = (list: HTTPFlow[], tagsFilter: string[], onlyFavorite: boolean) => { + return list.filter((flow) => { + if (onlyFavorite && !isHTTPFlowFavorite(flow)) return false + return matchHTTPFlowTagsFilter(flow, tagsFilter) + }) +} + export const availableColors = [ { color: 'RED', @@ -685,6 +720,7 @@ export const HTTPFlowTable = React.memo((props) => { }, [mitmContent.mitmStore.version]) const [data, setData] = useState([]) const [color, setColor] = useState([]) + const [onlyFavorite, setOnlyFavorite] = useState(false) const [isShowColor, setIsShowColor] = useState(false) const [params, setParams, getParams] = useGetSetState({ SourceType: props.params?.SourceType || 'mitm', @@ -1066,7 +1102,7 @@ export const HTTPFlowTable = React.memo((props) => { const newParams = { ...prev, ...filter, - Tags: [...tagsFilter], + Tags: buildHTTPFlowQueryTags(tagsFilter, onlyFavorite), bodyLength: !!(afterBodyLength || beforeBodyLength || checkBodyLength), // 用来判断响应长度的icon颜色是否显示蓝色 } return newParams @@ -1103,7 +1139,7 @@ export const HTTPFlowTable = React.memo((props) => { setTagsFilter(nextTags) setParams((prev) => ({ ...prev, - Tags: nextTags, + Tags: buildHTTPFlowQueryTags(nextTags, onlyFavorite), })) setScrollToIndex(0) setCurrentIndex(undefined) @@ -1112,7 +1148,7 @@ export const HTTPFlowTable = React.memo((props) => { setSelectedRows([]) setIsAllSelect(false) } - }, [campareTagsFilter, pageType]) + }, [campareTagsFilter, onlyFavorite, pageType]) /** * 网站树部分 @@ -1244,7 +1280,11 @@ export const HTTPFlowTable = React.memo((props) => { .invoke('QueryHTTPFlows', realQuery) .then((rsp: YakQueryHTTPFlowResponse) => { const resData = rsp?.Data || [] - const newData: HTTPFlow[] = getClassNameData(resData) + const newData: HTTPFlow[] = filterHTTPFlowsByFavoriteAndTags( + getClassNameData(resData), + tagsFilter, + onlyFavorite, + ) const copyData = newData.slice() if (type === 'top') { if (newData.length <= 0) { @@ -1254,12 +1294,14 @@ export const HTTPFlowTable = React.memo((props) => { } if (['desc', 'none'].includes(tableOrder)) { const reverseData = copyData.reverse() - setData([...reverseData, ...data]) + const nextData = [...reverseData, ...data] + setData(nextData) maxIdRef.current = reverseData[0].Id } else { // 升序 if (rsp.Pagination.Limit - data.length >= 0) { - setData([...data, ...newData]) + const nextData = [...data, ...newData] + setData(nextData) maxIdRef.current = newData[newData.length - 1].Id } } @@ -1672,6 +1714,22 @@ export const HTTPFlowTable = React.memo((props) => { { wait: 300 }, ).run + const onToggleOnlyFavorite = useMemoizedFn(() => { + const nextOnlyFavorite = !onlyFavorite + setOnlyFavorite(nextOnlyFavorite) + setParams((prev) => ({ + ...prev, + Tags: buildHTTPFlowQueryTags(tagsFilter, nextOnlyFavorite), + })) + setScrollToIndex(0) + setCurrentIndex(undefined) + setSelected(undefined) + setSelectedRowKeys([]) + setSelectedRows([]) + setIsAllSelect(false) + setTriggerParamsWatch((old) => !old) + }) + useEffect(() => { if (!selectedRowKeys.length) { setIsAllSelect(false) @@ -1946,7 +2004,7 @@ export const HTTPFlowTable = React.memo((props) => { return text ? `${text}` .split('|') - .filter((i) => !i.startsWith('YAKIT_COLOR_')) + .filter((i) => !i.startsWith('YAKIT_COLOR_') && i !== HTTP_FLOW_FAVORITE_TAG) .join(', ') : '' }, @@ -2144,17 +2202,38 @@ export const HTTPFlowTable = React.memo((props) => { { title: t('YakitTable.action'), dataKey: 'action', - width: 80, + width: 104, fixed: 'right', render: (_, rowData) => { if (!rowData.Hash) return <> const colorType = getSingleColorType(rowData.cellClassName) // 获取颜色类型 + const favorite = isHTTPFlowFavorite(rowData) return (
+ {favorite ? ( + { + e.stopPropagation() + toggleHTTPFlowFavorite(rowData, false, setData, onlyFavorite) + }} + /> + ) : ( + { + e.stopPropagation() + toggleHTTPFlowFavorite(rowData, true, setData, onlyFavorite) + }} + /> + )} +
((props) => { downstreamProxyStr, pageType, queryParams, + onlyFavorite, columnsOrder, excludeColumnsKey, idFixed, @@ -2330,6 +2410,43 @@ export const HTTPFlowTable = React.memo((props) => { }) }) + const toggleHTTPFlowFavoriteBatch = useMemoizedFn((flowList: HTTPFlow[], number: number, favorite: boolean) => { + if (flowList.length === 0) { + yakitNotify('warning', t('HTTPFlowTable.pleaseSelectData')) + return + } + if (flowList.length > number) { + yakitNotify('warning', t('HTTPFlowTable.maxOperateData', { number })) + return + } + const newList = flowList.map((flow) => ({ + Id: flow.Id, + Hash: flow.Hash, + Tags: buildFavoriteTags(flow.Tags, favorite), + })) + ipcRenderer + .invoke('SetTagForHTTPFlow', { + CheckTags: newList, + }) + .then(() => { + const newData = data.map((item) => { + const find = newList.find((ele) => ele.Hash === item.Hash) + if (!find) return item + return { + ...item, + Tags: find.Tags.join('|'), + } + }) + setData(onlyFavorite && !favorite ? newData.filter(isHTTPFlowFavorite) : newData) + setSelectedRowKeys([]) + setSelectedRows([]) + yakitNotify('success', favorite ? t('HTTPFlowTable.favoriteSuccess') : t('HTTPFlowTable.cancelFavoriteSuccess')) + }) + .catch((e) => { + yakitFailed(e + '') + }) + }) + //删除 const onRemoveHttpHistory = useMemoizedFn((query) => { setLoading(true) @@ -2981,6 +3098,23 @@ export const HTTPFlowTable = React.memo((props) => { ], onClickBatch: () => {}, }, + { + key: 'favorite', + label: t('HTTPFlowTable.RowContextMenu.favorite'), + default: true, + webSocket: true, + number: 20, + onClickSingle: (v) => toggleHTTPFlowFavorite(v, !isHTTPFlowFavorite(v), setData, onlyFavorite), + onClickBatch: (list, n) => toggleHTTPFlowFavoriteBatch(list, n, true), + }, + { + key: 'cancelFavorite', + label: t('HTTPFlowTable.RowContextMenu.cancelFavorite'), + default: false, + webSocket: false, + number: 20, + onClickBatch: (list, n) => toggleHTTPFlowFavoriteBatch(list, n, false), + }, { key: '数据包扫描', label: t('HTTPFlowTable.RowContextMenu.packetScan'), @@ -3289,7 +3423,7 @@ export const HTTPFlowTable = React.memo((props) => { codecSingleHistoryPluginCom, selectedRowKeysCom, selected?.Id, - data, + onlyFavorite, ]) /** 菜单自定义快捷键渲染处理事件 */ @@ -3323,8 +3457,13 @@ export const HTTPFlowTable = React.memo((props) => { return contextMenuKeybindingHandle(menuData) .filter((item) => (rowData.IsWebsocket ? item.webSocket : item.default)) .map((ele) => { + const isFavoriteMenu = ele.key === 'favorite' return { - label: ele.label, + label: isFavoriteMenu + ? isHTTPFlowFavorite(rowData) + ? t('HTTPFlowTable.RowContextMenu.cancelFavorite') + : t('HTTPFlowTable.RowContextMenu.favorite') + : ele.label, key: ele.key, children: ele.children || [], } @@ -3583,6 +3722,7 @@ export const HTTPFlowTable = React.memo((props) => { sortRef.current = defSort setIsReset(!isReset) setColor([]) + setOnlyFavorite(false) setCheckBodyLength(false) setBeforeBodyLength(undefined) setAfterBodyLength(undefined) @@ -4026,6 +4166,16 @@ export const HTTPFlowTable = React.memo((props) => { } }, [realData.length]) + const onlyFavoriteTag = useMemo( + () => + onlyFavorite && ( + onToggleOnlyFavorite()}> + {t('HTTPFlowTable.onlyFavorites')} + + ), + [onlyFavorite, i18n.language], + ) + return (
} tabIndex={-1} className={style['http-history-flow-table-wrapper']}> ((props) => {
{filterTagDom} + {onlyFavoriteTag}
{showAdvancedSearch && ( @@ -4202,6 +4353,16 @@ export const HTTPFlowTable = React.memo((props) => { ]} /> )} + + } + onClick={(e) => { + e.currentTarget.blur() + onToggleOnlyFavorite() + }} + /> + {showColorSwatch && (
((props) => { {showBatchActions && ( <> {(selectedRowKeys.length === 0 && ( - { - e.stopPropagation() - }} - > + {t('YakitButton.batchOperation')} @@ -4264,13 +4419,7 @@ export const HTTPFlowTable = React.memo((props) => { onVisibleChange={setBatchVisible} visible={batchVisible} > - { - e.stopPropagation() - }} - > + {t('YakitButton.batchOperation')} @@ -5093,6 +5242,32 @@ export const onRemoveCalloutColor = (flow: HTTPFlow, data: HTTPFlow[], setData) }) } +export const toggleHTTPFlowFavorite = (flow: HTTPFlow, favorite: boolean, setData, removeWhenUnfavorite = false) => { + if (!flow) return + const nextTags = buildFavoriteTags(flow.Tags, favorite) + ipcRenderer + .invoke('SetTagForHTTPFlow', { + Id: flow.Id, + Hash: flow.Hash, + Tags: nextTags, + }) + .then(() => { + setData((prev: HTTPFlow[]) => { + const newData = prev.map((item) => { + if (item.Hash !== flow.Hash) return item + return { + ...item, + Tags: nextTags.join('|'), + } + }) + return removeWhenUnfavorite && !favorite ? newData.filter(isHTTPFlowFavorite) : newData + }) + }) + .catch((e) => { + yakitFailed(e + '') + }) +} + const onBatchExecPacketScan = (params: { httpFlowIds: string[] maxLength: number diff --git a/app/renderer/src/main/src/components/HTTPHistory.tsx b/app/renderer/src/main/src/components/HTTPHistory.tsx index 1b7721be2a..2d18395fd4 100644 --- a/app/renderer/src/main/src/components/HTTPHistory.tsx +++ b/app/renderer/src/main/src/components/HTTPHistory.tsx @@ -2,6 +2,7 @@ import React, { CSSProperties, ReactElement, ReactNode, useContext, useEffect, u import 'react-resizable/css/styles.css' import { HistoryTableTitleShow, + HTTP_FLOW_FAVORITE_TAG, HTTPFlow, HTTPFlowsFieldGroupResponse, HTTPFlowTable, @@ -880,7 +881,7 @@ export const HistoryProcess: React.FC = React.memo((props) IsAll: true, }) .then((rsp: HTTPFlowsFieldGroupResponse) => { - const tags = (rsp.Tags || []).filter((item) => item.Value) + const tags = (rsp.Tags || []).filter((item) => item.Value && item.Value !== HTTP_FLOW_FAVORITE_TAG) const realTags: FiltersItemProps[] = tags.map(({ Value }) => ({ label: Value, value: Value, diff --git a/app/renderer/src/main/src/components/layout/FuncDomain.tsx b/app/renderer/src/main/src/components/layout/FuncDomain.tsx index da5bb86181..43661553c6 100644 --- a/app/renderer/src/main/src/components/layout/FuncDomain.tsx +++ b/app/renderer/src/main/src/components/layout/FuncDomain.tsx @@ -130,7 +130,7 @@ import { import { YakitAuditRiskDetails } from '@/pages/yakRunnerAuditHole/YakitAuditHoleTable/YakitAuditHoleTable' import SelectUpload from '@/pages/SelectUpload' import { ShortcutKeyPageName } from '@/utils/globalShortcutKey/events/pageMaps' -import useMcpStream, { mcpStreamHooks } from './hooks/useMcp/useMcp' +import { mcpStreamHooks } from './hooks/useMcp/useMcp' import { ConfigMcpModal } from '@/utils/ConfigSystemMcp' import { useCampare } from '@/hook/useCompare/useCompare' import { openConsoleNewWindow } from '@/utils/openWebsite' @@ -240,6 +240,7 @@ export interface FuncDomainProp { isReverse?: Boolean engineMode: YaklangEngineMode isRemoteMode: boolean + mcp: mcpStreamHooks onEngineModeChange: (type: YaklangEngineMode) => any typeCallback: (type: YakitSettingCallbackType) => any /** 远程控制 - 自动切换远程连接 */ @@ -265,6 +266,7 @@ export const FuncDomain: React.FC = React.memo((props) => { onEngineModeChange, runDynamicControlRemote, typeCallback, + mcp, showProjectManage = false, system, isJudgeLicense, @@ -549,8 +551,6 @@ export const FuncDomain: React.FC = React.memo((props) => { }, []) // mcp 全局监听 - const [mcpStreamInfo, mcpStreamEvent] = useMcpStream({}) - // 引擎日志 全局监听 useEngineConsole({}) @@ -599,10 +599,7 @@ export const FuncDomain: React.FC = React.memo((props) => { engineMode={engineMode} onEngineModeChange={onEngineModeChange} typeCallback={typeCallback} - mcp={{ - mcpStreamInfo, - mcpStreamEvent, - }} + mcp={mcp} /> )}
@@ -1157,6 +1154,7 @@ const GetUIOpSettingMenu = () => { // { key: "engineVar",label: "引擎环境变量" }, { key: 'config-network', label: '全局配置' }, { key: 'setShortcutKey', label: '快捷键设置' }, + { key: 'configMcp', label: 'Yak Mcp配置' }, ], }, { @@ -1254,6 +1252,7 @@ const UIOpSetting: React.FC = React.memo((props) => { showConfigSystemProxyForm() return case 'mcp': + case 'configMcp': setConfigMcpModalVisible(true) return case 'engineVar': diff --git a/app/renderer/src/main/src/components/layout/GlobalState.tsx b/app/renderer/src/main/src/components/layout/GlobalState.tsx index 4389bfc90d..fb3baf47d0 100644 --- a/app/renderer/src/main/src/components/layout/GlobalState.tsx +++ b/app/renderer/src/main/src/components/layout/GlobalState.tsx @@ -33,6 +33,8 @@ import styles from './globalState.module.scss' import { useRunNodeStore } from '@/store/runNode' import { YakitTag } from '../yakitUI/YakitTag/YakitTag' import { YakitCheckbox } from '../yakitUI/YakitCheckbox/YakitCheckbox' +import { mcpStreamHooks } from './hooks/useMcp/useMcp' +import { ConfigMcpModal } from '@/utils/ConfigSystemMcp' import emiter from '@/utils/eventBus/eventBus' import { serverPushStatus } from '@/utils/duplex/duplex' import { openABSFileLocated } from '@/utils/openWebsite' @@ -68,6 +70,7 @@ const ShowColorClass: Record = { export interface GlobalReverseStateProp { isEngineLink: boolean system: YakitSystem + mcp: mcpStreamHooks } export interface CheckSyntaxFlowRuleUpdateResponse { @@ -84,8 +87,17 @@ interface ReverseDetail { } export const GlobalState: React.FC = React.memo((props) => { - const { isEngineLink, system } = props - const { t, i18n } = useI18nNamespaces(['yakitRoute', 'home', 'yakitUi', 'layout']) + const { isEngineLink, system, mcp } = props + const { t, i18n } = useI18nNamespaces(['yakitRoute', 'home', 'yakitUi', 'layout', 'utils']) + const [configMcpModalVisible, setConfigMcpModalVisible] = useState(false) + const enableMcp = useMemo(() => { + if (!mcp.mcpStreamInfo.mcpCurrent) return false + if (['stopped', 'error'].includes(mcp.mcpStreamInfo.mcpCurrent.Status)) { + return false + } + if (!mcp.mcpStreamInfo.mcpServerUrl) return false + return true + }, [mcp.mcpStreamInfo]) /** 自启全局反连配置(默认指定为本地) */ useEffect(() => { @@ -997,7 +1009,18 @@ export const GlobalState: React.FC = React.memo((props) className={styles['btn-style']} onClick={() => { setShow(false) - showConfigSystemProxyForm() + yakitHost + .setSystemProxy({ + HttpProxy: systemProxy.CurrentProxy, + Enable: false, + }) + .then(() => { + info(t('ConfigSystemProxy.setSystemProxySuccess')) + emiter.emit('onRefConfigSystemProxy', '') + }) + .catch((err) => { + yakitFailed(t('GlobalState.setSystemProxyFailed', { error: String(err) })) + }) }} > {' '} @@ -1018,6 +1041,49 @@ export const GlobalState: React.FC = React.memo((props) )}
+
+
+ {enableMcp ? : } +
+
+ {t('GlobalState.mcp')} + + {enableMcp ? t('YakitButton.enabled') : t('YakitButton.notEnabled')} + +
+
{t('GlobalState.mcpDesc')}
+
+
+
+ {enableMcp ? ( +
+ {mcp.mcpStreamInfo.mcpServerUrl} + { + setShow(false) + mcp.mcpStreamEvent.onCancel() + }} + > + {t('GlobalState.disable')} + +
+ ) : ( + { + setShow(false) + setConfigMcpModalVisible(true) + }} + > + {t('GlobalState.toConfigure')} + + )} +
+
)} {isEnpriTraceAgent() && state === 'success' && ( @@ -1149,6 +1215,8 @@ export const GlobalState: React.FC = React.memo((props) stateNum, showCheckEngine, isChecking, + enableMcp, + mcp.mcpStreamInfo.mcpServerUrl, Array.from(runNodeList).length, i18n.language, zoomScale, @@ -1383,6 +1451,7 @@ export const GlobalState: React.FC = React.memo((props) setCloseRunNodeItemVerifyVisible(false) }} /> + {configMcpModalVisible && setConfigMcpModalVisible(false)} />} ) }) diff --git a/app/renderer/src/main/src/components/layout/UILayout.tsx b/app/renderer/src/main/src/components/layout/UILayout.tsx index ffb486d23a..5c263c4ac7 100644 --- a/app/renderer/src/main/src/components/layout/UILayout.tsx +++ b/app/renderer/src/main/src/components/layout/UILayout.tsx @@ -92,6 +92,7 @@ import { JSONParseLog } from '@/utils/tool' import { LocalGVS } from '@/enums/localGlobal' import { useSoftMode } from '@/store/softMode' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' +import useMcpStream from './hooks/useMcp/useMcp' import { YakParamProps } from '@/pages/plugins/pluginsType' import { yakitAI, @@ -130,6 +131,7 @@ export interface UILayoutProp { const UILayout: React.FC = (props) => { const { t, i18n } = useI18nNamespaces(['layout', 'yakitUi']) + const [mcpStreamInfo, mcpStreamEvent] = useMcpStream({}) const { currentPageTabRouteKey } = usePageInfo( (s) => ({ currentPageTabRouteKey: s.currentPageTabRouteKey, @@ -1596,6 +1598,14 @@ const UILayout: React.FC = (props) => { }) const dropClassName = { [styles['header-title-drop']]: drop } + const mcp = useMemo( + () => ({ + mcpStreamInfo, + mcpStreamEvent, + }), + [mcpStreamInfo, mcpStreamEvent], + ) + return (
@@ -1676,6 +1686,7 @@ const UILayout: React.FC = (props) => { isEngineLink={engineLink} engineMode={engineMode || 'remote'} isRemoteMode={isRemoteEngine} + mcp={mcp} onEngineModeChange={handleOperations} runDynamicControlRemote={runControlRemote} typeCallback={handleOperations} @@ -1687,7 +1698,7 @@ const UILayout: React.FC = (props) => { {!showProjectManage && ( <>
- + )} @@ -1710,7 +1721,7 @@ const UILayout: React.FC = (props) => {
{engineLink && ( <> - {!showProjectManage && } + {!showProjectManage && } {!isEnpriTraceAgent() && (
= (props) => { isReverse={true} engineMode={engineMode || 'remote'} isRemoteMode={isRemoteEngine} + mcp={mcp} onEngineModeChange={handleOperations} runDynamicControlRemote={runControlRemote} typeCallback={handleOperations} diff --git a/app/renderer/src/main/src/components/layout/hooks/useMcp/useMcp.ts b/app/renderer/src/main/src/components/layout/hooks/useMcp/useMcp.ts index 4008b9e7df..fbc73e9bfd 100644 --- a/app/renderer/src/main/src/components/layout/hooks/useMcp/useMcp.ts +++ b/app/renderer/src/main/src/components/layout/hooks/useMcp/useMcp.ts @@ -62,7 +62,7 @@ export default function useMcpStream(props: useMcpHooks) { const offError = yakitStream.onError(mcpToken, (error) => { setMcpServerUrl('') setMcpCurrent({ Status: 'error', Message: error + '', ServerUrl: '' }) - yakitNotify('error', t('McpHook.enableFailed', { error: error + '' })) + yakitNotify('success', t('McpHook.MCPStopped')) }) const offEnd = yakitStream.onEnd(mcpToken, () => { diff --git a/app/renderer/src/main/src/pages/fuzzer/HTTPFuzzerHotPatch.tsx b/app/renderer/src/main/src/pages/fuzzer/HTTPFuzzerHotPatch.tsx index 3fa39fb5eb..ca729b93e6 100644 --- a/app/renderer/src/main/src/pages/fuzzer/HTTPFuzzerHotPatch.tsx +++ b/app/renderer/src/main/src/pages/fuzzer/HTTPFuzzerHotPatch.tsx @@ -87,6 +87,53 @@ const HotPatchParamsGetterDefault = `__getParams__ = func() { const { ipcRenderer } = window.require('electron') +const getSharedHotReloadEnabled = async () => { + try { + const raw = await getRemoteValue(FuzzerRemoteGV.FuzzerHotCodeSwitchAndCode) + return raw === 'true' + } catch (error) { + return false + } +} + +const setSharedHotReloadEnabled = (enabled: boolean) => { + setRemoteValue(FuzzerRemoteGV.FuzzerHotCodeSwitchAndCode, `${enabled}`) +} + +const getWebFuzzerPageList = () => { + return usePageInfo.getState().pages.get(YakitRoute.HTTPFuzzer)?.pageList || [] +} + +const syncSharedHotReloadOwner = (ownerPageId?: string) => { + const pageInfoStore = usePageInfo.getState() + const fuzzerPages = getWebFuzzerPageList() + + fuzzerPages.forEach((item) => { + const webFuzzerPageInfo = item.pageParamsInfo?.webFuzzerPageInfo + if (!webFuzzerPageInfo) return + + const nextShared = !!ownerPageId && item.pageId === ownerPageId + if (!!webFuzzerPageInfo.sharedHotReloadCode === nextShared) return + + pageInfoStore.updatePagesDataCacheById(YakitRoute.HTTPFuzzer, { + ...item, + pageParamsInfo: { + ...item.pageParamsInfo, + webFuzzerPageInfo: { + ...webFuzzerPageInfo, + sharedHotReloadCode: nextShared, + }, + }, + }) + }) +} + +const getSharedHotReloadOwnerPageInfo = () => { + const fuzzerPages = getWebFuzzerPageList() + const owner = fuzzerPages.find((item) => item.pageParamsInfo?.webFuzzerPageInfo?.sharedHotReloadCode) + return owner?.pageParamsInfo?.webFuzzerPageInfo +} + export const HTTPFuzzerHotPatch: React.FC = (props) => { const { t, i18n } = useI18nNamespaces(['webFuzzer', 'yakitUi']) const { queryPagesDataById } = usePageInfo( @@ -463,29 +510,24 @@ export const HTTPFuzzerHotPatch: React.FC = (props) => { ) } -export const getHotPatchCodeInfo = async (currentWebFuzzerPageInfo?: WebFuzzerPageInfoProps) => { - if (!currentWebFuzzerPageInfo) { +export const getHotPatchCodeInfo = async () => { + const sharedHotReloadCode = await getSharedHotReloadEnabled() + if (!sharedHotReloadCode) { return { hotPatchCode: HotPatchDefaultContent, hotPatchOpen: false } } - let sharedHotReloadCode = false - try { - const raw = await getRemoteValue(FuzzerRemoteGV.FuzzerHotCodeSwitchAndCode) - if (raw) { - sharedHotReloadCode = raw === 'true' - } - } catch (error) {} - - if (sharedHotReloadCode) { - const disableHotPatch = currentWebFuzzerPageInfo.advancedConfigValue?.disableHotPatch - const hotPatchOpen = typeof disableHotPatch === 'boolean' ? !disableHotPatch : false - return { - hotPatchCode: currentWebFuzzerPageInfo.hotPatchCode || HotPatchDefaultContent, - hotPatchOpen, - } + const ownerPageInfo = getSharedHotReloadOwnerPageInfo() + if (!ownerPageInfo) { + setSharedHotReloadEnabled(false) + return { hotPatchCode: HotPatchDefaultContent, hotPatchOpen: false } } - return { hotPatchCode: HotPatchDefaultContent, hotPatchOpen: false } + const disableHotPatch = ownerPageInfo.advancedConfigValue?.disableHotPatch + const hotPatchOpen = typeof disableHotPatch === 'boolean' ? !disableHotPatch : false + return { + hotPatchCode: ownerPageInfo.hotPatchCode || HotPatchDefaultContent, + hotPatchOpen, + } } interface HTTPFuzzerHotPatchSidebarProp { @@ -563,8 +605,11 @@ export const HTTPFuzzerHotPatchSidebar: React.FC if (!visible || !props.inViewport) { return } - setRemoteValue(FuzzerRemoteGV.FuzzerHotCodeSwitchAndCode, `${sharedHotReloadCode}`) - }, [visible, props.inViewport]) + + const currentPageInfo = usePageInfo.getState().queryPagesDataById(YakitRoute.HTTPFuzzer, props.pageId) + ?.pageParamsInfo?.webFuzzerPageInfo + setSharedHotReloadCodeState(!!currentPageInfo?.sharedHotReloadCode) + }, [visible, props.inViewport, props.pageId]) useEffect(() => { tempNameRef.current = selectedTemplateNameProp || '' @@ -721,7 +766,10 @@ export const HTTPFuzzerHotPatchSidebar: React.FC const setSharedHotReloadCode = useMemoizedFn((checked: boolean) => { setSharedHotReloadCodeState(checked) - setRemoteValue(FuzzerRemoteGV.FuzzerHotCodeSwitchAndCode, `${checked}`) + + // 仅允许一个页面开启:打开时抢占当前页,关闭时清空全部 + syncSharedHotReloadOwner(checked ? props.pageId : undefined) + setSharedHotReloadEnabled(checked) }) useShortcutKeyTrigger( diff --git a/app/renderer/src/main/src/pages/hTTPHistoryAnalysis/HTTPHistory/HTTPHistoryFilter.module.scss b/app/renderer/src/main/src/pages/hTTPHistoryAnalysis/HTTPHistory/HTTPHistoryFilter.module.scss index d0ea276532..39414beb80 100644 --- a/app/renderer/src/main/src/pages/hTTPHistoryAnalysis/HTTPHistory/HTTPHistoryFilter.module.scss +++ b/app/renderer/src/main/src/pages/hTTPHistoryAnalysis/HTTPHistory/HTTPHistoryFilter.module.scss @@ -171,6 +171,25 @@ } } } + + .favorite-icon { + svg { + width: 16px; + height: 16px; + } + } + + .favorite-icon-active { + margin-top: -3px; + margin-left: -2px; + width: 19px; + color: var(--Colors-Use-Yellow-Primary); + + svg { + width: 19px; + height: 19px; + } + } } .http-history-table-color-popover { 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 1267db0732..db4aaa5b71 100644 --- a/app/renderer/src/main/src/pages/hTTPHistoryAnalysis/HTTPHistory/HTTPHistoryFilter.tsx +++ b/app/renderer/src/main/src/pages/hTTPHistoryAnalysis/HTTPHistory/HTTPHistoryFilter.tsx @@ -20,6 +20,7 @@ import { OutlineReplyIcon, OutlineSearchIcon, OutlineSelectorIcon, + OutlineStarIcon, OutlineXIcon, } from '@/assets/icon/outline' import classNames from 'classnames' @@ -28,14 +29,18 @@ import { TableVirtualResize } from '@/components/TableVirtualResize/TableVirtual import { AdvancedSet, availableColors, + buildFavoriteTags, + buildHTTPFlowQueryTags, CalloutColor, ColorSearch, ColumnAllInfoItem, contentType, ExportHTTPFlowStreamRequest, + filterHTTPFlowsByFavoriteAndTags, getClassNameData, getHTTPFlowReqAndResToString, HistorySearch, + HTTP_FLOW_FAVORITE_TAG, HTTPFlow, ImportExportProgress, onConvertBodySizeByUnit, @@ -43,12 +48,14 @@ import { onRemoveCalloutColor, onSendToTab, RangeInputNumberTableWrapper, + isHTTPFlowFavorite, SourceType, + toggleHTTPFlowFavorite, YakQueryHTTPFlowResponse, } from '@/components/HTTPFlowTable/HTTPFlowTable' import { YakQueryHTTPFlowRequest } from '@/utils/yakQueryHTTPFlow' import { ColumnsTypeProps, FiltersItemProps, SortProps } from '@/components/TableVirtualResize/TableVirtualResizeType' -import { filterColorTag, isCellRedSingleColor } from '@/components/TableVirtualResize/utils' +import { filterColorTag, getSingleColorType, isCellRedSingleColor } from '@/components/TableVirtualResize/utils' import { yakitNotify } from '@/utils/notification' import { ArrowCircleRightSvgIcon, CheckCircleIcon, ChromeFrameSvgIcon, ColorSwatchIcon } from '@/assets/newIcon' import { formatTimestamp } from '@/utils/timeUtil' @@ -95,6 +102,7 @@ import { ShortcutKeyFocusType, } from '@/utils/globalShortcutKey/events/global' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' +import { SolidStarIcon } from '@/assets/icon/solid' import styles from './HTTPHistoryFilter.module.scss' import useGetSetState from '@/pages/pluginHub/hooks/useGetSetState' @@ -457,6 +465,8 @@ const HTTPFlowFilterTable: React.FC = React.memo((props) => Order: 'desc', OrderBy: 'Id', }) + const [onlyFavorite, setOnlyFavorite] = useState(false) + const campareOnlyFavorite = useCampare(onlyFavorite) // #region 网站树、进程 const refreshTabsContRef = useRef(false) @@ -483,11 +493,11 @@ const HTTPFlowFilterTable: React.FC = React.memo((props) => setQuery((prev) => { return { ...prev, - Tags: nextTags, + Tags: buildHTTPFlowQueryTags(nextTags, onlyFavorite), } }) }, - [campareTagsFilter], + [campareTagsFilter, campareOnlyFavorite], { wait: 500 }, ) // #endregion @@ -759,7 +769,7 @@ const HTTPFlowFilterTable: React.FC = React.memo((props) => const newQuery = { ...prev, ...filter, - Tags: [...tagsFilter], + Tags: buildHTTPFlowQueryTags([...tagsFilter], onlyFavorite), bodyLength: !!(afterBodyLength || beforeBodyLength || getBodyLengthSort() || checkBodyLength), // 主要是用来响应长度icon显示颜色 } @@ -988,7 +998,7 @@ const HTTPFlowFilterTable: React.FC = React.memo((props) => return text ? `${text}` .split('|') - .filter((i) => !i.startsWith('YAKIT_COLOR_')) + .filter((i) => !i.startsWith('YAKIT_COLOR_') && i !== HTTP_FLOW_FAVORITE_TAG) .join(', ') : '' }, @@ -1195,12 +1205,38 @@ const HTTPFlowFilterTable: React.FC = React.memo((props) => { title: t('YakitTable.action'), dataKey: 'action', - width: 80, + width: 104, fixed: 'right', render: (_, rowData) => { if (!rowData.Hash) return <> + const colorType = getSingleColorType(rowData.cellClassName) + const favorite = isHTTPFlowFavorite(rowData) return ( -
+
+ {favorite ? ( + { + e.stopPropagation() + toggleHTTPFlowFavorite(rowData, false, setData, onlyFavorite) + }} + /> + ) : ( + { + e.stopPropagation() + toggleHTTPFlowFavorite(rowData, true, setData, onlyFavorite) + }} + /> + )} +
= React.memo((props) => bodyLengthUnit, contentType, i18n.language, + onlyFavorite, ]) // #endregion @@ -1355,6 +1392,23 @@ const HTTPFlowFilterTable: React.FC = React.memo((props) => ], onClickBatch: () => {}, }, + { + key: 'favorite', + label: t('HTTPFlowTable.RowContextMenu.favorite'), + default: true, + webSocket: true, + number: 20, + onClickSingle: (v) => toggleHTTPFlowFavorite(v, !isHTTPFlowFavorite(v), setData, onlyFavorite), + onClickBatch: (list, n) => toggleHTTPFlowFavoriteBatch(list, n, true), + }, + { + key: 'cancelFavorite', + label: t('HTTPFlowTable.RowContextMenu.cancelFavorite'), + default: false, + webSocket: false, + number: 20, + onClickBatch: (list, n) => toggleHTTPFlowFavoriteBatch(list, n, false), + }, { key: '复制 URL', label: t('HTTPFlowTable.RowContextMenu.copyURL'), @@ -1504,7 +1558,7 @@ const HTTPFlowFilterTable: React.FC = React.memo((props) => }, }, ] - }, [data, compareState, i18n.language]) + }, [data, compareState, i18n.language, onlyFavorite]) const contextMenuKeybindingHandle = useMemoizedFn((data) => { const menus: any = [] @@ -1545,8 +1599,13 @@ const HTTPFlowFilterTable: React.FC = React.memo((props) => return contextMenuKeybindingHandle(menuData) .filter((item) => (rowData.IsWebsocket ? item.webSocket : item.default)) .map((ele) => { + const isFavoriteMenu = ele.key === 'favorite' return { - label: ele.label, + label: isFavoriteMenu + ? isHTTPFlowFavorite(rowData) + ? t('HTTPFlowTable.RowContextMenu.cancelFavorite') + : t('HTTPFlowTable.RowContextMenu.favorite') + : ele.label, key: ele.key, children: ele.children || [], } @@ -1850,6 +1909,43 @@ const HTTPFlowFilterTable: React.FC = React.memo((props) => }) }) + const toggleHTTPFlowFavoriteBatch = useMemoizedFn((flowList: HTTPFlow[], number: number, favorite: boolean) => { + if (flowList.length === 0) { + yakitNotify('warning', t('HTTPFlowTable.pleaseSelectData')) + return + } + if (flowList.length > number) { + yakitNotify('warning', t('HTTPFlowTable.maxOperateData', { number })) + return + } + const newList = flowList.map((flow) => ({ + Id: flow.Id, + Hash: flow.Hash, + Tags: buildFavoriteTags(flow.Tags, favorite), + })) + ipcRenderer + .invoke('SetTagForHTTPFlow', { + CheckTags: newList, + }) + .then(() => { + const newData = data.map((item) => { + const find = newList.find((ele) => ele.Hash === item.Hash) + if (!find) return item + return { + ...item, + Tags: find.Tags.join('|'), + } + }) + setData(onlyFavorite && !favorite ? newData.filter(isHTTPFlowFavorite) : newData) + setSelectedRowKeys([]) + setSelectedRows([]) + yakitNotify('success', favorite ? t('HTTPFlowTable.favoriteSuccess') : t('HTTPFlowTable.cancelFavoriteSuccess')) + }) + .catch((e) => { + yakitNotify('error', `${e}`) + }) + }) + // 导出为EXCEL const [exportDataKey, setExportDataKey] = useState([]) const onExcelExport = (list: number[]) => { @@ -2088,6 +2184,18 @@ const HTTPFlowFilterTable: React.FC = React.memo((props) => { wait: 500 }, ).run + const onToggleOnlyFavorite = useMemoizedFn(() => { + const nextOnlyFavorite = !onlyFavorite + setOnlyFavorite(nextOnlyFavorite) + setQuery((prev) => ({ + ...prev, + Tags: buildHTTPFlowQueryTags(tagsFilter, nextOnlyFavorite), + })) + setIsAllSelect(false) + setSelectedRowKeys([]) + setSelectedRows([]) + }) + useUpdateEffect(() => { queyChangeUpdateData() }, [query, refresh]) @@ -2159,10 +2267,14 @@ const HTTPFlowFilterTable: React.FC = React.memo((props) => .invoke('QueryHTTPFlows', params) .then((res: YakQueryHTTPFlowResponse) => { const resData = res?.Data || [] - const dataHasClassName: HTTPFlow[] = getClassNameData(resData) + const dataHasClassName: HTTPFlow[] = filterHTTPFlowsByFavoriteAndTags( + getClassNameData(resData), + tagsFilter, + onlyFavorite, + ) const d = isInit ? dataHasClassName : mergeHTTPFlowsById(data, dataHasClassName) setData(d) - setTotal(res.Total) + setTotal(onlyFavorite && tagsFilter.length > 0 ? d.length : res.Total) const page = { ...res.Pagination } delete page['AfterId'] @@ -2218,6 +2330,16 @@ const HTTPFlowFilterTable: React.FC = React.memo((props) => { wait: 300 }, ) + const onlyFavoriteTag = useMemo( + () => + onlyFavorite && ( + onToggleOnlyFavorite()} style={{ marginLeft: 8 }}> + {t('HTTPFlowTable.onlyFavorites')} + + ), + [onlyFavorite, i18n.language], + ) + return (
@@ -2259,6 +2381,7 @@ const HTTPFlowFilterTable: React.FC = React.memo((props) => ))} + {onlyFavoriteTag}
{webFuzzerPageId && toWebFuzzer && closable && ( @@ -2360,6 +2483,16 @@ const HTTPFlowFilterTable: React.FC = React.memo((props) => }) })} /> + + } + onClick={(e) => { + e.currentTarget.blur() + onToggleOnlyFavorite() + }} + /> +
= React.memo((props) => trigger="click" placement="bottomLeft" > - { - e.stopPropagation() - }} - > + {t('YakitButton.batchOperation')} diff --git a/app/renderer/src/main/src/pages/layout/mainOperatorContent/MainOperatorContent.tsx b/app/renderer/src/main/src/pages/layout/mainOperatorContent/MainOperatorContent.tsx index 54a2a7cd0b..d8fed76bab 100644 --- a/app/renderer/src/main/src/pages/layout/mainOperatorContent/MainOperatorContent.tsx +++ b/app/renderer/src/main/src/pages/layout/mainOperatorContent/MainOperatorContent.tsx @@ -1611,12 +1611,7 @@ export const MainOperatorContent: React.FC = React.mem newAdvancedConfigValue.noSystemProxy = noSystemProxy } - // 共享热加载时,以当前选中的 WebFuzzer 状态为准 - const currentPageId = getCurrentSelectPageId(YakitRoute.HTTPFuzzer) - const currentPageInfo = currentPageId - ? queryPagesDataById(YakitRoute.HTTPFuzzer, currentPageId)?.pageParamsInfo?.webFuzzerPageInfo - : undefined - const { hotPatchCode, hotPatchOpen } = await getHotPatchCodeInfo(currentPageInfo) + const { hotPatchCode, hotPatchOpen } = await getHotPatchCodeInfo() openMenuPage( { route: YakitRoute.HTTPFuzzer }, diff --git a/app/renderer/src/main/src/store/pageInfo.ts b/app/renderer/src/main/src/store/pageInfo.ts index 6d8a4a5165..30b43cfff2 100644 --- a/app/renderer/src/main/src/store/pageInfo.ts +++ b/app/renderer/src/main/src/store/pageInfo.ts @@ -188,6 +188,8 @@ export interface WebFuzzerPageInfoProps { variableActiveKeys?: string[] // 热加载代码 hotPatchCode: string + // 共用热加载代码开关 + sharedHotReloadCode?: boolean } export interface PocPageInfoProps {