From 3aa0ff6a1e181f082541253209a4dad465ab79da Mon Sep 17 00:00:00 2001 From: Monster <389264167@qq.com> Date: Tue, 5 Aug 2025 18:51:11 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E5=8D=A1=E7=89=87=E4=B8=8D=E7=AD=89=E9=AB=98=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/src/components/label/index.tsx | 8 ++++---- ui/src/pages/model/components/modelCard.tsx | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/ui/src/components/label/index.tsx b/ui/src/components/label/index.tsx index 3db9b89..50cdc52 100644 --- a/ui/src/components/label/index.tsx +++ b/ui/src/components/label/index.tsx @@ -9,9 +9,9 @@ const StyledLabel = styled('div')( // 获取颜色值 const getColor = (colorProp: string) => { // 如果是主题预设颜色 - if (['success', 'warning', 'error', 'info'].includes(colorProp)) { + if (['success', 'warning', 'error', 'info', 'disabled'].includes(colorProp)) { return theme.palette[ - colorProp as 'success' | 'warning' | 'error' | 'info' + colorProp as 'success' | 'warning' | 'error' | 'info' | 'disabled' ].main; } // 如果是 default,使用灰色 @@ -26,10 +26,10 @@ const StyledLabel = styled('div')( // 获取背景颜色(淡化版本) const getBackgroundColor = (colorProp: string) => { - if (['success', 'warning', 'error', 'info'].includes(colorProp)) { + if (['success', 'warning', 'error', 'info', 'disabled'].includes(colorProp)) { // 使用主题的 light 版本,如果没有则使用 alpha 透明度 const palette = - theme.palette[colorProp as 'success' | 'warning' | 'error' | 'info']; + theme.palette[colorProp as 'success' | 'warning' | 'error' | 'info' | 'disabled']; return alpha(palette.main, 0.15); } // 如果是 default,使用淡灰色背景 diff --git a/ui/src/pages/model/components/modelCard.tsx b/ui/src/pages/model/components/modelCard.tsx index d3404a9..e38139e 100644 --- a/ui/src/pages/model/components/modelCard.tsx +++ b/ui/src/pages/model/components/modelCard.tsx @@ -194,9 +194,7 @@ const ModelItem = ({ sx={{ mt: 2 }} > - {data.is_active && ( - 正在使用 - )} + {data.is_active ? '正在使用' : '未激活'} {!data.is_active && ( @@ -204,6 +202,9 @@ const ModelItem = ({ disableRipple sx={{ color: 'success.main', + '&:hover': { + fontWeight: 700 + }, }} onClick={onActiveModel} > @@ -216,6 +217,9 @@ const ModelItem = ({ disableRipple sx={{ color: 'info.main', + '&:hover': { + fontWeight: 700 + }, }} onClick={() => onEdit(data)} > @@ -228,6 +232,9 @@ const ModelItem = ({ disableRipple sx={{ color: 'error.main', + '&:hover': { + fontWeight: 700 + }, }} onClick={onInactiveModel} > @@ -240,6 +247,9 @@ const ModelItem = ({ disableRipple sx={{ color: 'error.main', + '&:hover': { + fontWeight: 700 + }, }} onClick={onRemoveModel} > From 47b9a46c20f5ac0ab6e905ed36dbd046a515023b Mon Sep 17 00:00:00 2001 From: Monster <389264167@qq.com> Date: Thu, 7 Aug 2025 11:49:43 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=AE=89=E5=85=A8=E6=89=AB=E6=8F=8F=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E7=9A=84=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/src/api/Cli.ts | 10 +- ui/src/api/CodeSnippet.ts | 50 ++++ ui/src/api/OpenAiv1.ts | 19 ++ ui/src/api/index.ts | 1 + ui/src/api/types.ts | 215 ++++++-------- ui/src/pages/codeSecurity/index.tsx | 279 ++++++++++++++++++ .../components/cardServiceSettings.tsx | 2 +- ui/src/router.tsx | 3 +- ui/src/theme.ts | 8 + 9 files changed, 459 insertions(+), 128 deletions(-) create mode 100644 ui/src/api/CodeSnippet.ts create mode 100644 ui/src/pages/codeSecurity/index.tsx diff --git a/ui/src/api/Cli.ts b/ui/src/api/Cli.ts index 6b8565a..b4b8550 100644 --- a/ui/src/api/Cli.ts +++ b/ui/src/api/Cli.ts @@ -11,7 +11,11 @@ */ import request, { ContentType, RequestParams } from "./httpClient"; -import { DomainCodeFiles, DomainIndexResult, V1CliCreateParams } from "./types"; +import { + DomainIndexResult, + V1CliCreateParams, + V1CliCreatePayload, +} from "./types"; /** * @description 运行monkeycode-cli命令 @@ -26,14 +30,14 @@ import { DomainCodeFiles, DomainIndexResult, V1CliCreateParams } from "./types"; export const v1CliCreate = ( { command, ...query }: V1CliCreateParams, - codeFiles: DomainCodeFiles, + fileMetas: V1CliCreatePayload, params: RequestParams = {}, ) => request({ path: `/api/v1/cli/${command}`, method: "POST", query: query, - body: codeFiles, + body: fileMetas, type: ContentType.Json, format: "json", ...params, diff --git a/ui/src/api/CodeSnippet.ts b/ui/src/api/CodeSnippet.ts new file mode 100644 index 0000000..befc14d --- /dev/null +++ b/ui/src/api/CodeSnippet.ts @@ -0,0 +1,50 @@ +/* eslint-disable */ +/* tslint:disable */ +// @ts-nocheck +/* + * --------------------------------------------------------------- + * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## + * ## ## + * ## AUTHOR: acacode ## + * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## + * --------------------------------------------------------------- + */ + +import { ContentType, RequestParams } from "./httpClient"; +import { + DomainCodeSnippet, + InternalCodesnippetHandlerHttpV1GetContextReq, + WebResp, +} from "./types"; + +/** + * @description 为IDE端提供代码片段上下文检索功能,使用API Key认证。支持单个查询和批量查询。 + * + * @tags CodeSnippet + * @name PostGetContext + * @summary IDE端上下文检索 + * @request POST:/api/v1/ide/codesnippet/context + * @secure + * @response `200` `(WebResp & { + data?: (DomainCodeSnippet)[], + +})` OK + */ + +export const postGetContext = ( + request: InternalCodesnippetHandlerHttpV1GetContextReq, + params: RequestParams = {}, +) => + request< + WebResp & { + data?: DomainCodeSnippet[]; + } + >({ + path: `/api/v1/ide/codesnippet/context`, + method: "POST", + body: request, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }); diff --git a/ui/src/api/OpenAiv1.ts b/ui/src/api/OpenAiv1.ts index 8b9335b..9b66563 100644 --- a/ui/src/api/OpenAiv1.ts +++ b/ui/src/api/OpenAiv1.ts @@ -101,6 +101,25 @@ export const postEmbeddings = (params: RequestParams = {}) => ...params, }); +/** + * @description 固定回包 `{"code": 0, "data": "MonkeyCode"}` + * + * @tags OpenAIV1 + * @name GetHealth + * @summary 健康检查 + * @request GET:/v1/health + * @response `200` `WebResp` OK + */ + +export const getHealth = (params: RequestParams = {}) => + request({ + path: `/v1/health`, + method: "GET", + type: ContentType.Json, + format: "json", + ...params, + }); + /** * @description 模型列表 * diff --git a/ui/src/api/index.ts b/ui/src/api/index.ts index c7c24e0..0641a1d 100644 --- a/ui/src/api/index.ts +++ b/ui/src/api/index.ts @@ -1,6 +1,7 @@ export * from './Admin' export * from './Billing' export * from './Cli' +export * from './CodeSnippet' export * from './Dashboard' export * from './Model' export * from './OpenAiv1' diff --git a/ui/src/api/types.ts b/ui/src/api/types.ts index bd5b1ee..830b453 100644 --- a/ui/src/api/types.ts +++ b/ui/src/api/types.ts @@ -246,8 +246,47 @@ export interface DomainCheckModelReq { type: "llm" | "coder" | "embedding" | "rerank"; } -export interface DomainCodeFiles { - files?: DomainFileMeta[]; +export interface DomainCodeSnippet { + /** 结构化信息 */ + definition?: Record; + /** 定义文本 */ + definitionText?: string; + /** 依赖项 */ + dependencies?: string[]; + /** 结束列号 */ + endColumn?: number; + /** 结束行号 */ + endLine?: number; + /** 容器名称 */ + field?: string; + /** 内容哈希 */ + fileHash?: string; + /** 文件路径 */ + filePath?: string; + /** 代码片段ID */ + id?: string; + /** 编程语言 */ + language?: string; + /** 代码片段名称 */ + name?: string; + /** 命名空间 */ + namespace?: string; + /** 参数列表 */ + parameters?: Record[]; + /** 代码片段内容 */ + rangeText?: string; + /** 作用域信息 */ + scope?: string[]; + /** 函数签名 */ + signature?: string; + /** 起始列号 */ + startColumn?: number; + /** 起始行号 */ + startLine?: number; + /** 代码片段类型 */ + type?: string; + /** 关联的workspace file ID */ + workspace_file_id?: string; } export interface DomainCompletionData { @@ -357,6 +396,7 @@ export interface DomainCreateModelReq { export interface DomainCreateSecurityScanningReq { /** 扫描语言 */ language?: ConstsSecurityScanningLanguage; + user_id?: string; /** 项目目录 */ workspace?: string; } @@ -506,7 +546,7 @@ export interface DomainIndexResult { language?: string; name?: string; rangeText?: string; - scope?: Record[]; + scope?: unknown; signature?: string; startLine?: number; type?: string; @@ -564,19 +604,11 @@ export interface DomainListSecurityScanningReq { author?: string; /** 下一页标识 */ next_token?: string; - /** - * 分页 - * @min 1 - * @default 1 - */ + /** 分页 */ page?: number; /** 项目名称 */ project_name?: string; - /** - * 每页多少条记录 - * @min 1 - * @default 10 - */ + /** 每页多少条记录 */ size?: number; } @@ -796,6 +828,8 @@ export interface DomainSecurityScanningResult { id?: string; /** 扫描任务 */ name?: string; + /** 项目路径 */ + path?: string; /** 项目名称 */ project_name?: string; /** 风险结果 */ @@ -1130,6 +1164,26 @@ export interface DomainWorkspaceFile { workspace_id?: string; } +export interface InternalCodesnippetHandlerHttpV1GetContextReq { + /** 返回结果数量限制,默认10 */ + limit?: number; + /** 批量查询参数 */ + queries?: InternalCodesnippetHandlerHttpV1Query[]; + /** 单个查询参数 */ + query?: InternalCodesnippetHandlerHttpV1Query; + /** 工作区路径(必填) */ + workspacePath?: string; +} + +export interface InternalCodesnippetHandlerHttpV1Query { + /** 编程语言(可选) */ + language?: string; + /** 代码片段名称(可选) */ + name?: string; + /** 代码片段类型(可选) */ + snippetType?: string; +} + export interface WebResp { code?: number; data?: unknown; @@ -1144,34 +1198,18 @@ export interface DeleteDeleteAdminParams { export interface GetListAdminUserParams { /** 下一页标识 */ next_token?: string; - /** - * 分页 - * @min 1 - * @default 1 - */ + /** 分页 */ page?: number; - /** - * 每页多少条记录 - * @min 1 - * @default 10 - */ + /** 每页多少条记录 */ size?: number; } export interface GetAdminLoginHistoryParams { /** 下一页标识 */ next_token?: string; - /** - * 分页 - * @min 1 - * @default 1 - */ + /** 分页 */ page?: number; - /** - * 每页多少条记录 - * @min 1 - * @default 10 - */ + /** 每页多少条记录 */ size?: number; } @@ -1189,17 +1227,9 @@ export interface GetListChatRecordParams { language?: string; /** 下一页标识 */ next_token?: string; - /** - * 分页 - * @min 1 - * @default 1 - */ + /** 分页 */ page?: number; - /** - * 每页多少条记录 - * @min 1 - * @default 10 - */ + /** 每页多少条记录 */ size?: number; /** 工作模式 */ work_mode?: string; @@ -1219,22 +1249,17 @@ export interface GetListCompletionRecordParams { language?: string; /** 下一页标识 */ next_token?: string; - /** - * 分页 - * @min 1 - * @default 1 - */ + /** 分页 */ page?: number; - /** - * 每页多少条记录 - * @min 1 - * @default 10 - */ + /** 每页多少条记录 */ size?: number; /** 工作模式 */ work_mode?: string; } +/** 代码文件信息 */ +export type V1CliCreatePayload = DomainFileMeta[]; + export interface V1CliCreateParams { /** 标志 */ flag?: string; @@ -1370,19 +1395,11 @@ export interface GetSecurityScanningListParams { author?: string; /** 下一页标识 */ next_token?: string; - /** - * 分页 - * @min 1 - * @default 1 - */ + /** 分页 */ page?: number; /** 项目名称 */ project_name?: string; - /** - * 每页多少条记录 - * @min 1 - * @default 10 - */ + /** 每页多少条记录 */ size?: number; } @@ -1405,17 +1422,9 @@ export interface GetUserListChatRecordParams { language?: string; /** 下一页标识 */ next_token?: string; - /** - * 分页 - * @min 1 - * @default 1 - */ + /** 分页 */ page?: number; - /** - * 每页多少条记录 - * @min 1 - * @default 10 - */ + /** 每页多少条记录 */ size?: number; /** 工作模式 */ work_mode?: string; @@ -1435,17 +1444,9 @@ export interface GetUserListCompletionRecordParams { language?: string; /** 下一页标识 */ next_token?: string; - /** - * 分页 - * @min 1 - * @default 1 - */ + /** 分页 */ page?: number; - /** - * 每页多少条记录 - * @min 1 - * @default 10 - */ + /** 每页多少条记录 */ size?: number; /** 工作模式 */ work_mode?: string; @@ -1493,34 +1494,18 @@ export interface DeleteDeleteUserParams { export interface GetListUserParams { /** 下一页标识 */ next_token?: string; - /** - * 分页 - * @min 1 - * @default 1 - */ + /** 分页 */ page?: number; - /** - * 每页多少条记录 - * @min 1 - * @default 10 - */ + /** 每页多少条记录 */ size?: number; } export interface GetLoginHistoryParams { /** 下一页标识 */ next_token?: string; - /** - * 分页 - * @min 1 - * @default 1 - */ + /** 分页 */ page?: number; - /** - * 每页多少条记录 - * @min 1 - * @default 10 - */ + /** 每页多少条记录 */ size?: number; } @@ -1550,19 +1535,11 @@ export interface GetUserSecurityScanningListParams { author?: string; /** 下一页标识 */ next_token?: string; - /** - * 分页 - * @min 1 - * @default 1 - */ + /** 分页 */ page?: number; /** 项目名称 */ project_name?: string; - /** - * 每页多少条记录 - * @min 1 - * @default 10 - */ + /** 每页多少条记录 */ size?: number; } @@ -1576,19 +1553,11 @@ export interface GetListWorkspaceFilesParams { language?: string; /** 下一页标识 */ next_token?: string; - /** - * 分页 - * @min 1 - * @default 1 - */ + /** 分页 */ page?: number; /** 搜索关键词(文件路径) */ search?: string; - /** - * 每页多少条记录 - * @min 1 - * @default 10 - */ + /** 每页多少条记录 */ size?: number; /** 用户ID */ user_id?: string; diff --git a/ui/src/pages/codeSecurity/index.tsx b/ui/src/pages/codeSecurity/index.tsx new file mode 100644 index 0000000..a66ee30 --- /dev/null +++ b/ui/src/pages/codeSecurity/index.tsx @@ -0,0 +1,279 @@ +import React, { useState, useEffect } from 'react'; +import { Table } from '@c-x/ui'; +import { getSecurityScanningList, getSecurityScanningDetail } from '@/api/SecurityScanning'; +import dayjs from 'dayjs'; + +import Card from '@/components/card'; +import { + Autocomplete, + Box, + Stack, + TextField, + Tooltip, +} from '@mui/material'; + +import { ColumnsType } from '@c-x/ui/dist/Table'; +import { useRequest } from 'ahooks'; +import { getListUser } from '@/api/User'; +import { DomainSecurityScanningResult, DomainSecurityScanningRiskResult, DomainUser } from '@/api/types'; +import User from '@/components/user'; + +const StatusText = { + pending: '等待扫描', + running: '正在扫描', + success: '扫描完成', + failed: '扫描失败', +} + +const Chat = () => { + const [page, setPage] = useState(1); + const [size, setSize] = useState(20); + const [total, setTotal] = useState(0); + const [loading, setLoading] = useState(false); + const [dataSource, setDataSource] = useState([]); + + const [filterUser, setFilterUser] = useState(''); + + + const { data: userOptions = { users: [] } } = useRequest(() => + getListUser({ + page: 1, + size: 9999, + }) + ); + + const fetchData = async (params: { + page?: number; + size?: number; + work_mode?: string; + author?: string; + }) => { + setLoading(true); + const res = await getSecurityScanningList({ + page: params.page || page, + size: params.size || size, + author: params.author || filterUser, + }); + setLoading(false); + setTotal(res.total_count || 0); + setDataSource(res.items || []); + }; + + useEffect(() => { + setPage(1); + fetchData({ + page: 1, + author: filterUser + }); + }, [filterUser]); + + const columns: ColumnsType = [ + { + dataIndex: 'name', + title: '扫描任务', + width: 180, + render: (project_name, record) => { + return ( + + + {record?.name} + + + {record?.status ? StatusText[record.status] : '未知状态'} + + + ); + }, + }, + { + title: '项目名称', + dataIndex: 'project_name', + render: (project_name, record) => { + return ( + + + {record?.project_name} + + + {record?.path} + + + ); + }, + }, + { + dataIndex: 'risk', + title: '扫描结果', + render(risk: DomainSecurityScanningRiskResult, record) { + const hasNoRisk = record.status !== 'pending' && + (!risk.severe_count || risk.severe_count <= 0) && + (!risk.critical_count || risk.critical_count <= 0) && + (!risk.suggest_count || risk.suggest_count <= 0); + + const tip = [] + if (risk.severe_count && risk.severe_count > 0) { + tip.push(`严重安全告警 ${risk.severe_count} 个`) + } + if (risk.critical_count && risk.critical_count > 0) { + tip.push(`高风险安全提醒 ${risk.critical_count} 个`) + } + if (risk.suggest_count && risk.suggest_count > 0) { + tip.push(`低风险安全提醒 ${risk.suggest_count} 个`) + } + + return ( + + + {hasNoRisk ? ( + // 如果没有风险,显示"无风险" + + 暂无风险 + + ) : ( + // 否则,显示原有的风险条 + <> + {!!risk.severe_count && risk.severe_count > 0 && {risk.severe_count}} + {!!risk.critical_count && risk.critical_count > 0 && {risk.critical_count}} + {!!risk.suggest_count && risk.suggest_count > 0 && {risk.suggest_count}} + + )} + + + ) + }, + }, + { + dataIndex: 'user', + title: '成员', + width: 200, + render(value: DomainUser) { + return ( + + ); + }, + }, + { + title: '扫描时间', + dataIndex: 'created_at', + width: 160, + render: (text) => { + return ( + + + {dayjs.unix(text).format('YYYY-MM-DD')} + + + {dayjs.unix(text).format('HH:mm:ss')} + + + ) + }, + }, + ]; + return ( + + + option.username || ''} + value={ + userOptions.users?.find((item) => item.username === filterUser) || + null + } + onChange={(_, newValue) => + setFilterUser(newValue ? newValue.username! : '') + } + isOptionEqualToValue={(option, value) => + option.username === value.username + } + renderInput={(params) => } + clearOnEscape + /> + + { + setPage(page); + setSize(size); + fetchData({ + page: page, + size: size, + }); + }, + }} + /> + + ); +}; + +export default Chat; diff --git a/ui/src/pages/generalSetting/components/cardServiceSettings.tsx b/ui/src/pages/generalSetting/components/cardServiceSettings.tsx index 654ae0d..df5e42a 100644 --- a/ui/src/pages/generalSetting/components/cardServiceSettings.tsx +++ b/ui/src/pages/generalSetting/components/cardServiceSettings.tsx @@ -108,7 +108,7 @@ const CardServiceSettings = () => { 用于解决 VSCode 插件无法连接 MonkeyCode 服务的问题 diff --git a/ui/src/router.tsx b/ui/src/router.tsx index fb67798..1988b1f 100644 --- a/ui/src/router.tsx +++ b/ui/src/router.tsx @@ -40,6 +40,7 @@ const Login = LazyLoadable(lazy(() => import('@/pages/login'))); const UserLogin = LazyLoadable(lazy(() => import('@/pages/user/login'))); const Expectation = LazyLoadable(lazy(() => import('@/pages/expectation'))); const UserChat = LazyLoadable(lazy(() => import('@/pages/user/chat'))); +const CodeSecurity = LazyLoadable(lazy(() => import('@/pages/codeSecurity'))); const UserCompletion = LazyLoadable( lazy(() => import('@/pages/user/completion')) ); @@ -68,7 +69,7 @@ const routerConfig = [ }, { path: 'code-security', - element: , + element: , }, { path: 'completion', diff --git a/ui/src/theme.ts b/ui/src/theme.ts index 9e6a741..e97ed30 100644 --- a/ui/src/theme.ts +++ b/ui/src/theme.ts @@ -25,9 +25,17 @@ const lightTheme = createTheme( dark: '#229A16', contrastText: '#fff', }, + warning: { + main: '#FFA500', + }, info: { main: '#3248F2', }, + risk: { + severe: '#FF6262', + critical: '#FFA762', + suggest: '#FFCF62' + }, disabled: { main: '#666', }, From 9bc8abfede334f0ac733f85c0ffa613bd9ec3199 Mon Sep 17 00:00:00 2001 From: Monster <389264167@qq.com> Date: Thu, 7 Aug 2025 21:18:06 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=AE=89=E5=85=A8=E6=89=AB=E6=8F=8F=E7=9A=84=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/src/api/types.ts | 15 + ui/src/components/codescan/taskDetail.tsx | 157 ++++++++++ ui/src/components/codescan/taskList.tsx | 343 ++++++++++++++++++++++ ui/src/components/header/Bread.tsx | 2 + ui/src/components/header/index.tsx | 3 +- ui/src/components/sidebar/index.tsx | 12 +- ui/src/pages/codeSecurity/index.tsx | 279 ------------------ ui/src/pages/codescan/index.tsx | 19 ++ ui/src/pages/user/codescan/index.tsx | 10 + ui/src/router.tsx | 11 +- 10 files changed, 565 insertions(+), 286 deletions(-) create mode 100644 ui/src/components/codescan/taskDetail.tsx create mode 100644 ui/src/components/codescan/taskList.tsx delete mode 100644 ui/src/pages/codeSecurity/index.tsx create mode 100644 ui/src/pages/codescan/index.tsx create mode 100644 ui/src/pages/user/codescan/index.tsx diff --git a/ui/src/api/types.ts b/ui/src/api/types.ts index 830b453..61496d4 100644 --- a/ui/src/api/types.ts +++ b/ui/src/api/types.ts @@ -843,17 +843,26 @@ export interface DomainSecurityScanningResult { export interface DomainSecurityScanningRiskDetail { /** 风险描述 */ desc?: string; + /** 风险代码行结束位置 */ + end?: GithubComChaitinMonkeyCodeBackendEntTypesPosition; /** 风险文件名 */ filename?: string; + /** 修复建议 */ + fix?: string; /** 风险id */ id?: string; /** 风险等级 */ level?: ConstsSecurityScanningRiskLevel; + /** 风险代码行 */ + lines?: string; + /** 风险代码行开始位置 */ + start?: GithubComChaitinMonkeyCodeBackendEntTypesPosition; } export interface DomainSecurityScanningRiskResult { /** 高危数 */ critical_count?: number; + id?: string; /** 严重数 */ severe_count?: number; /** 建议数 */ @@ -1164,6 +1173,12 @@ export interface DomainWorkspaceFile { workspace_id?: string; } +export interface GithubComChaitinMonkeyCodeBackendEntTypesPosition { + col?: number; + line?: number; + offset?: number; +} + export interface InternalCodesnippetHandlerHttpV1GetContextReq { /** 返回结果数量限制,默认10 */ limit?: number; diff --git a/ui/src/components/codescan/taskDetail.tsx b/ui/src/components/codescan/taskDetail.tsx new file mode 100644 index 0000000..6a5a098 --- /dev/null +++ b/ui/src/components/codescan/taskDetail.tsx @@ -0,0 +1,157 @@ +import Card from '@/components/card'; +import { Ellipsis, Modal } from '@c-x/ui'; + +import { useEffect, useState } from 'react'; +import { DomainSecurityScanningResult, DomainSecurityScanningRiskDetail } from '@/api/types'; +import { getSecurityScanningDetail } from '@/api'; +import { Box, CircularProgress, List, ListItem, ListItemButton, Stack } from '@mui/material'; + +interface RiskLevelBoxProps { + level: 'severe' | 'critical' | 'suggest'; +} + +const RiskLevelBox = ({ level }: RiskLevelBoxProps) => { + const riskConfig = { + severe: { + text: '严重', + color: 'risk.severe', + }, + critical: { + text: '高风险', + color: 'risk.critical', + }, + suggest: { + text: '低风险', + color: 'risk.suggest', + }, + }; + + const config = riskConfig[level]; + + if (!config) return null; + + return ( + + {config.text} + + ); +}; + +const TaskDetail = ({ + task, + open, + onClose, +}: { + task?: DomainSecurityScanningResult; + open: boolean; + onClose: () => void; +}) => { + const [loading, setLoading] = useState(true); + const [vulns, setVulns] = useState([]); + + const fetchData = async () => { + setLoading(true); + const resp = await getSecurityScanningDetail({ + id: task?.id as string + }); + setVulns(resp); + setLoading(false); + }; + + useEffect(() => { + console.log(task) + if (open) { + fetchData(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [task, open]); + + return ( + + {task?.name} / {task?.project_name} + + } + sx={{ + '.MuiDialog-paper': { + maxWidth: 1300, + }, + }} + width={1200} + open={open} + onCancel={onClose} + footer={null} + > + + + {loading ? ( +
+ +
+ ) : ( + vulns.map((vuln) => ( + + + + + + {vuln.desc} + + + {vuln.filename}:{vuln?.start?.line} + + + + + )) + )} +
+
+
+ ); +}; + +export default TaskDetail; diff --git a/ui/src/components/codescan/taskList.tsx b/ui/src/components/codescan/taskList.tsx new file mode 100644 index 0000000..5c1f0ac --- /dev/null +++ b/ui/src/components/codescan/taskList.tsx @@ -0,0 +1,343 @@ +import { useState, useEffect } from 'react'; +import { Table } from '@c-x/ui'; +import { getSecurityScanningList } from '@/api/SecurityScanning'; +import dayjs from 'dayjs'; + +import Card from '@/components/card'; +import { + Autocomplete, + Box, + Stack, + TextField, + Tooltip, +} from '@mui/material'; + +import { ColumnsType } from '@c-x/ui/dist/Table'; +import { DomainSecurityScanningResult, DomainSecurityScanningRiskResult, DomainUser } from '@/api/types'; +import User from '@/components/user'; +import TaskDetail from './taskDetail'; +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; +import AutoModeIcon from '@mui/icons-material/AutoMode'; +import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; +import { getUserSecurityScanningList } from '@/api'; + +const CodeScanTaskList = ({ + admin, + users +}: { + admin: boolean, + users: DomainUser[] +}) => { + const [page, setPage] = useState(1); + const [size, setSize] = useState(20); + const [total, setTotal] = useState(0); + const [loading, setLoading] = useState(false); + const [dataSource, setDataSource] = useState([]); + const [detail, setDetail] = useState(); + const [filterUser, setFilterUser] = useState(''); + + const fetchData = async (params: { + page?: number; + size?: number; + work_mode?: string; + author?: string; + }) => { + setLoading(true); + const res = await (admin ? getSecurityScanningList : getUserSecurityScanningList)({ + page: params.page || page, + size: params.size || size, + author: params.author || filterUser, + }); + setLoading(false); + setTotal(res.total_count || 0); + setDataSource(res.items || []); + }; + + useEffect(() => { + setPage(1); + fetchData({ + page: 1, + author: filterUser + }); + }, [filterUser]); + + const columns: ColumnsType = [ + { + dataIndex: 'name', + title: '扫描任务', + width: 240, + render: (project_name, record) => { + return ( + + + {record?.name} + + + {(record.status === 'pending') && + + 等待扫描 + } + {(record.status === 'running') && + + 正在扫描 + } + {(record.status === 'success') && + + 扫描完成 + } + {(record.status === 'failed') && + + 扫描失败 + } + + + ); + }, + }, + { + title: '项目名称', + dataIndex: 'project_name', + render: (project_name, record) => { + return ( + + + {record?.project_name} + + + {record?.path} + + + ); + }, + }, + { + dataIndex: 'risk', + title: '扫描结果', + width: 260, + render(risk: DomainSecurityScanningRiskResult, record) { + const hasNoRisk = record.status !== 'pending' && + (!risk.severe_count || risk.severe_count <= 0) && + (!risk.critical_count || risk.critical_count <= 0) && + (!risk.suggest_count || risk.suggest_count <= 0); + + const tip = [] + if (risk.severe_count && risk.severe_count > 0) { + tip.push(`严重安全告警 ${risk.severe_count} 个`) + } + if (risk.critical_count && risk.critical_count > 0) { + tip.push(`高风险安全提醒 ${risk.critical_count} 个`) + } + if (risk.suggest_count && risk.suggest_count > 0) { + tip.push(`低风险安全提醒 ${risk.suggest_count} 个`) + } + + return ( + + { + if (!hasNoRisk) { + setDetail(record) + } + }} + sx={{ + color: '#fff', + fontSize: '12px', + width: '200px', + height: '24px', + lineHeight: '24px', + background: (record.status === 'pending' || record.status === 'running') ? 'repeating-linear-gradient(45deg, #f0f0f0, #f0f0f0 10px, #e0e0e0 10px, #e0e0e0 20px)' : '#F1F2F8', + backgroundSize: '30px 30px', + animation: 'stripes 1s linear infinite', + borderRadius: '4px', + overflow: 'hidden', + transition: 'box-shadow 0.3s ease', + userSelect: 'none', + '&:hover': { + cursor: "pointer", + boxShadow: hasNoRisk ? '' : '0 0px 8px #FFCF62', + }, + '@keyframes stripes': { + '0%': { + backgroundPosition: '0 0', + }, + '100%': { + backgroundPosition: '30px 0', + }, + }, + }}> + {((record.status === 'success' || record.status === 'failed') && hasNoRisk) ? ( + // 如果没有风险,显示"无风险" + + 暂无风险 + + ) : ( + // 否则,显示原有的风险条 + <> + {!!risk.severe_count && risk.severe_count > 0 && {risk.severe_count}} + {!!risk.critical_count && risk.critical_count > 0 && {risk.critical_count}} + {!!risk.suggest_count && risk.suggest_count > 0 && {risk.suggest_count}} + + )} + + + ) + }, + }, + { + dataIndex: 'user', + title: '成员', + width: 200, + render(value: DomainUser) { + return ( + + ); + }, + }, + { + title: '扫描时间', + dataIndex: 'created_at', + width: 160, + render: (text) => { + return ( + + + {dayjs.unix(text).format('YYYY-MM-DD')} + + + {dayjs.unix(text).format('HH:mm:ss')} + + + ) + }, + }, + ]; + return ( + + {admin && + option.username || ''} + value={ + users?.find((item) => item.username === filterUser) || + null + } + onChange={(_, newValue) => + setFilterUser(newValue ? newValue.username! : '') + } + isOptionEqualToValue={(option, value) => + option.username === value.username + } + renderInput={(params) => } + clearOnEscape + /> + } +
{ + setPage(page); + setSize(size); + fetchData({ + page: page, + size: size, + }); + }, + }} + /> + setDetail(undefined)} + task={detail} + /> + + ); +}; + +export default CodeScanTaskList; diff --git a/ui/src/components/header/Bread.tsx b/ui/src/components/header/Bread.tsx index fa363ec..550e1c1 100644 --- a/ui/src/components/header/Bread.tsx +++ b/ui/src/components/header/Bread.tsx @@ -7,6 +7,7 @@ const ADMIN_BREADCRUMB_MAP: Record = { dashboard: { title: '仪表盘', to: '/' }, chat: { title: '对话记录', to: '/chat' }, completion: { title: '补全记录', to: '/completion' }, + codescan: { title: '代码安全', to: '/codescan' }, model: { title: '模型管理', to: '/model' }, 'member-management': { title: '成员管理', to: '/member-management' }, admin: { title: '管理员', to: '/admin' }, @@ -16,6 +17,7 @@ const USER_BREADCRUMB_MAP: Record = { dashboard: { title: '仪表盘', to: '/user/dashboard' }, chat: { title: '对话记录', to: '/user/chat' }, completion: { title: '补全记录', to: '/user/completion' }, + codescan: { title: '代码安全', to: '/user/codescan' }, }; const Bread = () => { diff --git a/ui/src/components/header/index.tsx b/ui/src/components/header/index.tsx index cdfc741..6a97e7a 100644 --- a/ui/src/components/header/index.tsx +++ b/ui/src/components/header/index.tsx @@ -43,9 +43,8 @@ const Header = () => { > 下载客户端 - + { - const [page, setPage] = useState(1); - const [size, setSize] = useState(20); - const [total, setTotal] = useState(0); - const [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState([]); - - const [filterUser, setFilterUser] = useState(''); - - - const { data: userOptions = { users: [] } } = useRequest(() => - getListUser({ - page: 1, - size: 9999, - }) - ); - - const fetchData = async (params: { - page?: number; - size?: number; - work_mode?: string; - author?: string; - }) => { - setLoading(true); - const res = await getSecurityScanningList({ - page: params.page || page, - size: params.size || size, - author: params.author || filterUser, - }); - setLoading(false); - setTotal(res.total_count || 0); - setDataSource(res.items || []); - }; - - useEffect(() => { - setPage(1); - fetchData({ - page: 1, - author: filterUser - }); - }, [filterUser]); - - const columns: ColumnsType = [ - { - dataIndex: 'name', - title: '扫描任务', - width: 180, - render: (project_name, record) => { - return ( - - - {record?.name} - - - {record?.status ? StatusText[record.status] : '未知状态'} - - - ); - }, - }, - { - title: '项目名称', - dataIndex: 'project_name', - render: (project_name, record) => { - return ( - - - {record?.project_name} - - - {record?.path} - - - ); - }, - }, - { - dataIndex: 'risk', - title: '扫描结果', - render(risk: DomainSecurityScanningRiskResult, record) { - const hasNoRisk = record.status !== 'pending' && - (!risk.severe_count || risk.severe_count <= 0) && - (!risk.critical_count || risk.critical_count <= 0) && - (!risk.suggest_count || risk.suggest_count <= 0); - - const tip = [] - if (risk.severe_count && risk.severe_count > 0) { - tip.push(`严重安全告警 ${risk.severe_count} 个`) - } - if (risk.critical_count && risk.critical_count > 0) { - tip.push(`高风险安全提醒 ${risk.critical_count} 个`) - } - if (risk.suggest_count && risk.suggest_count > 0) { - tip.push(`低风险安全提醒 ${risk.suggest_count} 个`) - } - - return ( - - - {hasNoRisk ? ( - // 如果没有风险,显示"无风险" - - 暂无风险 - - ) : ( - // 否则,显示原有的风险条 - <> - {!!risk.severe_count && risk.severe_count > 0 && {risk.severe_count}} - {!!risk.critical_count && risk.critical_count > 0 && {risk.critical_count}} - {!!risk.suggest_count && risk.suggest_count > 0 && {risk.suggest_count}} - - )} - - - ) - }, - }, - { - dataIndex: 'user', - title: '成员', - width: 200, - render(value: DomainUser) { - return ( - - ); - }, - }, - { - title: '扫描时间', - dataIndex: 'created_at', - width: 160, - render: (text) => { - return ( - - - {dayjs.unix(text).format('YYYY-MM-DD')} - - - {dayjs.unix(text).format('HH:mm:ss')} - - - ) - }, - }, - ]; - return ( - - - option.username || ''} - value={ - userOptions.users?.find((item) => item.username === filterUser) || - null - } - onChange={(_, newValue) => - setFilterUser(newValue ? newValue.username! : '') - } - isOptionEqualToValue={(option, value) => - option.username === value.username - } - renderInput={(params) => } - clearOnEscape - /> - -
{ - setPage(page); - setSize(size); - fetchData({ - page: page, - size: size, - }); - }, - }} - /> - - ); -}; - -export default Chat; diff --git a/ui/src/pages/codescan/index.tsx b/ui/src/pages/codescan/index.tsx new file mode 100644 index 0000000..1c1a578 --- /dev/null +++ b/ui/src/pages/codescan/index.tsx @@ -0,0 +1,19 @@ +import { useRequest } from 'ahooks'; +import { getListUser } from '@/api/User'; +import CodeScanTaskList from '../../components/codescan/taskList'; + +const AdminCodeScanTaskList = ( +) => { + const res = useRequest(() => { + return getListUser({ + page: 1, + size: 9999, + }) + }) + + return ( + + ); +}; + +export default AdminCodeScanTaskList; diff --git a/ui/src/pages/user/codescan/index.tsx b/ui/src/pages/user/codescan/index.tsx new file mode 100644 index 0000000..8cfec93 --- /dev/null +++ b/ui/src/pages/user/codescan/index.tsx @@ -0,0 +1,10 @@ +import CodeScanTaskList from '../../../components/codescan/taskList'; + +const AdminCodeScanTaskList = ( +) => { + return ( + + ); +}; + +export default AdminCodeScanTaskList; diff --git a/ui/src/router.tsx b/ui/src/router.tsx index 1988b1f..05d3aa9 100644 --- a/ui/src/router.tsx +++ b/ui/src/router.tsx @@ -40,7 +40,8 @@ const Login = LazyLoadable(lazy(() => import('@/pages/login'))); const UserLogin = LazyLoadable(lazy(() => import('@/pages/user/login'))); const Expectation = LazyLoadable(lazy(() => import('@/pages/expectation'))); const UserChat = LazyLoadable(lazy(() => import('@/pages/user/chat'))); -const CodeSecurity = LazyLoadable(lazy(() => import('@/pages/codeSecurity'))); +const AdminCodeScan = LazyLoadable(lazy(() => import('@/pages/codescan'))); +const UserCodeScan = LazyLoadable(lazy(() => import('@/pages/user/codescan'))); const UserCompletion = LazyLoadable( lazy(() => import('@/pages/user/completion')) ); @@ -68,8 +69,8 @@ const routerConfig = [ element: , }, { - path: 'code-security', - element: , + path: 'codescan', + element: , }, { path: 'completion', @@ -110,6 +111,10 @@ const routerConfig = [ path: 'completion', element: , }, + { + path: 'codescan', + element: , + }, { path: 'setting', element: ,